27 April 2017

Automated Import of PowerShell SCCM Module

While writing quite a few PowerShell scripts for SCCM, I got tired of having to look up the location of the SCCM PowerShell module to import. I decided while writing the current PowerShell SCCM script, I would automate this process from now on. This makes it a snap using SAPIEN's PowerShell Studio to make this function a snippet and quickly add it each time I write a new SCCM script.

This script allows you to import the SCCM module without having to look up the location. The only parameter you have to specify is the -SCCMServer, which is the name of the SCCM server. The script will then connect to the SCCM server, get the parent installation directory, including the drive letter, and then search for the ConfigurationManager.psd1 file. If it finds more than one file, it will import the latest one.

You can use the function in this script in other scripts to make the process a breeze.

The script can be downloaded from my GitHub repository.


 <#  
      .SYNOPSIS  
           Imports the SCCM PowerShell Module  
        
      .DESCRIPTION  
           This function will import the SCCM PowerShell module without the need of knowing the location. The only thing that needs to be specified is the name of the SCCM server.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       4/26/2017 3:56 PM  
           Created by:       Mick Pletcher  
           Filename:         ImportSCCMModule.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 Import-SCCMModule -SCCMServer "SCCMServer"  
   

14 April 2017

Application Build Reporting for MDT and SCCM

A few years ago, I had published a PowerShell module that contained a function to log all app installs during a build process. With the project of upgrading systems to Windows 10, I decided to explore that once again.

This new script I have written is designed to query the programs and features registry entries to verify if an application is installed and to write that verification to a .CSV file. The purpose of this is to generate a report that is easy to open and see if all applications were successfully installed.

When I first started designing this script, I intended to have it read the ts.xml file from MDT and query the task sequence via PowerShell in SCCM. It was going to grab the name of the application that was exactly how it was listed in programs and features. That method had issues for the fact that some applications install more than one app during the installation. If you wanted to verify that more than one app was installed, then the script would not work correctly.

After much thought, I decided that writing a script in which I could designate in the parameters what application to search for would be much better. The script now becomes a task sequence after the installation sequence of the app. This allows a separate task for each app that an installation might install to be done.

Here is how I used the script in my task sequence. I build my reference images in MDT and deploy through SCCM. As you can see below, I have a task sequence to install that app and the next sequence is to check if it is installed and write to the CSV file.

Each of the task sequence checks are setup as Run PowerShell Scripts. In MDT, you will need to copy the PowerShell script to the %MDTDeploymentShare%\Scripts folder.
In the parameters field, you will need the following info:

  • -Application
  • -LogFileName
  • -LogFileLocation
  • -ExactFileName
The Application parameter will be the one that you will definitely need to define in the MDT task sequence because it will be different for each app. This parameter can be named either a partial name or the exact name as displayed in the Programs and Features. Such applications as Java Runtime can use a partial name 'Java 8' to get any version of the Java 8 that may be installed. Java 8 is always displayed Java 8u60 for instance.

The LogFileName parameter is what the logfile will be called. I define it in the task sequence because %OSDCOMPUTERNAME% is the computer name defined in the user input fields of an MDT image. If you hardcoded $env:computername to the parameter in the script, the computername you input will not be the same. 

The LogFileLocation parameter defines where you want the CSV file to be written.

The ExactFileName parameter specifies to search the programs and features for the name that matches exactly what is defined in the Application parameter. I ran into an instance with Microsoft Office 2016 where it had more than one entry. It still had two entries even using the exact name. In this case, the script measures the length of the registry entry and chooses the larger. I found if there are two entries with the exact names, the larger one is what is displayed in Programs and Features. 

Here is an example of Dell Command | Configure:


-Application 'Dell Command | Configure' -LogFileName %OSDCOMPUTERNAME% -LogFileLocation '\\Directory\ApplicationBuildReport\Reports'

This is what the CSV file report looks like after a reference build is completed. Some of the registry entries include the version within the filename.

As you can see, it makes for an easy way to check if everything got installed correctly in a build. One more thing I do in my reference image task sequence is to suspend the imaging process so I can check this report before a reference image is generated. That way, if items are missing, I can either install them manually and then proceed with grabbing an image or fix the problem and then restart the image all over.

