Device Snapshots and Crafting A Restore Device User Experience for Macs

I recently worked on something where a design requirement was to build a good snapshot restore option but not to use Time Machine backups. You can capture a snapshot of a Mac without enabling Time Machine. To do so, you’d still use the same binary as you would with Time Machine, /usr/bin/tmutil. To do so, simply use the snapshot verb as follows:

/usr/bin/tmutil snapshot

Once you’ve run that, you get output similar to the following:

Created local snapshot with date: 2019-04-12-110248

Now you have a snapshot that can be used to restore a Mac using the steps shown in this article: https://maclovin.org/blog-native/2017/restoring-from-a-snapshot-with-apfs.

You can make a snapshot at the provisioning time of a Mac and then see that snapshot at any point by awking for the first line in a list of snapshots, unless it’s been deleted:

snapshot=`tmutil listlocalsnapshots / | awk -F "." '{print $4}' | head -1`

Now when it comes to making it simple for end user restores, there’s a few gaps. The first is that you have to boot with Command-R and can’t build a scripted restore option. So the following won’t work on a machine with SIP:

sudo nvram boot-args="-x"; restart

You can prune the list of snapshots so it’s simple for a user to pick the right one though. To see a list of all your snapshots for the current boot volume, use that listlocalsnapshots verb:

tmutil listlocalsnapshots /

This provides the output as com.apple.TimeMachine.2019-04-12-110248 and when deleting snapshots you don’t need the com.apple.TimeMachine. portion of that string, so to delete the last snapshot we’ll gather it into a variable with the following:

snapshot=`tmutil listlocalsnapshots / | awk -F "." '{print $4}' | tail -1`

Alternatively you can list snapshots with the diskutil command, using the apfs option and listSnapshots verb, followed by your boot volume as follows, but the output isn’t pretty:

diskutil apfs listSnapshots disk1s1

Once you’ve parsed the service name portion out, you can then delete a given snapshot using the deletelocalsnapshots verb with tmutil:

tmutil deletelocalsnapshots 2019-04-12-110248

Or to use that snapshot variable:

tmutil deletelocalsnapshots $snapshot

So I can present the user, when they try to revert, with a shorter list of snapshots, hopefully helping them to select the right one. But, I can only do a scripted restore if I’m actually using Time Machine and I have a volume taking those backups. If I’m doing a snapshot restore I have to restart the Mac holding down the command- R keys and then once booted in recovery mode, select the snapshot and wait for the restore.

Booting into recovery mode takes a long time so seems like something you’d teach the desktop techs to do rather than expect users to do a-la-“Erase all Contents and Settings” button. The whole process takes about 20 minutes because of how long it takes to boot into this state and revert.

It’s actually just as quick to do a full reinstallation. This can be as simple as using the startosinstall --eraseinstall command, which provides a number of options. It seems silly to load up a whole new operating system when you have one there, but assuming you have decent internet speeds, it’s just as fast to do so. There’s even a gui wrapper for it to make the whole process easier for your users at https://scriptingosx.com/2018/10/eraseinstall-application/ which could then be provided in tools like Managed Software Center and Self Service. You could also try and remove all user and app data using a script but that has turned out to be a lot of moving parts that requires constant updates.

If you were to opt into a snapshot-based restore option, it’s also worth noting that if you have outdated certificates, outdated passwords, outdated software, etc, that it can be problematic. So we’re pretty much right back to erase-install.

Other people have gone down this same rabbit hole:

  • https://grahamrpugh.com/2018/03/26/reinstall-macos-from-system-volume.html
  • https://www.jamf.com/jamf-nation/discussions/26716/summer-2018-wiping-student-systems

So my initial goal was to create an article on doing an elegant restore process for the Mac, where you script more of it and provide a better experience. But alas, I have failed ya’. Sorry. Do you have a better way? If so, please comment!

Approve Or Deny GSuite Access For Devices

The Google Directory integration with GSuite allows you to manage which devices have access to GSuite. This allows you to control access based on a variety of factors.

Below, you’ll find a Google Cloud Function that is meant to respond to a webhook. This function takes an action to set a device into ‘approve’ or ‘deny’ as a state within Google Directory. Before using the function you’ll want to set CustomerID, ResourceID, and EMAIL_ACCOUNT for your GSuite account before using.

Once you have all that, you can upload mobiledevice.py in your Google Cloud Console.

#
# Google Cloud Function meant to respond to a webhook
# Takes an action to set a device into approve or deny state
# Set CustomerID, ResourceID, and EMAIL_ACCOUNT for your GSuite account before using
#

from google.oauth2 import service_account
import googleapiclient.discovery

