Skip to content

Commit fa164de

Browse files
Copilotjgarcesres
andcommitted
Implement directory exclusion and EA script processing functions
Co-authored-by: jgarcesres <5065761+jgarcesres@users.noreply.github.com>
1 parent 2b75e4b commit fa164de

3 files changed

Lines changed: 221 additions & 35 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
action/test
22
.env
33
.DS_Store
4-
*.code-workspace
4+
*.code-workspace
5+
__pycache__/
6+
*.pyc

__pycache__/action.cpython-312.pyc

-21.8 KB
Binary file not shown.

action.py

Lines changed: 218 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -151,45 +151,103 @@ def find_jamf_script(url, token, script_name, page = 0):
151151
raise Exception("failed to find the script, please investigate!")
152152

153153

154-
#function to find a EA script using the filename as the script name
154+
#function to get all extension attributes using the new API
155155
@logger.catch
156-
def find_ea_script(ea_name):
157-
ea_script = requests.get(url = f"{url}/JSSResource/computerextensionattributes/name/{ea_name}", auth=(username,password))
158-
if ea_script.status_code == requests.codes.ok:
159-
return ea_script.json()['computer_extension_attribute']
160-
elif ea_script.status_code == requests.codes.not_found:
161-
logger.warning(f"Found no script with name: {ea_name}")
162-
return None
156+
def get_all_jamf_extension_attributes(url, token, eas = [], page = 0):
157+
header = {"Authorization": f"Bearer {token}"}
158+
page_size=50
159+
params = {"page": page, "page-size": page_size, "sort": "name:asc"}
160+
ea_list = requests.get(url=f"{url}/uapi/v1/computer-extension-attributes", headers=header, params=params)
161+
if ea_list.status_code == requests.codes.ok:
162+
ea_list = ea_list.json()
163+
logger.info(f"we got {len(ea_list['results'])+page} of {ea_list['totalCount']} EA results")
164+
page+=1
165+
if (page*page_size) < ea_list['totalCount']:
166+
logger.info("seems there's more EAs to grab")
167+
eas.extend(ea_list['results'])
168+
return get_all_jamf_extension_attributes(url, token, eas, page)
169+
else:
170+
logger.info("reached the end of our EA search")
171+
eas.extend(ea_list['results'])
172+
logger.success(f"retrieved {len(eas)} total extension attributes")
173+
return eas
163174
else:
164-
logger.error("encountered an error retriving the extension attribute, stopping")
165-
logger.error(ea_script.text)
166-
raise Exception("encountered an error retriving the extension attribute, stopping")
175+
logger.error(f"status code: {ea_list.status_code}")
176+
logger.error("error retrieving extension attribute list")
177+
logger.error(ea_list.text)
178+
raise Exception("error retrieving extension attribute list")
167179

168180

