Archive
Collecting Data Using Plist Files
At our recent Dallas area Casper User Group meeting, we got into a discussion around collecting data during a Casper recon. Specifically we were discussing the use of Extension Attributes to collect information about virtual machines.
Extension Attributes are a way to capture information from your systems. You can use scripts to pull information or drop downs or text boxes to store static information in the database. In the instance of collecting info about virtual machines, a script would be run on the systems during the recon to gather the information. Running a script on the system each time a recon happens can be processor heavy, depending on the data that is being gathered. For example, gathering home folder size by running “du” each time a recon happens can be taxing.
Rather than run the script each recon, you can use a policy to run the script once a week, once a month, or just one time, to gather the information you need and place it in a plist file somewhere. During the standard recon period, you can then use an Extension Attribute to read the information in that plist file. This is much less taxing on the systems than running the script during a recon.
Stash The Data
For our example, rather than run through grabbing info about virtual machines, let’s work on grabbing the home folder size for the logged on user. We will store the info in a plist file that we will stash in /Library/IT_Data.
First we need to find the logged in user name. There are plenty of ways to do this, but we’ll use the “Apple approved” method, using a Python one liner. Okay, it’s not really a one liner, it’s just built like one.
loggedInUser=`python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");'` | |
view rawcontents.sh |
Now that we have our logged in user, we just need to find the user’s home folder and use du to grab the data. We’ll use dscl to grab the home folder location and then du to get the home folder size.
homeDir=`dscl . read /Users/$loggedInUser NFSHomeDirectory | awk '{ print $2 }'` | |
homeSize=`du -hs ${homeDir}` |
The next thing we need to do is to store this information into our plist file. Using the defaults command, we can write as much data as we want into the plist file, We can use different keys to store whatever data you want, and then recall it during recon by asking for those specific keys.
defaults write /Library/IT_Data/com.mycompany.homesize.plist HomeFolderSize -string "${homeSize}" |
Retrieve The Data
Now that we have the data stashed away, we just need to grab it during the recon process. To do this, we’ll use the defaults command again, to grab the data. We’ll use some variables for the folder path and the plist name, that way we can re-use this code fairly easily. We also want to make sure the file actually exists before trying to read data from it, hence the If statement.
if [[ -e ${dataFile} ]]; then | |
homeSize=`defaults read ${dataFile} HomeFolderSize` | |
fi |
Once we’ve read the data, all that’s left is to echo it out into the EA.
if [[ ${homeSize} ]] | |
echo "<result>${homeSize}</result>" | |
else | |
echo "<result>No Results Found</result>" | |
fi |
That’s All Folks
That’s pretty much all there is. Now that we know how save data to a plist and then read it back, this method could be used for any data we only need to gather once, or gather at infrequent times.
The full script to write the data and to then read the data are below.
#!/bin/bash | |
loggedInUser=`python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");'` | |
homeDir=`dscl . read /Users/$loggedInUser NFSHomeDirectory | awk '{ print $2 }'` | |
homeSize=`du -hs ${homeDir}` | |
# check for our storage folder, and create if missing | |
if [[ ! -d "/Library/IT_Data" ]]; then | |
mkdir "/Library/IT_Data" | |
fi | |
defaults write /Library/IT_Data/com.mycompany.homesize.plist HomeFolderSize -string "${homeSize}" |
#!/bin/bash | |
dataFolder="/Library/IT_Data" | |
dataFile="${dataFolder}/com.mycompany.homesize.plist" | |
if [[ -e ${dataFile} ]]; then | |
homeSize=`defaults read ${dataFile} HomeFolderSize` | |
fi | |
if [[ ${homeSize} ]]; then | |
echo "<result>${homeSize}</result>" | |
else | |
echo "<result>No Results Found</result>" | |
fi |
To Image or To First Boot
When I first started out with the Casper Suite back in 2008, it was commonplace to create imaging configurations with their Casper Imaging tool. Drop an OS image into Casper, add in some applications in the order you want them installed, maybe a preference or two, and voila, you now have an imaging configuration for use in Casper Imaging. The next steps after that were to boot the machine you want imaged from an external source (NetBoot, USB drive, DVD, etc), run Casper Imaging, choose the configuration you want to run, and after some amount of time you’d have a machine ready to deploy.
Fast forward a few years, and more and more admins are no longer using an imaging methodology like this. Instead we’ve switched to leaving the factory operating system in place and simply adding the necessary applications and settings onto the systems. You can still utilize an imaging configuration deployed with Casper Imaging for this method, and that’s exactly how I first started with this method, but then I switched methods again, and started deploying apps and preferences with a post image, or First Boot, script.
This method, I felt, allowed me more opportunities to update the imaging process without having to touch the configuration. All I had to do was create a simple Bash script (or Python or whatever language you prefer) that would get called after Casper was done. After Casper had done it’s thing and restarted, my script would run and apply any config type items (set NTP server, time zone, etc) and then use the jamf binary to install software by calling policies.
Let’s take a look at the actual script and the LaunchDaemon I use to call it. The script in its entirety can be found on my GitHub repo here.
Script City
So the first bit of the script is just for setting up some variables and to setup logging. The script will output everything into this log file so that you can go back and troubleshoot later. If you are passing any sensitive data in the script, you may want to ship the log to a secure server and then delete it.
## Set global variables | |
LOGPATH='/path/to/your/logs' | |
JSSURL='https://<yourjssurl>' | |
JSSCONTACTTIMEOUT=120 # amount of time to wait before timing out waiting for connection | |
LOGFILE=$LOGPATH/deployment-$(date +%Y%m%d-%H%M).logging #re-name to whatever name you want | |
VERSION=10.11.5 | |
## Setup logging | |
if [[ ! -d $LOGPATH ]]; then | |
mkdir $LOGPATH | |
fi | |
set -xv; exec 1> $LOGFILE 2>&1 |
After we’ve taken care of some of that housekeeping, we lock the screen so the end user or tech knows that we are working on the system. You can use the default lock icon that Apple uses, or you can upload your own icon and declare that in the swuIcon variable. This bit of code is not my own, but was borrowed from Mike Morales out of this JAMFNation post. Thanks Mike!
## Block the user from being able to see our trickery | |
## Define the name and path to the LaunchAgent plist | |
PLIST="/Library/LaunchAgents/com.LockLoginScreen.plist" | |
## set the icon | |
swuIcon="<set to your ICON for use on lock screen if you want>" | |
## Define the text for the xml plist file | |
LAgentCore="<?xml version=\"1.0\" encoding=\"UTF-8\"?> | |
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> | |
<plist version=\"1.0\"> | |
<dict> | |
<key>Label</key> | |
<string>com.LockLoginScreen</string> | |
<key>RunAtLoad</key> | |
<true/> | |
<key>LimitLoadToSessionType</key> | |
<string>LoginWindow</string> | |
<key>ProgramArguments</key> | |
<array> | |
<string>/System/Library/CoreServices/RemoteManagement/AppleVNCServer.bundle/Contents/Support/LockScreen.app/Contents/MacOS/LockScreen</string> | |
<string>-session</string> | |
<string>256</string> | |
</array> | |
</dict> | |
</plist>" | |
## Create the LaunchAgent file | |
echo "Creating the LockLoginScreen LaunchAgent..." | |
echo "$LAgentCore" > "$PLIST" | |
## Set the owner, group and permissions on the LaunchAgent plist | |
echo "Setting proper ownership and permissions on the LaunchAgent..." | |
chown root:wheel "$PLIST" | |
chmod 644 "$PLIST" | |
## Use SIPS to copy and convert the SWU icon to use as the LockScreen icon | |
## First, back up the original Lock.jpg image | |
echo "Backing up Lock.jpg image..." | |
mv /System/Library/CoreServices/RemoteManagement/AppleVNCServer.bundle/Contents/Support/LockScreen.app/Contents/Resources/Lock.jpg \ | |
/System/Library/CoreServices/RemoteManagement/AppleVNCServer.bundle/Contents/Support/LockScreen.app/Contents/Resources/Lock.jpg.bak | |
## Now, copy and convert the SWU icns file into a new Lock.jpg file | |
## Note: We are converting it to a png to preserve transparency, but saving it with the .jpg extension so LockScreen.app will recognize it. | |
## Also resize the image to 400 x 400 pixels so its not so honkin' huge! | |
##echo "Creating SoftwareUpdate icon as png and converting to Lock.jpg..." | |
##sips -s format png "$swuIcon" --out /System/Library/CoreServices/RemoteManagement/AppleVNCServer.bundle/Contents/Support/LockScreen.app/Contents/Resources/Lock.jpg \ | |
##--resampleWidth 400 --resampleHeight 400 | |
cp $swuIcon /System/Library/CoreServices/RemoteManagement/AppleVNCServer.bundle/Contents/Support/LockScreen.app/Contents/Resources/Lock.jpg | |
## Now, kill/restart the loginwindow process to load the LaunchAgent | |
echo "Ready to lock screen. Restarting loginwindow process..." | |
kill -9 $(ps axc | awk '/loginwindow/{print $1}') |
Next we put a dummy receipt down in the JAMF Receipts folder (/Library/Application Support/JAMF/Receipts). This is so we can scope via Smart Groups to machines imaged on a certain day, if we need or want. The modelName variable uses system_profiler to grab the machine model.
TODAY=`date +"%Y-%m-%d"` | |
if [[ ! -d "/Library/Application Support/JAMF/Receipts" ]]; then | |
mkdir /Library/Application\ Support/JAMF/Receipts | |
fi | |
touch /Library/Application\ Support/JAMF/Receipts/$modelName_Imaged_$TODAY.pkg |
After this, we get into setting system preferences like time servers and such. Rather than post all of the code, I’m going to point out a few key blocks. Like this one for enabling Location Services:
### Enable Location Services to set time based on location | |
/bin/launchctl unload /System/Library/LaunchDaemons/com.apple.locationd.plist | |
uuid=`ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'` | |
/usr/bin/defaults write /var/db/locationd/Library/Preferences/ByHost/com.apple.locationd.$uuid \ | |
LocationServicesEnabled -int 1 | |
/usr/sbin/chown -R _locationd:_locationd /var/db/locationd | |
/bin/launchctl load /System/Library/LaunchDaemons/com.apple.locationd.plist | |
# set time zone automatically using current location | |
/usr/bin/defaults write /Library/Preferences/com.apple.timezone.auto Active -bool true | |
#### |
UPDATE: With Apple’s continuing security stance and the introduction of SIP, it is no longer possible to set the Location Services settings via script.
Or how about how to set the system preferences authorization to allow users access to the Network pref panel, etc:
########################################## | |
# /etc/authorization changes | |
########################################## | |
security authorizationdb write system.preferences allow | |
security authorizationdb write system.preferences.datetime allow | |
security authorizationdb write system.preferences.printing allow | |
security authorizationdb write system.preferences.energysaver allow | |
security authorizationdb write system.preferences.network allow | |
security authorizationdb write system.services.systemconfiguration.network allow |
And finally, the script checks for the location of the jamf binary file (to combat post-10.11 woes) and uses the binary to call policies. Thanks to Rich Trouton (derflounder.wordpress.com)for the code that checks for the binary location. Just copy and paste (changing policy ID and description) the policy install piece to add as many policies as you need.
# check for jamf binary | |
/bin/echo "Checking for JAMF binary" | |
/bin/date | |
if [[ "$jamf_binary" == "" ]] && [[ -e "/usr/sbin/jamf" ]] && [[ ! -e "/usr/local/bin/jamf" ]]; then | |
jamf_binary="/usr/sbin/jamf" | |
elif [[ "$jamf_binary" == "" ]] && [[ ! -e "/usr/sbin/jamf" ]] && [[ -e "/usr/local/bin/jamf" ]]; then | |
jamf_binary="/usr/local/bin/jamf" | |
elif [[ "$jamf_binary" == "" ]] && [[ -e "/usr/sbin/jamf" ]] && [[ -e "/usr/local/bin/jamf" ]]; then | |
jamf_binary="/usr/local/bin/jamf" | |
fi | |
## JSS Policies for installing software | |
## Duplicate each block for the number of policies you need to run at post imaging time | |
/bin/echo "Installing <change to name of software>" | |
/bin/date | |
${jamf_binary} policy -id 1 # MAKE SURE TO SET TO THE CORRECT POLICY ID # |
After installing everything, run software update to install updates, remove the LaunchDaemon that controls the lock screen, and then restart the computer.
LaunchDaemon And Delivery
Now that we have our script built, we need to get it onto the system and have the system call the script. Let’s start with the LaunchDaemon. It’s a simple process to create a the file, just open up your favorite text editor (I like TextMate for writing Bash and Sublime Text for writing Python), drop in your XML and save out as a .plist file.
<?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.yourcompany.postimage</string> | |
<key>ProgramArguments</key> | |
<array> | |
<string>/path/to/your/scripts/postimage_10.11.5.sh</string> | |
</array> | |
<key>RunAtLoad</key> | |
<true/> | |
</dict> | |
</plist> |
With our script written and our LaunchDaemon ready, we just need to bundle it all up and get it into Casper. I use Packages for this part, but you can use your favorite packaging application.
I utilize a folder that I create inside of /private/var to hide all of my admin stuff, like scripts and binaries. So for this script, I would place it in this folder path I create, place the LaunchDaemon inside of /Library/LaunchDaemons, and then package it up.
Once packaged, drop your package into Casper Admin, set the priority to something low, like 5, and make sure to select “Install on boot drive after imaging”
With all of that work done, just add that to a configuration for Casper Imaging and image away. Casper Imaging will reboot the computer, at which time Casper’s first boot script will run and install any packages that were set to “Install on boot drive after imaging” and then restart the computer.
With our package installed during that process, on reboot of the computer our LaunchDaemon will take over and call our script. From there it’s just a matter of watching the paint dry until our computer is ready for us.
Final Thoughts
I’ve glossed over some things to try and shorten an overly long post. There are plenty of ways to image computers, and while this process works for me right now, it may not be your cup of tea. I’m currently looking at whether I should get rid of the system preference pieces and move that to Configuration Profiles, or if there’s some other trick to try. Either way, never stop tinkering with what you do, it’s what makes the job enjoyable and is the quickest way to learn something new.
If you’d like more information, or have a question, hit me up on the inter webs.