SCOPES = ['https://www.googleapis.com/auth/admin.directory.device.mobile']
SERVICE_ACCOUNT_FILE = 'auth.json'
EMAIL_ACCOUNT = ''


def get_credential():
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
delegated_credentials = credentials.with_subject(EMAIL_ACCOUNT)
# admin = googleapiclient.discovery.build('admin', 'directory_v1', credentials=credentials)
admin = googleapiclient.discovery.build('admin', 'directory_v1', credentials=delegated_credentials)
return admin


def get_mobiledevice_list(admin, customerId):
results = admin.mobiledevices().list(customerId=customerId).execute()
mobiledevices = results.get('mobiledevices', [])
print('mobile devices name and resourceId')
for mobiledevice in mobiledevices:
print(u'{0} ({1})'.format(mobiledevice['name'], mobiledevice['resourceId']))
return results


def action_mobiledevice(admin, customerId, resourceId, actionName): # actionName: "approve", "block",etc body = dict(action=actionName)
results = admin.mobiledevices().action(customerId=customerId, resourceId=resourceId, body=body).execute()
return results


def main():
admin = get_credential()
customerId = ''
resourceId = ''
action = "approve"
#action = "block"

mobiledevice_list = get_mobiledevice_list(admin, customerId)
print(mobiledevice_list)

action_mobiledevice(admin, customerId, resourceId, action)
print ("Approved successfully")


if __name__ == '__main__':
main()

This is likely to evolve, given that you’ll likely want to migrate your settings into a database as part of your build process, but the general logic is here for now. Happy Googleatinging!

Manage the Look of Launchpad

You can control the number of columns and rows in LaunchPad. To do so, edit the com.apple.doc defaults domain with the key springboard-rows for the number of rows to display and springboard-columns to control the number of columns displayed. So to set the number of rows LaunchPad will show per screen, send the write verb into defaults for com.apple.dock along with the springboard-rows and an -int of 4:

defaults write com.apple.dock springboard-rows -int 4

Likewise, to set columns to 8:

defaults write com.apple.dock springboard-columns -int 8

Then just killall for Dock:

killall Dock

In some cases you will also need to send a resetlaunchpad boolean into com.apple.dock (for TRUE) along with a killall for Dock (or reboot):

defaults write com.apple.dock resetlaunchpad -bool TRUE; killall Dock

Obtain A List Of Devices or Apps In ZuluDesk Using Bash

The curl command can be used to authenticate to an API using a variety of authentication types such as Bearer, OAuth, Token, and of course Basic. To authenticate to the ZuluDesk API, first create an API token. This is done by logging into ZuluDesk, clicking Organization, then Settings, then API, an then clicking on the Add API Key button.

Once you have your API key, your header will look as follows:

GET /users HTTP/1.1 User-Agent: curl/7.24.0 X-Server-Protocol-Version: 2 Authorization: Basic YOURTOKENHERExxx000111222== Content-Length: 0

The curl command can do this would be as follows, simply converting these into separate values in the -H or header. The URL provided will do a GET against devices, displaying a list of devices in json:

curl -S -i -k -H "Content-Length: 0" "User-Agent: curl/7.24.0" X-Server-Protocol-Version: 2" "Authorization: Basic YOURAPITOKENxx000111222==" https://apiv6.zuludesk.com/devices/

Once you have the “serialNumber” you can programmatically perform a number of other tasks using a POST. Another example would be obtaining a list of apps, done using the /apps/ endpoint.

curl -S -i -k -H "Content-Length: 0" "User-Agent: curl/7.24.0" X-Server-Protocol-Version: 2" "Authorization: Basic YOURAPITOKENxx000111222" https://apiv6.zuludesk.com/apps/

You can also run a POST in the same fashion. In the following we’ll do that, sending a simple delete command to the group 505

curl -X DELETE -S -i -k -H "Content-Length: 0" "User-Agent: curl/7.24.0" X-Server-Protocol-Version: 2" "Authorization: Basic YOURAPITOKENxx000111222" https://apiv6.zuludesk.com/users/groups/:505

Overall, the ZuluDesk API is pretty easy to use and follow with just some basic curl commands.

Pull iTunes App Categories via Bash

I love bash one-liners. Here’s one it took me a bit to get just right that will pull the Category of an app based on the URL of the app.

curl -s 'https://itunes.apple.com/us/app/self-service-mobile/id718509958?mt=8' | grep -Eo '"applicationCategory":.*?[^\\]",'

If you don’t already have the URL for an app, it can be obtained via a lookup using

curl https://itunes.apple.com/lookup?id=718509958

