Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
44e7fff
Adding config file
rustymyers Oct 18, 2023
7891597
Adding Requirements, configparser to download.py
rustymyers Oct 18, 2023
af81b87
Adding token auth, configparser, and export_path
rustymyers Oct 18, 2023
8029a4c
Adding token auth, configparser, and sync_path
rustymyers Oct 18, 2023
01cf00f
Update sync.py
rustymyers Oct 21, 2023
d51f998
Update sync.py
rustymyers Oct 21, 2023
08ba3e3
Update sync.py
rustymyers Oct 21, 2023
b651837
Update README.md
rustymyers Oct 21, 2023
0b98075
Update download.py
rustymyers Oct 21, 2023
2dcbe32
Merge branch 'master' of https://github.com/badstreff/git2jss
rustymyers Oct 21, 2023
5eb0183
Update sync.py
rustymyers Oct 21, 2023
e586502
Update download.py
rustymyers Oct 21, 2023
d78edb4
Black formatter
rustymyers Oct 23, 2023
4b38a65
Updated download.py using Black auto formatter
rustymyers Oct 24, 2023
a56f956
Removnig missing config file message
rustymyers Oct 24, 2023
c8ed1a2
Fixing "is not" testing a litereal to "!="
rustymyers Oct 24, 2023
74670ef
Updating readme badge and bullet list
rustymyers Oct 30, 2023
bdec1ce
Fixing duplicate imports and functions, removing token when completed
rustymyers Oct 30, 2023
deb0401
Removing except and updating invalidate token call
rustymyers Oct 30, 2023
a5b2188
Adding timeout for requests
rustymyers Oct 30, 2023
39bf70d
Adding exception type for username
rustymyers Oct 31, 2023
7bf580d
Adding configparser.NoOptionError exception
rustymyers Nov 1, 2023
91a1edc
Merge branch 'master' of https://github.com/badstreff/git2jss
rustymyers Nov 1, 2023
f8f49d3
Revert "Merge branch 'master' of https://github.com/badstreff/git2jss"
rustymyers Nov 1, 2023
5df38bc
Updating codacy to check tools folder
rustymyers Nov 1, 2023
3ae4671
defusedxml and timeouts for download.sh
rustymyers Nov 1, 2023
6bb56e9
Updating Errors and CI tests
rustymyers Nov 1, 2023
2749dc5
Adding exception type to download, compressing configs for verify
rustymyers Nov 1, 2023
1306f48
Update verifyEA.py
rustymyers Nov 1, 2023
06d19b9
Update validate xml to work on folders except exclude. Removing pass …
rustymyers Nov 1, 2023
baa6ea8
Fix config style
rustymyers Aug 14, 2026
e7b96c0
Update README.md
rustymyers Aug 14, 2026
a0c71c9
Update verifyEA.py text formatting
rustymyers Aug 15, 2026
8089943
Additional formatting changes
rustymyers Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .codacy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
exclude_paths:
- 'aiojss/**'
- 'tools/**'
- 'scripts/**'
- 'extension_attributes/**'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ extension_attributes/*
!extension_attributes/Last User/
scripts/*
!scripts/Install Software Updates/
!scripts/templates
!extension_attributes/templates
tools/ci_tests/computers.json
20 changes: 10 additions & 10 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Expand Down
54 changes: 27 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
# git2jss
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d9d618c32e93436ea67102fd3d3f5b21)](https://www.codacy.com/app/adam-furbee/git2jss?utm_source=github.com&utm_medium=referral&utm_content=BadStreff/git2jss&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/c49c0bd6a88d4f1e8c6808455171178e)](https://app.codacy.com/gh/rustymyers/git2jss/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)

A fast asynchronous python library for syncing your scripts in git with your JSS easily. This allows admins to keep their script in a version control system for easy updating rather than googling and copy-pasting from resources that they find online.

## Getting Started
1. Fork the Project
2. Install [Python version 3.6](https://www.python.org/downloads/) or higher. (this is because of the async requirements)
3. Run `python3.6 -m pip install -r requirements.txt` to install required modules
4. Run `./tools/download.py --url https://your.jss.url:8443 --username api_user` to download all scripts and extension attributes to the repository
5. Run `./sync.py --url https://your.jss.url:8443 --username api_user` to sync all scripts back to your JSS
1. Fork the Project
2. Install [Python version 3.6](https://www.python.org/downloads/) or higher. (this is because of the async requirements)
3. Run `python3.6 -m pip install -r requirements.txt` to install required modules
4. Run `./tools/download.py --url https://your.jss.url:8443 --username api_user` to download all scripts and extension attributes to the repository
5. Run `./sync.py --url https://your.jss.url:8443 --username api_user` to sync all scripts back to your JSS

Optional flags for `download.py`:

- `--password` for CI/CD (Will prompt for password if not set)
- `--do_not_verify_ssl` to skip ssl verification
- `--overwrite` to overwrite all scripts and extension attributes
- `--password` for CI/CD (Will prompt for password if not set)
- `--do_not_verify_ssl` to skip ssl verification
- `--overwrite` to overwrite all scripts and extension attributes

Optional flags for `sync.py`:

- `--password` for CI/CD (Will prompt for password if not set)
- `--do_not_verify_ssl` to skip ssl verification
- `--overwrite` to overwrite all scripts and extension attributes
- `--limit` to limit max connections (default=25)
- `--timeout` to limit max connections (default=60)
- `--verbose` to add additional logging
- `--update_all` to upload all resources in `./extension_attributes` and `./scripts`
- `--jenkins` to write a Jenkins file:`jenkins.properties` with `$scripts` and `$eas` and compare `$GIT_PREVIOUS_COMMIT` with `$GIT_COMMIT`

### [ConfigParser](https://docs.python.org/3/library/configparser.html) (Optional):

A config file can be created in the project root or the users home folder. When a config file exists, the script will not promt for a password.

A jamfapi.cfg file can provide the following variables:

- username
- password
- url
- `--password` for CI/CD (Will prompt for password if not set)
- `--do_not_verify_ssl` to skip ssl verification
- `--overwrite` to overwrite all scripts and extension attributes
- `--limit` to limit max connections (default=25)
- `--timeout` to limit max connections (default=60)
- `--verbose` to add additional logging
- `--update_all` to upload all resources in `./extension_attributes` and `./scripts`
- `--jenkins` to write a Jenkins file:`jenkins.properties` with `$scripts` and `$eas` and compare `$GIT_PREVIOUS_COMMIT` with `$GIT_COMMIT`
### [ConfigParser](https://docs.python.org/3/library/configparser.html) (Optional):
A config file can be created in the project root or the users home folder. When a config file exists, the script will not promt for a password.
A jamfapi.cfg file can provide the following variables:
- username
- password
- url

### Prerequisites
git2jss requires [Python 3.6](https://www.python.org/downloads/) and the python modules listed in `requirements.txt`
Expand Down
7 changes: 7 additions & 0 deletions extension_attributes/templates/Last User/ea.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
lastUser=`defaults read /Library/Preferences/com.apple.loginwindow lastUserName`

if [ $lastUser == "" ]; then
echo "<result>No logins</result>"
else
echo "<result>$lastUser</result>"
13 changes: 13 additions & 0 deletions extension_attributes/templates/Last User/ea.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" ?>
<computer_extension_attribute>
<name>Last User</name>
<enabled>true</enabled>
<description>This attribute displays the last user to log in. This attribute applies to both Mac and Windows.</description>
<data_type>String</data_type>
<input_type>
<type>script</type>
<platform>Mac</platform>
<script/>
</input_type>
<inventory_display>General</inventory_display>
</computer_extension_attribute>
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ cchardet
aiodns
uvloop
requests
configparser
configparser
defusedxml
8 changes: 8 additions & 0 deletions scripts/templates/Install Software Updates/script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

# Install all software updates
# Example for git2jss

softwareupdate -i -a

exit 0
11 changes: 11 additions & 0 deletions scripts/templates/Install Software Updates/script.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<script>
<name>Install Software Updates</name>
<category>No category assigned</category>
<info/>
<notes>Created by Brad Schmidt</notes>
<priority>Before</priority>
<parameters/>
<os_requirements/>
<script_contents/>
</script>
82 changes: 31 additions & 51 deletions sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
from os.path import dirname, join, realpath
import sys
import xml.etree.ElementTree as ET
import getpass
import argparse
import logging
Expand All @@ -14,8 +13,7 @@
import uvloop
import configparser
import requests
import configparser
import requests
from defusedxml import ElementTree as eTree

logging.basicConfig(
level=logging.DEBUG,
Expand All @@ -40,7 +38,7 @@ def get_uapi_token():
fetches api token
"""
jamf_test_url = url + "/api/v1/auth/token"
response = requests.post(url=jamf_test_url, auth=(username, password))
response = requests.post(url=jamf_test_url, auth=(username, password), timeout=5)
response_json = response.json()
return response_json["token"]

Expand All @@ -51,27 +49,7 @@ def invalidate_uapi_token(uapi_token):
"""
jamf_test_url = url + "/api/v1/auth/invalidate-token"
headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token}
_ = requests.post(url=jamf_test_url, headers=headers)


# https://github.com/lazymutt/Jamf-Pro-API-Sampler/blob/5f8efa92911271248f527e70bd682db79bc600f2/jamf_duplicate_detection.py#L99
def get_uapi_token():
"""
fetches api token
"""
jamf_test_url = url + "/api/v1/auth/token"
response = requests.post(url=jamf_test_url, auth=(username, password))
response_json = response.json()
return response_json["token"]


def invalidate_uapi_token(uapi_token):
"""
invalidates api token
"""
jamf_test_url = url + "/api/v1/auth/invalidate-token"
headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token}
_ = requests.post(url=jamf_test_url, headers=headers)
_ = requests.post(url=jamf_test_url, headers=headers, timeout=5)


def check_for_changes():
Expand Down Expand Up @@ -208,7 +186,7 @@ async def upload_extension_attribute(session, url, user, passwd, ext_attr, semap
if has_script and data:
template.find("input_type/script").text = data
if args.verbose:
print(ET.tostring(template))
print(eTree.tostring(template))
print("response status initial get: ", resp.status)
if resp.status == 200:
put_url = (
Expand All @@ -217,12 +195,12 @@ async def upload_extension_attribute(session, url, user, passwd, ext_attr, semap
+ template.find("name").text
)
resp = await session.put(
put_url, data=ET.tostring(template), headers=headers
put_url, data=eTree.tostring(template), headers=headers
)
else:
post_url = url + "/JSSResource/computerextensionattributes/id/0"
resp = await session.post(
post_url, data=ET.tostring(template), headers=headers
post_url, data=eTree.tostring(template), headers=headers
)
if args.verbose:
print("response status: ", resp.status)
Expand All @@ -248,7 +226,7 @@ async def get_ea_template(session, url, user, passwd, ext_attr):
with open(
join(sync_path, "extension_attributes", ext_attr, xml_file[0]), "r"
) as file:
template = ET.fromstring(file.read())
template = eTree.parse(file.read())
except IndexError:
with async_timeout.timeout(args.timeout):
headers = {
Expand All @@ -268,23 +246,25 @@ async def get_ea_template(session, url, user, passwd, ext_attr):
+ ext_attr,
headers=headers,
) as response:
template = ET.fromstring(await response.text())
template = eTree.fromstring(await response.text())
else:
template = ET.parse(join(sync_path, "templates/ea.xml")).getroot()
template = eTree.parse(
join(sync_path, "templates/ea.xml")
).getroot()
# name is mandatory, so we use the foldername if nothing is set in
# a template
if args.verbose:
print(ET.tostring(template))
print(eTree.tostring(template))
if template.find("category") and template.find("category").text not in CATEGORIES:
ET.SubElement(template, "category").text = "None"
eTree.SubElement(template, "category").text = "None"
if args.verbose:
c = template.find("category").text
print(
f"""WARNING: Unable to find category {c} in the JSS,
setting to None"""
)
if template.find("name") is None:
ET.SubElement(template, "name").text = ext_attr
eTree.SubElement(template, "name").text = ext_attr
elif not template.find("name").text or template.find("name").text is None:
template.find("name").text = ext_attr
return template
Expand Down Expand Up @@ -344,12 +324,12 @@ async def upload_script(session, url, user, passwd, script, semaphore):
url + "/JSSResource/scripts/name/" + template.find("name").text
)
resp = await session.put(
put_url, data=ET.tostring(template), headers=headers
put_url, data=eTree.tostring(template), headers=headers
)
else:
post_url = url + "/JSSResource/scripts/id/0"
resp = await session.post(
post_url, data=ET.tostring(template), headers=headers
post_url, data=eTree.tostring(template), headers=headers
)
if resp.status in (201, 200):
print("Uploaded script: %s" % template.find("name").text)
Expand All @@ -369,7 +349,7 @@ async def get_script_template(session, url, user, passwd, script):
]
try:
with open(join(sync_path, "scripts", script, xml_file[0]), "r") as file:
template = ET.fromstring(file.read())
template = eTree.fromstring(file.read())
except IndexError:
with async_timeout.timeout(args.timeout):
headers = {
Expand All @@ -384,14 +364,14 @@ async def get_script_template(session, url, user, passwd, script):
async with session.get(
url + "/JSSResource/scripts/name/" + script, headers=headers
) as response:
template = ET.fromstring(await response.text())
template = eTree.fromstring(await response.text())
else:
template = ET.parse(
template = eTree.parse(
join(sync_path, "templates/script.xml")
).getroot()
# name is mandatory, so we use the filename if nothing is set in a template
if args.verbose:
print(ET.tostring(template))
print(eTree.tostring(template))
if (
template.find("category") is not None
and template.find("category").text not in CATEGORIES
Expand All @@ -404,7 +384,7 @@ async def get_script_template(session, url, user, passwd, script):
setting to None"""
)
if template.find("name") is None:
ET.SubElement(template, "name").text = script
eTree.SubElement(template, "name").text = script
elif not template.find("name").text or template.find("name").text is None:
template.find("name").text = script
return template
Expand All @@ -427,7 +407,7 @@ async def get_existing_categories(session, url, user, passwd, semaphore):
c.find("name").text
for c in [
e
for e in ET.fromstring(await resp.text()).findall(
for e in eTree.fromstring(await resp.text()).findall(
"category"
)
]
Expand Down Expand Up @@ -490,26 +470,23 @@ async def main():
CONFIG_FILE = config_path

if CONFIG_FILE != "":
try:
# Get config
CONFPARSER.read(CONFIG_FILE)
except:
print("Can't read config file")
# Get config
CONFPARSER.read(CONFIG_FILE)
try:
username = CONFPARSER.get("jss", "username")
except:
except configparser.NoOptionError:
print("Can't find username in configfile")
try:
password = CONFPARSER.get("jss", "password")
except:
except configparser.NoOptionError:
print("Can't find password in configfile")
try:
url = CONFPARSER.get("jss", "server")
except:
except configparser.NoOptionError:
print("Can't find url in configfile")
try:
sync_path = CONFPARSER.get("jss", "sync_path")
except:
except configparser.NoOptionError:
print("Can't find sync_path in config")

# Ask for password if not supplied via command line args
Expand Down Expand Up @@ -537,3 +514,6 @@ async def main():
warnings.simplefilter("always", ResourceWarning)

loop.run_until_complete(main())

# Remove token
invalidate_uapi_token(token)
Loading