25 February 2016

Laptop Mandatory Reboot Management

Managing laptops in certain environments can be daunting. Reboots are a must every now and then, especially for monthly windows updates. With the sleep and hibernate features being enabled, the chances of a user rebooting a laptop become far less. A laptop can go weeks and even months without a reboot. Working in the legal industry, as I do, adds to the complexity of forcing reboots as you have the issue of not causing a reboot during a hearing or during a client meeting for instance. You want to be as unobtrusive as possible. You might say that this is not needed as SCCM could be setup to automatically perform this same function on a regular schedule. That is true. Where this becomes valuable is when you don't want to force a reboot on users that regularly reboot their machines. If they are already doing this, which we have a fair number of users that do, then there is no need to reboot an additional time that will inconvenience them.

To make this process as unobtrusive as possible, I have written the following two PowerShell scripts, using Sapien's PowerShell Studio, that work in conjunction with SCCM 2012 to give users the leisure of a full business day to reboot the machine. One script is the custom detection method and the other is the installer.

The custom detection method works by reading the last event viewer 1074. It looks at the date of the ID and then sees if it is 14 days or older. This can be set to any number of days other than the 14 my firm has chosen. If it is 14 days old, the script then sets the registry key Rebooted to a DWORD value of 1 and fails the detection method. When the method fails, SCCM will then run the second PowerShell script.

The second script operates by running the same detection method. Once it detects the same values, it resets the Rebooted Key to 0 and then returns the error code 3010 back to SCCM. SCCM then reruns the detection method. The detection method sees there has been 14 days or more and the Rebooted key is set to 0. It returns and error code 0 back to SCCM with a write-host of "Success". This tells SCCM the application ran successfully and to now process with the soft reboot, which was required by the 3010 return code.

The final part is to configure the Computer Restart under the SCCM client settings. I configured ours to be 480 minutes, which is 8 hours, with a mandatory dialog window that cannot be closed the final 60 minutes.

When the system reboots, the custom detection method runs again and sees there is a new 1074 entry in the event viewer, along with the registry key Rebooted being a 0, therefor it shows successfully installed. As the days progress and SCCM reruns the custom detection method, it will rerun the application script to reboot the machine again if the machine is not rebooted in the 14 allotted days. If the user reboots the machine every week, the SCCM application will never reboot the machine.

I had a few to ask for a tutorial on setting this up. Here is a video I made on how I setup and configured the scripts in SCCM.



Here are the links to the two scripts:



Here are the two scripts below:

MandatoryRebootCustomDetection.ps1
1:  <#  
2:       .SYNOPSIS  
3:            Mandatory Reboot Custom Detection Method  
4:         
5:       .DESCRIPTION  
6:            This script will read the last time a system rebooted from the event viewer logs. It then calculates the number of days since that time. If the number of days equals or exceeds the RebootThreshold variable, the script will exit with a return code 0 and no data output. No data output is read by SCCM as a failure. If the number of days is less than the RebootThreshold, then a message is written saying the system is within the threshold and the script exits with a return code of 0. SCCM reads an error code 0 with data output as a success.  
7:         
8:       .NOTES  
9:            ===========================================================================  
10:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
11:            Created on:       6/8/2016 2:04 PM  
12:            Created by:       Mick Pletcher  
13:            Organization:  
14:            Filename:         MandatoryRebootCustomDetection.ps1  
15:            ===========================================================================  
16:  #>  
17:    
18:  #Number of days until reboot becomes mandatory  
19:  $RebootThreshold = 14  
20:  $Today = Get-Date  
21:  #Returns "32-bit" or "64-bit"  
22:  $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
23:  $Architecture = $Architecture.OSArchitecture  
24:  #Gets the last reboot from the event viewer logs  
25:  $LastReboot = get-winevent -filterhashtable @{ logname = 'system'; ID = 1074 } -maxevents 1 -ErrorAction SilentlyContinue  
26:  #Tests if the registry key Rebooted exists and creates it if it does not. It then reads if the system has been rebooted by the value being either a 0 or 1. This determines if the reboot has occurred and is set in the MandatoryReboot.ps1 file when the custom detection method triggers its execution  
27:  if ($Architecture -eq "32-bit") {  
28:       if ((Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
29:            New-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
30:       }  
31:       $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
32:  } else {  
33:       if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
34:            New-Item "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
35:       }  
36:       $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
37:  }  
38:  #Get the 0 or 1 value if a system has been rebooted within the $RebootThreshold period  
39:  $Rebooted = $Rebooted.Rebooted  
40:  #Calculate how long since last reboot. If no event viewer entries since last reboot, then trigger a reboot  
41:  if ($LastReboot -eq $null) {  
42:       $Difference = $RebootThreshold  
43:  } else {  
44:       $Difference = New-TimeSpan -Start $Today -End $LastReboot.TimeCreated  
45:       $Difference = [math]::Abs($Difference.Days)  
46:  }  
47:  #The first two conditions report to SCCM that the deployment is "installed" thereby not triggering a reboot. The last two report to SCCM the app is "not installed" and trigger an install  
48:  if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 0)) {  
49:       Write-Host "Success"  
50:       exit 0  
51:  }  
52:  if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 1)) {  
53:       Write-Host "Success"  
54:       exit 0  
55:  }  
56:  if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 0)) {  
57:       exit 0  
58:  }  
59:  if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 1)) {  
60:       exit 0  
61:  }  
62:    


