├── .gitignore ├── FoundationPlist.py ├── README.md ├── config_creator.py └── testing_imagr_config.plist /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /FoundationPlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright 2009-2011 Greg Neagle. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """FoundationPlist.py -- a tool to generate and parse MacOSX .plist files. 18 | 19 | This is intended as a drop-in replacement for Python's included plistlib, 20 | with a few caveats: 21 | - readPlist() and writePlist() operate only on a filepath, 22 | not a file object. 23 | - there is no support for the deprecated functions: 24 | readPlistFromResource() 25 | writePlistToResource() 26 | - there is no support for the deprecated Plist class. 27 | 28 | The Property List (.plist) file format is a simple XML pickle supporting 29 | basic object types, like dictionaries, lists, numbers and strings. 30 | Usually the top level object is a dictionary. 31 | 32 | To write out a plist file, use the writePlist(rootObject, filepath) 33 | function. 'rootObject' is the top level object, 'filepath' is a 34 | filename. 35 | 36 | To parse a plist from a file, use the readPlist(filepath) function, 37 | with a file name. It returns the top level object (again, usually a 38 | dictionary). 39 | 40 | To work with plist data in strings, you can use readPlistFromString() 41 | and writePlistToString(). 42 | """ 43 | 44 | from Foundation import NSData, \ 45 | NSPropertyListSerialization, \ 46 | NSPropertyListMutableContainers, \ 47 | NSPropertyListXMLFormat_v1_0 48 | 49 | class FoundationPlistException(Exception): 50 | pass 51 | 52 | class NSPropertyListSerializationException(FoundationPlistException): 53 | pass 54 | 55 | class NSPropertyListWriteException(FoundationPlistException): 56 | pass 57 | 58 | def readPlist(filepath): 59 | """ 60 | Read a .plist file from filepath. Return the unpacked root object 61 | (which is usually a dictionary). 62 | """ 63 | plistData = NSData.dataWithContentsOfFile_(filepath) 64 | dataObject, plistFormat, error = \ 65 | NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_( 66 | plistData, NSPropertyListMutableContainers, None, None) 67 | if error: 68 | error = error.encode('ascii', 'ignore') 69 | errmsg = "%s in file %s" % (error, filepath) 70 | raise NSPropertyListSerializationException(errmsg) 71 | else: 72 | return dataObject 73 | 74 | 75 | def readPlistFromString(data): 76 | '''Read a plist data from a string. Return the root object.''' 77 | plistData = buffer(data) 78 | dataObject, plistFormat, error = \ 79 | NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_( 80 | plistData, NSPropertyListMutableContainers, None, None) 81 | if error: 82 | error = error.encode('ascii', 'ignore') 83 | raise NSPropertyListSerializationException(error) 84 | else: 85 | return dataObject 86 | 87 | 88 | def writePlist(dataObject, filepath): 89 | ''' 90 | Write 'rootObject' as a plist to filepath. 91 | ''' 92 | plistData, error = \ 93 | NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_( 94 | dataObject, NSPropertyListXMLFormat_v1_0, None) 95 | if error: 96 | error = error.encode('ascii', 'ignore') 97 | raise NSPropertyListSerializationException(error) 98 | else: 99 | if plistData.writeToFile_atomically_(filepath, True): 100 | return 101 | else: 102 | raise NSPropertyListWriteException( 103 | "Failed to write plist data to %s" % filepath) 104 | 105 | 106 | def writePlistToString(rootObject): 107 | '''Return 'rootObject' as a plist-formatted string.''' 108 | plistData, error = \ 109 | NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_( 110 | rootObject, NSPropertyListXMLFormat_v1_0, None) 111 | if error: 112 | error = error.encode('ascii', 'ignore') 113 | raise NSPropertyListSerializationException(error) 114 | else: 115 | return str(plistData) 116 | 117 | 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imagr Config Creator (CLI) 2 | 3 | This interactive script will generate or edit a [workflow plist](https://github.com/grahamgilbert/imagr/wiki/Workflow-Config) for [Imagr](https://github.com/grahamgilbert/imagr). 4 | 5 | ## Usage 6 | 7 | The only command line argument *must* be a path to a plist file. If no plist exists at that location, it will create one (and fail if it can't write to it). 8 | 9 | ```bash 10 | ./config_creator.py /Users/nmcspadden/Desktop/imagr_config.plist 11 | ``` 12 | 13 | ``` 14 | nmcspadden$ ./config_creator.py imagr_config.plist 15 | Entering interactive mode... (type "help" for commands) 16 | > help 17 | Available sub-commands: 18 | add-computername-component 19 | add-erase-component 20 | add-image-component 21 | add-package-component 22 | add-partition-component 23 | add-script-component 24 | add-workflow 25 | display-components 26 | display-workflows 27 | exit 28 | help 29 | new-password 30 | remove-component 31 | remove-workflow 32 | set-bless-target 33 | set-description 34 | set-restart-action 35 | show-password 36 | show-workflow 37 | > 38 | ``` 39 | 40 | ### Command list: 41 | 42 | For more information on what these arguments represent, consult the [Imagr documentation](https://github.com/grahamgilbert/imagr/wiki/Workflow-Config). 43 | 44 | `exit` - **Changes are only saved when you exit. Ctrl-C or Ctrl-D will NOT save the plist.** 45 | 46 | Password related: 47 | 48 | * `show-password` - shows the existing password hash. 49 | * `new-password PASSWORD` - sets the password to the hash of "PASSWORD". 50 | 51 | Workflow related: 52 | 53 | * `display-workflows` - displays an indexed list of all workflows found in the plist. 54 | * `show-workflow NAME OR INDEX` - displays the contents of a workflow by "name" or at "index". If the name contains spaces, it must be quoted - i.e. 'My Workflow'. 55 | * `add-workflow NAME --index INDEX` - adds a new workflow with "name" to the list at 'index' location. If no index is specified, the workflow is added to the end of the list. 56 | * `remove-workflow NAME OR INDEX` - deletes the workflow from the list by "name" or at "index". 57 | 58 | Per-Workflow settings related: 59 | 60 | * `set-restart-action --workflow NAME OR INDEX --restart ACTION` - sets the RestartAction for a workflow by "name" or at "index" to "action". "action" must be either `restart`, `shutdown`, or `none`. If "action" is not specified, `none` is chosen by default. By default, this is `none`. 61 | * `set-bless-target --workflow NAME OR INDEX --no-bless` - sets the Bless_Target for a workflow by "name" or at "index" to "False". By default, the bless_target option for a workflow is True. 62 | * `set-description --workflow NAME OR INDEX --desc DESCRIPTION` - sets the Description for a workflow by "name" or at "index" to "Description". "Description" should be quoted. By default, this is blank. 63 | 64 | Component related: 65 | 66 | * `display-components NAME OR INDEX` - displays the list of components for a workflow by "name" or at "index". 67 | * `remove-component --workflow NAME OR INDEX --component INDEX` - removes a component from the list at index "component" for a workflow by "name" or at "index". 68 | * `add-image-component --workflow NAME OR INDEX --url URL --index INDEX` - adds an Image task to the component list at "index" for a workflow by "name" or at "index". If "index" is not specified, task is added to the end of the component list. "URL" should be a URL. Only one image task is allowed per workflow. 69 | * `add-package-component --workflow NAME OR INDEX --url URL --no-firstboot --index INDEX` - adds a Package task to the component list at "component index" for a workflow by "name" or at "index". "URL" should be a URL. By default, this package will be installed at first boot, unless "--no-firstboot" is specified. If "component index" is not specified, task is added to the end of the component list. 70 | * `add-computername-component --workflow NAME OR INDEX --use-serial --auto --index INDEX` - adds a ComputerName task to the component list at "component index" for a workflow by "name" or at "index". If "use-serial" is specified, the serial number will be the default computer name choice. If "auto" is specified, the serial number will be forced as the computer name and not allow overriding. If "component index" is not specified, task is added to the end of the component list. 71 | * `add-script-component --workflow NAME OR INDEX --content CONTENT --no-firstboot --index INDEX` - adds a Script task to the component list at "component index" for a workflow by "name" or at "index". "CONTENT" should be a valid path to a script that will be parsed and added to the plist. By default, this package will be installed at first boot, unless "--no-firstboot" is specified. If "component index" is not specified, task is added to the end of the component list. 72 | * `add-erase-component --workflow NAME OR INDEX --name NAME --format FORMAT --index INDEX` - adds an Erase task to the component list at "component index" for a workflow by "name" or at "index". "NAME" is the name to set the erased volume to. By default, this is "Macintosh HD". "FORMAT" is the format type to use for the new volume. By default, this is "Journaled HFS+". For a list of acceptable format types, use `diskutil listFileSystems`. If "component index" is not specified, task is added to the end of the component list. 73 | * `add-partition-component --workflow NAME OR INDEX --map MAP --names NAMES --formats FORMATS --sizes SIZES --target NAME --index INDEX` - adds a Partition task to the component list at "component index" for a workflow by "name" or at "index". "MAP" is the partition map to use for the disk. By default, this is "GPTFormat" (GUID). "NAMES", "FORMATS", and "SIZES" will take any number of arguments, each corresponding to a volume that will be created with NAME, FORMAT, and SIZE. For details on acceptable SIZES values, please see the `diskutil` man pages. If "component index" is not specified, task is added to the end of the component list. 74 | 75 | 76 | 77 | ## Examples 78 | 79 | ``` 80 | $ ./config_creator.py new_imagr_config.plist 81 | Entering interactive mode... (type "help" for commands) 82 | > add-workflow 'First Image' 83 | Workflow 'First Image': 84 | {'bless_target': False, 85 | 'components': [], 86 | 'description': '', 87 | 'name': 'First Image', 88 | 'restart_action': 'none'} 89 | > add-image-component --workflow 'First Image' --url 'http://server/image.dmg' 90 | Workflow 'First Image': 91 | {'bless_target': False, 92 | 'components': [{'type': 'image', 'url': 'http://server/image.dmg'}], 93 | 'description': '', 94 | 'name': 'First Image', 95 | 'restart_action': 'none'} 96 | > add-package-component --workflow 'First Image' --url 'http://server/munki.pkg' 97 | Workflow 'First Image': 98 | {'bless_target': False, 99 | 'components': [{'type': 'image', 'url': 'http://server/image.dmg'}, 100 | {'first_boot': True, 101 | 'type': 'package', 102 | 'url': 'http://server/munki.pkg'}], 103 | 'description': '', 104 | 'name': 'First Image', 105 | 'restart_action': 'none'} 106 | > add-script-component --workflow 'First Image' --content 'test_postinstall_script.sh' --no-firstboot 107 | Workflow 'First Image': 108 | {'bless_target': False, 109 | 'components': [{'type': 'image', 'url': 'http://server/image.dmg'}, 110 | {'first_boot': True, 111 | 'type': 'package', 112 | 'url': 'http://server/munki.pkg'}, 113 | {'content': '#!/bin/bash\necho "<"\necho "{{target_volume}}"\n/usr/bin/touch "{{target_volume}}/some_file"', 114 | 'first_boot': False, 115 | 'type': 'script'}], 116 | 'description': '', 117 | 'name': 'First Image', 118 | 'restart_action': 'none'} 119 | > exit 120 | 121 | ``` 122 | 123 | Resulting plist: 124 | ``` 125 | 126 | 127 | 128 | 129 | password 130 | 131 | workflows 132 | 133 | 134 | bless_target 135 | 136 | components 137 | 138 | 139 | type 140 | image 141 | url 142 | http://server/image.dmg 143 | 144 | 145 | first_boot 146 | 147 | type 148 | package 149 | url 150 | http://server/munki.pkg 151 | 152 | 153 | content 154 | #!/bin/bash 155 | echo "<" 156 | echo "{{target_volume}}" 157 | /usr/bin/touch "{{target_volume}}/some_file" 158 | first_boot 159 | 160 | type 161 | script 162 | 163 | 164 | description 165 | 166 | name 167 | First Image 168 | restart_action 169 | none 170 | 171 | 172 | 173 | 174 | ``` 175 | 176 | ### Partition example 177 | 178 | ``` 179 | $ ./config_creator.py new.plist 180 | Entering interactive mode... (type "help" for commands) 181 | > add-workflow 'hello' 182 | Workflow 'hello': 183 | {'bless_target': False, 184 | 'components': [], 185 | 'description': '', 186 | 'name': 'hello', 187 | 'restart_action': 'none'} 188 | > add-partition-component --workflow 'hello' --map 'GPTFormat' --names 'First' 'Second' --formats 'Journaled HFS+' 'Journaled HFS+' --sizes '50%' '50%' --target 'First' 189 | Workflow 'hello': 190 | {'bless_target': False, 191 | 'components': [{'map': 'GPTFormat', 192 | 'partitions': [{'format_type': 'Journaled HFS+', 193 | 'name': 'First', 194 | 'size': '50%'}, 195 | {'format_type': 'Journaled HFS+', 196 | 'name': 'Second', 197 | 'size': '50%'}], 198 | 'type': 'partition'}], 199 | 'description': '', 200 | 'name': 'hello', 201 | 'restart_action': 'none'} 202 | > 203 | ``` 204 | 205 | Resulting plist: 206 | ``` 207 | 208 | 209 | 210 | 211 | password 212 | 213 | workflows 214 | 215 | 216 | bless_target 217 | 218 | components 219 | 220 | 221 | map 222 | GPTFormat 223 | partitions 224 | 225 | 226 | format_type 227 | Journaled HFS+ 228 | name 229 | First 230 | size 231 | 50% 232 | 233 | 234 | format_type 235 | Journaled HFS+ 236 | name 237 | Second 238 | size 239 | 50% 240 | 241 | 242 | type 243 | partition 244 | 245 | 246 | description 247 | 248 | name 249 | hello 250 | restart_action 251 | none 252 | 253 | 254 | 255 | 256 | ``` -------------------------------------------------------------------------------- /config_creator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import subprocess 4 | import sys 5 | import os 6 | import hashlib 7 | import pprint 8 | import argparse 9 | import readline 10 | import shlex 11 | import fnmatch 12 | import copy 13 | 14 | #try: 15 | # import FoundationPlist as plistlib 16 | #except ImportError: 17 | # import plistlib 18 | import FoundationPlist as plistlib 19 | 20 | 21 | # Imagr Config Plist class 22 | 23 | class ImagrConfigPlist(): 24 | workflowComponentTypes = { 'image' : {'url':'http'}, 25 | 'package' : { 'url' : 'http', 'first_boot' : True }, 26 | 'computername' : { 'use_serial' : False, 'auto' : False }, 27 | 'script' : { 'content' : '#!/bin/bash', 'first_boot' : True }, 28 | 'eraseVolume' : { 'name' : 'Macintosh HD', 'format' : 'Journaled HFS+' }, 29 | 'partition' : { 'map' : 'GPTFormat', 'partitions' : [ { 'format_type' : 'Journaled HFS+', 'name' : 'Macintosh HD', 'size' : '100%', 'target' : True } ] } 30 | } 31 | 32 | def __init__(self, path): 33 | if os.path.exists(path): 34 | self.internalPlist = plistlib.readPlist(path) 35 | else: 36 | self.internalPlist = { 'password':'', 'workflows':[] } 37 | self.plistPath = path 38 | 39 | def synchronize(self): 40 | """Writes the current plist to disk""" 41 | plistlib.writePlist(self.internalPlist, self.plistPath) 42 | 43 | def findWorkflowIndexByName(self, name): 44 | """Return the workflow index that matches a given name""" 45 | index = 0 46 | for workflow in self.internalPlist.get('workflows'): 47 | if workflow.get('name') == name: 48 | return index 49 | index += 1 50 | 51 | def findWorkflowNameByIndex(self, index): 52 | """Return the workflow name that matches a given index""" 53 | return self.internalPlist['workflows'][index]['name'] 54 | 55 | def replaceWorkflowByName(self, newWorkflow, name): 56 | """Replace the workflow (dict) that matches a given name with new workflow""" 57 | for workflow in self.internalPlist.get('workflows'): 58 | if workflow.get('name') == name: 59 | workflow = newWorkflow 60 | 61 | # Workflow-related functions that are not subcommands 62 | def getWorkflowComponentTypes(self): 63 | """Returns a list of possible workflowComponentTypes""" 64 | return self.workflowComponentTypes.keys() 65 | 66 | def getWorkflowNames(self): 67 | """Returns a list of names of workflows in the plist""" 68 | nameList = list() 69 | for workflow in self.internalPlist['workflows']: 70 | nameList.append(str(workflow['name'])) 71 | return nameList 72 | 73 | # Workflow subcommands 74 | def display_workflows(self, args): 75 | """Displays a pretty-print list of workflows""" 76 | #args is basically ignored 77 | if len(args) != 0: 78 | print >> sys.stderr, 'Usage: display-workflows' 79 | return 22 # Invalid argument 80 | workflows = list() 81 | for workflow in self.internalPlist.get('workflows'): 82 | workflows.append(workflow) 83 | for i, elem in enumerate(self.internalPlist['workflows']): 84 | print '\n{0}:\n{1}'.format(i, elem) 85 | return 0 86 | 87 | def add_workflow(self, args): 88 | """Adds a new workflow to the list of workflows at index. Index defaults to end of workflow list""" 89 | p = argparse.ArgumentParser(prog='add-workflow', 90 | description='''add-workflow NAME --index INDEX 91 | Adds a new workflow with NAME to workflow list. If INDEX is specified, 92 | workflow is added at that INDEX, otherwise added to end of list.''') 93 | p.add_argument('name', 94 | metavar='NAME', 95 | help='''quoted name of new workflow''') 96 | p.add_argument('--index', 97 | metavar='INDEX', 98 | help='''where in the component list the task will go - defaults to end of list''', 99 | default = False) 100 | try: 101 | arguments = p.parse_args(args) 102 | except argparse.ArgumentError, errmsg: 103 | print >> sys.stderr, str(errmsg) 104 | return 22 # Invalid argument 105 | except SystemExit: 106 | return 22 107 | # validate that the name isn't being reused 108 | for tempworkflow in self.internalPlist.get('workflows'): 109 | if tempworkflow['name'] == arguments.name: 110 | print >> sys.stderr, 'Error: name is already in use. Workflow names must be unique.' 111 | return 22 112 | if arguments.index == False: #this means one wasn't specified 113 | index = len(self.internalPlist['workflows']) 114 | else: 115 | index = int(arguments.index) 116 | workflow = dict() 117 | workflow['name'] = arguments.name 118 | workflow['description'] = '' 119 | workflow['restart_action'] = 'none' 120 | workflow['bless_target'] = False 121 | workflow['components'] = list() 122 | self.internalPlist['workflows'].insert(index, workflow) 123 | self.show_workflow(args) 124 | return 0 125 | 126 | def remove_workflow(self, args): 127 | """Removes workflow with given name or index from list""" 128 | p = argparse.ArgumentParser(prog='remove-workflow', 129 | description='''remove-workflow WORKFLOW NAME OR INDEX 130 | Removes workflow WORKFLOW from workflow list.''') 131 | p.add_argument('workflow', 132 | metavar='WORKFLOW NAME OR INDEX', 133 | help='''quoted name or index number of target workflow''', 134 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 135 | ) 136 | try: 137 | arguments = p.parse_args(args) 138 | except argparse.ArgumentError, errmsg: 139 | print >> sys.stderr, str(errmsg) 140 | return 22 # Invalid argument 141 | except SystemExit: 142 | return 22 143 | try: 144 | key = int(arguments.workflow) 145 | # If an index is provided, it can be cast to an int 146 | except ValueError: 147 | # A name was provided that can't be cast to an int 148 | key = self.findWorkflowIndexByName(arguments.workflow) 149 | try: 150 | del self.internalPlist['workflows'][key] 151 | except (IndexError, TypeError): 152 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 153 | return 22 154 | print "Removed workflow '%s' from list." % arguments.workflow 155 | print "Remaining workflows:" 156 | pprint.pprint(self.getWorkflowNames()) 157 | return 0 158 | 159 | def show_workflow(self, args): 160 | """Shows a workflow with a given name or index""" 161 | p = argparse.ArgumentParser(prog='show-workflow', 162 | description='''show-workflow WORKFLOW NAME OR INDEX 163 | Displays the contents of WORKFLOW.''') 164 | p.add_argument('workflow', 165 | metavar='WORKFLOW NAME OR INDEX', 166 | help='''quoted name or index number of target workflow''', 167 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 168 | ) 169 | try: 170 | arguments = p.parse_args(args) 171 | except argparse.ArgumentError, errmsg: 172 | print >> sys.stderr, str(errmsg) 173 | return 22 # Invalid argument 174 | except SystemExit: 175 | return 22 176 | try: 177 | key = int(arguments.workflow) 178 | # If an index is provided, it can be cast to an int 179 | except ValueError: 180 | # A name was provided that can't be cast to an int 181 | key = self.findWorkflowIndexByName(arguments.workflow) 182 | try: 183 | print "Workflow '%s':" % arguments.workflow 184 | pprint.pprint(self.internalPlist['workflows'][key]) 185 | except (IndexError, TypeError): 186 | # If it gets here, no workflow by that name or index was found. 187 | print >> sys.stderr, 'No workflow found at %s: ' % arguments.workflow 188 | return 22 189 | return 0 190 | 191 | # Password subcommands 192 | def show_password(self, args): 193 | """Returns the password hash""" 194 | #args is basically ignored 195 | if len(args) != 0: 196 | print >> sys.stderr, 'Usage: show-password' 197 | return 22 # Invalid argument 198 | print self.internalPlist.get('password') 199 | return 0 200 | 201 | def new_password(self, args): 202 | """Sets a new password""" 203 | p = argparse.ArgumentParser(prog='new-password', 204 | description='''new-password PASSWORD 205 | Sets a new PASSWORD to configuration plist.''') 206 | p.add_argument('password', 207 | metavar='PASSWORD', 208 | help='''new password''') 209 | try: 210 | arguments = p.parse_args(args) 211 | except argparse.ArgumentError, errmsg: 212 | print >> sys.stderr, str(errmsg) 213 | return 22 # Invalid argument 214 | except SystemExit: 215 | return 22 216 | self.internalPlist['password'] = hashlib.sha512(str(arguments.password)).hexdigest() 217 | self.show_password([]) 218 | return 0 219 | 220 | # RestartAction subcommands 221 | def set_restart_action(self, args): 222 | """Sets a restart action for the given workflow""" 223 | p = argparse.ArgumentParser(prog='set-restart-action', 224 | description='''set-restart-action --workflow WORKFLOW --restart RESTART 225 | Sets a restart action for WORKFLOW to RESTART. If --restart is not specified, it defaults to \'none\'.''') 226 | p.add_argument('--workflow', 227 | metavar='WORKFLOW NAME OR INDEX', 228 | help='''quoted name or index number of target workflow''', 229 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 230 | required = True) 231 | p.add_argument('--restart', 232 | metavar='RESTART', 233 | help='''restart action to use: restart, shutdown, or none''', 234 | choices=['restart', 'shutdown', 'none'], 235 | default = 'none') 236 | try: 237 | arguments = p.parse_args(args) 238 | except argparse.ArgumentError, errmsg: 239 | print >> sys.stderr, str(errmsg) 240 | return 22 # Invalid argument 241 | except SystemExit: 242 | return 22 243 | try: 244 | key = int(arguments.workflow) 245 | # If an index is provided, it can be cast to an int 246 | name = self.findWorkflowNameByIndex(key) 247 | except ValueError: 248 | # A name was provided that can't be cast to an int 249 | key = self.findWorkflowIndexByName(arguments.workflow) 250 | name = [ arguments.workflow ] 251 | try: 252 | self.internalPlist['workflows'][key]['restart_action'] = arguments.restart 253 | except (IndexError, TypeError): 254 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 255 | return 22 256 | self.show_workflow(name) 257 | return 0 258 | 259 | # Bless subcommands 260 | def set_bless_target(self, args): 261 | """Sets bless to True or False for the given workflow""" 262 | p = argparse.ArgumentParser(prog='set-bless-target', 263 | description='''set-bless-target --workflow WORKFLOW --no-bless 264 | Sets the bless_target option for WORKFLOW to False. By default, bless is true.''') 265 | p.add_argument('--workflow', 266 | metavar='WORKFLOW NAME OR INDEX', 267 | help='''quoted name or index number of target workflow''', 268 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 269 | required = True) 270 | p.add_argument('--no-bless', 271 | help='''sets bless_target value to False''', 272 | action='store_false') 273 | try: 274 | arguments = p.parse_args(args) 275 | except argparse.ArgumentError, errmsg: 276 | print >> sys.stderr, str(errmsg) 277 | return 22 # Invalid argument 278 | except SystemExit: 279 | return 22 280 | try: 281 | key = int(arguments.workflow) 282 | # If an index is provided, it can be cast to an int 283 | name = self.findWorkflowNameByIndex(key) 284 | except ValueError: 285 | # A name was provided that can't be cast to an int 286 | key = self.findWorkflowIndexByName(arguments.workflow) 287 | name = [ arguments.workflow ] 288 | try: 289 | self.internalPlist['workflows'][key]['bless_target'] = arguments.no_bless 290 | except (IndexError, TypeError): 291 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 292 | return 22 293 | self.show_workflow(name) 294 | return 0 295 | 296 | # Description subcommands 297 | def set_description(self, args): 298 | """Sets description for the given workflow""" 299 | p = argparse.ArgumentParser(prog='set-description', 300 | description='''set-description --workflow WORKFLOW --desc DESCRIPTION 301 | Sets the description for WORKFLOW to DESCRIPTION.''') 302 | p.add_argument('--workflow', 303 | metavar='WORKFLOW NAME OR INDEX', 304 | help='''quoted name or index number of target workflow''', 305 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 306 | required = True) 307 | p.add_argument('--desc', 308 | metavar='DESCRIPTION', 309 | help='''description for workflow''', 310 | required = True) 311 | try: 312 | arguments = p.parse_args(args) 313 | except argparse.ArgumentError, errmsg: 314 | print >> sys.stderr, str(errmsg) 315 | return 22 # Invalid argument 316 | except SystemExit: 317 | return 22 318 | try: 319 | key = int(arguments.workflow) 320 | # If an index is provided, it can be cast to an int 321 | name = self.findWorkflowNameByIndex(key) 322 | except ValueError: 323 | # A name was provided that can't be cast to an int 324 | key = self.findWorkflowIndexByName(arguments.workflow) 325 | name = [ arguments.workflow ] 326 | try: 327 | self.internalPlist['workflows'][key]['description'] = arguments.desc 328 | except (IndexError, TypeError): 329 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 330 | return 22 331 | self.show_workflow(name) 332 | return 0 333 | 334 | # Component subcommands 335 | def display_components(self, args): 336 | """Displays a list of components for a given workflow""" 337 | p = argparse.ArgumentParser(prog='display-components', 338 | description='''display-components WORKFLOW NAME OR INDEX 339 | Displays the components of WORKFLOW.''') 340 | p.add_argument('workflow', 341 | metavar='WORKFLOW NAME OR INDEX', 342 | help='''quoted name or index number of target workflow''', 343 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 344 | ) 345 | try: 346 | arguments = p.parse_args(args) 347 | except argparse.ArgumentError, errmsg: 348 | print >> sys.stderr, str(errmsg) 349 | return 22 # Invalid argument 350 | except SystemExit: 351 | return 22 352 | try: 353 | key = int(arguments.workflow) 354 | # If an index is provided, it can be cast to an int 355 | except ValueError: 356 | # A name was provided that can't be cast to an int 357 | key = self.findWorkflowIndexByName(arguments.workflow) 358 | try: 359 | for i, elem in enumerate(self.internalPlist['workflows'][key]['components']): 360 | print '{0}: {1}'.format(i, elem) 361 | except (IndexError, TypeError): 362 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 363 | return 22 364 | return 0 365 | 366 | def remove_component(self, args): 367 | """Removes a component at index from workflow""" 368 | p = argparse.ArgumentParser(prog='remove-component', 369 | description='''remove-component --workflow WORKFLOW NAME OR INDEX --component INDEX 370 | Remove the component at INDEX from WORKFLOW.''') 371 | p.add_argument('--workflow', 372 | metavar='WORKFLOW NAME OR INDEX', 373 | help='''quoted name or index number of target workflow''', 374 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 375 | required=True) 376 | p.add_argument('--component', 377 | metavar='INDEX', 378 | help='''index of component from list''', 379 | type=int, 380 | required=True) 381 | try: 382 | arguments = p.parse_args(args) 383 | except argparse.ArgumentError, errmsg: 384 | print >> sys.stderr, str(errmsg) 385 | return 22 # Invalid argument 386 | except SystemExit: 387 | return 22 388 | try: 389 | key = int(arguments.workflow) 390 | # If an index is provided, it can be cast to an int 391 | except ValueError: 392 | # A name was provided that can't be cast to an int 393 | key = self.findWorkflowIndexByName(arguments.workflow) 394 | try: 395 | del self.internalPlist['workflows'][key]['components'][arguments.component] 396 | except (IndexError, TypeError): 397 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 398 | return 22 399 | return 0 400 | 401 | def add_image_component(self, args): 402 | """Adds an Image task at index with URL for a workflow. If no index is specified, defaults to end""" 403 | p = argparse.ArgumentParser(prog='add-image-component', 404 | description='''add-image-component --workflow WORKFLOW --url URL --index INDEX 405 | Adds an Image task to the component list of the WORKFLOW from URL. If INDEX is specified, 406 | task is added at that INDEX, otherwise added to end of list.''') 407 | p.add_argument('--workflow', 408 | metavar='WORKFLOW NAME OR INDEX', 409 | help='''quoted name or index number of target workflow''', 410 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 411 | required = True) 412 | p.add_argument('--url', 413 | metavar='URL', 414 | help='''URL of image to apply''', 415 | required = True) 416 | p.add_argument('--index', 417 | metavar='INDEX', 418 | help='''where in the component list the task will go - defaults to end of list''', 419 | default = False) 420 | try: 421 | arguments = p.parse_args(args) 422 | except argparse.ArgumentError, errmsg: 423 | print >> sys.stderr, str(errmsg) 424 | return 22 # Invalid argument 425 | except SystemExit: 426 | return 22 427 | imageComponent = self.workflowComponentTypes['image'].copy() 428 | imageComponent['url'] = arguments.url 429 | imageComponent['type'] = 'image' 430 | try: 431 | key = int(arguments.workflow) 432 | # If an index is provided, it can be cast to an int 433 | name = self.findWorkflowNameByIndex(key) 434 | except ValueError: 435 | # A name was provided that can't be cast to an int 436 | key = self.findWorkflowIndexByName(arguments.workflow) 437 | name = [ arguments.workflow ] 438 | try: 439 | if arguments.index == False: #this means one wasn't specified 440 | index = len(self.internalPlist['workflows'][key]['components']) 441 | else: 442 | index = int(arguments.index) 443 | # Check here to make sure we only have one image component per workflow 444 | for component in self.internalPlist['workflows'][key]['components']: 445 | if component.get('type') == 'image': 446 | print >> sys.stderr, 'Error: only one image task allowed per workflow.' 447 | return 21 448 | self.internalPlist['workflows'][key]['components'].insert(index, imageComponent) 449 | except (IndexError, TypeError): 450 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 451 | return 22 452 | self.show_workflow(name) 453 | return 0 454 | 455 | def add_package_component(self, args): 456 | """Adds a Package task at index with URL, first_boot for workflow""" 457 | p = argparse.ArgumentParser(prog='add-package-component', 458 | description='''add-package-component --workflow WORKFLOW --url URL --no-firstboot --index INDEX 459 | Adds a Package task to the component list of the WORKFLOW from URL at first boot. If --no-firstboot is specified, the package is installed 'live' instead. 460 | If INDEX is specified, task is added at that INDEX, otherwise added to end of list.''') 461 | p.add_argument('--workflow', 462 | metavar='WORKFLOW NAME OR INDEX', 463 | help='''quoted name or index number of target workflow''', 464 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 465 | required = True) 466 | p.add_argument('--url', 467 | metavar='URL', 468 | help='''URL of image to apply''', 469 | required = True) 470 | p.add_argument('--no-firstboot', 471 | help='''sets first_boot value for package to False''', 472 | action='store_false') 473 | p.add_argument('--index', 474 | metavar='INDEX', 475 | help='''where in the component list the task will go - defaults to end of list''', 476 | default = False) 477 | try: 478 | arguments = p.parse_args(args) 479 | except argparse.ArgumentError, errmsg: 480 | print >> sys.stderr, str(errmsg) 481 | return 22 # Invalid argument 482 | except SystemExit: 483 | return 22 484 | packageComponent = self.workflowComponentTypes['package'].copy() 485 | packageComponent['url'] = arguments.url 486 | packageComponent['first_boot'] = arguments.no_firstboot 487 | packageComponent['type'] = 'package' 488 | try: 489 | key = int(arguments.workflow) 490 | # If an index is provided, it can be cast to an int 491 | name = self.findWorkflowNameByIndex(key) 492 | except ValueError: 493 | # A name was provided that can't be cast to an int 494 | key = self.findWorkflowIndexByName(arguments.workflow) 495 | name = [ arguments.workflow ] 496 | try: 497 | if arguments.index == False: #this means one wasn't specified 498 | index = len(self.internalPlist['workflows'][key]['components']) 499 | else: 500 | index = int(arguments.index) 501 | self.internalPlist['workflows'][key]['components'].insert(index, packageComponent) 502 | except (IndexError, TypeError): 503 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 504 | return 22 505 | self.show_workflow(name) 506 | return 0 507 | 508 | def add_computername_component(self, args): 509 | """Adds a ComputerName task at index with use_serial and auto for workflow""" 510 | p = argparse.ArgumentParser(prog='add-computername-component', 511 | description='''add-computername-component --workflow WORKFLOW --use-serial --auto --index INDEX 512 | Adds a ComputerName task to the component list of the WORKFLOW. If --user-serial is specified, the computer's serial number is chosen as default. 513 | If --auto is specified, the computer's serial number will be used and not allow overriding. 514 | If INDEX is specified, task is added at that INDEX, otherwise added to end of list.''') 515 | p.add_argument('--workflow', 516 | metavar='WORKFLOW NAME OR INDEX', 517 | help='''quoted name or index number of target workflow''', 518 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 519 | required = True) 520 | p.add_argument('--use-serial', 521 | help='''use the computer's serial number as the default name''', 522 | action='store_true') 523 | p.add_argument('--auto', 524 | help='''enforce using the computer's serial number as the default name''', 525 | action='store_true') 526 | p.add_argument('--index', 527 | metavar='INDEX', 528 | help='''where in the component list the task will go - defaults to end of list''', 529 | default = False) 530 | try: 531 | arguments = p.parse_args(args) 532 | except argparse.ArgumentError, errmsg: 533 | print >> sys.stderr, str(errmsg) 534 | return 22 # Invalid argument 535 | except SystemExit: 536 | return 22 537 | computerNameComponent = self.workflowComponentTypes['computername'].copy() 538 | computerNameComponent['use_serial'] = arguments.use_serial 539 | computerNameComponent['auto'] = arguments.auto 540 | computerNameComponent['type'] = 'computer_name' 541 | try: 542 | key = int(arguments.workflow) 543 | # If an index is provided, it can be cast to an int 544 | name = self.findWorkflowNameByIndex(key) 545 | except ValueError: 546 | # A name was provided that can't be cast to an int 547 | key = self.findWorkflowIndexByName(arguments.workflow) 548 | name = [ arguments.workflow ] 549 | try: 550 | if arguments.index == False: #this means one wasn't specified 551 | index = len(self.internalPlist['workflows'][key]['components']) 552 | else: 553 | index = int(arguments.index) 554 | self.internalPlist['workflows'][key]['components'].insert(index, computerNameComponent) 555 | except (IndexError, TypeError): 556 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 557 | return 22 558 | self.show_workflow(name) 559 | return 0 560 | 561 | def add_script_component(self, args): 562 | """Adds a Script component at index with content for workflow""" 563 | p = argparse.ArgumentParser(prog='add-script-component', 564 | description='''add-script-component --workflow WORKFLOW --content CONTENT --no-firstboot --index INDEX 565 | Adds a Script task to the component list of the WORKFLOW at first boot. CONTENT must be a path to a file. 566 | If --no-firstboot is specified, the package is installed 'live' instead of at first boot. 567 | If INDEX is specified, task is added at that INDEX, otherwise added to end of list.''') 568 | p.add_argument('--workflow', 569 | metavar='WORKFLOW NAME OR INDEX', 570 | help='''quoted name or index number of target workflow''', 571 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 572 | required = True) 573 | p.add_argument('--content', 574 | metavar='CONTENT', 575 | help='''path to a file containing a script''', 576 | required = True) 577 | p.add_argument('--no-firstboot', 578 | help='''sets first_boot value for package to False''', 579 | action='store_false') 580 | p.add_argument('--index', 581 | metavar='INDEX', 582 | help='''where in the component list the task will go - defaults to end of list''', 583 | default = False) 584 | try: 585 | arguments = p.parse_args(args) 586 | except argparse.ArgumentError, errmsg: 587 | print >> sys.stderr, str(errmsg) 588 | return 22 # Invalid argument 589 | except SystemExit: 590 | return 22 591 | try: 592 | fileobject = open(os.path.expanduser(arguments.content), mode='r', buffering=1) 593 | data = fileobject.read() 594 | fileobject.close() 595 | except (OSError, IOError): 596 | print >> sys.stderr, "Error: Couldn't read %s" % arguments.content 597 | return 22 #Invalid argument 598 | scriptComponent['content'] = data 599 | scriptComponent['first_boot'] = arguments.no_firstboot 600 | scriptComponent['type'] = 'script' 601 | try: 602 | key = int(arguments.workflow) 603 | # If an index is provided, it can be cast to an int 604 | name = self.findWorkflowNameByIndex(key) 605 | except ValueError: 606 | # A name was provided that can't be cast to an int 607 | key = self.findWorkflowIndexByName(arguments.workflow) 608 | name = [ arguments.workflow ] 609 | try: 610 | if arguments.index == False: #this means one wasn't specified 611 | index = len(self.internalPlist['workflows'][key]['components']) 612 | else: 613 | index = int(arguments.index) 614 | self.internalPlist['workflows'][key]['components'].insert(index, scriptComponent) 615 | except (IndexError, TypeError): 616 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 617 | return 22 618 | self.show_workflow(name) 619 | return 0 620 | 621 | def add_erase_component(self, args): 622 | """Adds an eraseVolume component at index with content for workflow""" 623 | p = argparse.ArgumentParser(prog='add-erase-component', 624 | description='''add-erase-component --workflow WORKFLOW --name NAME --format FORMAT --index INDEX 625 | Adds an eraseVolume task to the component list of the WORKFLOW. 626 | NAME is the name of the newly formatted volume. Defaults to "Macintosh HD". 627 | FORMAT is the format of the volume. Defaults to "Journaled HFS+". 628 | If INDEX is specified, task is added at that INDEX, otherwise added to end of list.''') 629 | p.add_argument('--workflow', 630 | metavar='WORKFLOW NAME OR INDEX', 631 | help='''quoted name or index number of target workflow''', 632 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 633 | required = True) 634 | p.add_argument('--name', 635 | metavar='NAME', 636 | help='''name of volume - defaults to "Macintosh HD"''', 637 | default = 'Macintosh HD') 638 | p.add_argument('--format', 639 | metavar='FORMAT', 640 | help='''format of volume - defaults to "Journaled HFS+"''', 641 | default = 'Journaled HFS+') 642 | p.add_argument('--index', 643 | metavar='INDEX', 644 | help='''where in the component list the task will go - defaults to end of list''', 645 | default = False) 646 | try: 647 | arguments = p.parse_args(args) 648 | except argparse.ArgumentError, errmsg: 649 | print >> sys.stderr, str(errmsg) 650 | return 22 # Invalid argument 651 | except SystemExit: 652 | return 22 653 | eraseComponent = self.workflowComponentTypes['eraseVolume'].copy() 654 | eraseComponent['name'] = arguments.name 655 | eraseComponent['format'] = arguments.format 656 | eraseComponent['type'] = 'eraseVolume' 657 | try: 658 | key = int(arguments.workflow) 659 | # If an index is provided, it can be cast to an int 660 | name = self.findWorkflowNameByIndex(key) 661 | except ValueError: 662 | # A name was provided that can't be cast to an int 663 | key = self.findWorkflowIndexByName(arguments.workflow) 664 | name = [ arguments.workflow ] 665 | try: 666 | if arguments.index == False: #this means one wasn't specified 667 | index = len(self.internalPlist['workflows'][key]['components']) 668 | else: 669 | index = int(arguments.index) 670 | self.internalPlist['workflows'][key]['components'].insert(index, eraseComponent) 671 | except (IndexError, TypeError): 672 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 673 | return 22 674 | self.show_workflow(name) 675 | return 0 676 | 677 | def add_partition_component(self, args): 678 | """Adds a Partition component at index with content for workflow""" 679 | p = argparse.ArgumentParser(prog='add-erase-component', 680 | description='''add-partition-component --workflow WORKFLOW --map MAP --names NAMES --formats FORMATS --sizes SIZES --target NAME --index INDEX 681 | Adds a Partition task to the component list of the WORKFLOW. 682 | MAP is the partition map of the disk. Defaults to "GPTFormat" (GUID). 683 | NAMES, FORMATS, and SIZES are lists of volumes to create, in order, with specific names, formats, and sizes. 684 | If INDEX is specified, task is added at that INDEX, otherwise added to end of list.''') 685 | p.add_argument('--workflow', 686 | metavar='WORKFLOW NAME OR INDEX', 687 | help='''quoted name or index number of target workflow''', 688 | choices=self.getWorkflowNames() + [str(s) for s in range(len(self.internalPlist['workflows']))], 689 | required = True) 690 | p.add_argument('--map', 691 | metavar='MAP', 692 | help='''partition map of disk - defaults to "GPTFormat"''', 693 | choices = ['GPTFormat', 'APMFormat', 'MBRFormat'], 694 | default = 'GPTFormat') 695 | p.add_argument('--names', 696 | metavar='NAMES', 697 | nargs = '+', 698 | help='''names of volumes to create, in order''', 699 | required = True) 700 | p.add_argument('--formats', 701 | metavar='FORMATS', 702 | nargs = '+', 703 | help='''formats of volumes to create, in order''', 704 | required = True) 705 | p.add_argument('--sizes', 706 | metavar='SIZES', 707 | nargs = '+', 708 | help='''sizes of volumes to create, in order''', 709 | required = True) 710 | p.add_argument('--target', 711 | metavar='NAME', 712 | help='''this volume will be set as the target for future actions in the workflow, must be among the list of names provided''', 713 | required = True) 714 | p.add_argument('--index', 715 | metavar='INDEX', 716 | help='''where in the component list the task will go - defaults to end of list''', 717 | default = False) 718 | try: 719 | arguments = p.parse_args(args) 720 | except argparse.ArgumentError, errmsg: 721 | print >> sys.stderr, str(errmsg) 722 | return 22 # Invalid argument 723 | except SystemExit: 724 | return 22 725 | partitionComponent = self.workflowComponentTypes['partition'].copy() 726 | partitionComponent['map'] = arguments.map 727 | partitionComponent['type'] = 'partition' 728 | partitionList = list() 729 | thePartition = dict() 730 | targetSet = False 731 | for i in range(0,len(arguments.names)): 732 | thePartition = dict() 733 | thePartition['name'] = arguments.names[i] 734 | thePartition['format_type'] = arguments.formats[i] 735 | thePartition['size'] = arguments.sizes[i] 736 | if arguments.names[i] == arguments.target: 737 | thePartition['target'] = True 738 | targetSet = True 739 | partitionList.append(thePartition) 740 | if not targetSet: 741 | print >> sys.stderr, 'Error: target "%s" is not a valid partition target choice.' % arguments.target 742 | return 22 743 | partitionComponent['partitions'] = partitionList 744 | try: 745 | key = int(arguments.workflow) 746 | # If an index is provided, it can be cast to an int 747 | name = self.findWorkflowNameByIndex(key) 748 | except ValueError: 749 | # A name was provided that can't be cast to an int 750 | key = self.findWorkflowIndexByName(arguments.workflow) 751 | name = [ arguments.workflow ] 752 | try: 753 | if arguments.index == False: #this means one wasn't specified 754 | index = len(self.internalPlist['workflows'][key]['components']) 755 | else: 756 | index = int(arguments.index) 757 | self.internalPlist['workflows'][key]['components'].insert(index, partitionComponent) 758 | except (IndexError, TypeError): 759 | print >> sys.stderr, 'Error: No workflow found at %s' % arguments.workflow 760 | return 22 761 | self.show_workflow(name) 762 | return 0 763 | 764 | 765 | # Generic helper functions for autocomplete, stolen from Munki manifestutil 766 | 767 | def tab_completer(text, state): 768 | """Called by the readline lib to calculate possible completions""" 769 | array_to_match = None 770 | if readline.get_begidx() == 0: 771 | # since we are at the start of the line 772 | # we are matching commands 773 | array_to_match = 'cmds' 774 | match_list = CMD_ARG_DICT.get('cmds', {}).keys() 775 | else: 776 | # we are matching args 777 | cmd_line = readline.get_line_buffer()[0:readline.get_begidx()] 778 | cmd = shlex.split(cmd_line)[-1] 779 | array_to_match = CMD_ARG_DICT.get('cmds', {}).get(cmd) 780 | if array_to_match: 781 | match_list = CMD_ARG_DICT[array_to_match] 782 | else: 783 | array_to_match = CMD_ARG_DICT.get('options', {}).get(cmd) 784 | if array_to_match: 785 | match_list = CMD_ARG_DICT[array_to_match] 786 | else: 787 | array_to_match = 'options' 788 | match_list = CMD_ARG_DICT.get('options',{}).keys() 789 | 790 | matches = [item for item in match_list 791 | if item.upper().startswith(text.upper())] 792 | try: 793 | return matches[state] 794 | except IndexError: 795 | return None 796 | 797 | def setUpTabCompleter(): 798 | """Starts our tab-completer when running interactively""" 799 | readline.set_completer(tab_completer) 800 | if sys.platform == 'darwin': 801 | readline.parse_and_bind ("bind ^I rl_complete") 802 | else: 803 | readline.parse_and_bind("tab: complete") 804 | 805 | def help(args): 806 | '''Prints available subcommands''' 807 | print "Available sub-commands:" 808 | subcommands = CMD_ARG_DICT['cmds'].keys() 809 | subcommands.sort() 810 | for item in subcommands: 811 | print '\t%s' % item 812 | return 0 813 | 814 | def handleSubcommand(args, plist): 815 | '''Does all our subcommands''' 816 | # strip leading hyphens and 817 | # replace embedded hyphens with underscores 818 | # so '--add-pkg' becomes 'add_pkg' 819 | # and 'new-manifest' becomes 'new_manifest' 820 | subcommand = args[0].lstrip('-').replace('-', '_') 821 | 822 | # special case the exit command 823 | if subcommand == 'exit': 824 | # we'll do something special here 825 | plist.synchronize() 826 | sys.exit(0) 827 | 828 | if subcommand == 'help': 829 | return help(args) 830 | 831 | try: 832 | # find function to call by looking in the ImagrConfigPlist name table 833 | # for a function with a name matching the subcommand 834 | subcommand_function = getattr(plist, subcommand) 835 | return subcommand_function(args[1:]) 836 | except (TypeError, KeyError, AttributeError), errmsg: 837 | # print >> sys.stderr, 'Unknown subcommand: %s: %s' % (subcommand, errmsg) 838 | print >> sys.stderr, 'Unknown subcommand: %s' % subcommand 839 | help(args) 840 | return 2 841 | 842 | 843 | # global variable 844 | CMD_ARG_DICT = {} 845 | 846 | def main(): 847 | global CMD_ARG_DICT 848 | parser = argparse.ArgumentParser() 849 | parser.add_argument("plist", help="Path to a plist to edit. Will create if it doesn't exist.") 850 | plistArgs = parser.parse_args() 851 | 852 | if os.path.exists(plistArgs.plist): 853 | try: 854 | configPlist = ImagrConfigPlist(plistArgs.plist) 855 | except Exception, errmsg: 856 | print >> sys.stderr, 'Could not read plist %s because: %s' % (plistArgs.plist, errmsg) 857 | sys.exit(-1) 858 | else: 859 | # file does not exist, we'll save it on exit 860 | configPlist = ImagrConfigPlist(plistArgs.plist) 861 | 862 | # List of commands mapped to data types that they'll autocomplete with 863 | cmds = { 864 | 'new-password': 'workflows', # new-password 865 | 'show-password': 'workflows', # show-password 866 | 'add-workflow': 'workflows', # add-workflow 867 | 'display-workflows': 'workflows', # display-workflows 868 | 'show-workflow': 'workflows', # show-workflow 869 | 'remove-workflow': 'workflows', # remove-workflow 870 | 'set-restart-action': 'workflows', # set-restart-action 871 | 'set-bless-target': 'workflows', # set-bless-target 872 | 'set-description': 'workflows', # set-description 873 | 'add-image-component': 'workflows', # add-image-component 874 | 'add-package-component': 'workflows', # add-package-component 875 | 'add-computername-component': 'workflows', # add-image-component 876 | 'add-script-component': 'workflows', # add-image-component 877 | 'add-erase-component': 'workflows', 878 | 'add-partition-component': 'workflows', 879 | 'remove-component': 'components', # remove-component 880 | 'display-components': 'components', # display-components 881 | 'exit': 'default', 882 | 'help': 'default', 883 | } 884 | CMD_ARG_DICT['cmds'] = cmds 885 | 886 | CMD_ARG_DICT['default'] = [] 887 | CMD_ARG_DICT['workflows'] = configPlist.getWorkflowNames() 888 | CMD_ARG_DICT['components'] = configPlist.getWorkflowComponentTypes() 889 | 890 | setUpTabCompleter() 891 | print 'Entering interactive mode... (type "help" for commands)' 892 | while 1: 893 | try: 894 | cmd = raw_input('> ') 895 | except (KeyboardInterrupt, EOFError): 896 | # React to Control-C and Control-D 897 | print # so we finish off the raw_input line 898 | sys.exit(0) 899 | args = shlex.split(cmd) 900 | #print "Args: %s" % args 901 | handleSubcommand(args, configPlist) 902 | 903 | if __name__ == '__main__': 904 | main() 905 | -------------------------------------------------------------------------------- /testing_imagr_config.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | password 6 | 278c0c9360772fc0ab2f25c7bcafdd2579e3aa05ba4041abbf30d0ebe4c9b8b1b56f4b1ba822ccf32072e3ac443b14b49d99fa1201b02011fd486078d83b3cde 7 | workflows 8 | 9 | 10 | name 11 | Yosemite Upgrade - Base 12 | description 13 | Erases the target volume, deploys a 10.10.3 base image. 14 | restart_action 15 | restart 16 | bless_target 17 | 18 | components 19 | 20 | 21 | type 22 | eraseVolume 23 | 24 | 25 | type 26 | package 27 | url 28 | http://repo.sacredsf.org/munki/imagr/InstallYosemite-10.10.3.dmg 29 | first_boot 30 | 31 | 32 | 33 | 34 | 35 | name 36 | Yosemite Upgrade 37 | description 38 | Deploys a 10.10.3 image with Munki Tools and its configuration. Does NOT wipe the drive first. 39 | restart_action 40 | restart 41 | bless_target 42 | 43 | components 44 | 45 | 46 | type 47 | package 48 | url 49 | http://repo.sacredsf.org/munki/imagr/InstallYosemite-10.10.3.dmg 50 | first_boot 51 | 52 | 53 | 54 | type 55 | package 56 | url 57 | http://repo.sacredsf.org/munki/imagr/Profile-ManagedInstalls-10.10Signed-2.5.pkg 58 | first_boot 59 | 60 | 61 | 62 | 63 | 64 | name 65 | Yosemite Upgrade - Aggressive 66 | description 67 | Erases target volume, then deploys a 10.10.3 image with Munki Tools and its configuration. 68 | restart_action 69 | restart 70 | bless_target 71 | 72 | components 73 | 74 | 75 | type 76 | eraseVolume 77 | 78 | 79 | type 80 | package 81 | url 82 | http://repo.sacredsf.org/munki/imagr/InstallYosemite-10.10.3.dmg 83 | first_boot 84 | 85 | 86 | 87 | type 88 | package 89 | url 90 | http://repo.sacredsf.org/munki/imagr/Profile-ManagedInstalls-10.10Signed-2.5.pkg 91 | first_boot 92 | 93 | 94 | 95 | 96 | 97 | name 98 | Attempted Formatting 99 | description 100 | Partition into "Hell" + "Yeah", set "Hell" as target, install package 101 | restart_action 102 | none 103 | bless_target 104 | 105 | components 106 | 107 | 108 | type 109 | partition 110 | map 111 | GPTFormat 112 | partitions 113 | 114 | 115 | format_type 116 | Journaled HFS+ 117 | name 118 | Hell 119 | size 120 | 50% 121 | target 122 | 123 | 124 | 125 | format_type 126 | Journaled HFS+ 127 | name 128 | Yeah 129 | size 130 | 50% 131 | 132 | 133 | 134 | 135 | type 136 | package 137 | url 138 | http://repo.sacredsf.org/munki/imagr/Profile-ManagedInstalls-10.10Signed-2.5.pkg 139 | first_boot 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | --------------------------------------------------------------------------------