21 February 2020

Upgrading to both Windows 10 1903 and 1909 with Configuration Manager

The time has come to do another creator update to the corporate systems. We skipped the 1903 upgrade because of the Windows 7 to Windows 10 deployment that completed just after the release of 1903. It was too soon to start another creator update project, so we waited until 1909. One of the new features we are wanting is the reserved space that was introduced in 1903. This reserves seven gigabytes of data for future updates and upgrades. The feature is set by default when an OS is installed during a build. It has to be turned on after the OS is deployed during an upgrade via a registry key. The problem is that once the key is set to one, the reserved space does not take effect until the next creator update. Another major issue I had was that the task sequence can only execute one OS upgrade at a time. This is because once the upgraded OS is installed, it will continue running the same task sequence during the upgrade process. The screenshot below shows where it executes the rest of the task sequence. There is no way to circumvent this process. If it continues with the following OS install, the task sequence errors out.


To get both OSs installed within the same task sequence, I set up the EndTS variable that stops the second install if the first one has executed. This means the same task sequence will need to be run twice to install both versions. The trick is to set the Rerun behavior to Rerun if succeeded on the previous attempt, as shown below. This allows for the task sequence to be executed a second time. It will skip over the first upgrade, 1903, and go right to 1909. If 1909 is already installed and the task sequence reruns, the top-level folder sequence will prevent it from executing any other tasks. The other trick to stop this from running again after both installs are complete is to have the collection filter out systems that have 1909 installed.



The first thing you want to do is to import the desired Windows 10 versions as an Operating System Upgrade in the configuration manager. Once the OS Upgrades are imported, one thing you might want to consider is copying the content to a distribution share so that you can access the package in remote offices in the event of an OS upgrade failure, as shown below. This will allow you to be able to manually execute the install on a failed machine remotely for further troubleshooting.


Next will be creating the task sequence. You will want to create a new task sequence to upgrade an operating system from an upgrade package.


Stepping through the process of creating this task sequence is self-explanatory. I am going to include screenshots of each screen.










Here is what the task sequence looks like after I got it set up and added all of the necessary features. Your task sequence will likely be somewhat different, as every company has a unique configuration.




This is the conditional WMI query I use for the top-level Upgrade folder. This is so if the latest OS version is already installed, then it will skip over all other task sequences.


The next thing I have it do is to set the task sequence variable EndTS to 0. This variable is used to skip over the second OS upgrade if the first one runs. It does this by including a task sequence under the first OS upgrade that sets the variable to one. When the second OS upgrade task sequence is executed, there is a rule to only run if the EndTS equals zero.



The PowerShell 7 upgrade is next and simply run the PowerShell 7 application installation.



Here are the specs for the Check Readiness for Upgrade:



One of the specifications we have is that laptops are left in the office and docked. The following PowerShell script does just that as a condition for continuing the task sequence:



I had a lot of problems getting the cleanmgr.exe to work and was never successful. I saw on several sites where others had the same issue. I ran across Fabian Castagna's PowerShell script that does much of the same as cleanmgr.exe does. I could never get it to delete the windows.old folder, but I have a solution further on in this blog.


For the first upgrade, Upgrade the Operating System to 1903, I used the WMI filter so that it will only install it if the OS is older than 1903.



The next step is installing the OS. This step readies the system for the actual upgrade that takes place in the proceeding reboot.



The reboot step will restart the system, at which point the actual upgrade will take place. The OS does not load in this step. I have a 60 second wait time along with a message in the event a user is on the system at 1:00 am when the task sequence starts.


The next sequence is to Enable Reserved Storage. This is where a registry key is set that enables this feature upon the next upgrade.

REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager /v ShippedWithReserves /t REG_DWORD /d 1 /f


The next sequence reruns the disk cleanup script.


Next comes deleting the Windows.old directory. I worked on this for quite a while. It is not easy to do if you can't automate cleanmgr.exe. It ended up being a hit or miss thing. Sometime the script I had written would work, other times, it either was unsuccessful, or it crashed the OS when it deleted the files still being used as a service. What I came up with was to remove the entire folder at once within WinPE. The OS does not load, so no files are in use, thereby allowing for the complete deletion.

The first thing is to boot into WinPE.


The next step is to delete the folder using the following PowerShell code:


 $Drive = Get-PSDrive | Where-Object {($_.Provider -like 'Microsoft.PowerShell.Core\FileSystem*') -and ($_.Description -like 'Windows*')}  
 If (Test-Path ($Drive.Root + 'Windows.old')) {  
      $Argument = '/c RMDIR.exe' + [char]32 + '/S /Q' + [char]32 + $Drive.Root + 'Windows.old'  
      $ExitCode = (Start-Process -FilePath ($env:windir + '\system32\cmd.exe') -ArgumentList $Argument -PassThru -WindowStyle Minimized -Wait).ExitCode  
 }  



Next comes a reboot using the currently installed OS.



Now comes setting the EndTS variable to 1, so the following OS upgrade does not execute.


The next step is the 1909 upgrade. The only differences in this compared to the 1903 update are:

  • The Task sequence variable will be a condition set for the top-level folder of this task sequence
  • Enable Reserved Storage task sequence is not needed
  • End Task Sequence is also not required. 
Here are the conditions to run this set of task sequences that are set on the Upgrade the Operating System to the 1909 folder. We don't want this to execute unless 1903 is installed, and we don't want it to run if the task sequence just installed 1903. 




02 January 2020

Microsoft Endpoint Manager Configuration Manager PowerShell Upgrade Script

With the advent of Microsoft Endpoint Manager Configuration Manager 1910, I started researching the upgrade requirements. I happened to run into a great blog post by fellow MVP Martin Bengtsson on what should be done before the upgrade takes place. That got me to thinking that most of the tasks he listed can be automated with PowerShell.

The script below will automate the following tasks from his list of prerequisites:

  • Backs up the cd.latest directory to the specified UNC path
  • Disables the following maintenance tasks
    • Backup site server
    • Delete aged client operations
    • Delete aged discovery data
    • Delete aged log data
  • Checks if the server is fully patched
  • Backs up the Configuration Manager SQL database
