Mick's IT Blogs

Mick's IT Blogs

Latest Updates

21 June 2016

PowerShell: Automatically Populate the Microsoft Office Updates folder via SCCM

Posted By: Mick Pletcher - 1:41 PM
One of the things that has annoyed me in the past is having to keep the Microsoft Office Updates folder up-to-date with the latest .MSP files. The reason I bother with keeping the Updates folder up-to-date is because it speeds up the process of building a golden image, which in my environment, Office is installed during that phase. To automate this process, I created an automatic deployment rule in SCCM that keeps the Microsoft Office software update group up-to-date with the latest Office updates. To bridge those updates with the Updates folder under the Office installation directory, I wrote the script below with the help of Sapien's PowerShell Studio that made this a breeze to write and properly code. 

The script can be executed either by a scheduled task or using Microsoft Orchestrator, which is what I am using. I also set all of my parameter values within the script, but if you do not feel comfortable doing that, you can also specify the parameter values at the command line.

The script checks the folder where the automatic deployment rule in SCCM writes the new updates to. If there are new updates since the last time it was checked, it will copy them over and email the appropriate IT staff so they know when new updates are being applied in their environment. It also has an exclusion file that allows you to exclude updates if they interfere in your environment. It keeps track of what updates have been copied by writing the copied updates to a text file. The parameters should be the only thing you need to change to adapt the script to your environment. I have put a couple of examples inside the script as how to call the script from command line, especially if you want the logs emailed.

The script can be downloaded from here.

NOTE: Last December, I wrote a version of this script to automatically inject updates into the Microsoft Office folder. I found the updates were hit and miss as to what got installed. The problem came from the fact that when the .MSP files were extracted, they would be named the same as previous versions, therefor overwriting other updates. I then thought that I would need to rename those files to a unique name so they would all get applied. I continued to rewrite the script. In talking online about it, a user pointed me to the fact that he wrote a script that did the same thing. I liked his script, but I also wanted additional features, such as email and reporting. 

NOTE: Also, Microsoft uses the same KB number in the metadata field for each patch within a service pack. Because of that, make sure to extract a service pack to the updates folder manually and don't rename them. I would add the KB number for the office service pack to the exclusion list. Here is a screenshot of how it should look for Office 2010 with the SP2 applied to the updates folder.


Here is a demo of the script being executed manually:



1:  <#  
2:       .SYNOPSIS  
3:            Microsoft Office Updater  
4:         
5:       .DESCRIPTION  
6:            This script will keep the Updates folder populated with the latest Office  
7:            updates that SCCM has downloaded. It should be setup to execute as a  
8:            scheduled task on the SCCM server. I suggest executing it once a week.  
9:         
10:       .PARAMETER Country  
11:            The language-country of the updates  
12:         
13:       .PARAMETER EmailLogs  
14:            True or false on whether to email logs of the latest updates applied to the Office Updates folder.  
15:         
16:       .PARAMETER EmailRecipients  
17:            Email address to send report to  
18:         
19:       .PARAMETER EmailSender  
20:            Sender email address  
21:         
22:       .PARAMETER ExclusionFileName  
23:            Text file containing a list of updates to exclude  
24:         
25:       .PARAMETER LogFileName  
26:            Name of the log file to be written to  
27:         
28:       .PARAMETER ProcessedUpdatesFile  
29:            Name of the file containing a list of Microsoft Office updates that have already been copied over  
30:         
31:       .PARAMETER SMTPServer  
32:            fully qualified SMTP server name  
33:         
34:       .PARAMETER SourceFolder  
35:            The folder where SCCM stores the updates  
36:         
37:       .PARAMETER UpdatesFolder  
38:            The folder where Microsoft Office looks for the updates. This is typically <Office installer folder>\Updates.  
39:         
40:       .EXAMPLE  
41:            All parameters pre-populated  
42:            powershell.exe -executionpolicy bypass -file OfficeUpdater.ps1  
43:              
44:            All parameters pre-populated without email send (-command must be used when populating boolean values at the command line)  
45:            powershell.exe -executionpolicy bypass -command OfficeUpdater.ps1 -EmailLogs $false  
46:         
47:       .NOTES  
48:            ===========================================================================  
49:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
50:            Created on:       6/15/2016 10:29 AM  
51:            Created by:       Mick Pletcher  
52:            Organization:  
53:            Filename:         OfficeUpdater.ps1  
54:            ===========================================================================  
55:  #>  
56:  [CmdletBinding()]  
57:  param  
58:  (  
59:            [ValidateNotNullOrEmpty()][string]$Country,  
60:            [ValidateNotNullOrEmpty()][boolean]$EmailLogs,  
61:            [string]$EmailRecipients,  
62:            [string]$EmailSender,  
63:            [ValidateNotNullOrEmpty()][string]$ExclusionFileName,  
64:            [ValidateNotNullOrEmpty()][string]$LogFileName,  
65:            [ValidateNotNullOrEmpty()][string]$ProcessedUpdatesFile,  
66:            [string]$SMTPServer,  
67:            [ValidateNotNullOrEmpty()][string]$SourceFolder,  
68:            [ValidateNotNullOrEmpty()][string]$UpdatesFolder  
69:  )  
70:    
71:  function Copy-Updates {  
72:  <#  
73:       .SYNOPSIS  
74:            Copy the Office updates to the updates folder  
75:         
76:       .DESCRIPTION  
77:            This will extract the .msp file from the .cab and then copy it to the Microsoft Office installation updates folder.  
78:         
79:       .PARAMETER UnprocessedUpdates  
80:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
81:         
82:       .NOTES  
83:            Additional information about the function.  
84:  #>  
85:         
86:       [CmdletBinding()]  
87:       param  
88:       (  
89:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
90:       )  
91:         
92:       $RelativePath = Get-RelativePath  
93:       $LogFile = $RelativePath + $LogFileName  
94:       $ExclusionList = Get-ExclusionList  
95:       foreach ($Update in $UnprocessedUpdates) {  
96:            $ExtractedFolder = $SourceFolder + '\' + $Update.Name + '\extracted'  
97:            If ((Test-Path $ExtractedFolder) -eq $true) {  
98:                 $Files = Get-ChildItem -Path $ExtractedFolder  
99:                 foreach ($File in $Files) {  
100:                      if ($File.Extension -eq '.msp') {  
101:                           [string]$KBUpdate = Get-MSPFileInfo -Path $File.Fullname -Property 'KBArticle Number'  
102:                           $KBUpdate = 'KB' + $KBUpdate.Trim()  
103:                           $KBUpdateShortName = $KBUpdate + '.msp'  
104:                           $KBUpdateFullName = $File.DirectoryName + '\' + $KBUpdateShortName  
105:                           $DestinationFile = $UpdatesFolder + '\' + $KBUpdateShortName  
106:                           If ($KBUpdate -notin $ExclusionList) {  
107:                                Write-Host "Renaming"$File.Name"to"$KBUpdateShortName"....." -NoNewline  
108:                                $NoOutput = Copy-Item -Path $File.Fullname -Destination $KBUpdateFullName -Force  
109:                                If ((Test-Path $KBUpdateFullName) -eq $true) {  
110:                                     Write-Host "Success" -ForegroundColor Yellow  
111:                                } else {  
112:                                     Write-Host "Failed" -ForegroundColor Red  
113:                                }  
114:                                Write-Host "Copying"$KBUpdateShortName" to Office updates folder....." -NoNewline  
115:                                $NoOutput = Copy-Item -Path $KBUpdateFullName -Destination $UpdatesFolder -Force  
116:                                If ((Test-Path $DestinationFile) -eq $true) {  
117:                                     Write-Host "Success" -ForegroundColor Yellow  
118:                                     Add-Content -Path $LogFile -Value $KBUpdate -Force  
119:                                } else {  
120:                                     Write-Host "Failed" -ForegroundColor Red  
121:                                }  
122:                           } else {  
123:                                Write-Host $KBUpdate"....." -NoNewline  
124:                                Write-Host "Excluded" -ForegroundColor Green  
125:                           }  
126:                      }  
127:                 }  
128:            }  
129:            Start-Sleep -Seconds 1  
130:       }  
131:  }  
132:    
133:  function Expand-CABFiles {  
134:  <#  
135:       .SYNOPSIS  
136:            Extract the contents of the .CAB file  
137:         
138:       .DESCRIPTION  
139:            This will extract the contents of the CAB file to a temporary subfolder for all unprocessed updates. It filters the updates to expand by the country code specified in the Country parameter. It also expands all cab with none in the name.  
140:         
141:       .PARAMETER UnprocessedUpdates  
142:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
143:         
144:       .NOTES  
145:            Additional information about the function.  
146:  #>  
147:         
148:       [CmdletBinding()]  
149:       param  
150:       (  
151:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
152:       )  
153:         
154:       $Executable = $env:windir + "\System32\expand.exe"  
155:       $Country = '*' + $Country + '*'  
156:       foreach ($Update in $UnprocessedUpdates) {  
157:            $Folder = $SourceFolder + '\' + $Update.Name  
158:            $Files = Get-ChildItem -Path $Folder  
159:            foreach ($File in $Files) {  
160:                 If (($File.Name -like $Country) -or ($File.Name -like "*none*")) {  
161:                      $ExtractedDirectory = $File.DirectoryName + '\extracted'  
162:                      If ((Test-Path $ExtractedDirectory) -eq $true) {  
163:                           $NoOutput = Remove-Item $ExtractedDirectory -Recurse -Force  
164:                      }  
165:                      $NoOutput = New-Item $ExtractedDirectory -ItemType Directory  
166:                      Write-Host "Extracting"$File.Name"....." -NoNewline  
167:                      $Parameters = [char]34 + $File.FullName + [char]34 + [char]32 + [char]34 + $ExtractedDirectory + [char]34 + [char]32 + "-f:*"  
168:                      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -WindowStyle Minimized -Passthru).ExitCode  
169:                      If ($ErrCode -eq 0) {  
170:                           Write-Host "Success" -ForegroundColor Yellow  
171:                      } else {  
172:                           Write-Host "Failed" -ForegroundColor Red  
173:                      }  
174:                 }  
175:            }  
176:       }  
177:  }  
178:    
179:  function Get-ExclusionList {  
180:  <#  
181:       .SYNOPSIS  
182:            Office Updates Exclusion List  
183:         
184:       .DESCRIPTION  
185:            Contains a list of Microsoft Office updates to exclude from installation  
186:         
187:       .NOTES  
188:            Additional information about the function.  
189:  #>  
190:         
191:       [CmdletBinding()][OutputType([object])]  
192:       param ()  
193:         
194:       $RelativePath = Get-RelativePath  
195:       $ExclusionFile = $RelativePath + $ExclusionFileName  
196:       If ((Test-Path $ExclusionFile) -eq $true) {  
197:            $ExclusionList = Get-Content $ExclusionFile -Force  
198:       } else {  
199:            $NoOutput = New-Item -Path $ExclusionFile -Force  
200:            $ExclusionList = Get-Content $ExclusionFile -Force  
201:       }  
202:       Return $ExclusionList  
203:  }  
204:    
205:  function Get-ExtractedUpdatesList {  
206:  <#  
207:       .SYNOPSIS  
208:            List of updates already extracted  
209:         
210:       .DESCRIPTION  
211:            This function retrieves the list of updates that have already been extracted to the updates folder.  
212:         
213:       .NOTES  
214:            Additional information about the function.  
215:  #>  
216:         
217:       [CmdletBinding()]  
218:       param ()  
219:         
220:       $RelativePath = Get-RelativePath  
221:       $ExtractedUpdatesFile = $RelativePath + $ProcessedUpdatesFile  
222:       If ((Test-Path $ExtractedUpdatesFile) -eq $true) {  
223:            $File = Get-Content -Path $ExtractedUpdatesFile -Force  
224:            Return $File  
225:       } else {  
226:            $NoOutput = New-Item -Path $ExtractedUpdatesFile -ItemType File -Force  
227:            Return $null  
228:       }  
229:  }  
230:    
231:  function Get-MSPFileInfo {  
232:  <#  
233:       .SYNOPSIS  
234:            Extract MSP information  
235:         
236:       .DESCRIPTION  
237:            This function will extract MSP file information from the metadata table. It has been written to be able to read data from a lot of different MSP files, including Microsoft Office updates and most application patches. There are some MSP files that were not populated with the metadata table, therefor no data is obtainable.  
238:         
239:       .PARAMETER Path  
240:            Location of the MSP file  
241:         
242:       .PARAMETER Property  
243:            A pre-defined set of properties in the msi metadata table  
244:         
245:       .NOTES  
246:            Additional information about the function.  
247:  #>  
248:         
249:       param  
250:       (  
251:                 [Parameter(Mandatory = $true)][IO.FileInfo]$Path,  
252:                 [Parameter(Mandatory = $true)][ValidateSet('Classification', 'Description', 'DisplayName', 'KBArticle Number', 'ManufacturerName', 'ReleaseVersion', 'TargetProductName')][string]$Property  
253:       )  
254:         
255:       try {  
256:            $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
257:            $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($Path.FullName, 32))  
258:            $Query = "SELECT Value FROM MsiPatchMetadata WHERE Property = '$($Property)'"  
259:            $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))  
260:            $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)  
261:            $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)  
262:            $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
263:            return $Value  
264:       } catch {  
265:            Write-Output $_.Exception.Message  
266:       }  
267:  }  
268:    
269:  function Get-NewUpdatesList {  
270:  <#  
271:       .SYNOPSIS  
272:            List new updates to install  
273:         
274:       .DESCRIPTION  
275:            Get the list of updates to process  
276:         
277:       .NOTES  
278:            Additional information about the function.  
279:  #>  
280:         
281:       [CmdletBinding()][OutputType([object])]  
282:       param ()  
283:         
284:       $UnprocessedFolders = @()  
285:       $ExtractedUpdatesList = Get-ExtractedUpdatesList  
286:       $List = Get-ChildItem $SourceFolder  
287:       foreach ($Update in $List) {  
288:            If ($Update.Name -notin $ExtractedUpdatesList ) {  
289:                 $UnprocessedFolders = $UnprocessedFolders + $Update  
290:            }  
291:       }  
292:       Return $UnprocessedFolders  
293:  }  
294:    
295:  function Get-RelativePath {  
296:  <#  
297:       .SYNOPSIS  
298:            Get the relative path  
299:         
300:       .DESCRIPTION  
301:            Returns the location of the currently running PowerShell script  
302:         
303:       .NOTES  
304:            Additional information about the function.  
305:  #>  
306:         
307:       [CmdletBinding()][OutputType([string])]  
308:       param ()  
309:         
310:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
311:       Return $Path  
312:  }  
313:    
314:  function New-LogFile {  
315:  <#  
316:       .SYNOPSIS  
317:            Generate a log file  
318:         
319:       .DESCRIPTION  
320:            Generate a log file containing a list of the KB updates copied into the updates folder  
321:         
322:       .NOTES  
323:            Additional information about the function.  
324:  #>  
325:         
326:       [CmdletBinding()][OutputType([boolean])]  
327:       param ()  
328:         
329:       $RelativePath = Get-RelativePath  
330:       $LogFile = $RelativePath + $LogFileName  
331:       If ((Test-path $LogFile) -eq $true) {  
332:            Write-Host 'Deleting old log file.....' -NoNewline  
333:            $NoOutput = Remove-Item $LogFile -Force  
334:            If ((Test-path $LogFile) -eq $false) {  
335:                 Write-Host "Success" -ForegroundColor Yellow  
336:            } else {  
337:                 Write-Host "Failed" -ForegroundColor Red  
338:                 $Success = $false  
339:            }  
340:       }  
341:       If ((Test-path $LogFile) -eq $false) {  
342:            Write-Host "Creating new log file....." -NoNewline  
343:            $NoOutput = New-Item $LogFile -Force  
344:            If ((Test-path $LogFile) -eq $true) {  
345:                 Write-Host "Success" -ForegroundColor Yellow  
346:                 $Success = $true  
347:            } else {  
348:                 Write-Host "Failed" -ForegroundColor Red  
349:                 $Success = $false  
350:            }  
351:       }  
352:       Return $Success  
353:  }  
354:    
355:  function Remove-ExtractionFolders {  
356:  <#  
357:       .SYNOPSIS  
358:            Delete the extraction folders  
359:         
360:       .DESCRIPTION  
361:            Delete the extracted folders and contents of the unprocessed updates.  
362:         
363:       .PARAMETER UnprocessedUpdates  
364:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
365:         
366:       .NOTES  
367:            Additional information about the function.  
368:  #>  
369:         
370:       [CmdletBinding()]  
371:       param  
372:       (  
373:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
374:       )  
375:         
376:       foreach ($Update in $UnprocessedUpdates) {  
377:            $ExtractedFolder = $SourceFolder + '\' + $Update.Name + '\extracted'  
378:            $Deleted = $false  
379:            $Counter = 1  
380:            If ((Test-Path $ExtractedFolder) -eq $true) {  
381:                 Do {  
382:                      Try {  
383:                           Write-Host "Removing"$ExtractedFolder"....." -NoNewline  
384:                           $NoOutput = Remove-Item $ExtractedFolder -Recurse -Force -ErrorAction Stop  
385:                           If ((Test-Path $ExtractedFolder) -eq $false) {  
386:                                Write-Host "Success" -ForegroundColor Yellow  
387:                                $Deleted = $true  
388:                           } else {  
389:                                Write-Host "Failed" -ForegroundColor Red  
390:                                $Deleted = $false  
391:                           }  
392:                      } Catch {  
393:                           $Counter++  
394:                           Write-Host 'Failed. Retrying in 5 seconds' -ForegroundColor Red  
395:                           Start-Sleep -Seconds 5  
396:                           If ((Test-Path $ExtractedFolder) -eq $true) {  
397:                                $Deleted = $false  
398:                                Write-Host "Removing"$ExtractedFolder"....." -NoNewline  
399:                                $NoOutput = Remove-Item $ExtractedFolder -Recurse -Force -ErrorAction SilentlyContinue  
400:                                If ((Test-Path $ExtractedFolder) -eq $false) {  
401:                                     Write-Host "Success" -ForegroundColor Yellow  
402:                                     $Deleted = $true  
403:                                } else {  
404:                                     Write-Host "Failed" -ForegroundColor Red  
405:                                     $Deleted = $false  
406:                                }  
407:                           }  
408:                           If ($Counter = 5) {  
409:                                $Deleted = $true  
410:                           }  
411:                      }  
412:                 } while ($Deleted = $false)  
413:                 Start-Sleep -Seconds 1  
414:            }  
415:       }  
416:  }  
417:    
418:  function Send-UpdateReport {  
419:  <#  
420:       .SYNOPSIS  
421:            Send a report of new updates applied  
422:         
423:       .DESCRIPTION  
424:            Send the log file with a list of Office updates that were copied over to the Office updates folder.  
425:         
426:       .NOTES  
427:            Additional information about the function.  
428:  #>  
429:         
430:       [CmdletBinding()]  
431:       param ()  
432:         
433:       $RelativePath = Get-RelativePath  
434:       $LogFile = $RelativePath + $LogFileName  
435:       $Date = Get-Date -Format "dd-MMMM-yyyy"  
436:       $Subject = 'Microsoft Office Updates Report as of ' + $Date  
437:       $Body = 'List of Microsoft Office Updates added to the Office installation updates folder.'  
438:       $Count = 1  
439:       Do {  
440:            Try {  
441:                 Write-Host "Emailing report....." -NoNewline  
442:                 Send-MailMessage -To $EmailRecipients -From $EmailSender -Subject $Subject -Body $Body -Attachments $LogFile -SmtpServer $SMTPServer  
443:                 Write-Host "Success" -ForegroundColor Yellow  
444:                 $Exit = $true  
445:            } Catch {  
446:                 $Count++  
447:                 If ($Count -lt 4) {  
448:                      Write-Host 'Failed to send message. Retrying.....' -ForegroundColor Red  
449:                 } else {  
450:                      Write-Host 'Failed to send message' -ForegroundColor Red  
451:                      $Exit = $true  
452:                 }  
453:            }  
454:       } Until ($Exit = $true)  
455:         
456:  }  
457:    
458:  function Update-ProcessedUpdatesFile {  
459:  <#  
460:       .SYNOPSIS  
461:            Update the Processed Updates File with new updates  
462:         
463:       .DESCRIPTION  
464:            Add updates that were copied over to the processed updates file so they do not get processed again.  
465:         
466:       .PARAMETER UnprocessedUpdates  
467:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
468:         
469:       .NOTES  
470:            Additional information about the function.  
471:  #>  
472:         
473:       [CmdletBinding()]  
474:       param  
475:       (  
476:                 [ValidateNotNullOrEmpty()]$UnprocessedUpdates  
477:       )  
478:         
479:       $RelativePath = Get-RelativePath  
480:       $LogFile = $RelativePath + $ProcessedUpdatesFile  
481:       foreach ($Update in $UnprocessedUpdates) {  
482:            $Success = $false  
483:            Do {  
484:                 Try {  
485:                      Write-Host 'Adding'$Update.Name'to Processed updates.....' -NoNewline  
486:                      Add-Content -Path $LogFile -Value $Update.Name -Force -ErrorAction Stop  
487:                      $Success = $true  
488:                      Write-Host "Success" -ForegroundColor Yellow  
489:                 } Catch {  
490:                      Write-Host "Failed" -ForegroundColor Red  
491:                      $Success = $false  
492:                 }  
493:            } while ($Success -eq $false)  
494:       }  
495:  }  
496:    
497:  Clear-Host  
498:  $UnprocessedFolders = Get-NewUpdatesList  
499:  If ($UnprocessedFolders -ne $null) {  
500:       $NewLog = New-LogFile  
501:       If ($NewLog -eq $true) {  
502:            Expand-CABFiles -UnprocessedUpdates $UnprocessedFolders  
503:            Copy-Updates -UnprocessedUpdates $UnprocessedFolders  
504:            Remove-ExtractionFolders -UnprocessedUpdates $UnprocessedFolders  
505:            Update-ProcessedUpdatesFile -UnprocessedUpdates $UnprocessedFolders  
506:            If ($EmailLogs -eq $true) {  
507:                 Send-UpdateReport  
508:            }  
509:       } else {  
510:            Write-Host "Failed to create log file."  
511:       }  
512:  } else {  
513:       Write-Host "No updates to process"  
514:  }  
515:    

