├── .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 |
--------------------------------------------------------------------------------