Payton Flint's Tech Blog
Menu
  • Home
  • Blog
  • Categories
  • Resources
  • About
  • Contact
Menu

PowerShell – CM Uninstall Collections + Deployments Tool

Posted on December 12, 2022June 4, 2023 by paytonflint

To maintain a healthy environment, providing capability for other supporting teams to remove applications using CM may be desirable. One way this might be arranged is to provide the ability to place devices in predetermined uninstall collections that already have required uninstall deployments for their corresponding application.

In this scenario, it would be desirable for the uninstall collection directory structure to mimic that of the targeted applications. This script does just that and places them inside a parent uninstall directory. It then identifies applications with uninstall commands specified, and creates a collection for each in its corresponding directory. Then, it creates a required uninstall deployment to the proper collection for its corresponding application.

This allows for outside groups that have limited knowledge or ability within CM to easily add devices to a collection and uninstall software. This script makes the configuration a cake walk – although, I should mention that it does take a long time to run.

# Written by Payton Flint
# See https://paytonflint.com/powershell-cm-uninstall-collections-deployments-tool/

#=//Prerequisites//==============================================================================================
 
# Clear variables for repeatability
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0
 
# Identify location of script
$ScriptPath = Split-Path ($MyInvocation.MyCommand.Path) -Parent
 
# Install/check for ConfigurationManager module
Import-Module ConfigurationManager -ErrorAction 'Stop'
 
#=//Variables//=================================================================================================
 
# Set Site Code
$SiteCode = <SITE CODE>
 
# CM Server Name
$CMServer = <CM SERVER NAME>
 
# Set Target Directory
$TargetDirName = <TARGET DIR NAME>
 
# Report Location
$ReportPath = <REPORT LOC>
 
#=//Functions//==================================================================================================
 
# GCI -Recurse has not yet been implemented for the CM provider
# Recursive Get-Directory Function
Function Get-Directories {
    # If SearchPath is not empty
    If ($script:SearchPath -ne $null) {
        # Get subdirectories and properties for each SearchPath
        ForEach ($dir in $script:SearchPath) {
            $SubDirs = Get-ChildItem -Path $dir | Select -Property Name,ContainerNodeID
 
            # If subdirectories are present...
            If ($SubDirs -ne $null) {
                $SubDirs | ForEach-Object {
                    # Derive full path by appending name
                    $Path = Join-Path $dir $_.Name
                    $_ | Add-Member -NotePropertyName Path -NotePropertyValue $Path
                    $SubPaths += ,$Path
                    $script:AllDirs += $_   
                }
            # If subdirectories are not present...
            } else {
                # Get directory properties
                $Dirs = Get-Item -Path $dir | Select -Property Name,ContainerNodeID
                # Add path property to all directories
                $Dirs | ForEach-Object {
                    $_ | Add-Member -NotePropertyName Path -NotePropertyValue $dir
                    # Add directories to AllDirs
                    If ($_.Path -notin $script:AllDirs.Path) {
                        $script:AllDirs += $Dirs
                    }
                }
            }
        }
    }                              
    # Clear SearchPath variable and add new subdirectory paths
    $script:SearchPath = $null
    $script:SearchPath += $SubPaths
 
    # If no new subdirectory paths found...
    If ($SubPaths -eq $null) {
        $script:GetDirs = 'Get-Directories Complete'
    }
} # End Get-Directories function (outputs to $script:AllDirs)
 
# Get display name, path, ContainerNodeID, install/uninstall commands for each application
Function Get-Applications {
    param (
        $Directories,
        $SiteCode
    )
    ForEach ($dir in $Directories) {
 
        # Get ContainerNodeID for all directories
        $ContainerNodeID = $dir | Select -ExpandProperty ContainerNodeID
 
        # WMI query
        $Query = "select * from SMS_ApplicationLatest where ModelName is in(select InstanceKey from SMS_ObjectContainerItem where ObjectType='6000' and ContainerNodeID='$ContainerNodeID')"
 
        # Get applications' names,paths
        $AppsInDir = Get-WmiObject -Namespace "ROOT\SMS\Site_$SiteCode"-ComputerName $CMServer -Query $Query |
            Select -Property LocalizedDisplayName
 
        # Get SDMPackageXML from Deployment Type for each application in directory
        ForEach ($app in $AppsInDir) {
 
            # Get XML from DeploymentType
            $AppXML= Get-CMDeploymentType -ApplicationName $app.LocalizedDisplayName | Select -ExpandProperty SDMPackageXML
 
            try {
            # Ignore errors
            $ErrorActionPreference = 'Continue'
 
            # Get info from XML
            $AppProps = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($AppXML)
 
            # Set Install/Uninstall properties
            $Install   = $appProps.DeploymentTypes.Installer.InstallCommandLine
            $Uninstall = $appProps.DeploymentTypes.Installer.UninstallCommandLine
            } catch { }
 
            # Application Properties
            $AppObjectProperties = @{
                Name      = $app.LocalizedDisplayName
                Path      = $dir.Path
                DirID     = $ContainerNodeID
                Install   = $Install
                Uninstall = $Uninstall
            }
            # Create application objects using above properties
            $AppObject = New-Object psobject -Property $AppObjectProperties
 
            # Add objects to list
            $script:Applications += ,$AppObject
 
            # Nullify variables for repeatability
            $Install   = $null
            $Uninstall = $null
            $AppXML    = $null
            $AppProps  = $null
        }
    }
} # End Get-Applications function (outputs to $script:Applications)
 
