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
LoginHook
/path/to/script
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](http://rsug.itd.umich.edu/software/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](http://rsug.itd.umich.edu/software/radmind/download.html)
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](http://www.bombich.com/mactips/scripts.html) 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