Uncategorized

Generating Pre-Populated Munki Manifests Utilizing the AirWatch API

Introduction

While recently setting up Munki in our environment, I was doing some Googling, and asking for advice on how to handle client manifests. I saw several folks recommending the per-device/per-user manifest method (examples here and here), but from our point of view it was hard to see the need for per-client manifests as the majority of our software/patches are deployed to all equipment. But trusting that these folks knew what they were talking about, and knowing that they had used Munki for far longer than we had, we decided to trust the advice of those smarter than us. We chose to utilize the site_default manifest that Munki defaults to as our primary manifest that all general software would be assigned to. We then made it an included_manifest in the per-device manifests. This way, we would have the granularity to make certain pieces of software available to specific devices as needed, without having to assign every device every piece of standard software. And, if a client manifest somehow got deleted or was unavailable, Munki would default to site_default anyway and the client state really wouldn’t change much.

Once we decided to go this route, we wanted a way to automatically create and pre-populate these client manifests. The idea being that if a customer asked for a piece of software to be made available to their machine(s), we wouldn’t have to ask them their device serial number and go create the manifest, it would already exist. We also wanted a way to have some of the “pretty” information filled out, such as the DisplayName (which we would set to the device hostname) and User (which we would set to the Assigned User in AirWatch), since looking through 1000 manifests named after Serial Numbers isn’t as easy as looking for a customer’s name or device asset tag. The question then became “How can we gather this information in an automated fashion?” We have a property database that holds accountable users and their devices, so perhaps this is straightforward! Alas, it was not. We have several fellows and volunteers that work for us, and since they are not technically contractors or government employees, they cannot directly be “accountable” for a government asset. Typically, this results in their supervisor being listed as the accountable user. So while that option would work, it would not be very accurate, and would be harder to manage down the line.

TL;DR

If you just want to look at the script, it can be found as a Gist on my GitHub here: manifest_generator.py

AirWatch’s Robust API

AirWatch has an incredibly robust API built into their product. I mean, huge. Knowing this, I thought it should be pretty easy to get a device’s Assigned User, Serial Number, and Device Friendly Name (the device name in the AirWatch DB). Then, with the information pulled from AirWatch, we could create per-client Munki manifests.

Side Note: Before going further, if you are unfamiliar with the AirWatch API or how to use it/access it, I’d recommend checking out Ben Toms nice “Getting Started” post here. I originally wrote the script simply wrapping the curl command up, but as Ben points out, the requests library is awesome and made things incredibly easy. So if you are not already using it, I’d recommend installing that before trying to work with AirWatch’s API (at least when using Python).

Ok, so on to the actual API calls. We first wanted to start by just pulling all of the devices in AirWatch, and their associated information. A call like that would go something like this:


#!/usr/bin/python

import requests

###### AirWatch Variables ######
airwatch_server = '' ## Enter your AirWatch server here, for example https://airwatch.com
b64auth = '' ## Base 64 Encoded AirWatch Username and Password here
aw_tenant_code = '' ## Enter the AirWatch API Key from your server here
request_headers = {'aw-tenant-code':'%s' % aw_tenant_code, 'Accept':'application/json', 'Authorization':'Basic %s' % b64auth}
###### AirWatch Variables ######

all_devices = requests.get('%s/api/mdm/devices/search' % airwatch_server, headers=request_headers)

try:
    parsed_device_output = all_devices.json()
except ValueError:
    print "The API call failed."
    sys.exit()

This will return a parsed dictionary into the variable parsed_device_output for all of your currently enrolled devices, with which you can begin to access all of the juicy bits of info that AirWatch stores. From here, it should be very straightforward to extract the keys we want (Friendly Name, Assigned User, etc.) and use that to generate the Munki manifests for our devices.

The Upstaging “Default Staging User”

Due to the fact that we are migrating devices from our previous system into AirWatch, the vast majority of them are “Staged” because we are enrolling the devices into AirWatch by manually pushing the AirWatch Enrollment profile. Therefore, until our users reboot or logout, their devices are enrolled in AirWatch as the “Default Staging User.” Well so obviously that’s not what we want. We needed to add a little condition in our script to make sure that the enrolled user is in fact real. This would go something like this:


...

## We are going to extract the device fields we care about
## (SerialNumber, DeviceFriendlyName, etc.) from the
## "parsed_device_output" variable we got earlier and assign it to
## a new list called "airwatch_devices"

for i in xrange(0, len(parsed_device_output['Devices'])):
    client_dict = {}
    client_dict['SerialNumber'] = parsed_device_output['Devices'][i]['SerialNumber']
    client_dict['FriendlyName'] = parsed_device_output['Devices'][i]['DeviceFriendlyName']
    client_dict['AssetNumber'] = parsed_device_output['Devices'][i]['AssetNumber']
    client_dict['Username'] = parsed_device_output['Devices'][i]['UserName']
    airwatch_devices.append(client_dict)

for device in airwatch_devices:
    if 'Default Staging User' in device['Username']:
        print "Device [%s] has not been fully provisioned yet, skipping manifest creation" % device['SerialNumber']
...

One slight downside to this method is that it means that the client manifest wouldn’t be generated until the user information is updated, which as we know, some people don’t reboot very frequently, so it could take awhile. But it doesn’t matter much for us since the client manifest will just be inheriting the site_default manifest, which Munki on the device will default to anyway without a device manifest.

Unfriendly “FriendlyName[s]”