#=//Body//========================================================================================================
 
# Change location to CM site
Set-Location $SiteCode':'
 
# Set Application directory path using above variables
$ApplicationDir = $SiteCode +':\Application'
 
# Set target directory path
$TargetDirPath = Join-Path $ApplicationDir $TargetDirName
 
# Create object for target dir
$TargetDirObj = Get-Item -Path $TargetDirPath | Select -Property Name,ContainerNodeID
# Add path property to object
$TargetDirObj | Add-Member -NotePropertyName Path -NotePropertyValue $TargetDirPath
# Add object to AllDirs
$script:AllDirs += ,$TargetDirObj
 
# Initially set SearchPath to TargetDir
$script:SearchPath = ,$TargetDirPath
 
# Execute Recursive Get-Directories function until completion
Do {
    Get-Directories
} Until ($script:GetDirs -like '*Complete')
 
# Get all application display names for all directories
Get-Applications -Directories $script:AllDirs -SiteCode $SiteCode
 
# Set DeviceCollections path
$DevColls = "$SiteCode`:\DeviceCollection"
# Set name for Uninstall Collections folder
$UninstallName = "$TargetDirName Uninstalls"
# Set path for Uninstall Collections folder
$UninstallCollDir = "$DevColls\$UninstallName"
 
# Get paths of all subdirectories from AllDirs
$SubPaths = $AllDirs.Path[1..$AllDirs.Count]
 
# Get leaf from each subpath
$SubPathLeafs = $SubPaths | ForEach-Object {
    Split-Path -Path $_ -Leaf
}
 
# If Uninstall collections folder doesn't exist, create it
If (!(Test-Path -Path $UninstallCollDir)) {
    New-CMFolder -Name "$UninstallName" -ParentFolderPath "$DevColls"
}
 
# Create subfolders in uninstall collections folder if they don't exist
$SubPathLeafs | ForEach-Object {
    If (!(Test-Path -Path "$UninstallCollDir\$_")) {
        New-CMFolder -Name "$_" -ParentFolderPath "$UninstallCollDir"
    }
}
 
# Get only apps with uninstall command specified
$AppsWUninstall = $Applications | Where-Object {$_.Uninstall -ne $null}
 
# For each application...
$AppsWUninstall | ForEach-Object {
 
    # Get leaf from application path
    $AppLeaf = Split-Path -Path $_.Path -Leaf
 
    # Append "(UNINSTALL)" to application name
    $CollName = ($_ | Select-Object -ExpandProperty Name) + " (UNINSTALL)"
 
    # If collection does not already exist, create it and move to the appropriate folder
    If (!(Get-CMCollection -Name "$CollName")) {
 
        # Create Collection and move to the appropriate folder
        $NewCollection = New-CMDeviceCollection -Name "$CollName" -LimitingCollectionName "All KCPL Workstations"
        Move-CMObject -FolderPath "$UninstallCollDir\$AppLeaf" -InputObject $NewCollection
    }
 
    # Get name from application
    $AppDeployName = $_ | Select-Object -ExpandProperty Name
 
    # If uninstall deployment does not already exist, create it
    If (!(Get-CMApplicationDeployment -Name "$AppDeployName" -CollectionName "$CollName")) {
 
        # Create application uninstall deployment
        New-CMApplicationDeployment -ApplicationName "$AppDeployName" -CollectionName "$CollName" -DeployAction Uninstall -DeadlineDateTime (Get-Date) -DeployPurpose Required -UserNotification DisplayAll
    }
}

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

About The Author

Author's Portrait

In my journey as a technologist and 11 years of experience as an IT professional, I have found my niche as Director of Infrastructure Services; developing my skillsets in management, scripting, cloud infrastructure, identity management, and networking.

I have experience as a Systems Administrator and Engineer for large enterprises including the DoD, government agencies, and a nuclear-generation site.

I've been blessed to collaborate with engineers at esteemed Fortune 50 corporations, and one of Africa's largest, to ensure successful implementation of my work.

GitHub Button

Credentials

M365 Endpoint Administrator Associate
M365 Fundamentals
Microsoft AZ-900
CompTIA CSIS
CompTIA CIOS
CompTIA Security+
CompTIA Network+
CompTIA A+
  • April 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
© 2022 Payton Flint | The views and opinions expressed on this website belong solely to the author/owner and do not represent the perspectives of any individuals, institutions, or organizations, whether affiliated personally or professionally, unless explicitly stated otherwise. The content and products on this website are provided as-is with no warranties or guaranties, are for informational/demonstrative purposes only, do not constitute professional advice, and are not to be used maliciously. The author/owner is not responsible for any consequences arising from actions taken based on information provided on this website, nor from the use/misuse of products from this site. All trademarks are the property of their respective owners.