#!/usr/local/bin/ksh -p
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END

#
# zfs-replicate
#
# Replicate a filesystem and its associated snapshots from one pool to another.
# Takes care of handling incremental snapshots, stopping affected services,
# unmounting the source, creating a final migration snapshot, migrating that
# one, re-mounting the new filesystem onto the old mountpoint, restarting the
# affected services, pattern matching to select snapshots, remote replication
# etc.
#

#
# Comments, suggestios, bugreports please to:
# Constantin Gonzalez <constantin dot gonzalez at sun dot com>
#
# Version: 0.7, 20080813, Brushed up code, comments here and there.
# Version: 0.6, 20080813, Applied Mike Hallock's remote patch.
# Version: 0.5, 20080813, Applied Mike Hallock's pattern patch.
# Version: 0.4, 20070816, More cleanups to make it blog-ready.
# Version: 0.3, 20070421, Code cleanups.
# Version: 0.2, 20070409, "Works on my system" certified.
# Version: 0.1, 20070327, Initial experimental hack.
#
# Acknowledgements:
# Thanks to Mike Hallock of uiuc.edu for the pattern and remote patches. 
# Thanks to Tim Foster for lots of good feedback. Visit blogs.sun.com/timf
# Thanks to Chris Gerhard for a similar script. Visit blogs.sun.com/chrisg
#


# Default values
option_n=0
option_F=""
option_s=0
option_m=0
option_v=0
option_p="."

services=""
torestart=""

source=""
sourcefs=""
sourcesnap=""
destination=""

LZFS="/sbin/zfs"
RZFS="/sbin/zfs"

#
# Print out usage information
#
usage() {
cat <<EOT
usage: zfs-replicate [-h] [-F ] [-n] [-s] [-v] [-p pattern]
         [-m [-c "FMRI|pattern[ FMRI|pattern]...]" ] [-r host]
         source dest

where source and dest is a ZFS filesystem, snapshot or volume.

Options:
-h: Print this help.
-F: Force a rollback of the destination filesystem to the most recent
    snapshot before replicating a snapshot. This is equivalent to the -F
    option of zfs receive.
-n: Don't actually replicate anything, just print what would be done.
    This will only print the next step but nothing dependent on that step
    since it won't actually be executed. For instance, -ns will print the
    snapshot command but not print the subsequent send/receive command as it
    depends on the snapshot actually being taken.
-s: After sending existing snapshots, make a final one and replicate it as well.
    This option requires that the source be a filesystem and not a snapshot.
-m: After sending all snapshots, migrate the source to the dest filesystem by
    unmounting the source filesystem and changing the new filesystem's
    mountpoint to that of the source one. This option includes -s.
-c: A space delimited list of SMF services in quotes to be temporarily disabled
    before unmounting the source, then re-enable after changing the mountpoint
    of the destination. Requires -m.
-v: Be verbose.
-p: Pattern to match snapshots against for replication selection.
-r: Remote host serving pool.
EOT
}

#
# Print out information if in verbose mode
#
echov() {
  if [ $option_v -eq 1 ]; then echo $*; fi
}

#
# Stop a list of SMF services. The services are read in from stdin.
#
stopsvcs() {
  typeset service

  while read service; do
    echov "Disabling service $service."
    svcadm disable -st $service || \
      { echo "Could not disable service $service."; relaunch; exit 1; }
    torestart=`echo $torestart $service`
  done
}

#
# Relaunch a list of stopped services
#
relaunch() {
  typeset i

  for i in $torestart; do
    echov "Restarting service $i"
    svcadm enable $i || { echo "Couldn't re-enable service $i."; exit; }
  done
}

#
# Copy a snapshot using zfs send/receive. If a third argument is used, then use
# send -i and the third argument is the base to create the increment from.
# Arguments should be compatible with zfs send and receive commands. Does
# nothing if the snapshot already exists.
#
copy_snap() {
  # Test if the snapshot exists already.
  typeset copysrc=$1
  typeset copydest=$2
  typeset copyprev=$3
  typeset copysrctail=`echo $copysrc | cut -d/ -f2-`

  if [ -z "`$RZFS list -Ho name -s creation | grep $dest/$copysrctail`" ]; then
    echov "Sending $copysrc to $copydest."
    if [ "$copyprev" = "" ]; then
      if [ $option_n -eq 0 ]; then
        $LZFS send $copysrc | $RZFS receive $option_F -d $copydest || \
          { echo "Error when zfs send/receiving."; exit 1; }
      else
        echo "$LZFS send $copysrc | $RZFS receive $option_F -d $copydest"
      fi
    else
      echov "  (incremental to $copyprev.)"
      if [ $option_n -eq 0 ]; then
        $LZFS send -i $copyprev $copysrc | $RZFS receive $option_F -d $copydest || \
          { echo "Error when zfs send/receiving."; exit 1; }
      else
        echo "$LZFS send -i $copyprev $copysrc | $RZFS receive $option_F -d $copydest"
      fi
    fi
  fi
}

#
# Copy the list of snapshots given in stdin to the destination in $1.
# Use incremental snapshots where possible. Assumes that the list of snapshots
# is given in creation order. copy_snap is responsible for skipping already
# existing snapshots on the destination side.
#
copy_snap_multiple() {
  typeset dest=$1
  typeset snapshot=""
  typeset destsnap=""
  typeset desttest=""
  typeset lastsnap=""

  while read snapshot; do
    copy_snap $snapshot $dest $lastsnap
    lastsnap=$snapshot
  done
}

#
# Copy all snapshots of a given filesystem. If the argument is a snapshot,
# copy all snapshots up to and including the given snapshot for the
# filesystem.
#
copy_fs() {
  typeset src=$1
  typeset dest=$2

  # Are we copying up to a specific snapshot?
  if [ -n "$sourcesnap" ]; then
    # Cut off all that comes after it
    $LZFS list -Hr -o name -s creation -t snapshot $src | grep $option_p | \
      sed -e "s%$sourcesnap .*\$%$sourcesnap%" | copy_snap_multiple $dest
  else
    $LZFS list -Hr -o name -s creation -t snapshot $src | grep $option_p | \
      copy_snap_multiple $dest
  fi
}

#
# Create a new recursive snapshot.
#
newsnap() {
  typeset snap=zfs-replicate_$$_`date +%Y%m%d%H%M%S`

  echov "Creating recursive snapshot $sourcefs@$snap."
  if [ $option_n -eq 0 ]; then
    $LZFS snapshot -r $sourcefs@$snap
  else
    echo "$LZFS snapshot -r $sourcefs@$snap"
  fi
}

#
# Check command line parameters.
#

# Read command line switches
while getopts cFhmnsvp:r:?: i
do
  case $i in
    c)
      services="$OPTARG"
      ;;
    F)
      option_F="-F"
      ;;
    h)
      usage
      exit 2
      ;;
    m)
      option_m=1
      option_s=1
      ;;
    n)
      option_n=1
      ;;
    s)
      option_s=1
      ;;
    v)
      option_v=1
      ;;
    p)
      option_p="$OPTARG"
      ;;
    r)
      RZFS="ssh $OPTARG /sbin/zfs"
      ;;
    \?)
      usage
      exit 2;;
  esac
