├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SimpleMDMpy ├── Account.py ├── AppGroups.py ├── Apps.py ├── AssignmentGroups.py ├── CustomAttributes.py ├── CustomConfigurationProfiles.py ├── DepServers.py ├── DeviceGroups.py ├── Devices.py ├── Enrollments.py ├── InstalledApps.py ├── Logs.py ├── LostMode.py ├── ManagedAppConfigs.py ├── PushCertificate.py ├── ScriptJobs.py ├── Scripts.py ├── SimpleMDM.py └── __init__.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg └── tests ├── readme.md ├── settings-sample.py ├── test_Account.py ├── test_CustomConfigurationProfiles.py └── test_Devices.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg* 3 | .DS_Store 4 | settings.py 5 | 6 | .vscode 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [Unreleased] 4 | 5 | ## [3.0.7] 6 | 7 | ### Added 8 | - setup.cfg - Python package setup file ([#29](https://github.com/macadmins/simpleMDMpy/issues/29)) - TY [@bryanheinz](https://github.com/bryanheinz) 9 | - pyproject.toml - Python package meta setup file ([#29](https://github.com/macadmins/simpleMDMpy/issues/29)) - TY [@bryanheinz](https://github.com/bryanheinz) 10 | - tests - Added a few basic tests and including a readme on how to setup testing - TY [@bryanheinz](https://github.com/bryanheinz) 11 | 12 | ### Changes 13 | 14 | - 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) 15 | - 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) 16 | - Fixes calls that return a single item ([#26](https://github.com/macadmins/simpleMDMpy/issues/26)) - TY [@MagerValp](https://github.com/MagerValp) 17 | - Add method to download profiles ([#40](https://github.com/macadmins/simpleMDMpy/issues/40)) - TY [@joncrain](https://github.com/joncrain) 18 | - Adds option for get_devices to include_awaiting_enrollment ([#43](https://github.com/macadmins/simpleMDMpy/issues/43)) - TY [@joncrain](https://github.com/joncrain) 19 | - Fixes `Devices.delete_device()` - TY [@MagerValp](https://github.com/MagerValp) 20 | - Add Devices methods for enabling/disabling remote desktop, and profile and user listing ([@MagerValp](https://github.com/MagerValp)) 21 | - Add /devices request rate limiting to `_get_data` - TY [@MagerValp](https://github.com/MagerValp) 22 | - Add retry on 5xx errors to GET requests to `_get_data` - TY [@MagerValp](https://github.com/MagerValp) 23 | - 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) 24 | - Adds help docs to Devices.get_device() - TY [@bryanheinz](https://github.com/bryanheinz) 25 | - Add Scripts and ScriptJobs - TY [@MagerValp](https://github.com/MagerValp) 26 | - Fix pagination - TY [@jcfrt](https://github.com/jcfrt) 27 | - Fix rate limiting - TY [@MagerValp](https://github.com/MagerValp) 28 | 29 | ### Issues 30 | 31 | - Closes issue #24 32 | - Closes issue #38 33 | - Closes issue #25 34 | - Closes issue #26 35 | - Closes issue #40 36 | - Closes issue #43 37 | - Closes issue #29 38 | - Closes issue #45 39 | - Closes issue #57 40 | 41 | ## [v3.0.6] 42 | 43 | ### PRs Included 44 | 45 | - [#35](https://github.com/macadmins/simpleMDMpy/pull/25) 46 | - [#34](https://github.com/macadmins/simpleMDMpy/pull/34) 47 | - [#27](https://github.com/macadmins/simpleMDMpy/pull/27) 48 | 49 | ### Changes 50 | 51 | - Add method to get all custom attributes for a device 52 | 53 | ## [v3.0.5] 54 | 55 | ### Issues 56 | 57 | - Closes #21 58 | 59 | ### Added 60 | 61 | - CODEOWNERS 62 | 63 | ## [v3.0.4] 64 | 65 | ### Issues 66 | 67 | - Closes #19 68 | - Closes #18 69 | 70 | ### Added 71 | 72 | - LICENSE 73 | - `Contributing.md` 74 | 75 | ### Changed 76 | 77 | - Merged with @MagerValp / simpleMDMpy @ [508540928](https://github.com/MagerValp/simpleMDMpy/commit/50854094bee2ac5306eded7c5614d76f3eab4c25) 78 | - minor tweaks on the readme 79 | 80 | ## [v3.0.3] 81 | 82 | ### Issues 83 | 84 | - Closes #15 85 | - Closed #16 86 | - Closed #14 87 | 88 | ### Added 89 | 90 | - support for setting custom attr on initial creation 91 | - support for updating a custom attribute 92 | 93 | ### Changed 94 | 95 | - default branch is now `main` 96 | - remove `data` payload from Devices.delete_device 97 | 98 | ## [v3.0.2] 99 | 100 | ### Issues 101 | 102 | - Closes #11 103 | - Closes #12 104 | 105 | ### Added 106 | 107 | - _get_data now has `id_override=None` so you can override `&starting_after=` as you wish 108 | - `id_override=0` implemented in Logs get_logs() 109 | 110 | ### Changed 111 | 112 | - Changed paginaition to work without compounding to a `414` 113 | 114 | ## [v3.0.1] 115 | 116 | ### Issues 117 | 118 | - Closes #9 119 | 120 | ## Changed 121 | 122 | - Changed paginaition to work, now returns obj not response 123 | - good catch @bryanheinz 124 | 125 | ## [v3.0.0] 126 | 127 | - Closes #3 128 | 129 | ## Changed 130 | 131 | - removed forced encoding for `GET` responses 132 | - added some pylint comments 133 | 134 | ## [v2.1.0] 135 | 136 | ### Issues 137 | 138 | - Closes #5 139 | 140 | ### Changed 141 | 142 | - fixed module names 143 | 144 | ## [v2.0.0] 145 | 146 | ### Issues 147 | 148 | - Closes #1 149 | 150 | ### Added 151 | 152 | - Partial/Full Support for: 153 | - Assignment Groups 154 | - Custom Attributes 155 | - Custom Configuration 156 | - DEP Servers 157 | - Enrollments 158 | - Logs 159 | - Lost Mode 160 | - Webhooks 161 | - (Somewhat) Help(ful) Strings 162 | 163 | ### Changed 164 | 165 | - removed PIP items for now 166 | 167 | ### Base 168 | 169 | - forked from [SteveKueng/simple_mdm_py](https://github.com/SteveKueng/simple_mdm_py/blob/master/setup.py) at [cf6650f](https://github.com/SteveKueng/simpleMDMpy/commit/cf6650fe72220577abd5c654d03476c88b81bcb0) 170 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default code owners 2 | * @MagerValp 3 | * @rickheil 4 | * @bryanheinz 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing TO macadmins/simpleMDMpy 2 | 3 | ## Getting in contact 4 | 5 | If you're interested in contributing or just have a question, 6 | we'd love to chat. 7 | 8 | You can find us in [#simplemdm](https://macadmins.slack.com/archives/C4HJ6U742) in the [MacAdmins Slack](https://www.macadmins.org/). 9 | 10 | ## Submitting Issues 11 | 12 | At this point in time there is no restriction for submitting issue, there is also to established labeling system. Feel free to create issues that seem pertinent to the project. 13 | 14 | ## Contribution Process 15 | 16 | We have a 3 step process for contributions: 17 | 18 | 1. An issue is created, and changes are committed via a git branch, 19 | named after a relating issue (Issue-2, for example). 20 | 2. Create a GitHub Pull Request for your change, please see [Pull Request Requirements](#pull-request-requirements), 21 | 3. Code owners will perform a [Code Review](#code-review-process) on the pull request. 22 | 23 | ### Pull Request Requirements 24 | 25 | We strive to ensure high quality throughout the experience. In order to ensure this, all pull requests must meet these specifications: 26 | 27 | 1. **Linting:** To ensure high-quality code and protect against future regressions, we require all code to be linted against pylint. 28 | 29 | 2. **CHANGELOG.md:** We use the CHANGELOG for tracking changes to semver tagged releases of the code, so ensure it matches the changelog syntax. Feel free to reference [Keep a CHANGELOG](https://keepachangelog.com/en/0.3.0/) if you have any questions. 30 | 31 | 32 | ### CODE REVIEW PROCESS 33 | 34 | Code review takes place in GitHub pull requests. Issues will be used for more general discussion around the topic or issue. 35 | 36 | Once you open a pull request, cookbook owners will review your code using the built-in code review process in Github PRs. We leverage the CODEOWNERS file to assist with this. 37 | 38 | The process at this point is as follows: 39 | 40 | 1. A cookbook maintainer will review your code and merge it if no changes are 41 | necessary. 42 | 43 | 2. If a maintainer has feedback or questions on your changes they they will 44 | set `request changes` in the review and provide an explanation. 45 | 46 | Please see TESTING.md for system requirements. 47 | 48 | ### Branches and Commits 49 | 50 | You should submit your patch as a git branch named after the Github issue, such as Issue-11. This is called a _topic branch_ and allows users to associate a branch of code with the Issue. 51 | 52 | It is a best practice to have your commit message have mimic the CHANGELOG, whenever possible. 53 | 54 | This also helps other contributors understand the purpose of changes to the code. 55 | 56 | ```text 57 | v1.0.6 58 | 59 | Issue 60 | 61 | - Closes #11 62 | 63 | Changed 64 | 65 | - updated data processor to process the data 66 | ``` 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 macadmins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleMDMpy 2 | 3 | A python library for simpleMDM API. 4 | 5 | [SimpleMDM API Documentation](https://simplemdm.com/docs/api) 6 | 7 | ## Contributing 8 | 9 | - [Contributing.md](./CONTRIBUTING.md) 10 | 11 | ## Install 12 | 13 | Your SimpleMDM API key will need to be set as an environmental variable `api_key`. 14 | 15 | Help available via `help(SimpleMDMpy)` 16 | 17 | ## Sample Projects 18 | 19 | * [Making SimpleMDM Complicated](https://github.com/lucasjhall/CONF-2021_MDO_YVR-Making_SimpleMDM_Complicated) 20 | * [SimpleCLI](https://github.com/MagerValp/SimpleCLI) 21 | 22 | ## Available Modules 23 | 24 | ### Account 25 | 26 | ```python 27 | class Account(SimpleMDMpy.SimpleMDM.Connection) 28 | | account class provides auth and basic account details 29 | | 30 | | Methods defined here: 31 | | 32 | | __init__(self, api_key) 33 | | 34 | | get_account_details(self) 35 | | returns account details as dict 36 | | 37 | | set_account_details(self, name=None, country_code=None) 38 | | set account detail 39 | ``` 40 | 41 | ### App Groups 42 | 43 | ```python 44 | class AppGroups(SimpleMDMpy.SimpleMDM.Connection) 45 | | App Groups class provides interaction with Application Groups 46 | | 47 | | Methods defined here: 48 | | 49 | | __init__(self, api_key) 50 | | 51 | | assign_app(self, app_group_id, app_id) 52 | | remove app group from group 53 | | 54 | | assign_device(self, app_group_id, device_id) 55 | | assign device to app group 56 | | 57 | | assign_device_group(self, app_group_id, device_group_id) 58 | | assign device group from app group 59 | | 60 | | create_app_group(self, name, auto_deploy='true') 61 | | create app group 62 | | 63 | | delete_app_group(self, app_group_id) 64 | | remove app group 65 | | 66 | | get_app_group(self, app_group_id='all') 67 | | Get an app group, defaults to 'all' if 'app_group_id' is not present 68 | | 69 | | push_apps(self, app_group_id) 70 | | push apps in app group 71 | | 72 | | un_assign_app(self, app_group_id, app_id) 73 | | unassign app from app group 74 | | 75 | | un_assign_device(self, app_group_id, device_id) 76 | | unassign apps in app group 77 | | 78 | | un_assign_device_group(self, app_group_id, device_group_id) 79 | | remove device group from app group 80 | | 81 | | update_app_group(self, app_group_id, name=None, auto_deploy='true') 82 | | update app group 83 | | 84 | | update_apps(self, app_group_id) 85 | | update apps 86 | | 87 | 88 | ``` 89 | 90 | ### Apps 91 | 92 | ```python 93 | class Apps(SimpleMDMpy.SimpleMDM.Connection) 94 | | apps module for SimpleMDMpy 95 | | 96 | | Methods defined here: 97 | | 98 | | __init__(self, api_key) 99 | | 100 | | create_app(self, name=None, app_store_id=None, bundle_id=None, binary=None) 101 | | upload an app binary 102 | | 103 | | delete_app(self, app_id) 104 | | delete an app 105 | | 106 | | get_app(self, app_id='all') 107 | | list app, if none specified all return 108 | | 109 | | update_app(self, app_id, binary=None, name=None) 110 | | update an apps info binary etc 111 | | 112 | ``` 113 | 114 | ### Assignment Groups 115 | 116 | ```python 117 | class AssignmentGroups(SimpleMDMpy.SimpleMDM.Connection) 118 | | assignment groups module for SimpleMDMpy 119 | | 120 | | Methods defined here: 121 | | 122 | | __init__(self, api_key) 123 | | 124 | | assign_app(self, assignment_group_id, app_id) 125 | | assign app to an assignment group 126 | | 127 | | assign_device(self, assignment_group_id, device_id) 128 | | assign device to an assignment group 129 | | 130 | | assign_device_group(self, assignment_group_id, device_group_id) 131 | | assigns a device group 132 | | 133 | | create_assignment_group(self, name, auto_deploy) 134 | | creates an assignment group 135 | | 136 | | delete_assignment_group(self, assignment_group_id) 137 | | delete an app to an assignment group 138 | | 139 | | get_assignment_groups(self, assignment_group_id='all') 140 | | returns assignment group(s), defaults to all if none specified 141 | | 142 | | push_apps(self, assignment_group_id) 143 | | push apps in an assignment group 144 | | 145 | | unassign_app(self, assignment_group_id, app_id) 146 | | unassign app to an assignment group 147 | | 148 | | unassign_device(self, assignment_group_id, device_id) 149 | | unassign device to an assignment group 150 | | 151 | | unassign_device_group(self, assignment_group_id, device_group_id) 152 | | delete a device group assignment 153 | | 154 | | update_apps(self, assignment_group_id) 155 | | update apps in an assignment group 156 | | 157 | | update_assignment_group(self, assignment_group_id, name, auto_deploy) 158 | | updates an assignment group 159 | | 160 | 161 | ``` 162 | 163 | ### Custom Attributes 164 | 165 | ```python 166 | class CustomAttributes(SimpleMDMpy.SimpleMDM.Connection) 167 | | work with custom attributes 168 | | 169 | | Methods defined here: 170 | | 171 | | __init__(self, api_key) 172 | | 173 | | create_custom_attribute(self, name) 174 | | create custom attribute 175 | | 176 | | delete_custom_attribute(self, custom_attribute_id) 177 | | deletes custom attribute 178 | | 179 | | get_custom_attributes(self) 180 | | lists all custom attributes 181 | | 182 | ``` 183 | 184 | 185 | 186 | ### Custom Configuration Profiles 187 | ```python 188 | class CustomConfigurationProfiles(SimpleMDMpy.SimpleMDM.Connection) 189 | | work with custom profiles 190 | | 191 | | Methods defined here: 192 | | 193 | | __init__(self, api_key) 194 | | 195 | | assign_to_device_group(self, profile_id, device_group_id) 196 | | assigns custom profile to group 197 | | 198 | | create_profile(self, name, mobileconfig, user_scope=None, attribute_support=False) 199 | | upload a config file 200 | | 201 | | delete_profile(self, profile_id) 202 | | deletes custom profile 203 | | 204 | | download_profile(self, profile_id) 205 | | downloads custom profile 206 | | 207 | | get_profiles(self) 208 | | returns profiles 209 | | 210 | | unassign_from_device_group(self, profile_id, device_group_id) 211 | | deletes profile from device group 212 | | 213 | | update_profile(self, profile_id, name=None, mobileconfig=None, user_scope=None, attribute_support=None) 214 | | update a config file 215 | | 216 | ``` 217 | 218 | ### DEP Servers 219 | 220 | ```python 221 | class DepServers(SimpleMDMpy.SimpleMDM.Connection) 222 | | module for interacting with dep server configurations 223 | | 224 | | Methods defined here: 225 | | 226 | | __init__(self, api_key) 227 | | 228 | | get_dep_devices(self, dep_server_id, dep_device_id='all') 229 | | return a DEP device via an ID, defaults to all if none specified 230 | | 231 | | get_dep_servers(self, dep_server_id='all') 232 | | returns dep servers, defaults to all if none specified 233 | | 234 | | sync_dep_servers(self, dep_server_id) 235 | | syncs specified server with Apple DEP 236 | | 237 | ``` 238 | 239 | ### Devices 240 | 241 | ```python 242 | class Devices(SimpleMDMpy.SimpleMDM.Connection) 243 | | devices module for SimpleMDMpy 244 | | 245 | | Methods defined here: 246 | | 247 | | __init__(self, api_key) 248 | | 249 | | clear_firmware_password(self, device_id) 250 | | You can use this method to remove the firmware password from a device. 251 | | The firmware password must have been originally set using SimpleMDM for 252 | | this to complete successfully. 253 | | 254 | | clear_passcode_device(self, device_id) 255 | | You can use this method to unlock and remove the passcode of a device. 256 | | 257 | | create_device(self, name, group_id) 258 | | Creates a new device object in SimpleMDM. The response 259 | | body includes an enrollment URL that can be used once to 260 | | enroll a physical device. 261 | | 262 | | delete_device(self, device_id) 263 | | Unenroll a device and remove it from the account. 264 | | 265 | | get_custom_attribute(self, device_id, custom_attribute_name) 266 | | get a devices custom attributes 267 | | 268 | | get_device(self, device_id='all', search=None, include_awaiting_enrollment=False) 269 | | Returns a device specified by id. If no ID or search is 270 | | specified all devices will be returned. Default does not include devices 271 | | waiting for enrollment 272 | | 273 | | list_installed_apps(self, device_id) 274 | | Returns a listing of the apps installed on a device. 275 | | 276 | | lock_device(self, device_id, message, phone_number, pin=None) 277 | | You can use this method to lock a device and optionally display 278 | | a message and phone number. The device can be unlocked with the 279 | | existing passcode of the device. 280 | | 281 | | push_apps_device(self, device_id) 282 | | You can use this method to push all assigned apps 283 | | to a device that are not already installed. 284 | | 285 | | refresh_device(self, device_id) 286 | | Request a refresh of the device information and app inventory. 287 | | SimpleMDM will update the inventory information when the device responds 288 | | to the request. 289 | | 290 | | restart_device(self, device_id) 291 | | This command sends a restart command to the device. 292 | | 293 | | set_custom_attribute(self, value, device_id, custom_attribute_name) 294 | | set a devices custom attribute to a specific value 295 | | 296 | | shutdown_device(self, device_id) 297 | | This command sends a shutdown command to the device. 298 | | 299 | | update_device(self, device_id, name=None, device_name=None) 300 | | Update the SimpleMDM name or device name of a device object. 301 | | 302 | | update_os(self, device_id) 303 | | You can use this method to update a device to the latest OS version. 304 | | Currently supported by iOS devices only. 305 | | 306 | | wipe_device(self, device_id) 307 | | You can use this method to erase all content and settings stored on a 308 | | device. The device will be unenrolled from SimpleMDM and returned to a 309 | | factory default configuration. 310 | | 311 | ``` 312 | 313 | ### Device Groups 314 | 315 | ```python 316 | class DeviceGroups(SimpleMDMpy.SimpleMDM.Connection) 317 | | device groups module for SimpleMDMpy 318 | | 319 | | Methods defined here: 320 | | 321 | | __init__(self, api_key) 322 | | 323 | | assign_device(self, device_id, device_group_id) 324 | | assign device to a group 325 | | 326 | | get_device_group(self, device_group_id='all') 327 | | get a devices group 328 | | 329 | ``` 330 | 331 | ### Enrollments 332 | 333 | ```python 334 | class Enrollments(SimpleMDMpy.SimpleMDM.Connection) 335 | | enrollments module for SimpleMDMpy 336 | | 337 | | Methods defined here: 338 | | 339 | | __init__(self, api_key) 340 | | 341 | | delete_enrollment(self, enrollment_id) 342 | | delete enrollment 343 | | 344 | | get_enrollments(self, enrollment_id='all') 345 | | get a devices group 346 | | 347 | | send_invitation(self, enrollment_id, contact) 348 | | Send an enrollment invitation to an email address or phone number. 349 | | 350 | ``` 351 | 352 | ### Installed Apps 353 | 354 | ```python 355 | class InstalledApps(SimpleMDMpy.SimpleMDM.Connection) 356 | | Installed apps represent apps that are installed and exist on devices. 357 | | 358 | | Methods defined here: 359 | | 360 | | __init__(self, api_key) 361 | | 362 | | delete_app(self, installed_app_id) 363 | | This submits a request to the device to uninstall the specified app. 364 | | The app must be managed for this request to succeed. 365 | | 366 | | get_app(self, installed_app_id) 367 | | retrieve an installed app 368 | | 369 | | update(self, installed_app_id) 370 | | This submits a request to the device to update the specified app to 371 | | the latest version. The app must be managed for this request to succeed. 372 | | 373 | ``` 374 | 375 | ### Logs 376 | 377 | ```python 378 | class Logs(SimpleMDMpy.SimpleMDM.Connection) 379 | | GET all the LOGS 380 | | 381 | | Methods defined here: 382 | | 383 | | __init__(self, api_key) 384 | | 385 | | get_logs(self, starting_after=None, limit=None) 386 | | And I mean all the LOGS, before pagination 387 | | 388 | 389 | ``` 390 | 391 | ### Lost Mode 392 | 393 | ```python 394 | class LostMode(SimpleMDMpy.SimpleMDM.Connection) 395 | | Interact with lost mode on a device. 396 | | 397 | | Methods defined here: 398 | | 399 | | __init__(self, api_key) 400 | | 401 | | disable(self, device_id) 402 | | Disable lost mode on a device. 403 | | 404 | | enable(self, device_id) 405 | | Activate lost mode on a device. 406 | | 407 | | play_sound(self, device_id) 408 | | Request that the device play a sound to assist 409 | | with locating it. 410 | | 411 | | update_location(self, device_id) 412 | | Request that the device provide its current, 413 | | up-to-date location. Location data can be viewed 414 | | using the devices endpoint. 415 | | 416 | 417 | ``` 418 | 419 | ### Managed App Configs 420 | 421 | ```python 422 | class ManagedAppConfigs(SimpleMDMpy.SimpleMDM.Connection) 423 | | Create, modify, and remove the managed app configuration 424 | | associated with an app. 425 | | 426 | | Methods defined here: 427 | | 428 | | __init__(self, api_key) 429 | | 430 | | delete_config(self, app_id, managed_config_id) 431 | | Delete managed config from an app by ID. 432 | | 433 | | get_managed_configs(self, app_id) 434 | | "Retrieve the managed configs for an app. 435 | | 436 | | push_updates(self, app_id) 437 | | Push any updates to the managed configurations 438 | | for an app to all devices. This is not necessary 439 | | when making managed config changes through the UI. 440 | | This is necessary after making changes through the API. 441 | | 442 | ``` 443 | 444 | ### Push Certificate 445 | 446 | ```python 447 | class PushCertificate(SimpleMDMpy.SimpleMDM.Connection) 448 | | Push cert module actions 449 | | 450 | | Methods defined here: 451 | | 452 | | __init__(self, api_key) 453 | | 454 | | get_signed_csr(self) 455 | | Download a signed CSR file. This file is 456 | | provided to Apple when creating and renewing a 457 | | push certificate. The API returns a base64 458 | | encoded plist for upload to the Apple Push 459 | | Certificates Portal. The value of the data 460 | | key can be uploaded to Apple as is 461 | | 462 | | getpush_certificate(self) 463 | | Show details related to the current push 464 | | certificate being used. 465 | | 466 | | update_certificate(self, file, apple_id) 467 | | Upload a new certificate and replace the 468 | | existing certificate for your account. 469 | | 470 | ``` 471 | -------------------------------------------------------------------------------- /SimpleMDMpy/Account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """accounts module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class Account(SimpleMDMpy.SimpleMDM.Connection): 9 | """account class provides auth and basic account details""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/account") 13 | 14 | def get_account_details(self): 15 | """returns account details as dict""" 16 | return self._get_data(self.url) 17 | 18 | def set_account_details(self, name=None, country_code=None): 19 | """set account detail""" 20 | data = {} 21 | if name: 22 | data['name'] = name 23 | if country_code: 24 | data['apple_store_country_code'] = country_code 25 | return self._patch_data(self.url, data) 26 | -------------------------------------------------------------------------------- /SimpleMDMpy/AppGroups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """App Groups module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class AppGroups(SimpleMDMpy.SimpleMDM.Connection): 9 | """App Groups class provides interaction with Application Groups""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/app_groups") 13 | 14 | def get_app_group(self, app_group_id="all"): 15 | """Get an app group, defaults to 'all' if 'app_group_id' is not present""" 16 | url = self.url 17 | if app_group_id != 'all': 18 | url = url + "/" + app_group_id 19 | return self._get_data(url) 20 | 21 | def create_app_group(self, name, auto_deploy="true"): 22 | """create app group""" 23 | url = self.url 24 | data = {'name': name, 'auto_deploy': auto_deploy} 25 | return self._post_data(url, data) 26 | 27 | def update_app_group(self, app_group_id, name=None, auto_deploy="true"): 28 | """update app group""" 29 | url = self.url + "/" + app_group_id 30 | data = {} 31 | if name: 32 | data['name'] = name 33 | data['auto_deploy'] = auto_deploy 34 | return self._patch_data(url, data) 35 | 36 | def delete_app_group(self, app_group_id): 37 | """remove app group""" 38 | url = self.url + "/" + app_group_id 39 | data = {} 40 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 41 | 42 | def assign_app(self, app_group_id, app_id): 43 | """remove app group from group""" 44 | url = self.url + "/" + app_group_id + "/apps/" + app_id 45 | data = {} 46 | return self._post_data(url, data) 47 | 48 | def un_assign_app(self, app_group_id, app_id): 49 | """unassign app from app group""" 50 | url = self.url + "/" + app_group_id + "/apps/" + app_id 51 | data = {} 52 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 53 | 54 | def assign_device_group(self, app_group_id, device_group_id): 55 | """assign device group from app group""" 56 | url = self.url + "/" + app_group_id + "/device_groups/" + device_group_id 57 | data = {} 58 | return self._post_data(url, data) 59 | 60 | def un_assign_device_group(self, app_group_id, device_group_id): 61 | """remove device group from app group""" 62 | url = self.url + "/" + app_group_id + "/device_groups/" + device_group_id 63 | data = {} 64 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 65 | 66 | def assign_device(self, app_group_id, device_id): 67 | """assign device to app group""" 68 | url = self.url + "/" + app_group_id + "/devices/" + str(device_id) 69 | data = {} 70 | return self._post_data(url, data) 71 | 72 | def un_assign_device(self, app_group_id, device_id): 73 | """unassign apps in app group""" 74 | url = self.url + "/" + app_group_id + "/devices/" + str(device_id) 75 | data = {} 76 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 77 | 78 | def push_apps(self, app_group_id): 79 | """push apps in app group""" 80 | url = self.url + "/" + app_group_id + "/push_apps" 81 | data = {} 82 | return self._post_data(url, data) 83 | 84 | def update_apps(self, app_group_id): 85 | """update apps""" 86 | url = self.url + "/" + app_group_id + "/update_apps" 87 | data = {} 88 | return self._post_data(url, data) 89 | -------------------------------------------------------------------------------- /SimpleMDMpy/Apps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ apps module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class Apps(SimpleMDMpy.SimpleMDM.Connection): 9 | """ apps module for SimpleMDMpy""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/apps") 13 | 14 | def get_app(self, app_id="all"): 15 | """list app, if none specified all return""" 16 | url = self.url 17 | if app_id != 'all': 18 | url = url + "/" + app_id 19 | return self._get_data(url) 20 | 21 | def create_app(self, name=None, app_store_id=None, bundle_id=None, binary=None): 22 | """upload an app binary""" 23 | data = {} 24 | files = {} 25 | if name: 26 | data['name'] = name 27 | if app_store_id: 28 | data['app_store_id'] = app_store_id 29 | elif bundle_id: 30 | data['bundle_id'] = bundle_id 31 | elif binary: 32 | files['binary'] = open(binary, 'rb') 33 | return self._post_data(self.url, data, files) 34 | 35 | def update_app(self, app_id, binary=None, name=None): 36 | """update an apps info binary etc""" 37 | url = self.url + "/" + app_id 38 | data = {} 39 | files = {} 40 | if name: 41 | data['name'] = name 42 | if binary: 43 | files['binary'] = open(binary, 'rb') 44 | return self._patch_data(url, data, files) 45 | 46 | def delete_app(self, app_id): 47 | """delete an app""" 48 | url = self.url + "/" + app_id 49 | data = {} 50 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 51 | -------------------------------------------------------------------------------- /SimpleMDMpy/AssignmentGroups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """assignment groups module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class AssignmentGroups(SimpleMDMpy.SimpleMDM.Connection): 9 | """assignment groups module for SimpleMDMpy""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/assignment_groups") 13 | 14 | def get_assignment_groups(self, assignment_group_id="all"): 15 | """returns assignment group(s), defaults to all if none specified""" 16 | url = self.url 17 | if assignment_group_id != 'all': 18 | url = url + "/" + assignment_group_id 19 | return self._get_data(url) 20 | 21 | def create_assignment_group(self, name, auto_deploy): 22 | """creates an assignment group""" 23 | url = self.url 24 | data = {'name': name, 'auto_deploy': auto_deploy} 25 | return self._post_data(url, data) 26 | 27 | def update_assignment_group(self, assignment_group_id, name, auto_deploy): 28 | """updates an assignment group""" 29 | url = self.url + "/" + assignment_group_id 30 | data = {'name': name, 'auto_deploy': auto_deploy} 31 | return self._patch_data(url, data) 32 | 33 | def delete_assignment_group(self, assignment_group_id): 34 | """delete an app to an assignment group""" 35 | url = self.url + "/" + assignment_group_id 36 | return self._delete_data(url) 37 | 38 | def assign_app(self, assignment_group_id, app_id): 39 | """assign app to an assignment group""" 40 | url = self.url + "/" + assignment_group_id + "/apps/" + app_id 41 | data = {} 42 | return self._post_data(url, data) 43 | 44 | def unassign_app(self, assignment_group_id, app_id): 45 | """unassign app to an assignment group""" 46 | url = self.url + "/" + assignment_group_id + "/apps/" + app_id 47 | return self._delete_data(url) 48 | 49 | def assign_device_group(self, assignment_group_id, device_group_id): 50 | """assigns a device group""" 51 | url = self.url + "/" + assignment_group_id + "/device_groups/" + device_group_id 52 | data = {} 53 | return self._post_data(url, data) 54 | 55 | def unassign_device_group(self, assignment_group_id, device_group_id): 56 | """delete a device group assignment""" 57 | url = self.url + "/" + assignment_group_id + "/device_groups/" + device_group_id 58 | return self._delete_data(url) 59 | 60 | def assign_device(self, assignment_group_id, device_id): 61 | """assign device to an assignment group""" 62 | url = self.url + "/" + assignment_group_id + "/devices/" + str(device_id) 63 | data = {} 64 | return self._post_data(url, data) 65 | 66 | def unassign_device(self, assignment_group_id, device_id): 67 | """unassign device to an assignment group""" 68 | url = self.url + "/" + assignment_group_id + "/devices/" + str(device_id) 69 | return self._delete_data(url) 70 | 71 | def push_apps(self, assignment_group_id): 72 | """push apps in an assignment group""" 73 | url = self.url + "/" + assignment_group_id + "/push_apps" 74 | data = {} 75 | return self._post_data(url, data) 76 | 77 | def update_apps(self, assignment_group_id): 78 | """update apps in an assignment group""" 79 | url = self.url + "/" + assignment_group_id + "/update_apps" 80 | data = {} 81 | return self._post_data(url, data) 82 | -------------------------------------------------------------------------------- /SimpleMDMpy/CustomAttributes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """custom attributes module""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class CustomAttributes(SimpleMDMpy.SimpleMDM.Connection): 9 | """work with custom attributes""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/custom_attributes") 13 | 14 | def get_custom_attributes(self): 15 | """lists all custom attributes""" 16 | url = self.url 17 | data = {} 18 | return self._get_data(url, data) 19 | 20 | def create_custom_attribute(self, name, default_value=None): 21 | """create custom attribute""" 22 | url = self.url 23 | data = {'name': name, 'default_value': default_value} 24 | return self._post_data(url, data) 25 | 26 | def update_custom_attribute(self, custom_attribute_id, default_value): 27 | """create custom attribute""" 28 | url = self.url + "/" + custom_attribute_id 29 | data = {'default_value': default_value} 30 | return self._put_data(url, data) 31 | 32 | def delete_custom_attribute(self, custom_attribute_id): 33 | """deletes custom attribute""" 34 | url = self.url + "/" + custom_attribute_id 35 | return self._delete_data(url) 36 | -------------------------------------------------------------------------------- /SimpleMDMpy/CustomConfigurationProfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Custom Configuration Profiles module """ 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class CustomConfigurationProfiles(SimpleMDMpy.SimpleMDM.Connection): 9 | """work with custom profiles""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/custom_configuration_profiles") 13 | 14 | def get_profiles(self): 15 | """returns profiles""" 16 | url = self.url 17 | return self._get_data(url) 18 | 19 | def create_profile(self, name, mobileconfig, user_scope=None, attribute_support=False): 20 | """upload a config file""" 21 | url = self.url 22 | data = {'name': name} 23 | files = {'mobileconfig': open(mobileconfig, 'rb')} 24 | if user_scope: 25 | data['user_scope'] = user_scope 26 | if attribute_support: 27 | data['attribute_support'] = attribute_support 28 | return self._post_data(url, data, files) 29 | 30 | def update_profile(self, profile_id, name=None, mobileconfig=None, # pylint: disable=too-many-arguments 31 | user_scope=None, attribute_support=None): 32 | """update a config file""" 33 | url = self.url + "/" + profile_id 34 | data = {} 35 | files = {} 36 | if name: 37 | data['name'] = name 38 | if mobileconfig: 39 | files['mobileconfig'] = open(mobileconfig, 'rb') 40 | if user_scope: 41 | data['user_scope'] = user_scope 42 | if attribute_support: 43 | data['attribute_support'] = attribute_support 44 | return self._patch_data(url, data, files) 45 | 46 | 47 | def delete_profile(self, profile_id): 48 | """deletes custom profile""" 49 | url = self.url + "/" + profile_id 50 | return self._delete_data(url) 51 | 52 | def download_profile(self, profile_id): 53 | """downloads custom profile""" 54 | url = self.url + "/" + profile_id + "/download/" 55 | return self._get_xml(url) 56 | 57 | def assign_to_device_group(self, profile_id, device_group_id): 58 | """assigns custom profile to group""" 59 | url = self.url + "/" + profile_id + "/device_groups/" + device_group_id 60 | data = {} 61 | return self._post_data(url, data) 62 | 63 | def unassign_from_device_group(self, profile_id, device_group_id): 64 | """deletes profile from device group""" 65 | url = self.url + "/" + profile_id + "/device_groups/" + device_group_id 66 | return self._delete_data(url) 67 | -------------------------------------------------------------------------------- /SimpleMDMpy/DepServers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """module for interacting with dep server configurations""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class DepServers(SimpleMDMpy.SimpleMDM.Connection): 9 | """module for interacting with dep server configurations""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/dep_servers") 13 | 14 | def get_dep_servers(self, dep_server_id="all"): 15 | """returns dep servers, defaults to all if none specified""" 16 | url = self.url 17 | if dep_server_id != 'all': 18 | url = url + "/" + dep_server_id 19 | return self._get_data(url) 20 | 21 | def sync_dep_servers(self, dep_server_id): 22 | """syncs specified server with Apple DEP""" 23 | url = self.url + "/" + dep_server_id + "/sync" 24 | data = {} 25 | return self._post_data(url, data) 26 | 27 | def get_dep_devices(self, dep_server_id, dep_device_id="all"): 28 | """return a DEP device via an ID, defaults to all if none specified""" 29 | url = self.url + "/" + dep_server_id + "/dep_devices" 30 | if dep_device_id != 'all': 31 | url + "/" + dep_device_id # pylint: disable=W0104 32 | data = {} 33 | return self._get_data(url, data) 34 | -------------------------------------------------------------------------------- /SimpleMDMpy/DeviceGroups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """device groups module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class DeviceGroups(SimpleMDMpy.SimpleMDM.Connection): 9 | """device groups module for SimpleMDMpy""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/device_groups") 13 | 14 | def get_device_group(self, device_group_id="all"): 15 | """get a devices group""" 16 | url = self.url 17 | if device_group_id != 'all': 18 | url = url + "/" + device_group_id 19 | return self._get_data(url) 20 | 21 | def assign_device(self, device_id, device_group_id): 22 | """assign device to a group""" 23 | url = self.url + "/" + device_group_id + "/devices/" + str(device_id) 24 | data = {} 25 | return self._post_data(url, data) 26 | -------------------------------------------------------------------------------- /SimpleMDMpy/Devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """devices module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class Devices(SimpleMDMpy.SimpleMDM.Connection): 9 | """devices module for SimpleMDMpy""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/devices") 13 | 14 | def get_device(self, device_id="all", search=None, include_awaiting_enrollment=False): 15 | """ 16 | Returns a device specified by id. If no ID or search is specified all 17 | devices will be returned. 18 | 19 | Args: 20 | device_id (str, optional): Returns a dictionary of the specified 21 | device id. By default, it returns a list of all devices. If a 22 | device_id and search is specified, then search will be ignored. 23 | search (str, optional): Returns a list of devices that match the 24 | search criteria. Defaults to None. Ignored if device_id is set. 25 | include_awaiting_enrollment (bool, optional): Returns a list of all 26 | devices including devices in the "awaiting_enrollment" state. 27 | 28 | Returns: 29 | dict: A single dictionary object with device information. 30 | array: An array of dictionary objects with device information. 31 | """ 32 | url = self.url 33 | params = {'include_awaiting_enrollment': include_awaiting_enrollment} 34 | # if a device ID is specified, then ignore any searches 35 | if device_id != 'all': 36 | url = url + "/" + str(device_id) 37 | elif search: 38 | params['search'] = search 39 | return self._get_data(url, params) 40 | 41 | def create_device(self, name, group_id): 42 | """Creates a new device object in SimpleMDM. The response 43 | body includes an enrollment URL that can be used once to 44 | enroll a physical device.""" 45 | data = {'name': name, 'group_id': group_id} 46 | return self._post_data(self.url, data) 47 | 48 | def update_device(self, device_id, name=None, device_name=None): 49 | """Update the SimpleMDM name and/or device name of a device object.""" 50 | url = self.url + "/" + str(device_id) 51 | data = {} 52 | if name is not None: 53 | data.update({'name':name}) 54 | if device_name is not None: 55 | data.update({'device_name':device_name}) 56 | if data == {}: 57 | raise Exception(f"Missing name and/or device_name variables.") 58 | return self._patch_data(url, data) 59 | 60 | def delete_device(self, device_id): 61 | """Unenroll a device and remove it from the account.""" 62 | url = self.url + "/" + str(device_id) 63 | return self._delete_data(url) #pylint: disable=too-many-function-args 64 | 65 | def list_profiles(self, device_id): 66 | """Returns a listing of profiles that are directly assigned to the device.""" 67 | url = self.url + "/" + str(device_id) + "/profiles" 68 | return self._get_data(url) 69 | 70 | def list_installed_apps(self, device_id): 71 | """Returns a listing of the apps installed on a device.""" 72 | url = self.url + "/" + str(device_id) + "/installed_apps" 73 | return self._get_data(url) 74 | 75 | def list_users(self, device_id): 76 | """Returns a listing of the user accounts on a device.""" 77 | url = self.url + "/" + str(device_id) + "/users" 78 | return self._get_data(url) 79 | 80 | def push_apps_device(self, device_id): 81 | """You can use this method to push all assigned apps 82 | to a device that are not already installed.""" 83 | url = self.url + "/" + str(device_id) + "/push_apps" 84 | data = {} 85 | return self._post_data(url, data) 86 | 87 | def restart_device(self, device_id): 88 | """This command sends a restart command to the device.""" 89 | url = self.url + "/" + str(device_id) + "/restart" 90 | data = {} 91 | return self._post_data(url, data) 92 | 93 | def shutdown_device(self, device_id): 94 | """This command sends a shutdown command to the device.""" 95 | url = self.url + "/" + str(device_id) + "/shutdown" 96 | data = {} 97 | return self._post_data(url, data) 98 | 99 | def lock_device(self, device_id, message, phone_number, pin=None): 100 | """You can use this method to lock a device and optionally display 101 | a message and phone number. The device can be unlocked with the 102 | existing passcode of the device.""" 103 | url = self.url + "/" + str(device_id) + "/lock" 104 | data = {'message': message, 'phone_number': phone_number, 'pin':pin} 105 | return self._post_data(url, data) 106 | 107 | def clear_passcode_device(self, device_id): 108 | """You can use this method to unlock and remove the passcode of a device.""" 109 | url = self.url + "/" + str(device_id) + "/clear_passcode" 110 | data = {} 111 | return self._post_data(url, data) 112 | 113 | def clear_firmware_password(self, device_id): 114 | """You can use this method to remove the firmware password from a device. 115 | The firmware password must have been originally set using SimpleMDM for 116 | this to complete successfully.""" 117 | url = self.url + "/" + str(device_id) + "/clear_firmware_password" 118 | data = {} 119 | return self._post_data(url, data) 120 | 121 | def wipe_device(self, device_id): 122 | """You can use this method to erase all content and settings stored on a 123 | device. The device will be unenrolled from SimpleMDM and returned to a 124 | factory default configuration.""" 125 | url = self.url + "/" + str(device_id) + "/wipe" 126 | data = {} 127 | return self._post_data(url, data) 128 | 129 | def update_os(self, device_id): 130 | """You can use this method to update a device to the latest OS version. 131 | Currently supported by iOS devices only.""" 132 | url = self.url + "/" + str(device_id) + "/update_os" 133 | data = {} 134 | return self._post_data(url, data) 135 | 136 | def enable_remote_desktop(self, device_id): 137 | """You can use this method to enable remote desktop. Supported by macOS 10.14.4+ devices only.""" 138 | url = self.url + "/" + str(device_id) + "/remote_desktop" 139 | data = {} 140 | return self._post_data(url, data) 141 | 142 | def disable_remote_desktop(self, device_id): 143 | """You can use this method to disable remote desktop. Supported by macOS 10.14.4+ devices only.""" 144 | url = self.url + "/" + str(device_id) + "/remote_desktop" 145 | return self._delete_data(url) 146 | 147 | def refresh_device(self, device_id): 148 | """Request a refresh of the device information and app inventory. 149 | SimpleMDM will update the inventory information when the device responds 150 | to the request.""" 151 | url = self.url + "/" + str(device_id) + "/refresh" 152 | data = {} 153 | return self._post_data(url, data) 154 | 155 | def get_custom_attributes(self, device_id): 156 | """Get all custom attributes for a device.""" 157 | url = self.url + "/" + str(device_id) + "/custom_attribute_values" 158 | data = {} 159 | return self._get_data(url, data) 160 | 161 | def get_custom_attribute(self, device_id, custom_attribute_name): 162 | """Get a specific custom attribute for a device.""" 163 | url = self.url + "/" + str(device_id) + "/custom_attribute_values/" + custom_attribute_name 164 | data = {} 165 | return self._get_data(url, data) 166 | 167 | def set_custom_attribute(self, value, device_id, custom_attribute_name): 168 | """Set a custom attribute value.""" 169 | url = self.url + "/" + str(device_id) + "/custom_attribute_values/" + custom_attribute_name 170 | data = {'value': value} 171 | return self._put_data(url, data) 172 | -------------------------------------------------------------------------------- /SimpleMDMpy/Enrollments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """enrollments module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class Enrollments(SimpleMDMpy.SimpleMDM.Connection): 9 | """enrollments module for SimpleMDMpy""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/enrollments") 13 | 14 | def get_enrollments(self, enrollment_id="all"): 15 | """get a devices group""" 16 | url = self.url 17 | if enrollment_id != 'all': 18 | url = url + "/" + enrollment_id 19 | data = {} 20 | return self._get_data(url, data) 21 | 22 | def send_invitation(self, enrollment_id, contact): 23 | """Send an enrollment invitation to an email address or phone number.""" 24 | url = self.url + "/" + enrollment_id + "/invitations" 25 | data = {'contact': contact} 26 | return self._post_data(url, data) 27 | 28 | def delete_enrollment(self, enrollment_id): 29 | """delete enrollment""" 30 | url = self.url + "/" + enrollment_id 31 | data = {} 32 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 33 | -------------------------------------------------------------------------------- /SimpleMDMpy/InstalledApps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """installed apps module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class InstalledApps(SimpleMDMpy.SimpleMDM.Connection): 9 | """Installed apps represent apps that are installed and exist on devices.""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/installed_apps") 13 | 14 | def get_app(self, installed_app_id): 15 | """retrieve an installed app""" 16 | url = self.url + "/" + installed_app_id 17 | return self._get_data(url) 18 | 19 | def update(self, installed_app_id): 20 | """This submits a request to the device to update the specified app to 21 | the latest version. The app must be managed for this request to succeed.""" 22 | url = self.url + "/" + installed_app_id 23 | return self._get_data(url) 24 | 25 | def delete_app(self, installed_app_id): 26 | """This submits a request to the device to uninstall the specified app. 27 | The app must be managed for this request to succeed.""" 28 | url = self.url + "/" + installed_app_id 29 | return self._delete_data(url) 30 | -------------------------------------------------------------------------------- /SimpleMDMpy/Logs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Logs module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class Logs(SimpleMDMpy.SimpleMDM.Connection): 9 | """GET all the LOGS""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/logs") 13 | 14 | def get_logs(self, starting_after=None, limit=None): 15 | """Returns logs, and I mean all the LOGS 16 | 17 | Args: 18 | starting_after (str, optional): set to the id of the log object you 19 | want to start with. Defaults to the first object. 20 | limit (str, optional): A limit on the number of objects that will be 21 | returned per API call. Setting this will still return all logs. 22 | Defaults to 100. 23 | 24 | Returns: 25 | array: An array of dictionary log objects. 26 | """ 27 | url = self.url 28 | params = {} 29 | if starting_after: 30 | params['starting_after'] = starting_after 31 | if limit: 32 | params['limit'] = limit 33 | return self._get_data(url, params=params) 34 | -------------------------------------------------------------------------------- /SimpleMDMpy/LostMode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Lost Mode module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class LostMode(SimpleMDMpy.SimpleMDM.Connection): 9 | """Interact with lost mode on a device.""" 10 | def __init__(self, api_key): 11 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 12 | self.url = self._url("/devices") 13 | 14 | def enable(self, device_id): 15 | """Activate lost mode on a device.""" 16 | url = self.url + "/" + str(device_id) + "/lost_mode" 17 | data = {} 18 | return self._post_data(url, data) 19 | 20 | def disable(self, device_id): 21 | """Disable lost mode on a device.""" 22 | url = self.url + "/" + str(device_id) + "/lost_mode" 23 | return self._delete_data(url) 24 | 25 | def play_sound(self, device_id): 26 | """Request that the device play a sound to assist 27 | with locating it.""" 28 | url = self.url + "/" + str(device_id) + "/lost_mode/play_sound" 29 | data = {} 30 | return self._post_data(url, data) 31 | 32 | def update_location(self, device_id): 33 | """Request that the device provide its current, 34 | up-to-date location. Location data can be viewed 35 | using the devices endpoint.""" 36 | url = self.url + "/" + str(device_id) + "/lost_mode/update_location" 37 | data = {} 38 | return self._post_data(url, data) 39 | -------------------------------------------------------------------------------- /SimpleMDMpy/ManagedAppConfigs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """managed app configs module for SimpleMDMpy""" 4 | #pylint: disable=invalid-name 5 | 6 | import SimpleMDMpy.SimpleMDM 7 | 8 | class ManagedAppConfigs(SimpleMDMpy.SimpleMDM.Connection): 9 | """Create, modify, and remove the managed app configuration 10 | associated with an app.""" 11 | def __init__(self, api_key): 12 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 13 | self.url = self._url("/apps") 14 | 15 | def get_managed_configs(self, app_id): 16 | """"Retrieve the managed configs for an app.""" 17 | url = self.url + "/" + app_id + "/managed_configs" 18 | data = {} 19 | return self._get_data(url, data) 20 | 21 | def push_updates(self, app_id): 22 | """Push any updates to the managed configurations 23 | for an app to all devices. This is not necessary 24 | when making managed config changes through the UI. 25 | This is necessary after making changes through the API.""" 26 | url = self.url + "/" + app_id + "/managed_configs/push" 27 | data = {} 28 | return self._post_data(url, data) 29 | 30 | def delete_config(self, app_id, managed_config_id): 31 | """Delete managed config from an app by ID.""" 32 | url = self.url + "/" + app_id + "/managed_configs/" + managed_config_id 33 | data = {} 34 | return self._delete_data(url, data) #pylint: disable=too-many-function-args 35 | -------------------------------------------------------------------------------- /SimpleMDMpy/PushCertificate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Methods related to the Apple Push Notification 4 | Certificate utilized by the account.""" 5 | #pylint: disable=invalid-name 6 | 7 | import SimpleMDMpy.SimpleMDM 8 | 9 | class PushCertificate(SimpleMDMpy.SimpleMDM.Connection): 10 | """Push cert module actions""" 11 | def __init__(self, api_key): 12 | SimpleMDMpy.SimpleMDM.Connection.__init__(self, api_key) 13 | self.url = self._url("/push_certificate") 14 | 15 | def getpush_certificate(self): 16 | """Show details related to the current push 17 | certificate being used.""" 18 | return self._get_data(self.url) 19 | 20 | def update_certificate(self, file, apple_id): 21 | """Upload a new certificate and replace the 22 | existing certificate for your account.""" 23 | files = {'file': open(file, 'rb')} 24 | data = {} 25 | if apple_id: 26 | data["apple_id"] = apple_id 27 | return self._put_data(self.url, data, files) 28 | 29 | def get_signed_csr(self): 30 | """Download a signed CSR file. This file is 31 | provided to Apple when creating and renewing a 32 | push certificate. The API returns a base64 33 | encoded plist for upload to the Apple Push 34 | Certificates Portal. The value of the data 35 | key can be uploaded to Apple as is""" 36 | return self._get_data(self.url + "/scsr") 37 | -------------------------------------------------------------------------------- /SimpleMDMpy/ScriptJobs.py: -------------------------------------------------------------------------------- 1 | """Scripts module for SimpleMDMpy""" 2 | 3 | 4 | from SimpleMDMpy.SimpleMDM import Connection, ApiError 5 | 6 | 7 | class ScriptJobs(Connection): 8 | """scripts module for SimpleMDMpy""" 9 | def __init__(self, api_key): 10 | Connection.__init__(self, api_key) 11 | self.url = self._url("/script_jobs") 12 | 13 | def get_job(self, job_id="all"): 14 | """Jobs represent scripts that have been set to run on a collection of 15 | devices. Jobs remain listed for one month. 16 | 17 | Args: 18 | job_id (int, optional): Returns a dictionary of the specified job 19 | id. By default, it returns a list of all jobs. 20 | 21 | Returns: 22 | dict: A single dictionary object with job information. 23 | array: An array of dictionary objects with job information. 24 | """ 25 | url = self.url 26 | if job_id != 'all': 27 | url = url + "/" + str(job_id) 28 | return self._get_data(url) 29 | 30 | def create_job(self, script_id, device_ids=None, group_ids=None, assignment_group_ids=None): 31 | """ 32 | You can use this method to upload a new script to your account. 33 | 34 | Args: 35 | script_id (int): Required. The ID of the script to be run on the devices 36 | device_ids (list of ints, optional): A list of device IDs to run 37 | the script on 38 | group_ids (list of ints, optional): A list of group IDs to run the 39 | script on. All macOS devices from these groups will be included. 40 | assignment_group_ids (list of ints, optional): A comma separated 41 | list of assignment group IDs to run the script on. All macOS 42 | devices from these assignment groups will be included. 43 | 44 | Returns: 45 | dict: A dictionary object with job information. 46 | """ 47 | params = {} 48 | if device_ids is not None: 49 | params['device_ids'] = ",".join(str(x) for x in device_ids) 50 | if group_ids is not None: 51 | params['group_ids'] = ",".join(str(x) for x in group_ids) 52 | if assignment_group_ids is not None: 53 | params['assignment_group_ids'] = ",".join(str(x) for x in assignment_group_ids) 54 | if not params: 55 | raise ApiError(f"At least one of device_ids, group_ids, or assignment_group_ids must be provided") 56 | params['script_id'] = str(script_id) 57 | resp = self._post_data(self.url, params) 58 | if not 200 <= resp.status_code <= 207: 59 | raise ApiError(f"Job creation failed with status code {resp.status_code}: {resp.content}") 60 | return resp.json()['data'] 61 | 62 | def cancel_job(self, job_id): 63 | """ 64 | You can use this method delete cancel a job. Jobs can only be canceled 65 | before the device has received the command. 66 | """ 67 | url = self.url + "/" + str(job_id) 68 | return self._delete_data(url) 69 | -------------------------------------------------------------------------------- /SimpleMDMpy/Scripts.py: -------------------------------------------------------------------------------- 1 | """Scripts module for SimpleMDMpy""" 2 | 3 | 4 | from SimpleMDMpy.SimpleMDM import Connection, ApiError 5 | 6 | 7 | class Scripts(Connection): 8 | """scripts module for SimpleMDMpy""" 9 | def __init__(self, api_key): 10 | Connection.__init__(self, api_key) 11 | self.url = self._url("/scripts") 12 | 13 | def get_script(self, script_id="all"): 14 | """ 15 | Returns a listing of all scripts in the account, or the script 16 | specified by id. 17 | 18 | Args: 19 | script_id (int, optional): Returns a dictionary of the specified 20 | script id. By default, it returns a list of all scripts. 21 | 22 | Returns: 23 | dict: A single dictionary object with script information. 24 | array: An array of dictionary objects with script information. 25 | """ 26 | url = self.url 27 | if script_id != 'all': 28 | url = url + "/" + str(script_id) 29 | return self._get_data(url) 30 | 31 | def create_script(self, name, variable_support, content): 32 | """ 33 | You can use this method to upload a new script to your account. 34 | 35 | Args: 36 | name (str): The name for the script. This is how it will appear 37 | in the Admin UI. 38 | variable_support (bool): Whether or not to enable variable support 39 | in this script. 40 | content (str): The script content. All scripts must begin with a 41 | valid shebang such as #!/bin/sh to be processed. 42 | """ 43 | params = { 44 | 'name': name, 45 | 'variable_support': "1" if variable_support else "0", 46 | } 47 | files = { 48 | 'file': ('script.sh', content) 49 | } 50 | resp = self._post_data(self.url, params, files) 51 | if not 200 <= resp.status_code <= 207: 52 | raise ApiError(f"Script creation failed with status code {resp.status_code}: {resp.content}") 53 | return resp.json()['data'] 54 | 55 | def update_script(self, script_id, name=None, variable_support=None, content=None): 56 | """ 57 | You can use this method to update an existing script in your account. 58 | Any existing Script Jobs will not be changed. 59 | """ 60 | url = self.url + "/" + str(script_id) 61 | params = {} 62 | files = None 63 | if name is not None: 64 | params['name'] = name 65 | if variable_support is not None: 66 | params['variable_support'] = "1" if variable_support else "0" 67 | if content is not None: 68 | files = { 69 | 'file': ('script.sh', content) 70 | } 71 | if not params and not files: 72 | raise ApiError(f"Missing updated variables.") 73 | resp = self._patch_data(url, params, files) 74 | if not 200 <= resp.status_code <= 207: 75 | raise ApiError(f"Script update failed with status code {resp.status_code}: {resp.content}") 76 | return resp.json()['data'] 77 | 78 | def delete_script(self, script_id): 79 | """You can use this method to delete a script from your account. Any 80 | existing Script Jobs will not be changed.""" 81 | url = self.url + "/" + str(script_id) 82 | return self._delete_data(url) 83 | -------------------------------------------------------------------------------- /SimpleMDMpy/SimpleMDM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """base module for calling simplemdm api""" 4 | #pylint: disable=invalid-name 5 | 6 | from builtins import str 7 | from builtins import range 8 | from builtins import object 9 | import requests 10 | from requests.adapters import HTTPAdapter 11 | from requests.packages.urllib3.util.retry import Retry 12 | import time 13 | 14 | 15 | class ApiError(Exception): 16 | """Catch for API Error""" 17 | pass 18 | 19 | class Connection(object): #pylint: disable=old-style-class,too-few-public-methods 20 | """create connection with api key""" 21 | proxyDict = dict() 22 | 23 | last_device_req_timestamp = 0 24 | device_req_rate_limit = 1.0 25 | 26 | def __init__(self, api_key): 27 | self.api_key = api_key 28 | # setup a session that can retry, helps with rate limiting end-points 29 | # https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/#retry-on-failure 30 | # https://macadmins.slack.com/archives/C4HJ6U742/p1652996411750219 31 | retry_strategy = Retry( 32 | total = 5, 33 | backoff_factor = 1, 34 | status_forcelist = [500, 502, 503, 504], 35 | ) 36 | adapter = HTTPAdapter(max_retries=retry_strategy) 37 | self.session = requests.Session() 38 | self.session.mount("https://", adapter) 39 | self.session.mount("http://", adapter) 40 | 41 | def __del__(self): 42 | # this runs when the Connection object is being deinitialized 43 | # this properly closes the session 44 | self.session.close() 45 | 46 | def _url(self, path): #pylint: disable=no-self-use 47 | """base api url""" 48 | return 'https://a.simplemdm.com/api/v1' + path 49 | 50 | # TODO: make _is_devices_req generic for any future rate limited endpoints 51 | def _is_devices_req(self, url): 52 | return url.startswith(self._url("/devices")) 53 | 54 | def _get_data(self, url, params=None): 55 | """GET call to SimpleMDM API""" 56 | has_more = True 57 | list_data = [] 58 | # by using the local req_params variable, we can set our own defaults if 59 | # the parameters aren't included with the input params. This is needed 60 | # so that certain other functions, like Logs.get_logs(), can send custom 61 | # starting_after and limit parameters. 62 | if params is None: 63 | req_params = {} 64 | else: 65 | req_params = params.copy() 66 | req_params['limit'] = req_params.get('limit', 100) 67 | while has_more: 68 | # Calls to /devices should be rate limited 69 | if self._is_devices_req(url): 70 | seconds_since_last_device_req = time.monotonic() - self.last_device_req_timestamp 71 | if seconds_since_last_device_req < self.device_req_rate_limit: 72 | time.sleep(self.device_req_rate_limit - seconds_since_last_device_req) 73 | self.last_device_req_timestamp = time.monotonic() 74 | while True: 75 | resp = self.session.get(url, params=req_params, auth=(self.api_key, ""), proxies=self.proxyDict) 76 | # A 429 means we've hit the rate limit, so back off and retry 77 | if resp.status_code == 429: 78 | time.sleep(1) 79 | else: 80 | break 81 | if not 200 <= resp.status_code <= 207: 82 | raise ApiError(f"API returned status code {resp.status_code}") 83 | resp_json = resp.json() 84 | data = resp_json['data'] 85 | # If the response isn't a list, return the single item. 86 | if not isinstance(data, list): 87 | return data 88 | # If it's a list we save it and see if there is more data coming. 89 | list_data.extend(data) 90 | has_more = resp_json.get('has_more', False) 91 | if has_more: 92 | req_params["starting_after"] = data[-1].get('id') 93 | return list_data 94 | 95 | def _get_xml(self, url, params=None): 96 | """GET call to SimpleMDM API""" 97 | resp = requests.get(url, params, auth=(self.api_key, ""), proxies=self.proxyDict) 98 | return resp.content 99 | 100 | def _patch_data(self, url, data, files=None): 101 | """PATCH call to SimpleMDM API""" 102 | resp = requests.patch(url, data, auth=(self.api_key, ""), \ 103 | files=files, proxies=self.proxyDict) 104 | return resp 105 | 106 | def _post_data(self, url, data, files=None): 107 | """POST call to SimpleMDM API""" 108 | resp = requests.post(url, data, auth=(self.api_key, ""), \ 109 | files=files, proxies=self.proxyDict) 110 | return resp 111 | 112 | def _put_data(self, url, data, files=None): 113 | """PUT call to SimpleMDM API""" 114 | resp = requests.put(url, data, auth=(self.api_key, ""), \ 115 | files=files, proxies=self.proxyDict) 116 | return resp 117 | 118 | def _delete_data(self, url): 119 | """DELETE call to SimpleMDM API""" 120 | return requests.delete(url, auth=(self.api_key, ""), proxies=self.proxyDict) 121 | -------------------------------------------------------------------------------- /SimpleMDMpy/__init__.py: -------------------------------------------------------------------------------- 1 | """ SimpleMDMpy - A python API for interacting with SimpleMDM API. 2 | Your Simple MDM API is required.""" 3 | #pylint: disable=invalid-name 4 | from SimpleMDMpy.Account import Account 5 | from SimpleMDMpy.AppGroups import AppGroups 6 | from SimpleMDMpy.Apps import Apps 7 | from SimpleMDMpy.AssignmentGroups import AssignmentGroups 8 | from SimpleMDMpy.CustomAttributes import CustomAttributes 9 | from SimpleMDMpy.CustomConfigurationProfiles import CustomConfigurationProfiles 10 | from SimpleMDMpy.DepServers import DepServers 11 | from SimpleMDMpy.DeviceGroups import DeviceGroups 12 | from SimpleMDMpy.Devices import Devices 13 | from SimpleMDMpy.Enrollments import Enrollments 14 | from SimpleMDMpy.InstalledApps import InstalledApps 15 | from SimpleMDMpy.Logs import Logs 16 | from SimpleMDMpy.LostMode import LostMode 17 | from SimpleMDMpy.ManagedAppConfigs import ManagedAppConfigs 18 | from SimpleMDMpy.PushCertificate import PushCertificate 19 | from SimpleMDMpy.ScriptJobs import ScriptJobs 20 | from SimpleMDMpy.Scripts import Scripts 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = SimpleMDMpy 3 | version = 3.0.7 4 | author = Steve Küng 5 | maintainer = MacAdmins 6 | description = A Python Library for SimpleMDM. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = MIT 10 | license_file = LICENSE 11 | url = https://github.com/macadmins/simpleMDMpy 12 | classifiers = 13 | Development Status :: 3 - Alpha 14 | Intended Audience :: System Administrators 15 | License :: OSI Approved :: MIT License 16 | Operating System :: OS Independent 17 | Programming Language :: Python :: 3 18 | 19 | [options] 20 | packages = SimpleMDMpy 21 | install_requires = requests 22 | -------------------------------------------------------------------------------- /tests/readme.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | 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 4 | 5 | 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. 6 | 7 | ## Testing Steps 8 | - Duplicate `settings-sample.py` as `settings.py` and fill in the variables 9 | - Create virtual environments folder if it doesn't already exist `mkdir ~/.env` 10 | - Create the virtual env `python3 -m venv ~/.env/smdm-tests` 11 | - Activate the environment `source ~/.env/smdm-tests/bin/activate` 12 | - 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+) 13 | - Run the tests `python3 -m unittest discover -s /path/to/cloned/simpleMDMpy/tests` 14 | - Uninstall the test module `pip uninstall SimpleMDMpy` 15 | - Deactivate the Python virtual environment `deactivate` 16 | -------------------------------------------------------------------------------- /tests/settings-sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # add a SimpleMDM API key to use for tests 4 | api_key = '' 5 | 6 | # add the ID of a profile in your instance to test profile functions 7 | profile_id = '' # note what profile this is 8 | 9 | # add the id of a device in your instance to test functions against 10 | device_id = '' # note what device this is 11 | -------------------------------------------------------------------------------- /tests/test_Account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import settings 4 | import unittest 5 | import SimpleMDMpy 6 | 7 | 8 | class TestCustomConfigurationProfiles(unittest.TestCase): 9 | def test_get_account_details(self): 10 | account_details = SimpleMDMpy.Account(settings.api_key) \ 11 | .get_account_details() 12 | account_name = account_details.get('attributes', {}).get('name') 13 | self.assertIsNotNone(account_name) 14 | 15 | if __name__ == '__main__': 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /tests/test_CustomConfigurationProfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import plistlib 4 | import settings 5 | import unittest 6 | import SimpleMDMpy 7 | 8 | 9 | @unittest.skipIf(settings.profile_id == '', 10 | "profile_id not specified in settings.py.") 11 | class TestCustomConfigurationProfiles(unittest.TestCase): 12 | def test_get_profile(self): 13 | profile_found = False 14 | all_profiles = SimpleMDMpy.CustomConfigurationProfiles( 15 | settings.api_key).get_profiles() 16 | for prof in all_profiles: 17 | prof_id = str(prof.get('id', '')) 18 | if prof_id == settings.profile_id: profile_found = True 19 | self.assertTrue(profile_found) 20 | 21 | def test_download_profile(self): 22 | # if settings.profile_id == '': 23 | profile = SimpleMDMpy.CustomConfigurationProfiles(settings.api_key) \ 24 | .download_profile(settings.profile_id) 25 | self.assertIsInstance(plistlib.loads(profile), dict) 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/test_Devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import settings 3 | import unittest 4 | import SimpleMDMpy 5 | 6 | class TestDevices(unittest.TestCase): 7 | def test_get_device(self): 8 | all_devices = SimpleMDMpy.Devices( 9 | settings.api_key).get_device(include_awaiting_enrollment=True) 10 | self.assertGreaterEqual(len(all_devices), 1) 11 | # print(len(all_devices)) 12 | cid = all_devices[0].get('id') 13 | self.assertIsNotNone(cid) 14 | single_device = SimpleMDMpy.Devices(settings.api_key) \ 15 | .get_device(device_id=cid) 16 | self.assertEqual(single_device.get('id'), cid) 17 | 18 | def test_list_profiles(self): 19 | device_profiles = SimpleMDMpy.Devices(settings.api_key) \ 20 | .list_profiles(settings.device_id) 21 | self.assertGreaterEqual(len(device_profiles), 1) 22 | 23 | def test_list_users(self): 24 | device_users = SimpleMDMpy.Devices(settings.api_key) \ 25 | .list_users(settings.device_id) 26 | self.assertGreaterEqual(len(device_users), 1) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | --------------------------------------------------------------------------------