The next hurdle we had to overcome was the “DeviceFriendlyName” that AirWatch was returning. We use a standardized naming convention on all of our machines that goes like so: MH + 8 digit asset tag + MAC + LT or DT (for laptop or desktop respectively). This results in a device name like so: MH01234567MACLT

However, up until this point, all of our devices had AirWatch’s default Asset Tag, which is the same as the device’s UDID, some crazy long alphanumeric string. Since we have a property database with this information, we were able to add a function to our script which would query the property database, and make a PUT request to AirWatch to update the device’s Asset Tag (this snippet is not included in the code on GitHub as it relies on internal resources that is likely not relevant to others).

With regards to the script, we just needed to add another conditional below the one we just created above, that will make sure the Asset tag was updated:


...
    elif len(device['AssetNumber']) < 8:
        print "Device [%s] Asset tag has not been updated yet, skipping manifest creation.
...

These are all basically just simple checks the script does before creating the manifest on the server. If these checks didn't matter to you, or didn't apply, they could easily be changed or completely removed.

Side Note: I did not show this code, but in the script on GitHub, there is also a check to make sure that the manifest does not already exist on the server, so that we don’t unnecessarily generate one or overwrite anything.

Generating the Manifest

Now that we have all the pieces we need, and have checked to make sure that the device is in the state we want, we can confidently create the device manifest with the necessary bits of info.

We can add the following code to actually create the manifest for us:


...
    else:
        print "\tCreating a manifest for device."
        manifest_template = {}
        manifest_template['catalogs'] = ['production']
        manifest_template['included_manifests'] = ['site_default']
        manifest_template['managed_installs'] = []
        manifest_template['optional_installs'] = []
        manifest_template['display_name'] = device['FriendlyName']
        manifest_template['user'] = device['Username']
        plistlib.writePlist(manifest_template, '%s/%s' % (manifests_dir, device['SerialNumber']))

Fin.

And there you have it, a way to dynamically generate client manifests for Munki, utilizing AirWatch’s API!

We are hoping to expand upon this idea to make even more use of AirWatch’s API. We would like to do something to the effect of allowing a customer to choose a piece of licensed software that they want on their device, that information would then get sent to AirWatch into something like a Custom Attribute, and then manifest_generator could look and add that piece of software to the client’s manifest.

As always, thanks for reading!

Advertisements

8 thoughts on “Generating Pre-Populated Munki Manifests Utilizing the AirWatch API

  1. “We chose to utilize the site_default manifest that Munki defaults to as our primary manifest that all general software would be assigned to.”

    I’d recommend a slightly different approach here:

    Put all your general/global software in a “StandardConfiguration” manifest. Add that manifest as an included manifest to site_default, and any other machine-specific manifests. This preserves catalog flexibility for all machine manifests.

    Liked by 1 person

    1. Hmmm I might need help understanding what you mean. Currently we have the approach described in this post for all standard machines. Then we created a second manifest called “dev”, which our test devices include. And the test devices all have the “testing” catalog assigned to them.

      I would be lying to admit that I’ve still fully wrapped my head around catalogs and such, so any advice is welcome! I’ll try to read up on the Munki wiki to see if I can grasp what you’re suggesting in the meantime though, as it sounds like a good approach, I just have to understand WHY it’s a good approach haha.

      Like

  2. I’ll add a bit more detail:

    Put all your general/global software in a “StandardConfiguration” manifest. This manifest would have an empty catalogs array (or non-existent catalogs array).

    Add this “StandardConfiguration” manifest as an included_manifest in the site_default manifest. The site_default manifest would typically have a catalogs array containing only the “production” catalog (or whatever you’ve decided to name it).

    Other machine-specific manifests would also include the “StandardConfiguration” manifest, but they might have different catalog lists. A group of machines designated to get new software earlier than the majority might have a catalog list containing “testing” and “production”.

    https://github.com/munki/munki/wiki/Manifests#included_manifests
    https://github.com/munki/munki/wiki/Using-Munki-Catalogs
    https://github.com/munki/munki/wiki/Using-Munki-Catalogs#catalogs-and-included_manifests

    Like

    1. Ahhh the idea being that rather than having two virtually identical, separate manifests (one that’s assigned the production catalog, and the other test), you would assign the client manifests themselves the desired catalog, allowing certain machines to get the latest version of apps vs other clients.

      That makes a lot of sense! Thanks again for commenting and elaborating for me.

      Like

  3. Hi!

    First, let me say that that is kind of neat.

    And second, that it comes closer to my vision of having the manifest generated dynamically, at runtime or whenever the machine’s attributes change (and cached for performance), based on some inventory system. I wish i was more of a coder 🙂

    Like

    1. Ha! I’m far from anything more than an amateur who Googles a lot when it comes to code, so don’t sell yourself short! But yes, that is very much the kind of goal we are striving towards as well. In its current form, a change of user or device name in AirWatch wouldn’t change the manifest, because that would require looping through every manifest essentially every run to see if the attributes matched, and that’s just not worth it for us right now. But I could definitely see something like that being very useful! I had originally thought of trying to have the client itself generate the manifest and upload it, but didn’t want to go through the hassle of how to do that securely without just opening up our munki server for anyone to write to. But I’m sure there are ways to do that securely, and that would distribute the load of checking to see if attributes changed and changing the manifest to each individual machine, which would be nice!

      Thanks for taking the time to read and discuss!

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s