From 743b40f0e9df20e724d7a3a69828e8445dbe73b0 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 29 Jun 2021 15:38:13 +0200 Subject: [PATCH 01/21] Switched to semicolon as csv separator for better Excel compatibility and optimised csv handling --- Template.csv | 4 +-- print_generator.py | 68 +++++++++++++++++++++++----------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Template.csv b/Template.csv index 2701a22..ee313d3 100644 --- a/Template.csv +++ b/Template.csv @@ -1,2 +1,2 @@ -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 +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 \ No newline at end of file diff --git a/print_generator.py b/print_generator.py index b170a3f..3dbb941 100755 --- a/print_generator.py +++ b/print_generator.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/local/munki/munki-python from __future__ import absolute_import, print_function import argparse @@ -48,67 +48,67 @@ def getOptionsString(optionList): if args.csv: # A CSV was found, use that for all data. with open(args.csv, mode='r') as infile: - reader = csv.reader(infile) - next(reader, None) # skip the header row + reader = csv.DictReader(infile, delimiter=';') + for row in reader: newPlist = dict(templatePlist) - # each row contains 10 elements: - # Printer name, location, display name, address, driver, description, options, version, requires, icon - # options in the form of "Option=Value Option2=Value Option3=Value" - # requires in the form of "package1 package2" Note: the space seperator + # each row contains up to 12 elements: + # Printer name, Location, Display Name, Address, Driver, Description, Options, Version, Requires, Icon, Catalog, Directory + # Options in the form of "Option=Value Option2=Value Option3=Value" + # Requires in the form of "package1 package2" Note: the space seperator theOptionString = '' - if row[6] != "": - theOptionString = getOptionsString(row[6].split(" ")) + if row['Options'] != "": + theOptionString = getOptionsString(row['Options'].split(" ")) # First, change the plist keys in the pkginfo itself - newPlist['display_name'] = row[2] - newPlist['description'] = row[5] - newPlist['name'] = "AddPrinter_" + str(row[0]) # set to printer name + newPlist['display_name'] = row['Display Name'] + newPlist['description'] = row['Description'] + newPlist['name'] = "AddPrinter_" + str(row['Printer Name']) # set to printer name # Check for an icon - if row[9] != "": - newPlist['icon_name'] = row[9] + if row['Icon'] != "": + newPlist['icon_name'] = row['Icon'] # Check for a version number - if row[7] != "": + if row['Version'] != "": # Assume the user specified a version number - version = row[7] + version = row['Version'] else: # Use the default version of 1.0 version = "1.0" newPlist['version'] = version # Check for a protocol listed in the address - if '://' in row[3]: + if '://' in row['Address']: # Assume the user passed in a full address and protocol - address = row[3] + address = row['Address'] else: # Assume the user wants to use the default, lpd:// - address = 'lpd://' + row[3] + address = 'lpd://' + row['Address'] # Append the driver path to the driver file specified in the csv - driver = '/Library/Printers/PPDs/Contents/Resources/%s' % row[4] - base_driver = row[4] - if row[4].endswith('.gz'): - base_driver = row[4].replace('.gz', '') + driver = '/Library/Printers/PPDs/Contents/Resources/%s' % row['Driver'] + base_driver = row['Driver'] + if row['Driver'].endswith('.gz'): + base_driver = row['Driver'].replace('.gz', '') if base_driver.endswith('.ppd'): base_driver = base_driver.replace('.ppd', '') # Now change the variables in the installcheck_script - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", row[0]) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", row['Printer Name']) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("OPTIONS", theOptionString) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", row[1].replace('"', '')) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", row[2].replace('"', '')) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", row['Location'].replace('"', '')) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", row['Display Name'].replace('"', '')) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("ADDRESS", address) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", base_driver) # Now change the variables in the postinstall_script - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("PRINTERNAME", row[0]) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", row[1].replace('"', '')) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", row[2].replace('"', '')) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("PRINTERNAME", row['Printer Name']) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", row['Location'].replace('"', '')) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", row['Display Name'].replace('"', '')) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) # Now change the one variable in the uninstall_script - newPlist['uninstall_script'] = newPlist['uninstall_script'].replace("PRINTERNAME", row[0]) + newPlist['uninstall_script'] = newPlist['uninstall_script'].replace("PRINTERNAME", row['Printer Name']) # Add required packages if passed in the csv - if row[8] != "": - newPlist['requires'] = row[8].split(' ') + if row['Requires'] != "": + newPlist['requires'] = row['Requires'].split(' ') # Write out the file - newFileName = "AddPrinter-" + row[0] + "-" + version + ".pkginfo" + newFileName = "AddPrinter-" + row['Printer Name'] + "-" + version + ".pkginfo" f = open(newFileName, 'wb') dump_plist(newPlist, f) f.close() @@ -182,7 +182,7 @@ def getOptionsString(optionList): address = 'lpd://' + args.address newPlist = dict(templatePlist) - # root pkginfo variable replacement + # root pkginfo variable replacement newPlist['description'] = description newPlist['display_name'] = displayName newPlist['name'] = "AddPrinter_" + displayName.replace(" ", "") From 3c054ef16fa8dbdd9f12cb41caa369bf62358755 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Wed, 30 Jun 2021 17:23:15 +0200 Subject: [PATCH 02/21] Refactoring and extending functionality --- Template.csv | 8 +- print_generator.py | 398 +++++++++++++++++++++++++++++++-------------- 2 files changed, 278 insertions(+), 128 deletions(-) diff --git a/Template.csv b/Template.csv index ee313d3..dafe04a 100644 --- a/Template.csv +++ b/Template.csv @@ -1,2 +1,6 @@ -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 \ No newline at end of file +Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;scripts/printers/hp/;PrinterSetup +;;;;;;;;;;;; +;;;;;;;;;;;; +;;;;;;;;;;;; +;;;;;;;;;;;; \ No newline at end of file diff --git a/print_generator.py b/print_generator.py index 3dbb941..025f2eb 100755 --- a/print_generator.py +++ b/print_generator.py @@ -6,14 +6,66 @@ import os import re import sys +from typing import Optional +from xml.parsers.expat import ExpatError + +from plistlib import load as load_plist # Python 3 +from plistlib import dump as dump_plist + + +# Preference hanlding copied from Munki: +# https://github.com/munki/munki/blob/e8ccc5f53e8f69b59fbc153a783158a34ca6d1ea/code/client/munkilib/cliutils.py#L55 + +BUNDLE_ID = 'com.googlecode.munki.munkiimport' +PREFSNAME = BUNDLE_ID + '.plist' +PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences', PREFSNAME)) + +FOUNDATION_SUPPORT = True try: - from plistlib import load as load_plist # Python 3 - from plistlib import dump as dump_plist + # PyLint cannot properly find names inside Cocoa libraries, so issues bogus + # No name 'Foo' in module 'Bar' warnings. Disable them. + # pylint: disable=E0611 + from Foundation import CFPreferencesCopyAppValue + # pylint: enable=E0611 except ImportError: - from plistlib import readPlist as load_plist # Python 2 - from plistlib import writePlist as dump_plist + # CoreFoundation/Foundation isn't available + FOUNDATION_SUPPORT = False +if FOUNDATION_SUPPORT: + def pref(prefname, default=None): + """Return a preference. Since this uses CFPreferencesCopyAppValue, + Preferences can be defined several places. Precedence is: + - MCX/Configuration Profile + - ~/Library/Preferences/ByHost/ + com.googlecode.munki.munkiimport.XX.plist + - ~/Library/Preferences/com.googlecode.munki.munkiimport.plist + - /Library/Preferences/com.googlecode.munki.munkiimport.plist + """ + value = CFPreferencesCopyAppValue(prefname, BUNDLE_ID) + if value is None: + return default + + return value + +else: + def pref(prefname, default=None): + """Returns a preference for prefname. This is a fallback mechanism if + CoreFoundation functions are not available -- for example to allow the + possible use of makecatalogs or manifestutil on Linux""" + if not hasattr(pref, 'cache'): + pref.cache = None + if not pref.cache: + try: + f = open(os.path.join(pwd, PREFSPATH), 'rb') + pref.cache = load_plist(f) + f.close() + except (IOError, OSError, ExpatError): + pref.cache = {} + if prefname in pref.cache: + return pref.cache[prefname] + # no pref found + return default def getOptionsString(optionList): # optionList should be a list item @@ -36,100 +88,211 @@ def getOptionsString(optionList): parser.add_argument('--options', nargs='*', dest='options', help='Printer options in form of space-delimited \'Option1=Key Option2=Key Option3=Key\', etc. Optional.') parser.add_argument('--version', help='Version number of Munki pkginfo. Optional. Defaults to 1.0.', default='1.0') parser.add_argument('--icon', help='Specifies an existing icon in the Munki repo to display for the printer in Managed Software Center. Optional.') -parser.add_argument('--csv', help='Path to CSV file containing printer info. If CSV is provided, all other options are ignored.') +parser.add_argument('--catalogs', help='Space delimited list of Munki catalogs. Defaults to \'testing\'. Optional.') +parser.add_argument('--munkiname', help='Name of Munki item. Defaults to printername. Optional.') +parser.add_argument('--repo', help='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.') +parser.add_argument('--subdirectory', help='Subdirectory of Munki\'s pkgsinfo directory. Optional.') +parser.add_argument('--csv', help='Path to CSV file containing printer info. If CSV is provided, all other options besides \'--repo\' are ignored.') args = parser.parse_args() +def throwError(message='Unknown error',exitcode=1,show_usage=True): + print(os.path.basename(sys.argv[0]) + ': Error: ' + message, file=sys.stderr) + + if show_usage: + parser.print_usage() + + sys.exit(exitcode) +manifestPath = '' + +if args.repo: + args.repo = os.path.realpath(os.path.expanduser(args.repo)) + manifestPath = os.path.realpath(args.repo + '/manifests') + if not os.access(manifestPath, os.W_OK): + throwError('The manifest directory in given munki repo is not writable.') + pwd = os.path.dirname(os.path.realpath(__file__)) f = open(os.path.join(pwd, 'AddPrinter-Template.plist'), 'rb') templatePlist = load_plist(f) f.close() +def createPlist( + printer_name: str, + address: str, + driver: str, + display_name: Optional[str] = '', + location: Optional[str] = '', + description: Optional[str] = '', + options: Optional[str] = '', + version: Optional[str] = '1.0', + requires: Optional[str] = '', + icon: Optional[str] = '', + catalogs: Optional[str] = 'testing', + subdirectory: Optional[str] = '', + munki_name: Optional[str] = '' +): + newPlist = dict(templatePlist) + + # Options in the form of "Option=Value Option2=Value Option3=Value" + # Requires in the form of "package1 package2" Note: the space seperator + theOptionString = '' + if options: + theOptionString = getOptionsString(options.split(" ")) + + # First, change the plist keys in the pkginfo itself + newPlist['display_name'] = display_name + newPlist['description'] = description + + if munki_name: + newPlist['name'] = munki_name + else: + newPlist['name'] = "AddPrinter_" + str(printer_name) # set to printer name + + # Set Icon + if icon: + newPlist['icon_name'] = icon + + # Check for a version number + newPlist['version'] = version + + # Check for a protocol listed in the address + if '://' in address: + # Assume the user passed in a full address and protocol + address = address + else: + # Assume the user wants to use the default, lpd:// + address = 'lpd://' + address + + if driver.startswith('/Library'): + # Assume the user passed in a full path rather than a relative filename + driver_path = driver + base_driver = os.path.splitext(os.path.basename(driver))[0].replace('"', '') + else: + # Assume only a relative filename + driver_path = '/Library/Printers/PPDs/Contents/Resources/%s' % driver + base_driver = driver + + if base_driver.endswith('.gz'): + base_driver = base_driver.replace('.gz', '') + if base_driver.endswith('.ppd'): + base_driver = base_driver.replace('.ppd', '') + + # Now change the variables in the installcheck_script + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", printer_name) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("OPTIONS", theOptionString) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", location.replace('"', '')) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", display_name.replace('"', '')) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("ADDRESS", address) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", base_driver) + + # Now change the variables in the postinstall_script + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("PRINTERNAME", printer_name) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", location.replace('"', '')) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", display_name.replace('"', '')) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) + + # Now change the one variable in the uninstall_script + newPlist['uninstall_script'] = newPlist['uninstall_script'].replace("PRINTERNAME", printer_name) + + # Add required packages if passed in the csv + if requires: + newPlist['requires'] = requires.split(' ') + + # Define catalogs for this package + if catalogs: + newPlist['catalogs'] = catalogs.split(' ') + + # Write out the file + newFileName = newPlist['name'] + "-" + newPlist['version'] + pref('pkginfo_extension', default='.pkginfo') + + if manifestPath: + if subdirectory: + os.makedirs(manifestPath + os.path.sep + subdirectory, exist_ok=True) + newFileName = os.path.realpath(manifestPath + os.path.sep + subdirectory + os.path.sep + newFileName) + else: + newFileName = os.path.realpath(manifestPath + os.path.sep + newFileName) + + print('Write pkginfo file to %s' % newFileName) + + f = open(newFileName, 'wb') + dump_plist(newPlist, f) + f.close() + return True + if args.csv: # A CSV was found, use that for all data. with open(args.csv, mode='r') as infile: reader = csv.DictReader(infile, delimiter=';') for row in reader: - newPlist = dict(templatePlist) - # each row contains up to 12 elements: - # Printer name, Location, Display Name, Address, Driver, Description, Options, Version, Requires, Icon, Catalog, Directory - # Options in the form of "Option=Value Option2=Value Option3=Value" - # Requires in the form of "package1 package2" Note: the space seperator - theOptionString = '' - if row['Options'] != "": - theOptionString = getOptionsString(row['Options'].split(" ")) - # First, change the plist keys in the pkginfo itself - newPlist['display_name'] = row['Display Name'] - newPlist['description'] = row['Description'] - newPlist['name'] = "AddPrinter_" + str(row['Printer Name']) # set to printer name - # Check for an icon - if row['Icon'] != "": - newPlist['icon_name'] = row['Icon'] - # Check for a version number - if row['Version'] != "": - # Assume the user specified a version number - version = row['Version'] - else: - # Use the default version of 1.0 - version = "1.0" - newPlist['version'] = version - # Check for a protocol listed in the address - if '://' in row['Address']: - # Assume the user passed in a full address and protocol - address = row['Address'] - else: - # Assume the user wants to use the default, lpd:// - address = 'lpd://' + row['Address'] - # Append the driver path to the driver file specified in the csv - driver = '/Library/Printers/PPDs/Contents/Resources/%s' % row['Driver'] - base_driver = row['Driver'] - if row['Driver'].endswith('.gz'): - base_driver = row['Driver'].replace('.gz', '') - if base_driver.endswith('.ppd'): - base_driver = base_driver.replace('.ppd', '') - # Now change the variables in the installcheck_script - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", row['Printer Name']) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("OPTIONS", theOptionString) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", row['Location'].replace('"', '')) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", row['Display Name'].replace('"', '')) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("ADDRESS", address) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", base_driver) - # Now change the variables in the postinstall_script - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("PRINTERNAME", row['Printer Name']) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", row['Location'].replace('"', '')) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", row['Display Name'].replace('"', '')) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) - # Now change the one variable in the uninstall_script - newPlist['uninstall_script'] = newPlist['uninstall_script'].replace("PRINTERNAME", row['Printer Name']) - # Add required packages if passed in the csv - if row['Requires'] != "": - newPlist['requires'] = row['Requires'].split(' ') - # Write out the file - newFileName = "AddPrinter-" + row['Printer Name'] + "-" + version + ".pkginfo" - f = open(newFileName, 'wb') - dump_plist(newPlist, f) - f.close() + + # In earlier versions, each row contains up to 10 elements: + # Printer Name, Location, Display Name, Address, Driver, Description, Options, Version, Requires, Icon + # To preserve backward compatibility, define all possible elements with default values and check for + # required values + if 'Printer Name' not in row: + throwError('Printer Name is required') + if 'Location' not in row: + row['Location'] = '' + if 'Display Name' not in row: + row['Display Name'] = row['Printer Name'] + if 'Address' not in row: + throwError('Address is required') + if 'Driver' not in row: + throwError('Driver is required') + if 'Description' not in row: + row['Description'] = '' + if 'Options' not in row: + row['Options'] = '' + if 'Version' not in row: + row['Version'] = '1.0' + if 'Requires' not in row: + row['Requires'] = '' + if 'Icon' not in row: + row['Icon'] = '' + if 'Catalogs' not in row: + row['Catalogs'] = pref('default_catalog', default='testing') + if 'Subdirectory' not in row: + row['Subdirectory'] = '' + if 'Munki Name' not in row: + row['Munki Name'] = '' + + createPlist( + printer_name=row['Printer Name'], + address=row['Address'], + driver=row['Driver'], + display_name=row['Display Name'], + location=row['Location'], + description=row['Description'], + options=row['Options'], + version=row['Version'], + icon=row['Icon'], + catalogs=row['Catalogs'], + subdirectory=row['Subdirectory'], + munki_name=row['Munki Name']) + + else: if not args.printername: - print(os.path.basename(sys.argv[0]) + ': error: argument --printername is required', file=sys.stderr) - parser.print_usage() - sys.exit(1) - if not args.driver: - print(os.path.basename(sys.argv[0]) + ': error: argument --driver is required', file=sys.stderr) - parser.print_usage() - sys.exit(1) - if not args.address: - print(os.path.basename(sys.argv[0]) + ': error: argument --address is required', file=sys.stderr) - parser.print_usage() - sys.exit(1) + throwError('Argument --printername is required') + else: + printer_name = args.printername - if re.search(r"[\s#/]", args.printername): + if re.search(r"[\s#/]", printer_name): # printernames can't contain spaces, tabs, # or /. See lpadmin manpage for details. - print("ERROR: Printernames can't contain spaces, tabs, # or /.", file=sys.stderr) - sys.exit(1) + throwError("Printernames can't contain spaces, tabs, # or /.", show_usage=False) + + if not args.driver: + throwError('Argument --driver is required') + else: + driver = args.driver + + if not args.address: + throwError('Argument --address is required') + else: + address = args.address if args.desc: description = args.desc @@ -137,9 +300,9 @@ def getOptionsString(optionList): description = "" if args.displayname: - displayName = args.displayname + display_name = args.displayname else: - displayName = str(args.printername) + display_name = str(args.printername) if args.location: location = args.location @@ -162,53 +325,36 @@ def getOptionsString(optionList): icon = "" if args.options: - optionsString = str(args.options[0]).split(' ') - optionsString = getOptionsString(optionsString) + options = args.options + else: + options = '' + + if args.catalogs: + catalogs = args.catalogs else: - optionsString = '' + catalogs = "" - if args.driver.startswith('/Library'): - # Assume the user passed in a full path rather than a relative filename - driver = args.driver + if args.munkiname: + munki_name = args.munkiname else: - # Assume only a relative filename - driver = os.path.join('/Library/Printers/PPDs/Contents/Resources', args.driver) + munki_name = "" - if '://' in args.address: - # Assume the user passed in a full address and protocol - address = args.address + if args.subdirectory: + subdirectory = args.subdirectory else: - # Assume the user wants to use the default, lpd:// - address = 'lpd://' + args.address + subdirectory = "" - newPlist = dict(templatePlist) - # root pkginfo variable replacement - newPlist['description'] = description - newPlist['display_name'] = displayName - newPlist['name'] = "AddPrinter_" + displayName.replace(" ", "") - newPlist['version'] = version - newPlist['icon_name'] = icon - # installcheck_script variable replacement - newPlist['installcheck_script'] = templatePlist['installcheck_script'].replace("PRINTERNAME", args.printername) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("ADDRESS", address) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", displayName) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", location.replace('"', '')) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", os.path.splitext(os.path.basename(driver))[0].replace('"', '')) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("OPTIONS", optionsString) - # postinstall_script variable replacement - newPlist['postinstall_script'] = templatePlist['postinstall_script'].replace("PRINTERNAME", args.printername) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", displayName) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", location.replace('"', '')) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver.replace('"', '')) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", optionsString) - # uninstall_script variable replacement - newPlist['uninstall_script'] = templatePlist['uninstall_script'].replace("PRINTERNAME", args.printername) - # required packages - if requires != "": - newPlist['requires'] = [r.replace('\\', '') for r in re.split(r"(? Date: Wed, 30 Jun 2021 17:43:07 +0200 Subject: [PATCH 03/21] Updated README --- README.md | 245 ++++++++++++------------------------------------------ 1 file changed, 55 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index c910558..baee33a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,28 @@ -# 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 and 11 +* Usage of the Munki-included Python3 +* Enables usage of Microsoft Excel to edit the CSV file – **this required switching the separator from comma to semicolon**, you may need to update your files accordingly +* 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 define a path to Munki repo and an optional subdirectory +* Option to define a separate name for the Munki pkginfo item +* Besides switching to semicolons as csv separator: **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. @@ -27,27 +42,28 @@ The uninstall_script will delete the printer queue named PRINTERNAME if uninstal ### 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/ or an absolute path to a ppd file. * 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. +* 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". * Icon: Optionally specify an existing icon in the Munki repo to display for the printer in Managed Software Center. - -The CSV file is not sanity-checked for invalid entries or blank fields, so double check your file and test your pkginfos thoroughly. +* Catalogs: Space separated list of Munki catalogs in which this pkginfo should be listed +* 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` ### Command-line options: @@ -55,41 +71,33 @@ 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] [--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 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. --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'. 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. + --munkiname MUNKINAME + Name of Munki item. Defaults to printername. 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. + --subdirectory SUBDIRECTORY + Subdirectory of Munki's pkgsinfo 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: @@ -104,8 +112,12 @@ 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. +* `--munkiname`: Name of Munki item. Defaults to printername. 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. +* `--subdirectory`: Subdirectory of Munki's pkgsinfo 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` @@ -128,150 +140,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: - -``` - - - - - autoremove - - catalogs - - testing - - description - Black and white printer in Tech Office - display_name - My Printer Queue - icon_name - HP LaserJet 4250.icns - installcheck_script - #!/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) - installer_type - nopkg - minimum_os_version - 10.7.0 - name - AddPrinter_MyPrinterQueue - postinstall_script - #!/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) - requires - - CanonPrinterDriver - - unattended_install - - uninstall_method - uninstall_script - uninstall_script - #!/bin/bash -/usr/sbin/lpadmin -x MyPrinterQueue - uninstallable - - version - 1.5 - - -``` From d17229ca33bf21f548a02b2ede0c599a629df628 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Wed, 7 Jul 2021 14:54:13 +0200 Subject: [PATCH 04/21] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baee33a..ef7f438 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ The CSV file's columns should be pretty self-explanatory: * 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` -### Command-line options: +### Command-line options A full description of usage is available with: From 9df1db4997561c2924de3e67155e0a1057cfa633 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Wed, 7 Jul 2021 14:54:31 +0200 Subject: [PATCH 05/21] Cleaned up Template.csv --- Template.csv | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Template.csv b/Template.csv index dafe04a..478207f 100644 --- a/Template.csv +++ b/Template.csv @@ -1,6 +1,2 @@ Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;scripts/printers/hp/;PrinterSetup -;;;;;;;;;;;; -;;;;;;;;;;;; -;;;;;;;;;;;; -;;;;;;;;;;;; \ No newline at end of file +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;scripts/printers/hp/;PrinterSetup \ No newline at end of file From fe46ac0c6ad610841ef5fd60c4337f03a78e6681 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Wed, 7 Jul 2021 14:56:14 +0200 Subject: [PATCH 06/21] Added exit code to script --- print_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/print_generator.py b/print_generator.py index 025f2eb..62667ab 100755 --- a/print_generator.py +++ b/print_generator.py @@ -357,4 +357,5 @@ def createPlist( catalogs=catalogs, subdirectory=subdirectory, munki_name=munki_name) - + +exit(0) From 7643db925699e1f185ad9a0a2c47bb684b6d62f1 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Fri, 9 Jul 2021 13:07:11 +0200 Subject: [PATCH 07/21] Fixed target directory be pkgsinfo and not manifests --- print_generator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/print_generator.py b/print_generator.py index 62667ab..ac23316 100755 --- a/print_generator.py +++ b/print_generator.py @@ -103,13 +103,13 @@ def throwError(message='Unknown error',exitcode=1,show_usage=True): sys.exit(exitcode) -manifestPath = '' +pkgsinfoPath = '' if args.repo: args.repo = os.path.realpath(os.path.expanduser(args.repo)) - manifestPath = os.path.realpath(args.repo + '/manifests') - if not os.access(manifestPath, os.W_OK): - throwError('The manifest directory in given munki repo is not writable.') + pkgsinfoPath = os.path.realpath(args.repo + '/pkgsinfo') + if not os.access(pkgsinfoPath, os.W_OK): + throwError('The pkgsinfo directory in given munki repo is not writable.') pwd = os.path.dirname(os.path.realpath(__file__)) f = open(os.path.join(pwd, 'AddPrinter-Template.plist'), 'rb') @@ -207,12 +207,12 @@ def createPlist( # Write out the file newFileName = newPlist['name'] + "-" + newPlist['version'] + pref('pkginfo_extension', default='.pkginfo') - if manifestPath: + if pkgsinfoPath: if subdirectory: - os.makedirs(manifestPath + os.path.sep + subdirectory, exist_ok=True) - newFileName = os.path.realpath(manifestPath + os.path.sep + subdirectory + os.path.sep + newFileName) + os.makedirs(pkgsinfoPath + os.path.sep + subdirectory, exist_ok=True) + newFileName = os.path.realpath(pkgsinfoPath + os.path.sep + subdirectory + os.path.sep + newFileName) else: - newFileName = os.path.realpath(manifestPath + os.path.sep + newFileName) + newFileName = os.path.realpath(pkgsinfoPath + os.path.sep + newFileName) print('Write pkginfo file to %s' % newFileName) From e4e855fc657a029e18e0c0f724b0284532171f59 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Mon, 24 Jan 2022 14:38:04 +0100 Subject: [PATCH 08/21] Fix: Added required packages at csv import --- print_generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/print_generator.py b/print_generator.py index ac23316..9afb3aa 100755 --- a/print_generator.py +++ b/print_generator.py @@ -268,6 +268,7 @@ def createPlist( description=row['Description'], options=row['Options'], version=row['Version'], + requires=row['Requires'], icon=row['Icon'], catalogs=row['Catalogs'], subdirectory=row['Subdirectory'], From 3dc18f3a41e6020dc8bfcd07d1acb998bac88358 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Mon, 24 Jan 2022 14:43:11 +0100 Subject: [PATCH 09/21] Fix: Added required packages at manual execution --- print_generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/print_generator.py b/print_generator.py index 9afb3aa..d826e77 100755 --- a/print_generator.py +++ b/print_generator.py @@ -354,6 +354,7 @@ def createPlist( description=description, options=options, version=version, + requires=requires, icon=icon, catalogs=catalogs, subdirectory=subdirectory, From 14760c7890aab624c7471734db28d9da55997ef6 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 25 Jan 2022 11:50:07 +0100 Subject: [PATCH 10/21] Fixed driver assignment --- print_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/print_generator.py b/print_generator.py index d826e77..26d4714 100755 --- a/print_generator.py +++ b/print_generator.py @@ -190,7 +190,7 @@ def createPlist( newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", location.replace('"', '')) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", display_name.replace('"', '')) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", base_driver) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) # Now change the one variable in the uninstall_script From 2844c398722c7112c1d60eae7e65ce3969ee915e Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 25 Jan 2022 12:00:21 +0100 Subject: [PATCH 11/21] Fixed bug in driver naming --- print_generator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/print_generator.py b/print_generator.py index 26d4714..117608b 100755 --- a/print_generator.py +++ b/print_generator.py @@ -173,9 +173,9 @@ def createPlist( base_driver = driver if base_driver.endswith('.gz'): - base_driver = base_driver.replace('.gz', '') + driver_path = driver_path.replace('.gz', '') if base_driver.endswith('.ppd'): - base_driver = base_driver.replace('.ppd', '') + driver_path = driver_path.replace('.ppd', '') # Now change the variables in the installcheck_script newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", printer_name) @@ -183,15 +183,15 @@ def createPlist( newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("LOCATION", location.replace('"', '')) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DISPLAY_NAME", display_name.replace('"', '')) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("ADDRESS", address) - newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", base_driver) + newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("DRIVER", driver_path) # Now change the variables in the postinstall_script newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("PRINTERNAME", printer_name) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("LOCATION", location.replace('"', '')) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DISPLAY_NAME", display_name.replace('"', '')) newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("ADDRESS", address) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", base_driver) - newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("OPTIONS", theOptionString) + newPlist['postinstall_script'] = newPlist['postinstall_script'].replace("DRIVER", driver_path) # Now change the one variable in the uninstall_script newPlist['uninstall_script'] = newPlist['uninstall_script'].replace("PRINTERNAME", printer_name) From 9c39759ee62916154c4a75d2ec51de8dac12cb3b Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 25 Jan 2022 12:07:19 +0100 Subject: [PATCH 12/21] Remove suffix correctly --- print_generator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/print_generator.py b/print_generator.py index 117608b..0dfd6b3 100755 --- a/print_generator.py +++ b/print_generator.py @@ -172,6 +172,8 @@ def createPlist( driver_path = '/Library/Printers/PPDs/Contents/Resources/%s' % driver base_driver = driver + if base_driver.endswith('.ppd.gz'): + driver_path = driver_path.replace('.ppd.gz', '') if base_driver.endswith('.gz'): driver_path = driver_path.replace('.gz', '') if base_driver.endswith('.ppd'): From 34e9736c2675cf8c770f78b9b8c2088913a83b6a Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 25 Jan 2022 12:16:19 +0100 Subject: [PATCH 13/21] Cleaned up driver referencing --- print_generator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/print_generator.py b/print_generator.py index 0dfd6b3..085c76a 100755 --- a/print_generator.py +++ b/print_generator.py @@ -166,17 +166,13 @@ def createPlist( if driver.startswith('/Library'): # Assume the user passed in a full path rather than a relative filename driver_path = driver - base_driver = os.path.splitext(os.path.basename(driver))[0].replace('"', '') else: # Assume only a relative filename driver_path = '/Library/Printers/PPDs/Contents/Resources/%s' % driver - base_driver = driver - if base_driver.endswith('.ppd.gz'): - driver_path = driver_path.replace('.ppd.gz', '') - if base_driver.endswith('.gz'): + if driver_path.endswith('.gz'): driver_path = driver_path.replace('.gz', '') - if base_driver.endswith('.ppd'): + if driver_path.endswith('.ppd'): driver_path = driver_path.replace('.ppd', '') # Now change the variables in the installcheck_script From a49d6a12cf9ff94b8ff5413a2e029bca54696f06 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 25 Jan 2022 12:27:52 +0100 Subject: [PATCH 14/21] Preserve file name suffix for printer drivers --- print_generator.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/print_generator.py b/print_generator.py index 085c76a..8f004d3 100755 --- a/print_generator.py +++ b/print_generator.py @@ -170,11 +170,6 @@ def createPlist( # Assume only a relative filename driver_path = '/Library/Printers/PPDs/Contents/Resources/%s' % driver - if driver_path.endswith('.gz'): - driver_path = driver_path.replace('.gz', '') - if driver_path.endswith('.ppd'): - driver_path = driver_path.replace('.ppd', '') - # Now change the variables in the installcheck_script newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("PRINTERNAME", printer_name) newPlist['installcheck_script'] = newPlist['installcheck_script'].replace("OPTIONS", theOptionString) From f11c6ef3438267e8ddaa2331b63e0f7bcebb2a03 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Fri, 25 Feb 2022 13:54:53 +0100 Subject: [PATCH 15/21] Support for AirPrint printers via airprint-ppd --- AddPrinter-Template.plist | 28 +++++++++++++++++++++++++++- README.md | 4 ++-- Template.csv | 3 ++- print_generator.py | 14 +++++++++++--- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/AddPrinter-Template.plist b/AddPrinter-Template.plist index 2a15b36..f3b2c97 100644 --- a/AddPrinter-Template.plist +++ b/AddPrinter-Template.plist @@ -79,7 +79,33 @@ sys.exit(1) 10.7.0 name AddPrinter_DISPLAYNAME - postinstall_script + preinstall_script + #!/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) + + postinstall_script #!/usr/local/munki/munki-python import subprocess import sys diff --git a/README.md b/README.md index ef7f438..7c606f7 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ The CSV file's columns should be pretty self-explanatory: * 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/ or an absolute path to a ppd file. +* 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.** * 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". +* 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 * Subdirectory: Subdirectory inside Munki's pkgsinfo directory, only used if `--repo` is defined. diff --git a/Template.csv b/Template.csv index 478207f..8f74bca 100644 --- a/Template.csv +++ b/Template.csv @@ -1,2 +1,3 @@ Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;scripts/printers/hp/;PrinterSetup \ No newline at end of file +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;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;scripts/printers/brother/;PrinterSetup_Brother \ No newline at end of file diff --git a/print_generator.py b/print_generator.py index 8f004d3..10e6932 100755 --- a/print_generator.py +++ b/print_generator.py @@ -79,19 +79,19 @@ def getOptionsString(optionList): parser = argparse.ArgumentParser(description='Generate a Munki nopkg-style pkginfo for printer installation.') parser.add_argument('--printername', help='Name of printer queue. May not contain spaces, tabs, # or /. Required.') -parser.add_argument('--driver', help='Name of driver file in /Library/Printers/PPDs/Contents/Resources/. Can be relative or full path. Required.') +parser.add_argument('--driver', help='Either the name of driver file in /Library/Printers/PPDs/Contents/Resources/ (relative or full path) or \'airprint-ppd\' for AirPrint printers. Required.') parser.add_argument('--address', help='IP or DNS address of printer. If no protocol is specified, defaults to lpd://. Required.') parser.add_argument('--location', help='Location name for printer. Optional. Defaults to printername.') parser.add_argument('--displayname', help='Display name for printer (and Munki pkginfo). Optional. Defaults to printername.') parser.add_argument('--desc', help='Description for Munki pkginfo only. Optional.') -parser.add_argument('--requires', help='Required packages in form of space-delimited \'CanonDriver1 CanonDriver2\'. Optional.') +parser.add_argument('--requires', help='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.') parser.add_argument('--options', nargs='*', dest='options', help='Printer options in form of space-delimited \'Option1=Key Option2=Key Option3=Key\', etc. Optional.') parser.add_argument('--version', help='Version number of Munki pkginfo. Optional. Defaults to 1.0.', default='1.0') parser.add_argument('--icon', help='Specifies an existing icon in the Munki repo to display for the printer in Managed Software Center. Optional.') parser.add_argument('--catalogs', help='Space delimited list of Munki catalogs. Defaults to \'testing\'. Optional.') parser.add_argument('--munkiname', help='Name of Munki item. Defaults to printername. Optional.') -parser.add_argument('--repo', help='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.') parser.add_argument('--subdirectory', help='Subdirectory of Munki\'s pkgsinfo directory. Optional.') +parser.add_argument('--repo', help='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.') parser.add_argument('--csv', help='Path to CSV file containing printer info. If CSV is provided, all other options besides \'--repo\' are ignored.') args = parser.parse_args() @@ -163,6 +163,14 @@ def createPlist( # Assume the user wants to use the default, lpd:// address = 'lpd://' + address + if driver == 'airprint-ppd': + # This printer should use airprint-ppd so retrieve a PPD on the fly + newPlist['preinstall_script'] = newPlist['preinstall_script'].replace("PRINTERNAME", printer_name) + newPlist['preinstall_script'] = newPlist['preinstall_script'].replace("ADDRESS", address) + driver = '/Library/Printers/PPDs/Contents/Resources/%s.ppd' % printer_name + else: + newPlist.pop('preinstall_script', None) + if driver.startswith('/Library'): # Assume the user passed in a full path rather than a relative filename driver_path = driver From 52ca9f8cf1af6bdb2e8f1777f4e8c82a34e6bb20 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Fri, 25 Feb 2022 13:57:29 +0100 Subject: [PATCH 16/21] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c606f7..a1c2da0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a fork of [nmcspadden/PrinterGenerator](https://github.com/nmcspadden/Pr This updated version implements some cool new things: -* Support for macOS 10.14, 10.15 and 11 +* Support for macOS 10.14, 10.15, 11 and 12 * Usage of the Munki-included Python3 * Enables usage of Microsoft Excel to edit the CSV file – **this required switching the separator from comma to semicolon**, you may need to update your files accordingly * The order of the csv columns do not have to be preserved, but **keep the names of the 1st row**. From a40f54218c7ff9f0a0e73ee65291b3769c7c5790 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Fri, 25 Feb 2022 15:36:31 +0100 Subject: [PATCH 17/21] Updated README --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a1c2da0..3547b1b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This updated version implements some cool new things: * Enables usage of Microsoft Excel to edit the CSV file – **this required switching the separator from comma to semicolon**, you may need to update your files accordingly * 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 * Besides switching to semicolons as csv separator: **This script should preserve backward compatibility!** @@ -30,11 +31,14 @@ The script can either take arguments on the command line, or a CSV file containi 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.* @@ -47,6 +51,7 @@ A template CSV file is provided to make it easy to generate multiple pkginfos in ``` ./print_generator.py --csv /path/to/printers.csv ``` + *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: @@ -71,8 +76,7 @@ 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 ...]] [--version VERSION] [--icon ICON] [--catalogs CATALOGS] [--munkiname MUNKINAME] [--repo REPO] - [--subdirectory SUBDIRECTORY] [--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] [--munkiname MUNKINAME] [--repo REPO] [--subdirectory SUBDIRECTORY] [--csv CSV] Generate a Munki nopkg-style pkginfo for printer installation. @@ -80,13 +84,15 @@ 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. + --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. --desc DESC Description for Munki pkginfo only. Optional. - --requires REQUIRES Required packages in form of space-delimited 'CanonDriver1 CanonDriver2'. Optional. + --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. @@ -94,16 +100,16 @@ optional arguments: --catalogs CATALOGS Space delimited list of Munki catalogs. Defaults to 'testing'. Optional. --munkiname MUNKINAME Name of Munki item. Defaults to printername. 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. --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`. @@ -114,8 +120,8 @@ As in the above CSV section, the arguments are all the same: * `--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. * `--munkiname`: Name of Munki item. Defaults to printername. 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. * `--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 From 1469784b43426b759a9b7243ab68da8afaec6c65 Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Mon, 7 Mar 2022 10:31:30 +0100 Subject: [PATCH 18/21] Bring back support for comma as a CSV separator --- README.md | 4 ++-- Template.csv | 6 +++--- Template_with_semicolons.csv | 3 +++ print_generator.py | 11 +++++++++-- 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 Template_with_semicolons.csv diff --git a/README.md b/README.md index 3547b1b..851d444 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ This updated version implements some cool new things: * Support for macOS 10.14, 10.15, 11 and 12 * Usage of the Munki-included Python3 -* Enables usage of Microsoft Excel to edit the CSV file – **this required switching the separator from comma to semicolon**, you may need to update your files accordingly +* 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 -* Besides switching to semicolons as csv separator: **This script should preserve backward compatibility!** +* **This script should preserve backward compatibility!** ## Caveats diff --git a/Template.csv b/Template.csv index 8f74bca..8522630 100644 --- a/Template.csv +++ b/Template.csv @@ -1,3 +1,3 @@ -Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;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;scripts/printers/brother/;PrinterSetup_Brother \ No newline at end of file +Printer Name,Location,Display Name,Address,Driver,Description,Options,Version,Requires,Icon,Catalogs,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,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,scripts/printers/brother/,PrinterSetup_Brother \ No newline at end of file diff --git a/Template_with_semicolons.csv b/Template_with_semicolons.csv new file mode 100644 index 0000000..8f74bca --- /dev/null +++ b/Template_with_semicolons.csv @@ -0,0 +1,3 @@ +Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;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;scripts/printers/brother/;PrinterSetup_Brother \ No newline at end of file diff --git a/print_generator.py b/print_generator.py index 10e6932..782d132 100755 --- a/print_generator.py +++ b/print_generator.py @@ -14,7 +14,7 @@ from plistlib import dump as dump_plist -# Preference hanlding copied from Munki: +# Preference handling copied from Munki: # https://github.com/munki/munki/blob/e8ccc5f53e8f69b59fbc153a783158a34ca6d1ea/code/client/munkilib/cliutils.py#L55 BUNDLE_ID = 'com.googlecode.munki.munkiimport' @@ -116,6 +116,13 @@ def throwError(message='Unknown error',exitcode=1,show_usage=True): templatePlist = load_plist(f) f.close() +# Identify the delimiter of a given CSV file, props to https://stackoverflow.com/questions/69817054/python-detection-of-delimiter-separator-in-a-csv-file +def find_delimiter(filename): + sniffer = csv.Sniffer() + with open(filename) as fp: + delimiter = sniffer.sniff(fp.read(5000)).delimiter + return delimiter + def createPlist( printer_name: str, address: str, @@ -225,7 +232,7 @@ def createPlist( if args.csv: # A CSV was found, use that for all data. with open(args.csv, mode='r') as infile: - reader = csv.DictReader(infile, delimiter=';') + reader = csv.DictReader(infile, delimiter=find_delimiter(args.csv)) for row in reader: From fdbac7f567beda07bbcdcf2886d4b348f60d8569 Mon Sep 17 00:00:00 2001 From: a-vogel Date: Thu, 14 Apr 2022 15:18:49 +0200 Subject: [PATCH 19/21] Added category for Munki pkginfo --- README.md | 6 +++++- Template.csv | 6 +++--- Template_with_semicolons.csv | 6 +++--- print_generator.py | 16 ++++++++++++++-- 4 files changed, 25 insertions(+), 9 deletions(-) mode change 100755 => 100644 print_generator.py diff --git a/README.md b/README.md index 851d444..518caf6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This updated version implements some cool new things: * 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 @@ -67,6 +68,7 @@ The CSV file's columns should be pretty self-explanatory: * 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` @@ -76,7 +78,7 @@ 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 ...]] [--version VERSION] [--icon ICON] [--catalogs CATALOGS] [--munkiname MUNKINAME] [--repo REPO] [--subdirectory SUBDIRECTORY] [--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. @@ -98,6 +100,7 @@ optional arguments: --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 @@ -119,6 +122,7 @@ As in the above CSV section, the arguments are all the same: * `--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. diff --git a/Template.csv b/Template.csv index 8522630..683e894 100644 --- a/Template.csv +++ b/Template.csv @@ -1,3 +1,3 @@ -Printer Name,Location,Display Name,Address,Driver,Description,Options,Version,Requires,Icon,Catalogs,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,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,scripts/printers/brother/,PrinterSetup_Brother \ No newline at end of file +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 \ No newline at end of file diff --git a/Template_with_semicolons.csv b/Template_with_semicolons.csv index 8f74bca..7524d77 100644 --- a/Template_with_semicolons.csv +++ b/Template_with_semicolons.csv @@ -1,3 +1,3 @@ -Printer Name;Location;Display Name;Address;Driver;Description;Options;Version;Requires;Icon;Catalogs;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;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;scripts/printers/brother/;PrinterSetup_Brother \ No newline at end of file +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 \ No newline at end of file diff --git a/print_generator.py b/print_generator.py old mode 100755 new mode 100644 index 782d132..2e6942b --- a/print_generator.py +++ b/print_generator.py @@ -35,7 +35,7 @@ if FOUNDATION_SUPPORT: def pref(prefname, default=None): """Return a preference. Since this uses CFPreferencesCopyAppValue, - Preferences can be defined several places. Precedence is: + Preferences can be defined in several places. Precedence is: - MCX/Configuration Profile - ~/Library/Preferences/ByHost/ com.googlecode.munki.munkiimport.XX.plist @@ -84,6 +84,7 @@ def getOptionsString(optionList): parser.add_argument('--location', help='Location name for printer. Optional. Defaults to printername.') parser.add_argument('--displayname', help='Display name for printer (and Munki pkginfo). Optional. Defaults to printername.') parser.add_argument('--desc', help='Description for Munki pkginfo only. Optional.') +parser.add_argument('--category', help='Category for Munki pgkinfo only. Optional. Defaults to \'Printers\'.') parser.add_argument('--requires', help='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.') parser.add_argument('--options', nargs='*', dest='options', help='Printer options in form of space-delimited \'Option1=Key Option2=Key Option3=Key\', etc. Optional.') parser.add_argument('--version', help='Version number of Munki pkginfo. Optional. Defaults to 1.0.', default='1.0') @@ -130,6 +131,7 @@ def createPlist( display_name: Optional[str] = '', location: Optional[str] = '', description: Optional[str] = '', + category: Optional[str] = 'Printers', options: Optional[str] = '', version: Optional[str] = '1.0', requires: Optional[str] = '', @@ -149,6 +151,7 @@ def createPlist( # First, change the plist keys in the pkginfo itself newPlist['display_name'] = display_name newPlist['description'] = description + newPlist['category'] = category if munki_name: newPlist['name'] = munki_name @@ -222,7 +225,7 @@ def createPlist( else: newFileName = os.path.realpath(pkgsinfoPath + os.path.sep + newFileName) - print('Write pkginfo file to %s' % newFileName) + print('Wrote pkginfo file to %s' % newFileName) f = open(newFileName, 'wb') dump_plist(newPlist, f) @@ -252,6 +255,8 @@ def createPlist( throwError('Driver is required') if 'Description' not in row: row['Description'] = '' + if 'Category' not in row: + row['Category'] = 'Printers' if 'Options' not in row: row['Options'] = '' if 'Version' not in row: @@ -279,6 +284,7 @@ def createPlist( requires=row['Requires'], icon=row['Icon'], catalogs=row['Catalogs'], + category=row['Category'], subdirectory=row['Subdirectory'], munki_name=row['Munki Name']) @@ -308,6 +314,11 @@ def createPlist( else: description = "" + if args.category: + description = args.category + else: + category = "Printers" + if args.displayname: display_name = args.displayname else: @@ -360,6 +371,7 @@ def createPlist( display_name=display_name, location=location, description=description, + category=category, options=options, version=version, requires=requires, From 7fa1b609df85de2445a23277ddcdc57d9a69242b Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 19 Apr 2022 07:50:55 +0200 Subject: [PATCH 20/21] Removed whitespaces and fixed console output --- print_generator.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/print_generator.py b/print_generator.py index 2e6942b..1746fdd 100644 --- a/print_generator.py +++ b/print_generator.py @@ -45,7 +45,7 @@ def pref(prefname, default=None): value = CFPreferencesCopyAppValue(prefname, BUNDLE_ID) if value is None: return default - + return value else: @@ -111,7 +111,7 @@ def throwError(message='Unknown error',exitcode=1,show_usage=True): pkgsinfoPath = os.path.realpath(args.repo + '/pkgsinfo') if not os.access(pkgsinfoPath, os.W_OK): throwError('The pkgsinfo directory in given munki repo is not writable.') - + pwd = os.path.dirname(os.path.realpath(__file__)) f = open(os.path.join(pwd, 'AddPrinter-Template.plist'), 'rb') templatePlist = load_plist(f) @@ -180,7 +180,7 @@ def createPlist( driver = '/Library/Printers/PPDs/Contents/Resources/%s.ppd' % printer_name else: newPlist.pop('preinstall_script', None) - + if driver.startswith('/Library'): # Assume the user passed in a full path rather than a relative filename driver_path = driver @@ -224,8 +224,8 @@ def createPlist( newFileName = os.path.realpath(pkgsinfoPath + os.path.sep + subdirectory + os.path.sep + newFileName) else: newFileName = os.path.realpath(pkgsinfoPath + os.path.sep + newFileName) - - print('Wrote pkginfo file to %s' % newFileName) + + print('Writing pkginfo file to %s' % newFileName) f = open(newFileName, 'wb') dump_plist(newPlist, f) @@ -238,7 +238,6 @@ def createPlist( reader = csv.DictReader(infile, delimiter=find_delimiter(args.csv)) for row in reader: - # In earlier versions, each row contains up to 10 elements: # Printer Name, Location, Display Name, Address, Driver, Description, Options, Version, Requires, Icon # To preserve backward compatibility, define all possible elements with default values and check for @@ -288,7 +287,6 @@ def createPlist( subdirectory=row['Subdirectory'], munki_name=row['Munki Name']) - else: if not args.printername: throwError('Argument --printername is required') @@ -348,7 +346,7 @@ def createPlist( options = args.options else: options = '' - + if args.catalogs: catalogs = args.catalogs else: From 5ed7feffdef2c25a608514048a686e7e0f12648b Mon Sep 17 00:00:00 2001 From: Matthias Choules Date: Tue, 19 Apr 2022 07:54:41 +0200 Subject: [PATCH 21/21] Make script executable --- print_generator.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 print_generator.py diff --git a/print_generator.py b/print_generator.py old mode 100644 new mode 100755