If you’ll be performing these kinds of operations en masse from within server-side scripting, Apple has a number of programs, including the Affiliate Program, which allow you to do so more gracefully. But as a quick and dirty part of a script, this could solve a need. More importantly, hey, parse some json from bash without piping to python or perl or whatevers… Enjoy!

Register A Webhook In Jamf Pro

A webhook is a small web trigger that when fired can easily send amount of small json to a web listener. Most modern software solutions support webhooks. They provide an easy way to trigger events from a piece of software to happen in another piece of software.

An example of this is when a smart group change happens in Jamf Pro, do something elsewhere. To start, you register a webhook in Jamf Pro by opening an instance of Jamf Pro, clicking on Settings, clicking on Global Management, and then clicking on Webhooks.

Registering Webhooks

From the Webhooks screen, click New.

New Webhook Screen

At the New Webhook screen, you will see a number of fields. Here,

  • Display Name: The name used to identify the webhook in Jamf Pro.
  • Enabled: Check to enable the webhook, uncheck the box to disable the webhook.
  • Webhook URL: The URL that the json or xml will be sent to (note that you’ll need something at this URL to accept your webhook).
  • Authentication Type: None is used for an anonymous webhook and basic can be used to send a username and password to the webhook listener.
  • Connection Timeout: How long the webhook will attempt to open a connection before sending data.
  • Read Timeout: How long the webhook will attempt to send data for before it turns off.
  • Content Type: Choose to send information via xml or json.
  • Webhook Event: The type of event that Jamf Pro can send a hook based on.

The options for webhook events include:

  • ComputerAdded
  • ComputerCheckin
  • ComputerInventoryCompleted
  • ComputerPatchPolicyCompleted
  • ComputerPolicyFinished
  • CoputerPushCapabilityChanged
  • DeviceRateLimited
  • JSSShutdown
  • JSSStartup
  • MobileDeviceCheckin
  • MobileDeviceCommandCompleted
  • MobileDeviceEnrolled
  • PatchSoftwareTitleUpdated
  • PushSent
  • RestAPIOperation
  • SCEPChallenge
  • SmartGroupComputerMembershipChange
  • SmartGroupMobileDeviceMembershipChange

An example of a full workflow would be what we did to trigger a Zapier action, documented at http://krypted.com/mac-os-x/add-jamf-pro-smart-group-google-doc-using-zapier/. Here, we look at sending smart group membership changes to a google sheet so we can analyze it with other tools, a pretty standard use case.

.

NFS. Not… Dead… Yet…


NFS may just never die. I’ve seen many an xsan covert to NFS-based storage with dedicated pipes and less infrastructure requirements. I’m rarely concerned about debating the merits of technology but usually interested in mapping out a nice workflow despite said merits. So in the beginning… there is rpc. Why? Because before we establish a connection to an nfs share, we first want to check that we can talk to the system hosting it. Do so with rpcinfo:

rpcinfo server.pretendco.com

Now that we’ve established that we can actually communicate with the system, let’s use the mount command (for more on creating mounts see `man exports`). Here, we’ll 

mount -t nfs nfs://server.pretendco.com/bigfileshare /Network/Servers/server.pretendco.com/bigfileshare

ncctl is a one-stop shop for manipulating kerberized NFS. Ish. You also have ncinit, ncdestroy, and nclist. So almost a one-stop shop. First, let’s check the list of shares you have and how you’re authoring to each:

nclist -v

ncctl list can also be used. The output will be similar to the following:

/Network/Servers/server.pretendco.com/bigfileshare       : No credentials are set

We should probably authenticate into that share. Now let’s actually set our username (assuming you’ve already kerberized via kinit or a gui somewheres):

ncctl set -p krypted@me.com

Now that spiffy nclist command should return something like the following:

/Network/Servers/server.pretendco.com/bigfileshare: krypted@me.com

Finally, ncdestroy is used to terminate your connection. So let’s just turn off the share for the evening:

ncctl destroy

Or ncdestroy is quicker to type. And viola, you’ve got a functional nfs again. Ish. 

Now that you’re connected, nfsstat should show you how the system is performing. For more on using that, see: 

man nfsstat

Limit Upload and Download Streams for Google Drive File Stream on macOS

Google Drive File Stream allows you to access files from Google’s cloud. It’s pretty easy for a lot of our coworkers to saturate our pipes. So you can configure a maximum download and upload speed in kilobytes per second. To do so write a com.google.drivefs.settings defaults domain into /Library/Preferences/com.google.drivefs.settings and use a key of BandwidthRxKBPS for download and BandwidthTxKBPS for upload (downstream and upstream as they refer to them) as follows:

defaults write com.google.drivefs.settings BandwidthRxKBPS -int 200
defaults write com.google.drivefs.settings BandwidthTxKBPS -int 200