16 June 2016

PowerShell: Pulling information from MSP files

Posted By: Mick Pletcher - 11:02 AM
Recently, I began making significant updates to one of my other scripts on here. In the process of asking questions about MSP files on Facebook PowerShell groups, it was clear to me that it would only take a few tweaks to make scripts that extract MSI data also be able to extract MSP data.

I first started out by pulling the code from stack overflow on how to extract data from an MSI. Obviously, that code did not work with an MSP. It was putting up the exception code message. When I debugged it, the script was catching on line 28. Since an MSP is just a patch, and it is still a database, I figured that same code could be adapted to read the data from the MSP. The key information that helped me what from Microsoft's Installer.OpenDatabase method webpage. The parameter values and what each value does was the key to figuring this out. The parameter value of 32 indicates the database is associated with an MSP file. As you can see at the end of line 28, there is a value of 32. That was originally a 0, which associated this script with an MSI, instead of an MSP. I suggest going to the Microsoft link above and reviewing the difference parameter values that can be used.

The script below was written for obtaining information from an MSP pertaining to Microsoft Office updates. From what I have seen in several MSPs I have looked at, they all differ in what tables are available. The table you want to look at for pulling data is called MsiPatchMetadata. There are some that do not have the MsiPatchMetadata table. This is defined on line 29. Property is what contains the list of available data fields. Below is a screenshot of Adobe Reader XI update MSP file viewed through ORCA. As you can see, there are two tables and several Property fields available.


In the script below, I have included the standard fields I have seen in most MSP files. Those fields are: Classification, Description, DisplayName, KBArticle Number, ManufacturerName, ReleaseVersion, and TargetProductName. Thanks to Sapien's PowerShell Studio, the script was a snap to write! I reviewed MSP files from many different applications my firm uses, and these were the most common, which also include Microsoft Office MSP updates. You may run into additional fields also which will require you to add them to the parameter validation set of the property variable.

I did not use any main parameters because I figure this function will likely be copied and used inside other scripts.

You can download the script from here:


1:  <#  
2:       .SYNOPSIS  
3:            Extract MSP information  
4:         
5:       .DESCRIPTION  
6:            This script will extract MSP file information from the metadata table. It has been written to be able to read data from a lot of different MSP files, including Microsoft Office updates and most application patches. There are some MSP files that were not populated with the metadata table, therefor no data is obtainable.   
7:         
8:       .NOTES  
9:            ===========================================================================  
10:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
11:            Created on:       6/15/2016 4:07 PM  
12:            Created by:       Mick Pletcher  
13:            Organization:  
14:            Filename:         MSPInfo.ps1
15:            ===========================================================================  
16:  #>  
17:    
18:    
19:  function Get-MSPFileInfo {  
20:       param  
21:       (  
22:                 [Parameter(Mandatory = $true)][IO.FileInfo]$Path,  
23:                 [Parameter(Mandatory = $true)][ValidateSet('Classification', 'Description', 'DisplayName', 'KBArticle Number', 'ManufacturerName', 'ReleaseVersion', 'TargetProductName')][string]$Property  
24:       )  
25:         
26:       try {  
27:            $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
28:            $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($Path.FullName, 32))  
29:            $Query = "SELECT Value FROM MsiPatchMetadata WHERE Property = '$($Property)'"  
30:            $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))  
31:            $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)  
32:            $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)  
33:            $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
34:            return $Value  
35:       } catch {  
36:            Write-Output $_.Exception.Message  
37:       }  
38:  }  
39:    
40:  Get-MSPFileInfo -Path "mstore-x-none.msp" -Property 'KBArticle Number'  
41:    

06 June 2016

PowerShell: Microsoft Office and Windows MAK Activation Script

Posted By: Mick Pletcher - 1:59 PM
Here is a script that will activate both Microsoft Office and Microsoft Windows. I wrote this script because we currently have both x86 and x64 systems, therefore the OSPP.vbs resides in different locations. I also chose to write this script so that I did not have to enter the product keys and activation in the image. If an imaging process fails, I don't want a MAK activation wasted. This script will allow you to enter a product key and/or activate the product. I setup the script so the product keys can be entered at the command line prompt to protect the exposure of the key when used in MDT or SCCM. If you wanted to hard code the keys, that can be done at the parameter fields. I have used the script on Windows 7, Windows 10, Office 2010, and Office 2016. I also wrote this script so that if any of the functions fail, it will return an error code 1 so the build will report an error in this task sequence.

I used Sapien's PowerShell Studio to write this script and I can attest that it made writing it a breeze! Here are some examples below of running the script at the command prompt:

  • Input the product keys and activate both office and windows
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice $true -ActivateWindows $true
  • Input the windows product key and activate windows
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActivateWindows $true
  • Input the windows product key without activation
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
  • Only activate windows 
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActivateWindows $true
  • Input the office product key and activate office
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice $true
  • Input the office product key without activation
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
  • Only activate office
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActiveOffice $true


NOTES: You will need to use -command instead of -file at the command prompt due to the script using boolean parameters.

Second, I advise putting the script near the end of a task sequence.

You can download the script from here.


