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.
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.
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
Multi-Shutdown is intended to help automate the shut-down process of servers or machines that are sensitive to the order that the shut-downs occur. It can also be used simply as a visual reference of a machine’s connectivity status.
GarbleLogs grabs Windows event logs from all providers and compiles them into a single CMTrace compliant file for review. The events are sorted by time and the range is specified by the user.
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.
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.
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.
To create an INF file Application deployment. Create a new application deployment. Set the command to
For the detection method, us the “Driver Detection” script in the “script” section of this page.
Create an application package for your printer.
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’
Set the detection method to the “Printer Detection” script in the “script” section of this page.
Set this package to install for the user.
Add a requirement that points to the appropriate driver application package that you created earlier.
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 (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:
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:
The script is fairly simple. You only need to configure the top 2 lines. First set $FilePath to the location of the pre-existing file. Then set $ExpectedHash to the MD5 hash of the deployed file. You can get the MD5 hash from powershell using Get-Hash in Powershell 4.0 or, if you are not up to 4.0, by using this Get-Hash.
General Usage
Inside of a powershell console enter the following:
&""
Example:
&"C:\Scripts\HashDetect.ps1"
SCCM Usage
After configuration, import the script as a new Detection Method for your deployment.
Script
$FilePath = "C:\ProgramData\MyApp\MySettings.xml"
$ExpectedHash = "00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00"
if (-not (Test-Path $FilePath))
{
#File path not found. Not installed.
exit 0
}
try
{
$FileStream = New-Object IO.FileStream $FilePath, "Open"
$MD5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
$Hash = [System.BitConverter]::ToString($MD5.ComputeHash($FileStream))
}
catch
{
Write-Error $_.ToString()
exit 1
}
#Compare the hash with a known hash
if ($Hash -eq $ExpectedHash)
{
Write-Output "Hash match. File already installed."
exit 0
}
else
{
#Hash MisMatch. Not installed
exit 0
}