Automate ASR Image Creation Using Radmind & Scripts

By: Richard Glaser - Revised: 2008-01-28 ben

Background

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.

Disk Utility - Zero Out Data

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.

Imaging Station Setup

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.

Download Script – -File, 10.5 KB

#!/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>