Re-Introducing Cacher

Several months ago I was tasked to prove that our brand new Caching Server was actually doing it’s job. I had already setup CacheWarmer with an hourly custom script/cronjob, so I knew when new iOS builds would come out, we wouldn’t be crippled.

SNMP could technically be an option but the Caching Service relies on random high ports. We could make assumptions, but that still wouldn’t be good enough to justify why we had this installed in out data center. Anecdotally, I knew it was working: Our users noticed app store improvements, I noticed app store improvements, but our executives wanted metrics.

Apple’s Server information is very minimal and that’s being generous.
Cacher Example
E-mailing screenshots filled with squiggly line isn’t good enough. Maybe the logs can help?

Reading the Advanced Caching Documentation page, I went ahead and enabled “LogClientIdentity” by running the following command.

sudo serveradmin settings caching:LogClientIdentity = 1

After looking at the generated log files, I knew I was on to something. The logs gave me everything I needed, but with roughly 30,000 machines possibly hitting our caching server, the logs were enormous. I needed to figure out something.

Enter Cacher

Cacher is a bash script that if configured as a daily cronjob, will e-mail you daily statistics on your devices.

Cacher requires the following:
– OS X Yosemite 10.10.3
– Server 4.1 with Alerts Enabled

Currently there are two branches: Master and HTML.

Master -> Outputs a standard message to Server 4’s alert mechanism.

HTML -> Adds some html to the outputted message to trick Server 4’s alert mechanism into sending indented text.

Here is an example e-mail alert from the HTML branch.
Cacher Example

Installation

Installation is pretty simple:

  1. Download Cacher from either branch (I personally use the HTML branch).
  2. Place it somewhere on your OS X Caching Server.
  3. Setup a cronjob/LaunchD

Example

Create a folder and download it

mkdir -p /Users/Shared/Cacher
curl "https://raw.githubusercontent.com/erikng/Cacher/HTML/Cacher" -o "/Users/Shared/Cacher/Cacher"

Make sure it can be executed

chmod a+x /Users/Shared/Cacher/Cacher

Create a root cronjob. This is required for the alert mechanism to work. Alternatively you could create a LaunchDaemon as well, but I find this easier.

sudo env EDITOR=nano crontab -e

Nano will open. Put in your preferred e-mail time alert. I personally have an e-mail sent at 6:30 AM every day.

30      6       *       *       *       /Users/Shared/Cacher/Cacher

After entering this into nano, use hit CTRL+X and save it. If successful, crontab will give you an installation status. If you need more information on setting up a different time, please see this maclife article

Now sit back, wait until tomorrow to see your new statistics and wonder why Apple doesn’t have something like already.

Moving your DeployStudio Workflows to Imagr

If you’re here, chances are you’re interested in Imagr and read a blog post or two or three. Unfortunately, something is holding you back. Maybe it’s time, your imaging process or a very specific function of DeployStudio that you absolutely need. As you’ll quickly find out, most barriers are short lived.

Benefits of Imagr:
Areas of Improvement:
  • Server Logging
  • Embedded Workflows
  • Fusion Drive Support
Imagr isn’t:
  • A tool for capturing thick images
  • GUI based for the Admin

A few basic scripts

I’ve created two Imagr Wiki pages:
DeployStudio Alternative Scripts
Admin Provided Scripts

Sir Gilbert has opted for the Wiki route for a few reasons:
– Smaller codebase to maintain
– Easier entry for admins to contribute to the project.

Currently there are only a few scripts. As you begin to transition over to Imagr, if there is a configuration setting that you find to script, please add it to the list.


Breaking down your DeployStudio Workflows

I’ve taken DeployStudio for granted for many years. While I document many other processes, due to DeployStudio being mostly WYSIWYG, I’ve never felt compelled to actually list out each process I used.

If you want a successful transition you’re going to want to document. You know those checkboxes you use in DeployStudio? Document them!

Deploy Studio Breakdown Example

1. DS Restore Task
- Restore System Recovery Partition
- Set as Default Startup Volume
- Preventative Volume Repair
- Convert to CoreStorage
2. DS HostName Task
3. DS Configure Task
- Skip Apple Setup Assistant
- Disable Gatekeeper
4. DS Generic Task
- Munki Manifest Selector
5. DS Package Install Task
- Munki
6. DS SoftwareUpdate Task
7. Time Task
8. DS Active Directory Task
9. Automatic Reboot after completion (built into DS)

Let’s tackle these one by one. All of these tasks should be added to the components array

DS Restore Task

This one can is rather simple. By default, Imagr will automatically bless the volume.

<dict>
  <key>type</key>
  <string>image</string>
  <key>url</key>
  <string>http://10.10.10.10/imagr/masters/OS_X_10.10.3-14D136.hfs.dmg</string>