For the script to complete all of these tasks, the following PowerShell modules are required:
  • SQLServer
  • ConfigurationManager.psd1
  • PSWindowsUpdate
The SQLServer and PSWindowsUpdate can both be installed from the PowerShell Gallery. The ConfigurationManager.psd1 resides on the Configuration Manager server. The script needs to be run on that server, along with the other two modules being installed. The script will automatically locate the ConfigMgr module. You will also need to have a UNC path that will house the cd.latest directory backup and the SQL database backup. 

When executing this script, I recommend doing so using PowerShell ISE. The reason for this is because as you can see in the screenshot below, ISE will give you a status on the SQL server backup so you do not think the script might be locked up. 



When the server has passed all prereqs, the output will look like this:



NOTE: The script is intended to be executed on the Configuration Manager server. 

Here is the script. It can also be downloaded from my GitHub site


 <#  
      .SYNOPSIS  
           Configuration Manager Upgrade Prerequisite  
        
      .DESCRIPTION  
           This script will prepare configuration manager for the upgrade to the newest version. This is based off of Martin Bengtsson's Updating MEMCM (Microsoft Endpoint Manager Configuration Manager) to version 1910 on Christmas Eve. https://www.imab.dk/early-christmas-present-updating-memcm-microsoft-endpoint-manager-configuration-manager-to-version-1910-on-christmas-eve/  
        
      .PARAMETER MEMCMModule  
           Name of the ConfigMgr PowerShell Module  
        
      .PARAMETER MEMCMServer  
           FQDN Name of the Configuration Manager server  
        
      .PARAMETER MEMCMSiteDescription  
           Arbitrary description of the configuration manager server  
        
      .PARAMETER SiteCode  
           Three letter ConfigMgr site code  
        
      .PARAMETER BackupLocation  
           Location for SQL and SCCM backups  
        
      .PARAMETER SQLServer  
           FQDN of the SQL server  
        
      .PARAMETER SQLDatabase  
           Name of the SQL database  
        
      .PARAMETER SQLModuleName  
           Name of the SQL PowerShell Module  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       12/26/2019 12:22 PM  
           Created by:       Mick Pletcher  
           Filename:         ConfigMgrUpgrade.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$MEMCMModule = 'ConfigurationManager.psd1',  
      [ValidateNotNullOrEmpty()]  
      [string]$MEMCMServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$MEMCMSiteDescription = 'MEMCM Site Server',  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode,  
      [ValidateNotNullOrEmpty()]  
      [string]$BackupLocation,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLModuleName = 'SQLServer'  
 )  
   
 #Import SQL Server PowerShell Module  
 If ((Get-Module -Name ((Import-Module -Name $SQLModuleName -ErrorAction SilentlyContinue -Force -PassThru).Name)) -eq $null) {  
      #Install module if it does not exist  
      Install-Module -Name $SQLModuleName -Confirm:$false -Force  
      #Verify module got installed. Exit the script if it failed  
      If ((Get-Module -Name ((Import-Module -Name $SQLModuleName -ErrorAction SilentlyContinue -Force -PassThru).Name)) -eq $null) {  
           Write-Host 'Failed'  
           Exit 2  
      } else {  
           Import-Module -Name $SQLModuleName -ErrorAction SilentlyContinue -Force  
      }  
 }  
 If ((Get-Module).Name -contains $SQLModuleName) {  
   Write-Host ('Successfully imported' + [char]32 + $SQLModuleName + [char]32 + 'PowerShell module')  
 } else {  
   Write-Host ('Failed to load' + [char]32 + $SQLModuleName + [char]32 + 'PowerShell module')  
 }  
 #Import ConfigMgr PowerShell Module  
 $Module = (Get-ChildItem ((Get-WmiObject -Class 'sms_site' -Namespace 'Root\sms\site_BNA').InstallDir) -Filter $MEMCMModule -Recurse -ErrorAction SilentlyContinue)  
 Import-Module -Name $Module[0].FullName -Force  
 If ((Get-Module).Name -contains $Module[0].BaseName) {  
   Write-Host ('Successfully imported' + [char]32 + $Module[0].BaseName + [char]32 + 'PowerShell module')  
 } else {  
   Write-Host ('Failed to load' + [char]32 + $MEMCMModule + [char]32 + 'PowerShell module')  
 }  
 #Import PSWindowsUpdate PowerShell Module  
 Import-Module -Name PSWindowsUpdate -Force  
 If ((Get-Module).Name -contains 'PSWindowsUpdate') {  
   Write-host 'Successfully imported PSWindowsUpdate PowerShell module'  
 }  
 #Map ConfigMgr Drive  
 If ((Test-Path ($SiteCode + ':')) -eq $false) {  
      New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $MEMCMServer -Description $MEMCMSiteDescription | Out-Null  
 }  
 #Change directory to ConfigMgr drive  
 Set-Location -Path ($SiteCode + ':')  
 #Backup cd.latest directory  
 $DIR = Get-ChildItem -Path (Get-WmiObject -Class 'sms_site' -Namespace 'Root\sms\site_BNA').InstallDir -Filter 'cd.latest' -Directory -Recurse -ErrorAction SilentlyContinue  
 robocopy $DIR.FullName ($BackupLocation + '\cd.latest') /e /eta ('/log:' + $BackupLocation + '\Robocopy.log')  
 If ($LastExitCode -le 7) {  
   Write-Host 'cd.latest backup succeeded' -ForegroundColor Yellow  
 } else {  
   Write-Host 'cd.latest backup failed' -ForegroundColor Red  
 }  
 #Disable Maintenance tasks for upgrade  
 $Enabled = $false  
 Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -like 'backup* site server'} | Set-CMSiteMaintenanceTask -Enabled $Enabled  
 If (((Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -like 'backup* site server'}).Enabled) -eq $false) {  
      $BackupSiteServer = $true  
 } else {  
      $BackupSiteServer = $false  
 }  
 If ($BackupSiteServer -eq $true) {  
      Write-Host 'Backup site server is disabled' -ForegroundColor Yellow  
 } else {  
      Write-Host 'Backup site server is still enabled' -ForegroundColor Red  
 }  
 Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged client operations'} | Set-CMSiteMaintenanceTask -Enabled $Enabled  
 If (((Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged client operations'}).Enabled) -eq $false) {  
      $AgedClientOperations = $true  
 } else {  
      $AgedClientOperations = $false  
 }  
 If ($AgedClientOperations -eq $true) {  
      Write-Host 'Delete aged client operations is disabled' -ForegroundColor Yellow  
 } else {  
      Write-Host 'Delete aged client operations is still enabled' -ForegroundColor Red  
 }  
 Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged discovery data'} | Set-CMSiteMaintenanceTask -Enabled $Enabled  
 If (((Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged discovery data'}).Enabled) -eq $false) {  
      $AgedDiscoveryData = $true  
 } else {  
      $AgedDiscoveryData = $false  
 }  
 If ($AgedDiscoveryData -eq $true) {  
      Write-Host 'Delete aged discovery data is disabled' -ForegroundColor Yellow  
 } else {  
      Write-Host 'Delete aged discovery data is still enabled' -ForegroundColor Red  
 }  
 Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged log data'} | Set-CMSiteMaintenanceTask -Enabled $Enabled  
 If (((Get-CMSiteMaintenanceTask | Where-Object {$_.ItemName -eq 'delete aged log data'}).Enabled) -eq $false) {  
      $AgedLogData = $true  
 } else {  
      $AgedLogData = $false  
 }  
 If ($AgedLogData -eq $true) {  
      Write-Host 'Delete aged log data is disabled' -ForegroundColor Yellow  
 } else {  
      Write-Host 'Delete aged log data is still enabled' -ForegroundColor Red  
 }  
 #Verify all windows updates are applied  
 If ((Get-WindowsUpdate -WindowsUpdate) -eq $null) {  
      $AppliedUpdates = $true  
 } else {  
      $AppliedUpdates = $false  
 }  
 If ($AppliedUpdates -eq $true) {  
      Write-Host ((Get-WmiObject -Class Win32_OperatingSystem).Caption + [char]32 + 'is fully patched') -ForegroundColor Yellow  
 } else {  
      Write-Host ((Get-WmiObject -Class Win32_OperatingSystem).Caption + [char]32 + 'is not fully patched') -ForegroundColor Red  
 }  
   
 #Backup the Configuration Manager SQL Database  
 Backup-SqlDatabase -ServerInstance $SQLServer -Database $SQLDatabase -BackupFile ($BackupLocation + '\CM_SQL_Backup.bak') -Checksum  
 $Verbose = $($Verbose = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('RESTORE VERIFYONLY FROM DISK = N' + [char]39 + $BackupLocation + '\CM_SQL_Backup.bak' + [char]39) -QueryTimeout 0 -Verbose) 4>&1  
 If ($Verbose -like '*The backup set on file 1 is valid*') {  
      $SQLBackup = $true  
 } else {  
      $SQLBackup = $false  
 }  
 #Output the results  
 If ($SQLBackup -eq $true) {  
      Write-Host 'SQL backup was successful' -ForegroundColor Yellow  
 } else {  
      Write-Host 'SQL backup failed' -ForegroundColor Red  
 }  
   

31 December 2019

Install Configuration Manager PowerShell Module

I wanted to install the Configuration Manager PowerShell module on my admin machine. After investigating a little, the module does not exist in the Microsoft PowerShell gallery.

The PowerShell script below will create the module directory under the WindowsPowerShell\module directory within %PROGRAMFILES%. It will then copy over all of the necessary files. After copying the files, it will import the module and then verify if the import was successful. Of course, when you want to actually use the module, you will need to connect to the ConfigMgr server using the following cmdlet and then change the site location to the $SiteCode drive:

  • New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $CMServer -Description $CMSiteDescription | Out-Null
This video shows the script running through the installation and verifying the install:


You will need to locate where the module exists and populate the location in the $ModuleSource parameter. On my ConfigMgr server, it was located at %PROGRAMFILES%\Microsoft Configuration Manager\AdminConsole\bin.

Here is the script below. You can download it from my GitHub site located here


 <#  
      .SYNOPSIS  
           Configuration Manager PowerShell Module  
        
      .DESCRIPTION  
           This will copy the files needed to the computer this script is being executed on. It must be run using an AD account that has priviledges to both the directory on the ConfigMgr server and to the program files directory on the local computer.  
        
      .PARAMETER ModuleSource  
           UNC path containing the Configuration Manager PowerShell module  
        
      .PARAMETER ModuleDirectoryName  
           Name of the directory under the PowerShell Modules directory where the module is copied to  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       12/27/2019 12:37 PM  
           Created by:       Mick Pletcher  
           Filename:         InstallConfigMgrModule.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$ModuleSource,  
      [ValidateNotNull()]  
      [ValidateNotNullOrEmpty()]  
      [string]$ModuleDirectoryName = 'ConfigurationManager'  
 )  
   
 #Location where to copy the module to  
 $ModuleDestination = $env:ProgramFiles + '\WindowsPowerShell\Modules\' + $ModuleDirectoryName  
 #Remove the module from memory if it has already been imported  
 Remove-Module -Name ConfigurationManager -Force -ErrorAction SilentlyContinue  
 #Remove ConfigurationManager directory if it already exists  
 Remove-item -Path $ModuleDestination -Recurse -Force  
 #Create the directories for the PowerShell module  
 New-Item -Path $ModuleDestination -ItemType Directory -Force | Out-Null  
 New-Item -Path ($ModuleDestination + '\en-US') -ItemType Directory -Force | Out-Null  
 #Copy all necessary files for the Configuration Manager PowerShell Module  
 Get-ChildItem -Path $ModuleSource -Filter 'adminui.ps.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'adminui.wqlqueryengine.dll' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'Microsoft.ConfigurationManagement.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter '*.ps1xml' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'AdminUI.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'ConfigurationManager.psd1' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'Dcm*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'Microsoft.Diagnostics.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'Microsoft.ConfigurationManagement.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Get-ChildItem -Path $ModuleSource -Filter 'Microsoft.ConfigurationManager.*' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination $ModuleDestination; If ((Test-Path ($ModuleDestination + '\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 #Copy all help files to the module folder  
 Get-ChildItem -Path ($ModuleSource + '\en-US') -Filter '*.xml' | ForEach-Object {Write-Host ('Copying ' + $_.Name + '.....') -NoNewline; Copy-Item -Path $_.FullName -Destination ($ModuleDestination + '\en-US'); If ((Test-Path ($ModuleDestination + '\en-US\' + $_.Name)) -eq $true) {Write-Host 'Success' -ForegroundColor Yellow} else {Write-Host 'Failed' -ForegroundColor Red}}  
 Import-Module -Name $ModuleDirectoryName  
 Write-Host  
 If (Get-Module -ListAvailable -Name $ModuleDirectoryName) {  
      Remove-Module -Name $ModuleDirectoryName -Force  
      Write-Host ($ModuleDirectoryName + [char]32 + 'PowerShell module installed successfully') -ForegroundColor Yellow  
 } else {  
      Write-Host ($ModuleDirectoryName + [char]32 + 'PowerShell module installation failed') -ForegroundColor Red  
 }  
   

26 December 2019

WMI Query for Dell Manufacture and First Power On Date

A recent project of mine was to obtain the Dell manufacture and ownership dates from all systems for depreciation and lifecycle purposes. This information is not readily available in SCCM and on the local machines. Luckily, the newest version of Dell Command | Configure now includes the manufacture date and first power on date. The first power on date, as referenced in DCC, is also the Ownership Date in the BIOS, as shown below.


To make this easy, I decided to write a PowerShell script that would utilize cctk.exe to query the BIOS for this information and then populate it in the WMI since the info never changes. The main reason I decided on WMI is that the Get-WMIObject has the -Computer allowing the information to be queried remotely. This information never changes, so it does not need to be populated in Configuration Manager on a reoccurring basis. I have another PowerShell script that I use with Orchestrator to generate an Excel spreadsheet report that contains this information from each system. The PowerShell script below was deployed out to all machines and then put in the build task sequence.

NOTE: In order for this script to work, the latest version of Dell Command | Configure will need to be installed. The version I have deployed out is 4.2.1. 

Also, thanks to Adam Bertram for the Invoke-Process used in this script.


You can download the script from my GitHub site located here

 <#  
      .SYNOPSIS  
           Dell Manufacture Information  
        
      .DESCRIPTION  
           This script is deployed out to machines so the manufacture and ownership dates are populated as a WMI entry. This allows for easy query of this information for depreciation and lifecycle purposes.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       12/23/2019 8:31 AM  
           Created by:       Mick Pletcher  
           Filename:         SystemInformationWMI.ps1  
           ===========================================================================  
 #>  
   
 function Invoke-Process {  
      [CmdletBinding(SupportsShouldProcess)]  
      param  
      (  
           [Parameter(Mandatory)]  
           [ValidateNotNullOrEmpty()]  
           [string]$FilePath,  
           [Parameter()]  
           [ValidateNotNullOrEmpty()]  
           [string]$ArgumentList  
      )  
        
      $ErrorActionPreference = 'Stop'  
        
      try {  
           $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)"  
           $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)"  
             
           $startProcessParams = @{  
                FilePath                     = $FilePath  
                ArgumentList                = $ArgumentList  
                RedirectStandardError      = $stdErrTempFile  
                RedirectStandardOutput  = $stdOutTempFile  
                Wait                          = $true;  
                PassThru                     = $true;  
                NoNewWindow                  = $true;  
           }  
           if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {  
                $cmd = Start-Process @startProcessParams  
                $cmdOutput = Get-Content -Path $stdOutTempFile -Raw  
                $cmdError = Get-Content -Path $stdErrTempFile -Raw  
                if ($cmd.ExitCode -ne 0) {  
                     if ($cmdError) {  
                          throw $cmdError.Trim()  
                     }  
                     if ($cmdOutput) {  
                          throw $cmdOutput.Trim()  
                     }  
                } else {  
                     if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {  
                          Write-Output -InputObject $cmdOutput  
                     }  
                }  
           }  
      } catch {  
           $PSCmdlet.ThrowTerminatingError($_)  
      } finally {  
           Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore  
      }  
 }  
   
 [string]$MFGDate = (((Invoke-Process -FilePath (${env:ProgramFiles(x86)} + '\Dell\Command Configure\X86_64\cctk.exe') -ArgumentList '--mfgdate').split('=')[1]).Trim())  
 [datetime]$MFGDate = [datetime]::ParseExact($MFGDate, 'yyyymmdd', $null)  
 [string]$FirstPowerOnDate = (((Invoke-Process -FilePath (${env:ProgramFiles(x86)} + '\Dell\Command Configure\X86_64\cctk.exe') -ArgumentList '--firstpowerondate').split('=')[1]).Trim())  
 [datetime]$FirstPowerOnDate = [datetime]::ParseExact($FirstPowerOnDate, 'yyyymmdd', $null)  
 [string]$SVCTag = (Invoke-Process -FilePath (${env:ProgramFiles(x86)} + '\Dell\Command Configure\X86_64\cctk.exe') -ArgumentList '--svctag').split('=')[1]  
 $newClass = New-Object System.Management.ManagementClass ('root\cimv2', [String]::Empty, $null)  
 $newClass["__CLASS"] = 'Dell_System_Info'  
 $newClass.Qualifiers.Add("Static", $true)  
 $newClass.Properties.Add("ServiceTag", [System.Management.CimType]::String, $false)  
 $newClass.Properties["ServiceTag"].Qualifiers.Add("Key", $true)  
 $newClass.Properties.Add("ManufactureDate", [System.Management.CimType]::DateTime, $false)  
 $newClass.Properties["ManufactureDate"].Qualifiers.Add("Key", $true)  
 $newClass.Properties.Add("FirstPowerOnDate", [System.Management.CimType]::DateTime, $false)  
 $newClass.Properties["FirstPowerOnDate"].Qualifiers.Add("Key", $true)  
 $newClass.Properties.Add("SystemName", [System.Management.CimType]::String, $false)  
 $newClass.Properties["SystemName"].Qualifiers.Add("Key", $true)  
 $newClass.Put()  
 $Properties = @{  
      ServiceTag = $SVCTag.Trim();`  
      ManufactureDate = $MFGDate;`  
      FirstPowerOnDate = $FirstPowerOnDate;`  
      SystemName = $env:COMPUTERNAME  
 }  
 $Properties  
 Get-CimInstance -ClassName 'Dell_System_Info' -Namespace 'root\cimv2' | Remove-CimInstance  
 New-CimInstance -ClassName 'Dell_System_Info' -Namespace 'root\cimv2' -Property $Properties  
   

05 December 2019

Using PowerShell for SQL Backup Verification

Earlier this year, we had a non-critical SQL server to crash. Come to find out, the backups were successful every night, but the data in the backup was corrupt. Needless to say, the server had to be recreated from scratch. Thankfully it was a non-critical server.

We wanted a way to automatically verify SQL backups are valid. Using PowerShell, I wrote the following script that runs RESTORE VERIFYONLY against the latest backup contained in the designated directory. The script looks for the output to be "The backup set on file 1 is valid". If it does not return that, then the script will exit with an error code 1. I am using this script in Microsoft Orchestrator, so the link looks for an error code 1 to proceed to the next task of sending an email to the pertinent IT staff if the backup is not valid. If it is successful, an error code 0 is generated, and the link in Orchestrator does not proceed. The script can be used in Orchestrator or Azure Automation. It can also be modified using the Send-MailMessage cmdlet to send an email out upon failure. This can be added to be used in a scheduled task if the other two options are not available. 

To use this script, you will need to define the following parameters:
  • $SQLBackupDir designates the location where the .BAK files are located
  • $TimeSpan defines how many days out a backup can be invalid or missing before an email is triggered
  • $SQLServer is the name of the SQL server
  • $SQLDatabase is the name of the SQL database
  • $ModuleName is the name of the PowerShell SQL module, which should be SQLServer
While talking about the PowerShell module, I have written this script to automatically install it if it does not exist. The script will exit with an error code 2 if it is unable to install the module, so further troubleshooting can be performed. 

The script goes out an retrieve the latest backup from $SQLBackupDir. We keep 5 backups of the SCCM server on hand, for instance, so the script gets the most recent of those five.

As you can see in the screenshot below, the script successfully executed against the SCCM SQL server and verified the backup is good.


Below shows an easy implementation of this script in Orchestrator:



You can download the script from my Github Site .

NOTE: There may be differences in your environment, such that you might need to change the actual SQL query to work. 


 <#  
      .SYNOPSIS  
           SQL Backup Verification  
        
      .DESCRIPTION  
           This script retrieves the latest SQL database backup file and verifies its date matches the current date while also verifying the backup is good.  
        
      .PARAMETER SQLBackupDir  
           Location where the SQL backups exist  
        
      .PARAMETER TimeSpan  
           Number of days allowed since last SQL backup  
        
      .PARAMETER SQLServer  
           Name of the SQL server database to verify the backup  
        
      .PARAMETER SQLDatabase  
           Name of the SCCM SQL database the backup file is for  
        
      .PARAMETER ModuleName  
           Name of module to import  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:      11/11/2019 4:49 PM  
           Created by:      Mick Pletcher  
           Filename:        SQLBackupVerification.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLBackupDir,  
      [ValidateNotNullOrEmpty()]  
      $TimeSpan,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase,  
      [ValidateNotNullOrEmpty()]  
      [string]$ModuleName  
 )  
   
 #Import SQL Server PowerShell Module  
 If ((Get-Module -Name ((Import-Module -Name $ModuleName -ErrorAction SilentlyContinue -Force -PassThru).Name)) -eq $null) {  
      #Install module if it does not exist  
      Install-Module -Name $ModuleName -Confirm:$false -Force  
      #Verify module got installed. Exit the script if it failed  
      If ((Get-Module -Name ((Import-Module -Name $ModuleName -ErrorAction SilentlyContinue -Force -PassThru).Name)) -eq $null) {  
           Write-Host 'Failed'  
           Exit 2  
      }
 }  
 #Retrieve file attributes from the latest SQL backup  
 $LatestBackup = Get-ChildItem -Path $SQLBackupDir -Filter *.bak -ErrorAction SilentlyContinue | Select-Object -Last 1  
 #Verify there is a backup file that exists  
 If ($LatestBackup -ne $null) {  
      #Check if the latest SQL backup is within the designated allowable timespan  
      If ((New-TimeSpan -Start $LatestBackup.LastWriteTime -End (Get-Date)).Days -le $TimeSpan) {  
           #Execute SQL query to verify the backup is valid  
           $Verbose = $($Verbose = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('RESTORE VERIFYONLY FROM DISK = N' + [char]39 + $LatestBackup.FullName + [char]39) -QueryTimeout 0 -Verbose) 4>&1  
           If ($Verbose -like '*The backup set on file 1 is valid*') {  
                Write-Output 'Success'  
                Exit 0  
           } else {  
                Write-Output 'Invalid Backup'  
                Exit 1  
           }  
      } else {  
           #SQL Server did not perform a backup within the designated timespan  
           Write-Output 'Latest Backup does not exist'  
           Exit 1  
      }  
 } else {  
      #Backups are not being performed  
      Write-Output 'No backups available'  
      Exit 1  
 }  
   

22 November 2019

SCCM Client Installer for MDT

Recently, I wanted to revisit the process of installing the SCCM client during an MDT task sequence. At first, I tried to use the SCCM PowerShell module to initiate the install. I learned during testing that it does not work if a system is not present in SCCM. The process needed to include initializing the client deployment, waiting for the ccmsetup to begin, and then waiting until the ccmsetup disappears and ccmexec is running. An additional step was to run the install directly from the source folder on the SCCM server so the latest version will always get installed. I wanted SCCM to wait until the setup is complete before continuing because there is still the possibility that it could screw up if the task sequence proceeded to another task, and the system rebooted.

Once I finished rewriting this installer and verified it ran from the command line, I encountered the issue of the task failing. After several attempts, I realized it not only needed to be executed from a domain admin account, but the profile also needed to be loaded, as shown below. Once that was checked, the task completed without any errors. The reason this must be executed using a domain account is that in my environment, MDT is on a different server than SCCM, thereby not having access to it. One additional note is that I also shared out the folder where the client installer exists so that I can use \\<SCCM Server>\SCCMInstaller as the path to the ccmsetup.exe file. 



In the script below, it first initializes the ccmsetup.exe. It then waits for ccmsetup.exe to appear as a running task. It waits for ccmsetup.exe to disappear, and ccmexec.exe appears in the task list for the script to end with an error code 0. I included error codes in this so that I will know if something happens that causes the script to begin failing in order to mitigate the problem. 

The script below can be downloaded from my GitHub site.


 <#  
      .SYNOPSIS  
           Install SCCM Client  
        
      .DESCRIPTION  
           Install the SCCM client from the client installation folder located on the SCCM server.  
        
      .PARAMETER MP  
           Management Point  
        
      .PARAMETER FSP  
           Fallback Status Point  
        
      .PARAMETER SiteCode  
           Three letter SiteCode  
        
      .PARAMETER ClientPath  
           Network location on the SCCM server where the client resides  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       11/21/2019 4:14 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMClientInstaller.ps1  
           ===========================================================================  
 #>  
   
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$MP,  
      [ValidateNotNullOrEmpty()]  
      [string]$FSP,  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode,  
      [ValidateNotNullOrEmpty()]  
      [string]$ClientPath  
 )  
   
 #Add backslash to end of $ClientPath if it does not exist  
 If ($ClientPath.Substring($ClientPath.Length - 1) -ne '\') {  
      $ClientPath += '\'  
 }  
 Write-Host 'Initiating SCCM Client Installation.....'  
 #Execute the ccmsetup.exe file  
 $ExitCode=(Start-Process -FilePath ($ClientPath + 'ccmsetup.exe') -ArgumentList ('/mp:' + $MP + [char]32 + 'SMSSITECODE=' + $SiteCode + [char]32 + 'FSP=' + $FSP) -PassThru -WindowStyle Minimized -Wait).ExitCode  
 If ($ExitCode -eq 0) {  
      Write-Host 'Waiting for installation to begin.....'  
      $StartTime = Get-Date  
      #Wait until the ccmsetup.exe appears in the task manager  
   Do {  
     $Process = (Get-Process -Name ccm*).Name  
           $TimeSpan = New-TimeSpan -Start $StartTime -End (Get-Date)  
           #Exit if it takes more than 300 seconds for the ccmsetup.exe to begin  
     If ($TimeSpan.TotalSeconds -gt 300) {  
       Exit 2  
     }  
   } While ($Process -notcontains 'ccmsetup')  
      Write-Host 'Installing SCCM Client.....' -NoNewline  
      $StartTime = Get-Date  
      #Wait until ccmsetup.exe closes  
   Do {  
     $Process = (Get-Process -Name ccm*).Name  
     $TimeSpan = New-TimeSpan -Start $StartTime -End (Get-Date)  
           #Exit with error code 3 if the ccmsetup.exe runs longer than 600 seconds  
           If ($TimeSpan.TotalSeconds -gt 600) {  
       Exit 3  
     }  
   } While ($Process -contains 'ccmsetup')  
 } else {  
   Exit 1  
 }  
 #Exit with error code 0 if ccmexec.exe is running in the task manager, otherwise exit with an error code 4  
 If ((Get-Process -Name CcmExec) -ne $null) {  
      Write-Host 'SCCM Client Successfully installed' -ForegroundColor Yellow  
 }  
 else {  
      Write-Host 'SCCM Client installation failed' -ForegroundColor Red  
      Exit 4  
 }  
   

14 November 2019

Posting to Multiple Facebook Groups

I publish new blog postings to multiple Facebook groups regularly. It can be daunting if there are a lot of groups. Although there are several online options, there is a Google Chrome extension called Toolkit for Facebook that is fabulous. It lets you post to multiple groups while being able to designate a random delay in between posting so you don't go to Facebook jail. 

Recently, the extension was pulled from the Chrome Store and is now only available on the website. I cannot get Chrome to accept the extension from the site, so I ended up going to an old machine of mine and copying the extension directory and importing it into my new Chrome browser. It works great. 

As shown below, I enabled developer mode, clicked load unpacked, and then selected the directory where the zip file was exploded to that contains the extension. I did have to close out the browser and reopen it afterward for the extension to appear. The extension can be downloaded from my box account located here



05 November 2019

SCCM Pending Reboot Report

We wanted a list of servers that are waiting for a reboot. Thankfully, SCCM has a pending restart field that allows admins to see when systems are waiting for a reboot. Our server team wanted that list to be automatically generated and emailed to them daily. Other than myself, others that have the SCCM console rarely look at it since they wear many hats in IT.

I could not find any PowerShell cmdlets in the SCCM module for viewing a pending restart. Thankfully, Eswar Koneti's blog shows the information is stored in sms_combineddeviceresources.

After learning that part, I decided it would be dramatically faster to query the SQL database directly. The table the information resides in is dbo.vSMS_CombinedDeviceResources and is under ClientState. ClientState will have a value of zero if no pending reboot, and a value between 2 and 15 if there is a pending reboot. The list of those values is in the above blog link.

In the PowerShell script below, there are three parameters you will need to populate. The first is $Collection that contains the name of the collection you want to query. $SQLServer is the name of the SQL server. Finally, $SQLDatabase is the name of the SCCM SQL database. You can populate them either at the command line, or hard code the data in the parameter field of the script.

I wrote the script in a way that it can be easily implemented into Azure Automation, SMA, or Orchestrator. The script will output the list of machines waiting for a reboot using the Write-Output and Exit with a return code of 1 if there is nothing to report. The exit code 1 is used with Orchestrator or SMA for the Link between the PowerShell script and the email. The link would not continue to the email task if there were an exit code of 1, as shown below.

NOTE: For this to access the SQL server, the script must be executed on the SQL server, or on a machine that has the SQL console. This is required, so PowerShell has access to the SQL PowerShell module.


You can download this script from my GitHub site.


 <#  
      .SYNOPSIS  
           SCCM Reboot Report  
        
      .DESCRIPTION  
           This script will query SCCM for a list of machines that are waiting for a reboot.  
        
      .PARAMETER Collection  
           Name of the collection to query  
        
      .PARAMETER SQLServer  
           Name of the SQL server  
        
      .PARAMETER SQLDatabase  
           A description of the SQLDatabase parameter.  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/3/2019 12:04 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMRebootReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase  
 )  
   
 $RebootList = (Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query 'SELECT * FROM dbo.vSMS_CombinedDeviceResources WHERE ClientState <> 0').Name | Sort-Object  
 $CollectionQuery = 'SELECT * FROM' + [char]32 + 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT ResultTableName FROM dbo.v_Collections WHERE CollectionName = ' + [char]39 + $Collection + [char]39)).ResultTableName)  
 $CollectionList = (Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query $CollectionQuery).Name | Sort-Object  
 $List = @()  
 $RebootList | ForEach-Object { If ($_ -in $CollectionList) { $List += $_ } }  
 If ($List -ne '') {  
      Write-Output $List  
 } else {  
      Exit 1  
 }  
   

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  
 }  
   

15 October 2019

SCCM Duplicate Machine Cleanup

I got tired of duplicate systems appearing in SCCM caused by computers being reimaged while using the same computer name. To rid myself of this issue, I wrote the script below.

It queries the SCCM SQL database for a list of machines where the SCCM client installation was attempted with a return code of 120. This error code indicates the system is already present and active in SCCM, thereby indicating this system is the old one.

The script was designed to work with Orchestrator, SMA, Azure Automation, a scheduled task, or manual execution. Exit 1 is there if it is used with Orchestrator. Commented out line 68 is there for you to use and test this script. I highly recommend uncommenting that line and then commenting out line 69 to verify first before the script actually deletes systems from SCCM. 

You can download this script from my GitHub Repository.


 <#  
      .SYNOPSIS  
           SCCM Duplicate Cleanup  
        
      .DESCRIPTION  
           This script will query for a list of machines with error 120 when trying to install the SCCM client. This error indicates the system is a duplicate.   
        
      .PARAMETER SCCMModule  
           UNC path including file name of the configuration manager module  
        
      .PARAMETER SCCMServer  
           FQDN of SCCM Server  
        
      .PARAMETER SCCMSiteDescription  
           Description of the SCCM Server  
        
      .PARAMETER SiteCode  
           Three letter SCCM Site Code  
        
      .PARAMETER Collection  
           Name of the collection to query  
        
      .PARAMETER SQLServer  
           Name of the SQL server  
        
      .PARAMETER SQLDatabase  
           A description of the SQLDatabase parameter  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/3/2019 12:04 PM  
           Created by:       Mick Pletcher  
           Filename:          SCCMDuplicateCleanup.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMModule,  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMSiteDescription,  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode,  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase  
 )  
   
 $List = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT Name, MachineID, CP_LastInstallationError FROM' + [char]32 + 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('Select ResultTableName FROM dbo.Collections WHERE CollectionName =' + [char]32 + [char]39 + $Collection + [char]39)).ResultTableName) + [char]32 + 'WHERE ClientVersion IS NULL AND CP_LastInstallationError = 120 Order By MachineID')  
 If ($List -ne '') {  
      Import-Module -Name $SCCMModule -Force  
      New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $SCCMServer -Description $SCCMSiteDescription | Out-Null  
      Set-Location -Path ($SiteCode + ':')  
      #Test with output to screen before enabling the other line that also deletes each item  
      $List | ForEach-Object { (Get-CMDevice -ResourceId $_.MachineID -Fast).Name }  
      #$List | ForEach-Object { Get-CMDevice -ResourceId $_.MachineID -Fast | Remove-CMDevice -Confirm:$false -Force }  
      Remove-PSDrive -Name $SiteCode -Force  
      Write-Output ($List.Name | Sort-Object)  
 } else {  
      Exit 1  
 }  
   

03 October 2019

Importing and Using the SCCM PowerShell Module

Recently, I have begun setting up new front and back-office security runbooks in Microsoft Orchestrator. These runbooks needed to use PowerShell for getting data from the SCCM server. The SCCM console is not installed on the Orchestrator server, so PowerShell required to be able to connect directly to it.

The following script will import the SCCM PowerShell module, connect to the SCCM server, and then disconnect. I used the cmdlet Get-CMCollectionMember for all systems because that is shared in all SCCMs.

The first thing that needs to be done is to define the UNC path to the SCCM module. The module is located at Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1. I would put the drive letter in front, but like myself, you may not have SCCM installed on the c: drive. The next thing that needs to be defined is the SCCM server name. I used the FQDN for that. The SCCM Site Description is needed to map the drive. The description was made up of calling it Primary SCCM Site. Finally, the three-letter site code is necessary. That is the directory you will need to change to inside PowerShell before you can call any cmdlets.

Once these items are defined, you will need to first import the SCCM module. Next, you will need to open the connection to it through using the New-PSDrive. The directory then needs to be changed to the defined PSDrive. Now the SCCM cmdlets can be utilized. I included removing the PSDrive at the end of the script.

You can download the script from my GitHub site.


 <#  
      .SYNOPSIS  
           PowerShell SCCM Connection  
        
      .DESCRIPTION  
           This script will connect to the SCCM server and return a list of all systems in SCCM. It is a demo on how to accomplish this task.  
        
      .PARAMETER SCCMModule  
           UNC path including file name of the configuration manager module  
        
      .PARAMETER SCCMServer  
           A description of the SCCMServer parameter.  
        
      .PARAMETER SCCMSiteDescription  
           Description of the SCCM Server  
        
      .PARAMETER SiteCode  
           Three letter SCCM Site Code  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       4/24/2019 9:56 AM  
           Created by:       Mick Pletcher  
           Filename:         SCCMCleanup.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMModule = '<UNCPath>\ConfigurationManager.psd1',  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMServer = '<FQDN of SCCM Server>',  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMSiteDescription = '<SCCM Server Description>',  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode = '<Three letter SCCM Site Code>'  
 )  
   
 Import-Module -Name $SCCMModule -Force  
 New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $SCCMServer -Description $SCCMSiteDescription | Out-Null  
 Set-Location -Path ($SiteCode + ':')  
 $List = Get-CMCollectionMember -CollectionName 'All Systems'  
 Remove-PSDrive -Name $SiteCode -Force  
 Write-Output $List.Name  
   



11 September 2019

Zero Touch Dell Command Update for SCCM and MDT

I have used the Dell Command | Update in the build for quite some time for managing the drivers on systems because it makes it a hand-off approach with little setup and reliable updates direct from Dell. The one thing I have wanted to be able to rerun this task several times without having to have duplicate tasks in the task sequence. Sometimes there are multiple reboots required because not all drivers can be installed at the same time.

I finally discovered how ZTIWindowsUpdate.wsf reboots and reruns itself without corrupting the task sequence, and I have applied the same code to this script. 

This script will run the dcu-cli.exe to both install updates and generate inventory.xml and activitylog.xml files. The script then reads the activitylog.xml file to see if any drivers were installed. If not, the script ends. If there were updates installed, the script creates a rebootcount.log file if it does not exist, that contains the number of reboots that have been performed. It will increment that number inside the file each time the system is rebooted, and the script is rerun. This had to be included because a few updates Dell deploys do not register with the Dell Command | Update and reinstall every time it is run. Because of that, I limited the script to run 5 times like the ZTIWindowsUdpates.wsf does. I wrote in another blog entry why a task sequence variable cannot be used here to store the reboot count. 

Once the script either has no more updates to install or has run 5 times, it will remove both XML files and end so that SCCM/MDT can continue to the next task. 

The script can be downloaded from my GitHub site


 <#  
      .SYNOPSIS  
           Dell Driver Update  
        
      .DESCRIPTION  
           This script executes the Dell Command | Update and reboots MDT or SCCM up to five times when new updates are available. The reason for the 5 reboot limit is because some Dell updates have been know to not leave markers and will cause the Dell Command | Update to rerun indefinitely.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       7/18/2019 1:57 PM  
           Created by:       Mick Pletcher  
           Filename:         ZTIDellDriverUpdate.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 #Verify Dell Command | Update exists  
 If ((Test-Path -Path (Get-ChildItem -Path $env:ProgramFiles, ${env:ProgramFiles(x86)} -filter dcu-cli.exe -Recurse -ErrorAction SilentlyContinue).FullName) -eq $true) {  
      #Delete old logs if they exist  
      Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
      Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
      #Update the system with the latest drivers while also writing log files to the %windir%\temp directory  
      $ErrCode = (Start-Process -FilePath ((Get-ChildItem -Path $env:ProgramFiles, ${env:ProgramFiles(x86)} -Filter 'dcu-cli.exe' -Recurse).FullName) -ArgumentList ('/log ' + $env:windir + '\temp') -Wait).ExitCode  
      #Read the ActivityLog.xml file  
      $File = (Get-Content -Path ($env:windir + '\temp\ActivityLog.xml')).Trim()  
      #if no updates were found or updates were applied and no required reboot is necessary, then delete the log files  
      If (('<message>CLI: No application component updates found.</message>' -in $File) -and (('<message>CLI: No available updates can be installed.</message>' -in $File) -or ('<message>CLI: No updates are available.</message>' -in $File)))  
      {  
           Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
           Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
           Remove-Item -Path ($env:TEMP + '\RebootCount.log') -ErrorAction SilentlyContinue -Force  
      }  
      else  
      {  
           #Create the file containing number of times this script has rerun if it does not exist  
           If ((Test-Path ($env:TEMP + '\RebootCount.log')) -eq $false)  
           {  
                New-Item -Path ($env:TEMP + '\RebootCount.log') -ItemType File -Value 0 -Force  
           }  
           #Reboot the machine and rerun the Dell Driver Updates  
           If (([int](Get-Content -Path ($env:TEMP + '\RebootCount.log'))) -lt 5)  
           {  
                #Microsoft SCCM/MDT environmental variables  
                $TSEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment  
                #Reboot the machine once this task is completed and restart the task sequence  
                $TSEnv.Value('SMSTSRebootRequested') = $true  
                #Rerun the same task  
                $TSEnv.Value('SMSTSRetryRequested') = $true  
                #increment the reboot counter  
                New-Item -Path ($env:TEMP + '\RebootCount.log') -ItemType File -Value ([int](Get-Content -Path ($env:TEMP + '\RebootCount.log')) + 1) -Force  
                #End the update process if run 5 or more times, delete all associated log files, and proceed to the next task  
           }  
           else  
           {  
                Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
                Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
                Remove-Item -Path ($env:TEMP + '\RebootCount.log') -ErrorAction SilentlyContinue -Force  
           }  
      }  
 }  
 else {  
      Write-Output 'Dell Command | Update is not installed'  
      Exit 1  
 }