1:  <#  
2:       .SYNOPSIS  
3:            Online Activation for Microsoft Office  
4:         
5:       .DESCRIPTION  
6:            This script will perform an online activation of Microsoft Office  
7:         
8:       .PARAMETER ConsoleTitle  
9:            Set the title of the console  
10:         
11:       .PARAMETER OfficeProductKey  
12:            Microsoft Office Product Key  
13:         
14:       .PARAMETER WindowsProductKey  
15:            Microsoft Windows Product Key  
16:         
17:       .PARAMETER ActivateOffice  
18:            True or False on activating Microsoft Office  
19:         
20:       .PARAMETER ActivateWindows  
21:            True or False on activating Microsoft Windows  
22:    
23:       .EXAMPLE  
24:      You must use -command instead of -file due to boolean value parameters.  
25:    
26:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice $true -ActivateWindows $true  
27:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActivateWindows $true  
28:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"  
29:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActivateWindows $true  
30:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice $true  
31:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"  
32:            powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActiveOffice $true  
33:         
34:       .NOTES  
35:            ===========================================================================  
36:            Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
37:            Created on:       6/3/2016 4:00 PM  
38:            Created by:          Mick Pletcher  
39:            Organization:  
40:            Filename:        ActivateOfficeWindows.ps1  
41:            ===========================================================================  
42:  #>  
43:  [CmdletBinding()]  
44:  param  
45:  (  
46:       [Parameter(Mandatory = $false)][string]  
47:       $ConsoleTitle = 'Microsoft Office and Windows Activation',  
48:       [Parameter(Mandatory = $false)][string]  
49:       $OfficeProductKey,  
50:       [Parameter(Mandatory = $false)][string]  
51:       $WindowsProductKey,  
52:       [ValidateSet($true, $false)][boolean]  
53:       $ActivateOffice,  
54:       [ValidateSet($true, $false)][boolean]  
55:       $ActivateWindows  
56:  )  
57:    
58:  function Get-OfficeSoftwareProtectionPlatform {  
59:  <#  
60:       .SYNOPSIS  
61:            Find the Office Software Protection Platform  
62:         
63:       .DESCRIPTION  
64:            Find the OSPP.vbs script to apply the office product key and invoke online activation.  
65:  #>  
66:         
67:       [CmdletBinding()][OutputType([string])]  
68:       param ()  
69:         
70:       $File = Get-ChildItem $env:ProgramFiles"\Microsoft Office" -Recurse | Where-Object { $_.Name -eq "OSPP.VBS" }  
71:       If (($File -eq $null) -or ($File -eq '')) {  
72:            $File = Get-ChildItem ${env:ProgramFiles(x86)}"\Microsoft Office" -Recurse | Where-Object { $_.Name -eq "OSPP.VBS" }  
73:       }  
74:       $File = $File.FullName  
75:       Return $File  
76:  }  
77:    
78:  function Get-SoftwareLicenseManager {  
79:  <#  
80:       .SYNOPSIS  
81:            Find the Software License Manager script  
82:         
83:       .DESCRIPTION  
84:            Find the slmgr.vbs script to activate Microsoft Windows  
85:  #>  
86:         
87:       [CmdletBinding()][OutputType([string])]  
88:       param ()  
89:         
90:       $File = Get-ChildItem $env:windir"\system32" | Where-Object { $_.Name -eq "slmgr.vbs" }  
91:       $File = $File.FullName  
92:       Return $File  
93:  }  
94:    
95:  function Invoke-OfficeActivation {  
96:  <#  
97:       .SYNOPSIS  
98:            Activate Microsoft Office  
99:         
100:       .DESCRIPTION  
101:            Trigger the online Microsoft Office activation  
102:         
103:       .PARAMETER OSPP  
104:            A description of the OSPP parameter.  
105:         
106:       .NOTES  
107:            Additional information about the function.  
108:  #>  
109:         
110:       [CmdletBinding()][OutputType([boolean])]  
111:       param  
112:       (  
113:            [ValidateNotNullOrEmpty()][string]  
114:            $OSPP  
115:       )  
116:         
117:       $Errors = $false  
118:       Write-Host "Activate Microsoft Office....." -NoNewline  
119:       $Executable = $env:windir + "\System32\cscript.exe"  
120:       $Switches = [char]34 + $OSPP + [char]34 + [char]32 + "/act"  
121:       If ((Test-Path $Executable) -eq $true) {  
122:            $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
123:       }  
124:       If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
125:            Write-Host "Success" -ForegroundColor Yellow  
126:       } else {  
127:            Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
128:            $Errors = $true  
129:       }  
130:       Return $Errors  
131:  }  
132:    
133:  function Invoke-WindowsActivation {  
134:  <#  
135:       .SYNOPSIS  
136:            Activate Microsoft Windows  
137:         
138:       .DESCRIPTION  
139:            Trigger the online Microsoft Windows Activation  
140:         
141:       .PARAMETER SLMGR  
142:            A description of the SLMGR parameter.  
143:  #>  
144:         
145:       [CmdletBinding()]  
146:       param  
147:       (  
148:            [ValidateNotNullOrEmpty()][string]  
149:            $SLMGR  
150:       )  
151:         
152:       $Errors = $false  
153:       Write-Host "Activate Microsoft Windows....." -NoNewline  
154:       $Executable = $env:windir + "\System32\cscript.exe"  
155:       $Switches = [char]34 + $SLMGR + [char]34 + [char]32 + "-ato"  
156:       If ((Test-Path $Executable) -eq $true) {  
157:            $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
158:       }  
159:       If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
160:            Write-Host "Success" -ForegroundColor Yellow  
161:       } else {  
162:            Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
163:            $Errors = $true  
164:       }  
165:       Return $Errors  
166:  }  
167:    
168:  function Set-ConsoleTitle {  
169:  <#  
170:       .SYNOPSIS  
171:            Console Title  
172:         
173:       .DESCRIPTION  
174:            Sets the title of the PowerShell Console  
175:         
176:       .PARAMETER ConsoleTitle  
177:            Title of the PowerShell Console  
178:  #>  
179:         
180:       [CmdletBinding()]  
181:       param  
182:       (  
183:                 [Parameter(Mandatory = $true)][String]$ConsoleTitle  
184:       )  
185:         
186:       $host.ui.RawUI.WindowTitle = $ConsoleTitle  
187:  }  
188:    
189:  function Set-OfficeProductKey {  
190:  <#  
191:       .SYNOPSIS  
192:            Set the Office Product Key  
193:         
194:       .DESCRIPTION  
195:            This will install the office product key  
196:         
197:       .PARAMETER OSPP  
198:            Office Software Protection Platform script  
199:         
200:       .NOTES  
201:            Additional information about the function.  
202:  #>  
203:         
204:       [CmdletBinding()][OutputType([boolean])]  
205:       param  
206:       (  
207:            [ValidateNotNullOrEmpty()][string]  
208:            $OSPP  
209:       )  
210:         
211:       $Errors = $false  
212:       Write-Host "Set Microsoft Office Product Key....." -NoNewline  
213:       $Executable = $env:windir + "\System32\cscript.exe"  
214:       $Switches = [char]34 + $OSPP + [char]34 + [char]32 + "/inpkey:" + $OfficeProductKey  
215:       If ((Test-Path $Executable) -eq $true) {  
216:            $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
217:       }  
218:       If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
219:            Write-Host "Success" -ForegroundColor Yellow  
220:       } else {  
221:            Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
222:            $Errors = $true  
223:       }  
224:       Return $Errors  
225:  }  
226:    
227:  function Set-WindowsProductKey {  
228:  <#  
229:       .SYNOPSIS  
230:            Set the Microsoft Windows Product Key  
231:         
232:       .DESCRIPTION  
233:            This will set or change the Microsoft Windows Product Key  
234:         
235:       .PARAMETER SLMGR  
236:            A description of the SLMGR parameter.  
237:         
238:       .NOTES  
239:            Additional information about the function.  
240:  #>  
241:         
242:       [CmdletBinding()][OutputType([boolean])]  
243:       param  
244:       (  
245:            [ValidateNotNullOrEmpty()][string]  
246:            $SLMGR  
247:       )  
248:         
249:       $Errors = $false  
250:       Write-Host "Set Microsoft Windows Product Key....." -NoNewline  
251:       $Executable = $env:windir + "\System32\cscript.exe"  
252:       $Switches = [char]34 + $SLMGR + [char]34 + [char]32 + "/ipk" + [char]32 + $WindowsProductKey  
253:       If ((Test-Path $Executable) -eq $true) {  
254:            $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
255:       }  
256:       If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
257:            Write-Host "Success" -ForegroundColor Yellow  
258:       } else {  
259:            Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
260:            $Errors = $true  
261:       }  
262:       Return $Errors  
263:  }  
264:    
265:  cls  
266:  $ErrorReport = $false  
267:  Set-ConsoleTitle -ConsoleTitle $ConsoleTitle  
268:  $OSPP = Get-OfficeSoftwareProtectionPlatform  
269:  If (($OfficeProductKey -ne $null) -and ($OfficeProductKey -ne '')) {  
270:       If (($OSPP -ne $null) -and ($OSPP -ne '')) {  
271:            $Errors = Set-OfficeProductKey -OSPP $OSPP  
272:            If ($ErrorReport -eq $false) {  
273:                 $ErrorReport = $Errors  
274:            }  
275:       } else {  
276:            Write-Host "Office Software Protection Platform not found to set the Microsoft Office Product Key" -ForegroundColor Red  
277:       }  
278:  }  
279:  If (($ActivateOffice -ne $null) -and ($ActivateOffice -ne '')) {  
280:       If (($OSPP -ne $null) -and ($OSPP -ne '')) {  
281:            $Errors = Invoke-OfficeActivation -OSPP $OSPP  
282:            If ($ErrorReport -eq $false) {  
283:                 $ErrorReport = $Errors  
284:            }  
285:       } else {  
286:            Write-Host "Office Software Protection Platform not found to activate Microsoft Office" -ForegroundColor Red  
287:       }  
288:  }  
289:  $SLMGR = Get-SoftwareLicenseManager  
290:  If (($WindowsProductKey -ne $null) -and ($WindowsProductKey -ne '')) {  
291:       If (($SLMGR -ne $null) -and ($SLMGR -ne '')) {  
292:            $Errors = Set-WindowsProductKey -SLMGR $SLMGR  
293:            If ($ErrorReport -eq $false) {  
294:                 $ErrorReport = $Errors  
295:            }  
296:       } else {  
297:            Write-Host "Software licensing management tool not found to set the Microsoft Windows Product Key" -ForegroundColor Red  
298:       }  
299:  }  
300:  If (($ActivateWindows -ne $null) -and ($ActivateWindows -ne '')) {  
301:       If (($SLMGR -ne $null) -and ($SLMGR -ne '')) {  
302:            $Errors = Invoke-WindowsActivation -SLMGR $SLMGR  
303:            If ($ErrorReport -eq $false) {  
304:                 $ErrorReport = $Errors  
305:            }  
306:       } else {  
307:            Write-Host "Software licensing management tool not found to activate Microsoft Windows" -ForegroundColor Red  
308:       }  
309:  }  
310:  If ($ErrorReport -eq $true) {  
311:       Exit 1  
312:  }  

02 June 2016

PowerShell: Uninstall Windows 10 Built-In Applications with Verification

Posted By: Mick Pletcher - 12:55 PM














I am in the beginning stages of building a Windows 10 image for the firm I work at. One of the things that needs to be configured is the built-in apps. Obviously, we do not want all of them. You maybe wondering why you would use this over GPO or DSC. Those are fine for a blanket setting for all machines, but some apps maybe removed, but then allowed to be reinstalled on demand, which this script would be needed for.

With the help of Sapien's PowerShell Studio in building this script, I decided to make it multi-functional. It can generate a list of all built-in apps and display the formatted names to a screen. It can also output the list to a text file with the actual names of the apps needed to uninstall them. The script can uninstall a single app via command line parameters. It can also uninstall multiple app via a text file containing the application names. You can also hard code the specific uninstalls at the bottom of the script. Here are the five examples:


  • Generate a formatted list of all installed built-in apps
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true
  • Generate a list of all installed built-in apps and write the output to a log file
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true -Log $true
  • Uninstall a single built-in app by specifying its name at the command prompt. You do need to use the official name. You can get that by generating a formatted list.
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppName "Microsoft.WindowsCamera"
  • Uninstall multiple built-in apps from a list inside a text file
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppsFile "AppsUninstall.txt
  • Harcode the uninstall at the bottom of this script
    • Uninstall-BuiltInApp -AppName "Microsoft.WindowsCamera"

I included in the script the ability for it to verify the application was actually installed. While running, it will give an output to the screen with the status of each app it is uninstalling. I have only executed this on Windows 10, so I am not sure if it will work on Windows 8 and Windows 8.1, as we skipped those operating systems.

You can download the script from here.


