182 lines
8.7 KiB
Markdown
182 lines
8.7 KiB
Markdown
# clonetool
|
|
|
|
Block-level sync for a large file or block device between two machines (or
|
|
two paths on the same machine), driven from a third, passive "manager"
|
|
machine. Single static Go binary, no runtime dependencies beyond the
|
|
system `ssh` client for remote endpoints.
|
|
|
|
## How it works
|
|
|
|
- You run `clonetool sync` on the **manager**. Source, destination, and
|
|
manager may be three entirely different machines — the manager never
|
|
reads or writes a single block itself. It only spawns and talks to two
|
|
**control agents** (`clonetool agent --role control`, one per side,
|
|
local subprocess or over `ssh`) that do the stat/prepare/transfer work.
|
|
- The two agents compare content by **SHA-256 hash per fixed-size block**.
|
|
A block is only re-read once: it's hashed from the buffer it was read
|
|
into, and that same buffer is what gets sent on if it differs — never
|
|
read twice.
|
|
- **Source and destination are scanned in parallel.** The destination side
|
|
streams the hash of each block *as it computes it*, in block order; the
|
|
source side consumes that stream and immediately reads, compares and (if
|
|
it differs) sends that block. So the destination scan, the source scan
|
|
and the transfer of changed blocks all overlap — there is no "scan the
|
|
whole destination, then start" phase. The status line shows the transfer
|
|
progress with a trailing `scan H/M` until the destination fingerprint is
|
|
complete.
|
|
- **No state is kept between runs.** Every sync re-reads and re-hashes the
|
|
destination's *current* content and compares the source against that, so
|
|
a re-run only moves the blocks that actually differ and nothing needs to
|
|
be trusted from a previous run. (The destination side always does the
|
|
destination hashing — the remote `sink` in push mode, the local
|
|
destination agent in pull mode.)
|
|
- Bulk data goes **directly between source and destination**, not through
|
|
the manager. Each run tries:
|
|
1. **push** — the source agent connects straight to the destination
|
|
host over SSH and streams changed blocks to it.
|
|
2. **pull** (fallback) — if push isn't possible (no route/keys in that
|
|
direction), the destination agent connects to the source host
|
|
instead and pulls.
|
|
3. If neither direction works, the job fails with both reasons named.
|
|
Run the manager on the source or destination host, or set up SSH
|
|
connectivity in at least one direction.
|
|
- When source and destination are **both local** to the manager, no SSH
|
|
is used at all — the source agent spawns the write-side helper as a
|
|
plain local subprocess.
|
|
- **Self-deploy:** if a remote endpoint has no runnable `clonetool` on
|
|
`PATH` (or wherever `--remote-bin` points), the manager streams *this*
|
|
binary to `~/.clonetool/bin/clonetool` on that host over the existing
|
|
SSH connection and uses it — no install, no root. Disable with
|
|
`--deploy=false`. If the copied binary won't execute there (wrong CPU
|
|
architecture) the error says so; build one for the remote's arch
|
|
(`CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build`) and put it on `PATH`
|
|
or pass `--remote-bin`.
|
|
- **sudo:** if reading or writing an endpoint that is a **block device**
|
|
fails with a permission error, `--sudo=auto` (the default) transparently
|
|
restarts that side's agent — and the helper it spawns on the peer —
|
|
under `sudo`. Locally that may prompt for a password on your terminal;
|
|
**on a remote host it uses `sudo -n`, so passwordless sudo (NOPASSWD)
|
|
must be configured there** (a password prompt can't work — the transfer
|
|
protocol owns the SSH stdout stream). `--sudo=always` elevates from the
|
|
start; `--sudo=never` never does.
|
|
- Sizing rules:
|
|
- Destination is a **block device**: it can't be resized, so if the
|
|
source is larger the job fails; otherwise exactly `min(source, dest)`
|
|
bytes are synced and the remainder of the device is left untouched.
|
|
- Destination is a **regular file**: it's truncated (created if
|
|
missing) to exactly the source's size, growing or shrinking it.
|
|
Shrinking an existing non-empty file prompts for confirmation unless
|
|
`--yes` is passed.
|
|
|
|
## Build
|
|
|
|
```
|
|
CGO_ENABLED=0 go build -o clonetool .
|
|
```
|
|
|
|
Cross-compile for another OS/arch by setting `GOOS`/`GOARCH` (no cgo, so
|
|
these all work from any host):
|
|
|
|
```
|
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o clonetool .
|
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o clonetool.exe .
|
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o clonetool-darwin .
|
|
```
|
|
|
|
`./build.sh` writes all three of the above (plus linux/amd64) into `dist/`.
|
|
|
|
Copy the resulting binary to the manager host. Source and destination
|
|
hosts get it automatically (see self-deploy above), or place it yourself
|
|
and point `--remote-bin` at it. The binary is architecture-specific —
|
|
cross-compile if your hosts differ. Self-deploy streams a POSIX shell
|
|
script over SSH, so a **Windows** host can be the manager or a local
|
|
endpoint but cannot be an automatic deploy target — put `clonetool.exe`
|
|
on it yourself and point `--remote-bin` at it.
|
|
|
|
## Windows
|
|
|
|
clonetool runs on Windows and can clone **physical drives and volumes**,
|
|
not just files:
|
|
|
|
```
|
|
# Whole disk to an image file
|
|
clonetool sync --source \\.\PhysicalDrive2 --dest D:\backup\disk2.img
|
|
|
|
# Image file back onto a disk (must not be larger than the disk)
|
|
clonetool sync --source D:\backup\disk2.img --dest \\.\PhysicalDrive2
|
|
|
|
# A single volume
|
|
clonetool sync --source \\.\E: --dest \\.\F:
|
|
```
|
|
|
|
- Raw-disk paths are `\\.\PhysicalDrive<n>` (whole disk) or `\\.\<X>:` (a
|
|
volume). `\\?\` also works. Forward slashes are accepted.
|
|
- **Run from an elevated (Administrator) console** to open a raw disk.
|
|
There is no `sudo` fallback on Windows; `--sudo` is ignored. A
|
|
permission error tells you to elevate.
|
|
- A raw-disk **destination** should have no mounted filesystem in use
|
|
(take the disk offline in Disk Management, or target a volume that
|
|
nothing else has open) — Windows blocks writes to a disk region owned by
|
|
a mounted volume. Reading a live disk as the **source** is fine.
|
|
- Raw-disk I/O must be sector-aligned. `--block-size` must be a multiple
|
|
of the drive's sector size (512 or 4096); the default 4M is. clonetool
|
|
handles the final partial block itself.
|
|
- Device size is read with `IOCTL_DISK_GET_LENGTH_INFO`.
|
|
|
|
## Usage
|
|
|
|
```
|
|
clonetool sync --source LOC --dest LOC [options]
|
|
```
|
|
|
|
`LOC` is either a local path (`/dev/sdb`, `./image.bin`) or
|
|
`[user@]host:path` for a path reached over SSH.
|
|
|
|
```
|
|
# Same machine
|
|
clonetool sync --source /dev/sda --dest /dev/sdb
|
|
|
|
# Two remote machines, orchestrated from a third
|
|
clonetool sync --source db1:/dev/vdb --dest backup-host:/srv/db1.img
|
|
|
|
# Re-run any time; only changed blocks move
|
|
clonetool sync --source db1:/dev/vdb --dest backup-host:/srv/db1.img
|
|
```
|
|
|
|
Options:
|
|
|
|
| Flag | Default | Meaning |
|
|
|---|---|---|
|
|
| `--block-size` | `4M` | Block size (accepts `K`/`M`/`G` suffixes). |
|
|
| `--job` | — | Optional label shown in progress/log output. |
|
|
| `--yes` | off | Don't prompt before shrinking an existing destination file. |
|
|
| `--sudo` | `auto` | Block-device privilege escalation: `auto` (on a permission error), `always`, or `never`. Remote elevation needs passwordless sudo. |
|
|
| `--deploy` | `true` | Copy this binary to remote hosts that lack a runnable `clonetool`. `--deploy=false` to disable. |
|
|
| `--connect-timeout` | `8` | SSH connect timeout (seconds) used for the push/pull direction probe. |
|
|
| `--ssh` | `ssh` | ssh binary to use. |
|
|
| `--ssh-opt` | — | Extra `-o OPT` passed to ssh (repeatable). |
|
|
| `--remote-bin` | `clonetool` | Path to clonetool on remote hosts. |
|
|
| `--manager-host` | local hostname | Address a peer should use to reach this machine, needed only when source or dest is local to the manager *and* the other side is remote and ends up needing to dial back in (pull fallback). |
|
|
|
|
`clonetool version` prints the binary's `GOOS/GOARCH` (used internally for
|
|
the self-deploy check).
|
|
|
|
## Caveats
|
|
|
|
- Block-device size detection is implemented on Linux (`BLKGETSIZE64`) and
|
|
Windows (`IOCTL_DISK_GET_LENGTH_INFO`). On other systems only regular
|
|
files can be synced.
|
|
- If a destination path doesn't exist yet, it's created as a regular
|
|
file — clonetool won't create device nodes, so double-check device
|
|
paths for typos before running.
|
|
- SSH host keys are accepted on first connect (`StrictHostKeyChecking=accept-new`)
|
|
and rejected if they later change, same as normal SSH behavior.
|
|
- Because every run re-hashes both sides in full, a re-sync costs a full
|
|
read of source and destination even when little changed — the win is in
|
|
the bytes transferred, not the bytes read. The two reads run in
|
|
parallel; the status line shows a trailing `scan H/M` until the
|
|
destination fingerprint catches up.
|
|
- `agent` is an internal subcommand spawned automatically by `sync`; it's
|
|
not meant to be run by hand, though it will work standalone for
|
|
debugging.
|