Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion AddPrinter-Template.plist
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,33 @@ sys.exit(1)
<string>10.7.0</string>
<key>name</key>
<string>AddPrinter_DISPLAYNAME</string>
<key>postinstall_script</key>
<key>preinstall_script</key>
<string>#!/usr/local/munki/munki-python
import subprocess
import sys

# Install the printer
cmd = [ '/usr/local/wycomco/airprint-ppd/airprint-ppd.zsh',
'-p', 'ADDRESS',
'-n', 'PRINTERNAME' ]

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
(airprintPpdOut, airprintPpdErr) = proc.communicate()

if airprintPpdErr:
print("Error: %s" % airprintPpdErr)

print("Results: %s" % airprintPpdOut)

print("Return Code: %s" % proc.returncode)

if proc.returncode != None:
sys.exit(proc.returncode)
else:
print("Error: airprint-ppd exit code not recognized.")
sys.exit(543)
</string>
<key>postinstall_script</key>
<string>#!/usr/local/munki/munki-python
import subprocess
import sys
Expand Down
261 changes: 68 additions & 193 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,118 @@
# Update: The official project is now archived and is not being developed any further. This fork received an update for macOS 10.14 & 10.15 and uses the munki-included python 3 interpreter. It might show an error on systems not using english or german on the commandline but still successfully install all printers.
# Please be aware that this copy might not be under constant development.

PrinterGenerator
================
# PrinterGenerator