done

# Read out source and dest values
shift `expr $OPTIND - 1`
source=$1
destination=$2

# Split up source into source fs, last component and optional source snapshot.
sourcefs=`echo $source | cut -d@ -f1`
sourcefslast=`echo $sourcefs | sed -e "s%.*/%%"`
sourcesnap=`echo $source | grep @ | cut -d@ -f2`

# Basic consistency checking
if [ $# -lt 2 ]; then
  usage
  exit 1
fi

# Enforce the when using -c you must use -m as well rule. This forces the user
# To think twice if they really mean to do the migration.
[ -n "$services" -a $option_m -eq 0 ] && \
  { echo "When using -c, -m needs to be specified as well."; exit 1; }

# When using -s or -m, we don't want the source to be a snapshot.
[ $option_s -ne 0 -a -n "$sourcesnap" ] && \
  { echo "Snapshots are not allowed as source when using -s or -m."; exit 1; }

# If using the -m feature, check if the source filesystem's mountpoint is a
# valid one, which means a regular, non-default (pool/filesystem) one.
# Also check if the source is mounted, otherwise there's no point in us doing
# the remounting.
if [ $option_m -eq 1 ]; then
  [ "`zfs get -Ho value mounted $source`" = "yes" ] || \
    { echo "The source filesystem is not mounted, why use -m?"; exit 1; }
  mountpoint=`zfs get -Ho value mountpoint $source`
  propsource=`zfs get -Ho source mountpoint $source`
  echov "Mountpoint is: $mountpoint. Source: $propsource."

  if [ "$propsource" = "default" ]; then
    echo "$source has a default mountpoint."
    echo "such mountpoints can't be migrated to the destination filesystem!"
    exit 1
  fi
fi

# Since we'll mostly wrap around zfs send/receive, we'll leave further
# error-checking to them.

#
# We now have a valid source filesystem, volume or snapshot to copy from and an
# assumed valid destination filesystem to copy to with a possible snapshot name
# to give to the destination snapshot.
#
copy_fs $source $destination

#
# Now we have replicated all existing snapshots.
#

#
# If using -s, do a new recursive snapshot, then copy all new snapshots too.
#
if [ $option_s -eq 1 ]; then
  # Create the new snapshot with a unique name.
  newsnap

  # Make sure all new snapshots are copied.
  copy_fs $sourcefs $destination
fi 

#
# If migrating, stop the affected services, unmount the source filesystem, do
# one last snapshot and replicate that, then give the destination file system
# the mount point of the source one and restart the services.
if [ $option_m -eq 1 ]; then
  # Check if any services need to be disabled before doing a migration.
  if [ -n "$services" ]; then
    echo $services | stopsvcs
  fi

  # unmount the source filesystem before doing the last snapshot.
  echov "Unmounting $source."
  zfs unmount $source || \
    { echo "Couldn't unmount source $source."; relaunch; exit 1; }

  # Create the last snapshot with a unique name.
  newsnap

  copy_fs $source $destination

  # Complete migration by switching mountpoints.
  echov "Setting mountpoint for $destination to $mountpoint."
  zfs set mountpoint=$mountpoint $destination

  # Re-launch any stopped services.
  relaunch
fi
  
exit 0