You can download the script from my GitHub repository located here.


 <#  
      .SYNOPSIS  
           Check Application Install  
        
      .DESCRIPTION  
           This script will check if a specified application appears in the programs and features.  
        
      .PARAMETER Application  
           Name of application in the programs and features. It can be a partial name or the complete name.  
        
      .PARAMETER LogFileName  
           Name of the LogFileName containing the list of applications installed on the machine  
        
      .PARAMETER LogFileLocation  
           Location where to write the log file  
        
      .PARAMETER ExactFileName  
           Specifies to search for the exact filename, otherwise the script will search for filename that contain the designated search criteria.  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.138  
           Created on:      4/4/2017 4:51 PM  
           Created by:      Mick Pletcher  
           Filename:        AppChecker.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$Application,  
      [ValidateNotNullOrEmpty()][string]$LogFileName,  
      [string]$LogFileLocation,  
      [switch]$ExactFileName  
 )  
   
 function New-LogFile {  
 <#  
      .SYNOPSIS  
           Create new build log  
        
      .DESCRIPTION  
           This function will compare the date/time of the first event viewer log with the date/time of the log file to determine if it needs to be deleted and a new one created.  
        
      .PARAMETER LogFile  
           Full name of log file including the unc address  
        
      .EXAMPLE  
           PS C:\> New-LogFile  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$Log  
      )  
        
      #Get the creation date/time of the first event viewer log  
      $LogFile = Get-ChildItem -Path $LogFileLocation -Filter $LogFileName -ErrorAction SilentlyContinue  
      Write-Output "Log File Name: $LogFile"  
      $Output = "LogFile Creation Date: " + $LogFile.CreationTime  
      Write-Output $Output  
      If ($LogFile -ne $null) {  
           $OSInstallDate = Get-WmiObject Win32_OperatingSystem | ForEach-Object{ $_.ConvertToDateTime($_.InstallDate) -f "MM/dd/yyyy" }  
           Write-Output "    OS Build Date: $OSInstallDate"  
           If ($LogFile.CreationTime -lt $OSInstallDate) {  
                #Delete old log file  
                Remove-Item -Path $LogFile.FullName -Force | Out-Null  
                #Create new log file  
                New-Item -Path $Log -ItemType File -Force | Out-Null  
                #Add header row  
                Add-Content -Path $Log -Value "Application,Version,TimeStamp,Installation"  
           }  
      } else {  
           #Create new log file  
           New-Item -Path $Log -ItemType File -Force | Out-Null  
           #Add header row  
           Add-Content -Path $Log -Value "Application,Version,TimeStamp,Installation"  
      }  
 }  
   
 Clear-Host  
 #If the LogFileName is not predefined in the Parameter, then it is named after the computer name  
 If (($LogFileName -eq $null) -or ($LogFileName -eq "")) {  
      If ($LogFileName -notlike "*.csv*") {  
           $LogFileName += ".csv"  
      } else {  
           $LogFileName = "$env:COMPUTERNAME.csv"  
      }  
 } elseIf ($LogFileName -notlike "*.csv*") {  
           $LogFileName += ".csv"  
 }  
 #Add backslash to end of UNC path  
 If ($LogFileLocation[$LogFileLocation.Length - 1] -ne "\") {  
      $File = $LogFileLocation + "\" + $LogFileName  
 } else {  
      $File = $LogFileLocation + $LogFileName  
 }  
 #Create a new log file  
 New-LogFile -Log $File  
 #Get list of installed applications from programs and features  
 $Uninstall = Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue  
 $Uninstall += Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue  
 #Check if the list of Applications contains the search query  
 If ($ExactFileName.IsPresent) {  
      $ApplicationInstall = $Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -eq $Application }  
 } else {  
      $ApplicationInstall = $Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -like "*" + $Application + "*" }  
 }  
 #If more than one registry entry, select larger entry that contains more information  
 If ($ApplicationInstall.length -gt 1) {  
      $Size = 0  
      for ($i = 0; $i -lt $ApplicationInstall.length; $i++) {  
           If (([string]$ApplicationInstall[$i]).length -gt $Size) {  
                $Size = ([string]$ApplicationInstall[$i]).length  
                $Temp = $ApplicationInstall[$i]  
           }  
      }  
      $ApplicationInstall = $Temp  
 }  
 #Exit with error code 0 if the app is installed, otherwise exit with error code 1  
 If ($ApplicationInstall -ne $null) {  
      $InstallDate = (($ApplicationInstall.InstallDate + "/" + $ApplicationInstall.InstallDate.substring(0, 4)).Substring(4)).Insert(2, "/")  
      $Output = $ApplicationInstall.DisplayName + "," + $ApplicationInstall.Version + "," + $InstallDate + "," + "Success"  
      Add-Content -Path $File -Value $Output  
      Write-Host "Exit Code: 0"  
      Exit 0  
 } else {  
      $Output = $Application + "," + "," + "," + "Failed"  
      Add-Content -Path $File -Value $Output  
      Write-Host "Exit Code: 1"  
      Exit 1  
 }