1:  <#  
2:       .SYNOPSIS  
3:            Uninstall Build-In Apps  
4:         
5:       .DESCRIPTION  
6:            This script will uninstall built-in apps in Windows 10. The script can uninstall a single app by defining it at the command line. A list of apps can be read in from a text file and iterated through for uninstallation. Finally, they can also be hardcoded into the script.  
7:         
8:       .PARAMETER AppsFile  
9:            Text file to be read in by the script which contains a list of apps to uninstall.  
10:         
11:       .PARAMETER AppName  
12:            Name of the app to uninstall. This is defined when there is only one app to uninstall.  
13:         
14:       .PARAMETER GetAppList  
15:            True or false on generating a list of Built-in apps  
16:         
17:       .PARAMETER Log  
18:            Specify true or false on whether to generate a log file in the same directory as the script containing a list of all the built-in apps by their official name  
19:         
20:       .EXAMPLE  
21:            Generate a formatted list of all installed built-in apps  
22:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true  
23:    
24:            Generate a list of all installed built-in apps and write the output to a log file  
25:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true -Log $true  
26:    
27:            Uninstall a single built-in app by specifying its name at the command prompt. You do need to use the official name. You can get that by generating a formatted list.  
28:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppName "Microsoft.WindowsCamera"  
29:              
30:            Uninstall multiple built-in apps from a list inside a text file  
31:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppsFile "AppsUninstall.txt  
32:    
33:            Harcode the uninstall at the bottom of this script  
34:                 Uninstall-BuiltInApp -AppName "Microsoft.WindowsCamera"  
35:    
36:       .NOTES  
37:            ===========================================================================  
38:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
39:            Created on:       6/1/2016 3:21 PM  
40:            Created by:       Mick Pletcher  
41:            Organization:  
42:            Filename:         UninstallBuilt-InApps.ps1  
43:            ===========================================================================  
44:  #>  
45:  [CmdletBinding()]  
46:  param  
47:  (  
48:       [string]  
49:       $AppsFile = $null,  
50:       [string]  
51:       $AppName = $null,  
52:       [ValidateNotNullOrEmpty()][boolean]  
53:       $GetAppList = $false,  
54:       [ValidateNotNullOrEmpty()][boolean]  
55:       $Log = $false  
56:  )  
57:  Import-Module Appx  
58:    
59:  function Get-AppName {  
60:  <#  
61:       .SYNOPSIS  
62:            Format App name  
63:         
64:       .DESCRIPTION  
65:            This will format a built-in app name for proper display  
66:         
67:       .PARAMETER Name  
68:            Name of the application  
69:         
70:       .EXAMPLE  
71:                      PS C:\> Get-AppName -Name 'Value1'  
72:         
73:       .NOTES  
74:            Additional information about the function.  
75:  #>  
76:         
77:       [CmdletBinding()]  
78:       param  
79:       (  
80:            [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]  
81:            $Name  
82:       )  
83:         
84:       $Temp = $Name.Split('.')  
85:       For ($j = 0; $j -lt $Temp.Count; $j++) {  
86:            $Numeric = [bool]($Temp[$j] -as [double])  
87:            If ($Temp[$j] -eq 'Net') {  
88:                 $Temp[$j] = "." + $Temp[$j]  
89:            }  
90:            If ($Numeric -eq $true) {  
91:                 If ($Temp[$j + 1] -ne $null) {  
92:                      $Temp[$j] = $Temp[$j] + '.'  
93:                 }  
94:                 $FormattedName = $FormattedName + $Temp[$j]  
95:            } else {  
96:                 $FormattedName = $FormattedName + $Temp[$j] + [char]32  
97:            }  
98:       }  
99:       Return $FormattedName  
100:  }  
101:    
102:    
103:  function Get-BuiltInAppsList {  
104:  <#  
105:       .SYNOPSIS  
106:            List all Built-In Apps  
107:         
108:       .DESCRIPTION  
109:            Query for a list of all Build-In Apps  
110:         
111:       .EXAMPLE  
112:            PS C:\> Get-BuiltInAppsList  
113:         
114:       .NOTES  
115:            Additional information about the function.  
116:  #>  
117:         
118:       [CmdletBinding()]  
119:       param ()  
120:         
121:       $Apps = Get-AppxPackage  
122:       $Apps = $Apps.Name  
123:       $Apps = $Apps | Sort-Object  
124:       If ($Log -eq $true) {  
125:            $RelativePath = Get-RelativePath  
126:            $Apps | Out-File -FilePath $RelativePath"AllAppslist.txt" -Encoding UTF8  
127:       }  
128:       For ($i = 0; $i -lt $Apps.count; $i++) {  
129:            $Temp = Get-AppName -Name $Apps[$i]  
130:            $Apps[$i] = $Temp  
131:       }  
132:       $Apps  
133:  }  
134:    
135:  function Get-RelativePath {  
136:  <#  
137:       .SYNOPSIS  
138:            Get the relative path of the PowerShell script  
139:         
140:       .DESCRIPTION  
141:            Returns the path location of the PowerShell script being executed  
142:         
143:       .EXAMPLE  
144:            PS C:\> Get-RelativePath  
145:         
146:       .NOTES  
147:            Additional information about the function.  
148:  #>  
149:         
150:       [CmdletBinding()][OutputType([string])]  
151:       param ()  
152:         
153:       $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
154:       Return $RelativePath  
155:  }  
156:    
157:  function Uninstall-BuiltInApp {  
158:  <#  
159:       .SYNOPSIS  
160:            Uninstall Windows 10 Built In App  
161:         
162:       .DESCRIPTION  
163:            This will uninstall a built-in app by passing the name of the app in.  
164:         
165:       .PARAMETER AppName  
166:            Name of the App  
167:         
168:       .EXAMPLE  
169:            PS C:\> Uninstall-BuiltInApp -AppName 'Value1'  
170:         
171:       .NOTES  
172:            Additional information about the function.  
173:  #>  
174:         
175:       [CmdletBinding()]  
176:       param  
177:       (  
178:            [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]  
179:            $AppName  
180:       )  
181:         
182:       $App = Get-AppName -Name $AppName  
183:       Write-Host "Uninstalling"$App"....." -NoNewline  
184:       $Output = Get-AppxPackage $AppName  
185:       If ($Output -eq $null) {  
186:            Write-Host "Not Installed" -ForegroundColor Yellow  
187:       } else {  
188:            $Output = Get-AppxPackage $AppName | Remove-AppxPackage  
189:            $Output = Get-AppxPackage $AppName  
190:            If ($Output -eq $null) {  
191:                 Write-Host "Success" -ForegroundColor Yellow  
192:            } else {  
193:                 Write-Host "Failed" -ForegroundColor Red  
194:            }  
195:       }  
196:  }  
197:    
198:  function Uninstall-BuiltInApps {  
199:  <#  
200:       .SYNOPSIS  
201:            Uninstall Windows 10 Built In Apps  
202:         
203:       .DESCRIPTION  
204:            This will uninstall a list of built-in apps by reading the app names from a text file located in the same directory as this script.  
205:         
206:       .NOTES  
207:            Additional information about the function.  
208:  #>  
209:         
210:       [CmdletBinding()]  
211:       param ()  
212:         
213:       $RelativePath = Get-RelativePath  
214:       $AppsFile = $RelativePath + $AppsFile  
215:       $List = Get-Content -Path $AppsFile  
216:       foreach ($App in $List) {  
217:            Uninstall-BuiltInApp -AppName $App  
218:       }  
219:  }  
220:    
221:  cls  
222:  #Generate list of all Build-In apps  
223:  If ($GetAppList -eq $true) {  
224:       Get-BuiltInAppsList  
225:  }  
226:  #Uninstall a single app  
227:  If (($AppName -ne $null) -and ($AppName -ne "")) {  
228:       Uninstall-BuiltInApp -AppName $AppName  
229:  }  
230:  #Read list of apps to uninstall from text file and uninstall all on the list  
231:  If (($GetAppList -ne $null) -and ($GetAppList -ne "")) {  
232:       Uninstall-BuiltInApps  
233:  }  
234:    

31 May 2016

PowerShell: Set Windows Features with Verification

Posted By: Mick Pletcher - 2:09 PM














I am in the beginning stages of creating a Windows 10 build. One of the first things I needed to do was to install and set the Windows 10 features. Before, I used a batch script that executed DISM to set each feature. I know there is the Install-WindowsFeatures cmdlet, but I also wanted to incorporate verification and other features into a single script. With the help of Sapien's PowerShell Studio, this script was a breeze to write.

This script allows you to set windows features while also verifying each feature was set correctly by querying the feature for the status. It then outputs the feature name and status to the display. I have also included the option to run a report of all available features and their state. Here are the four features the script provides:

  1. Set an individual feature via command line:
    powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -Feature 'RSATClient-Features' -Setting 'disable'
  2. Set multiple features by reading a text file located in the same directory as the script. You can name the text file any name you want. The format for the file is: RSATClient,enable for example. Here is the command line:
    powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -FeaturesFile 'FeaturesList.txt'

  3. Hard code a feature setting at the bottom of the script:
    Set-WindowsFeature -Name 'RSATClient-Features' -State 'disable'
  4. Display a list of windows features:
    powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -ListFeatures $true


You will need to use the -command when executing this at the command line instead of -file. This is because the -ListFeatures is a boolean value. I have also included code that identifies an error 50 and returns a status that you must include the parent feature before activating the specified feature. I have also made the additional command line window be minimized when running the DISM.exe. 