169-
#function to create EA script
181+
#function to find a specific extension attribute by name using the new API
170182
@logger.catch
171-
def create_ea_script(payload, id):
172-
headers = {"Accept": "text/xml", "Content-Type": "text/xml"}
173-
ea_script = requests.post(url = f"{url}/JSSResource/computerextensionattributes/id/{id}", json=payload, auth=(username,password))
174-
if ea_script.status_code == requests.codes.ok:
175-
return "success"
183+
def find_jamf_extension_attribute(url, token, ea_name, page = 0):
184+
header = {"Authorization": f"Bearer {token}"}
185+
page_size=50
186+
params = {"page": page, "page-size": page_size, "sort": "name:asc"}
187+
ea_list = requests.get(url=f"{url}/uapi/v1/computer-extension-attributes", headers=header, params=params)
188+
if ea_list.status_code == requests.codes.ok:
189+
ea_list = ea_list.json()
190+
logger.info(f"we have searched {len(ea_list['results'])+page} of {ea_list['totalCount']} EA results")
191+
ea_search = jmespath.search(f"results[?name == '{ea_name}']", ea_list)
192+
if len(ea_search) == 1:
193+
logger.info('found the extension attribute, returning it')
194+
return ea_search[0]
195+
elif len(ea_search) == 0 and (page*page_size) < ea_list['totalCount']:
196+
logger.info("couldn't find the EA in this page, seems there's more to look through")
197+
return find_jamf_extension_attribute(url, token, ea_name, page+1)
198+
else:
199+
logger.info(f"did not find any extension attribute named {ea_name}")
200+
return "n/a"
201+
else:
202+
logger.error(f"status code: {ea_list.status_code}")
203+
logger.error("error retrieving extension attribute list")
204+
logger.error(ea_list.text)
205+
raise Exception("failed to find the extension attribute, please investigate!")
206+
207+
208+
#function to create a new extension attribute using the new API
209+
@logger.catch
210+
def create_jamf_extension_attribute(url, token, payload):
211+
header = {"Authorization": f"Bearer {token}"}
212+
ea_request = requests.post(url=f"{url}/uapi/v1/computer-extension-attributes", headers=header, json=payload)
213+
if ea_request.status_code == requests.codes.created:
214+
logger.success("extension attribute created")
215+
return True
216+
else:
217+
logger.warning("failed to create the extension attribute")
218+
logger.debug(f"status code for create: {ea_request.status_code}")
219+
logger.warning(ea_request.text)
220+
return False
221+
222+
223+
#function to update an existing extension attribute using the new API
224+
@logger.catch
225+
def update_jamf_extension_attribute(url, token, payload):
226+
header = {"Authorization": f"Bearer {token}"}
227+
ea_request = requests.put(url=f"{url}/uapi/v1/computer-extension-attributes/{payload['id']}", headers=header, json=payload)
228+
if ea_request.status_code in [requests.codes.accepted, requests.codes.ok]:
229+
logger.success("extension attribute was updated successfully")
230+
return True
176231
else:
177-
logger.error("encountered an error creating the extension attribute, stopping")
178-
logger.error(ea_script.text)
179-
raise Exception("encountered an error creating the extension attribute, stopping")
232+
logger.warning("failed to update the extension attribute")
233+
logger.debug(f"status code for put: {ea_request.status_code}")
234+
logger.warning(ea_request.text)
235+
return False
180236

181237

182-
#function to update existin EA script
238+
#function to delete an extension attribute using the new API
183239
@logger.catch
184-
def update_ea_script(payload, id):
185-
headers = {"Accept": "text/xml", "Content-Type": "text/xml"}
186-
ea_script = requests.put(url=f"{url}/JSSResource/computerextensionattributes/id/{id}", json=payload, auth=(username,password))
187-
if ea_script.status_code == requests.codes.ok:
188-
return "success"
240+
def delete_jamf_extension_attribute(url, token, id):
241+
header = {"Authorization": f"Bearer {token}"}
242+
ea_request = requests.delete(url=f"{url}/uapi/v1/computer-extension-attributes/{id}", headers=header)
243+
if ea_request.status_code in [requests.codes.ok, requests.codes.accepted, requests.codes.no_content]:
244+
logger.success("extension attribute was deleted successfully")
245+
return True
189246
else:
190-
logger.error("encountered an error creating the extension attribute, stopping")
191-
logger.error(ea_script.text)
192-
raise Exception("encountered an error creating the extension attribute, stopping")
247+
logger.warning("failed to delete the extension attribute")
248+
logger.debug(f"status code for delete: {ea_request.status_code}")
249+
logger.warning(ea_request.text)
250+
return False
193251

194252

195253
#function to compare sripts and see if they have changed. If they haven't, no need to update it
@@ -209,11 +267,20 @@ def compare_scripts(new, old):
209267

210268
#retrieves list of files given a folder path and the list of valid file extensions to look for
211269
@logger.catch
212-
def find_local_scripts(script_dir, script_extensions):
270+
def find_local_scripts(script_dir, script_extensions, exclude_dir=None):
213271
script_list = []
214272
logger.info(f"searching for files ending in {script_extensions} in {script_dir}")
215273
for file_type in script_extensions:
216274
script_list.extend(glob.glob(f"{script_dir}/**/*.{file_type}", recursive = True))
275+
276+
# Filter out files from the exclude directory if specified
277+
if exclude_dir and exclude_dir != 'false':
278+
original_count = len(script_list)
279+
script_list = [script for script in script_list if not script.startswith(exclude_dir)]
280+
excluded_count = original_count - len(script_list)
281+
if excluded_count > 0:
282+
logger.info(f"excluded {excluded_count} files from EA script directory: {exclude_dir}")
283+
217284
logger.info("found these: ", script_dir)
218285
logger.info(script_list)
219286
return script_list
@@ -226,14 +293,14 @@ def get_script_name(script_path):
226293

