Deploy Applications via SCCM 2016 PowerShell cmdlets

Continuing my PowerShell automation notes for SCCM. Below is a rough example on how to deploy Applications in SCCM 2016 using PowerShell. The real meat of it comes down to 5 cmdlets. As an extra goodie, also included the cmdlet to remove old deployments as well. Note: If you are using these cmdlets on a new machine or account, the account that is to run these cmdlets should open the SCCM console on that machine, and click the “Connect with PowerShell” option first. If the account has not performed these steps, the SCCM drive will not be available when the SCCM cmdlets are imported. You will see errors such as “A drive with the name ‘xyz’ does not exist.”

Command Run Down

The core commands we are interested in are

New-CMApplication # Creates a new application in SCCM
Add-CMScriptDeploymentType # Adds a script based deployment type or optionally
Add-​CM​Msi​Deployment​Type # Which will add an MSI deployment type

An additional note here on these two. These are currently your only deployment options and this directly limits your installation detection options. MSI is locked down to using the MSI product GUID for installation detection. If you need anything more complex than that, you are pretty much stuck with using a script based detection method and the script deployment type. The registry key and file options are not currently available. Luckily you can do nearly any detection method in PowerShell. The script below for example checks a registry key. For more information on creating a script for PowerShell based installation detection methods see the relevant docs.microsoft.com article and also David O’Brien’s blog.

Start-CMContentDistribution # Distributes our content to our Distribution Points
Start-CMApplicationDeployment # Actually deploys our finished application package to end users
Remove-CMDeployment # Removes deployments. Useful if you have an older deployment you are replacing
Move-CMObject # Moves your Application to a different folder within the SCCM console

Example Script


