#!/bin/bash
#$Id: rfs,v 1.49 2006/04/08 10:24:50 nico Exp $
# begin
###############################################################################
# rfs
#
# Copyright (C) 2004 2005 Nicolas Jouanne
# 
# This file is part of rfs, a spare system disk utility
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2 of the License,
# or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.# See the
# GNU General Public License for more details.
# 
###############################################################################

VERSION="2.13"

version() # display version and exit
{
  printf "
rfs version $VERSION.

Copyright (C) 2004 2005 Nicolas Jouanne.

rfs comes WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. For information about the terms of redistribution,
see the file named COPYING in the rfs distribution.
Homepage: http://1nj1.net/rfs/rfs.html

"
  exit 0
}

usage() # display usage and exit
{
  cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
usage   :
   rfs [options] SOURCE TARGET 
options :
   -D  debug mode
   -V  prints version
   -f  force mode, don't ask for confirmation
   -h  this help
   -v  verbose mode.
example :
   rfs /dev/hda /dev/hdb
   rfs -f -v /dev/hda /dev/hdb
authors :
   Nicolas Jouanne <nico@1nj1.net> 
   Eric Gerbier    <gerbier@users.sourceforge.net>
   Project url     http://1nj1.net/rfs/rfs.html
done.
EOF
  exit 0
}

checking_user() # check if the program is launched by user root (it should)
{
  [ "`id -ru`" = "0" ] 
  chk=$? ; return $chk
}

ask() # ask user for confirmation
{
  printf "(II) proceed y|n ? :"
  read answer
  if test "$answer" = "y" ; then
    return 0
  else
    cat <<EOF
(II) done.
EOF
    exit 0
  fi
}