</dict>
<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/bash
# Repair Disk Permissions
diskutil repairPermissions /
  </string>
</dict>
<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/bash
# Convert to CoreStorage
diskutil cs convert disk0s2
  </string>
</dict>

DS HostName Task

Imagr can now prompt for a name.

<dict>
    <key>type</key>
    <string>computer_name</string>
</dict>

If your naming convention is based via serial number (like me) you can even remove your custom DS script.

<dict>
    <key>type</key>
    <string>computer_name</string>
    <key>use_serial</key>
    <true/>
    <key>auto</key>
    <true/>
</dict>

DS Configure Task

Both of these tasks are rather simple.

<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/bash
# Disable Gatekeeper
spctl --master-disable
  </string>
</dict>
<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/bash
# Bypass Apple Assistant
/usr/bin/touch "{{target_volume}}/private/var/db/.AppleSetupDone"
  </string>
</dict>

DS Generic Task

Basically everything we are doing here are considered “Generic Tasks”. See my other post for an approach to non-scripted generic tasks.

DS Package Install Task

Packages are very straight forward

<dict>
    <key>type</key>
    <string>package</string>
    <key>url</key>
    <string>http://10.10.10.10/imagr/packages/munkitools.pkg</string>
    <key>first_boot</key>
    <false/>
</dict>

DS SoftwareUpdate Task

Here is where I recommend a mobile configuration file. If you still want to do it the DeployStudio way, here is an example.

<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/sh
# Variables
SUS="URLPATH"

/usr/bin/defaults write "{{target_volume}}/Library/Preferences/com.apple.SoftwareUpdate" CatalogURL $SUS
chmod 644 "{{target_volume}}/Library/Preferences/com.apple.SoftwareUpdate.plist"
/usr/sbin/chown root:admin "{{target_volume}}/Library/Preferences/com.apple.SoftwareUpdate.plist"
  </string>
</dict>

Time Task

Rich Trouton has a great script to accomplish this.

<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/sh
#Primary Time server for Company Macs

TimeServer1=timeserver1.company.com

#Secondary Time server for Company Macs

TimeServer2=timeserver2.company.com

#Tertiary Time Server for Company Macs, used outside of Company network

TimeServer3=time.apple.com

# Time zone for Company Macs

TimeZone=America/New_York

# Configure network time server and region

# Set the time zone
/usr/sbin/systemsetup -settimezone $TimeZone

# Set the primary network server with systemsetup -setnetworktimeserver
# Using this command will clear /etc/ntp.conf of existing entries and
# add the primary time server as the first line.

/usr/sbin/systemsetup -setnetworktimeserver $TimeServer1

# Add the secondary time server as the second line in /etc/ntp.conf
echo "server $TimeServer2" >> /etc/ntp.conf

# Add the tertiary time server as the third line in /etc/ntp.conf
echo "server $TimeServer3" >> /etc/ntp.conf

# Enables the Mac to set its clock using the network time server(s)
/usr/sbin/systemsetup -setusingnetworktime on
</string>
</dict>

DS Active Directory Task

I would highly recommend that you package this as saving this directly in the imagr_config.plist will leave your AD binding account exposed.

With that said, Sir Gilbert has a great script for this (taken from DS.)

<dict>
  <key>type</key>
  <string>script</string>
  <key>content</key>
  <string>#!/bin/sh

# This was stolen from DeployStudio. I didn't write it, but dammit, I'm going to use it.

#
# Script config
#

AD_DOMAIN="ad.company.com"
COMPUTER_ID=`/usr/sbin/scutil --get LocalHostName`
COMPUTERS_OU="OU=Macs,OU=London,DC=ad,DC=company,DC=com"
ADMIN_LOGIN="bindUser"
ADMIN_PWD="bindPassword"
MOBILE="enable"
MOBILE_CONFIRM="disable"
LOCAL_HOME="enable"
USE_UNC_PATHS="enable"
UNC_PATHS_PROTOCOL="smb"
PACKET_SIGN="allow"
PACKET_ENCRYPT="allow"
PASSWORD_INTERVAL="0"
ADMIN_GROUPS="COMPANY\Domain Admins,COMPANY\Enterprise Admins"

# UID_MAPPING=
# GID_MAPPING=
# GGID_MAPPING==

# disable history characters
histchars=

SCRIPT_NAME=`basename "${0}"`

echo "${SCRIPT_NAME} - v1.26 ("`date`")"

