Tag Archives: PowerShell

Get-PrintJobs.ps1 PowerShell script

After a recent upgrade of our print servers, I discovered that the Print Spooler service event logging had been enhanced, and changed enough that some PowerShell reporting scripts that worked just fine on Windows Server 2008 (32-bit) no longer worked on Server 2012 R2.

To get the reports working again, I had to enable the Microsoft-Windows-PrintService/Operational log. I also had to increase the log size from the default in order to retain more than one day’s events. The trickiest part was figuring out the XPath query syntax for retrieving events from a particular printer. The newer syntax makes more sense to me, but it took me a long time to arrive at it.

Following Don Jones‘ entreaty to build tools and controllers, I offer this tool script, which retrieves (simplified) print job events, and cares not a whit about formatting or saving.


<#
.SYNOPSIS
Gets successful print job events and returns simplified objects with relevant
details.

.DESCRIPTION
Collects the successful print jobs from the PrintService Operational log, with
optional query parameters including Printer name and start and end times.

.PARAMETER PrinterName
The share name of the printer for which events will be retrieved.

.PARAMETER StartTime
The beginning of the interval during which events will be retrieved.

.PARAMETER EndTime
The end of the interval during which events will be retrieved.

.EXAMPLE
C:\> Get-PrintJobs.ps1
Returns objects representing all the successful print jobs (events with id 307).

.EXAMPLE
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet'
Returns objects for all the jobs on the Accounting printer.

.EXAMPLE
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet' -StartTime (Get-Date).AddHours(-12)
Returns objects for all the jobs on the Accounting printer generated in the last twelve hours.
.NOTES
Script Name: Get-PrintJobs.ps1
Author : Geoff Duke <Geoffrey.Duke@uvm.edu>

Edit 2014-10-08: Generalizing from dept printer report script, fixing XPath
query syntax.

Edit 2012-11-29: Job is run as SYSTEM, and computer object has been granted
Modify rights to the destination directory.
#>

Param(
[string] $PrinterName,

[datetime] $StartTime,

[datetime] $EndTime
)
Set-StrictMode -version latest
# Building XPath query to select the right events
$filter_start = @'
<QueryList>
  <Query Id="0" Path="Microsoft-Windows-PrintService/Operational">
    <Select Path="Microsoft-Windows-PrintService/Operational">
'@

$filter_end = @'
    </Select>
  </Query>
</QueryList>
'@

$filter_match = '*[System[(EventID=307)' #need to add ']]' to close

if ( $StartTime -or $EndTime) {
    $filter_match += ' and TimeCreated[' #need to add ']' to close
    $time_conds = @()

    if ( $StartTime ) {
        $time_conds += ( '@SystemTime&gt;=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $StartTime.ToUniversalTime()
        )
    }
    if ( $EndTime ) {
        $time_conds += ( '@SystemTime&lt;=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $EndTime.ToUniversalTime()
        )
    }

    $filter_match += ( $time_conds -join ' and ' ) + ' ]' # Closing TimeCreated[
}

$filter_match += "]]`n" # Closing [System[

if ( $PrinterName ) {
    $filter_match += @"
  and
*[UserData[DocumentPrinted[(Param5='$PrinterName')]]]
"@
}

write-debug "Using Filter:`n $filter_match"

# The $filter variable below is cast as XML, that's getting munged
# by WordPress or the SyntaxHighlighter as '1'
1 $filter = ($filter_start + $filter_match + $filter_end)

get-winevent -filterXML $filter | foreach {
    $Properties = @{
        'Time' = $_.TimeCreated;
        'Printer' = $_.Properties[4].value;
        'ClientIP' = $_.properties[3].value.SubString(2);
        'User' = $_.properties[2].value;
        'Pages' = [int] $_.properties[7].value;
        'Size' = [int] $_.properties[6].value
    }

    New-Object PsObject -Property $Properties
}

If you find this script useful, please let me know. If you find any bugs, definitely let me know!

Set default printer with PowerShell

Closely related to my previous post, this simple script uses a WScript.Network COM object to set the default printer. The comment block is longer than the script, but I think it’s a useful little tool.

<#
.SYNOPSIS
Sets a Network Printer connection as the default printer.

