Automate ASR Image Creation Using Radmind & Scripts
By: Richard Glaser - Revised: 2008-01-28 benBackground
In our environment, we primarily manage around 500 clients with
radmind, but sometimes due to lower level hard disk issues we need to zero-erase & re-image the clients hard disk.The process of erasing a disk, partition, or volume by writing zeros to every sector is called zeroing. Zeroing finds bad sectors and maps them out of service, also known as sparing. When an attempt to write zeros to bad sectors fails, the bad sectors are both marked as occupied in the directory and added to the bad blocks file of the file system. Once the bad sectors have been spared, no attempt will ever be made to read-from or write-to them again.
For details, see Apple Developer Connection Technical Note TN1150, "
HFS Plus Volume Format."
From our experience, zero-out erasing hard disks has resolved issues with hard disks like I/O errors, kernel panics, etc. that wasn't resolved using a standard erase. So, as a standard procedure we zero-erase a hard disk when we re-imaging. Currently, we use NetRestore with pre/post scripts and read our radmind command files to automate image selection based on the client's radmind command file. If there is interest, we can post details in a future article.
To get the client up and working ASAP we have created Apple Software Restore [ASR] images based on our primary configurations:
- Kiosk - PowerPC
- Lab - Intel
- Lab - PowerPC
- Media Editing - PowerPC
We have many more configurations, like server, staff, etc. but based on distribution sizes & software we currently have these primary configurations.
The kiosk is a smallest image, roughly 1.5 GB; lab image, 36 GB and our media editing, 55 GB compressed.
For sheer speed, we decided to use the our primary configurations, instead of one base configuration and then use radmind to bring the client completely up-to-date. We still use radmind, but since radmind uses a file copy vs ASR's block copy, we try to have the ASR images distribute as much of the clients file system as possible, since block copy is much faster than file copy.
To keep the images up-to-date, we have setup imaging stations that use each of these primary configurations. Each imaging station has three volumes. First, the startup disk; second, the image source; and last the images volume where the images are saved.
Then weekly we update our source volume with radmind, and then create ASR image from the source volume, save it to the images volume, then upload it to our server we use with NetRestore to re-image clients. We use the same name for the images, so the NetRestore configuration remains the same.
Wow, you update your ASR images, weekly, that sounds a little obsessive. Yes, you are probably right, but we update our clients radmind file systems fairly often and many times each week and since its automated, it doesn't hurt us updating the ASR image each weekend, just in case we did update the client file system during that particular week.
ASR Creator Script
Below is the script, ASR Creator, that we use to update a volume with radmind, create the ASR image, and then upload it to a server to be with re-imaging.
#!/bin/sh
#ASR Creator V2
##################################
#Global Editable Variables
#Name of the volume that stores the radmind refresh
RADMIND_REFRESH="Refreshed"
#Location of the ASR Images
ASR_IMAGES_LOCATION="Images"
#Radmind server to use
RADMIND_SERVER="your_server@place.edu"
#Transcript Location
TRANS_LOCATION="/var/radmind/client"
#Who to email status updates too
MAIL_TO="you@whereYourAt.edu"
#Where the log file is saved
PATH_TO_LOG_FILE="/var/log/asr_creator_log"
##################################
#Global Non Editable Variables:
#BEFORE_TIME
#AFTER_TIME
#TIME_STAMP
#SET_DATE
##################################
#Functions:
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
set_box_type_name_f()
{
BOX_TYPE=`grep '#ASR_Image_Name:' ${TRANS_LOCATION}/command.K | egrep -o "(lab_intel|lab_ppc|kiosk_ppc|video_ppc)"`
if [ "$BOX_TYPE" = "" ];then
my_logger_f "BoxType naming problem!"
exit
fi
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
my_logger_f()
{
#Interdependencies:
#before_time_f via global var BEFORE_TIME
#after_time_f via global var AFTER_TIME
#Global vars:
#PATH_TO_LOG_FILE
if [ "$1" == "-i" ];then
echo "*****************************************" > "${PATH_TO_LOG_FILE}"
echo "$2" >> "${PATH_TO_LOG_FILE}"
echo "*****************************************" >> "${PATH_TO_LOG_FILE}"
echo >> "${PATH_TO_LOG_FILE}"
return
fi
if [ "$1" == "-n" ];then
echo -n "$2" >> "${PATH_TO_LOG_FILE}"
return
fi
if [ "$1" == "-b" ];then
echo >> "${PATH_TO_LOG_FILE}"
return
fi
if [ "$1" == "-t" ];then
local TIME_TAKEN
local DIV_SIXTY
let "TIME_TAKEN = $AFTER_TIME - $BEFORE_TIME"
let "DIV_SIXTY = TIME_TAKEN / 60"
echo "${2}: $DIV_SIXTY minutes" >> "$PATH_TO_LOG_FILE"
echo >> "${PATH_TO_LOG_FILE}"
return
fi
echo "$1" >> "${PATH_TO_LOG_FILE}"
echo >> "${PATH_TO_LOG_FILE}"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
#These two functions should be rewritten to be one function with different parameters
before_time_f()
{
#Gloabl vars:
#BEFORE_TIME
let "BEFORE_TIME = `date +%s`"
}
after_time_f()
{
#Global vars:
#AFTER_TIME
let "AFTER_TIME = `date +%s`"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
ktcheck_f()
{
local KTCHECK_ERROR
/usr/local/bin/ktcheck -c sha1 -h "$RADMIND_SERVER"
KTCHECK_ERROR="$?"
if [ "$KTCHECK_ERROR" -gt '1' ];then
my_logger_f "ktcheck has an error, exiting script"
exit 1
fi
return "$KTCHECK_ERROR"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
radmind_refresh()
{
if (test ! -d "/Volumes/${RADMIND_REFRESH}") then
disk_identifier_refreshed=`diskutil list | sed -n 's/.*Refreshed.*GB[[:blank:]]*//p'`
diskutil mount $disk_identifier_refreshed
if (test ! -d "/Volumes/${RADMIND_REFRESH}") then
my_logger_f "You need a volume named "${RADMIND_REFRESH}""
exit
fi
fi
ulimit -n 1024
ktcheck_f
#Verify CPU type vs. command file type
typeset CPU_TYPE=`system_profiler | egrep "CPU Type" | egrep -o "(PowerPC|Intel)"`
typeset COMMAND_FILE_TYPE=`egrep .*macosx.* /var/radmind/client/command.K | egrep -v "(neg|#)" | egrep -o "(ppc|intel)"`
if [[ "${CPU_TYPE}" == "PowerPC" && "${COMMAND_FILE_TYPE}" == "intel" ]] || [[ "${CPU_TYPE}" == "Intel" && "${COMMAND_FILE_TYPE}" == "ppc" ]]
then
my_logger_f "Command file type does not match CPU type!!!"
my_logger_f "CPU Type: '${CPU_TYPE}'"
my_logger_f "Command file type: '${COMMAND_FILE_TYPE}'"
exit
fi
#Make the negative base_load a positive
#DEPENDS ON OUR NAMING CONVENTION
sed 's/n os_base_macosx/p os_base_macosx/' "${TRANS_LOCATION}"/command.K > "${TRANS_LOCATION}"/temp_neg_to_pos
mv "${TRANS_LOCATION}"/temp_neg_to_pos "${TRANS_LOCATION}"/command.K
#Ignores permissions on the volume
vsdbutil -a "/Volumes/${RADMIND_REFRESH}"
before_time_f
cd "/Volumes/${RADMIND_REFRESH}"
/usr/local/bin/fsdiff -c sha1 -A ./ | /usr/local/bin/lapply -c sha1 -F -h ${RADMIND_SERVER}
if [ "$?" -gt '0' ];then
ktcheck_f
if [ "$?" != "1" ];then
my_logger_f "lapply errors and command file has not been updated. Please troubleshoot and try again"
my_logger_f "Exiting"
exit 1
fi
/usr/local/bin/fsdiff -c sha1 -A ./ | /usr/local/bin/lapply -c sha1 -F -h ${RADMIND_SERVER}
if [ "$?" -gt '0' ];then
my_logger_f "lapply errors multiple times. Please troubleshoot and try again."
my_logger_f "Exiting"
exit 1
fi
fi
after_time_f
my_logger_f -t "Time lapply spent running"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
create_asr()
{
if (test ! -d "/Volumes/${ASR_IMAGES_LOCATION}") then
disk_identifier_Images=`diskutil list | sed -n 's/.*Images.*GB[[:blank:]]*//p'`
diskutil mount $disk_identifier_Images
if (test ! -d "/Volumes/${ASR_IMAGES_LOCATION}") then
my_logger_f "You need a volume named "${ASR_IMAGES_LOCATION}""
exit
fi
fi
#Checking to see if dmg's already exhist if they do it deletes them
if (test -e "/Volumes/${ASR_IMAGES_LOCATION}/sparse_image.sparseimage") then
rm "/Volumes/${ASR_IMAGES_LOCATION}/sparse_image.sparseimage"
fi
if (test -e "/Volumes/${ASR_IMAGES_LOCATION}/compressed_image.dmg") then
mv "/Volumes/${ASR_IMAGES_LOCATION}/compressed_image.dmg" "/Volumes/${ASR_IMAGES_LOCATION}/old_compressed_image.dmg"
fi
#Now these lines create the disk images
before_time_f
hdiutil create -srcfolder "/Volumes/${RADMIND_REFRESH}/" -format SPARSE "/Volumes/${ASR_IMAGES_LOCATION}/sparse_image"
after_time_f
my_logger_f -t "Time spent creating ASR sparse image"
before_time_f
hdiutil convert "/Volumes/${ASR_IMAGES_LOCATION}/sparse_image.sparseimage" -format UDZO -o "/Volumes/${ASR_IMAGES_LOCATION}/compressed_image"
after_time_f
my_logger_f -t "Time spent creating compressed ASR image"
before_time_f
asr -imagescan -nostream "/Volumes/${ASR_IMAGES_LOCATION}/compressed_image.dmg"
after_time_f
my_logger_f -t "Time spent creating scanning compressed ASR image"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
#Function does not work yet need to see output of disk_verify() several times!
zero_out_HD()
{
diskutil zeroDisk "${Radmind Refreshed}"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
upload_asr()
{
mkdir /tmp/mount_point
mount -t afp "afp://username:password@server.you.use.edu/Mount_point" /tmp/mount_point
chmod -R 755 /tmp/mount_point
typeset TIME_STAMP=`date "+%m.%d.%Y_%H:%M"`
typeset NUMBER_OF_PREVIOUS=`ls /tmp/mount_point | grep -c "${BOX_TYPE}"`
typeset BOOL_COUNT="yes"
if [ "${NUMBER_OF_PREVIOUS}" -gt '0' ];then
for i in `ls -rt /tmp/mount_point/*${BOX_TYPE}*`;do
if [ "$BOOL_COUNT" == "yes" ];then
rm $i
fi
BOOL_COUNT="no"
done
fi
before_time_f
cp "/Volumes/${ASR_IMAGES_LOCATION}/compressed_image.dmg" "/tmp/mount_point/${BOX_TYPE}.dmg"
after_time_f
my_logger_f -t "Time to copy image up to server"
umount /tmp/mount_point
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
mail_status()
{
cat "$PATH_TO_LOG_FILE" | mail -s "ASR Image Update $SET_DATE" "$MAIL_TO"
}
disk_verify()
{
if (test ! -d "/Volumes/${ASR_IMAGES_LOCATION}") then
disk_identifier_Images=`diskutil list | sed -n 's/.*Images.*GB[[:blank:]]*//p'`
diskutil mount $disk_identifier_Images
if (test ! -d "/Volumes/${ASR_IMAGES_LOCATION}") then
my_logger_f "You need a volume named "${ASR_IMAGES_LOCATION}""
exit
fi
fi
if (test ! -d "/Volumes/${RADMIND_REFRESH}") then
disk_identifier_refreshed=`diskutil list | sed -n 's/.*Refreshed.*GB[[:blank:]]*//p'`
diskutil mount $disk_identifier_refreshed
if (test ! -d "/Volumes/${RADMIND_REFRESH}") then
my_logger_f "You need a volume named "${RADMIND_REFRESH}""
exit
fi
fi
typeset VOLUME_STATUS_1=`diskutil verifyVolume "/Volumes/$RADMIND_REFRESH"`
my_logger_f "The volume $RADMIND_REFRESH had this exit status from diskutil verifyVolume:"
my_logger_f "$VOLUME_STATUS_1"
typeset VOLUME_STATUS_2=`diskutil verifyVolume "/Volumes/$ASR_IMAGES_LOCATION"`
my_logger_f "The volume $ASR_IMAGES_LOCATION had this exit status from diskutil verifyVolume:"
my_logger_f "$VOLUME_STATUS_2"
}
#////////////////////////////////////////////////////////////////////////////////////////
#////////////////////////////////////////////////////////////////////////////////////////
##########################
#Main
my_logger_f -i "ASR Creator Log"
my_logger_f -n "Script ran on: "
SET_DATE=`date`
my_logger_f "$SET_DATE"
disk_verify
my_logger_f "Finished disk_verify"
radmind_refresh
my_logger_f "Finished radmind_refresh"
set_box_type_name_f
my_logger_f "Box Type: $BOX_TYPE"
create_asr
my_logger_f "Finished create_asr"
upload_asr
my_logger_f "Finished upload_asr"
mail_status
Scheduling
Under Mac OS X 10.4, the new launchd process invokes each script on a schedule specified in a script-specific property list (.plist file) stored in the /System/Library/LaunchDaemons directory.
We use the following launchd with the following plist file to run the above script every Saturday.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UserName</key>
<string>root</string>
<key>Label</key>
<string>edu.institution.your.imagecreator</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/path_of_script.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>6</integer>
<key>Hour</key>
<integer>0</integer>
<key>Minute</key>
<integer>45</integer>
</dict>
</dict>
</plist>