forked from arubdesu/jss-autopkg-addon
-
Notifications
You must be signed in to change notification settings - Fork 1
/
JSSImporter.py
executable file
·399 lines (385 loc) · 22.1 KB
/
JSSImporter.py
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#!/usr/bin/env python
# Calum Hunter
# 04-08-2014
# v.0.0.4
#
# Pretty much all of this code has been written
# by the awesome Allister Banks
# I have added a few little tweaks to have it play nice
# with the Patchoo! project by Lachlan Stewart.
# http://patchoo.github.io/patchoo/
# Copyright 2014 Allister Banks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import urllib2
import sys
import re
import base64
import shutil
import datetime
from xml.etree import ElementTree
from autopkglib import Processor, ProcessorError
__all__ = ["JSSImporter"]
class JSSImporter(Processor):
"""Imports a flat pkg to the JSS."""
input_variables = {
"pkg_path": {
"required": True,
"description": "Path to a pkg or dmg to import - provided by previous pkg recipe/processor.",
},
"version": {
"required": True,
"description": "Version number of software to import - provided by previous pkg recipe/processor.",
},
"JSS_REPO": {
"required": True,
"description": "Path to a mounted or otherwise locally accessible JSS dist point/share, optionally set as a key in the com.github.autopkg preference file.",
},
"JSS_URL": {
"required": True,
"description": "URL to a JSS that api the user has write access to, optionally set as a key in the com.github.autopkg preference file.",
},
"API_USERNAME": {
"required": True,
"description": "Username of account with appropriate access to jss, optionally set as a key in the com.github.autopkg preference file.",
},
"API_PASSWORD": {
"required": True,
"description": "Password of api user, optionally set as a key in the com.github.autopkg preference file.",
},
"category": {
"required": False,
"description": ("Category to create/associate imported app with"),
},
"smart_group": {
"required": False,
"description": "Name of scoping group to create with which to offer item to users that are not at the same version.",
},
"arb_group_name": {
"required": False,
"description": "Name of static group to offer imported item to.",
},
"selfserve_policy": {
"required": False,
"description": "Name of automatically activated self-service policy for offering software to test and older-version users. Will create if not present and update if data is not current or invalid",
},
}
output_variables = {
"jss_category_added": {
"description": "True if category was created."
},
"jss_repo_changed": {
"description": "True if item was imported."
},
"jss_smartgroup_added": {
"description": "True if smartgroup was added."
},
"jss_smartgroup_updated": {
"description": "True if smartgroup was updated."
},
"jss_staticgroup_added": {
"description": "True if staticgroup was added."
},
"jss_staticgroup_updated": {
"description": "True if staticgroup was updated."
},
"jss_policy_added": {
"description": "True if policy was added."
},
"jss_policy_updated": {
"description": "True if policy was updated."
},
}
description = __doc__
def checkItem(self, repoUrl, apiUrl, item_to_check, base64string):
"""This checks for all items of a certain type, and if found, returns the id"""
# build our request for the entire list of items
if apiUrl[-1] == "y":
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl[:-1] + "ies")
print('\nTrying to reach JSS and fetch all %s at URL %s' % (apiUrl[:-1] + "ies", repoUrl))
else:
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl + "s")
print('\nTrying to reach JSS and fetch all %s at URL %s' % (apiUrl + "s", repoUrl))
submitRequest.add_header("Authorization", "Basic %s" % base64string)
# try reaching the server and performing the GET
try:
submitResult = urllib2.urlopen(submitRequest)
except urllib2.URLError, e:
if hasattr(e, 'reason'):
print 'Error! reason:', e.reason
elif hasattr(e, 'code'):
print 'Error! code:', e.code
if e.code == 401:
raise RuntimeError('Got a 401 error.. check the api username and password')
raise RuntimeError('Did not get a valid response from the server')
# assign fetched JSS info to a string (or ET) and search if object to create already exists
jss_results = submitResult.read()
self.output("Results:", jss_results)
try:
xmldata = ElementTree.fromstring(jss_results)
except:
raise ProcessorError("Successfully communicated, but error'd when parsing XML return while checking for %s" % item_to_check)
if apiUrl == "computergroup":
item_ids = [e.text for e in xmldata.findall('computer_group/id')]
item_names = [e.text for e in xmldata.findall('computer_group/name')]
else:
item_ids = [e.text for e in xmldata.findall(apiUrl + '/id')]
item_names = [e.text for e in xmldata.findall(apiUrl + '/name')]
item_nameplusids = zip(item_ids, item_names)
self.output(item_nameplusids)
proceed_list = []
for tup in item_nameplusids:
if item_to_check == tup[1]:
proceed_list = ["proceed"]
proceed_list.append(tup[0])
break
self.output(jss_results)
return proceed_list
def checkSpecificItem(self, repoUrl, apiUrl, item_id, base64string, ids_list=None):
"""This checks for the targeted item in the JSS and returns applicable info"""
# build our request for the entire list of items
if apiUrl[-1] != "y":
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl + "s/id/" + item_id)
submitRequest.add_header("Authorization", "Basic %s" % base64string)
submitResult = urllib2.urlopen(submitRequest)
jss_results = submitResult.read()
try:
xmldata = ElementTree.fromstring(jss_results)
except:
raise ProcessorError("Error parsing the specific groups XML from checkSpecificItem results.")
crit_name = [e.text for e in xmldata.findall('criteria/criterion/name')]
crit_val = [e.text for e in xmldata.findall('criteria/criterion/value')]
item_nameplusvals = zip(crit_name, crit_val)
self.output(item_nameplusvals)
found_pkg_ver = None
for tup in item_nameplusvals:
if "Application Version" == tup[0]:
found_pkg_ver = tup[1]
break
return found_pkg_ver
else:
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl[:-1] + "ies/id/" + item_id)
submitRequest.add_header("Authorization", "Basic %s" % base64string)
submitResult = urllib2.urlopen(submitRequest)
jss_results = submitResult.read()
try:
xmldata = ElementTree.fromstring(jss_results)
except:
raise ProcessorError("Error parsing the XML for a specific policy from checkSpecificItem results.")
found_list = []
item_ids = [e.text for e in xmldata.findall('scope/computer_groups/computer_group/id')]
item_names = [e.text for e in xmldata.findall('scope/computer_groups/computer_group/name')]
item_nameplusids = zip(item_ids, item_names)
self.output(item_nameplusids)
for tup in item_nameplusids:
if ids_list[0] == tup[1]:
found_list = [tup[0]]
break
item_ids = [e.text for e in xmldata.findall('package_configuration/packages/package/id')]
item_names = [e.text for e in xmldata.findall('package_configuration/packages/package/name')]
item_nameplusids = zip(item_ids, item_names)
self.output(item_nameplusids)
for tup in item_nameplusids:
if ids_list[1] == tup[1]:
found_list.append(tup[0])
break
if len(found_list) == 2:
self.output("Found group id %s and pkg_id %s in policy" % (found_list[0], found_list[1]))
return found_list
def putUpdate(self, repoUrl, apiUrl, item_id, replace_dict, template_string, base64string):
"""After finding a mismatch, this PUTS a template with updated info"""
for key, value in replace_dict.iteritems():
template_string = template_string.replace(key, value)
if apiUrl[-1] == "y":
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl[:-1] + "ies/id/" + item_id, template_string, {'Content-type': 'text/xml'})
self.output("/JSSResource/" + apiUrl[:-1] + "ies/id/" + item_id, template_string)
else:
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl + "s/id/" + item_id, template_string, {'Content-type': 'text/xml'})
submitRequest.add_header("Authorization", "Basic %s" % base64string)
submitRequest.get_method = lambda: 'PUT'
submitResult = urllib2.urlopen(submitRequest)
jss_results = submitResult.read()
self.output(jss_results)
self.output("Added to %s section of JSS via API" % apiUrl)
def createObject(self, repoUrl, apiUrl, replace_dict, template_string, base64string):
"""After not finding an item, this updates and POSTs a template at id/0"""
for key, value in replace_dict.iteritems():
template_string = template_string.replace(key, value)
if apiUrl[-1] == "y":
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl[:-1] + "ies/id/0", template_string, {'Content-type': 'text/xml'})
else:
submitRequest = urllib2.Request(repoUrl + "/JSSResource/" + apiUrl + "s/id/0", template_string, {'Content-type': 'text/xml'})
submitRequest.add_header("Authorization", "Basic %s" % base64string)
try:
submitResult = urllib2.urlopen(submitRequest)
except urllib2.URLError, e:
if hasattr(e, 'reason'):
print 'Error! reason:', e.reason
elif hasattr(e, 'code'):
print 'Error! code:', e.code
if e.code == 409:
raise RuntimeError('Got a 409 error.. generic "instance busy" issue?')
raise RuntimeError('Did not get a valid response from the server')
jss_results = submitResult.read()
self.output(jss_results)
try:
xmldata = ElementTree.fromstring(jss_results)
except:
raise ProcessorError("Error parsing XML results after createObject.")
created_id = xmldata.find("id").text
self.output("Added to %s section of JSS via API" % apiUrl)
return created_id
def main(self):
# pull jss recipe-specific args, prep api auth
repoUrl = self.env["JSS_URL"]
authUser = self.env["API_USERNAME"]
authPass = self.env["API_PASSWORD"]
base64string = base64.encodestring('%s:%s' % (authUser, authPass)).replace('\n', '')
pkg_name = os.path.basename(self.env["pkg_path"])
prod_name = self.env["prod_name"]
version = self.env["version"]
patchoo_sgroup = ("update"+prod_name+"-"+version)
patchoo_policy = ("update"+prod_name+"-"+version)
patchoo_category = ("0patchoo-dev")
patchoo_trigger = ("update-dev")
timestamp = datetime.datetime.now().strftime("%A, %d. %B %Y %I:%M%p")
pkg_notes = (prod_name+" "+version+" Imported by AutoPKG on "+timestamp)
pkg_info = (prod_name+" "+version)
# pre-set 'changed/added/updated' output checks to False
self.env["jss_repo_changed"] = False
self.env["jss_category_added"] = False
self.env["jss_smartgroup_added"] = False
self.env["jss_smartgroup_updated"] = False
self.env["jss_staticgroup_added"] = False
self.env["jss_staticgroup_updated"] = False
self.env["jss_policy_added"] = False
self.env["jss_policy_updated"] = False
#
# check for category if var set
#
if self.env.get("category"):
category_name = self.env.get("category")
if not category_name == "*LEAVE_OUT*":
item_to_check = category_name
apiUrl = "category"
proceed_list = self.checkItem(repoUrl, apiUrl, item_to_check, base64string)
template_string = """<?xml version="1.0" encoding="UTF-8"?><category><name>%CAT_NAME%</name></category>"""
if "proceed" not in proceed_list:
replace_dict = {"%CAT_NAME%" : category_name}
cat_id = self.createObject(repoUrl, apiUrl, replace_dict, template_string, base64string)
self.env["jss_category_added"] = True
else:
self.output("Category already exists according to JSS, moving on")
else:
self.output("Category creation for the pkg not desired, moving on")
#
# check for package by pkg_name for both API POST
# and if exists at repo_path
#
template_string = """<?xml version="1.0" encoding="UTF-8"?><package><name>%PKG_NAME%</name><category>%PAT_CAT%</category><filename>%PKG_NAME%</filename><info>%PKG_INFO%</info><notes>%PKG_NOTE%</notes><priority>10</priority><reboot_required>false</reboot_required><fill_user_template>false</fill_user_template><fill_existing_users>false</fill_existing_users><boot_volume_required>false</boot_volume_required><allow_uninstalled>false</allow_uninstalled><os_requirements/><required_processor>None</required_processor><switch_with_package>Do Not Install</switch_with_package><install_if_reported_available>false</install_if_reported_available><reinstall_option>Do Not Reinstall</reinstall_option><triggering_files/><send_notification>false</send_notification></package>"""
replace_dict = {"%PKG_NAME%" : pkg_name, "%PROD_NAME%" : prod_name, "%CAT_NAME%" : category_name, "%PAT_CAT%" : patchoo_category, "%PKG_INFO%" : pkg_info, "%PKG_NOTE%" : pkg_notes}
else:
template_string = """<?xml version="1.0" encoding="UTF-8"?><package><name>%PKG_NAME%</name><filename>%PKG_NAME%</filename><info/><notes/><priority>10</priority><reboot_required>false</reboot_required><fill_user_template>false</fill_user_template><fill_existing_users>false</fill_existing_users><boot_volume_required>false</boot_volume_required><allow_uninstalled>false</allow_uninstalled><os_requirements/><required_processor>None</required_processor><switch_with_package>Do Not Install</switch_with_package><install_if_reported_available>false</install_if_reported_available><reinstall_option>Do Not Reinstall</reinstall_option><triggering_files/><send_notification>false</send_notification></package>"""
replace_dict = {"%PKG_NAME%" : pkg_name, "%PROD_NAME%" : prod_name}
apiUrl = "package"
item_to_check = pkg_name
proceed_list = self.checkItem(repoUrl, apiUrl, item_to_check, base64string)
if "proceed" not in proceed_list:
pkg_id = self.createObject(repoUrl, apiUrl, replace_dict, template_string, base64string)
else:
pkg_id = str(proceed_list[1])
self.output("Pkg already exists according to JSS, moving on")
source_item = self.env["pkg_path"]
dest_item = (self.env["JSS_REPO"] + "/" + pkg_name)
if os.path.exists(dest_item):
self.output("Pkg already exists at %s, moving on" % dest_item)
else:
try:
shutil.copyfile(source_item, dest_item)
self.output("Copied %s to %s" % (source_item, dest_item))
# set output variables
self.env["jss_repo_changed"] = True
except BaseException, err:
raise ProcessorError(
"Can't copy %s to %s: %s" % (source_item, dest_item, err))
#
# check for smartGroup if var set
#
if patchoo_sgroup :
smart_group_name = patchoo_sgroup
if not smart_group_name == "*LEAVE_OUT*":
item_to_check = smart_group_name
apiUrl = "computergroup"
proceed_list = self.checkItem(repoUrl, apiUrl, item_to_check, base64string)
template_string = """<?xml version="1.0" encoding="UTF-8"?><computer_group><name>%PAT_GRP%</name><is_smart>true</is_smart><criteria><size>4</size><criterion><name>Cached Packages</name><priority>0</priority><and_or>and</and_or><search_type>does not have</search_type><value>%PROD_NAME%-%version%.pkg</value></criterion><criterion><name>Packages Installed By Casper</name><priority>1</priority><and_or>and</and_or><search_type>does not have</search_type><value>%PROD_NAME%-%version%.pkg</value></criterion><criterion><name>Application Title</name><priority>2</priority><and_or>and</and_or><search_type>has</search_type><value>%PROD_NAME%.app</value></criterion><criterion><name>Application Version</name><priority>3</priority><and_or>and</and_or><search_type>is not</search_type><value>%version%</value></criterion></criteria><computers><size>0</size></computers></computer_group>"""
replace_dict = {"%PROD_NAME%" : prod_name, "%version%" : version, "%PAT_GRP%" : patchoo_sgroup}
if "proceed" not in proceed_list:
grp_id = self.createObject(repoUrl, apiUrl, replace_dict, template_string, base64string)
self.env["jss_smartgroup_added"] = True
else:
grp_id = str(proceed_list[1])
pkg_ver = self.checkSpecificItem(repoUrl, apiUrl, grp_id, base64string)
if pkg_ver != version:
self.putUpdate(repoUrl, apiUrl, grp_id, replace_dict, template_string, base64string)
self.env["jss_smartgroup_updated"] = True
else:
self.output("Smart group creation not desired, moving on")
#
# check for arbitraryGroupID if var set
#
if self.env.get("arb_group_name"):
static_group_name = self.env.get("arb_group_name")
if not static_group_name == "*LEAVE_OUT*":
item_to_check = static_group_name
apiUrl = "computergroup"
proceed_list = self.checkItem(repoUrl, apiUrl, item_to_check, base64string)
if "proceed" not in proceed_list:
raise ProcessorError("Static group not present in JSS.")
else:
grp_id = str(proceed_list[1])
else:
self.output("Static group check/creation not desired, moving on")
#
# check for policy if var set
#
if patchoo_policy :
item_to_check = patchoo_policy
if not item_to_check == "*LEAVE_OUT*":
apiUrl = "policy"
proceed_list = self.checkItem(repoUrl, apiUrl, item_to_check, base64string)
template_string = """<?xml version="1.0" encoding="UTF-8"?><policy><general><name>%PAT_POLICY%</name><enabled>true</enabled><trigger>EVENT</trigger><trigger_other>%PAT_TRIG%</trigger_other><frequency>Ongoing</frequency><category><name>0patchoo-dev</name></category></general><scope><computer_groups><computer_group><id>%grp_id%</id></computer_group></computer_groups></scope><package_configuration><packages><size>1</size><package><id>%pkg_id%</id><action>Cache</action></package></packages></package_configuration><scripts><size>1</size><script><name>0patchoo.sh</name><priority>After</priority><parameter4>--cache</parameter4></script></scripts></policy>"""
self.output("Current grp_id is %s, pkg_id is %s" % (grp_id, pkg_id))
replace_dict = {"%PROD_NAME%" : prod_name, "%grp_id%" : grp_id, "%pkg_id%" : pkg_id, "%version%" : version, "%PAT_POLICY%" : patchoo_policy, "%PAT_TRIG%" : patchoo_trigger}
if "proceed" not in proceed_list:
self.createObject(repoUrl, apiUrl, replace_dict, template_string, base64string)
self.env["jss_policy_added"] = True
else:
policy_id = str(proceed_list[1])
if self.env.get("smart_group"):
group_name = smart_group_name
else:
group_name = static_group_name
found_list = self.checkSpecificItem(repoUrl, apiUrl, policy_id, base64string, [group_name, pkg_name])
if len(found_list) != 2:
self.putUpdate(repoUrl, apiUrl, policy_id, replace_dict, template_string, base64string)
self.env["jss_policy_updated"] = True
elif grp_id != found_list[0] or pkg_id != found_list[1]:
self.output("current pkg_id is %s, found is %s current group_id is %s, found is %s" % (pkg_id, found_list[1], grp_id, found_list[0]))
self.putUpdate(repoUrl, apiUrl, policy_id, replace_dict, template_string, base64string)
self.env["jss_policy_updated"] = True
else:
self.output("Policy creation not desired, moving on")
if __name__ == "__main__":
processor = JSSImporter()
processor.execute_shell()