162 lines
4.9 KiB
Bash
162 lines
4.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Snapshot ZFS filesystems and stream each snapshot to a remote ZFS host.
|
|
|
|
set -Eeuo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: zfs-move-over-ssh.sh [options] SOURCE_POOL REMOTE_HOST DESTINATION_ROOT
|
|
|
|
Create one snapshot for every filesystem in SOURCE_POOL and send each one over
|
|
SSH. Filesystems retain their relative paths below DESTINATION_ROOT.
|
|
|
|
Arguments:
|
|
SOURCE_POOL Source ZFS pool, for example: tank
|
|
REMOTE_HOST SSH host, optionally user@host
|
|
DESTINATION_ROOT Existing or new destination ZFS filesystem, for example: backup/data
|
|
|
|
Options:
|
|
-n, --dry-run Print the ZFS and SSH commands without running them.
|
|
-i, --identity-file FILE SSH private key to use.
|
|
-p, --ssh-port PORT SSH port to use.
|
|
--destroy-source-snapshots Destroy each local migration snapshot after its successful receive.
|
|
-h, --help Show this help.
|
|
|
|
SSH must use key or agent authentication; password prompts are disabled. The
|
|
remote SSH account must be able to run `sudo -n zfs receive` without a password
|
|
prompt. This script does not destroy source filesystems or data.
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
printf 'Error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
DRY_RUN=false
|
|
DESTROY_SOURCE_SNAPSHOTS=false
|
|
SSH_OPTIONS=(-o BatchMode=yes)
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
-n|--dry-run)
|
|
DRY_RUN=true
|
|
;;
|
|
-i|--identity-file)
|
|
(($# >= 2)) || die "$1 requires a file path"
|
|
SSH_OPTIONS+=(-i "$2")
|
|
shift
|
|
;;
|
|
-p|--ssh-port)
|
|
(($# >= 2)) || die "$1 requires a port"
|
|
SSH_OPTIONS+=(-p "$2")
|
|
shift
|
|
;;
|
|
--destroy-source-snapshots)
|
|
DESTROY_SOURCE_SNAPSHOTS=true
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
die "unknown option: $1"
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
(($# == 3)) || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
|
|
SOURCE_POOL="$1"
|
|
REMOTE_HOST="$2"
|
|
DESTINATION_ROOT="${3%/}"
|
|
SNAPSHOT_NAME="move-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
[[ "$REMOTE_HOST" != *@ ]] || die "REMOTE_HOST is missing a hostname; use user@server-name or server-name"
|
|
|
|
command -v zfs >/dev/null || die "zfs command was not found"
|
|
command -v zpool >/dev/null || die "zpool command was not found"
|
|
command -v ssh >/dev/null || die "ssh command was not found"
|
|
zpool list -H -o name "$SOURCE_POOL" >/dev/null || die "source pool does not exist: $SOURCE_POOL"
|
|
|
|
if ! $DRY_RUN; then
|
|
ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" true \
|
|
|| die "cannot authenticate to $REMOTE_HOST with SSH keys or an SSH agent"
|
|
ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" 'sudo -n zfs list -H -o name -t filesystem >/dev/null' \
|
|
|| die "$REMOTE_HOST accepts SSH, but cannot run 'sudo -n zfs'; configure passwordless sudo for zfs"
|
|
fi
|
|
|
|
map_destination() {
|
|
local filesystem="$1"
|
|
local relative_path="${filesystem#"$SOURCE_POOL"}"
|
|
printf '%s%s\n' "$DESTINATION_ROOT" "$relative_path"
|
|
}
|
|
|
|
run() {
|
|
if $DRY_RUN; then
|
|
printf '+' >&2
|
|
printf ' %q' "$@" >&2
|
|
printf '\n' >&2
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
remote_snapshot_exists() {
|
|
local remote_snapshot="$1"
|
|
ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" \
|
|
"sudo -n zfs list -H -o name -t snapshot '$remote_snapshot' >/dev/null 2>&1"
|
|
}
|
|
|
|
remove_remote_snapshot() {
|
|
local remote_snapshot="$1"
|
|
local reply
|
|
|
|
printf 'Remote snapshot %s already exists. Delete it and retry? [y/N] ' "$remote_snapshot" >&2
|
|
read -r reply
|
|
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || die "remote snapshot was not deleted; stopping"
|
|
|
|
ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" "sudo -n zfs destroy '$remote_snapshot'" \
|
|
|| die "could not delete remote snapshot $remote_snapshot"
|
|
}
|
|
|
|
mapfile -t FILESYSTEMS < <(zfs list -H -o name -t filesystem -r "$SOURCE_POOL")
|
|
((${#FILESYSTEMS[@]} > 0)) || die "no filesystems found in $SOURCE_POOL"
|
|
|
|
printf 'Migration snapshot: %s\n' "$SNAPSHOT_NAME"
|
|
for filesystem in "${FILESYSTEMS[@]}"; do
|
|
destination="$(map_destination "$filesystem")"
|
|
snapshot="${filesystem}@${SNAPSHOT_NAME}"
|
|
remote_snapshot="${destination}@${SNAPSHOT_NAME}"
|
|
|
|
printf 'Transferring %s to %s:%s\n' "$filesystem" "$REMOTE_HOST" "$destination"
|
|
run zfs snapshot "$snapshot"
|
|
|
|
if $DRY_RUN; then
|
|
printf '+ zfs send -p %q | ssh' "$snapshot" >&2
|
|
printf ' %q' "${SSH_OPTIONS[@]}" "$REMOTE_HOST" "sudo -n zfs receive -uF '$destination'" >&2
|
|
printf '\n' >&2
|
|
else
|
|
if remote_snapshot_exists "$remote_snapshot"; then
|
|
remove_remote_snapshot "$remote_snapshot"
|
|
fi
|
|
zfs send -p "$snapshot" | ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" "sudo -n zfs receive -uF '$destination'"
|
|
fi
|
|
|
|
if $DESTROY_SOURCE_SNAPSHOTS; then
|
|
run zfs destroy "$snapshot"
|
|
fi
|
|
done
|
|
|
|
printf 'Completed. Local source filesystems were not destroyed.\n' |