Tuesday, December 4, 2018

Run commands on next startup using launchd and Jamf

As part of my solution for replacing traditional imaging on our Macs, I found the need to have a Jamf Policy run some commands the next time a Mac starts up. One way to do this is to have a separate Policy triggered on startup, with a script payload containing the commands you wish to run.

While this works great, it does create a situation where two sets of Jamf logs may need to be flushed should you need to re-run a Policy, and could lead to confusion if the same set of Macs aren't added to the scope of each Policy. You could solve that problem with a Smart or Static Computer Group, but that's another thing to keep track of, and another possible point of failure.

So, instead, I read up on launchd, Apple's replacement for the older Unix-style init system. launchd manages two different types of processes: LaunchAgents, which are run when a user logs into the computer, and LaunchDaemons, which are run when the computer starts up. Both LaunchAgents and LaunchDaemons can be configured to run either as a specific user, or as the root user. Both are configured using XML-based plist files with a variety of keys to specify programs to run, with what arguments they should be run, whether they should be kept alive after being loaded, and even conditional operators to control execution directly from the plist based on certain environmental triggers.

I already had a Policy set up that will remove all user and application data from the Mac (simulating a clean image), copy the latest macOS installer to the /Applications folder, and reinstall macOS. I wanted, upon reboot, for the Mac to automatically set its time from our NTP server, join itself to our Active Directory (placing itself into the correct OU based on its name), reset the Jamf management account's password, and check for Policies from Jamf so that it would reinstall any provisioned applications, etc. - all without requiring a tech's intervention after the Policy had kicked off.

Therefore, rather than adding complication in the form of additional Policies and Computer Groups, I decided to create a script that would build a LaunchDaemon for me directly from the reimaing Policy. I found on my first attempt that LaunchDaemons may run before the operating system's networking facilities are available, so commands needing network access - such as checking in with Jamf - would not work. Luckily, Jeff Kelley explains on his blog how to wait until networking is available using an Apple-provided function.

Here is the script I came up with to run commands the next time the system starts up:

#!/bin/bash
networkup='${NETWORKUP}' #have to insert this as a string into the script below, rather than letting bash try to give it a value, and return "" in the while below

# First, create the script that will run each of the specified commands at the next startup.
cat > /launchscript.sh <<EOF
#!/bin/bash

# Delay until networking is available.
#https://blog.slaunchaman.com/2010/07/01/how-to-run-a-launchdaemon-that-requires-networking/
. /etc/rc.common
CheckForNetwork

while [ "$networkup" != "-YES-" ]
do
        sleep 5
        NETWORKUP=
        CheckForNetwork
done

$4
$5
$6
$7
$8
$9
$(10)
$(11)

# Clean up this script, and the LaunchDaemon plist after the commands have been run.
rm /Library/LaunchDaemons/com.example.launchscript.plist
rm /launchscript.sh
EOF

# Next, make sure that the script is executable.
chmod +x /launchscript.sh

# Then, create a LaunchDaemon plist and register it so that the script will be run on the next startup.
cat > /Library/LaunchDaemons/com.example.launchscript.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.example.launchscript</string>
    <key>Program</key>
    <string>/launchscript.sh</string>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
<string>/var/log/launchscript.log</string>
    <key>StandardErrorPath</key>
<string>/var/log/launchscript.log</string>
  </dict>
</plist>
EOF

This script takes up to eight command strings in variables $4 - $11, as is normal for Jamf scripts, and inserts them into another script, /launchscript.sh. That script waits until networking is available, runs the commands specified, then cleans up both itself and the LaunchDaemon that runs it on startup. Then, after making the launch script executable, the script creates a simple LaunchDaemon plist file in /Library/LaunchDaemons, which will be loaded and run on the next system startup. This LaunchDaemon redirects its standard out and error streams to a log file, /var/log/launchscript.log, so that any error messages that happen during launch script execution can be captured for debugging purposes.

This script accepts up to eight commands, but those can be used to run much larger scripts, or even to call Jamf policies or installer packages. I have not yet tried to run applications with this script, but I would guess that anything requiring a desktop environment would not run correctly. Since the LaunchDaemon and its associated launch script run as the system root user, you can use this to perform any sort of management or maintenance you would like on the computer, even without an administrator logging in.

No comments:

Post a Comment

Tableau, TabPy, and the Case of No Input Rows

 I haven't scientifically confirmed this or anything, but it sure seems like if you pass an empty dataframe to a TabPy script, then no m...