Old SCUP update hanging around

We had some users complaining about old Adobe Reader updates not installing from WSUS. The issue was inconvenient, but as soon as SCCM pushed more recent Adobe updates to the user, the issue went away. We decided to expire these old updates and remove them, however there was an issue. Whenever we attempted to publish the update as expired from SCUP, we got Verification of file signature failed for file: <Some cab file path here>. I had issues like this before and tried to remove it using PowerShell/.Net instead. I have had to do this before when we lost our SCUP database file. My go-to code for that is:

#This code largely from https://myitforum.com/how-to-expire-a-custom-update-in-wsus-using-powershell/
#Run this from WSUS for central site server
#Load .NET assembly
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
#Connect to WSUS server
$wsusrv = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
#Get all the non-microsoft updates 
$otherupdates = $wsusrv.GetUpdates() | select * | ? {$_.UpdateSource -ne "MicrosoftUpdate"}
#$wsusrv.GetUpdate($_.id)}} #Get more info on a specific update
$otherupdates | where-object { <#$_.id -eq "" -or #> $_.title -like "*adobe*"} | foreach-object {$wsusrv.ExpirePackage($_.id)}

This still did not work however, the script returned the exact same error as when the update is expired using SCUP. It turns out, the SCUP certificate that signed these cab files had expired about a month prior to this issue. In a last ditch effort, we were able to expire these updates by rolling the server time back to a time when the certificate was still valid. We were then able to re-publish the updates as expired from SCUP and the issue was resolved.

Grand Unified Check Summary – HTML PowerShell Report

I have played around with the idea of making a single-file HTML report easily exportable from PowerShell before. A couple of these used to be hosted in the old version of this blog. We recently had to rebuild a report at my office and I decided it would be a good time to make another go at an HTML reporting framework. This time, something more generalized and customizable.

The end result this time is a framework which will take an HTML template one or more CSS templates, images, custom outputs from scripts, and combine these resources together into a single-file report that could be sent out without any dependency files. The idea behind the separate template elements is to keep the report structure, design, and the scripts relatively separate preventing a massive monolithic monstrosity. If you need to add a new item to the report, say cpu utilization or some other metric, you could just add a new child script. If you need to adjust the colors used in the report, but not the contents itself, you can just edit the CSS file master template or if you need to adjust the structure of the report, you could do so, without ever having to touch the PowerShell scripts responsible for gathering the information being reported.

The main script looks at custom tags in the template itself to fill in the final report with the necessary information from child scripts. If this sounds like something that could be useful, checkout the project on Github.

Managing Adobe CC Users from PowerShell

I wanted to automate our user management of Adobe Creative Cloud. This requires interfacing with Adobe’s user management API. One of the coolest functions I created in this initiative allows you to synchronize an adobe group based on an Active Directory group. I intend to use this AD Group for AppLocker, SCCM deployments, and syncing to Adobe Creative Cloud. This should largely automate the entire Creative Cloud deployment and reduce administrative overhead. The end result will be a single administrative user adds someone to the “Approved CC Users” group, and everything else is hands free.

See the GitHub repo for the PowerShell script and additional information and resources.

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.

Install-Printer

Code Release

This script is intended to assist in SCCM deployments of optional printers. The intended effect is to allow a user to open Software Center or the SCCM Application Catalog and click an option such as “Map me to XXX Printer”, and have the printer, port, and driver all install automatically.

Parameters

DriverName <Object> The name of the printer driver as defined in the INF file.
DriverInf <Object> Full path to the Driver INF file
DriverPath <Object> Path to the folder containing the driver files. (Default is the INF directory)
PrinterIP <Object> IP of the printer to map
PrinterPort <Object> Printer’s connection port. Default is 9100
PrinterPortName <Object> Name of the printer port. Defaults to “IP_<IP>”
PrinterCaption <Object> Name of the printer
Remove [<SwitchParameter>] Removes the specified printer instead of installing it
DriverOnly [<SwitchParameter>] Only installs the printer driver. Does not attempt to create the port or printer
AllSteps [<SwitchParameter>] Performs Driver install followed by the port and printer mappings. (Default without this and driver only is to only map a printer)
WriteLog [<SwitchParameter>] Writes Debug data to a log file. (Default is true)
RemoveAll [<SwitchParameter>] Removes all mapped printers. (You may need to run this once as the user, and once as an admin for full effect)
AdditionalRemovalExclusions <Object> When used with RemoveAll, prevents the removal of listed printers. By default, Send to One Note, and FAX are excluded.

Usage

PowerShell Script

To add a printer:

&"" -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -PrinterIP "192.168.1.2"

To add a driver:

&"" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -DriverOnly

To remove a printer and also disable logging

&"" Install-Printer.ps1 -PrinterCaption "My Printer" -Remove -WriteLog:$false

To install a driver and then map a printer (Note that, in general, only administrators have the permissions to successfully use the script this way)

&"" -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -PrinterIP "192.168.1.2" -AllSteps

To remove all printers, or a selection of printers

&"" -RemoveAll -AdditionalRemovalExclusions @("My Printer", "Send by E-mail printer")

SCCM Usage

To install a printer from SCCM, do the following.

  1. Create an application package for your driver. This can be an executable install or an INF install. If you are using an INF file, you may use the Install-Printer script to help.

    Example of the Driver Installation Programs Tab

    1. To create an INF file Application deployment. Create a new application deployment. Set the command to
      &"Install-Printer.ps1" -DriverOnly -DriverName '' -DriverINF ''

      Example of the Driver Installation Detect Tab

      Example of the Driver User Experience Tab

      Example of the Driver Installation Detect Script
    2. For the detection method, us the “Driver Detection” script in the “script” section of this page.
  2. Create an application package for your printer.
    1. Create a new Application Deployment. Set the command to “Install-Printer.ps1” -PrinterCaption ‘My Printer’ -DriverName ‘Xerox 7845 PS v4’ -PrinterIP ‘192.168.1.2’

      Example of the Printer Installation Programs Tab
    2. Set the detection method to the “Printer Detection” script in the “script” section of this page.

      Example of the Printer Installation Detect Script
    3. Set this package to install for the user.

      Example of the Printer Installation User Experience Tab
    4. Add a requirement that points to the appropriate driver application package that you created earlier.

      Example of the Printer Installation Dependency Tab

Script

Install-Printer

<#
.SYNOPSIS
    Provides helpfull script to add printers to a machine
 
.DESCRIPTION
    Provides a method to add a printer, driver, and port to a machine
 
.PARAMETER DriverName
    The name of the printer driver as defined in the INF file.
 
.PARAMETER DriverInf
    Full path to the Driver INF file
 
.PARAMETER DriverPath
    Path to the folder containing the driver files. (Default is the INF directory)
 
.PARAMETER PrinterIP
    IP of the printer to map
 
.PARAMETER PrinterPort
    Printer's connection port. Default is 9100
 
.PARAMETER PrinterPortName
    Name of the printer port. Defaults to "IP_". If you need to map the printer to a default local port, use the name of the port instead. Such as "FILE:".
 
.PARAMETER PrinterCaption
    Name of the printer
 
.PARAMETER Remove
    Removes the specified printer instead of installing it
 
.PARAMETER DriverOnly
    Only installs the printer driver. Does not attempt to create the port or printer
 
.PARAMETER AllSteps
    Performs Driver install followed by the port and printer mappings. (Default without this and driver only is to only map a printer)
 
.PARAMETER WriteLog
    Writes Debug data to a log file. (Default is true)
 
.PARAMETER RemoveAll
    Removes all mapped printers. (You may need to run this once as the user, and once as an admin for full effect)
 
.PARAMETER LocalPrinter
    Specified that the printer is local to the machine. This is needed if mapping a non-network device. (Such as the XPS printer)
 
.PARAMTER PrinterOnly
    Only maps a printer, does not add a port, nor install a driver.
 
.PARAMETER AdditionalRemovalExclusions
    When used with RemoveAll, prevents the removal of listed printers. By default, Send to One Note, and FAX are excluded.
 
.OUTPUTS
    Log file located at C:\Temp\.log
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -PrinterIP "192.168.1.2"
 
.EXAMPLE
    Install-Printer.ps1 -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -DriverOnly
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -Remove -WriteLog:$false
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -PrinterIP "192.168.1.2" -AllSteps
 
.EXAMPLE
    Install-Printer.ps1 -RemoveAll -AdditionalRemovalExclusions @("My Printer", "Send by E-mail printer")
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption 'Microsoft XPS Document Writer' -PrinterPortName 'FILE:' -DriverName 'Microsoft XPS Document Writer v4' -PrinterOnly -LocalPrinter
 
.NOTES
    Version:        1.3
    Author:         Matthew Thompson
    Creation Date:  2015-12-16
    Purpose/Change: PrinterOnly, and LocalPrinter Features, Printer Removal by IP, Bug fix in event no printers exist and script is run to install one.
    This script was heavily influenced by Kris Powell. (http://www.adminarsenal.com/admin-arsenal-blog/how-to-add-printers-with-powershell)
#>
[CmdletBinding()]
Param
(
    [string]$DriverName, 
    [string]$DriverInf,
    [string]$DriverPath={if ($DriverINF -ne $null){(Split-Path $DriverInf)}}, 
    [string]$PrinterIP, 
    [int]$PrinterPort=9100, 
    [string]$PrinterPortName=("IP_"+$PrinterIP), 
    [string]$PrinterCaption, 
    [Switch]$Remove,
    [switch]$DriverOnly,
    [switch]$AllSteps,
    [switch]$WriteLog=$true,
    [switch]$RemoveAll,
    [string[]]$AdditionalRemovalExclusions=@(),
    [switch]$LocalPrinter,
    [switch]$PrinterOnly
)
 
#Setup the exclusions for RemoveAll
$Exclusions = @("Fax", "Send To OneNote 2013", "Send To OneNote 2010", "Microsoft XPS Document Writer", "Microsoft XPS Document Writer v4")
$Exclusions+=$AdditionalRemovalExclusions
 
$Log=""
 
#Log File Info
$ScriptName = Split-Path -Leaf $PSCommandPath
$LogFile = Join-Path -Path "C:\Windows\Temp" -ChildPath ($ScriptName+".log")
#Do a self-referential lookup to get the script version. As long as the .NOTES section of the file header is up to date and the script is trusted, this should work.
(Get-Help $PSCommandPath -Full -ErrorAction SilentlyContinue).alertset.alert.Text -match "^\s*Version\s*:\s*[\w\d\.\,]*" | Out-Null
if ($matches-ne$null-and$matches.Count-ge1)
{
    $Version = $matches[0] -replace "^\s*Version\s*:\s*",""
}
 
#Write log data
$Log+="Version: "+$Version+"`r`n"
 
#Write Parameters to log
$PSBoundParameters.Keys | ForEach-Object -Process {$Log+=("`""+$_+"`" = `""+$PSBoundParameters[$_]+"`"`r`n")}
 
#Creates a printer object for the user.
function Create-Printer
{
    Param 
    (
        $PrinterCaption, 
        $PrinterPortName, 
        $DriverName,
        [Switch]$Local=$false
    )
    try
    {
        $Printers = @()
        $Printers += Get-WmiObject -Class "Win32_Printer"
        [string]$Log="Installed Printers ("+$Printers.Length.ToString()+") `r`n"
        $Printers | ForEach-Object -Process {$Log+=$_.Name+"`r`n"}
        if ($Printers.Length -eq 0 -or ($Printers.Name).Contains($PrinterCaption) -eq $false)
        {
            $Log+="Attempting to create printer`r`n"
            $Instance = Set-WmiInstance -Class "Win32_Printer" -Arguments @{Caption = $PrinterCaption;DriverName = $DriverName;PortName = $PrinterPortName;DeviceID = $PrinterCaption;Network = (-not $Local)} -ErrorAction Stop
            if ($Instance -ne $null)
            {
                $Log+= "Printer Created`r`n"+$Instance.ToString()+"`r`n"
            }
            else
            {
                $Log+="Install attempted, but printer null`r`n"
            }
        }
        $Log+= "Printer Exists: "+(($Printers.Name).Contains($PrinterCaption)).ToString()+"`r`n"
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Installs a driver from an INF file
function Install-Driver
{
    Param
    (
        $DriverName, 
        $DriverPath, 
        $DriverInf
    )
    $Log=""
    try
    {
        $Drivers = @()
        $Drivers += Get-WmiObject -Class "Win32_PrinterDriver"
        if ($Drivers.Length -eq 0 -or -not ($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")}))
        {
            $Instance = ([wmiclass]"Win32_PrinterDriver").CreateInstance()
            $Instance.Name = $DriverName
            $Instance.DriverPath = $DriverPath
            $Instance.InfName = $DriverInf
            Invoke-WmiMethod -Class "Win32_PrinterDriver" -Name "AddPrinterDriver" -ArgumentList @($Instance) -Impersonation Impersonate -EnableAllPrivileges | Out-Null
            $Log+= $Instance.ToString()+"`r`n"
        }
        else
        {
            $Log+= ("Driver Exists: "+(($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")})).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Creates a printer port required to map a printer
function Create-PrinterPort 
{
    Param
    (
        $PrinterIP, 
        $PrinterPort, 
        $PrinterPortName
    )
    $Log=""
    try
    {
        $PrinterPorts = @()
        $PrinterPorts += Get-WmiObject -Class "Win32_TCPIPPrinterPort"
        if ($PrinterPorts.Length -eq 0 -or -not ($PrinterPorts.Name).Contains($PrinterPortName))
        {
            $Instance = Set-WmiInstance -Class "Win32_TCPIPPrinterPort" -Arguments @{Name = $PrinterPortName;HostAddress = $PrinterIP;PortNumber = $PrinterPort;SNMPEnabled = $false;Protocol = 1}
            $Log+=  $Instance.ToString()+"`r`n"
        }
        else
        {
            $Log+= ("Printer Port Exists: "+(($PrinterPorts.Name).Contains($PrinterPortName)).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
function Get-PrinterPort
{
    Param($IP)
    $Log=""
    $ToReturn = @()
    try
    {
        $PrinterPorts = @()
        $PrinterPorts += Get-WmiObject -Class "Win32_TCPIPPrinterPort"
        if ($PrinterPorts.Length -eq 0 -or -not ($PrinterPorts.HostAddress).Contains($IP))
        {
            #$Instance = Set-WmiInstance -Class "Win32_TCPIPPrinterPort" -Arguments @{Name = $PrinterPortName;HostAddress = $PrinterIP;PortNumber = $PrinterPort;SNMPEnabled = $false;Protocol = 1}
            $Log+= "No Printer Port exists matching $IP"
        }
        else
        {
            $ToReturn = $PrinterPorts | Where-Object -FilterScript {$_.HostAddress -eq $IP}
            $Log+= ("Printer Port Exists: "+(($PrinterPorts.Name).Contains($PrinterPortName)).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $ToReturn
}
 
#Deletes an already mapped printer
function Delete-Printer
{
    Param($PrinterName, $IP)
    $Log=""
    try
    {
        $Printers = @()
        $Printers += Get-WmiObject -Class "Win32_Printer"
        if ($PrinterName -ne $null)
        {
            $Printers = $Printers | Where-Object -FilterScript {$_.Name -eq $PrinterName}
        }
        elseif ($IP -ne $null)
        {
            $Ports = Get-PrinterPort -IP $IP
            $NewPrinterList = @()
            foreach ($Port in $Ports)
            {
                $NewPrinterList += $Printers | Where-Object -FilterScript {$_.PortName -eq $Port.Name}
            }
            $Printers = $NewPrinterList
        }
        else
        {
            $Log+="No parameters specified to limit removal. No printers removed"
            return $Log
        }
        foreach($Printer in $Printers)
        {
            $Printer.Delete() | Out-Null
            $Log+= $Printer.Name+" deleted`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Script Entry
if ($Remove)
{
    Write-Host "Removing $PrinterCaption"
    #Remove a printer selected
    $Log+=Delete-Printer -PrinterName $PrinterCaption -IP $PrinterIP
}
elseif($RemoveAll)
{
    #Remove most printers
    $Printers = @()
    $Printers += Get-WmiObject -Class "Win32_Printer" | Where-Object -FilterScript {$Exclusions.Contains($_.Name) -eq $false}
    if ($Printers.Length -gt 0)
    {
        Write-Host ("Attempting to remove "+$Printers.Length+" printers. You may need to run this script once as admin, and once as the user for all printers to be removed.")
        foreach($Printer in $Printers)
        {
            $Log+=(Delete-Printer -PrinterName $Printer.Name)+"`r`n"
        }
    }
    else
    {
        Write-Host "No printers require removal."
    }
}
else
{
    #Install a printer
    if ($DriverOnly -or $AllSteps)
    {
        #Install the printer Driver
        Write-Host "Installing $DriverName"
        $Log+=(Install-Driver -DriverName $DriverName -DriverPath $DriverPath -DriverInf $DriverInf)+"`r`n"
    }
    if (-not $DriverOnly -and -not $PrinterOnly)
    {
        #Output for user visibility
        Write-Host "Mapping printer ($PrinterCaption) from IP $PrinterIP"
        #Add the printer port
        $Log+=(Create-PrinterPort -PrinterIP $PrinterIP -PrinterPort $PrinterPort -PrinterPortName $PrinterPortName)+"`r`n"
        #Finally, create the printer
        $Log+=(Create-Printer -PrinterCaption $PrinterCaption -PrinterPortName $PrinterPortName -DriverName $DriverName -Local $LocalPrinter)+"`r`n"
    }
    if ($PrinterOnly)
    {
        #Output for user visibility
        Write-Host "Mapping printer ($PrinterCaption) from port $PrinterPortName"
 
        $Log+=(Create-Printer -PrinterCaption $PrinterCaption -PrinterPortName $PrinterPortName -DriverName $DriverName -Local $LocalPrinter)+"`r`n"
    }
}
#Write the log to a file
if ($WriteLog)
{
    $Log | Out-file $LogFile
    Write-Verbose $Log
}

Driver Detection

Edit the “lt;DriverName>” Portion of the script below with the name of your driver. Then add the script into SCCM as your script-based detection method for the INF driver installation.

#Note that this script is not perfect, you may need to change the detection method if you have multiple similiarly named drivers.
$DriverName = "" #Replace this with your driver name
$Drivers = Get-WmiObject -Class "Win32_PrinterDriver"
if (($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")}))
{
    Write-Host "Driver already installed"
}
exit 0

Printer Detection

Edit the “<PrinterName>” portion of the script below with your own printer name. Then add this to your printer deployment as the script-based detection method.

$PrinterCaption = "" #Change this to your printer name
$Printers = Get-WmiObject -Class "Win32_Printer"
if (($Printers.Name).Contains($PrinterCaption))
{
    Write-Host "Printer already installed"
}
exit 0

Remove-Printer (Interactive Printer Removal Tool)

This must be placed in the same directory as Install-Printer.ps1 This will present a user with a list of printers to exclude. The user can then select a printer by inputting it’s number. If they do, it will be excluded. Once finished, the script will remove all other printers.

<# .SYNOPSIS Interactively assists a user in removing printers .DESCRIPTION Interactively assists a user in removing printers .NOTES Version: 1.0 Author: Matthew Thompson Creation Date: 2017-01-08 Purpose/Change: Updated for Blog #>
 
#Printers to exclude by default
$Exclusions = @("Fax", "Send To OneNote 2013", "Send To OneNote 2010")
 
#Grab a list of printers
$Printers = Get-WmiObject -Class "Win32_Printer" | Where-Object -FilterScript {$Exclusions.Contains($_.Name) -eq $false}
 
#Stores additional exclusions by user
$AdditionalExclusions=@()
 
#Infinite loop (Breaks out with user interaction)
while($true)
{
    Write-Host "Printers to remove:"
    #Print all non excluded printers for user to browse
    $NonExcluded=@()
    $NonExcluded += $Printers | Where-Object -FilterScript {$AdditionalExclusions.Contains($_.Name) -eq $false}
    for($I=0;$I -lt $NonExcluded.Length;$I++)
    {
        Write-Host ($I.ToString()+") "+$NonExcluded[$I].Name)
    }
    #Ask user to exlude another printer or continue
    $UInput = Read-Host -Prompt "Please select a printer to exclude or type `"f`" to finish"
    if ($UInput.ToLower() -eq "f")
    {
        #On continue, break out of infinite loop
        break;
    }
    #Otherwise matching selected index to printer, and add.
    $UIndex=$null
    if ([int]::TryParse($UInput, [ref]$UIndex) -and $UIndex -ge 0 -and $UIndex -lt $NonExcluded.Length)
    {
        $AdditionalExclusions+=$NonExcluded[$UIndex].Name
    }
    else
    {
        #In case someone can't type a number
        Write-Host "Input could not be parsed, please try again"
    }
}
#Call the parent script with the selected exclusions
Write-Host "Removing printers"
&".\Install-Printer.ps1" -RemoveAll -AdditionalRemovalExclusions $AdditionalExclusions -WriteLog

MTPing

Code Release

MTPing (or Multi-Threaded Ping) is a powershell script designed to quickly ping a large list of machines.

Parameters

MachineList

This parameter should be a string that points to the location of a list of machines. The list should be formatted so that there is one machine per line.
Example:

192.168.1.1
127.0.0.1
www.google.com
microsoft.com
::1

LogLocation

LogLocation should be a string path representing where the log file should be saved. The log file will be written in a simple tabular format. The first column contains the machine-name or ip as retrieved from the machine list. The second column contains the ping result. “True” meaning connection successful.
Example:

192.168.1.1	False
127.0.0.1	True
www.google.com	True
microsoft.com	True
::1	True

Usage

Inside of a powershell console enter the following:

&"" -MachineList "" -LogLocation ""

Example:

&"C:\Scripts\MTPing.ps1" -MachineList "C:\Scripts\Machines.txt" -LogLocation "C:\Logs\PingResults.txt"

Script


Param
(
    [Parameter(Mandatory=$True)]
    $MachineList,
    [Parameter(Mandatory=$True)]
    $LogLocation
)
 
$ToProcess = @()
$StreamReader = [System.IO.StreamReader] $MachineList
while (-not $StreamReader.EndOfStream)
{
    $ToProcess+=$StreamReader.ReadLine();
}
$StreamReader.Close();
 
$ScriptBlock = 
{
    param
    (
        [PSObject]$InputObject
    )
    return @((Test-Connection $InputObject -quiet), $InputObject)
}
 
$Stream = [System.IO.StreamWriter] $LogLocation
$Pool = [RunspaceFactory]::CreateRunspacePool(1, 10)
$Pool.ApartmentState = "STA"
$Pool.Open()
$Pipes = @()
 
foreach($Object in $ToProcess)
{
    $PipeLine = [System.Management.Automation.PowerShell]::create()
    $PipeLine.RunspacePool = $Pool
    $Mute=$PipeLine.AddScript($ScriptBlock).AddArgument($Object)
    $Pipes+= @{
                Pipe = $PipeLine
                AsyncHandle = $PipeLine.BeginInvoke()
              }
}
 
$Complete=$false;
$Completed=0;
while(-not $Complete)
{
    $Found = $false;
    for ($Index=0; $Index -lt $Pipes.Count; $Index++)
    {
        if ($Pipes[$Index] -ne $null)
        {
            $Found=$true;
            if ($Pipes[$Index].AsyncHandle.IsCompleted)
            {
                $Result = $Pipes[$Index].Pipe.EndInvoke($Pipes[$Index].AsyncHandle)
                $Pipes[$Index].Pipe.Dispose()
 
                if ($Result[0])
                {
                    Write-Host $Result[1] -ForegroundColor Green -NoNewline
                    Write-Host "`t"($Completed/$Pipes.Count*100)"%"
                }
                else
                {
                    Write-Host $Result[1] -ForegroundColor Red -NoNewline
                    Write-Host "`t"($Completed/$Pipes.Count*100)"%"
 
                }
 
                $Stream.WriteLine($Result[1]+"`t"+$Result[0].ToString());
                $Pipes[$Index] = $null;
                $Completed++;
            }
        }
    }
    if (-not $Found)
    {
        $Complete = $true;
    }
}
$Stream.Close();
$Pool.Close();