Param
(
    [string]$PackageDirectory="\\Path\To\Package\Source\",#Package source
    [string]$IconPath="C:\SomePath\SomeIcon.ico",#Icon to show in software center
    [string]$SCCMDrive="SCM:\",#Should be your 3 character site code generally
    [string]$SCCMAdmin="john.doe",#Owning admin
    [string]$TargetCollection="All Windows Workstations",#Collection to deploy to
    [string]$LogPath="C:\Logs\somelog.log",#Path to save log
    [string]$TargetDPs = "All Distribution Points"#Distribution point group to deploy to
)
#Start a log
$Log = "Starting Package Script`r`n"
try
{
    #Import SCCM Module
    Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -ErrorAction Stop
    if (-not (Test-Path -Path $SCCMDrive))
    {
        $Log+="SCCM PowerShell cmdlets provider is not initialized for this account, on this machine. Please open the SCCM console and select 'Connect with PowerShell' at least once before using this script on thise machine.`r`n"
        throw "SCCM PSProvider does not have a drive assigned"
}
catch
{
    #End script if we could not add module
    $Log += "Failed to add required module!`r`n"
    $Log += "----End Package Script----`r`n"
    $Log | Out-File -FilePath $LogPath -Append
    exit 1
}

#TODO: Prepare files as needed here
#Maybe dynamically get file verison, or application name, unzip files if needed, etc
$Version = "1.0.0.0"
$ProductName = "Sample Application"
$ApplicatioName = "$ProductName $Version"
$Publisher = "ACME"
$InstallCommand = "`"SomeInstaller.exe`" /s"
$DetectScript = "if (Test-Path `"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MyProdct`"){ if ((Get-ItemProperty -Path `"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MyProduct`" -Name `"DisplayVersion`").DisplayVersion -eq `"$Version`") { Write-Host `"Installed`" } } exit 0"

try
{
    $Log += "Creating `"$ApplicationName`"`r`n"
    #Change to SCCM powershell provider, SCCM cmdlets generally do not work otherwise, however, some cmdlets may fail in the SCCM drive. Keep this in mind, you may need to switch between providers
    CD $SCCMDrive
    #Create a new application. (Won't deploy, won't distribute, won't create deployment type. Just the application info)
    $MyNewApp = New-CMApplication -Name $ApplicationName -Description "Auto-Added by Packager.ps1" -Publisher $Publisher -SoftwareVersion $Version -LocalizedName $ApplicationName `
        -Owner $SCCMAdmin -SupportContact $SCCMAdmin -IconLocationFile $IconPath -ErrorAction Stop

    #Move the application
    $Log += "Moving`"$ApplicationName`"`r`n"
    $MyNewApp | Move-CMObject -FolderPath "$($SCCMDrive)Application\SomePath\ToPlace"

    $Log += "Creating `"$ApplicationName`" - Install`r`n"
    #Add a deployment type to the new application, this won't distribute or deploy it
    Add-CMScriptDeploymentType -ApplicationName $ApplicationName -ContentLocation $PackageDirectory -ContentFallback -EnableBranchCache -InstallCommand $InstallCommand `
        -LogonRequirementType WhetherOrNotUserLoggedOn -SlowNetworkDeploymentMode Download -UserInteractionMode Hidden -InstallationBehaviorType InstallForSystem `
        -DeploymentTypeName "Install" -ScriptLanguage PowerShell -ScriptText $DetectScript -ErrorAction Stop
    #If you are doing an MSI install, look into "Add-​CM​Msi​Deployment​Type" 
    #https://docs.microsoft.com/en-us/powershell/sccm/configurationmanager/vlatest/add-cmmsideploymenttype

    $Log += "Distributing `"$ApplicationName`" - Install`r`n"
    #Distribute the content, doesnt deploy
    Start-CMContentDistribution -ApplicationName $ApplicationName -DistributionPointGroupName $TargetDPs

    $Log += "Deploying `"$ApplicationName`" - Install`r`n"
    #Deploy the new application
    Start-CMApplicationDeployment -CollectionName $TargetCollection -Name $ApplicationName -DeadlineDateTime ([DateTime]::Now) -AvailableDateTime ([DateTime]::Now) `
        -DeployAction Install -DeployPurpose Required -OverrideServiceWindow $true -TimeBaseOn LocalTime -UseMeteredNetwork $true

    $Log += "Stopping old deployments`r`n"
    #Additionally, we can stop any old deployments.
    #Grab all apps similiarly named to what we just deployed, but are not what we deployed
    $Apps = @()+(Get-CMApplication -Fast | Where-Object -FilterScript {$_.LocalizedDisplayName -like "$ProductName *" -and $_.LocalizedDisplayName -ne $ApplicationName -and $_.IsDeployed})
    foreach ($App in $Apps)
    {
        Write-Log "Deployment for $($App.LocalizedDisplayName) stopped`r`n"
        #And remove their deployment rule
        #You may need to change application name. My ApplicationName and LocalizedDisplayName usually match
        Remove-CMDeployment -CollectionName $TargetCollection -ApplicationName $App.LocalizedDisplayName -Force 
    }
}
catch
{
    Write-Log "Failed to create package. $($_.ToString())"
}

#Return to filesystem provider
cd "$($env:SystemDrive)\"

$Log += "----End Package Script----`r`n"
$Log | Out-File -FilePath $LogPath -Append
exit 0

For additional information on the cmdlets, please see the 2016 cmdlet reference at docs.microsoft.com.

Using PowerShell to watch a log

I was recently troubleshooting an issue and needed to view the last few results of a log. CMTrace and text editors would crash due to the sheer size of the log. Powershell’s Get-Content with the “Tail” parameter worked like a charm however. Although this worked, I didn’t want to keep running the command over and over, so, I decided to replicate the watch command from Linux in PowerShell.


<#
.SYNOPSIS
    Repetitively runs a script block to allow you to track changes in the command output. An example use would be for watching log inputs. Press CTRL+C to cancel script. It runs indefinitely.

.PARAMETER ScriptBlock
    Script to execute

.PARAMETER Interval
    How often to rerun scriptblock in seconds

.NOTES
    Version:        1.0
    Author:         Matthew Thompson
    Creation Date:  2017-07-19
    Purpose/Change: Initial script development
  
.EXAMPLE
    &"Start-Watch.ps1" -ScriptBlock {Get-Content -Path "C:\Logs\SomeLog.log" -Tail 20} -Interval 10
#>
Param([scriptblock]$ScriptBlock, [int32]$Interval=5)
#Put the real code in a function so it can be quickly copy-pasted as a child function of other scripts
function Start-Watch
{
    Param([scriptblock]$ScriptBlock, [int32]$Interval)
    #Set lowest possible datetime, so that it will run script immediatly
    $Start = [DateTime]::MinValue
    #Infinite loop, cancel require user intervention (CTRL+C)
    while($true)
    {
        #If enough time has passed (Now - LastAttempt)>Selected interval
        if ([DateTime]::Now - $Start -ge [TimeSpan]::FromSeconds($Interval))
        {
            #Clear console and call function
            Clear-Host
            $ScriptBlock.Invoke()
            #Set new start time/last attempt
            $Start = [DateTime]::Now
        }
        #Sleep the thread, prevents CPU from falsly registering as 100% utilized
        [System.Threading.Thread]::Sleep(1)
    }
}
#Call watch function
Start-Watch -ScriptBlock $ScriptBlock -Interval $Interval

Deploy Updates via SCCM 2016 PowerShell cmdlets

I have found very few examples on how to use the SCCM PowerShell cmdlets to deploy updates. Maybe someone will find this example useful. The following script will search the update catalog for relevant updates, add them to a Software Update Group, create a Software Update Package, download the updates contained in the Software Update Group into the Software Update Package and distribute them to a distribution point group. Then optionally, you can deploy it to a collection of machines afterwards.

Command run down

The commands specifically relating to SCCM are:

Get-CMSoftwareUpdate # Lists available updates from SCCM catalog
New-CMSoftwareUpdateGroup  # Creates a new Software Update Group
New-CMSoftwareUpdateDeploymentPackage # Creates a Software Update Package
Save-CMSoftwareUpdate # Downloads updates into a Deployment Package
Start-CMContentDistribution # Distributes downloaded content to Distribution Points

The Script


Param($SoftwareUpdateSource="\\YourShare\PathTo\SCCMSupSource", $DistributionGroup="All Distribution Points")#, $CollectionName = "All Systems") #Uncomment this if you also want to deploy to a collection as the last step 

#Used in creating update group name
$Date = [DateTime]::Now.ToString("yyyy-MM-dd");

#Grab all updates in the catalog
$UpdateCatalog = Get-CMSoftwareUpdate -Fast

#Filter out the updates we don't need
#Specifically, this filter will pull Updates created in the last 31 days that are not deployed, expired, or superseded that are not preview updates and SCCM has confirmed at least 25 machines require them
$Updates = Where-Object -FilterScript {$_.DateCreated -gt [DateTime]::Now.AddDays(-31) -and $_.IsDeployed -eq $false -and $_.LocalizedDisplayName.Contains("Preview") -eq $false -and $_.IsExpired -eq $false -and $_.IsSuperseded -eq $false -and $_.NumMissing -gt 25}

#We only need the update IDs. Newer powershell will automatically loop through each update and pull the CI_ID properties into an array with this line
$UpdateIDs = $Updates.CI_ID 

#Now we create the software update group
New-CMSoftwareUpdateGroup -Name "Security Updates $Date" -UpdateId $UpdateIDs

#Create a new folder in the Sup Source directory to contain package files
New-Item -Path "filesystem::$SoftwareUpdateSource\$Date" -ItemType Directory

#Create the deployment package
New-CMSoftwareUpdateDeploymentPackage -Name "Security Updates $Date" -Path "$SoftwareUpdateSource\$Date"

#Download the software update group to the deployment package
Save-CMSoftwareUpdate -SoftwareUpdateGroupName "Security Updates $Date" -DeploymentPackageName "Security Updates $Date" 

#And finally distribute it to your Distribution Points
Start-CMContentDistribution -DeploymentPackageName "Security Updates $Date"-DistributionPointGroupName "$DistributionGroup"

#After this you would deploy, if you want to automate that, look into the following. You will likely want/need to customize this portion.
#See "Get-Help Start-CMSoftwareUpdateDeployment" for more options 
<# Start-CMSoftwareUpdateDeployment -AcceptEula -AllowRestart $true -AllowUseMeteredNetwork $true ` -CollectionName $CollectionName -DeploymentAvailableTime ([DateTime]::Now.AddDays(1)) ` -DeploymentName "Security Updates $Date - $CollectionName" -DeploymentType Required -Description "Automatic updates" ` -DownloadFromMicrosoftUpdate $false ` -EnforcementDeadline ([DateTime]::Now.AddDays(8)) ` -ProtectedType RemoteDistributionPoint -RestartServer $false -RestartWorkstation $true -SoftwareInstallation $true ` -SoftwareUpdateGroupName "Security Updates $Date" -TimeBasedOn LocalTime -UnprotectedType NoInstall -UseBranchCache $true ` -UserNotification DisplaySoftwareCenterOnly #>

See also

Googling for SCCM PowerShell cmdlets usually returns a link to the SCCM 2012 R2 library of cmdlets on technet. I managed to find the latest SCCM 2016 cmdlet references at docs.microsoft.com.

Blog Migrated

If you were looking for the WIKI, it has now been migrated to this WordPress Page. Further all applications previously available have been made open source. Scripts and such are still available under the “Code Release” category.

Alternatively, most of the code is now available on GitHub.