MandatoryReboot.ps1
1:  <#  
2:       .SYNOPSIS  
3:            Mandatory Reboot  
4:         
5:       .DESCRIPTION  
6:            This script will read the last time a system rebooted from the event viewer logs. It then calculates the number of days since that time. If the number of days equals or exceeds the RebootThreshold variable, the script will change the registry key Rebooted to a 0. It then exits with an error code 3010, which tells SCCM 2012 to perform a soft reboot.  
7:         
8:       .NOTES  
9:            ===========================================================================  
10:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
11:            Created on:       6/8/2016 1:58 PM  
12:            Created by:       Mick Pletcher  
13:            Organization:  
14:            Filename:         MandatoryReboot.ps1  
15:            ===========================================================================  
16:  #>  
17:    
18:  #Returns "32-bit" or "64-bit"  
19:  $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
20:  $Architecture = $Architecture.OSArchitecture  
21:  #Tests if the registry key Rebooted exists and creates it if it does not. It then reads if the system has been rebooted by the value being either a 0 or 1. This determines if the reboot has occurred and is set in the MandatoryReboot.ps1 file when the custom detection method triggers its execution  
22:  if ($Architecture -eq "32-bit") {  
23:       if ((Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
24:            New-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
25:       }  
26:       $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
27:  } else {  
28:       if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
29:            New-Item "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
30:       }  
31:       $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
32:  }  
33:  #Get the 0 or 1 value if a system has been rebooted within the $RebootThreshold period  
34:  $Rebooted = $Rebooted.Rebooted  
35:  #Number of days until reboot becomes mandatory  
36:  $RebootThreshold = 14  
37:  $Today = Get-Date  
38:  #Gets the last reboot from the event viewer logs  
39:  $LastReboot = get-winevent -filterhashtable @{ logname = 'system'; ID = 1074 } -maxevents 1 -ErrorAction SilentlyContinue  
40:  #Calculate how long since last reboot. If no event viewer entries since last reboot, then trigger a reboot  
41:  if ($LastReboot -eq $null) {  
42:       $Difference = $RebootThreshold  
43:  } else {  
44:       $Difference = New-TimeSpan -Start $Today -End $LastReboot.TimeCreated  
45:       $Difference = [math]::Abs($Difference.Days)  
46:  }  
47:  #Change the HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot key to a 1 if the system is over the pre-determined threshold and $Rebooted = 0  
48:  if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 0)) {  
49:       if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $true) {  
50:            Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" -Name Rebooted -Value 1 -Type DWORD -Force  
51:            $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
52:       } else {  
53:            Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" -Name Rebooted -Value 1 -Type DWORD -Force  
54:            $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
55:       }  
56:  }  
57:  $Rebooted = $Rebooted.Rebooted  
58:  #Change the HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot key back to 0 if the system has been rebooted within the $RebootThreshold period  
59:  if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 1)) {  
60:       if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $true) {  
61:            Set-ItemProperty -Name Rebooted -Value 0 -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" -Type DWORD -Force  
62:            $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
63:       } else {  
64:            Set-ItemProperty -Name Rebooted -Value 0 -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" -Type DWORD -Force  
65:            $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
66:       }  
67:       $Rebooted = $Rebooted.Rebooted  
68:       Write-Host "System is within"$RebootThreshold" Day Reboot Threshold"  
69:  }  
70:  Write-Host "Reboot Threshold:"$RebootThreshold  
71:  Write-Host "Difference:"$Difference  
72:  Write-Host "Rebooted:"$Rebooted  
73:  Exit 3010  

