#!/bin/ksh # clone # ================================= purpose ================================== # The purpose of this script is to clone a primary boot disk to a backup disk. # ================================== usage =================================== # PRODUCTION: Invoke non-interactively with root crontab entry (e.g., kdc1): # 30 3 * * 0 /usr/local/adm/bin/clone c0t0 c0t1 # DEBUGGING: Invoke interactively or with 3rd arg equal to your email address. # GENERAL: Expects two (optionally three) arguments: # first: the device identifier of the primary disk (e.g., c0t0) # second: the device name of the secondary disk (e.g., c0t1) # (third: email address to send debug results to) # ================================= history ================================== # 2001-03-09 dvc First working version. Inspired by 'copyroot' script at # ftp://ftp.mindspring.com/users/mwang/unix-prog/copyroot/. # 2001-03-21 dvc Added usage example to usage message. # 2001-03-22 dvc Moved usage delivery nearer script start. Some cleanup. # 2001-04-25 dvc Now removing restoresymtable. # 2002-02-25 dvc No longer necessary to specify slices in args. No longer # picking up jaz drives and other late-mounted file systems. # 2002-03-06 dvc Extensive revisions concluded this date include: # - better error detection and handling # - more robust I/O management # - now setting log file mode # - using mount dir less likely to collide with existing dir # - corrected CheckPaths error (was picking last path only) # - prompts for continuation when interactive # - in general, made similar to tape backup script # 2002-03-26 dvc Added Max() and revised CheckOldFS() for better formatting. # 2003-06-09 dvc Revisions this date: # - now accepting disk args as just (e.g.) 'c0t0' vs. 'c0t0d0' # - cleanup now includes removing mount point, lost+found dirs # - corrected error in CreateNewFS wherein the slice number of # a failed newfs was not being reported # - now surviving non-existent $_SUBJECT # - now picking up only $PRIMARY file systems in df output # - improved formatting of CheckOldFS output # 2003-12-02 dvc Now dumping stderr (as well as stdout) from newfs. # ================================ functions ================================= function CheckNewFS { ErrMs="" print "Checking newly cloned file systems ..." let i=0 while (( $i < ${#DumpMnts[*]} )) do if $_FSCK -y /dev/rdsk/${BACKUP}s${DumpPrts[$i]} 1> /dev/null then let i=$i+1 else let i=${#DumpMnts[*]} ErrMs="CheckNewFS: Unable to $_FSCK /dev/rdsk/${BACKUP}s${DumpPrts[$i]}" fi done test -n "$ErrMs" # Returns 0 if true ($ErrMs non-null), 1 if false. } function CheckOldFS { print "Checking file systems ..." sleep 1 let i=0 $_DF -k -F ufs | $_GREP $PRIMARY | # Collect data for each $PRIMARY UFS. { while read Dev Size Used Avail PercentUsed Mnt do if $_GREP $Dev $VFSTAB > /dev/null # Don't pick up jaz drives, etc. then # Note: '-q' option for grep not supported, despite man page. let MBs=$Used+500 # Divide KBs by 1000 (rather than 1024) let MBs=$MBs/1000 # since the tape drive manufacturer may DumpDevs[$i]=$Dev # consider a MB to be 1000 KBs. Add 500 DumpMnts[$i]=$Mnt # to compensate for integer truncation. DumpPrts[$i]=${Dev#/dev/dsk/*s} DumpUsed[$i]=$MBs [[ "/" = $Mnt ]] && RootPartition=${DumpPrts[$i]} let i=$i+1 let Total=$Total+$MBs Max $Max ${#Mnt} fi done } let FieldSize=$Max+2 let i=0 print "File systems to clone:" print -- "----------------------" while (( $i < ${#DumpDevs[*]} )) do Print ${DumpDevs[$i]} print -n " " Print "(${DumpMnts[$i]})" $FieldSize 1 Print ${DumpUsed[$i]} 7 print let i=$i+1 done let FieldSize=$Max+31 Print "-- -----" $FieldSize print print -n "Total backup size:" let FieldSize=$Max+10 Print $Total $FieldSize print " MB" } function CheckPaths { typeset CheckedPaths="" Cmd="" CmdStr="$* " Name="" Path="" PathSet="" typeset -i Abort=0 Found=0 Loops=0 Loops2=0 typeset -ir MaxLoops=100 MaxLoops2=10 ErrMs="" print "Checking paths to executables ..." while [[ -n $CmdStr && 0 -eq $Abort ]] do Cmd=${CmdStr%%+( *)} CmdStr=${CmdStr#$Cmd } Name=${Cmd%=*} PathSet="${Cmd#*=}," let Loops=$Loops+1 if [[ -z $Cmd || -z $Name || "," = $PathSet || $Loops -eq $MaxLoops ]] then let Abort=1 ErrMs="$ErrMs\nCheckPaths: Unable to parse command string \"$*\"." else let Found=0 let Loops2=0 CheckedPaths="" while [[ -n $PathSet && 0 -eq $Found && 0 -eq $Abort ]] do Path=${PathSet%%+(,*)} PathSet=${PathSet#$Path,} let Loops2=$Loops2+1 if [[ -z $Path || $Loops2 -eq $MaxLoops2 ]] then let Abort=1 ErrMs="$ErrMs\nCheckPaths: Unable to parse command string \"$*\"." elif [[ -n ${Path##/*} ]]; then ErrMs="$ErrMs\nCheckPaths: Non-absolute path found: $Path." elif [[ -x $Path ]]; then let Found=1 eval "$Name=$Path" 2> /dev/null if (( 0 != $? )) then let Abort=1 ErrMs="$ErrMs\nCheckPaths: Unable to parse command string \"$*\"." fi elif [[ -z $CheckedPaths ]]; then CheckedPaths=${Path%/*} else CheckedPaths="$CheckedPaths or ${Path%/*}" fi done if (( (0 == $Found) && (0 == $Abort) )) then ErrMs="$ErrMs\nCheckPaths: No \"${Path##*/}\" in $CheckedPaths." fi fi done ErrMs=${ErrMs#\\n} test -n "$ErrMs" # Returns 0 if true ($ErrMs non-null), 1 if false. } function CreateNewFS { ErrMs="" print "Creating new file systems on backup disk ..." let i=0 exec 7<&0 # Save STDIN. exec 0<&- # Close it so newfs won't prompt for go-ahead confirmation. while (( $i < ${#DumpMnts[*]} )) do if $_NEWFS /dev/rdsk/${BACKUP}s${DumpPrts[$i]} 1> /dev/null 2>&1 then let i=$i+1 else ErrMs="CreateNewFS: Unable to $_NEWFS /dev/rdsk/${BACKUP}s${DumpPrts[$i]}" let i=${#DumpMnts[*]} fi done exec 0<&7 # Restore STDIN. test -n "$ErrMs" # Returns 0 if true ($ErrMs non-null), 1 if false. } function Exit { typeset Ms=$1 typeset -ir Priority=$2 typeset -i RetCode=$3 typeset Comment=$4 typeset Host="unknown" Log="" Subject=$4 # Set hopefully sane defaults. [ -n "$Ms" ] && print $Ms if [ 0 -ne "$HaveAllCmds" ] then if [[ -n "$BACKUP" ]] then if $_MOUNT | $_GREP "^$BACKUP" then $_UMOUNT $BACKUP fi fi [[ -d "$MOUNT" ]] && $_RMDIR $MOUNT if [[ ! -t 0 ]] # If non-interactive, configure log & try to send message. then if [[ -e "$TMPLOG" ]] then Log=$LOGDIR/${0##*/}.$($_DATE +"%Y%m%d.%H%M") $_MV $TMPLOG $Log $_CHOWN root:sunman $Log $_CHMOD 0640 $Log fi Host=$($_UNAME -n) [ -x "$_SUBJECT" ] && Subject=`$_SUBJECT "$Priority" "$0" "$Host" "$RetCode" "$Comment"` if [[ -r $Log ]] then $_MAILX -s "$Subject" $NOTIFY < $Log else print $Ms | $_MAILX -s "$Subject" $NOTIFY fi fi fi [ 0 != $RetCode ] && print "Exiting ..." exit $RetCode } function Max { typeset Item Max=$1 shift for Item in $@ do if (( $Item > $Max )) then Max=$Item fi done return $Max } function Print { # Primitive printf. Accepts three arguments: # $1 - String to print. # $2 - Desired field width. Optional; defaults to string length. # Fatal error if not an integer (how to check?). # $3 - If non-null, use left justification. Optional; default is right j. typeset Str=$1 typeset -i Spaces=0 if [[ -n $Str ]] then if [[ "" = $2 ]] then print -n $Str else let Spaces=$2-${#Str} if [[ "" = $3 ]] then while (( 0 < $Spaces )) do print -n " " let Spaces=$Spaces-1 done print -n $Str else print -n $Str while (( 0 < $Spaces )) do print -n " " let Spaces=$Spaces-1 done fi fi fi } # =============================== main routine =============================== # Secure the environment and typeset some constants and variables: # ---------------------------------------------------------------------------- set -A DumpDevs DumpMnts DumpPrts DumpUsed unset IFS # Nonprinting chars in next IFS assignment; don't copy & paste! typeset -r IFS=" " typeset -r PATH=/usr/bin:/sbin:/usr/sbin typeset -r SHELL=/bin/ksh typeset -r TZ=AST9ADT typeset -r LOGDIR=/var/local/logs/clone typeset -r MOUNT=/clone.$$ typeset -r _SUBJECT=/usr/local/adm/bin/subject # Local ARSC script. typeset -r VFSTAB=/etc/vfstab typeset -i Abort=0 FieldSize=0 HaveAllCmds=0 Max=0 ValidResponse=0 # Set and check usage: # ---------------------------------------------------------------------------- read USAGE < [debug_address] \ \\\nExample: $0 /dev/dsk/c0t0 /dev/dsk/c0t1 \ \\\nor: $0 c0t0 c0t1 EOS [ 2 -gt $# ] && Exit "$USAGE" 2 -1 # Read in arguments: # ---------------------------------------------------------------------------- PRIMARY=$1 # Source disk to clone from. PRIMARY=${PRIMARY##*/} # Extract basename, if given a path. PRIMARY=${PRIMARY%s[0-9]} # Delete slice number, if present. PRIMARY=${PRIMARY%d[0-9]} # Delete logical unit number, if present. PRIMARY=${PRIMARY}d0 # Postpend LUN of 0. BACKUP=$2 # Target disk to clone to. BACKUP=${BACKUP##*/} # Extract basename, if given a path. BACKUP=${BACKUP%s[0-9]} # Delete slice number, if present. BACKUP=${BACKUP%d[0-9]} # Delete logical unit number, if present. BACKUP=${BACKUP}d0 # Postpend LUN of 0. NOTIFY=${3:-sunman@arsc.edu} # Whom to notify of results. # Set up I/O: # ---------------------------------------------------------------------------- if [[ ! -t 0 ]] # If not interactive. then if [[ -e $LOGDIR ]] then TMPLOG=$LOGDIR/${0##*/}.$$ exec 1>> $TMPLOG 2>&1 # Redirect stdout and stderr to log file. else ErrMs="Unable to access log dir \"$LOGDIR\"." Exit "$ErrMs" 2 -1 "execution error" fi fi # Set/check full paths to externals (note ',' and '\' assignment separators): # ---------------------------------------------------------------------------- read EXTERNALS < /dev/null then print "Dumping ${DumpMnts[$i]} ..." $_UFSDUMP 0f - /dev/rdsk/${PRIMARY}s${DumpPrts[$i]} |\ (cd $MOUNT; $_UFSRESTORE rf - 2>&1 |\ $_GREP -v "Warning: ./lost+found: File exists" |\ $_GREP -v "Cannot open(\"/dev/tty\"): No such device or address"; ) (( $i == $RootPartition )) && $_RMDIR $MOUNT$MOUNT 2> /dev/null $_RM $MOUNT/restoresymtable 2> /dev/null $_RMDIR $MOUNT/lost+found 2> /dev/null $_UMOUNT $MOUNT else Exit "Unable to backup file system ${DumpMnts[$i]}" 2 $? "execution error" fi let i=$i+1 done print "Disk clone completed ... time now is $($_DATE +%Y%m%d.%H%M)" # Check new file systems: # ---------------------------------------------------------------------------- CheckNewFS IsOK=$? [ 0 == $IsOK ] && Exit "$ErrMs" 2 -1 "execution error" # Copy bootblk: # ---------------------------------------------------------------------------- if [[ -z "$RootPartition" ]] then Exit "Unable to identify root partition" 2 -1 "execution error" else BootBlk=/usr/platform/$($_UNAME -i)/lib/fs/ufs/bootblk print "Installing boot block in root partition ..." $_INSTALLBOOT $BootBlk /dev/rdsk/${BACKUP}s$RootPartition fi # Modify vfstab: # ---------------------------------------------------------------------------- $_MOUNT /dev/dsk/${BACKUP}s$RootPartition $MOUNT if $_DF $MOUNT | $_GREP "/dev/dsk/${BACKUP}s$RootPartition" > /dev/null then print "Adjusting $VFSTAB ..." $_CP ${MOUNT}${VFSTAB} ${MOUNT}${VFSTAB}.old $_SED -e "s/$PRIMARY/$BACKUP/g" < ${MOUNT}${VFSTAB}.old \ > ${MOUNT}${VFSTAB} $_UMOUNT $MOUNT else Exit "Unable to update $BACKUP vfstab" 2 $? "execution error" fi # Exit (and cleanup): # ---------------------------------------------------------------------------- Exit "All done." 0 0 "disk clone report" # to do: # revise CheckOldFS to flag nonexistent device (in args to clone) # allow cloning of specified file systems (vs. all or none) # better sanity checking (don't want to newfs mounted FS, for instance) # fsck newly newfs'd file systems, before dumping to them (10813)