.DESCRIPTION
Uses a COM object to sets the specified, installed printer as the default. If
an error is encountered, e.g., the specified printer isn't installed, the
exception is written to a file called Set-DefaultPrinter.err in the current
$env:temp directory, and then the script terminates, throwing the exception.

Based on my colleague's VBScript solution:

http://blog.uvm.edu/jgm/2014/06/11/parting-scripts-add-a-new-network-printer-and-set-it-as-default/

.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560

.EXAMPLE
Set-DefaultPrinter.ps1 -PrinterShare '\\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560'

.NOTES
    Script Name: Set-DefaultPrinter.ps1
    Author     : Geoff Duke <Geoffrey.Duke@uvm.edu>
#>

[cmdletbinding()]

Param(
    [Parameter(Mandatory=$true,
        HelpMessage="Enter the UNC path to the network printer")]
    [ValidateNotNullOrEmpty()]
    [string] $PrinterShare
)

Set-PSDebug -Strict

$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}

$ws_net = New-Object -COM WScript.Network

try {
    $ws_net.SetDefaultPrinter($PrinterShare)
}
catch {
    $error[0].exception | out-file (join-path $env:temp 'Set-DefaultPrinter.err') 
    throw $error[0]
}
write-verbose "Default printer now $PrinterShare"

Add network printer with PowerShell

This is my PowerShwell translation of my colleague’s VBScript solution for mapping network printers with a script.

<#
.SYNOPSIS
Add a Network Printer connection, optionally making it the default printer.

.DESCRIPTION
Uses a COM object to add a Network Printer, and optionally sets that printer
as the default. If an error is encountered, the exception is written to a
file called Add-NetworkPrinter.err in the current $env:temp directory, and then
the script terminates.

This is my PowerShell translation of my colleague's VBScript solution:

http://blog.uvm.edu/jgm/2014/06/11/parting-scripts-add-a-new-network-printer-and-set-it-as-default/

.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560

.PARAMETER Default
Specifies that the printer will also be set as the default printer for the current user.

.EXAMPLE
Add-NetworkPrinter.ps1 -PrinterShare '\\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560' -Default

.NOTES
    Script Name: Add-NetworkPrinter.ps1
    Author     : Geoff Duke <Geoffrey.Duke@uvm.edu>
#>

[cmdletbinding()]

Param(
    [Parameter(Mandatory=$true,
        HelpMessage="Enter the UNC path to the network printer")]
    [ValidateNotNullOrEmpty()]
    [string] $PrinterShare,

    [parameter(Mandatory=$false)]
    [switch] $Default
)

Set-PSDebug -Strict

$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}

$ws_net = New-Object -COM WScript.Network

write-verbose "Adding connection to $PrinterShare"
try {
    $ws_net.AddWindowsPrinterConnection($PrinterShare)
}
catch {
    $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
    throw $error[0]
}

write-verbose "Setting the printer as the default"
if ( $Default ) {
    try {
        $ws_net.SetDefaultPrinter($PrinterShare)
    }
    catch {
        $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
        throw $error[0]
    }
}

# the end

For use with Group Policy, it will probably be helpful to create a simple Set-DefaultPrinter.ps1 script. But that’s just the second stanza from the script above.

Mark Minasi’s talk: The Case for Powershell

With Mark’s permission, I’m mirroring the video of his recent web talk “The Case for PowerShell: Why You Should Learn-PowerShell So You Needn’t Leave-Industry.” Read more about it in his recent newletter.

He shared it via Skydrive, but there are some capacity constraints in place that have been blocking access. (Really, Microsoft‽‽ You’ve suspended Mark Minasi’s SkyDrive account‽)

Hopefully, folks can download it here, or watch it online:

The Case for PowerShell

Thanks, Mark, for your evangelism and expertise.

Geoff

July 2, 2013

Wondering about the physical memory configuration of a system?

PS> gwmi Win32_PhysicalMemory | ft DeviceLocator,@{l='Size (GB)';e={$_.Capacity / 1GB} } -a

Geoff

June 23, 2013

Camera named files like this: P1050640.JPG. To rename the all files in the directory, I used the following:

PS> $i = 1; gci | % { ren $_ $( '2013 Pool Setup {0:D2}.JPG' -f $i++ ) }

Scripting printer permissions

I will probably need to refer to this myself, but maybe someone else will find this useful.

Still working through some print queue management tasks, I needed to change permissions on a collection of printers. Yesterday, I broke down and did a bunch of repetitive work in the GUI, and noticed there were some inconsistencies in the permissions applied to a clients print queues. Today, I decided that I would fix them, but I didn’t want to use the GUI.

Although the Server 2012 Printer Management Set-Printer cmdlet can change the permissions through its -PermissionSDDL parameter, I haven’t found a workable (even if you call SDDL workable) way to do so on a Server 2008 system.

So I fell back to using PowerShell with Helge Klein’s SetACL command-line tool. I could also have used his COM version if I wanted to be really efficient. I use an array of arguments to the external command. See this Scripting Guy post for more info on calling external commands.

Here’s what I did:

# Retrieve a list of target printers
PS C:> $rlprinters = gwmi Win32_Printer -filter "Name like 'RL -%' OR Name like 'RL-%' "

# Construct a list of printer names in \\[hostname]\[sharename] format
PS C:> $printers = $rlprinters | % { '\\' + $_.SystemName + '\' + $_.ShareName }

# Create some variables to make the command construction simpler
PS C:> $setacl = 'C:\local\bin\SetACL.exe'
PS C:> $group = 'DOMAIN\RL-PrinterGroup'

# Iterate through the list of printers, calling SetACL for each
PS C:> foreach ( $p in $printers ) {
>> & $setacl '-on', $p , '-ot', 'prn', '-actn', 'ace','-ace',"n:$group;p:full"
}

It worked like a champ.

Note that I needed to run an elevated PowerShell session (run as Administrator) to be able to make the changes. Also, I tested this on a single printer before attempting to make bulk changes. I also have a nightly dump of my printer configuration. You get the idea.

You can include multiple actions for each invocation of SetACL, and there are a number of ways to collect output in case you need to follow-up on problems when applying changes to large collections of printers. Hopefully, this is enough to get you started,

Printer management with PowerShell

I find the printer management scripts included with Server 2008 bothersome. However, until we get to Server 2012, with its shiny new printer management cmdlets, I’ll keep trying to make do with what we have.

I was pleased to find an old post on the Printing Team’s blog about using the .Net System.Printing namespace with PowerShell to automate things. Some brief experimenting looks promising.

For the moment, in order to switch the printer driver version on about fifteen printers (all the same model), I used PowerShell and WMI, like this:

PS C:\> $printers = Get-WmiObject Win32_Printer -filter "Name like 'RL -%'"
PS C:\> $printers | sort Name | ft Name, DriverName -a
PS C:\> $printers | Where DriverName -eq 'HP Universal Printing PCL 6 (v5.3)' | % {
>> $_.DriverName = 'HP Universal Printing PCL 6 (v5.6.1)'
>> $_.Put()
>> }

This took quite a while to execute, but did work. What I need to do now is modify the permissions on each printQueue. I doubt I can leverage Get-ACL and Set-ACL for this.

Printer drivers and architectures with PowerShell

We have a number of 32-bit Windows 2008 print servers that we want to migrate to Windows Server 2012, for the printer management PowerShell cmdlets, among other things. I found a helpful blog post about using the PRINTBRM utility to migrate print queues, which mentions that you need to have both 32-bit and 64-bit drivers versions of all the drivers in order to migrate from a 32-bit to a 64-bit OS instance.

I wrote a little script to quickly show me which print drivers need a 64-bit version installed. It can take a moment to run if you have many printers configured.

UPDATE: I made a couple changes, most notably that the count of printers using a driver is now optional (since it can take a while on a system with lots of printers).

<#
.SYNOPSIS
Lists printer drivers,  whether 32- and 64-bit versions are installed,
and how many printers are using each driver.


.PARAMETER Printers
Includes a counter of the printers using each printer driver. On a system
with many installed printers, this can take a a little time, so this 
functionality is optional.


.EXAMPLE
PS C:\local\scripts> .\Get-PrinterDriverArchitecture.ps1 | format-table -auto

Name                                       x86   x64
----                                       ---   ---
Brother HL-5250DN                         True False
Epson LQ-570+ ESC/P 2                     True False
HP Business Inkjet 2230/2280              True False
HP Business Inkjet 2250 (PCL5C)           True  True
HP Business Inkjet 2800 PCL 5             True False

This command lists the installed printer drivers, and whether 32-bit (x86)
or 64-bit (x64) drivers are available.


.EXAMPLE
PS C:\local\scripts> .\Get-PrinterDriverArchitecture.ps1 | where x64 -eq $false | ft -a

Name                                      x86   x64
----                                      ---   ---
Brother HL-5250DN                        True False
Epson LQ-570+ ESC/P 2                    True False
HP Business Inkjet 2230/2280             True False
HP Business Inkjet 2800 PCL 5            True False

This command uses the Where[-Object] cmdlet to filter out those drivers that
have a 64-bit driver installed.


.EXAMPLE
PS C:\local\scripts> .\Get-PrinterDriverArchitecture.ps1 -Printers | ft -a
Enumerating printers:
    WID - Kyocera TASKalfa 3050ci KX
    WC - Kyocera TASKalfa 300ci KX
    UFS - Canon iR-ADV C5035
    ...

Name                                       x86   x64 Printers
----                                       ---   --- --------
Brother HL-5250DN                         True False        5
Epson LQ-570+ ESC/P 2                     True False        0
HP Business Inkjet 2230/2280              True False        1
HP Business Inkjet 2250 (PCL5C)           True  True        0
HP Business Inkjet 2800 PCL 5             True False        1

This command includes the -Printers switch parameter to add a count of
the printers using each driver. Enumerating the printers can take a while
if there are lots of them installed, so this behavior is optional.


.NOTES
 - Author    : Geoffrey.Duke@uvm.edu
 - Mod. Date : May 28, 2013
#>

param( [switch] $printers )

$wmi_drivers = get-wmiobject Win32_PrinterDriver -Property Name
$drivers = @{}
foreach ($driver in $wmi_drivers) {
    # Isolate the driver name and platform
    $name,$null,$platform = $driver.Name -split ','

    if ( -not $drivers[$name] ) {
        switch ( $platform ) {
            'Windows NT x86' { $drivers[$name] = [ordered]@{ 
                                 'Name'=$name; 'x86'=$true; 'x64'=$false }; break }
            'Windows x64'    { $drivers[$name] = [ordered]@{ 
                                 'Name'=$name; 'x86'=$false; 'x64'=$true }; break }
             default         { write-warning "Unexpect platform $platform on driver $name"}
        }
    }
    else {
        switch ( $platform ) {
            'Windows x64'    { $drivers[$name]['x64'] = $true; break }
            'Windows NT x86' { $drivers[$name]['x86'] = $true; break }
             default         { write-warning "Unexpect platform $platform on driver $name"}
        }
    }
}

if ( $printers ) {
    # Initialize all printer counts
    $drivers.keys | foreach { $drivers[$_]['Printers'] = 0 }
    # Add a count of the number of printers using each driver
    # With some progress info
    write-host 'Enumerating printers:'
    $count = 0
    get-wmiobject Win32_Printer -Property Name,DriverName | foreach { 
        write-host "    $($_.Name)"  -foreground darkgray
        $drivers[$_.DriverName]['Printers']++ 
        $count++
    }

    write-host "Retrieved $count printers"
}

# Output collection of objects
$drivers.keys | sort | foreach { New-Object PSObject -Property $drivers[$_] } 
 

I hope this is useful to others.

Which Disk is that volume on?

I administer a server VM with a lot of disks, and many of them are the same size. When I need to make changes to the system’s storage, I’m always nervous that I’m going to poke the wrong disk. I could trust that the order of the disks listed in the vSphere client is the same as the order that the guest OS lists (starting at 1 and 0 respectively). But I want a little more assurance.

Using diskpart, you can list the details for individual disks, partitions and volumes, but I wanted a report showing all the disks, the partitions on those disks, and the volumes residing on those partitions. I have reported some of this info previously, using PowerShell’s Get-WMIObject cmdlet to query the Win32_DiskDrive, Win32_Partition, and Win32_Volume classes. I figured there must me a way to correlate instances of these classes.

I found these two blog posts:

They did most of the heavy lifting in building the WQL ASSOCIATOR OF queries. I put together a short script to give me a little more detail. Here’s some sample output:

PS C:\local\scripts> .\Get-DiskInfo.ps1
Disk 0 - SCSI 0:0:2:0 - 45.00 GB
    Partition 0  100.00 MB  Installable File System
    Partition 1  44.90 GB  Installable File System
        C: [NTFS] 44.90 GB ( 3.46 GB free )
[...]
Disk 5 - SCSI 0:0:2:5 - 39.99 GB
    Partition 0  40.00 GB  Installable File System
        B: [NTFS] 40.00 GB ( 34.54 GB free )

This will make it easier to be sure about the vSphere storage element that corresponds to a particular volume (or, more accurately, the Physical Disk on which the volume resides).

Here’s the actual script:

<#
.SYNOPSIS
Get information about the physical disks and volumes on a system.

.DESCRIPTION
Get details about the physical disks and the volumes located on
those disks, to make it easier to identify corresponding vSphere
storage (VMDKs).

.EXAMPLE

PS C:\> .\Get-DiskInfo.ps1

.NOTES
    Author: Geoff Duke <Geoffrey.Duke@uvm.edu>
    Based on http://bit.ly/XowLns and http://bit.ly/XeIqFh
#>

Set-PSDebug -Strict

Function Main {

    $diskdrives = get-wmiobject Win32_DiskDrive | sort Index

    $colSize = @{Name='Size';Expression={Get-HRSize $_.Size}}

    foreach ( $disk in $diskdrives ) {

        $scsi_details = 'SCSI ' + $disk.SCSIBus         + ':' +
                                  $disk.SCSILogicalUnit + ':' +
                                  $disk.SCSIPort        + ':' +
                                  $disk.SCSITargetID
        write $( 'Disk ' + $disk.Index + ' - ' + $scsi_details +
                 ' - ' + ( Get-HRSize $disk.size) )

        $part_query = 'ASSOCIATORS OF {Win32_DiskDrive.DeviceID="' +
                      $disk.DeviceID.replace('\','\\') +
                      '"} WHERE AssocClass=Win32_DiskDriveToDiskPartition'

        $partitions = @( get-wmiobject -query $part_query | 
                         sort StartingOffset )
        foreach ($partition in $partitions) {

            $vol_query = 'ASSOCIATORS OF {Win32_DiskPartition.DeviceID="' +
                         $partition.DeviceID +
                         '"} WHERE AssocClass=Win32_LogicalDiskToPartition'
            $volumes   = @(get-wmiobject -query $vol_query)

            write $( '    Partition ' + $partition.Index + '  ' +
                     ( Get-HRSize $partition.Size) + '  ' +
                     $partition.Type
                   )

            foreach ( $volume in $volumes) {
                write $( '        ' + $volume.name + 
                         ' [' + $volume.FileSystem + '] ' + 
                         ( Get-HRSize $volume.Size ) + ' ( ' +
                         ( Get-HRSize $volume.FreeSpace ) + ' free )'
                       )

            } # end foreach vol

        } # end foreach part

        write ''

    } # end foreach disk

}

#--------------------------------------------------------------------
function Get-HRSize {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True, ValueFromPipeline=$True)]
        [INT64] $bytes
    )
    process {
        if     ( $bytes -gt 1pb ) { "{0:N2} PB" -f ($bytes / 1pb) }
        elseif ( $bytes -gt 1tb ) { "{0:N2} TB" -f ($bytes / 1tb) }
        elseif ( $bytes -gt 1gb ) { "{0:N2} GB" -f ($bytes / 1gb) }
        elseif ( $bytes -gt 1mb ) { "{0:N2} MB" -f ($bytes / 1mb) }
        elseif ( $bytes -gt 1kb ) { "{0:N2} KB" -f ($bytes / 1kb) }
        else   { "{0:N} Bytes" -f $bytes }
    }
} # End Function:Get-HRSize

Main

Please let me know if you find this helpful.