├── test.plist ├── .gitignore ├── example-printer.plist ├── README.md ├── LICENSE └── printer-pkginfo /test.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | address 6 | 10.10.10.10 7 | display_name 8 | Epson Stylus Pro 4900 9 | location 10 | Upstairs, Holborn 11 | name 12 | ds_holborn_epson_4900 13 | ppd 14 | 2.4 15 | queue_name 16 | ds_epson_4900 17 | requires 18 | 19 | somedriver 20 | 21 | version 22 | 0.1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /example-printer.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | protocol 6 | ipp 7 | address 8 | hp-4100.domain.com/printers/Front_HP4100 9 | queue_name 10 | Front_HP4100 11 | name 12 | Reception HP 4100 13 | display_name 14 | Reception HP 4100 15 | location 16 | Front Desk 17 | options 18 | 19 | landscape 20 | media=A4 21 | 22 | ppd 23 | /Library/Printers/PPDs/Contents/Resources/HP LaserJet 4100 Series.gz 24 | default_catalog 25 | development 26 | default_category 27 | Printers 28 | requires 29 | 30 | HPPrinterDrivers 31 | 32 | default_developer 33 | Developer 34 | version 35 | 1.0 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | printer-pkginfo 2 | =============== 3 | 4 | Creates a nopkg-style pkginfo file from a plist file to install printers with 5 | [Munki](https://github.com/munki/munki). 6 | 7 | `printer-pkginfo` can be run interactively or using an XML plist file as 8 | input. In interactive mode, users are guided through filling in the basic 9 | information for the printer, but more advanced options used by `lpadmin` 10 | are not available (with the exception of duplexing). 11 | 12 | ``` 13 | usage: printer-pkginfo [-h] [-p PLIST] [-i] [-o OUTFILE] [-c CATALOG] 14 | 15 | A python script to create nopkg-style files for installing printers onto 16 | client systems using Munki (https://github.com/munki/munki). Printers can be 17 | imported from a pre-existing XML plist file or can be created interactively. 18 | Unless a file path is specified with -o, the pkg file will be written to 19 | STDOUT. 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | -p PLIST, --plist PLIST 24 | Path to an XML plist file containing key/value pairs 25 | for version, display_name, queue_name, ppd, location, 26 | and other options. See example-printer.plist for an 27 | example. 28 | -i, --interactive Create pkginfo file in interactive mode rather than 29 | with a .plist file 30 | -o OUTFILE, --outfile OUTFILE 31 | Write pkginfo to plist file rather than STDOUT 32 | -c CATALOG, --catalog CATALOG 33 | Set Munki catalog (default without -c is "testing") 34 | -v VERSION, --version VERSION 35 | Set package version if creating an updated pkginfo 36 | file for an existing printer (default without -v is 37 | 0.1) 38 | --prefix PREFIX With -i, set prefix for printer name as it appears in 39 | Munki manifests 40 | 41 | 42 | To import printers into Munki, copy the output file to the pkgsinfo/ directory 43 | of your Munki repo and then re-run makecatalogs 44 | ``` 45 | 46 | By default, pkginfo files are written to the STDOUT for easy inspection. This 47 | behavior can be changed by running the command with the `-o` option or using 48 | output redirection. 49 | 50 | ### Non-Interactive Usage 51 | 52 | After cloning the repo to your local system (this doesn't have to be the same 53 | system that you're running Munki on, but if it's not, that adds an extra step), 54 | copy the `example-printer.plist` file and edit the options to fit your own 55 | printer setup. 56 | 57 | Note that the `requires` string must match the name of the driver package 58 | installed on the target systems. If the drivers are unavailable from your 59 | Munki server and aren't found on the target system, the install will fail. 60 | 61 | After you've edited the plist file for your printer, run `printer-pkginfo` 62 | 63 | ``` 64 | ./printer-pkginfo -p example-printer.plist -o example_pkginfo-1.0.plist 65 | ``` 66 | 67 | Move the resulting plist file to your `pkgsinfo/` directory on your Munki 68 | server, then rebuild the catalogs using `makecatalogs`. You can now add the 69 | printer to manifests using `manifestutil`, where the `name` key from your 70 | originating plist file corresponds to the package name in Munki. 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /printer-pkginfo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Based on work by Graham Gilbert, 2014-2016 4 | # Modified by Hilary Brenum, 2018 5 | # 6 | # Copyright 2018 Hilary Brenum 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | 21 | import plistlib 22 | import argparse 23 | import tempfile 24 | import os 25 | import subprocess 26 | import sys 27 | 28 | makepkginfo = '/usr/local/munki/makepkginfo' 29 | 30 | 31 | def fail(errmsg=''): 32 | '''Print any error message to stderr, 33 | clean up install data, and exit''' 34 | if errmsg: 35 | print >> sys.stderr, errmsg 36 | # exit 37 | exit(1) 38 | 39 | def yesNo(question): 40 | reply = str(raw_input(question+' (y/n): ')).lower().strip() 41 | if reply[0] == 'y': 42 | return True 43 | else: 44 | return False 45 | 46 | def plistInput(plist): 47 | plist_opts = {} 48 | 49 | try: 50 | plist_opts = plistlib.readPlist(plist) 51 | except: 52 | fail('Could not read %s' % (plist)) 53 | 54 | # Make sure makepkginfo is available 55 | if not os.path.isfile(makepkginfo) or not os.access(makepkginfo, os.X_OK): 56 | fail("A required exeuctable, '%s', could not be found " 57 | "or is not executable!" % makepkginfo) 58 | 59 | if not plist_opts['name'] or not plist_opts['queue_name'] or not plist_opts['display_name'] or not plist_opts['ppd'] or not plist_opts['address']: 60 | fail("One or more of the required options are missing from %s" % 61 | plist) 62 | 63 | return plist_opts 64 | 65 | def interactiveInput(): 66 | plist_opts = {} 67 | plist_opts['address'] = raw_input('Printer Address: ') 68 | plist_opts['protocol'] = str(raw_input('Protocol (socket/ipp/lpd): [Default: lpd] ')).lower() 69 | if not plist_opts['protocol']: 70 | plist_opts['protocol'] = 'lpd' 71 | plist_opts['queue_name'] = raw_input('Server Queue Name: ') 72 | plist_opts['display_name'] = raw_input('Display Name: ') 73 | if plist_opts['queue_name']: 74 | plist_opts['name'] = plist_opts['queue_name'] 75 | else: 76 | plist_opts['name'] = str(plist_opts['display_name']).strip().replace(' ','') 77 | print "Package name for Munki manifests is: ", plist_opts['name'] 78 | plist_opts['location'] = raw_input('Location (optional): ') 79 | if yesNo("Use Generic PS Driver?"): 80 | plist_opts['ppd'] = "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/Resources/Generic.ppd" 81 | else: 82 | plist_opts['ppd'] = raw_input('PPD (driver) path: ') 83 | if yesNo("Duplex printing?"): 84 | plist_opts['options'] = [] 85 | plist_opts['options'].append('sides=two-sided-long-edge') 86 | 87 | return plist_opts 88 | 89 | def main(): 90 | description = "A python script to create nopkg-style files for installing printers onto client systems using Munki (https://github.com/munki/munki). Printers can be imported from a pre-existing XML plist file or can be created interactively. Unless a file path is specified with -o, the pkg file will be written to STDOUT." 91 | epilog = "To import printers into Munki, copy the output file to the pkgsinfo/ directory of your Munki repo and then re-run makecatalogs" 92 | 93 | o = argparse.ArgumentParser(description = description,epilog = epilog) 94 | 95 | o.add_argument("-p","--plist", 96 | help = "Path to an XML plist file containing key/value pairs for version, display_name, queue_name, ppd, location, and other options. See example-printer.plist for an example.") 97 | 98 | o.add_argument('-i','--interactive',help = "Create pkginfo file in interactive mode rather than with a .plist file",action = "store_true") 99 | 100 | o.add_argument('-o','--outfile',help = "Write pkginfo to plist file rather than STDOUT") 101 | 102 | o.add_argument('-c','--catalog',default = 'testing',help = "Set Munki catalog (default without -c is \"testing\")") 103 | 104 | o.add_argument('-v','--version',default = 0.1,help = "Set package version if creating an updated pkginfo file for an existing printer (default without -v is 0.1)",type = float) 105 | 106 | o.add_argument('--prefix',help = "With -i, set prefix for printer name as it appears in Munki manifests") 107 | 108 | args = o.parse_args() 109 | plist_opts = {} 110 | 111 | if args.interactive and args.outfile is None: 112 | o.error("Interactive mode (-i) requires output file to be specified with -o") 113 | 114 | if args.plist: 115 | plist_opts = plistInput(args.plist) 116 | elif args.interactive: 117 | plist_opts = interactiveInput() 118 | 119 | if plist_opts: 120 | try: 121 | if args['version']: 122 | version = args['version'] 123 | else: 124 | version = plist_opts['version'] 125 | except: 126 | version = '0.1' 127 | 128 | try: 129 | options = plist_opts['options'] 130 | except: 131 | options = None 132 | 133 | try: 134 | if args.catalog: 135 | catalog = args.catalog 136 | else: 137 | catalog = plist_opts['default_catalog'] 138 | except: 139 | catalog = 'testing' 140 | 141 | try: 142 | developer = plist_opts['default_developer'] 143 | except: 144 | developer = 'Printers' 145 | 146 | try: 147 | category = plist_opts['default_category'] 148 | except: 149 | category = 'Printers' 150 | 151 | try: 152 | protocol = plist_opts['protocol'] 153 | except: 154 | protocol = 'lpd' 155 | 156 | formatted_options = "" 157 | if options: 158 | for option in options: 159 | built = "-o " + option + " " 160 | formatted_options = formatted_options + built 161 | 162 | queue_name = plist_opts['queue_name'] 163 | if args.prefix: 164 | name = args.prefix + plist_opts['name'] 165 | else: 166 | name = plist_opts['name'] 167 | if len(queue_name) > 24: 168 | fail("Queue name is too long") 169 | 170 | display_name = plist_opts['display_name'] 171 | ppd = plist_opts['ppd'] 172 | if plist_opts['location']: 173 | location = plist_opts['location'] 174 | else: 175 | location = "" 176 | 177 | if plist_opts['queue_name']: 178 | address = plist_opts['address'] + "/" + plist_opts['queue_name'] 179 | else: 180 | address = plist_opts['address'] 181 | 182 | # build the install check script 183 | 184 | install_check_content = """#!/bin/sh 185 | 186 | # Based on 2010 Walter Meyer SUNY Purchase College (c) 187 | # Modified by Nick McSpadden, 2013 188 | 189 | # Script to install and setup printers on a Mac OS X system in a "Munki-Friendly" way. 190 | # Make sure to install the required drivers first! 191 | 192 | # Variables. Edit these. 193 | printername="%s" 194 | location="%s" 195 | gui_display_name="%s" 196 | address="%s" 197 | driver_ppd="%s" 198 | # Populate these options if you want to set specific options for the printer. E.g. duplexing installed, etc. 199 | option_1="" 200 | option_2="" 201 | option_3="" 202 | currentVersion="%s" 203 | 204 | function exitWithStatus () { 205 | if [ $1 -eq 0 ]; then 206 | echo "INSTALL REQUIRED" 207 | else 208 | echo "NO INSTALL REQUIRED" 209 | fi 210 | echo "-------------------" 211 | echo "" 212 | exit $1 213 | } 214 | 215 | echo "--------------------------------" 216 | echo "Checking $printername Printer... " 217 | echo "--------------------------------" 218 | 219 | 220 | ### Determine if receipt is installed ### 221 | printf "Receipt: " 222 | if [ -e "/private/etc/cups/deployment/receipts/$printername.plist" ]; then 223 | storedVersion=$(/usr/libexec/PlistBuddy -c "Print :version" "/private/etc/cups/deployment/receipts/$printername.plist") 224 | echo $storedVersion 225 | else 226 | echo "not stored." 227 | storedVersion="0" 228 | fi 229 | 230 | versionComparison=$(echo "$storedVersion < $currentVersion" | bc -l) 231 | # This will be 0 if the current receipt is greater than or equal to current version of the script 232 | 233 | printf "State: " 234 | 235 | ### Printer Install ### 236 | # If the queue already exists (returns 0), we don't need to reinstall it. 237 | LPSTATOUTPUT=$(/usr/bin/lpstat -p "$printername" 2>&1) 238 | if [ $? -eq 0 ]; then 239 | if [ "$versionComparison" -eq 0 ]; then 240 | # We are at the current or greater version 241 | echo "Configured." 242 | exitWithStatus 1 243 | fi 244 | # We are of lesser version, and therefore we should delete the printer and reinstall. 245 | echo "Configured, but receipt version $storedVersion doesn't match $currentVersion." 246 | exitWithStatus 0 247 | else 248 | # Not configured. For verbosity, say unconfigured and exit with status 0" 249 | echo "Unconfigured. $LPSTATOUTPUT" 250 | exitWithStatus 0 251 | fi 252 | """ % (queue_name, location, display_name, address, ppd, version) 253 | 254 | postinstall_content = """#!/bin/sh 255 | 256 | # Based on 2010 Walter Meyer SUNY Purchase College (c) 257 | # Modified by Nick McSpadden, 2013 258 | 259 | # Script to install and setup printers on a Mac OS X system in a "Munki-Friendly" way. 260 | # Make sure to install the required drivers first! 261 | 262 | # Variables. Edit these. 263 | printername="%s" 264 | location="%s" 265 | gui_display_name="%s" 266 | address="%s" 267 | driver_ppd="%s" 268 | # Populate these options if you want to set specific options for the printer. E.g. duplexing installed, etc. 269 | option_1="" 270 | option_2="" 271 | option_3="" 272 | currentVersion="%s" 273 | protocol="%s" 274 | 275 | function exitWithStatus () { 276 | if [ $1 -eq 0 ]; then 277 | echo "INSTALL SUCCESSFUL" 278 | echo 279 | exit 0 280 | fi 281 | echo "ERROR WITH INSTALL" 282 | echo 283 | exit 1 284 | } 285 | 286 | ### Determine if receipt is installed ### 287 | if [ -e "/private/etc/cups/deployment/receipts/$printername.plist" ]; then 288 | storedVersion=$(/usr/libexec/PlistBuddy -c "Print :version" "/private/etc/cups/deployment/receipts/$printername.plist") 289 | else 290 | storedVersion="0" 291 | fi 292 | 293 | versionComparison=$(echo "$storedVersion < $currentVersion" | bc -l) 294 | # This will be 0 if the current receipt is greater than or equal to current version of the script 295 | 296 | ### Printer Install ### 297 | # If the queue already exists (returns 0), we don't need to reinstall it. 298 | LPSTATOUTPUT=$(/usr/bin/lpstat -p "$printername" 2>&1) 299 | if [ $? -eq 0 ]; then 300 | if [ "$versionComparison" -eq 0 ]; then 301 | # We are at the current or greater version 302 | exitWithStatus 0 303 | fi 304 | # We are of lesser version, and therefore we should delete the printer and reinstall. 305 | printf "Newer install (${currentVersion})... removing existing printer ($printername)... " 306 | /usr/sbin/lpadmin -x "$printername" 307 | fi 308 | 309 | # Now we can install the printer. 310 | printf "Adding $printername... " 311 | /usr/sbin/lpadmin \ 312 | -p "$printername" \ 313 | -L "$location" \ 314 | -D "$gui_display_name" \ 315 | -v "${protocol}"://"${address}" \ 316 | -P "$driver_ppd" \ 317 | %s \ 318 | -o printer-is-shared=false \ 319 | -o printer-error-policy=abort-job \ 320 | -E 321 | 322 | 323 | if [ $? -ne 0 ]; then 324 | echo "Failed adding printer... Removing... " 325 | /usr/sbin/lpadmin -x "$printername" 326 | exitWithStatus 1 327 | fi 328 | 329 | # Get list of configured printers 330 | CONFIGUREDPRINTERS=$(lpstat -p | grep -w "printer" | awk '{print$2}' | tr '\n' ' ') 331 | 332 | # Check if configured. 333 | if [ $(echo "$CONFIGUREDPRINTERS" | grep -w "$printername" | wc -l | tr -d ' ') -eq 0 ]; then 334 | echo "ERROR" 335 | echo "$printername not in lpstat list after being configured. Currently configured printers are: $CONFIGUREDPRINTERS" 336 | exitWithStatus 1 337 | fi 338 | 339 | # Enable and start the printers on the system (after adding the printer initially it is paused). 340 | printf "Enabling... " 341 | /usr/sbin/cupsenable $CONFIGUREDPRINTERS 342 | 343 | # Create a receipt for the printer 344 | printf "Creating v${currentVersion} receipt... " 345 | mkdir -p /private/etc/cups/deployment/receipts 346 | PLISTBUDDYOUTPUT=$(/usr/libexec/PlistBuddy -c "Add :version string" "/private/etc/cups/deployment/receipts/$printername.plist" 2>&1) 347 | PLISTBUDDYOUTPUT+=$(/usr/libexec/PlistBuddy -c "Set :version $currentVersion" "/private/etc/cups/deployment/receipts/$printername.plist" 2>&1) 348 | 349 | # If problem setting version in receipt (above command), we tell user 350 | if [ $? -ne 0 ]; then 351 | echo "ERROR with receipt creation: $PLISTBUDDYOUTPUT" 352 | fi 353 | 354 | # Permission the directories properly. 355 | chown -R root:_lp /private/etc/cups/deployment 356 | chmod -R 700 /private/etc/cups/deployment 357 | 358 | echo "Complete!" 359 | echo "Current printers: $CONFIGUREDPRINTERS" 360 | 361 | exitWithStatus 0 362 | """ % (queue_name, location, display_name, address, ppd, version, protocol, formatted_options) 363 | 364 | uninstall_content = """#!/bin/sh 365 | printerName="%s" 366 | 367 | printf "Removing $printername... " 368 | /usr/sbin/lpadmin -x $printerName 369 | 370 | printf "Removing receipt... " 371 | rm -f /private/etc/cups/deployment/receipts/$printerName.plist 372 | 373 | echo "Uninstall complete." 374 | """ % (queue_name) 375 | 376 | # write the scripts to temp files 377 | tempdir = tempfile.mkdtemp() 378 | 379 | installcheck = os.path.join(tempdir, 'installcheck') 380 | f = open(installcheck, "w") 381 | f.write(install_check_content) 382 | f.close() 383 | installcheck_option = "--installcheck_script=%s" % installcheck 384 | 385 | postinstall = os.path.join(tempdir, 'postinstall') 386 | f = open(postinstall, "w") 387 | f.write(postinstall_content) 388 | f.close() 389 | postinstall_option = "--postinstall_script=%s" % postinstall 390 | 391 | uninstall = os.path.join(tempdir, 'uninstall') 392 | f = open(uninstall, "w") 393 | f.write(uninstall_content) 394 | f.close() 395 | uninstall_option = "--uninstall_script=%s" % uninstall 396 | 397 | catalog_opt = str('-c' + catalog) 398 | name_opt = '--name=%s' % name 399 | version_opt = '--pkgvers=%s' % version 400 | developer_opt = '--developer=%s' % developer 401 | category_opt = '--category=%s' % category 402 | displayname_opt = '--displayname=%s' % display_name 403 | # make pkg info 404 | cmd = [makepkginfo, installcheck_option, postinstall_option, uninstall_option, name_opt, version_opt, catalog_opt, 405 | developer_opt, category_opt, displayname_opt, '--unattended_install', '--uninstall_method=uninstall_script', '--nopkg'] 406 | 407 | task = subprocess.Popen( 408 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 409 | (stdout, stderr) = task.communicate() 410 | 411 | pkginfo = plistlib.readPlistFromString(stdout) 412 | 413 | # read it back so we can add in the requires (if required, sic) 414 | if args.plist and plist_opts['requires']: 415 | requires = plist_opts.get('requires') 416 | pkginfo['requires'] = requires 417 | 418 | # Add Uninstallable flag 419 | uninstallable = True 420 | pkginfo['uninstallable'] = uninstallable 421 | 422 | if args.outfile: 423 | plistlib.writePlist(pkginfo,args.outfile) 424 | else: 425 | print plistlib.writePlistToString(pkginfo) 426 | 427 | # print and clean up 428 | if __name__ == '__main__': 429 | main() 430 | --------------------------------------------------------------------------------