227294

228295
@logger.catch
229-
def push_scripts():
296+
def push_scripts(exclude_ea_dir=None):
230297
#grab the token from jamf
231298
logger.info('grabing the token from jamf')
232299
token = get_jamf_token(url,auth_type, username, password)
233300
logger.info('checking the list of local scripts to upload or create')
234301
scripts = {}
235302
#this retrives the full path of the scripts we're trying to sync from github
236-
scripts['github'] = find_local_scripts(script_dir, script_extensions)
303+
scripts['github'] = find_local_scripts(script_dir, script_extensions, exclude_ea_dir)
237304
#I need to simplify this array down to the just the name of the script, stripping out the path.
238305
scripts['github_simple_name'] = []
239306
for script in scripts['github']:
@@ -304,8 +371,121 @@ def push_scripts():
304371
logger.success("finished with the scripts")
305372

306373

374+
@logger.catch
307375
def push_ea_scripts():
308-
return ""
376+
if ea_script_dir == 'false':
377+
logger.warning("EA script directory not set, skipping EA script processing")
378+
return
379+
380+
logger.info('starting EA script processing')
381+
#grab the token from jamf
382+
logger.info('grabbing the token from jamf for EA scripts')
383+
token = get_jamf_token(url, auth_type, username, password)
384+
logger.info('checking the list of local EA scripts to upload or create')
385+
ea_scripts = {}
386+
387+
#this retrieves the full path of the EA scripts we're trying to sync from github
388+
ea_scripts['github'] = find_local_scripts(ea_script_dir, script_extensions)
389+
390+
#I need to simplify this array down to just the name of the script, stripping out the path.
391+
ea_scripts['github_simple_name'] = []
392+
for ea_script in ea_scripts['github']:
393+
ea_scripts['github_simple_name'].append(get_script_name(ea_script).lower())
394+
395+
logger.info('double-checking for duplicate EA script names')
396+
for count, ea_script in enumerate(ea_scripts['github_simple_name']):
397+
if ea_scripts['github_simple_name'].count(ea_script) >= 2:
398+
logger.error(f"the EA script name {ea_script} is duplicated {ea_scripts['github_simple_name'].count(ea_script)} times, please give it a unique name")
399+
sys.exit(1)
400+
401+
#continue if no dupes are found
402+
logger.success("nice, no duplicate EA script names, we can continue")
403+
logger.info('now checking jamf for its list of extension attributes')
404+
ea_scripts['jamf'] = get_all_jamf_extension_attributes(url, token)
405+
logger.info("setting all EA names to lower case to avoid false positives in our search.")
406+
logger.info("worry not, this won't affect the actual naming :)")
407+
408+
#save the EA names all in lower_case
409+
for ea_script in ea_scripts['jamf']:
410+
ea_script['lower_case_name'] = ea_script['name'].lower()
411+
412+
#make a copy of the jamf EAs, we'll use this to determine which to delete later on
413+
ea_scripts['to_delete'] = ea_scripts['jamf']
414+
415+
logger.info("processing each EA script now")
416+
for count, ea_script in enumerate(ea_scripts['github']):
417+
logger.info("----------------------")
418+
logger.info(f"EA script {count+1} of {len(ea_scripts['github'])}")
419+
logger.info(f"path of the EA script: {ea_script}")
420+
ea_script_name = get_script_name(ea_script)
421+
422+
if enable_prefix == "false":
423+
#don't use the prefix
424+
logger.info(f"EA script name is: {ea_script_name}")
425+
else:
426+
#use the branch name as prefix
427+
prefix = branch.split('/')[-1]
428+
ea_script_name = f"{prefix}_{ea_script_name}"
429+
logger.info(f"the new EA script name: {ea_script_name}")
430+
431+
#check to see if the EA script name exists in jamf
432+
logger.info(f"now let's see if {ea_script_name} exists in jamf already")
433+
ea_search = jmespath.search(f"[?lower_case_name == '{ea_script_name.lower()}']", ea_scripts['jamf'])
434+
435+
if len(ea_search) == 0:
436+
logger.info("it doesn't exist, lets create it")
437+
#it doesn't exist, we can create it
438+
with open(ea_script, 'r') as upload_ea_script:
439+
ea_script_content = upload_ea_script.read()
440+
payload = {
441+
"name": ea_script_name,
442+
"enabled": True,
443+
"description": "Extension attribute script created via git2jamf",
444+
"dataType": "String",
445+
"inputType": {
446+
"type": "Script",
447+
"platform": "Mac",
448+
"script": ea_script_content
449+
},
450+
"inventoryDisplay": "General",
451+
"reconDisplay": "Extension Attributes"
452+
}
453+
create_jamf_extension_attribute(url, token, payload)
454+
455+
elif len(ea_search) == 1:
456+
jamf_ea = ea_search.pop()
457+
del jamf_ea['lower_case_name']
458+
ea_scripts['to_delete'].remove(jamf_ea)
459+
logger.info("it does exist, lets compare them")
460+
#it does exist, lets see if it has changed
461+
with open(ea_script, 'r') as upload_ea_script:
462+
ea_script_content = upload_ea_script.read()
463+
# Check if the script content is different
464+
current_script = ""
465+
if 'inputType' in jamf_ea and 'script' in jamf_ea['inputType']:
466+
current_script = jamf_ea['inputType']['script']
467+
468+
if not compare_scripts(ea_script_content, current_script):
469+
logger.info("the local EA version is different than the one in jamf, updating jamf")
470+
#the hash of the scripts is not the same, so we'll update it
471+
jamf_ea['inputType']['script'] = ea_script_content
472+
update_jamf_extension_attribute(url, token, jamf_ea)
473+
else:
474+
logger.info("we're skipping this EA script.")
475+
476+
if delete == 'true':
477+
logger.warning(f"we have {len(ea_scripts['to_delete'])} extension attributes left to delete")
478+
for ea_script in ea_scripts['to_delete']:
479+
# Only delete extension attributes that have scripts (not other types like text input, etc.)
480+
if 'inputType' in ea_script and ea_script['inputType'].get('type', '').lower() == 'script':
481+
logger.info(f"attempting to delete extension attribute {ea_script['name']} in jamf")
482+
delete_jamf_extension_attribute(url, token, ea_script['id'])
483+
else:
484+
logger.info(f"skipping deletion of non-script extension attribute: {ea_script['name']}")
485+
486+
logger.info("expiring the token so it can't be used further")
487+
invalidate_jamf_token(url, token)
488+
logger.success("finished with the EA scripts")
309489

