Scrawls from Preston...

Powered by Pelican.

Thu 19 March 2009

Managing Macs with centralized login scripts

Managing a large number of Macs at an institution often requires a hodgepodge of tricks and tools. While Apple packages some useful stuff os OS X server, many of the most useful things are hidden, created by the community, or rely on OS X's UNIX underpinnings. One of the tricks that I came up with a couple years back that I have found invaluable, is a system where by I can create and manage a set of scripts on a server, that get run on each client at login. Read on for details and some snippets.

First some prerequisites and expectations. You will need a server on your network that supports sharing volumes to OS X, and a way to configure your clients to automount such a sharepoint. Both of these are easily achieved if you have an OS X server that your clients are binding to - but there are other ways. Also I assume that you have some familiarity with what shell scripts are, and file permissions, if not more of a command on how to write them.

At its most basic level, a system of login scripts relies on a single hook provided by the system. In the plist at:




This just is the path to an executable script that will be run with root privileges at user login. The script is passed a single argument, which is the name of the logging in user.

In this script you can do any number of things you can do in a script - it runs with root privileges (I'll outline some of those things later on).

Now if you start doing a lot of things in this script, or you want to do some things on some machines but not others, managing the contents of this script can get tricky or tedious. The solution here is the solution for many similar situations. Modularize it into a set of separate scripts. This has the advantage of atomizing different purposes into their own files that can be moved and reused, or even downloaded from other sources and added into your process. It also allows you to use scripts written in multiple languages. You may have some things that are best done in Bash, others that are better done with Python or Perl.

The basic way to modularize this is to have a folder of scripts, and then your login script pointed to by the plist key acts as a wrapper that might look like this

#! /bin/sh

export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin


if [ -d ${DIR} ]; then
    for script in ${DIR}/\*; do
    # if the file exists and is executable
    if [ -s ${script} -a -x ${script} ]; then
        # run the item
        ${script} $\*
        # bail if any sub script returns abnormally
        if [ ${exit\_value} -ne 0 ]; then
        logger -s -t Loginscript -p ${script} failed! 1>&2
        exit $exit\_value

echo Loginscript complete.
exit 0

This basically loops over the directory specified (in this case /etc/loginscripts/) and runs each script in turn - passing along whatever original arguments were sent to the primary loginscript.

This system has been used by several management packages, including both NetRestore, and Radmind/iHook.

>This is as good a time as any for a quick digression on [iHook]( iHook is a fantastic tool that allows you to provide graphical feedback of scripts that are running. If your login scripts are doing things that might take some time - there is no built in way to give the user feedback. iHook provides a UI window that scripts can write their output to (echo) or using iHook's special commands, can update a progress bar or put up images.

>I'm not going into the details here on iHook, but I use a modified version of the multiple scripts system that is distributed with the [Radmind Assistant](

The new (though I'm not claiming I'm the first/only one to think of or implement this) idea I want to introduce in this post is to take the modular approach another step and use a network folder to hold the scripts to be run at login.

That is, the login script first fetches all the scripts to run from a server, then runs them all locally.

The advantages of this are huge - mainly you no longer have to foresee at imaging time all the possible things you may want to do at login with a script, in order to have those actions written into scripts that are distributed as part of your image. The advantage over via ARD for taking such late breaking actions is that there is no issue if you have many laptops or machines that are often offline on your network, the commands are run at next login.

For me one of the best examples is printer creation. In the Tiger days I dutifully tried to deploy MCX printing - but had any number of problems with it, but primarily:

* Printers that I removed from the list, were not removed from the clients

* Many printer specific features could not be managed via MCX

Now I create printers with a script at login time - if we add or remove a printer from that list, I can make that change to a single copy of the script on the server, and all the machines who participate in this system will see the changes at login.

Another example is that midway through the school year, the administration asked that cameras on the Macs be disabled for students because they were posting to YouTube from campus. I was able to add in a script that did this to the folder on the server and poof, it was implemented instantly on all the machines at next login.

I'm using a combination of locally and remotely modular loginscripts that look something like this:

* login.hook (starts iHook with the contents of the wrapper script as above )

* locally defined login scripts are each run from /etc/hooks

* one of these scipts then fetches additional scripts from the server.

* each local copy of the remotely defined scripts is in turn run

The script that fetches and runs the remote scripts looks like this

if [ -d /Network/Library/management08/ ]; then
        echo Running Centralized Management
        mkdir /tmp/manage
        cp /Network/Library/management08/scripts/\* /tmp/manage
        chmod 755 /tmp/manage/\*
        for x in /tmp/manage/\*
                $x $\*
        rm -f /tmp/manage/\*
exit 0

I'm using OS X's server ability to define an automount at /Network/Library as this is in the search path for several other apps/technology that look in various Libraries. Obviously you want to make sure that such a location is read-only.

OK, so what are some of the useful things you can do with login scripts (local or remote)? Here is a collection of some snippets (see also Mike Bombich's [collection]( for more ideas). Remember these are snippets and should not just be pasted en-mass into one of your login scripts. Let me know of your own tricks in the comments! (and yes - I know many of these can also be done via MCX - I do use MCX, but sometimes this seems more straightforward to me, and generally works more predictably)

# set a user's preference (in this case disabling perian update

sudo -u $1 defaults write org.perian.Perian NextRunDate -date
'4000-12-31 16:00:00 -0800'

# install that cool little tool you didn't know about when making your


if [ ! -e /usr/local/bin/duti ]; then

    ditto /Network/Library/management08/files/duti/usr/local/


# then use that cool little tool to set quicktime player (not itunes)
as the default player for .wav files

# that are generated by that new phone system ;-)

echo ' all' \|
sudo -u $1 /usr/local/bin/duti

# This grabs the user's home directory path


input=\`dscl localhost read Search/Users/$user NFSHomeDirectory\`


# so you can then do things like disable the "Sharing" section of the

sudo -u $1 /usr/libexec/PlistBuddy -c "Set
bool True" $nethomedir/Library/Preferences/

sudo -u $1 /usr/libexec/PlistBuddy -c "Set
bool False" $nethomedir/Library/Preferences/

sudo -u $1 /usr/libexec/PlistBuddy -c "Set
bool False" $nethomedir/Library/Preferences/

# delete all printers and settings:

 # Clean up from previous script if needed

rm -f /tmp/print\*

# Set user variable


# This grabs the user's home directory server

input=\`dscl localhost read Search/Users/$user NFSHomeDirectory \|
head -1\`



### Delete Current Printers

# From the 10.4 script inside print utility

killall "PrinterProxy" "Printer Setup Utility" "cupsd" 2>/dev/null


# Give them all a chance to die

sleep 1


rm -rf $nethomedir/Library/Printers/\*.app

rm -rf $nethomedir/Library/Preferences/\*

rm -rf $nethomedir/Library/Preferences/ByHost/\*

rm -rf /etc/cups/printers.conf\*

rm -rf /etc/cups/classes.conf\*

rm -rf /etc/cups/ppds.dat

rm -rf /etc/cups/ppds/\* 2>/dev/null


launchctl start org.cups.cupsd

# set up printers using lpadmin:

#HP socket example

lpadmin -p "science\_laser" -L science -D "Science B&W (D)" -E -v
socket:// -P
"/Library/Printers/PPDs/Contents/Resources/HP LaserJet 2200.gz" -o

# Using airport/bonjour connected printer:

lpadmin -p "laser\_90299" -L 102 -D "Preschool Laser" -E -v
mdns://90299.\_pdl-datastream.\_tcp.local./?bidi -P
"/Library/Printers/PPDs/Contents/Resources/HP LaserJet 1200.gz"


# Using LPD

lpadmin -p "tower\_copier" -L Tower -D "Tower Copier" -E -v
lpd:// -P

# run some stuff only every 5 days - not every login:


if [ ! -e $tfile ]; then

    touch -t 200811010000 $tfile


r=\`find $tfile -mtime +5\`

if [ ! -z $r ]; then

    #do periodic stuff here

    touch $tfile


# fix the poor network performance on the netgear WAPs on the third

MODEL=\`system\_profiler SPHardwareDataType \grep 'Model Name' \|
awk '{print $3}'\`

echo $MODEL

if [ $MODEL == "MacBook" ]; then

    sysctl -w net.inet.tcp.delayed\_ack=0


#disable iSight if not staff member

ISSTAFF=\`dseditgroup -n /Search -m $1 -o checkmember srstaff \awk
'{print $1}'\`


if [ "$ISSTAFF" == "yes" ]; then

    chmod 755


    echo 'Camera Disabled'

    chmod 700


# run a full os update:

osversionlong=\`sw\_vers -productVersion\`


if [ $osvers -eq 4 ]


        MSG='Updating to 10.5.6 this takes about 5 minutes'

        echo $MSG

        #install updates

        hdiutil attach

        echo $MSG

        installer -package '/Volumes/Mac OS X Update
Combined/MacOSXUpdCombo10.5.6.pkg' -target /Volumes/Internal

        shutdown -r now