This script will generate a ["nopkg"](https://groups.google.com/d/msg/munki-dev/hmfPZ7sgW6k/q6vIkPIPepoJ) style pkginfo file for [Munki](https://github.com/munki/munki/wiki) to install a printer.

See [Managing Printers with Munki](https://github.com/munki/munki/wiki/Managing-Printers-With-Munki) for more details.

This is a fork of [nmcspadden/PrinterGenerator](https://github.com/nmcspadden/PrinterGenerator), initiated by [jutonium](https://github.com/jutonium).

## Enhancements

This updated version implements some cool new things:

* Support for macOS 10.14, 10.15, 11 and 12
* Usage of the Munki-included Python3
* Enhances usage of Microsoft Excel to edit the CSV file: for regions which use the comma as the decimal separator, Microsoft Excel expects a semicolon as separator in CSV files. The script will distinguish between both variants.
* The order of the csv columns do not have to be preserved, but **keep the names of the 1st row**.
* Sanity checks for the csv fields
* Option to setup printers using AirPrint provided PPDs, using [airprint-ppd](https://github.com/wycomco/airprint-ppd)
* Option to define a path to Munki repo and an optional subdirectory
* Option to define a separate name for the Munki pkginfo item
* Option to define a Munki category
* **This script should preserve backward compatibility!**

## Caveats

It might show an error on systems not using English or German on the command line but still successfully install all printers.

## Usage

The script can either take arguments on the command line, or a CSV file containing a list of printers with all the necessary information.

The script will generate a pkginfo file. This pkginfo file is a "nopkg" style, and thus has three separate scripts:

* installcheck_script
* preinstall_script
* postinstall_script
* uninstall_script

The installcheck_script looks for an existing print queue named PRINTERNAME. If it does not find one, it will exit 0 and trigger an installation request. If it does find one, it will compare all of the options provided (DRIVER, ADDRESS, DISPLAYNAME, LOCATION, and OPTIONS) for differences. If there are any differences, it will trigger an installation request.

The preinstall_script is used to retrieve the PPD for the printer on the fly, in case the given printer should be setup using AirPrint. In all other cases, this script will be completely removed.

The postinstall_script will attempt to delete the existing print queue named PRINTERNAME first, and then will reinstall the queue with the specified options.
*Note that it does not check to see if the printer queue is in use at the time, so it is possible that existing print jobs will be cancelled if a user is printing when a Munki reinstall occurs.*

The uninstall_script will delete the printer queue named PRINTERNAME if uninstallation is triggered.

### Using a CSV file:

A template CSV file is provided to make it easy to generate multiple pkginfos in one run. Pass the path to the csv file with `--csv`:
A template CSV file is provided to make it easy to generate multiple pkginfos in one run. Pass the path to the csv file with `--csv`:

```
./print_generator.py --csv /path/to/printers.csv
```
*Note: if a CSV file is provided, all other command line arguments are ignored.*

*Note: if a CSV file is provided, all other command line arguments – besides the optional `--repo` – are ignored.*

The CSV file's columns should be pretty self-explanatory:

* Printer name: Name of the print queue
* Location: The "location" of the printer
* Display name: The visual name that shows up in the Printers & Scanners pane of the System Preferences, and in the print dialogue boxes. Also used in the Munki pkginfo.
* Address: The IP or DNS address of the printer. The template uses the form: `lpr://ADDRESS`. Change to another protocol in the template if necessary.
* Driver: Name of the driver file in /Library/Printers/PPDs/Contents/Resources/.
* Location: The "location" of the printer as displayed in System Preferences
* Display name: The visual name that shows up in the Printers & Scanners pane of the System Preferences, and in the print dialogue boxes. Also used in the Munki pkginfo.
* Address: The IP or DNS address of the printer. If no protocol is specified, we expect `lpd://`
* Driver: Name of the driver file in `/Library/Printers/PPDs/Contents/Resources/`, an absolute path to a ppd file or `airprint-ppd` for generic [AirPrint printers](https://support.apple.com/en-us/HT201311) (requires [airprint-ppd](https://github.com/wycomco/airprint-ppd))
* Description: Used only in the Munki pkginfo.
* Options: Any printer options that should be specified. These **must** be space-delimited key=value pairs, such as "HPOptionDuplexer=True OutputMode=normal". **Do not use commas to separate the options, because this is a comma-separated values file.**
* Version: Used only in the Munki pkginfo.
* Requires: Required packages for Munki pkginfo. These **must** be space-delimited, such as "CanonDriver1 CanonDriver2".
* Options: Any printer options that should be specified. These **must** be space-delimited key=value pairs, such as "HPOptionDuplexer=True OutputMode=normal". **Do not use commas to separate the options.**
* Version: Used only in the Munki pkginfo, defaults to `1.0`
* Requires: Required packages for Munki pkginfo. These **must** be space-delimited, such as "CanonDriver1 CanonDriver2". Be sure to add a reference to airprint-ppd to setup your printer via AirPrint.
* Icon: Optionally specify an existing icon in the Munki repo to display for the printer in Managed Software Center.
* Catalogs: Space separated list of Munki catalogs in which this pkginfo should be listed
* Category: Populates the Munki category, defaults to `Printers`
* Subdirectory: Subdirectory inside Munki's pkgsinfo directory, only used if `--repo` is defined.
* Munki Name: A specific name for this pkgsinfo item. This defaults to `AddPrinter_Printer Name`

The CSV file is not sanity-checked for invalid entries or blank fields, so double check your file and test your pkginfos thoroughly.

### Command-line options:
### Command-line options

A full description of usage is available with:

```
./print_generator.py -h
usage: print_generator.py [-h] [--printername PRINTERNAME] [--driver DRIVER]
[--address ADDRESS] [--location LOCATION]
[--displayname DISPLAYNAME] [--desc DESC]
[--requires REQUIRES]
[--options [OPTIONS [OPTIONS ...]]]
[--version VERSION] [--icon ICON] [--csv CSV]
usage: print_generator.py [-h] [--printername PRINTERNAME] [--driver DRIVER] [--address ADDRESS] [--location LOCATION] [--displayname DISPLAYNAME] [--desc DESC] [--requires REQUIRES] [--options [OPTIONS ...]] [--version VERSION] [--icon ICON] [--catalogs CATALOGS] [--category CATEGORY] [--munkiname MUNKINAME] [--repo REPO] [--subdirectory SUBDIRECTORY] [--csv CSV]

Generate a Munki nopkg-style pkginfo for printer installation.

optional arguments:
-h, --help show this help message and exit
--printername PRINTERNAME
Name of printer queue. May not contain spaces, tabs, #
or /. Required.
--driver DRIVER Name of driver file in
/Library/Printers/PPDs/Contents/Resources/. Can be
relative or full path. Required.
--address ADDRESS IP or DNS address of printer. If no protocol is
specified, defaults to lpd://. Required.
--location LOCATION Location name for printer. Optional. Defaults to
printername.
Name of printer queue. May not contain spaces, tabs, # or /. Required.
--driver DRIVER Either the name of driver file in /Library/Printers/PPDs/Contents/Resources/ (relative or full path) or \'airprint-ppd\' for
AirPrint printers. Required.
--address ADDRESS IP or DNS address of printer. If no protocol is specified, defaults to lpd://. Required.
--location LOCATION Location name for printer. Optional. Defaults to printername.
--displayname DISPLAYNAME
Display name for printer (and Munki pkginfo).
Optional. Defaults to printername.
Display name for printer (and Munki pkginfo). Optional. Defaults to printername.
--desc DESC Description for Munki pkginfo only. Optional.
--requires REQUIRES Required packages in form of space-delimited
'CanonDriver1 CanonDriver2'. Optional.
--options [OPTIONS [OPTIONS ...]]
Printer options in form of space-delimited
'Option1=Key Option2=Key Option3=Key', etc. Optional.
--version VERSION Version number of Munki pkginfo. Optional. Defaults to
1.0.
--icon ICON Name of exisiting icon in Munki repo. Optional.
--csv CSV Path to CSV file containing printer info. If CSV is
provided, all other options are ignored.
--requires REQUIRES Required packages in form of space-delimited 'CanonDriver1 CanonDriver2'. Be sure to add a reference to airprint-ppd
to setup your printer via AirPrint.Optional.
--options [OPTIONS ...]
Printer options in form of space-delimited 'Option1=Key Option2=Key Option3=Key', etc. Optional.
--version VERSION Version number of Munki pkginfo. Optional. Defaults to 1.0.
--icon ICON Specifies an existing icon in the Munki repo to display for the printer in Managed Software Center. Optional.
--catalogs CATALOGS Space delimited list of Munki catalogs. Defaults to 'testing'. Optional.
--category CATEGORY Category for Munki pkginfo only. Optional. Defaults to 'Printers'.
--munkiname MUNKINAME
Name of Munki item. Defaults to printername. Optional.
--subdirectory SUBDIRECTORY
Subdirectory of Munki's pkgsinfo directory. Optional.
--repo REPO Path to Munki repo. If specified, we will try to write directly to its containing pkgsinfo directory. If not defined, we will write to current working directory. Optional.
--csv CSV Path to CSV file containing printer info. If CSV is provided, all other options besides '--repo' are ignored.
```

As in the above CSV section, the arguments are all the same:

* `--printername`: Name of the print queue. May not contain spaces, tabs, "#" or "/" characters. **Required.**
* `--driver`: Name of the driver file in /Library/Printers/PPDs/Contents/Resources/. This can be either a relative path (i.e. the filename in the path above), or a full path (starting with "/Library"). **Required.**
* `--driver`: Either the name of driver file in /Library/Printers/PPDs/Contents/Resources/ (relative or full path) or \'airprint-ppd\' for AirPrint printers. **Required.**
* `--address`: The IP or DNS address of the printer. If no protocol is specified, `lpd://ADDRESS` will be used. **Required.**
* `--location`: The "location" of the printer. If not provided, this will default to the value of `--printername`.
* `--displayname`: The visual name that shows up in the Printers & Scanners pane of the System Preferences, and in the print dialogue boxes. Also used in the Munki pkginfo. If not provided, this will default to the value of `--printername`.
Expand All @@ -104,8 +121,13 @@ As in the above CSV section, the arguments are all the same:
* `--options`: Any number of printer options that should be specified. These should be space-delimited key=value pairs, such as "HPOptionDuplexer=True OutputMode=normal".
* `--version`: The version number of the Munki pkginfo. Defaults to "1.0".
* `--icon`: Used only in the Munki pkginfo. If not provided, will default to an empty string ("").
* `--catalogs`: Space delimited list of Munki catalogs. Defaults to 'testing'. Optional.
* `--category`: Name of the Munki category. Defaults to 'Printers'. Optional.
* `--munkiname`: Name of Munki item. Defaults to printername. Optional.
* `--subdirectory`: Subdirectory of Munki's pkgsinfo directory. Optional.
* `--repo`: Path to Munki repo. If specified, we will try to write directly to its containing pkgsinfo directory. If not defined, we will write to current working directory. Optional.

### Figuring out options:
### Figuring out options

Printer options can be determined by using `lpoptions` on an existing printer queue:
`/usr/bin/lpoptions -p YourPrinterQueueName -l`
Expand All @@ -128,150 +150,3 @@ Despite `lpoptions` using a "Name/Nice Name: Value *Value Value" format, the opt
This is the format you must use when passing options to `--options`, or specifying them in the CSV file.

*Note that `/usr/bin/lpoptions -l` without specifying a printer will list options for the default printer.*


### Example:
```
./print_generator.py --printername="MyPrinterQueue" \
--driver="HP officejet 5500 series.ppd.gz" \
--address="10.0.0.1" \
--location="Tech Office" \
--displayname="My Printer Queue" \
--desc="Black and white printer in Tech Office" \
--requires="CanonPrinterDriver" \
--options="HPOptionDuplexer=True OutputMode=normal" \
--icon="HP LaserJet 4250.icns" \
--version=1.5
```

The output pkginfo file generated:

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>autoremove</key>
<false/>
<key>catalogs</key>
<array>
<string>testing</string>
</array>
<key>description</key>
<string>Black and white printer in Tech Office</string>
<key>display_name</key>
<string>My Printer Queue</string>
<key>icon_name</key>
<string>HP LaserJet 4250.icns</string>
<key>installcheck_script</key>
<string>#!/usr/bin/python
import subprocess
import sys
import shlex

printerOptions = { "HPOptionDuplexer":"True", "OutputMode":"normal" }

cmd = ['/usr/bin/lpoptions', '-p', 'MyPrinterQueue', '-l']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(lpoptLongOut, lpoptErr) = proc.communicate()

# lpoptions -p printername -l will still exit 0 even if printername does not exist
# but it will print to stderr
if lpoptErr:
print lpoptErr
sys.exit(0)

cmd = ['/usr/bin/lpoptions', '-p', 'MyPrinterQueue']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(lpoptOut, lpoptErr) = proc.communicate()

#Note: lpoptions -p printername will never fail. If MyPrinterQueue does not exist, it
#will still exit 0, but just produce no output.
#Thanks, cups, I was having a good day until now.

for option in lpoptLongOut.splitlines():
for myOption in printerOptions.keys():
optionName = option.split("/", 1)[0]
optionValues = option.split("/",1)[1].split(":")[1].strip().split(" ")
for opt in optionValues:
if "*" in opt:
actualOptionValue = opt.replace('*', '')
break
if optionName == myOption:
if not printerOptions[myOption].lower() == actualOptionValue.lower():
print "Found mismatch: %s is '%s', should be '%s'" % (myOption, printerOptions[myOption], actualOptionValue)
sys.exit(0)

optionDict = {}
for builtOption in shlex.split(lpoptOut):
try:
optionDict[builtOption.split("=")[0]] = builtOption.split("=")[1]
except:
optionDict[builtOption.split("=")[0]] = None

comparisonDict = { "device-uri":"lpd://10.0.0.1", "printer-info":"My Printer Queue", "printer-location":"Tech Office" }
for keyName in comparisonDict.keys():
if not comparisonDict[keyName] == optionDict[keyName]:
print "Settings mismatch: %s is '%s', should be '%s'" % (keyName, optionDict[keyName], comparisonDict[keyName])
sys.exit(0)

sys.exit(1)</string>
<key>installer_type</key>
<string>nopkg</string>
<key>minimum_os_version</key>
<string>10.7.0</string>
<key>name</key>
<string>AddPrinter_MyPrinterQueue</string>
<key>postinstall_script</key>
<string>#!/usr/bin/python
import subprocess
import sys

# Populate these options if you want to set specific options for the printer. E.g. duplexing installed, etc.
printerOptions = { "HPOptionDuplexer":"True", "OutputMode":"normal" }

cmd = [ '/usr/sbin/lpadmin', '-x', 'MyPrinterQueue' ]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(lpadminxOut, lpadminxErr) = proc.communicate()

# Install the printer
cmd = [ '/usr/sbin/lpadmin',
'-p', 'MyPrinterQueue',
'-L', 'Tech Office',
'-D', 'My Printer Queue',
'-v', 'lpd://10.0.0.1',
'-P', "/Library/Printers/PPDs/Contents/Resources/HP officejet 5500 series.ppd.gz",
'-E',
'-o', 'printer-is-shared=false',
'-o', 'printer-error-policy=abort-job' ]

for option in printerOptions.keys():
cmd.append("-o")
cmd.append(str(option) + "=" + str(printerOptions[option]))

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(lpadminOut, lpadminErr) = proc.communicate()

if lpadminErr:
print "Error: %s" % lpadminErr
sys.exit(1)
print "Results: %s" % lpadminOut
sys.exit(0)</string>
<key>requires</key>
<array>
<string>CanonPrinterDriver</string>
</array>
<key>unattended_install</key>
<true/>
<key>uninstall_method</key>
<string>uninstall_script</string>
<key>uninstall_script</key>
<string>#!/bin/bash
/usr/sbin/lpadmin -x MyPrinterQueue</string>
<key>uninstallable</key>
<true/>
<key>version</key>
<string>1.5</string>
</dict>
</plist>
```
5 changes: 3 additions & 2 deletions Template.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Printer Name,Location,Display Name,Address,Driver,Description,Options,Version,Requires,Icon
MyPrinterQueue,Tech Office,My Printer Queue,10.0.0.1,HP officejet 5500 series.ppd.gz,Black and white printer in Tech Office,HPOptionDuplexer=True OutputMode=normal,1.0,HPPrinterDriver,HP LaserJet 4250.icns
Printer Name,Location,Display Name,Address,Driver,Description,Options,Version,Requires,Icon,Catalogs,Category,Subdirectory,Munki Name
MyPrinterQueue,Tech Office,My Printer Queue,10.0.0.1,HP officejet 5500 series.ppd.gz,Black and white printer in Tech Office,HPOptionDuplexer=True OutputMode=normal,5.0,HPPrinterDriver,HP LaserJet 4250.icns,testing,Printers,scripts/printers/hp/,PrinterSetup_Office
BrotherPrinter,Home Office,Printer at home,ipp://brother9022.local,airprint-ppd,A simple AirPrint printer at home,,1.0,airprint_ppd,,testing,Printers,scripts/printers/brother/,PrinterSetup_Brother
Loading