forked from imagr/imagr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
validateplist
executable file
·239 lines (203 loc) · 9.17 KB
/
validateplist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/python
"""
This script will validate an Imagr configuration plist. Usage:
./validateplist ~/path/to/config.plist
The rules of Imagr
#1 - if there is a password, its hash must not be empty
#2 - there must be at least one workflow
#3 - Workflow names must be unique
#4 - Image components must have a url
#5 - Image urls must end in dmg
#6 - Package components must have a url
#7 - Package urls must end with either pkg or dmg
#8 - Scripts must have content or a url
#9 - Restart actions must be 'none', 'shutdown' or 'restart'
#10 - first_boot must be a boolean on all component types
#11 - Partition actions must have at least one partition
#12 - Partition actions must have a target
#13 - Partition actions must have one 'size' for each partition, if 'size' is specified
#14 - Partition actions must not be done at first boot
#15 - eraseVolume actions must not be done at first boot
#16 - if a default workflow is specified, it must match the name of an existing workflow
#17 - if a workflow to autorun is specified, it must match the name of an existing workflow
#18 - If a destructive task comes after a non-destructive task, the admin should be warned
"""
import os
import sys
import argparse
import plistlib
import subprocess
import tempfile
import shutil
if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Imagr')):
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Imagr'))
elif os.path.exists('/usr/local/munki/munkilib'):
sys.path.append('/usr/local/munki/munkilib')
else:
pass
def fail(error):
"""
ERROR with message from function
"""
print "ERROR: %s" % error
sys.exit(1)
def validate_component(component, workflow):
"""
Validate components as per the rules of Imagr, for they must be obeyed.
"""
# Rule 10
if 'first_boot' in component:
if str(type(component['first_boot'])) != "<type 'bool'>":
fail("'first_boot' must be a boolen (<true/> or <false/>). \
Not found in %s" % workflow['name'])
if 'type' not in component:
fail("'type' is a required key in a component. Not found in %s" % workflow['name'])
if component['type'] == 'image':
# Rule 4
if 'url' not in component:
fail("'url' is a required key in an 'image' component. \
Not found in %s" % workflow['name'])
# Rule 5
if not component['url'].endswith(".dmg") and not \
component['url'].endswith(".sparseimage"):
fail("The 'url' in 'image' components must end with '.dmg' or \
'.sparseimage'. Not found in %s" % workflow['name'])
if component['type'] == 'package':
# Rule 6
if 'url' not in component:
fail("'url' is a required key in a 'package' component. \
Not found in %s" % workflow['name'])
# Rule 7
if not component['url'].endswith(".dmg") and not component['url'].endswith(".pkg"):
fail("The 'url' in 'package' components must end with '.dmg' or '.pkg'. \
Not found in %s" % workflow['name'])
if component['type'] == 'script':
# Rule 8
if 'content' not in component and 'url' not in component:
fail("'content' is a required key in a 'script' component. \
Not found in %s" % workflow['name'])
if component['type'] == 'partition':
# Rule 11
if 'partitions' not in component:
fail("'partitions' is a required key in a 'partition' component. \
Not found in %s" % workflow['name'])
if len(component['partitions']) == 0:
fail("'partitions' must have at least one item in the array. \
Not found in %s" % workflow['name'])
# Rule 12
target_found = 0
size_found = 0
for partition in component['partitions']:
if 'target' in partition and partition['target'] is True:
target_found += 1
# Rule 13
if 'size' in partition:
size_found += 1
if target_found == 0:
fail("'partitions' must have at least one 'target'. \
Not found in %s" % workflow['name'])
elif target_found > 1:
fail("'partitions' must only have one target. \
Too many found in %s" % workflow['name'])
if size_found != len(component['partitions']):
fail("'partitions' must have a size for each partition. \
Not enough found in %s" % workflow['name'])
# Rule 14
try:
if (component['type'] == 'partition') and (component['first_boot'] is True):
fail("'partitions' must not be a first-boot action. \
'first_boot' = True found in %s" % workflow['name'])
except:
pass
# Rule 15
try:
if (component['type'] == 'eraseVolume') and (component['first_boot'] is True):
fail("'eraseVolume' must not be a first-boot action. \
'first_boot' = True found in %s" % workflow['name'])
except:
pass
def main():
"""
Gimme some main
"""
temp_plist = None
parser = argparse.ArgumentParser()
parser.add_argument('plist', help='Path or URL to your Imagr config plist')
args = parser.parse_args()
if 'plist' not in args:
fail('Path to configuration plist must be specified.')
plist = args.plist
if plist.startswith('http://') or plist.startswith('https://'):
temp_dir = tempfile.mkdtemp()
cmd = ['/usr/bin/curl', '-fsSL', plist, '-o', os.path.join(temp_dir, 'config.plist')]
task = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc = task.communicate()[0]
if task.returncode != 0:
fail(proc)
temp_plist = os.path.join(temp_dir, 'config.plist')
plist = temp_plist
if not os.path.exists(plist):
fail("Couldn't find configuration plist at %s" % plist)
# Lint plist
cmd = ['/usr/bin/plutil', '-lint', plist]
task = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc = task.communicate()[0]
if task.returncode != 0:
fail(proc)
# Convert to XML so plistlib can read it
cmd = ['/usr/bin/plutil', '-convert', 'xml1', plist]
task = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc = task.communicate()[0]
if task.returncode != 0:
fail(proc)
try:
config = plistlib.readPlist(plist)
except:
fail("Couldn't read plist. Make sure it's a valid ")
if temp_plist:
shutil.rmtree(temp_dir)
# Rule 1
if 'password' in config and len(config['password']) == 0:
fail('There must be a valid password set.')
# Rule 2
if 'workflows' not in config:
fail('There are no workflows.')
if len(config['workflows']) == 0:
fail('There are no workflows.')
existing_names = []
for workflow in config['workflows']:
# Rule 3
if workflow['name'] in existing_names:
fail('Workflow names must be unique. %s has already been used.' % workflow['name'])
else:
existing_names.append(workflow['name'])
# Rule 9
if 'restart_action' in workflow:
if workflow['restart_action'] != 'none' and workflow['restart_action'] != 'restart' \
and workflow['restart_action'] != 'shutdown':
fail("restart_action is not one of 'none', 'shutdown' or 'restart' in workflow %s" \
% workflow['name'])
for component in workflow['components']:
validate_component(component, workflow)
# Rule 18
seen_components = []
for component in workflow['components']:
if component.get('type') == 'partition' or component.get('type') == 'image':
for seen_component in seen_components:
if seen_component.get('type') != 'partition' \
and seen_component.get('type') != 'image':
print 'WARNING: The %s component in workflow %s is a destructive action \
that comes after non destructive tasks. This may be intentional, \
but the results of the previous actions may be removed from the \
target disk.' % (component.get('type'), workflow['name'])
seen_components.append(component)
# Rule 16
if 'default_workflow' in config and config['default_workflow'] not in existing_names:
fail('Default workflow must match the name of an existing workflow.')
# Rule 17
if 'autorun' in config and config['autorun'] not in existing_names:
fail('Autorun workflow must match the name of an existing workflow')
# if we get this far, it looks good.
print "SUCCESS: %s looks like a valid Imagr configuration plist." % plist
if __name__ == '__main__':
main()