22 February 2016

MDT Build Reporting Tool

I have been wishing for a reporting system to which MDT would automatically email IT staff when a build is finished. I began investigating how to access MDT using PowerShell and found this great resource. I used their information and function to expand the process into a full fledged reporting tool.

This tool will email the appropriate IT staff when a build successfully completes, fails, unknown error, or continues running over a designated time frame. This is a sample email of a test image used to test this script out:


The script writes to a log file when a system has been imaged and an email was sent to the IT staff. This lets the script know in the future that it has already sent an email and not to send another one. By default, MDT keeps systems in the Monitoring section for 3 days. After 3 days, a system is purged. The script will delete the system also from the log file at that point. The log file is written at the same location where the script resides.

I have setup the script as a task sequence on one of our servers to be executed every 15 minutes. You can choose a different interval if you desire. 

If a build exceeds two hours, an email is sent to the IT staff. I designated two hours, as most of our builds take 53 minutes. When an email is sent, it does not get written to the log file. It is just a warning. If the system continues on and completes successfully or not, it will then send a final email. 

All customizations are done at the command line through parameters. Here is the list:

  • LogFile - Not Mandatory - Name of the log file must be <Filename>.log. Default value is ImagedSystems.log
  • MonitoringHost - Mandatory - FQDN of the MDT monitoring host
  • EmailAddress - Mandatory - Email address to send status emails to
  • SMTPServer - Mandatory - FQDN of the SMTP server
  • Sender - Mandatory - email address of the sender
  • MaxImageTime - Not Mandatory - Designates the maximum allowable imaging time before an email is sent to IT staff that the image is taking longer than expected
  • DaysSince - Not Mandatory - Number of days since the system was imaged before removing from log file. Default value is 3
Here is an example command line execution:
powershell.exe -executionpolicy bypass -file %~dp0MDT.ps1 -LogFile ImagedSystems.log -MonitoringHost condeco.test.com -EmailAddress helpdesk@test.com -SMTPServer smtp.test.com -Sender build@test.com -MaxImageTime 02:00:00 -DaysSince 3


You can download the script from here.

NOTE: I used Sapien's PowerShell Studio to write this script that significantly simplified the process and made it a snap to write. I highly recommend this product for all PowerShell scripters!

