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:

/var/root/Library/Preferences/com.apple.loginwindow.plist

<key>LoginHook</key>
<string>/path/to/script</string>

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

DIR="/etc/loginscripts"

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} $*
        exit_value=$?
        # bail if any sub script returns abnormally
        if [ ${exit_value} -ne 0 ]; then
        logger -s -t Loginscript -p user.info ${script} failed! 1>&2
        exit $exit_value
        fi
    fi
    done
fi

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/*
        do
                $x $*
        done
        rm -f /tmp/manage/*
fi
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 notices)
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 image
# http://duti.sourceforge.net/
if [ ! -e /usr/local/bin/duti ]; then
    ditto /Network/Library/management08/files/duti/usr/local/ /usr/local/
fi

# 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 ‘com.apple.quicktimeplayer com.microsoft.waveform-audio all’ | sudo -u $1 /usr/local/bin/duti
# This grabs the user’s home directory path
user="$1"
input=`dscl localhost read Search/Users/$user NFSHomeDirectory`
nethomedir=${input:18}


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

sudo -u $1 /usr/libexec/PlistBuddy -c "Set :networkbrowser:CustomListProperties:com.apple.NetworkBrowser.bonjourEnabled bool True" $nethomedir/Library/Preferences/com.apple.sidebarlists.plist
sudo -u $1 /usr/libexec/PlistBuddy -c "Set :networkbrowser:CustomListProperties:com.apple.NetworkBrowser.backToMyMacEnabled bool False" $nethomedir/Library/Preferences/com.apple.sidebarlists.plist
sudo -u $1 /usr/libexec/PlistBuddy -c "Set :networkbrowser:CustomListProperties:com.apple.NetworkBrowser.connectedEnabled bool False" $nethomedir/Library/Preferences/com.apple.sidebarlists.plist


# delete all printers and settings:
 # Clean up from previous script if needed
rm -f /tmp/print
    

# Set user variable
user="$1"
# This grabs the user’s home directory server
input=dscl localhost read Search/Users/$user NFSHomeDirectory | head -1
nethomedir=${input:18}

### Delete Current Printers
# From the 10.4 PrintingReset.sh 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/com.apple.print.
rm -rf $nethomedir/Library/Preferences/ByHost/com.apple.print.

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://10.5.5.36/?bidi -P "/Library/Printers/PPDs/Contents/Resources/HP LaserJet 2200.gz" -o Duplex=DuplexNoTumble

# 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://10.5.5.46/ -P "/Library/Printers/PPDs/Contents/Resources/en.lproj/Kyocera KM-C4035E.PPD"

# run some stuff only every 5 days – not every login:
tfile=/Library/Management/.timestamp
if [ ! -e $tfile ]; then
    touch -t 200811010000 $tfile
fi
r=`find $tfile -mtime +5`
if [ ! -z $r ]; then
    #do periodic stuff here
    touch $tfile
fi
# fix the poor network performance on the netgear WAPs on the third floor
MODEL=`system_profiler SPHardwareDataType | grep ‘Model Name’ | awk ‘{print $3}’`
echo $MODEL

if [ $MODEL == "MacBook" ]; then
    sysctl -w net.inet.tcp.delayed_ack=0
fi
#disable iSight if not staff member
ISSTAFF=`dseditgroup -n /Search -m $1 -o checkmember srstaff | awk ‘{print $1}’`

if [ "$ISSTAFF" == "yes" ]; then
    chmod 755 /System/Library/QuickTime/QuickTimeUSBVDCDigitizer.component/Contents/MacOS/QuickTimeUSBVDCDigitizer /System/Library/PrivateFrameworks/CoreMediaIOServicesPrivate.framework/Versions/A/Resources/VDC.plugin/Contents/MacOS/VDC
else
    echo ‘Camera Disabled’
    chmod 700 /System/Library/QuickTime/QuickTimeUSBVDCDigitizer.component/Contents/MacOS/QuickTimeUSBVDCDigitizer /System/Library/PrivateFrameworks/CoreMediaIOServicesPrivate.framework/Versions/A/Resources/VDC.plugin/Contents/MacOS/VDC
fi
# run a full os update:
osversionlong=`sw_vers -productVersion`
osvers=${osversionlong:5:1}
if [ $osvers -eq 4 ]
then
        MSG=‘Updating to 10.5.6 this takes about 5 minutes’
        echo $MSG
        #install updates
        hdiutil attach /Network/Library/management08/files/installers/MacOSXUpdCombo10.5.6.dmg
        echo $MSG
        installer -package ‘/Volumes/Mac OS X Update Combined/MacOSXUpdCombo10.5.6.pkg’ -target /Volumes/Internal
        shutdown -r now
fi

2 comments ↓

#1 Trevor on 07.30.09 at 3:26 pm

I’ve implemented this at my workplace and have visited this site 20 times or more in the last month to refer back to the solution. Just noticed there are no comments. Well, a hearty thanks Chris, you made my life so much easier

#2 Tim on 12.15.09 at 1:02 am

Wow what a great idea. It’s been a long time since I found some thing as useful as this on the web. I have tried on and off to get MCX login scripts to work via workgroup manager to no avail. Now I know I have different options. I have locked down the screen saver desktop and security preference pane to make sure screen savers kick in with a password prompt to unlock it. The problem I have is trying to prevent users from installing screensavers because it installs them to ~/Library/Screen Savers. Do you know if it is possible to use this method to have a script which denies the user permission to their ~/Library/Screen Savers so they cannot change it from the default flurry one?

Leave a Comment