You can download the script from here.
1:  <#  
2:       .SYNOPSIS  
3:            Process Windows Features  
4:         
5:       .DESCRIPTION  
6:            This script can return a list of online windows features and/or set specific windows features.  
7:         
8:       .PARAMETER ListFeatures  
9:            Return a list of all Windows Features  
10:         
11:       .PARAMETER Feature  
12:            A description of the Feature parameter.  
13:         
14:       .PARAMETER Setting  
15:            A description of the Setting parameter.  
16:         
17:       .PARAMETER FeaturesFile  
18:            Name of the features file that contains a list of features with their corresponding settings for this script to process through. The files resides in the same directory as this script.  
19:         
20:       .EXAMPLE  
21:            Return a list of all available online Windows Features  
22:            powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -ListFeatures $true  
23:              
24:            Set one Windows Feature from the command line  
25:            powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -Feature 'RSATClient-Features' -Setting 'disable'  
26:              
27:            Set multiple features by reading contents of a text file  
28:            powershell.exe -executionpolicy bypass -command WindowsFeatures.ps1 -FeaturesFile 'FeaturesList.txt'  
29:         
30:       .NOTES  
31:            You must use -command instead of -file in the command line because of the use of boolean parameters  
32:    
33:            An error code 50 means you are trying to enable a feature in which the required parent feature is disabled  
34:              
35:            I have also included two commented out lines at the bottom of the script as examples if you want to hardcode the features within the script.  
36:            ===========================================================================  
37:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
38:            Created on:       5/27/2016 2:46 PM  
39:            Created by:       Mick Pletcher  
40:            Organization:  
41:            Filename:         WindowsFeatures.ps1  
42:            ===========================================================================  
43:  #>  
44:  [CmdletBinding()]  
45:  param  
46:  (  
47:            [boolean]$ListFeatures = $false,  
48:            [string]$Feature,  
49:            [ValidateSet('enable', 'disable')][string]$Setting,  
50:            [String]$FeaturesFile  
51:  )  
52:    
53:  function Confirm-Feature {  
54:  <#  
55:       .SYNOPSIS  
56:            Confirm the feature setting  
57:         
58:       .DESCRIPTION  
59:            Confirm the desired change took place for a feature  
60:         
61:       .PARAMETER FeatureName  
62:            Name of the feature  
63:         
64:       .PARAMETER FeatureState  
65:            Desired state of the feature  
66:         
67:       .EXAMPLE  
68:            PS C:\> Confirm-Feature  
69:         
70:       .NOTES  
71:            Additional information about the function.  
72:  #>  
73:         
74:       [CmdletBinding()][OutputType([boolean])]  
75:       param  
76:       (  
77:                 [ValidateNotNull()][string]$FeatureName,  
78:                 [ValidateSet('Enable', 'Disable')][string]$FeatureState  
79:       )  
80:         
81:       $WindowsFeatures = Get-WindowsFeaturesList  
82:       $WindowsFeature = $WindowsFeatures | Where-Object { $_.Name -eq $FeatureName }  
83:       switch ($FeatureState) {  
84:            'Enable' {  
85:                 If (($WindowsFeature.State -eq 'Enabled') -or ($WindowsFeature.State -eq 'Enable Pending')) {  
86:                      Return $true  
87:                 } else {  
88:                      Return $false  
89:                 }  
90:            }  
91:            'Disable' {  
92:                 If (($WindowsFeature.State -eq 'Disabled') -or ($WindowsFeature.State -eq 'Disable Pending')) {  
93:                      Return $true  
94:                 } else {  
95:                      Return $false  
96:                 }  
97:            }  
98:            default {  
99:                 Return $false  
100:            }  
101:       }  
102:         
103:  }  
104:    
105:  function Get-WindowsFeaturesList {  
106:  <#  
107:       .SYNOPSIS  
108:            List Windows Features  
109:         
110:       .DESCRIPTION  
111:            This will list all available online windows features  
112:         
113:       .NOTES  
114:            Additional information about the function.  
115:  #>  
116:         
117:       [CmdletBinding()]  
118:       param ()  
119:         
120:       $Temp = dism /online /get-features  
121:       $Temp = $Temp | Where-Object { ($_ -like '*Feature Name*') -or ($_ -like '*State*') }  
122:       $i = 0  
123:       $Features = @()  
124:       Do {  
125:            $FeatureName = $Temp[$i]  
126:            $FeatureName = $FeatureName.Split(':')  
127:            $FeatureName = $FeatureName[1].Trim()  
128:            $i++  
129:            $FeatureState = $Temp[$i]  
130:            $FeatureState = $FeatureState.Split(':')  
131:            $FeatureState = $FeatureState[1].Trim()  
132:            $Feature = New-Object PSObject  
133:            $Feature | Add-Member noteproperty Name $FeatureName  
134:            $Feature | Add-Member noteproperty State $FeatureState  
135:            $Features += $Feature  
136:            $i++  
137:       } while ($i -lt $Temp.Count)  
138:       $Features = $Features | Sort-Object Name  
139:       Return $Features  
140:  }  
141:    
142:  function Set-WindowsFeature {  
143:  <#  
144:       .SYNOPSIS  
145:            Configure a Windows Feature  
146:         
147:       .DESCRIPTION  
148:            Enable or disable a windows feature  
149:         
150:       .PARAMETER Name  
151:            Name of the windows feature  
152:         
153:       .PARAMETER State  
154:            Enable or disable windows feature  
155:         
156:       .NOTES  
157:            Additional information about the function.  
158:  #>  
159:         
160:       [CmdletBinding()]  
161:       param  
162:       (  
163:                 [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Name,  
164:                 [Parameter(Mandatory = $true)][ValidateSet('enable', 'disable')][string]$State  
165:       )  
166:         
167:       $EXE = $env:windir + "\system32\dism.exe"  
168:       Write-Host $Name"....." -NoNewline  
169:       If ($State -eq "enable") {  
170:            $Parameters = "/online /enable-feature /norestart /featurename:" + $Name  
171:       } else {  
172:            $Parameters = "/online /disable-feature /norestart /featurename:" + $Name  
173:       }  
174:       $ErrCode = (Start-Process -FilePath $EXE -ArgumentList $Parameters -Wait -PassThru -WindowStyle Minimized).ExitCode  
175:       If ($ErrCode -eq 0) {  
176:            $FeatureChange = Confirm-Feature -FeatureName $Name -FeatureState $State  
177:            If ($FeatureChange -eq $true) {  
178:                 If ($State -eq 'Enable') {  
179:                      Write-Host "Enabled" -ForegroundColor Yellow  
180:                 } else {  
181:                      Write-Host "Disabled" -ForegroundColor Yellow  
182:                 }  
183:            } else {  
184:                 Write-Host "Failed" -ForegroundColor Red  
185:            }  
186:       } elseif ($ErrCode -eq 3010) {  
187:            $FeatureChange = Confirm-Feature -FeatureName $Name -FeatureState $State  
188:            If ($FeatureChange -eq $true) {  
189:                 If ($State -eq 'Enable') {  
190:                      Write-Host "Enabled & Pending Reboot" -ForegroundColor Yellow  
191:                 } else {  
192:                      Write-Host "Disabled & Pending Reboot" -ForegroundColor Yellow  
193:                 }  
194:            } else {  
195:                 Write-Host "Failed" -ForegroundColor Red  
196:            }  
197:       } else {  
198:            If ($ErrCode -eq 50) {  
199:                 Write-Host "Failed. Parent feature needs to be enabled first." -ForegroundColor Red  
200:            } else {  
201:                 Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
202:            }  
203:       }  
204:  }  
205:    
206:  function Set-FeaturesFromFile {  
207:  <#  
208:       .SYNOPSIS  
209:            Set multiple features from a text file  
210:         
211:       .DESCRIPTION  
212:            This function reads the comma separated features and values from a text file and executes each feature.  
213:         
214:       .NOTES  
215:            Additional information about the function.  
216:  #>  
217:         
218:       [CmdletBinding()]  
219:       param ()  
220:         
221:       $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + '\'  
222:       $FeaturesFile = $RelativePath + $FeaturesFile  
223:       If ((Test-Path $FeaturesFile) -eq $true) {  
224:            $FeaturesFile = Get-Content $FeaturesFile  
225:            foreach ($Item in $FeaturesFile) {  
226:                 $Item = $Item.split(',')  
227:                 Set-WindowsFeature -Name $Item[0] -State $Item[1]  
228:            }  
229:       }  
230:  }  
231:    
232:  Clear-Host  
233:  If ($ListFeatures -eq $true) {  
234:       $WindowsFeatures = Get-WindowsFeaturesList  
235:       $WindowsFeatures  
236:  }  
237:  If ($FeaturesFile -ne '') {  
238:       Set-FeaturesFromFile  
239:  }  
240:  If ($Feature -ne '') {  
241:       Set-WindowsFeature -Name $Feature -State $Setting  
242:  }  
243:    

26 May 2016

Windows 10: Windows could not parse or process the unattend answer file for pass [specialize]

Posted By: Mick Pletcher - 2:50 PM
















I am in the beginning stages of working on upgrading the firm I work at to Windows 10. Using MDT, The OS would inject drivers and lay down with no issues. Upon the first reboot, it would pop up the following error message:


The first thing I did was to look at the Unattend.xml file by opening the task sequence properties--->OS Info tab. When I looked under the Specialize--->Microsoft-Windows-Shell-Setup, everything looked normal, except for one thing. The product code was showing the code for Windows 7 and I had entered the code during the task sequence setup phase. I proceeded to change to the correct Windows 10 product code. The same error came up again. At that point, I cleared every field within the Specialize--->Microsoft-Windows-Shell-Setup by right-clicking and selecting Revert Change. The same issue occurred again. I hit Shift-F10 to pull up a command line. I got the c:\windows\panther files copied to a thumb drive and looked at the logs on my other machine. I saw the following errors in the setupact.log file:

At this point, I decided to completely delete the task sequence and start all over. The issue persisted. Finally, I deleted the Microsoft-Windows-Shell-Setup from the Specialize component. I re-imaged the machine and it now goes through the first bootup/setup phase with no problems. I already use PowerShell to set the product code after the image is laid down, so that was not a big deal.

After deleting the Microsoft-Windows-Shell-Setup, the image went through successfully, but I ran into other issues. The system was not joined to the domain and was not named for the computer name I set.

I added the amd64_Microsoft-Windows-Shell-Setup_neutral back to the 4 specialize component. I then right-clicked on the ComputerName setting and selected Write Empty String as shown below.


I proceeded to reimage the machine and it imaged successfully, joined the domain, and had the correct computer name.

20 May 2016

Initiating SCCM Actions with Verification

Posted By: Mick Pletcher - 2:36 PM









The latest script I am writing requires me to initiate both a software updates scan cycle and a software updates deployment evaluation cycle before continuing with the script. The actions needed to be completed before the script can continue, which is what started me on the path to writing this script. I figured if I was going to write two functions for those actions, I would write functions for the rest of the actions. I do realize there are more actions out there that I could include, but for now, I only included the ones in the Actions tab of the Configuration Manager Properties. Thanks to Eswar Knoeti's blog post, I had a reference to refer to on what log files to scan for the action being taken.

The way these scripts work, they kick off an action and then monitor the log file associated with that action until the completion/failed message appears. I have also included a 5 minute maximum wait time if something goes wrong and the action does not complete successfully, and a new message appears. I have noticed that some actions will not initiate if they have been initiated recently. For those, the status will return as failed.

I was going to originally include parameters so that you could select which functions to execute at command line. I chose not to because I figured that functions will likely be taken from this script to be included in other scripts and it is just as easy to comment out ones you do not want to execute between lines 446 and 456.

This has worked great in my environment. There maybe new messages in the log files that you may encounter that cause the function to not read the log file correctly. If so, please write a comment about that in the blog so that I can update the script. Thanks in advance.

With the help of Sapien's PowerShell Studio, I wrote the following script. You can download the script from here.


1:  <#  
2:       .SYNOPSIS  
3:            Initiate SCCM actions  
4:         
5:       .DESCRIPTION  
6:            This script will initiate SCCM actions and wait until the action is complete before continuing.   
7:         
8:       .EXAMPLE  
9:            powershell.exe -executionpolicy bypass -file SCCMActions.ps1  
10:         
11:       .NOTES  
12:       ===========================================================================  
13:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
14:        Created on:       5/20/2016 2:28 PM  
15:        Created by:       Mick Pletcher  
16:        Organization:         
17:        Filename:         SCCMActions.ps1  
18:       ===========================================================================  
19:  #>  
20:  [CmdletBinding()]  
21:  param ()  
22:    
23:  function Get-CurrentDate {  
24:  <#  
25:       .SYNOPSIS  
26:            Get the current date and return formatted value  
27:         
28:       .DESCRIPTION  
29:            Return the current date in the following format: mm-dd-yyyy  
30:         
31:       .NOTES  
32:            Additional information about the function.  
33:  #>  
34:         
35:       [CmdletBinding()][OutputType([string])]  
36:       param ()  
37:         
38:       $CurrentDate = Get-Date  
39:       $CurrentDate = $CurrentDate.ToShortDateString()  
40:       $CurrentDate = $CurrentDate -replace "/", "-"  
41:       If ($CurrentDate[2] -ne "-") {  
42:            $CurrentDate = $CurrentDate.Insert(0, "0")  
43:       }  
44:       If ($CurrentDate[5] -ne "-") {  
45:            $CurrentDate = $CurrentDate.Insert(3, "0")  
46:       }  
47:       Return $CurrentDate  
48:  }  
49:    
50:  function Invoke-ApplicationDeploymentEvaluationCycle {  
51:       $Completed = $false  
52:       $StartTime = Get-Date -UFormat %R  
53:       $CurrentDate = Get-CurrentDate  
54:       Write-Host "Running Application Deployment Evaluation Cycle....." -NoNewline  
55:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000121"  
56:       Do {  
57:            Start-Sleep -Seconds 1  
58:            $CurrentTime = Get-Date -UFormat %R  
59:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
60:            $Log = Get-Content $env:windir"\ccm\logs\DCMReporting.log"  
61:            $Count = $Log.count  
62:            $Count = $Count - 1  
63:            $Log = $Log[$Count]  
64:            $LogTable = $Log.split("<")[-1]  
65:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
66:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
67:            $LogTime = $LogTable.time.Substring(0, 5)  
68:            [datetime]$StringTime = $LogTable.time  
69:            If (($Log -like "*FinalRelease*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
70:                 Write-Host "Completed" -ForegroundColor Yellow  
71:                 $Completed = $true  
72:            }  
73:            If ($TimeDifference.Minutes -ge 5) {  
74:                 Write-Host "Failed" -ForegroundColor Yellow  
75:                 $Completed = $true  
76:            }  
77:       } while ($Completed -eq $false)  
78:  }  
79:    
80:  function Invoke-DiscoveryDataCollectionCycle {  
81:       $Completed = $false  
82:       $StartTime = Get-Date -UFormat %R  
83:       $CurrentDate = Get-CurrentDate  
84:       Write-Host "Running Discovery Data Collection Cycle....." -NoNewline  
85:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000003"  
86:       Do {  
87:            Start-Sleep -Seconds 1  
88:            $CurrentTime = Get-Date -UFormat %R  
89:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
90:            $Log = Get-Content $env:windir"\ccm\logs\InventoryAgent.log"  
91:            $Count = $Log.count  
92:            $Count = $Count - 1  
93:            $Log = $Log[$Count]  
94:            $LogTable = $Log.split("<")[-1]  
95:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
96:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
97:            $LogTime = $LogTable.time.Substring(0, 5)  
98:            [datetime]$StringTime = $LogTable.time  
99:            If (($Log -like "*End of message processing*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
100:                 Write-Host "Completed" -ForegroundColor Yellow  
101:                 $Completed = $true  
102:            }  
103:            If ($TimeDifference.Minutes -ge 5) {  
104:                 Write-Host "Failed" -ForegroundColor Yellow  
105:                 $Completed = $true  
106:            }  
107:       } while ($Completed -eq $false)  
108:  }  
109:    
110:  function Invoke-FileCollectionCycle {  
111:       $Completed = $false  
112:       $StartTime = Get-Date -UFormat %R  
113:       $CurrentDate = Get-CurrentDate  
114:       Write-Host "Running File Collection Cycle....." -NoNewline  
115:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000010"  
116:       Do {  
117:            Start-Sleep -Seconds 1  
118:            $CurrentTime = Get-Date -UFormat %R  
119:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
120:            $Log = Get-Content $env:windir"\ccm\logs\InventoryAgent.log"  
121:            $Count = $Log.count  
122:            $Count = $Count - 1  
123:            $Log = $Log[$Count]  
124:            $LogTable = $Log.split("<")[-1]  
125:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
126:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
127:            $LogTime = $LogTable.time.Substring(0, 5)  
128:            [datetime]$StringTime = $LogTable.time  
129:            If ((($Log -like "*Action completed*") -or ($Log -like "*Exiting as no items to collect*")) -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
130:                 Write-Host "Completed" -ForegroundColor Yellow  
131:                 $Completed = $true  
132:            }  
133:            If ($TimeDifference.Minutes -ge 5) {  
134:                 Write-Host "Failed" -ForegroundColor Yellow  
135:                 $Completed = $true  
136:            }  
137:       } while ($Completed -eq $false)  
138:  }  
139:    
140:  function Invoke-HardwareInventoryCycle {  
141:       $Completed = $false  
142:       $StartTime = Get-Date -UFormat %R  
143:       $CurrentDate = Get-CurrentDate  
144:       Write-Host "Running Hardware Inventory Cycle....." -NoNewline  
145:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000001"  
146:       Do {  
147:            Start-Sleep -Seconds 1  
148:            $CurrentTime = Get-Date -UFormat %R  
149:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
150:            $Log = Get-Content $env:windir"\ccm\logs\InventoryAgent.log"  
151:            $Count = $Log.count  
152:            $Count = $Count - 1  
153:            $Log = $Log[$Count]  
154:            $LogTable = $Log.split("<")[-1]  
155:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
156:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
157:            $LogTime = $LogTable.time.Substring(0, 5)  
158:            [datetime]$StringTime = $LogTable.time  
159:            If (($Log -like "*End of message processing*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
160:                 Write-Host "Completed" -ForegroundColor Yellow  
161:                 $Completed = $true  
162:            }  
163:            If (($Log -like "*already in queue. Message ignored.*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
164:                 Write-Host "Ignored" -ForegroundColor Red  
165:                 $Completed = $true  
166:            }  
167:            If ($TimeDifference.Minutes -ge 5) {  
168:                 Write-Host "Failed" -ForegroundColor Yellow  
169:                 $Completed = $true  
170:            }  
171:       } while ($Completed -eq $false)  
172:  }  
173:    
174:  function Invoke-MachinePolicyEvaluationCycle {  
175:       $Completed = $false  
176:       $StartTime = Get-Date -UFormat %r  
177:       $StartTime = $StartTime.Split(' ')  
178:       $StartTime = $StartTime[0]  
179:       $StartTime = $StartTime.Substring(0, $StartTime.Length - 3)  
180:       $CurrentDate = Get-CurrentDate  
181:       Write-Host "Running Machine Policy Evaluation Cycle....." -NoNewline  
182:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000022"  
183:       Do {  
184:            Start-Sleep -Seconds 1  
185:            $CurrentTime = Get-Date -UFormat %R  
186:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
187:            $Log = Get-Content $env:windir"\ccm\logs\PolicyEvaluator.log"  
188:            $Count = $Log.count  
189:            $LogTime = $Log[$Count - 6]  
190:            $LogTime = $LogTime.Split('"')  
191:            $LogTime = $LogTime[1]  
192:            $LogTime = [management.managementdatetimeconverter]::ToDateTime($LogTime)  
193:            $LogDate = $LogTime  
194:            $LogTime = $LogTime.ToShortTimeString()  
195:            $LogTime = $Logtime.Split(' ')  
196:            $LogTime = $LogTime[0]  
197:            If ($LogTime[2] -ne ":") {  
198:                 $LogTime = $LogTime.Insert(0, "0")  
199:            }  
200:            $LogDate = $LogDate.ToShortDateString()  
201:            $LogDate = Get-CurrentDate  
202:            $LogStatus = $Log[$Count - 9]  
203:            If (($LogStatus -like "*instance of CCM_PolicyAgent_PolicyEvaluationComplete*") -and ($CurrentDate -eq $LogDate) -and ($LogTime -ge $StartTime)) {  
204:                 Write-Host "Completed" -ForegroundColor Yellow  
205:                 $Completed = $true  
206:            }  
207:            If ($TimeDifference.Minutes -ge 5) {  
208:                 Write-Host "Failed" -ForegroundColor Yellow  
209:                 $Completed = $true  
210:            }  
211:       } while ($Completed -eq $false)  
212:  }  
213:    
214:  function Invoke-MachinePolicyRetrievalCycle {  
215:       $Completed = $false  
216:       $StartTime = Get-Date -UFormat %r  
217:       $StartTime = $StartTime.Split(' ')  
218:       $StartTime = $StartTime[0]  
219:       $StartTime = $StartTime.Substring(0, $StartTime.Length - 3)  
220:       $CurrentDate = Get-CurrentDate  
221:       Write-Host "Running Machine Policy Retrieval Cycle....." -NoNewline  
222:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000021"  
223:       Do {  
224:            Start-Sleep -Seconds 1  
225:            $CurrentTime = Get-Date -UFormat %R  
226:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
227:            $Log = Get-Content $env:windir"\ccm\logs\PolicyEvaluator.log"  
228:            $Count = $Log.count  
229:            $LogTime = $Log[$Count - 6]  
230:            $LogTime = $LogTime.Split('"')  
231:            $LogTime = $LogTime[1]  
232:            $LogTime = [management.managementdatetimeconverter]::ToDateTime($LogTime)  
233:            $LogDate = $LogTime  
234:            $LogTime = $LogTime.ToShortTimeString()  
235:            $LogTime = $Logtime.Split(' ')  
236:            $LogTime = $LogTime[0]  
237:            If ($LogTime[2] -ne ":") {  
238:                 $LogTime = $LogTime.Insert(0, "0")  
239:            }  
240:            $LogDate = $LogDate.ToShortDateString()  
241:            $LogDate = Get-CurrentDate  
242:            $LogStatus = $Log[$Count - 9]  
243:            If (($LogStatus -like "*instance of CCM_PolicyAgent_PolicyEvaluationComplete*") -and ($CurrentDate -eq $LogDate) -and ($LogTime -ge $StartTime)) {  
244:                 Write-Host "Completed" -ForegroundColor Yellow  
245:                 $Completed = $true  
246:            }  
247:            If ($TimeDifference.Minutes -ge 5) {  
248:                 Write-Host "Failed" -ForegroundColor Yellow  
249:                 $Completed = $true  
250:            }  
251:       } while ($Completed -eq $false)  
252:  }  
253:    
254:  function Invoke-SoftwareInventoryCycle {  
255:       $Completed = $false  
256:       $StartTime = Get-Date -UFormat %R  
257:       $CurrentDate = Get-CurrentDate  
258:       Write-Host "Running Software Inventory Cycle....." -NoNewline  
259:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000002"  
260:       Do {  
261:            Start-Sleep -Seconds 1  
262:            $CurrentTime = Get-Date -UFormat %R  
263:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
264:            $Log = Get-Content $env:windir"\ccm\logs\InventoryAgent.log"  
265:            $Count = $Log.count  
266:            $Count = $Count - 1  
267:            $Log = $Log[$Count]  
268:            $LogTable = $Log.split("<")[-1]  
269:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
270:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
271:            $LogTime = $LogTable.time.Substring(0, 5)  
272:            [datetime]$StringTime = $LogTable.time  
273:            If (($Log -like "*Initialization completed in*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
274:                 Write-Host "Completed" -ForegroundColor Yellow  
275:                 $Completed = $true  
276:            }  
277:            If ($TimeDifference.Minutes -ge 5) {  
278:                 Write-Host "Failed" -ForegroundColor Yellow  
279:                 $Completed = $true  
280:            }  
281:       } while ($Completed -eq $false)  
282:  }  
283:    
284:  function Invoke-SoftwareMeteringUsageReportCycle {  
285:       $Completed = $false  
286:       $StartTime = Get-Date -UFormat %R  
287:       $CurrentDate = Get-CurrentDate  
288:       Write-Host "Running Software Metering Usage Report Cycle....." -NoNewline  
289:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000031"  
290:       Do {  
291:            Start-Sleep -Seconds 1  
292:            $CurrentTime = Get-Date -UFormat %R  
293:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
294:            $Log = Get-Content $env:windir"\ccm\logs\SWMTRReportGen.log"  
295:            $Count = $Log.count  
296:            $Count = $Count - 1  
297:            $Log = $Log[$Count]  
298:            $LogTable = $Log.split("<")[-1]  
299:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
300:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
301:            $LogTime = $LogTable.time.Substring(0, 5)  
302:            [datetime]$StringTime = $LogTable.time  
303:            If ((($Log -like "*No usage data found to generate software metering report*") -or ($Log -like "*Successfully generated report header*") -or ($Log -like "*Message ID of sent message*")) -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
304:                 Write-Host "Completed" -ForegroundColor Yellow  
305:                 $Completed = $true  
306:            }  
307:            If ($TimeDifference.Minutes -ge 5) {  
308:                 Write-Host "Failed" -ForegroundColor Yellow  
309:                 $Completed = $true  
310:            }  
311:       } while ($Completed -eq $false)  
312:  }  
313:    
314:  function Invoke-SoftwareUpdatesDeploymentEvaluationCycle {  
315:  <#  
316:       .SYNOPSIS  
317:            Scan for software updates that are out of compliance  
318:         
319:       .DESCRIPTION  
320:            Initiates a scan for software updates compliance. Before client computers can scan for software update compliance, the software updates environment must be configured.  
321:         
322:       .NOTES  
323:            Additional information about the function.  
324:  #>  
325:         
326:       [CmdletBinding()]  
327:       param ()  
328:         
329:       $Completed = $false  
330:       $StartTime = Get-Date -UFormat %R  
331:       $CurrentDate = Get-CurrentDate  
332:       Write-Host "Running Software Updates Deployment Evaluation Cycle....." -NoNewline  
333:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000108"  
334:       Do {  
335:            Start-Sleep -Seconds 1  
336:            $CurrentTime = Get-Date -UFormat %R  
337:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
338:            $Log = Get-Content $env:windir"\ccm\logs\ScanAgent.log"  
339:            $Count = $Log.count  
340:            $Count = $Count - 1  
341:            $Log = $Log[$Count]  
342:            $LogTable = $Log.split("<")[-1]  
343:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
344:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
345:            $LogTime = $LogTable.time.Substring(0, 5)  
346:            [datetime]$StringTime = $LogTable.time  
347:            If (($Log -like "*Calling back to client on Scan request complete*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
348:                 Write-Host "Completed" -ForegroundColor Yellow  
349:                 $Completed = $true  
350:            }  
351:            If ($TimeDifference.Minutes -ge 5) {  
352:                 Write-Host "Failed" -ForegroundColor Yellow  
353:                 $Completed = $true  
354:            }  
355:       } while ($Completed -eq $false)  
356:  }  
357:    
358:  function Invoke-SoftwareUpdatesScanCycle {  
359:       $Completed = $false  
360:       $StartTime = Get-Date -UFormat %R  
361:       $CurrentDate = Get-CurrentDate  
362:       Write-Host "Running Software Updates Scan Cycle....." -NoNewline  
363:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000113"  
364:       Do {  
365:            Start-Sleep -Seconds 1  
366:            $CurrentTime = Get-Date -UFormat %R  
367:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
368:            $Log = Get-Content $env:windir"\ccm\logs\scanagent.log"  
369:            $Count = $Log.count  
370:            $Count = $Count - 1  
371:            $Log = $Log[$Count]  
372:            $LogTable = $Log.split("<")[-1]  
373:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
374:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
375:            $LogTime = $LogTable.time.Substring(0, 5)  
376:            [datetime]$StringTime = $LogTable.time  
377:            If (($Log -like "*scan completion received*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
378:                 Write-Host "Completed" -ForegroundColor Yellow  
379:                 $Completed = $true  
380:            }  
381:            If ($TimeDifference.Minutes -ge 5) {  
382:                 Write-Host "Failed" -ForegroundColor Yellow  
383:                 $Completed = $true  
384:            }  
385:       } while ($Completed -eq $false)  
386:  }  
387:    
388:  function Invoke-WindowsInstallerSourceListUpdateCycle {  
389:       $Completed = $false  
390:       $StartTime = Get-Date -UFormat %R  
391:       $CurrentDate = Get-CurrentDate  
392:       Write-Host "Running Windows Installer Source List Update Cycle....." -NoNewline  
393:       Start-ConfigurationManagerClientScan -ScheduleID "00000000-0000-0000-0000-000000000032"  
394:       Do {  
395:            Start-Sleep -Seconds 1  
396:            $CurrentTime = Get-Date -UFormat %R  
397:            $TimeDifference = New-TimeSpan -Start $StartTime -End $CurrentTime  
398:            $Log = Get-Content $env:windir"\ccm\logs\SrcUpdateMgr.log"  
399:            $Count = $Log.count  
400:            $Count = $Count - 1  
401:            $Log = $Log[$Count]  
402:            $LogTable = $Log.split("<")[-1]  
403:            $LogTable = $LogTable.Substring(0, $LogTable.length - 1) -replace ' ', ';'  
404:            $LogTable = "@{$($LogTable)}" | Invoke-Expression  
405:            $LogTime = $LogTable.time.Substring(0, 5)  
406:            [datetime]$StringTime = $LogTable.time  
407:            If (($Log -like "*MSI update source list task finished successfully*") -and ($CurrentDate -eq $LogTable.date) -and ($LogTime -ge $StartTime)) {  
408:                 Write-Host "Completed" -ForegroundColor Yellow  
409:                 $Completed = $true  
410:            }  
411:            If ($TimeDifference.Minutes -ge 5) {  
412:                 Write-Host "Failed" -ForegroundColor Yellow  
413:                 $Completed = $true  
414:            }  
415:       } while ($Completed -eq $false)  
416:  }  
417:    
418:  function Start-ConfigurationManagerClientScan {  
419:  <#  
420:       .SYNOPSIS  
421:            Initiate Configuration Manager Client Scan  
422:         
423:       .DESCRIPTION  
424:            This will initiate an SCCM action  
425:         
426:       .PARAMETER ScheduleID  
427:            GUID ID of the SCCM action  
428:         
429:       .NOTES  
430:            Additional information about the function.  
431:  #>  
432:         
433:       [CmdletBinding()]  
434:       param  
435:       (  
436:                 [ValidateSet('00000000-0000-0000-0000-000000000121', '00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000021', '00000000-0000-0000-0000-000000000022', '00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000031', '00000000-0000-0000-0000-000000000108', '00000000-0000-0000-0000-000000000113', '00000000-0000-0000-0000-000000000111', '00000000-0000-0000-0000-000000000026', '00000000-0000-0000-0000-000000000027', '00000000-0000-0000-0000-000000000032')]$ScheduleID  
437:       )  
438:         
439:       $WMIPath = "\\" + $env:COMPUTERNAME + "\root\ccm:SMS_Client"  
440:       $SMSwmi = [wmiclass]$WMIPath  
441:       $Action = [char]123 + $ScheduleID + [char]125  
442:       [Void]$SMSwmi.TriggerSchedule($Action)  
443:  }  
444:    
445:  Clear-Host  
446:  Invoke-SoftwareUpdatesScanCycle  
447:  Invoke-SoftwareUpdatesDeploymentEvaluationCycle  
448:  Invoke-ApplicationDeploymentEvaluationCycle  
449:  Invoke-DiscoveryDataCollectionCycle  
450:  Invoke-FileCollectionCycle  
451:  Invoke-HardwareInventoryCycle  
452:  Invoke-MachinePolicyEvaluationCycle  
453:  Invoke-MachinePolicyRetrievalCycle  
454:  Invoke-SoftwareInventoryCycle  
455:  Invoke-SoftwareMeteringUsageReportCycle  
456:  Invoke-WindowsInstallerSourceListUpdateCycle  
457:    

Copyright © 2013 Mick's IT Blogs™ is a registered trademark.