first commit

This commit is contained in:
2026-09-01 22:28:25 +03:00
commit 2ce4afdfdd
2 changed files with 256 additions and 0 deletions
+122
View File
@@ -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`.
+134
View File
@@ -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'