#
# functions
#
is_ip_address() {
  IP_REGEX="\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
  IP_CHECK=`echo ${1} | egrep ${IP_REGEX}`
  if [ ${#IP_CHECK} -gt 0 ]
  then
    return 0
  else
    return 1
  fi
}


#
# Wait for the naming script to have run
#
if [ ${COMPUTER_ID} -eq "" ]
then
echo "The mac doesn't have a name, exiting."
  exit 1
fi

# AD can only use a 15 character name
COMPUTER_ID=`echo ${COMPUTER_ID} | cut -c1-15`

#
# Wait for network services to be initialized
#
echo "Checking for the default route to be active..."
ATTEMPTS=0
MAX_ATTEMPTS=18
while ! (netstat -rn -f inet | grep -q default)
do
  if [ ${ATTEMPTS} -le ${MAX_ATTEMPTS} ]
  then
    echo "Waiting for the default route to be active..."
    sleep 10
    ATTEMPTS=`expr ${ATTEMPTS} + 1`
  else
    echo "Network not configured, AD binding failed (${MAX_ATTEMPTS} attempts), will retry at next boot!" 2>&1
    exit 1
  fi
done

#
# Wait for the related server to be reachable
# NB: AD service entries must be correctly set in DNS
#
SUCCESS=
is_ip_address "${AD_DOMAIN}"
if [ ${?} -eq 0 ]
then
  # the AD_DOMAIN variable contains an IP address, let's try to ping the server
  echo "Testing ${AD_DOMAIN} reachability" 2>&1
  if ping -t 5 -c 1 "${AD_DOMAIN}" | grep "round-trip"
  then
    echo "Ping successful!" 2>&1
    SUCCESS="YES"
  else
    echo "Ping failed..." 2>&1
  fi
else
  ATTEMPTS=0
  MAX_ATTEMPTS=12
  while [ -z "${SUCCESS}" ]
  do
    if [ ${ATTEMPTS} -lt ${MAX_ATTEMPTS} ]
    then
      AD_DOMAIN_IPS=( `host "${AD_DOMAIN}" | grep " has address " | cut -f 4 -d " "` )
      for AD_DOMAIN_IP in ${AD_DOMAIN_IPS[@]}
      do
        echo "Testing ${AD_DOMAIN} reachability on address ${AD_DOMAIN_IP}" 2>&1
        if ping -t 5 -c 1 ${AD_DOMAIN_IP} | grep "round-trip"
        then
          echo "Ping successful!" 2>&1
          SUCCESS="YES"
        else
          echo "Ping failed..." 2>&1
        fi
        if [ "${SUCCESS}" = "YES" ]
        then
          break
        fi
      done
      if [ -z "${SUCCESS}" ]
      then
        echo "An error occurred while trying to get ${AD_DOMAIN} IP addresses, new attempt in 10 seconds..." 2>&1
        sleep 10
        ATTEMPTS=`expr ${ATTEMPTS} + 1`
      fi
    else
      echo "Cannot get any IP address for ${AD_DOMAIN} (${MAX_ATTEMPTS} attempts), aborting lookup..." 2>&1
      break
    fi
  done
fi

if [ -z "${SUCCESS}" ]
then
  echo "Cannot reach any IP address of the domain ${AD_DOMAIN}." 2>&1
  echo "AD binding failed, will retry at next boot!" 2>&1
  exit 1
fi

#
# Unbinding computer first
#
echo "Unbinding computer..." 2>&1
dsconfigad -remove -username "${ADMIN_LOGIN}" -password "${ADMIN_PWD}" 2>&1

#
# Try to bind the computer
#
ATTEMPTS=0
MAX_ATTEMPTS=12
SUCCESS=
while [ -z "${SUCCESS}" ]
do
  if [ ${ATTEMPTS} -le ${MAX_ATTEMPTS} ]
  then
    echo "Binding computer to domain ${AD_DOMAIN}..." 2>&1
    dsconfigad -add "${AD_DOMAIN}" -computer "${COMPUTER_ID}" -ou "${COMPUTERS_OU}" -username "${ADMIN_LOGIN}" -password "${ADMIN_PWD}" -force 2>&1
    IS_BOUND=`dsconfigad -show | grep "Active Directory Domain"`
    if [ -n "${IS_BOUND}" ]
    then
      SUCCESS="YES"
    else
      echo "An error occured while trying to bind this computer to AD, new attempt in 10 seconds..." 2>&1
      sleep 10
      ATTEMPTS=`expr ${ATTEMPTS} + 1`
    fi
  else
    echo "AD binding failed (${MAX_ATTEMPTS} attempts), will retry at next boot!" 2>&1
    SUCCESS="NO"
  fi
done

if [ "${SUCCESS}" = "YES" ]
then
  #
  # Update AD plugin options
  #
  echo "Setting AD plugin options..." 2>&1
  dsconfigad -mobile ${MOBILE} 2>&1
  sleep 1
  dsconfigad -mobileconfirm ${MOBILE_CONFIRM} 2>&1
  sleep 1
  dsconfigad -localhome ${LOCAL_HOME} 2>&1
  sleep 1
  dsconfigad -useuncpath ${USE_UNC_PATHS} 2>&1
  sleep 1
  dsconfigad -protocol ${UNC_PATHS_PROTOCOL} 2>&1
  sleep 1
  dsconfigad -packetsign ${PACKET_SIGN} 2>&1
  sleep 1
  dsconfigad -packetencrypt ${PACKET_ENCRYPT} 2>&1
  sleep 1
  dsconfigad -passinterval ${PASSWORD_INTERVAL} 2>&1
  if [ -n "${ADMIN_GROUPS}" ]
  then
    sleep 1
    dsconfigad -groups "${ADMIN_GROUPS}" 2>&1
  fi
  sleep 1

  if [ -n "${AUTH_DOMAIN}" ] && [ "${AUTH_DOMAIN}" != 'All Domains' ]
  then
    dsconfigad -alldomains disable 2>&1
  else
    dsconfigad -alldomains enable 2>&1
  fi
  AD_SEARCH_PATH=`dscl /Search -read / CSPSearchPath | grep "Active Directory" | sed 's/^ *//' | sed 's/ *$//'`
  if [ -n "${AD_SEARCH_PATH}" ]
  then
    echo "Deleting '${AD_SEARCH_PATH}' from authentication search path..." 2>&1
    dscl localhost -delete /Search CSPSearchPath "${AD_SEARCH_PATH}" 2>/dev/null
    echo "Deleting '${AD_SEARCH_PATH}' from contacts search path..." 2>&1
    dscl localhost -delete /Contact CSPSearchPath "${AD_SEARCH_PATH}" 2>/dev/null
  fi
  dscl localhost -create /Search SearchPolicy CSPSearchPath 2>&1
  dscl localhost -create /Contact SearchPolicy CSPSearchPath 2>&1
  AD_DOMAIN_NODE=`dscl localhost -list "/Active Directory" | head -n 1`
  if [ "${AD_DOMAIN_NODE}" = "All Domains" ]
  then
    AD_SEARCH_PATH="/Active Directory/All Domains"
  elif [ -n "${AUTH_DOMAIN}" ] && [ "${AUTH_DOMAIN}" != 'All Domains' ]
  then
    AD_SEARCH_PATH="/Active Directory/${AD_DOMAIN_NODE}/${AUTH_DOMAIN}"
  else
    AD_SEARCH_PATH="/Active Directory/${AD_DOMAIN_NODE}/All Domains"
  fi
  echo "Adding '${AD_SEARCH_PATH}' to authentication search path..." 2>&1
  dscl localhost -append /Search CSPSearchPath "${AD_SEARCH_PATH}"
  echo "Adding '${AD_SEARCH_PATH}' to contacts search path..." 2>&1
  dscl localhost -append /Contact CSPSearchPath "${AD_SEARCH_PATH}"

  if [ -n "${UID_MAPPING}" ]
  then
    sleep 1
    dsconfigad -uid "${UID_MAPPING}" 2>&1
  fi
  if [ -n "${GID_MAPPING}" ]
  then
    sleep 1
    dsconfigad -gid "${GID_MAPPING}" 2>&1
  fi
  if [ -n "${GGID_MAPPING}" ]
  then
    sleep 1
    dsconfigad -ggid "${GGID_MAPPING}" 2>&1
  fi

  GROUP_MEMBERS=`dscl /Local/Default -read /Groups/com.apple.access_loginwindow GroupMembers 2>/dev/null`
  NESTED_GROUPS=`dscl /Local/Default -read /Groups/com.apple.access_loginwindow NestedGroups 2>/dev/null`
  if [ -z "${GROUP_MEMBERS}" ] && [ -z "${NESTED_GROUPS}" ]
  then
    echo "Enabling network users login..." 2>&1
    dseditgroup -o edit -n /Local/Default -a netaccounts -t group com.apple.access_loginwindow 2>/dev/null
  fi

  #
  # Self-removal
  #
  if [ "${SUCCESS}" = "YES" ]
  then
    if [ -e "/System/Library/CoreServices/ServerVersion.plist" ]
    then
      DEFAULT_REALM=`more /Library/Preferences/edu.mit.Kerberos | grep default_realm | awk '{ print $3 }'`
      if [ -n "${DEFAULT_REALM}" ]
      then
        echo "The binding process looks good, will try to configure Kerberized services on this machine for the default realm ${DEFAULT_REALM}..." 2>&1
        /usr/sbin/sso_util configure -r "${DEFAULT_REALM}" -a "${ADMIN_LOGIN}" -p "${ADMIN_PWD}" all
      fi
      #
      # Give OD a chance to fully apply new settings
      #
      echo "Applying changes..." 2>&1
      sleep 10
    fi
    if [ -e "${CONFIG_FILE}" ]
    then
      /usr/bin/srm -mf "${CONFIG_FILE}"
    fi
    /usr/bin/srm -mf "${0}"
    exit 0
  fi
fi

exit 1
</string>
</dict>

DS Automatic Reboot

If you would like Image to automatically reboot after deploying your image (and begin it’s first boot processes), add this somewhere in your workflow.

<key>restart_action</key>
<string>restart</string>

Final Thoughts

As you can see, many of DeployStudio’s checkboxes are very small configuration changes. While none of these examples contain any error checking, they work quite well.

Through the years my workflows have slimmed down considerably. Most have been converted to mobile configuration files, while others have simply moved into munki/outset. While you may consider some aspects of DeployStudio essential, I continue to find myself decreasing the amount of steps needed for a “successful image”. As you transfer your workflows over to Imagr, think about trimming down as much fat as possible. In the end your results will be much more stable and you’ll become a more dynamic admin.

Using Munki Manifest Selector with Imagr

There is one tool that is integral to my technicians workflow and my sanity. With several hundred locations, I needed a way to properly set Munki’s ClientIdentifier. At previous employers, I was able to define a machine naming convention that worked well, but with so many moving parts where I am at now, I needed something dynamic, consistent and stable: Munki Manifest Selector

If you have never used it, I highly recommend at least trying it out. It requires minimal work and although I use an internal fork, the functionality is for the most part identical. Joe has a great post here

As I began to test and move my workflows to Imagr, I hit a road block. While both DeployStudio and Imagr have a generic task, they are radically different architecturally.

DeployStudio vs Imagr Generic Task:

DeployStudio

Since DeployStudio mounts a network volume, the Runtime has direct access to MMS. All an admin needs to do is place both Munki Manifest Selector.app and a corresponding script into the DeployStudio Scripts folder and then call it with a Generic Task. Here is an example script.

#!/bin/bash
BASE_DIR=`dirname "${0}"`
$BASE_DIR/Munki\ Manifest\ Selector.app/Contents/MacOS/Munki\ Manifest\ Selector\
    --targetVolume "${DS_LAST_SELECTED_TARGET}"\
Imagr

Imagr’s Generic Tasks are completely different. In many ways they mimic the behavior of Munki. Scripts are taken from imagr_config.plist and then ran.

<dict>
    <key>type</key>
    <string>script</string>
    <key>content</key>
    <string>#!/bin/bash
/usr/bin/touch "{{target_volume}}/some_file"</string>
    <key>first_boot</key>
    <false/>
</dict>

Unlike DeployStudio, all Imagr components are downloaded individually. What’s an admin to do?

Get Creative – Curl to the rescue!

To begin, let’s download MMS here. For this example please rename the dmg to “Munki_Manifest_Selector.dmg”

If you’ve followed Nick McSpadden’s Imagr Guide, in your recently made Imagr folder, create a new folder called “packages” and place the DMG there.

mkdir -p /yourmunkirepo/imagr/packages

After placing it there, we need to add a workflow to your imagr_config.plist but let’s break this down first.

After pulling down the image and verifying a successful deployment, we are going to utilize Imagr’s Generic Task with first_boot disabled. This task is going to do a few things:
– Curl the DMG (using the new deployment’s curl binary)
– Mount the dmg using hdiutil from the NBI
– Run Munki Manifest Selector and wait for user input
– Unmount the DMG using hdiutil from the NBI
– Remove the DMG from the deployment

Currently, AutoNBI does not add the curl binary. Don’t fret though – it is going to happen. For now we will use this somewhat hacky method.

<?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>workflows</key>
  <array>
    <dict>
      <key>name</key>
      <string>Munki Manifest Selector Test </string>
      <key>restart_action</key>
      <string>restart</string>
      <key>description</key>
      <string>This workflow deploys an AutoDMG base image with Munki Manifest Selector</string>
      <key>components</key>
      <array>
        <dict>
          <key>type</key>
          <string>image</string>
          <key>url</key>
          <string>http://10.10.10.10/imagr/masters/OS_X_10.10.3-14D136.hfs.dmg</string>
        </dict>
        <dict>
            <key>type</key>
            <string>script</string>
            <key>content</key>
            <string>#!/bin/bash

# Downloading MMS DMG
"/Volumes/Macintosh HD/usr/bin/curl" http://10.10.10.10/imagr/packages/Munki_Manifest_Selector.dmg -o "/Volumes/Macintosh HD/private/tmp/Munki_Manifest_Selector.dmg"
sleep 1

# Mount MMS DMG
hdiutil attach "/Volumes/Macintosh HD/private/tmp/Munki_Manifest_Selector.dmg"
sleep 1

# Run MMS
"/Volumes/Munki_Manifest_Selector/Munki Manifest Selector.app/Contents/MacOS/Munki Manifest Selector" --targetVolume "/Volumes/Macintosh HD"
sleep 2

# Unmount MMS DMG
hdiutil unmount "/Volumes/Munki Manifest Selector"
sleep 1

# Delete MMS DMG
rm -rf "/Volumes/Macintosh HD/private/tmp/Munki_Manifest_Selector.dmg"

exit 0
            </string>
            <key>first_boot</key>
            <false/>
        </dict>
  </array>
</dict>
</plist>

Voila!

That’s it. There’s no need to re-architect MMS – just simply wrap it in a DMG. Obviously this is a first version and there isn’t any download verification but with MMS being so small (~100k) for most deployments this should be sufficient.

Don’t stop yourself from moving to Imagr. Get creative and you will be happy with the results.

Yosemite Style Banners for Munki 2

Customizing munki can be quite rewarding when done right. By utilizing the client_resources.zip file you can make a great looking GUI for your users.

Trick 1: Add CSS into footer_template.html

Bart Reardon first documented this trick. You can modify (for better or worse) the css Munki utilizes by adding your desired changes directly into the footer_template.html. The following is what I am currently using to make Munki look more like the App Store in Yosemite.

<style>
html, body {
    -webkit-background-size: auto;
    background-repeat: repeat-x;
    background-color: #ffffff;
    background: -webkit-linear-gradient(top, #ffffff 0%, #f5f5f5 75%, #e1e1e1 100%);
    background-attachment: fixed;
}

This simply changes the background color to white and adds a subtle gradient to the bottom of the window. You can add CSS to any template file, but the footer_template is on every page view.

Trick 2: Adding an icon to the sidebar (sidebar_template.html)

Adding an icon to the sidebar is quite simple – simply add your .png file to your resources folder and use a relative link. Make sure you center it (and don’t go over 128×128 px) or it will look terrible!

<div class="sidebar">
    <div class="chart titled-box quick-links">
        <h2>Quick Links</h2>
        <div class="content">
           		<div class="artwork">
                	<center><img target="_blank" href="https://github.com/munki/munki" width="128" height="128" alt="Munki Github" class="artwork" src="custom/resources/MSC.png" />
                </div>
            </ol>
        </div>
    </div>
</div>

Trick 3: Linking banners to Optional Installs (showcase_template.html)

This isn’t really a trick, but more of a feature that isn’t completely documented. You can link individual banners to specific items in your repository.

Below you will find three examples: All Categories, Music and iMovie. Make sure you add .html to each item or it will not work.

<div class="showcase">
    <div class="stage" onClick='stageClicked();'>
        <img href="categories.html" alt="Categories" src="custom/resources/App_Store_1.png" />
        <img href="category-Music.html" alt="Music Category" src="custom/resources/Making_Music.png" />
        <img href="detail-iMovie.html" alt="iMovie" src="custom/resources/iMovie.png" />
    </div>
</div>

MSC Yosemite
Erik’s Sweet Giveaway (Oprah Style)

Attached below are several banners that I have modified from the Mac App Store. These banners are the correct dimension (1158×200) and configured in a way so no matter how small MSC is, items will not be cut off. Enjoy!

1Password App_Development App_Store_1 Apps_For_Photographers Apps_Made_By_Apple Autodesk_Pixlr Autodesk_Sketchbook Better_Together Business_Apps Clear Compressor DayOne DJay_Pro Evernote Fantastical_2 Final_Cut_Pro_X GarageBand Get_Stuff_Done iA_Writer_Pro iBooks_Author iMovie Keynote Logic_Pro_X Mainstage Making_Music Microsoft_OneNote Motion Notability Notification_Center_Widgets Numbers Pages Pixelmator Reeder Skitch Twitter Wunderlist Xcode Yosemite

Controlling the Diagnostics & Usage report settings on Yosemite – A profile alternative

For the past few weeks I have been trying to rid myself of all Default User Template changes. The last piece to the puzzle is Setup Assistant.

Beginning with Yosemite, Apple introduced a new page for submitting diagnostics and usage. Rich Trouton and Tim Sutton had documented this fairly well, but I wanted to put this in a mobile configuration profile. After digging around, it looks as if Apple has now added a feature in Profile Manager for this feature.

Attached is an example of the settings you will need to manage.

Once applied by your favorite tool, you’re golden.

Using Luggage, Outset and Yo for awesome User Notifications

I am currently in the midst of radically changing the OS X experience at my company. Users will be going from monolithic/unmanaged to a self-service/managed model. This could be jarring for them and without proper user training, they could easily miss some of the key features we would like them to know about.

Enter Yo. Written by Craig Shea and utilizing Swift, it allows you to target Notification Center.

By utilizing Outset, Luggage, and Yo, we can create a user login condition that will notify the user of available documentation.

YoExample

Let’s begin…

On a test machine, install the latest versions of Luggage, Outset and Yo. It will help to have Pages, Xcode and the Xcode command line tools as well.
https://github.com/unixorn/luggage/releases
https://github.com/chilcote/outset/releases
https://github.com/sheagcraig/yo/releases

Create a working directory. When playing with things like this, I prefer the desktop. Let’s open terminal.

mkdir -p ~/Desktop/YoExample

If you have Pages, go ahead and make a document, export as a PDF and save it to our working directory. In this example, let’s use yo.pdf.

Screen Shot 2015-03-26 at 6.40.05 PM

Outset is a powerful tool that allows you to run packages/scripts at various stages. Let’s create a script and save it in our working directory. Outset requires that your scripts contain the proper extension, so don’t forget it.

Once again in terminal:

cd ~/Desktop/YoExample
nano yo.sh

This will bring up a command line editor called nano. Nano is a replacement for pico and recommended by Apple for modifying configuration files.

Inside nano let’s copy/paste the following:

#!/bin/sh
/Applications/Utilities/Yo.app/Contents/MacOS/yo\
	-t "Yo is awesome" \
	-s "To view the document" \
	-n "Please click on Open PDF" \
	-b "Open PDF" \
	-a "/Library/Documentation/Yo.pdf" \
	-p \
	-z "sms_alert_note"

Once entered, press ctrl+o to write the file. It will confirm the name you previously entered – hit enter to save. If done correctly, you should now have a bash script file located in your working directory.

Craig has some great documentation. Let’s look at the values we have setup.

-t, –title:
Title for notification. REQUIRED.
-s, –subtitle:
Subtitle for notification.
-n, –info:
Informative text.
-b, –action-btn:
Include an action button, with the button label text supplied to this argument.
-p, –poofs-on-cancel:
Set to make your notification ‘poof’ when the cancel button is hit.
-a, –action-path:
Application to open if user selects the action button. Provide the full path as the argument. This option only does something if -b/–action-btn is also specified. Defaults to opening nothing.
-z, –delivery-sound:
The name of the sound to play when delivering. Usually this is the filename of a system sound minus the extension. See the README for more info.

We are going to have a title, subtitle and informative text. For a little flair, we will have the notification “poof” if a user hits cancel. By pointing to a private framework sound, we can also use the same alert that a user will hear when receiving an iMessage (located at /System/Library/PrivateFrameworks/ToneLibrary.framework/Versions/A/Resources/AlertTones/Modern/). The action path itself can point to anything and invokes the open command. The options are endless.

If you are paying attention, you’ll notice this script is pointing to /Library/Documentation/yo.pdf. How might one get this file to this location? You could manually move it for this test or build a pkg with Packages, but let’s take a slightly different approach.

Enter Luggage, a great tool that doesn’t seem to get a lot of blog posts. For this post we won’t go too in depth, but let’s do a quick overview.

Luggage allows you to easily create a package, based on a set of parameters through the use of a Makefile.

Let’s use nano once again to make our file.

cd ~/Desktop/YoExample
nano Makefile

Copy/paste the following code and save it.

USE_PKGBUILD=1
include /usr/local/share/luggage/luggage.make
PACKAGE_VERSION=1.0
TITLE=YoDocumentationExample
REVERSE_DOMAIN=com.github.erikng
USE_PKGBUILD=1
PAYLOAD= \
		pack-usr-local-outset-login-once-yo.sh \
		pack-Library-Documentation-yo.pdf

l_usr_local_outset_login_once: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/login-once
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/login-once
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/login-once

pack-usr-local-outset-login-once-%: % l_usr_local_outset_login_once
	@sudo ${INSTALL} -m 755 -g wheel -o root &amp;quot;${&amp;amp;lt;}&amp;quot; ${WORK_D}/usr/local/outset/login-once

l_Library_Documentation: l_Library
	@sudo mkdir -p ${WORK_D}/Library/Documentation
	@sudo chown root:admin ${WORK_D}/Library/Documentation
	@sudo chmod 775 ${WORK_D}/Library/Documentation

pack-Library-Documentation-%: % l_Library_Documentation
	@sudo ${INSTALL} -m 664 -g admin -o root &amp;quot;${&amp;amp;lt;}&amp;quot; ${WORK_D}/Library/Documentation

Let’s briefly discuss what we are doing here.
l_usr_local_outset_login_once tells Luggage to make the directories for Outset (if they don’t already exist) and ensure proper permissions. l_Library_Documentation does the same thing for our Documentation folder.

pack-usr-local-outset-login-once tells Luggage to install our script into the correct Outset folder (calling the l_usr_local_outset_login_once) and pack-Library-Documentation installs our PDF.

With our Makefile in hand, let’s generate our pkg file.

In terminal type:

make pkg

Luggage will ask you for your password. If everything goes right, a package will be generated in your working folder called YoDocumentationExample-1.0 and terminal will look something like this.

Please note that Luggage is very particular with tab spacing and it is possible that wordpress will strip them. See the bottom of the post for the actual files.

Screen Shot 2015-03-26 at 7.24.05 PM

With your package in tow, install it, reboot (or logout) and log back in to see your notification appear.

YoExample

If you click on Open PDF, you should see something like this:

YoPDF

Great! You have a fully functioning setup for all users, and a deployable package. You can stop here but what if you want more?

What if you want to change the icon on the alert? For this, we will need to download the source files and modify them in Xcode. Back in terminal let’s use git to clone Craig’s github repository. Make sure you have already installed Xcode and the command line tools prior to running this command or it will fail.

cd ~/Desktop/YoExample
git clone https://github.com/sheagcraig/yo.git

Browse to your YoExample folder, and a new folder called yo will now exist. Double click on yo.xcodeproj to open the project in Xcode.

Expand the root yo folder, the sub folder called yo and the Supporting Files folder. Click on Images.xcassets and then click on AppIcon. Drag and drop any PNG image (size 128×128) into the Mac 128pt 1x area and replace the original icon. Here is a great icon you could use.

neaglecon
NeagleCon!

XCodeIcon

Now that we’ve replaced the icon, let’s change the BundleIdentifier to ensure our new icon will take effect. Click on Info.plist and change the bundle identifier to something you see fit. In this example let’s use com.github.erikng.

XcodeInfo

With our changes made, let’s save the project (CMD +S) and build our project (CMD+B).

By default Xcode saves projects to ~/Library/Developer/Xcode/DerivedData.
Once there, you should find a folder called “yo-xxxxxxxxx”. Expand that folder and continue down the rabbit hole.
~/Library/Developer/Xcode/DerivedData/yo-xxxxxxxxx/Build/Products/Debug

You should see an application bundle with your new (awesome) icon. Copy this bundle to our original working folder ~/Desktop/YoExample.

Have I told you how awesome Luggage is? It’s awesome. Let’s change the payload section of our Makefile. Using nano or your favorite text editor, change the payload section to the following:

PAYLOAD= \
		pack-usr-local-outset-login-once-yo.sh \
		pack-Library-Documentation-yo.pdf \
		pack-utilities-yo.app

Once again in terminal browse to your working directory and make pkg.

cd ~/Desktop/YoExample
nano Makefile

Your package will regenerate and now include your customized application. Install the new package, log out and log back in and your notification should re-appear, but look different.

YoNotification

Congrats! You now have a customized notification that will appear for all of your users and a package you can deploy out with your favorite deployment tool.

If you’d like to use my own example go to https://github.com/erikng/blogposts for all the necessary files.

Outset Luggage Companion

l_usr_local_outset_custom: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/custom
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/custom
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/custom

l_usr_local_outset_everyboot_scripts: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/everyboot-scripts
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/everyboot-scripts
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/everyboot-scripts
	
l_usr_local_outset_firstboot_packages: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/firstboot-packages
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/firstboot-packages
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/firstboot-packages
	
l_usr_local_outset_firstboot_scripts: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/firstboot-scripts
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/firstboot-scripts
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/firstboot-scripts
	
l_usr_local_outset_login_every: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/login-every
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/login-every
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/login-every
	
l_usr_local_outset_login_once: l_usr_local
	@sudo mkdir -p ${WORK_D}/usr/local/outset/login-once
	@sudo chown -R root:wheel ${WORK_D}/usr/local/outset/login-once
	@sudo chmod -R 755 ${WORK_D}/usr/local/outset/login-once
	
pack-usr-local-outset-custom-%: % l_usr_local_outset_custom
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/custom
	
pack-usr-local-outset-everyboot-scripts-%: % l_usr_local_outset_everyboot_scripts
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/everyboot-scripts

pack-usr-local-outset-firstboot-packages-%: % l_usr_local_outset_firstboot_packages
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/firstboot-packages

pack-usr-local-outset-firstboot-scripts-%: % l_usr_local_outset_firstboot_scripts
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/firstboot-scripts

pack-usr-local-outset-login-every-%: % l_usr_local_outset_login_every
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/login-every
	
pack-usr-local-outset-login-once-%: % l_usr_local_outset_login_once
	@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/login-once
USE_PKGBUILD=1
include /usr/local/share/luggage/luggage.make
PACKAGE_VERSION=1.1
TITLE=OutsetMakefileExample
REVERSE_DOMAIN=com.github.outset
PAYLOAD= \
		pack-usr-local-outset-custom-sample.py \
		pack-usr-local-outset-everyboot-scripts-sample_script_every.py \
		pack-usr-local-outset-firstboot-packages-sample_pkg.dmg \
		pack-usr-local-outset-firstboot-scripts-sample_script_firstboot.py \
		pack-usr-local-outset-login-every-sample_script_every.py \
		pack-usr-local-outset-login-once-sample_script_once.py