30 October 2019

End User Reboot Management

I have wrestled with the issue of managing mandatory reboots for quite a while. Back before laptops were introduced into the environment, we used an SCCM package that triggered a reboot once a week. Today, with the majority of systems being laptops and tablets, it is not so easy to implement a reboot. The problem came when the package ran, and a system was offline. It would not get done. The second issue comes from laptops being closed instead of shutdown. A reboot rarely happens.

I first started out by creating a complex SCCM application that used a custom detection method. It was not the most successful. After that, I have worked on and off for months until I finally figured out how to implement this. The first thing that needs to be done is to create an SCCM package that reboots the machine. The package is only advertised to it and not mandatory. The package program executes a PowerShell one-liner that exits with a return code of 3010. The after running is set to Configuration Manager restarts computers as shown below.




The PowerShell one-liner is as follows:

 powershell.exe -executionpolicy bypass -command "&{Exit 3010}"  

Under User Experience, both software installation and system restart are checked. Since this is an advertisement, allow users to run the program is checked by default.



Under Distribution Points, I set both to Run program from distribution point, so there is no need to download anything to the machine as it is a PowerShell one-liner that is executed.



We came up with the following list of requirements needed for the script. The requirements are:

  • The system has not been rebooted in X number of days. X is determined by the company and is implemented by populating the parameter $MaxDays with an integer value.
  • The ClientState does not equal zero. ClientState contains integer values from zero to fifteen. Zero means no pending reboots. Any other value from one to fifteen means the system is pending a reboot. Thanks to Eswar Koneti for that info! 
  • LastBootUpTime0 is not a null value. If there is a null value, it means the system has been offline for quite a while. 

The PowerShell script queries the SCCM SQL database for a list of systems. The above requirements were all implemented within the SQL query that PowerShell executes. Once the query is complete, PowerShell will go through the query results to generate an object for each machine. The object includes the computer name, the last boot-up time, if the system is reboot pending, and if the system is online. It then runs the function that will trigger the reboot.

In the function, I have it do a more thorough check if the system is online. It then reaches out to the machine with a WMI query to verify the system has not been rebooted in X amount of days. This is in case a hardware inventory has not been executed to update the SCCM SQL database, so a reboot is not run twice on a machine. It then retrieves the information of the above advertisement from SCCM. The script will change the advertisement from advertise to mandatory for that specific machine. This is necessary for the script to initiate the reboot package. It then executes the package and returns the package from mandatory to advertised.

The object containing the system information is then added to an array for reporting purposes. The report is printed to the screen at the end. If no machines were needing a reboot, then the script exits with an exit code of 1. The reason for this is in case the script is implemented in SMA/Orchestrator/Azure Automation, the Link can be specified not to continue on to send out an email if an error code of 1 was returned. If there were machines in a list, then the Write-Output makes that output list available to the next activity, such as email. I have ours set up to send an email out to the help desk with a list of machines that got the reboot implemented on in case users call in asking why.

As far as using the script, you will need to populate the parameter fields. A detailed description is in the script of each parameter.

This can be implemented in several ways. I implemented it using Orchestrator in a runbook. It can also be implemented by a manual execution of the PowerShell script. You could also set it up to run from a scheduled task. I have it execute every morning at 9 am.

The last thing that needs to be implemented is the computer restart settings. I created a collection named Reboot Compliance that I deployed the client settings and the package to. That way when I added machines to the collection, they got both the settings and package. These are the settings I use. This gives a user 8 hours to reboot with the final hour in which the reboot message will not go away at the bottom left of the screen.



NOTE: This script needs to be executed on a system that has the sqlserver PowerShell module.

NOTE: You will probably want to execute this script in a test mode. I would highly recommend commenting out lines 79 through 106 for this. The script will then only obtain a list of machines and not trigger reboots. 