launch() # calls exit function if the launched command fails
{
  # launch [-v] "message" command

  # if `-v' is present or $verbose set to "yes", echo to stdout, else no.
  int_verbose="no"
  if test "$1" = "-v" ; then
    int_verbose="yes"
    shift
  fi
  msg="$1" ; shift ; cmd="$*"

  cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
running \`$cmd'
EOF
  if test "$verbose$int_verbose" = "nono" ; then
    $cmd >/dev/null || out "(EE) $msg"; chk=$?
  else
    $cmd  || out "(EE) $msg"; chk=$?
  fi
  return $chk
}

out() # exit fonction
{
  umount_target
  cat <<EOF
$*
(II) done.
EOF
  exit 1
}

std_space() # converts combinations of spaces and/or tabs to a single space
{
  tr '\t' ' ' | sed -e 's/  */ /g'
    
}

get_digits() # gets the number of the given partition. e.g : hda6 --> 6
{
  sed -e 's/..*[a-z][a-z]*\([0-9][0-9]*\)$/\1/g'
}

cr2sp() # converts carriadge return to space
{
  tr '\n' ' '
}

sp2cr() # converts space to carriadge return
{
  tr ' ' '\n'
}

exclude_comments() # filter out commented lines
{
  grep -v '#'
}

get_field() # get the field given
{
  cut -f$1 -d' '
}

filter() #  /etc/fstab
{
  cat $1 \
  | exclude_comments \
  | std_space
}

umount_target() # unmount all the fs of the target disk 
{
  umount `mount \
  | grep "$dest" \
  | get_field 3 \
  | sort -ru \
  | cr2sp` 2>/dev/null

  # remove temp dir
  [ -d $pivot ] && rmdir $pivot 
}

checking_fstab() # exits if /etc/fstab contains labels or uuid's
{
  mount -a 

  grep  -i -e "LABEL\|UUID" /etc/fstab 2>&1 >/dev/null ; chk=$?

  if [ "$chk" = "0" ] ; then
    cat <<EOF | sed -e 's/\(..*\)/(EE) \1/' 
\`LABEL=' or \`UUID=' entries detected in /etc/fstab.
fstab entries have to start with a device name, e.g : \`/dev/hxx'.
EOF
    return 1
  fi
  filter /etc/fstab | grep "$dest" 2>&1 >/dev/null
  chk=$?
  if [ "$chk" = "0" ] ; then
    cat <<EOF | sed -e 's/\(..*\)/(EE) \1/'
/etc/fstab refers to following $dest partition(s) :
`cat /etc/fstab | exclude_comments | grep "$dest"`
please comment those entries and rerun rfs.
EOF
    return 1
  fi
  
  # retrieve list of partitions to clone from /etc/fstab ( /dev/xxx )

  fstab="`filter /etc/fstab \
  | grep $src \
  | sort \
  | get_field 1 \
  | cr2sp`"  
  
  # retrieve list of partitions to clone by reading partition tables
 
  # tr ':' ' ' added below for cases like :
  #      /dev/sda11: start= 29350818, size=  4192902, Id=83
  
  ptab="`sfdisk -d $src \
  | grep \"^$src\" \
  | sort \
  | grep -e 'Id=82\|Id=83' \
  | std_space \
  | tr ':' ' ' \
  | get_field 1 \
  | cr2sp`"  
  
  # compare partitions lists

  [ "$fstab" = "$ptab" ] ; chk=$?
  
  if [ "$chk" != "0" ] ; then
    cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
$src partitions listed in /etc/fstab : $fstab
$src partitions found on disk        : $ptab
EOF

    # case 1 : surnumerous entries in /etc/fstab
    
    if test $(echo "$fstab" | wc -w) -gt $(echo "$ptab"  | wc -w) ; then
      cat <<EOF | sed -e 's/\(..*\)/(EE) \1/'
there are more partitions listed in /etc/fstab than found on disk.
please update your /etc/fstab before continuing.
EOF
      return 1
    fi
     
    # case 2 : /etc/fstab lacks entries

    if test $(echo "$fstab" | wc -w) -lt $(echo "$ptab"  | wc -w) ; then
      cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
there are more partitions found on disk than listed in /etc/fstab.
you may or may not update your /etc/fstab before continuing.
EOF
      if test "$ask_user" = "yes" ; then 
        ask
      else
        cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
force mode on. continuing."
EOF
      fi
      return 0
    fi

    # case 3 : Dark Vador is close to us.
    if test $(echo "$fstab" | wc -w) -eq $(echo "$ptab"  | wc -w) ; then
      cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
partitions found in /etc/fstab differs from what's really on disk.
EOF
      if test "$ask_user" = "yes" ; then 
        cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
please correct that before continuing, or use \`-f' switch to force execution.
EOF
        return 1
      else
        cat <<EOF | sed -e 's/\(..*\)/(WW) \1/'
force mode on. continuing."
EOF
      fi
      return 0
    fi
  fi
  return 0 
}

checking_partitions() # compare partitions tables of source and target disks
{
  sfdisk -d $src  | exclude_comments | cut -c 13- > $0.1
  sfdisk -d $dest | exclude_comments | cut -c 13- > $0.2
  
  diff $0.1 $0.2 2>&1 >/dev/null ; chk=$? 

  # remove temporary files

  [ -f $0.1 ] && rm -f $0.1
  [ -f $0.2 ] && rm -f $0.2

  return $chk
}

partitionning() # clone partition table of source disk
{
  # -uS switch seems to work better with raid devices ( tested by Eric )
  sfdisk -d -uS $src>$0.1

  # write partition table on target disk
  if test "$verbose" = "yes" ; then
    sfdisk $dest -f -uS < $0.1  ; chk=$?
  else
    sfdisk $dest -f -uS < $0.1 >/dev/null  ; chk=$?
  fi

  # remove temporary file
  [ -f $0.1 ] && rm -f $0.1

  return $chk
}

synchronization() # write to disks
{
  sync 
  chk=$? ; return $chk
}

formatting_partitions() # format newly created partitions on target disk
{ 

  # loop on each partition to be formatted

  for DEV in ` filter /etc/fstab \
    | grep "$src" \
    | get_field 1` \
    ; do

  # the type of the fs is located in the 3rd field of /etc/fstab

  fs_type=`filter /etc/fstab \
  | grep $DEV \
  | get_field 3`
  
  # build the name of the target partition

  target_partition="$dest`echo $DEV | get_digits`"
  
  # format the target partition
  # tune2fs is used to check if it is already formatted or not

  case $fs_type in
    swap)
      launch "making swap failed." mkswap $target_partition
      ;;
    ext2|ext3)
      tune2fs -c 30 $target_partition 2>&1 >/dev/null
      chk=$?
      if [ "${chk}" != "0" ] ; then
        launch  "making ext2 filesystem failed." mke2fs $target_partition
        launch  "making ext3 filesystem failed." mke2fs -j $target_partition
      fi
      ;;
    *)
      ;;
  esac
    
  done
  chk=$?
  return $chk
}

copying() # copy files using rsync
{

  # mount everything
  mount -a

  # temporary directory used to mount the filesystems of target disk
  pivot="/$$"
  
  # directories not to be cloned
  excluded_list="/sys /proc /tmp $RFS_EXCLUDE"

  # restrict clonage to fs of source disk 
  excluded_list="$excluded_list \
  `mount \
  | grep -e "^/dev/" \
  | grep -v "$src" \
  | std_space \
  | get_field 3 \
  | sort -ru` "

  # tell rsync not to clone excluded dirs
  rsync_pattern=""
  for DIR in $excluded_list ; do 
    rsync_pattern="$rsync_pattern --exclude=$DIR/*"
  done

  # exclude $pivot too
  rsync_pattern="$rsync_pattern --exclude=$pivot"

  # list of mount points for source disk

  mount_point_list=`filter /etc/fstab \
  | grep "^$src" \
  | grep -v swap \
  | get_field 2 \
  | sort -u`
  
  #  echo "debug : \$mount_point_list : $mount_point_list"

  # clone everything mounted under each mount point

  for mount_point in `echo $mount_point_list` ; do

    # build the name of the target partition
    dest_device="$dest`filter /etc/fstab \
    | grep \" $mount_point \" \
    | get_field 1 \
    | get_digits`"

    # creation of mount point
    dest_dir="$pivot$mount_point"
    launch "creating mount point $dest_dir failed." mkdir -p $dest_dir

    # mount the target fs
    dest_fs="`filter /etc/fstab \
    | grep \" $mount_point \" \
    | get_field 3`"
    
    launch "mounting target filesystem failed." mount -t $dest_fs $dest_device $dest_dir 

    if test "$verbose" = "yes" ; then
      rsync_options="v"
    fi

    # clone the current fs
    
    cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
running rsync on \`$mount_point'
EOF
    rsync -axR$rsync_options --delete `echo $rsync_pattern` $mount_point $pivot
    chk=$?
    if test "$chk" != "0" ; then
      cat <<EOF | sed -e 's/\(..*\)/(WW) \1/' 
rsyncing $mount_point failed.
skipping to next partition.
EOF
    fi

# /dev specific
    rsync -axR$rsync_options --delete /dev $pivot
    chk=$?
    if test "$chk" != "0" ; then
      cat <<EOF | sed -e 's/\(..*\)/(WW) \1/' 
rsyncing /dev tree failed.
skipping to next partition.
EOF
    fi

# synchronize disks
      launch "syncing disks failed." synchronization
  done


# force exit code to 0
  chk=0 ; return $chk
}

postcopying() # post-copy tasks
{
# remove any mark present on source disk
  [ -f /BACKUP ] && rm -f /BACKUP

# mark source disk
  touch /ORIG

# remove any mark present on target disk
  [ -f $pivot/ORIG ] && rm -f $pivot/ORIG

# mark target disk
  touch $pivot/BACKUP

# exit if lilo.conf is not up to date
  grep "boot=$src" /etc/lilo.conf 2>&1 >/dev/null ; chk=$?
  if [ "$chk" != "0" ] ; then
    cat <<EOF | sed -e 's/\(..*\)/(EE) \1/' 
directive \`boot=$src' not found. please update /etc/lilo.conf.
done.
EOF
    exit 0
  fi

# update boot message
  
  echo "backup disk $dest" > $pivot/boot/message

# create temp lilo.conf : lilo is going to chroot to $pivot, where
# the root fs of $dest is mounted. lilo has to believe it is $src.

  tmplilo="tmp/$$"
  
  launch "creating lilo config file failed." touch $pivot/$tmplilo
 
  echo "
disk=$dest
bios=0x80
boot=$dest
" > $pivot/$tmplilo

  cat /etc/lilo.conf | grep -v "disk=\|bios=\|boot=" >> $pivot/$tmplilo

  if test "$debug" = "yes" ; then
    cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
debug : begin local /etc/lilo.conf :
EOF
    cat /etc/lilo.conf
    cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
debug : end   local /etc/lilo.conf .
debug : begin temporary lilo conf  :
EOF
    cat $pivot/$tmplilo
    cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
debug : end   temporary lilo conf  .
EOF
  fi
# write MBR

  launch "writing $dest MBR failed." lilo -r $pivot -C $tmplilo

# remove temp file
  [ "$pivot/$tmplilo" != "/" ] && rm $pivot/$tmplilo

# display compared sizes of source/target fs  
  cat <<EOF | sed -e 's/\(..*\)/(II) \1/' 
comparing sizes:
`df -P -k | grep -e "^/dev" | sort -k 5`
EOF

# umount target disk
  umount_target
  echo "(II) done."

  return 0 
}

# main

# default options
ask_user="yes"
verbose="no"

# parse command line
while getopts "DVfhv" opt1 ; do
    case "$opt1" in
      D)      debug="yes"
              verbose="yes"
              set -x        ;;
      V)      version       ;;
      f)      ask_user='no' ;;
      h)      usage         ;;
      v)     verbose='yes'  ;;
      *)      usage         ;;
    esac
done

while getopts "DVfhv" opt2 ; do
    case "$opt2" in
      D)      debug="yes"
              verbose="yes"
              set -x        ;;
      V)      version       ;;
      f)      ask_user='no' ;;
      h)      usage         ;;
      v)     verbose='yes'  ;;
      *)      usage         ;;
    esac
done
shift $((OPTIND-1))

#echo "debug : ask_user : $ask_user"
#echo "debug : verbose  : $verbose"
#echo "debug : args : $* "

# exit if not enough arguments are supplied
[ "$#" -eq "2" ] || usage

# first arg : name of the disk to be cloned (source disk)
src="$1"

# second arg : name of the disk where to put cloned data (target disk)
dest="$2"

if test "$verbose" = "yes" ; then
       echo "rfs version $VERSION"
fi

# exit if the program is not ran as root
launch "must be root to run rfs." checking_user

# check fstab for compliance
launch -v "incorrect /etc/fstab." checking_fstab 

# clone partition table if it does not already exist on target disk
checking_partitions || partitionning

# format partitions on target disk if they are not already formatted
formatting_partitions

# clone
launch -v "copy failed." copying

# do post-copy job
launch -v "post-copy actions failed." postcopying
# end