MDT.ps1
 <#  
      .SYNOPSIS  
           This script sends out emails to the IT staff with the status of  
           MDT builds.  
        
      .DESCRIPTION  
           You can find detailed information on my blog:  
           http://mickitblog.blogspot.com/2016/02/mdt-build-reporting-tool.html  
        
      .PARAMETER LogFile  
           Name of the log file must be <Filename>.log  
        
      .PARAMETER MonitoringHost  
           The FQDN of the MDT server  
        
      .PARAMETER EmailAddress  
           Enter the email address of the user(s) to email status messages  
        
      .PARAMETER SMTPServer  
           FQDN of the SMTP server  
        
      .PARAMETER Sender  
           Email address of the sender  
        
      .PARAMETER MaxImageTime  
           Designates the maximum allowable imaging time before an email is sent to IT staff that the image is taking longer than expected.  
        
      .PARAMETER DaysSince  
           Number of days since the system was imaged before removing from log file  
        
      .NOTES  
           ===========================================================================  
           Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
           Created on:       2/9/2016 12:22 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:        MDTReportingTool.ps1  
           ===========================================================================  
 #>  
 param  
 (  
      [Parameter(Mandatory = $false)][string]$LogFile = 'ImagedSystems.log',  
      [Parameter(Mandatory = $true)][string]$MonitoringHost,  
      [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$EmailAddress,  
      [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$SMTPServer,  
      [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Sender,  
      [Parameter(Mandatory = $false)]$MaxImageTime = '02:00:00',  
      [Parameter(Mandatory = $false)][int]$DaysSince = 3  
 )  
   
 Function Get-LocalTime {  
      param ($UTCTime)  
        
      #Declare Local Variables  
      Set-Variable -Name LocalTime -Scope Local -Force  
      Set-Variable -Name strCurrentTimeZone -Scope Local -Force  
      Set-Variable -Name TimeZone -Scope Local -Force  
        
      $strCurrentTimeZone = (Get-WmiObject win32_timezone).StandardName  
      $TimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone)  
      $LocalTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TimeZone)  
      Return $LocalTime  
        
      #Cleanup Local Variables  
      Remove-Variable -Name LocalTime -Scope Local -Force  
      Remove-Variable -Name strCurrentTimeZone -Scope Local -Force  
      Remove-Variable -Name TimeZone -Scope Local -Force  
        
 }  
   
 function Get-MDTData {  
      param ($MonitoringHost)  
        
      #Declare Local Variables  
      Set-Variable -Name Data -Scope Local -Force  
      Set-Variable -Name Property -Scope Local -Force  
        
      $Data = Invoke-RestMethod $MonitoringHost  
        
      foreach ($property in ($Data.content.properties)) {  
           New-Object PSObject -Property @{  
                Name = $($property.Name);  
                PercentComplete = $($property.PercentComplete.’#text’);  
                Warnings = $($property.Warnings.’#text’);  
                Errors = $($property.Errors.’#text’);  
                DeploymentStatus = $(  
                Switch ($property.DeploymentStatus.’#text’) {  
                     1 { "Active/Running" }  
                     2 { "Failed" }  
                     3 { "Successfully completed" }  
                     Default { "Unknown" }  
                }  
                );  
                StartTime = $($property.StartTime.’#text’) -replace "T", " ";  
                EndTime = $($property.EndTime.’#text’) -replace "T", " ";  
           }  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Data -Scope Local -Force  
      Remove-Variable -Name Property -Scope Local -Force  
 }  
   
 function Get-RelativePath {  
      #Declare Local Variables  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $RelativePath  
        
      #Cleanup Local Variables  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function New-Logs {  
      param ($LogFile)  
        
      #Declare Local Variables  
      Set-Variable -Name Temp -Scope Local -Force  
        
      if ((Test-Path $LogFile) -eq $false) {  
           $Temp = New-Item -Path $LogFile -ItemType file -Force  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Temp -Scope Local -Force  
 }  
   
 function New-Report {  
      param ($System)  
        
      #Declare Variables  
      Set-Variable -Name Body -Scope Local -Force  
      Set-Variable -Name EndTime -Scope Local -Force  
      Set-Variable -Name Imaging -Scope Local -Force  
      Set-Variable -Name StartTime -Scope Local -Force  
      Set-Variable -Name Subject -Scope Local -Force  
        
      $StartTime = $System.StartTime  
      $StartTime = $StartTime -split " "  
      [DateTime]$StartTime = $StartTime[1]  
      $StartTime = Get-LocalTime -UTCTime $StartTime  
      If ($System.EndTime -eq "") {  
           $CurrentTime = Get-Date  
           $Imaging = "{2:D2}:{4:D2}:{5:D2}" -f (New-TimeSpan -Start $StartTime -End $CurrentTime).psobject.Properties.Value  
           $EndTime = "N/A"  
      } else {  
           $Imaging = "{2:D2}:{4:D2}:{5:D2}" -f (New-TimeSpan -Start $System.StartTime -End $System.EndTime).psobject.Properties.Value  
           $EndTime = $System.EndTime  
           $EndTime = $EndTime -split " "  
           [DateTime]$EndTime = $EndTime[1]  
           $EndTime = Get-LocalTime -UTCTime $EndTime  
      }  
      Write-Host  
      Write-Host "System:"$System.Name  
      Write-Host "Deployment Status:"$System.DeploymentStatus  
      Write-Host "Completed:"$System.PercentComplete  
      Write-Host "Imaging Time:"$Imaging  
      Write-Host "Start:" $StartTime  
      Write-Host "End:" $EndTime  
      Write-Host "Errors:"$System.Errors  
      Write-Host "Warnings:"$System.Warnings  
      $Subject = "Image Status:" + [char]32 + $System.Name  
      $Body = "System:" + [char]32 + $System.Name + [char]13 +`  
           "Deployment Status:" + [char]32 + $System.DeploymentStatus + [char]13 +`  
           "Completed:" + [char]32 + $System.PercentComplete + "%" + [char]13 +`  
           "Start Time:" + [char]32 + $StartTime + [char]13 +`  
           "End Time:" + [char]32 + $EndTime + [char]13 +`  
           "Imaging Time:" + [char]32 + $Imaging + [char]13 +`  
           "Errors:" + [char]32 + $System.Errors + [char]13 +`  
           "Warnings:" + [char]32 + $System.Warnings + [char]13  
      Send-MailMessage -To $EmailAddress -From $Sender -Subject $Subject -Body $Body -SmtpServer $SMTPServer  
        
      #Cleanup Variables  
      Remove-Variable -Name Body -Scope Local -Force  
      Remove-Variable -Name EndTime -Scope Local -Force  
      Remove-Variable -Name Imaging -Scope Local -Force  
      Remove-Variable -Name StartTime -Scope Local -Force  
      Remove-Variable -Name Subject -Scope Local -Force  
 }  
   
 function Remove-OldSystems {  
      param  
      (  
           [parameter(Mandatory = $true)]$Systems  
      )  
        
      #Declare Local Variables  
      Set-Variable -Name Log -Scope Local -Force  
      Set-Variable -Name Logs -Scope Local -Force  
      Set-Variable -Name NewLogs -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name System -Scope Local -Force  
        
      $NewLogs = @()  
      $RelativePath = Get-RelativePath  
      $Logs = (Get-Content $LogFile)  
      #Remove systems from logfile that do not exist in MDT Monitoring  
      foreach ($Log in $Logs) {  
           If (($Log -in $Systems.Name)) {  
                $System = $Systems | where { $_.Name -eq $Log }  
                If (($System.DeploymentStatus -eq "Successfully completed") -or ($System.DeploymentStatus -eq "Failed") -or ($System.DeploymentStatus -eq "Unknown")) {  
                     $NewLogs = $NewLogs + $Log  
                }  
           }  
      }  
      Out-File -FilePath $LogFile -InputObject $NewLogs -Force  
        
        
      #Cleanup Local Variables  
      Remove-Variable -Name Log -Scope Local -Force  
      Remove-Variable -Name Logs -Scope Local -Force  
      Remove-Variable -Name NewLogs -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name System -Scope Local -Force  
        
 }  
   
 function Add-NewSystems {  
      param  
      (  
           [parameter(Mandatory = $true)]$Systems  
      )  
        
      #Declare Local Variables  
      Set-Variable -Name CurrentTime -Scope Local -Force  
      Set-Variable -Name Imaging -Scope Local -Force  
      Set-Variable -Name Log -Scope Local -Force  
      Set-Variable -Name Logs -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name StartTime -Scope Local -Force  
      Set-Variable -Name System -Scope Local -Force  
      Set-Variable -Name SystemName -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      #Read Log File  
      $Logs = (Get-Content $LogFile)  
      #Add new systems to logfile  
      foreach ($SystemName in $Systems.Name) {  
           If (-not($SystemName -in $Logs)) {  
                $System = $Systems | where { $_.Name -eq $SystemName }  
                If (($System.DeploymentStatus -eq "Successfully completed") -or ($System.DeploymentStatus -eq "Failed") -or ($System.DeploymentStatus -eq "Unknown")) {  
                     New-Report -System $System  
                     Out-File -FilePath $LogFile -InputObject $SystemName -Append -Force  
                } else {  
                     $StartTime = Get-LocalTime -UTCTime $System.StartTime  
                     $CurrentTime = Get-Date  
                     $Imaging = "{2:D2}:{4:D2}:{5:D2}" -f (New-TimeSpan -Start $StartTime -End $CurrentTime).psobject.Properties.Value  
                     If ($Imaging -ge $MaxImageTime) {  
                          New-Report -System $System  
                     }  
                }  
           }  
      }  
   
      #Cleanup Local Variables  
      Remove-Variable -Name CurrentTime -Scope Local -Force  
      Remove-Variable -Name Imaging -Scope Local -Force  
      Remove-Variable -Name Log -Scope Local -Force  
      Remove-Variable -Name Logs -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name StartTime -Scope Local -Force  
      Remove-Variable -Name System -Scope Local -Force  
      Remove-Variable -Name SystemName -Scope Local -Force  
 }  
   
 #Declare Local Variables  
 Set-Variable -Name ImagedSystems -Scope Local -Force  
 Set-Variable -Name RelativePath -Scope Local -Force  
   
 cls  
 $RelativePath = Get-RelativePath  
 $LogFile = $RelativePath + $LogFile  
 $MonitoringHost = "http://" + $MonitoringHost + ":9801/MDTMonitorData/Computers"  
 New-Logs -LogFile $LogFile  
 $ImagedSystems = Get-MDTData -MonitoringHost $MonitoringHost | Select Name, DeploymentStatus, PercentComplete, Warnings, Errors, StartTime, EndTime | Sort -Property Name  
 #Remove systems from logfile that do not exist in MDT Monitoring  
 Remove-OldSystems -Systems $ImagedSystems  
 #Add new systems to the logfile and report their status  
 Add-NewSystems -Systems $ImagedSystems  
   
 #Cleanup Local Variables  
 Remove-Variable -Name ImagedSystems -Scope Local -Force  
 Remove-Variable -Name RelativePath -Scope Local -Force  
   

18 February 2016

PowerShell MSI Uninstaller By Application Name

This is now an old version. The new version is now located in this blog posting

Here is a function that will uninstall an MSI installed application by the name of the app. You do not need to input the entire name either. For instance, say you are uninstalling all previous versions of Adobe Reader. Adobe Reader is always labeled Adobe Reader X, Adobe Reader XI, and so forth. This script allows you to do this without having to find out every version that is installed throughout a network and then enter an uninstaller line for each version. You just need to enter Adobe Reader as the application name and the desired switches. It will then search the name fields in the 32 and 64 bit uninstall registry keys to find the associated GUID. Finally, it will execute an msiexec.exe /x {GUID} to uninstall that version.

NOTE: I used Sapien's PowerShell Studio to write this script that significantly simplified the process and made it a snap to write. I highly recommend this product for all PowerShell scripters!

You can download the script from here.

 <#       
      .NOTES  
      ===========================================================================  
       Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
       Created on:       2/18/2016 12:32 PM  
       Created by:       Mick Pletcher  
       Organization:         
       Filename:          
      ===========================================================================  
      .DESCRIPTION  
           Here is a function that will uninstall an MSI installed application by  
           the name of the app. You do not need to input the entire name either.   
           For instance, say you are uninstalling all previous versions of Adobe   
           Reader. Adobe Reader is always labeled Adobe Reader X, Adobe Reader XI,  
           and so forth. You just need to enter Adobe Reader as the application   
           name and the desired switches. It will then search the name fields in   
           the 32 and 64 bit uninstall registry keys to find the associated GUID.   
           Finally, it will execute an msiexec.exe /x {GUID} to uninstall that   
           version.   
 #>  
   
 Function Uninstall-MSIByName {  
    <#   
    .SYNOPSIS   
       Uninstall-MSIByName   
    .DESCRIPTION   
       Uninstalls an MSI application using the MSI file   
    .EXAMPLE   
       Uninstall-MSIByName -ApplicationName "Adobe Reader" -Switches "/qb- /norestart"   
    #>       
        
      Param ([String]$ApplicationName,  
           [String]$Switches)  
        
      #Declare Local Variables   
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name Executable -Scope Local -Force  
      Set-Variable -Name Key -Scope Local -Force  
      Set-Variable -Name KeyName -Scope Local -Force  
      Set-Variable -Name Parameters -Scope Local -Force  
      Set-Variable -Name SearchName -Scope Local -Force  
      Set-Variable -Name TempKey -Scope Local -Force  
      Set-Variable -Name Uninstall -Scope Local -Force  
        
      $Uninstall = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $Uninstall += Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $SearchName = "*" + $ApplicationName + "*"  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      Foreach ($Key in $Uninstall) {  
           $TempKey = $Key.Name -split "\\"  
           If ($TempKey[002] -eq "Microsoft") {  
                $Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           } else {  
                $Key = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           }  
           If ((Test-Path $Key) -eq $true) {  
                $KeyName = Get-ItemProperty -Path $Key  
                If ($KeyName.DisplayName -like $SearchName) {  
                     $TempKey = $KeyName.UninstallString -split " "  
                     If ($TempKey[0] -eq "MsiExec.exe") {  
                          Write-Host "Uninstall"$KeyName.DisplayName"....." -NoNewline  
                          $Parameters = "/x " + $KeyName.PSChildName + [char]32 + $Switches  
                          $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
                          If (($ErrCode -eq 0) -or ($ErrCode -eq 3010) -or ($ErrCode -eq 1605)) {  
                               Write-Host "Success" -ForegroundColor Yellow  
                          } else {  
                               Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
                          }  
                     }  
                }  
           }  
      }  
        
      #Cleanup Local Variables   
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name Executable -Scope Local -Force  
      Remove-Variable -Name Key -Scope Local -Force  
      Remove-Variable -Name KeyName -Scope Local -Force  
      Remove-Variable -Name Parameters -Scope Local -Force  
      Remove-Variable -Name SearchName -Scope Local -Force  
      Remove-Variable -Name TempKey -Scope Local -Force  
      Remove-Variable -Name Uninstall -Scope Local -Force  
        
 }  
   
 Uninstall-MSIByName -ApplicationName "Adobe Reader" -Switches "/qb- /norestart"