You can download the script from my GitHub site.

 <#  
      .SYNOPSIS  
           LastBootup  
        
      .DESCRIPTION  
           This script will query the SCCM SQL database for a list of machines that have not been rebooted for more than ten days  
        
      .PARAMETER Collection  
           Name of the collection to query. PowerShell will find the SQL table name from the collection name
        
      .PARAMETER SQLServer  
           Name of the SQL server that contains the SCCM database
        
      .PARAMETER SQLDatabase  
           Name of the SCCM SQL database  
        
      .PARAMETER DeploymentName  
           Name of deployment of the SCCM reboot package
        
      .PARAMETER MaxDays  
           If system has not rebooted for this number of days, then add to $Report  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/22/2019 10:21 AM  
           Created by:       Mick Pletcher  
           Filename:         RebootManagement.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase,  
      [ValidateNotNullOrEmpty()]  
      [string]$DeploymentName,  
      [ValidateNotNullOrEmpty()]  
      [int]$MaxDays  
 )  
   
 function Initialize-Reboot {  
 <#  
      .SYNOPSIS  
           Initialize Timed Reboot  
        
      .DESCRIPTION  
           This will change the reboot package from advertised to mandatory in order to initiate the reboot. It will then change package back to advertised once the package has been executed.  
        
      .PARAMETER Object  
           A description of the Object parameter.  
        
      .EXAMPLE  
           PS C:\> Initialize-Reboot  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           $Object  
      )  
        
      If ((Test-Connection -ComputerName $Object.Name -Quiet) -eq $true) {  
           #Query the remote system to make sure it has not been rebooted since the LastBootUpTime was updated to SCCM  
           If ((New-TimeSpan -Start ([Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class win32_operatingsystem -ComputerName $Object.Name).LastBootUpTime))).Days -gt $MaxDays) {  
                #Package and Advertisement IDs of the SCCM package  
                $Advertisement = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {$_.PKG_Name -eq $DeploymentName} | Select-Object -Property PKG_PackageID, ADV_AdvertisementID  
                #Continue to next array item if the advertisement is null, meaning the reboot application was not advertised to the system  
                If ($Advertisement -ne $null) {  
                     #Schedule IS of the SCCM package deployment  
                     $ScheduleID = Get-WmiObject -Namespace "root\ccm\scheduler" -Class "CCM_Scheduler_History" -ComputerName $Object.Name | Where-Object {  
                          $_.ScheduleID -like "*$($Advertisement.PKG_PackageID)*"  
                     } | Select-Object -ExpandProperty ScheduleID  
                     #Retrieve advertisement policy  
                     $Policy = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {  
                          $_.PKG_Name -eq $DeploymentName  
                     }  
                     #Change advertisement policy to mandatory so the package can be executed  
                     If ($Policy.ADV_MandatoryAssignments -eq $false) {  
                          $Policy.ADV_MandatoryAssignments = $true  
                          $Policy.Put() | Out-Null  
                     }  
                     #Execute the advertisement  
                     Invoke-WmiMethod -Namespace "root\ccm" -Class "SMS_Client" -Name "TriggerSchedule" -ArgumentList $ScheduleID -ComputerName $Object.Name  
                     #Wait one second to give time for the package to initiate  
                     Start-Sleep -Seconds 1  
                     #Retrieve advertisement policy  
                     $Policy = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {  
                          $_.PKG_Name -eq $DeploymentName  
                     }  
                     #Remove the mandatory assignment from the package so this is not rerun  
                     If ($Policy.ADV_MandatoryAssignments -eq $true) {  
                          $Policy.ADV_MandatoryAssignments = $false  
                          $Policy.Put() | Out-Null  
                     }  
                }  
           }  
      } else {  
           Return $null  
      }  
      Return $object  
 }  
   
 #Get the table name from the $Collection value  
 $TableName = 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT ResultTableName FROM dbo.v_Collections WHERE CollectionName = ' + [char]39 + $Collection + [char]39)).ResultTableName)  
 #Query active systems in the above table to list the computer name and the last boot up time  
 $Query = 'SELECT Name, LastBootUpTime0, ClientState FROM dbo.v_GS_OPERATING_SYSTEM INNER JOIN' + [char]32 + $TableName + [char]32 + 'ON dbo.v_GS_OPERATING_SYSTEM.ResourceID =' + [char]32 + $TableName + '.MachineID WHERE ((((DATEDIFF(DAY,LastBootUpTime0,GETDATE())) >' + [char]32 + $MaxDays + ') OR ClientState <> 0) AND LastBootUpTime0 IS NOT NULL)'  
 #Query SCCM SQL database  
 $List = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query $Query  
 #Create report array to contain list of all systems that have not rebooted for $Threshold days  
 $Report = @()  
 #Check if list is null. If so, exit with error code 1 so if script is used in Orchestrator or SMA, it can be set to not proceed with email.  
 If ($List -ne '') {  
      #Get list of machines that are exceed number of days allowed without rebooting. This is also used as a report if desired to be emailed.  
      $List | ForEach-Object {  
           If ($_.ClientState -ne 0) {  
                $PendingReboot = $true  
           } else {  
                $PendingReboot = $false  
           }  
           If ((Test-Connection -ComputerName $_.Name -Count 1 -Quiet) -eq $true) {  
                $Online = $true  
           } else {  
                $Online = $false  
           }  
           #Create new object  
           $object = New-Object -TypeName System.Management.Automation.PSObject  
           $object | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name  
           $object | Add-Member -MemberType NoteProperty -Name LastBootUpTime -Value $_.LastBootUpTime0  
           $object | Add-Member -MemberType NoteProperty -Name PendingReboot -Value $PendingReboot  
           $object | Add-Member -MemberType NoteProperty -Name Online -Value $Online  
           If ($object.Online -eq $true) {  
                $obj = Initialize-Reboot -Object $object  
           }  
           #If the reboot was initiated, then add to $Report  
           If ($obj -ne $null) {  
                $Report += $obj  
           }  
      }  
      If ($Report -eq $null) {  
           #This exit code is used for signaling to a link in Orchestrator or Azure Automation to not proceed to the next activity  
           Write-Host "Null"  
           Exit 1  
      } else {  
           Write-Output $Report | Sort-Object LastBootUpTime, Name  
      }  
 } else {  
      #This exit code is used for signaling to a link in Orchestrator or Azure Automation to not proceed to the next activity  
      Write-Host "Null"  
      Exit 1  
 }  
   

0 comments:

Post a Comment