Working with the SimpleMDM API

27 Mar 2018

Baby steps…

We’ve been evaluating SimpleMDM to manage our Macs at ThoughtWorks. It seems a pretty decent fit for a number of reasons - namely:

  • Agentless - it does what it does using Apple’s native management APIs
  • Low cost - which is nice
  • SaaS - it’s cloud hosted, so we don’t have to run any internal Services
  • It has a well documented API.

Filevault fail?

There are all kinds of exciting things we can probably do with their API - but first up we had a real-world problem to solve. One of our primary drivers for moving to MDM is the promise of zero-touch laptop setup via Apple’s Device Enrolment Programme (DEP) - but in my early testing, it appears when you push a Filevault profile during a DEP deployment, the profile is successfully pushed to the Mac - but is then effectively ignored.

I’d tested with 10.12 and 10.13, with real machines and virtual machines, but the bug was there and repeatable. Frustratingly if you changed any of the Filevault settings afterwards (causing the push of an updated profile) Filevault behaves as expected.

bugs…

After raising a support ticket with SimpleMDM, it turns out this is apparently a bug at Apple’s end… and the suggested workaround is to leave Filevault out of the initial DEP enrolment profile - but to dynamically move devices into a group that does enable filevault via the SimpleMDM api using some kind of script on the Mac, once it reaches a user session.

This does at least tie in with my current plan to use InstallApplications to be able to deploy more than one package or script dynamically post setup. But it still leaves me with a how?

The API docs provide examples in shell and ruby - but not in the language I currently prefer to mangle (read, “am trying to learn”) - but it should be easy enough to get started right? Plus I barely know any ruby - and it turns out there are other reasons to write something in python…

security?

The really big problem with just using the api via a script as a suggestion is - the API uses a key for authentication. At the moment SimpleMDM doesn’t allow for different levels of api access key - it’s all or nothing root level access… (There’s a improvement suggestion via their user portal asking for more granular api key access - do me a favour and vote it up)

I’m no infosec expert, but even I know that sticking that API key into a script that runs locally on each device is a daft thing to do. At which point my colleague suggested this would be a perfect use case for an Amazon Lamdba Function - Lamdba supports code in python, at which point prototyping locally on my Mac in python means that code should be a stepping stone to building My first Lambda.

Local code?

MacOS still ships with Python 2.7 - so that’s what we’re using here. Our interaction with SimpleMDM is going to be via http - so we’ll need a suitable python http library. The internet suggests we should use requests which we add to our code thus:

  import requests

And all we’re trying to do is move a device from one SimpleMDM group to another, using the SimpleMDM API and some python.

Problem one…

To do almost anything device-related in SimpleMDM you need to know a device’s ID - a laptop has no idea what it’s SimpleMDM ID is - so you need to submit a serial number to SimpleMDM - and then the API can return the device id - NOW we can do things with a device.

I borrowed a function from Frogor to look up serial numbers… but actually, initially just hard-coded the serial number value as I only had one device to test with.

SimpleMDM returns json - so we need to be able not only go and get some data, but to parse the data that gets returned. The code to do both of those things looks a bit like this:

def get_simplemdm_machine_id(serial_number):
  url = 'https://a.simplemdm.com/api/v1/devices?search={SERIAL_NUMBER}'.format(SERIAL_NUMBER=serial_number)
  laptop_data = requests.get(url, auth=(key, ''))
  json_laptop_data = laptop_data.json()
  try:
    simplemdm_machine_id = json_laptop_data['data'][0]['id']
    print('Laptop serial number {} has a SimpleMDM machine id of {}'.format(serial_number, simplemdm_machine_id))
    return simplemdm_machine_id
  except:
    print ('{} is not in SimpleMDM'.format(serial_number))
    return

we define our url as a variable containing the SimpleMDM API address with our chosen serial number appended to it. Then we can use the requests library to connect to the API endpoint. The key variable (defined elsewhere in the code) is our SimpleMDM API key. The data is returned as json - which we can turn into a structure that python can deal with by running it though the python json library - so json_laptop_data = laptop_data.json()

I’ve learned that me and json don’t really get on… the full record for a laptop is returned with this code:

print json_laptop_data

Which returns a complex python dictionary full of stuff. But I don’t need all that stuff, I just want the laptop id. I’d love to say this was quick and easy, but instead there was much trial and error (and Googling) and messing around with the json data structures to pull out an id. The code to do so looks like this

simplemdm_machine_id = json_laptop_data['data'][0]['id']

but that gets us the id we need to do the next stuff :)

Problem two, we have a device id, now what?

I’m glad you asked. Now we can query SimpleMDM to find out what group that device is currently in. The code to do so looks like this - and again, finding the group id took a lot of faffing…

def query_simplemdm_group(simplemdm_machine_id):
  url = 'https://a.simplemdm.com/api/v1/devices/{}'.format(simplemdm_machine_id)
  response = requests.get(url, auth=(key, ''))
  #print response
  json_response = response.json()
  try:
    simplemdm_group = json_response["data"]["relationships"]["device_group"]["data"]["id"]
    print ('Machine id {} is in SimpleMDM Group {}'.format(simplemdm_machine_id, simplemdm_group))
    return simplemdm_group
  except:
    print "no such group"
    return

Finding group ids…

Again - in the SimpleMDM GUI, all of the work you do with groups is via a Group name… but everything in the SimpleMDM API is via a group id. In my test environment, I don’t have many groups, so to be honest I can just use the example code via SimpleMDM’s site - return all the groups - and then pull out the group ids. Like this:

def list_all_groups():
  url = 'https://a.simplemdm.com/api/v1/device_groups'
  response = requests.get(url, auth=(key, ''))
  json_response = response.json()
  print json_response

Which produces output a bit like this:

{
    "data": [
        {
            "type": "device_group",
            "id": 37,
            "attributes": {
              "name": "Remote Employees"
            }
        },
        {
            "type": "device_group",
            "id": 38,
            "attributes": {
              "name": "Executives"
        }
  }
]
}

Although not formatted like that… I’ve tried to make this look prettier for you, dear reader! (which means I’ve probably mangled the syntax… either way this is an example from the SimpleMDM API reference page…)

I like to move it move it…

I know exactly where I want to move my devices, so I can write a function to do so:

def move_to_group(simplemdm_machine_id):
  url = 'https://a.simplemdm.com/api/v1/device_groups/[numeric_device_id]]/devices/{}'.format(simplemdm_machine_id)
  response = requests.post(url, auth=(key, ''))
  print ('Moving a machine with SimpleMDM ID {} to the Enable Filevault Group'.format(simplemdm_machine_id))
  return response

But replace [numeric group id] with one of the group ids you’ve pulled out via the list_all_groups(): function…

so from the example above, if I wanted to move a device to the Executives group my url variable would look like this:

url = 'https://a.simplemdm.com/api/v1/device_groups/37/devices/{}'.format(simplemdm_machine_id)

And we’re done…

That’ll do for now. I hope this is a useful guide to getting started with the SimpleMDM API and Python. Next time I’ll talk about my Lambda learning curve… (spoilers - it was pretty steep!)

I’ll stick some of my code examples in github too…

Published on 27 Mar 2018 Find me on Twitter and Mastodon.