Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A little clean up, some fixen, and a few tests. #48

Merged
merged 13 commits into from
May 30, 2022
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.pyc
*.egg*
.DS_Store
settings.py

.vscode
25 changes: 17 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

## [Unreleased]

### Added
- setup.cfg - Python package setup file ([#29](https://github.com/macadmins/simpleMDMpy/issues/29)) - TY [@bryanheinz](https://github.com/bryanheinz)
- pyproject.toml - Python package meta setup file ([#29](https://github.com/macadmins/simpleMDMpy/issues/29)) - TY [@bryanheinz](https://github.com/bryanheinz)
- tests - Added a few basic tests and including a readme on how to setup testing - TY [@bryanheinz](https://github.com/bryanheinz)

### Changes

- Added ability to update the actual device name via SimpleMDM (#24, #38) ([@bryanheinz](https://github.com/bryanheinz))
- Replaced get_logs() `id_override` input parameter with `starting_after` and `limit` (#25) ([@bryanheinz](https://github.com/bryanheinz))
- Fixes calls that return a single item (#26) ([@MagerValp](https://github.com/MagerValp))
- Add method to download profiles (#40) ([@joncrain](https://github.com/joncrain))
- Adds option for get_devices to include_awaiting_enrollment (#43) ([@joncrain](https://github.com/joncrain))
- Fixes `Devices.delete_device()` ([@MagerValp](https://github.com/MagerValp))
- Added ability to update the actual device name via SimpleMDM ([#24](https://github.com/macadmins/simpleMDMpy/issues/24), [#38](https://github.com/macadmins/simpleMDMpy/issues/38)) - TY [@bryanheinz](https://github.com/bryanheinz)
- Replaced get_logs() `id_override` input parameter with `starting_after` and `limit` ([#25](https://github.com/macadmins/simpleMDMpy/issues/25)) - TY [@bryanheinz](https://github.com/bryanheinz)
- Fixes calls that return a single item ([#26](https://github.com/macadmins/simpleMDMpy/issues/26)) - TY [@MagerValp](https://github.com/MagerValp)
- Add method to download profiles ([#40](https://github.com/macadmins/simpleMDMpy/issues/40)) - TY [@joncrain](https://github.com/joncrain)
- Adds option for get_devices to include_awaiting_enrollment ([#43](https://github.com/macadmins/simpleMDMpy/issues/43)) - TY [@joncrain](https://github.com/joncrain)
- Fixes `Devices.delete_device()` - TY [@MagerValp](https://github.com/MagerValp)
- Add Devices methods for enabling/disabling remote desktop, and profile and user listing ([@MagerValp](https://github.com/MagerValp))
- Add /devices request rate limiting to `_get_data` ([@MagerValp](https://github.com/MagerValp))
- Add retry on 5xx errors to GET requests to `_get_data` ([@MagerValp](https://github.com/MagerValp))
- Add /devices request rate limiting to `_get_data` - TY [@MagerValp](https://github.com/MagerValp)
- Add retry on 5xx errors to GET requests to `_get_data` - TY [@MagerValp](https://github.com/MagerValp)
- Fixes `_get_data` so that it properly preserves all input parameters ([#45](https://github.com/macadmins/simpleMDMpy/issues/45)) - TY [@bryanheinz](https://github.com/bryanheinz)
- Adds help docs to Devices.get_device() - TY [@bryanheinz](https://github.com/bryanheinz)

### Issues

Expand All @@ -22,6 +29,8 @@
- Closes issue #26
- Closes issue #40
- Closes issue #43
- Closes issue #29
- Closes issue #45

## [v3.0.6]

Expand Down
34 changes: 23 additions & 11 deletions SimpleMDMpy/Devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,31 @@ def __init__(self, api_key):
self.url = self._url("/devices")

def get_device(self, device_id="all", search=None, include_awaiting_enrollment=False):
"""Returns a device specified by id. If no ID or search is
specified all devices will be returned"""
"""
Returns a device specified by id. If no ID or search is specified all
devices will be returned.

Args:
device_id (str, optional): Returns a dictionary of the specified
device id. By default, it returns a list of all devices. If a
device_id and search is specified, then search will be ignored.
search (str, optional): Returns a list of devices that match the
search criteria. Defaults to None. Ignored if device_id is set.
include_awaiting_enrollment (bool, optional): Returns a list of all
devices including devices in the "awaiting_enrollment" state.

Returns:
dict: A single dictionary object with device information.
array: An array of dictionary objects with device information.
"""
url = self.url
data = {}
if search:
data['search'] = search
if include_awaiting_enrollment:
data['include_awaiting_enrollment'] = True
elif device_id != 'all':
params = {'include_awaiting_enrollment': include_awaiting_enrollment}
# if a device ID is specified, then ignore any searches
if device_id != 'all':
url = url + "/" + str(device_id)
if include_awaiting_enrollment:
data.update({'include_awaiting_enrollment': include_awaiting_enrollment})
return self._get_data(url, data)
elif search:
params['search'] = search
return self._get_data(url, params)

def create_device(self, name, group_id):
"""Creates a new device object in SimpleMDM. The response
Expand Down
25 changes: 18 additions & 7 deletions SimpleMDMpy/SimpleMDM.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Connection(object): #pylint: disable=old-style-class,too-few-public-method

def __init__(self, api_key):
self.api_key = api_key
# setup a session that can retry, helps with rate limiting end-points
# https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/#retry-on-failure
# https://macadmins.slack.com/archives/C4HJ6U742/p1652996411750219
retry_strategy = Retry(
total = 5,
backoff_factor = 1,
Expand All @@ -34,24 +37,32 @@ def __init__(self, api_key):
self.session = requests.Session()
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)

def __del__(self):
# this runs when the Connection object is being deinitialized
# this properly closes the session
self.session.close()

def _url(self, path): #pylint: disable=no-self-use
"""base api url"""
return 'https://a.simplemdm.com/api/v1' + path

# TODO: make _is_devices_req generic for any future rate limited endpoints
def _is_devices_req(self, url):
return url.startswith(self._url("/devices"))

def _get_data(self, url, params=None):
def _get_data(self, url, params={}):
"""GET call to SimpleMDM API"""
start_id = 0
has_more = True
list_data = []
if params is None:
params = {}
params["limit"] = 100
# by using the local req_params variable, we can set our own defaults if
# the parameters aren't included with the input params. This is needed
# so that certain other functions, like Logs.get_logs(), can send custom
# starting_after and limit parameters.
req_params = {}
req_params['limit'] = params.get('limit', 100)
req_params['starting_after'] = params.get('starting_after', 0)
while has_more:
params["starting_after"] = start_id
# Calls to /devices should be rate limited
if self._is_devices_req(url):
if time.time() - self.last_device_req_timestamp < self.device_req_rate_limit:
Expand All @@ -75,7 +86,7 @@ def _get_data(self, url, params=None):
list_data.extend(data)
has_more = resp_json.get('has_more', False)
if has_more:
start_id = data[-1].get('id')
params["starting_after"] = data[-1].get('id')
return list_data

def _get_xml(self, url, params=None):
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
21 changes: 21 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[metadata]
name = SimpleMDMpy
version = 3.0.6
author = Steve Küng
maintainer = MacAdmins
description = A Python Library for SimpleMDM.
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT
license_file = LICENSE
url = https://github.com/macadmins/simpleMDMpy
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: System Administrators
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python :: 3

[options]
packages = SimpleMDMpy
install_requires = requests
15 changes: 15 additions & 0 deletions tests/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Tests

This is my first foray into unit tests. If you see any issues or have any recommendations, please open a [Github issue](https://github.com/macadmins/simpleMDMpy/issues) or reach out to me on the [Mac Admins](https://www.macadmins.org) Slack @bheinz - @bryanheinz

Because our testing currently has to be done on our own SimpleMDM instances, please be extra cautious and review all code before running. This is also why the tests largely don't test for specific data.

## Testing Steps
- Duplicate `settings-sample.py` as `settings.py` and fill in the variables
- Create virtual environments folder if it doesn't already exist `mkdir ~/.env`
- Create the virtual env `python3 -m venv ~/.env/smdm-tests`
- Activate the environment `source ~/.env/smdm-tests/bin/activate`
- Install the simpleMDMpy module version that you'd like to test `pip install -e /path/to/cloned/simpleMDMpy` (requires pyproject.toml and setup.cfg files, and pip v22+)
- Run the tests `python3 -m unittest discover -s /path/to/cloned/simpleMDMpy/tests`
- Uninstall the test module `pip uninstall SimpleMDMpy`
- Deactivate the Python virtual environment `deactivate`
10 changes: 10 additions & 0 deletions tests/settings-sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3

# add a SimpleMDM API key to use for tests
api_key = ''

# add the ID of a profile in your instance to test profile functions
profile_id = '' # note what profile this is

# add the id of a device in your instance to test functions against
device_id = '' # note what device this is
16 changes: 16 additions & 0 deletions tests/test_Account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python3

import settings
import unittest
import SimpleMDMpy


class TestCustomConfigurationProfiles(unittest.TestCase):
def test_get_account_details(self):
account_details = SimpleMDMpy.Account(settings.api_key) \
.get_account_details()
account_name = account_details.get('attributes', {}).get('name')
self.assertIsNotNone(account_name)

if __name__ == '__main__':
unittest.main()
29 changes: 29 additions & 0 deletions tests/test_CustomConfigurationProfiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3

import plistlib
import settings
import unittest
import SimpleMDMpy


@unittest.skipIf(settings.profile_id == '',
"profile_id not specified in settings.py.")
class TestCustomConfigurationProfiles(unittest.TestCase):
def test_get_profile(self):
profile_found = False
all_profiles = SimpleMDMpy.CustomConfigurationProfiles(
settings.api_key).get_profiles()
for prof in all_profiles:
prof_id = str(prof.get('id', ''))
if prof_id == settings.profile_id: profile_found = True
self.assertTrue(profile_found)

def test_download_profile(self):
# if settings.profile_id == '':
profile = SimpleMDMpy.CustomConfigurationProfiles(settings.api_key) \
.download_profile(settings.profile_id)
self.assertIsInstance(plistlib.loads(profile), dict)


if __name__ == '__main__':
unittest.main()
30 changes: 30 additions & 0 deletions tests/test_Devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import settings
import unittest
import SimpleMDMpy

class TestDevices(unittest.TestCase):
def test_get_device(self):
all_devices = SimpleMDMpy.Devices(
settings.api_key).get_device(include_awaiting_enrollment=True)
self.assertGreaterEqual(len(all_devices), 1)
# print(len(all_devices))
cid = all_devices[0].get('id')
self.assertIsNotNone(cid)
single_device = SimpleMDMpy.Devices(settings.api_key) \
.get_device(device_id=cid)
self.assertEqual(single_device.get('id'), cid)

def test_list_profiles(self):
device_profiles = SimpleMDMpy.Devices(settings.api_key) \
.list_profiles(settings.device_id)
self.assertGreaterEqual(len(device_profiles), 1)

def test_list_users(self):
device_users = SimpleMDMpy.Devices(settings.api_key) \
.list_users(settings.device_id)
self.assertGreaterEqual(len(device_users), 1)


if __name__ == '__main__':
unittest.main()