first commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
# ZFS Move Over SSH
|
||||||
|
|
||||||
|
`zfs-move-over-ssh.sh` snapshots every ZFS filesystem at and beneath a source
|
||||||
|
dataset, then sends each snapshot to a remote ZFS server over SSH.
|
||||||
|
|
||||||
|
For a source tree such as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tank/data
|
||||||
|
tank/data/documents
|
||||||
|
tank/data/media
|
||||||
|
```
|
||||||
|
|
||||||
|
and destination root `backup/data`, the script sends the filesystems to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backup/data
|
||||||
|
backup/data/documents
|
||||||
|
backup/data/media
|
||||||
|
```
|
||||||
|
|
||||||
|
Each local snapshot is named `move-YYYYMMDD-HHMMSS`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Run the script as an account that can use `zfs` on the source server.
|
||||||
|
- The source host needs Bash, ZFS utilities, and SSH.
|
||||||
|
- The destination host needs ZFS utilities and the destination pool must exist.
|
||||||
|
- The SSH account on the destination must be able to execute `sudo -n zfs`
|
||||||
|
without a password prompt.
|
||||||
|
- SSH connectivity and host-key verification must already work.
|
||||||
|
|
||||||
|
The script is intended for one-shot full snapshot transfers. It does not use
|
||||||
|
incremental sends or resume tokens.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash zfs-move-over-ssh.sh [options] SOURCE_ROOT REMOTE_HOST DESTINATION_ROOT
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash zfs-move-over-ssh.sh tank/data root@new-server backup/data
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a non-standard SSH key or port when needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash zfs-move-over-ssh.sh \
|
||||||
|
--identity-file ~/.ssh/zfs-migration \
|
||||||
|
--ssh-port 2222 \
|
||||||
|
tank/data admin@new-server backup/data
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `-n`, `--dry-run` | Print planned snapshot, send, and receive commands without running them. |
|
||||||
|
| `-i`, `--identity-file FILE` | Use the specified SSH private key. |
|
||||||
|
| `-p`, `--ssh-port PORT` | Connect to the specified SSH port. |
|
||||||
|
| `--destroy-source-snapshots` | Remove each local migration snapshot only after its receive succeeds. |
|
||||||
|
| `-h`, `--help` | Show command help. |
|
||||||
|
|
||||||
|
## Recommended Procedure
|
||||||
|
|
||||||
|
1. Confirm the source datasets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zfs list -r tank/data
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Confirm remote privileged ZFS access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@new-server 'sudo -n zfs list'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Preview the operation. Review every destination name in the output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash zfs-move-over-ssh.sh --dry-run tank/data admin@new-server backup/data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash zfs-move-over-ssh.sh tank/data admin@new-server backup/data
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the received datasets and data before retiring the source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@new-server 'sudo zfs list -r backup/data'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety and Recovery
|
||||||
|
|
||||||
|
- The script does **not** destroy source filesystems or source data.
|
||||||
|
- By default, the generated local snapshots remain on the source. This allows a
|
||||||
|
failed run to be inspected or sent again. Remove them manually only after
|
||||||
|
verification, or use `--destroy-source-snapshots` once the workflow is trusted.
|
||||||
|
- The remote command is `zfs receive -uF`. `-u` leaves received filesystems
|
||||||
|
unmounted. `-F` can roll back the destination filesystem and destroy newer
|
||||||
|
snapshots or changes that conflict with the incoming snapshot. Do not point
|
||||||
|
the script at a destination containing data you need unless you have reviewed
|
||||||
|
the dry-run output and have a current backup.
|
||||||
|
- The script processes one filesystem at a time. If it stops partway through,
|
||||||
|
completed destination filesystems remain received. Re-running with the same
|
||||||
|
source can create another timestamped snapshot; inspect both source and
|
||||||
|
destination state before retrying.
|
||||||
|
- A successful transfer is not a destructive move. Verify applications and
|
||||||
|
data on the destination, then perform any source cleanup separately.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
The destination root itself does not have to exist before the transfer, but
|
||||||
|
its parent pool or filesystem must exist and the remote account must have ZFS
|
||||||
|
permission to create the target datasets. Dataset properties are included in
|
||||||
|
the stream through `zfs send -p`.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#!/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_ROOT REMOTE_HOST DESTINATION_ROOT
|
||||||
|
|
||||||
|
Create one snapshot for every filesystem under SOURCE_ROOT and send each one
|
||||||
|
over SSH. Filesystems retain their relative paths below DESTINATION_ROOT.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
SOURCE_ROOT Source ZFS filesystem, for example: tank/data
|
||||||
|
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.
|
||||||
|
|
||||||
|
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=()
|
||||||
|
|
||||||
|
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_ROOT="$1"
|
||||||
|
REMOTE_HOST="$2"
|
||||||
|
DESTINATION_ROOT="${3%/}"
|
||||||
|
SNAPSHOT_NAME="move-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
command -v zfs >/dev/null || die "zfs command was not found"
|
||||||
|
command -v ssh >/dev/null || die "ssh command was not found"
|
||||||
|
zfs list -H -o name -t filesystem "$SOURCE_ROOT" >/dev/null || die "source filesystem does not exist: $SOURCE_ROOT"
|
||||||
|
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
ssh "${SSH_OPTIONS[@]}" "$REMOTE_HOST" 'sudo -n zfs list -H -o name -t filesystem >/dev/null' \
|
||||||
|
|| die "cannot run 'sudo -n zfs' on $REMOTE_HOST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
map_destination() {
|
||||||
|
local filesystem="$1"
|
||||||
|
local relative_path="${filesystem#"$SOURCE_ROOT"}"
|
||||||
|
printf '%s%s\n' "$DESTINATION_ROOT" "$relative_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if $DRY_RUN; then
|
||||||
|
printf '+' >&2
|
||||||
|
printf ' %q' "$@" >&2
|
||||||
|
printf '\n' >&2
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mapfile -t FILESYSTEMS < <(zfs list -H -o name -t filesystem -r "$SOURCE_ROOT")
|
||||||
|
((${#FILESYSTEMS[@]} > 0)) || die "no filesystems found below $SOURCE_ROOT"
|
||||||
|
|
||||||
|
printf 'Migration snapshot: %s\n' "$SNAPSHOT_NAME"
|
||||||
|
for filesystem in "${FILESYSTEMS[@]}"; do
|
||||||
|
destination="$(map_destination "$filesystem")"
|
||||||
|
snapshot="${filesystem}@${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
|
||||||
|
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'
|
||||||
Reference in New Issue
Block a user