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:

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 "/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 "/Library/Preferences/com.apple.SoftwareUpdate" CatalogURL $SUS
chmod 644 "/Library/Preferences/com.apple.SoftwareUpdate.plist"
/usr/sbin/chown root:admin "/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.

Table Of Contents