310490

311491
#run this thing
@@ -325,6 +505,9 @@ def push_ea_scripts():
325505
workspace_dir = os.getenv('GITHUB_WORKSPACE')
326506
if script_dir != workspace_dir:
327507
script_dir = f"{workspace_dir}/{script_dir}"
508+
# Process EA script directory path if it's set
509+
if ea_script_dir != 'false' and ea_script_dir != workspace_dir:
510+
ea_script_dir = f"{workspace_dir}/{ea_script_dir}"
328511
enable_prefix = os.getenv('INPUT_PREFIX')
329512
branch = os.getenv('GITHUB_REF')
330513
script_extensions = os.getenv('INPUT_SCRIPT_EXTENSIONS')
@@ -333,6 +516,7 @@ def push_ea_scripts():
333516
logger.info(f"url is: {url}")
334517
logger.info(f"workspace dir is: {workspace_dir}")
335518
logger.info(f"script_dir is: {script_dir}")
519+
logger.info(f"ea_script_dir is: {ea_script_dir}")
336520
logger.info(f"branch is set to: {branch}")
337521
logger.info(f"script_deletion is: {delete}")
338522
logger.info(f"scripts_extensions are: {script_extensions}")
@@ -341,7 +525,7 @@ def push_ea_scripts():
341525
else:
342526
logger.warning(f"prefix enabled, using: {branch.split('/')[-1]}")
343527
#run the block to push the "normal" scripts to jamf
344-
push_scripts()
528+
push_scripts(ea_script_dir if ea_script_dir != 'false' else None)
345529
#check to see if we have an EA scripts to push over
346530
if ea_script_dir != 'false':
347531
logger.info("we have some EA scripts to process")

0 commit comments

Comments
 (0)