macadmins / jamf-pro-sdk-python Goto Github PK
View Code? Open in Web Editor NEWA client library for the Jamf Pro APIs and webhooks.
Home Page: https://macadmins.github.io/jamf-pro-sdk-python/
License: MIT License
A client library for the Jamf Pro APIs and webhooks.
Home Page: https://macadmins.github.io/jamf-pro-sdk-python/
License: MIT License
In a similar fashion to other models such as classic.computer_groups
there is a need for a model for classic.categories
>>> categories = client.classic_api.list_all_categories()
>>> len(categories)
4
>>> type(categories[0])
<class 'jamf_pro_sdk.models.classic.categories.CategoriesItem'>
>>> for c in categories:
... print(c.name)
...
Enrolment
Applications
Security
Tools
I actually wanted to add a model for policies but thought I would start with a simpler model, so categories.
(This project is a stretch for my python skills. We will see how I go.)
$ git clone [email protected]:macadmins/jamf-pro-sdk-python.git
$ cd Jamf-pro-sdk-python.git
$ make test
I would expect a clean test.
pytest
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --cov-report=html:htmlcov --cov-report=term-missing
inifile: /Users/tonyw/Library/CloudStorage/Dropbox/work/jamf-pro-sdk-python/pyproject.toml
rootdir: /Users/tonyw/Library/CloudStorage/Dropbox/work/jamf-pro-sdk-python
make: *** [test] Error 4
macOS 14.0, Python 3.10.5, latest SDK
During the JNUC presentation a question was raised about SDK compatibility with different versions of Jamf Pro as APIs are added, deprecated, and removed. This proposal outlines an approach to alerting developers to when they are using an API that may not be compatible with the version of Jamf Pro they've connected a client to, or if they are using an API that has been deprecated.
Currently all API changes have to be referenced from release notes for each Jamf Pro version. The SDK does not include any mechanisms for alerting developers.
The SDK should contain metadata about the API methods that have been added that track the version they were added (minimum), if they are deprecated (a true/false flag), and the version they were removed (maximum).
The SDK would need to know the version of Jamf Pro during client init. This could be provided statically, automatically retrieved from the Pro API jamf-pro-version
endpoint, or ignored.
As a part of this proposal the default behavior on client init would include an authenticated call to GET jamf-pro-version. This behavior may not be desirable as it requires authentication which means the client is making two network calls immediately.
Passing the version string on init would bypass this. Choosing to ignore the client version through an argument would then disable ALL of the warning system.
This new option should be added to the client config.
from jamf_pro_sdk import JamfProClient, BasicAuthProvider, SessionConfig
config = SessionConfig()
config.server_version = "10.50"
client.server_version = None # <-- Default, triggers call to jamf-pro-version API
client.ignore_server_version = True # <-- Default is False, setting True will disable all Jamf Pro version warnings
client = JamfProClient(
server="jamf.my.org",
credentials=BasicAuthProvider("oscar", "j@mf1234!"),
config=config
)
If the client is instantiated with a version then all endpoints would need to emit WARNINGS when a method is being used that:
This will be handled using Python's warnings module. Each unique warning will only appear once. The loggers can be configured to capture warning messages so developers will know when a particular client is making requests that meet one of the conditions above.
import logging
import warnings
from jamf_pro_sdk import logger_quick_setup
logger_quick_setup() # Updated to include capturing warnings
# logging.captureWarnings(True)
# warnings_logger = logging.getLogger("py.warnings")
# warnings_logger.addHandler(handler)
warnings.warn("This API is deprecated as of version 10.50 and will be removed in a future.")
# 2023-10-05 10:30:04,746 py.warnings WARNING MainThread <stdin>:1: UserWarning: This API is deprecated as of Jamf Pro 10.50 and will be removed in a future version.
When a deprecation flag is set there should be included a message for which API method the developer should migrate to using (if one exists). This migration message should carry forward for case 3.
The SDK will NOT throw version exceptions as a part of this warnings system. The client will return 404 errors if the API does not exist and the warning message can be captured in logs if the developer follows guidance in the documentation.
It would be nice to have a method to pull inventory based on email address. I suspect the adoption of this project would be much higher if that existed.
When I run the send_mdm_command_preview with a UUID and a command, I get a pydantic.error_wrappers.ValidationError
from jamf_pro_sdk import JamfProClient
from jamf_pro_sdk.clients import auth
from jamf_pro_sdk.models.pro.mdm import LogOutUserCommand
client = JamfProClient(
server="*.jamfcloud.com",
credentials=auth.ApiClientCredentialsProvider(
client_id="",
client_secret=""
)
)
logout_user = client.pro_api.send_mdm_command_preview(
management_ids=[""],
command=LogOutUserCommand
)
All empty quotes have valid values.
The MDM command is sent successfully.
Traceback (most recent call last):
File "/Users/jasonausmus/repos/wayspring-jamf/auth.py", line 14, in <module>
logout_user = client.pro_api.send_mdm_command_preview(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jasonausmus/.local/share/virtualenvs/wayspring-jamf-_868w2GM/lib/python3.11/site-packages/jamf_pro_sdk/clients/pro_api/__init__.py", line 232, in send_mdm_command_preview
data = SendMdmCommand(
^^^^^^^^^^^^^^^
File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 2 validation errors for SendMdmCommand
commandData
Discriminator 'commandType' is missing in value (type=value_error.discriminated_union.missing_discriminator; discriminator_key=commandType)
commandData
value is not a valid dict (type=type_error.dict)
I'm not sure what I'm doing wrong here and I can't find any documented examples of using this method.
At the moment the delete and update methods for each endpoint are returning None
regardless of success. They should return something
I propose that api_request
method return a Bool
and the methods in question return the <object>_id
on success for those two methods in each endpoint
def delete_computer_by_id(self, computer: ComputerId) -> int:
"""Delete a single computer record using the ID.
:param computer: A computer ID or supported Classic API model.
:type computer: Union[int, ClassicComputer, ClassicComputersItem]
"""
computer_id = ClassicApi._parse_id(computer)
if self.api_request(method="delete", resource_path=f"computers/id/{computer_id}"):
return computer_id
else:
return -1
0.4a1 Release
Currently, JSON responses are requested from the Classic API and can be parsed with the .json()
method:
response = api.classic_api_request("GET", "accounts/userid/257")
user_info = response.json()
However, there's no equivalent option for parsing XML responses if the headers are overridden to request XML:
response = api.classic_api_request("GET", "accounts/userid/257", override_headers={"Accept": "application/xml"})
user_info = response.xml() # this doesn't exist
Some orgs may benefit from the ability to choose between XML and JSON responses. One use case would be working around Jamf product issues that affect the accuracy of information returned via one format โ like PI104345: /accounts endpoint giving different results when searching by userid vs. username
.
The SDK could provide an .xml()
method similar to the existing .json()
method that converts XML responses to dictionary form, possibly leveraging a module like xmltodict to do the work behind the scenes.
Alternatively, the response could contain a .data
attribute that provides the relevant information in structured data (lists or dicts) regardless of what serialization format was requested โ a parsed equivalent to .text
. This attribute could replace both .json()
and .xml()
functionalities.
Thanks for considering.
client.classic_api.update_static_computer_group_membership_by_id(1, computers_to_add=["1"])
The computer is added. The underlying code should be casting the string value to an integer. The Pydantic model exports the correct XML document if the provided value is a string or an integer.
Debug logging to the console:
[ERROR] HTTPError: 409 Client Error: for url: https//XXXXXXXXXX/JSSResource/computergroups/id/1
Error: Unable to match computer
I think this is an encoding issue with the XML and Jamf is throwing an incorrect error.
When looking up a computer using the Pro API that returns the id
as a string value. Passing this directly to the static group update call surfaced the error.
jamf-pro-sdk 0.3a1
Jamf Pro 10.48.2
Simple script:
from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider
url="https://test.jamfcloud.com"
client_id="<redacted>"
client_secret="<redacted>"
client = JamfProClient(
server="snyk.jamfcloud.com",
credentials=ApiClientCredentialsProvider(client_id, client_secret)
)
all_computers = client.pro_api.get_computer_inventory_v1()
print(all_computers)
This results in: cannot import name 'ApiClientCredentialsProvider' from 'jamf_pro_sdk' (/opt/homebrew/lib/python3.11/site-packages/jamf_pro_sdk/init.py)
Installed:
jamf-pro-sdk 0.4a1
Feel free to tell me to go figure it out for myself but I though I'd check in with you first in case you were just still working on this part. It's still alpha, after all.
I'm getting an error trying ApiClientCredentialsProvider. from jamf_pro_sdk import ApiClientCredentialsProvider
didn't work but it does if I add it to the from .clients.auth import list in ./jamf_pro_sdk/__init__.py
or if I import it by path... from jamf_pro_sdk.clients.auth import ApiClientCredentialsProvider
I tried using it and get an error where pydantic is expecting a scope that doesn't yet exist.
File "/Users/admin/PycharmProjects/Jamf Client API/bryson.py", line 32, in <module>
credentials=ApiClientCredentialsProvider(client_id=get_env_value("client_id"),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/admin/PycharmProjects/Jamf Client API/venv/lib/python3.11/site-packages/jamf_pro_sdk/clients/auth.py", line 155, in __init__
super().__init__()
File "/Users/admin/PycharmProjects/Jamf Client API/venv/lib/python3.11/site-packages/jamf_pro_sdk/clients/auth.py", line 41, in __init__
self._access_token = AccessToken()
^^^^^^^^^^^^^
File "/Users/admin/PycharmProjects/Jamf Client API/venv/lib/python3.11/site-packages/jamf_pro_sdk/models/__init__.py", line 9, in __init__
super().__init__(**kwargs)
File "/Users/admin/PycharmProjects/Jamf Client API/venv/lib/python3.11/site-packages/pydantic/main.py", line 164, in __init__
__pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)
pydantic_core._pydantic_core.ValidationError: 1 validation error for AccessToken
scope
Field required [type=missing, input_value={}, input_type=dict]
This is what I was trying...
import logging
import os
from jamf_pro_sdk import JamfProClient
from jamf_pro_sdk.clients.auth import ApiClientCredentialsProvider
from jamf_pro_sdk.helpers import logger_quick_setup
logger_quick_setup(level=logging.DEBUG)
def get_env_value(variable_name):
"""
:param variable_name:
:return: value
Throws env error if 1) env var does not exist or 2) it exists but has no value
"""
value = os.getenv(variable_name)
if not value:
raise EnvironmentError(f"get_env_value: The {variable_name} variable exists but does not have a value")
return value
credentials=ApiClientCredentialsProvider(client_id=get_env_value("client_id"),
client_secret= get_env_value("client_secret"))
client = JamfProClient(
server=get_env_value("server"),
credentials=credentials
)
Python 3.11
Sonoma 14.1
Jamf Pro SDK 0.4a1
The current behaviour is to put all the functions for all the pro endpoints into the __init__.py
file. I realise this allows calls like
ifrom jamf_pro_sdk import JamfProClient, BasicAuthProvider
client = JamfProClient(
server="dummy.jamfcloud.com",
credentials=BasicAuthProvider("username", "password")
)
all_computers = client.pro_api.get_computer_inventory_v1()
but when the number of endpoints gets high this file will grow to be difficult to deal with.
I would propose that in the directory there could be a file computer_v1
which contains (among others) the function get_inventory
then the call would be:
from jamf_pro_sdk import JamfProClient, BasicAuthProvider
client = JamfProClient(
server="dummy.jamfcloud.com",
credentials=BasicAuthProvider("username", "password")
)
all_computers = client.pro_api.computer_v1.get_inventory()
SDK version 4.0a
Attempting to get a list of computers missing a Recovery lock password, target report was:
"is not" Recovery Lock Enabled = Enabled
"is" Recovery Lock Enabled = arm64
"member of" Computer Group = All Managed Clients
workaround is to change the "member of" operator to "is" Managed = Managed
from jamf_pro_sdk import JamfProClient
reportdata = client.classic_api.get_advanced_computer_search_by_id(reportwithmemberof)
Output of report computers
Traceback (most recent call last):
File "/usr/local/bin/Scripts/./JAMF_Script.py", line 35, in
reportdata = client.classic_api.get_advanced_computer_search_by_id(98)
File "/home/username/.local/lib/python3.10/site-packages/jamf_pro_sdk/clients/classic_api.py", line 490, in get_advanced_computer_search_by_id
return ClassicAdvancedComputerSearch(**resp.json()["advanced_computer_search"])
File "/home/username/.local/lib/python3.10/site-packages/jamf_pro_sdk/models/init.py", line 9, in init
super().init(**kwargs)
File "/home/username/.local/lib/python3.10/site-packages/pydantic/main.py", line 175, in init
self.pydantic_validator.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 1 validation error for ClassicAdvancedComputerSearch
criteria.2.search_type
Input should be 'is', 'is not', 'like', 'not like', 'has', 'does not have', 'matches regex', 'does not match regex', 'before (yyyy-mm-dd)', 'after (yyyy-mm-dd)', 'more than x days ago', 'less than x days ago', 'current', 'not current', 'greater than', 'less than', 'greater than or equal' or 'less than or equal' [type=enum, input_value='member of', input_type=str]
For further information visit https://errors.pydantic.dev/2.7/v/enum
Ubuntu 22.04.4 LTS
Name: jamf-pro-sdk
Version: 0.6a1
Name: pydantic
Version: 2.7.0
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.