| 48 |
rodolico |
1 |
```markdown
|
|
|
2 |
# sneakernet — Sneakernet replication script
|
| 34 |
rodolico |
3 |
|
| 48 |
rodolico |
4 |
Perl script to perform sneakernet replication of ZFS datasets between two servers using
|
|
|
5 |
an external transport drive. The script is designed for FreeBSD systems and integrates
|
|
|
6 |
with `ZFS_Utils.pm` for shared helpers (mounting by GPT label, GELI handling, logging, etc.).
|
| 34 |
rodolico |
7 |
|
| 48 |
rodolico |
8 |
Version: 1.0
|
|
|
9 |
License: Simplified BSD (FreeBSD) — see header in `sneakernet` script for full terms.
|
| 34 |
rodolico |
10 |
|
|
|
11 |
---
|
|
|
12 |
|
| 48 |
rodolico |
13 |
## Summary / Purpose
|
| 34 |
rodolico |
14 |
|
| 48 |
rodolico |
15 |
`sneakernet` automates ZFS snapshot export/import using a removable transport disk. On the
|
|
|
16 |
source server it creates zfs send streams (optionally encrypted) and writes them to files on
|
|
|
17 |
the transport disk. On the target server it reads those files (optionally decrypts) and
|
|
|
18 |
pipes them into `zfs receive` to update the target datasets. The script also supports
|
|
|
19 |
using GELI to protect disks on the target and can build combined GELI keys from a
|
|
|
20 |
remote binary key and a local hex key (via helpers in `ZFS_Utils.pm`).
|
| 34 |
rodolico |
21 |
|
| 48 |
rodolico |
22 |
## Usage
|
| 34 |
rodolico |
23 |
|
| 48 |
rodolico |
24 |
Run from the command line. A YAML config file is expected next to the script named
|
|
|
25 |
`$scriptname.conf.yaml` (the script will create or update it if needed).
|
| 34 |
rodolico |
26 |
|
| 48 |
rodolico |
27 |
Basic options:
|
| 34 |
rodolico |
28 |
|
| 48 |
rodolico |
29 |
- `--dryrun`, `-n` : run without making destructive changes (no writes)
|
|
|
30 |
- `--verbose`, `-v` : increase logging verbosity (can be stacked)
|
|
|
31 |
- `--help`, `-h` : print help and exit
|
|
|
32 |
- `--version`, `-V` : print script version and exit
|
| 34 |
rodolico |
33 |
|
| 48 |
rodolico |
34 |
Example:
|
| 34 |
rodolico |
35 |
|
| 48 |
rodolico |
36 |
```bash
|
|
|
37 |
perl sneakernet --dryrun --verbose
|
|
|
38 |
```
|
| 34 |
rodolico |
39 |
|
| 48 |
rodolico |
40 |
## Dependencies
|
| 34 |
rodolico |
41 |
|
| 48 |
rodolico |
42 |
- FreeBSD system utilities: `geli`, `zfs`, `zpool`, `geom`, `gpart`, `mount`, `/usr/sbin/sendmail`
|
|
|
43 |
- Perl core modules: `strict`, `warnings`, `FindBin`, `Getopt::Long`, `File::Basename`, `Data::Dumper`
|
|
|
44 |
- Shared module in repository: `ZFS_Utils.pm` (provides `loadConfig`, `mountDriveByLabel`, `logMsg`, etc.)
|
|
|
45 |
- Optional CPAN modules: YAML::XS or YAML::Tiny (used by `ZFS_Utils::loadConfig`)
|
| 34 |
rodolico |
46 |
|
| 48 |
rodolico |
47 |
## Configuration (YAML)
|
| 34 |
rodolico |
48 |
|
| 48 |
rodolico |
49 |
The script ships with an in-code default `$config` hash that is used as the basis for the
|
|
|
50 |
YAML configuration. The following documents the important keys, types and defaults. When you
|
|
|
51 |
run `sneakernet` it attempts to `loadConfig($scriptFullPath.conf.yaml, $config)` and will
|
|
|
52 |
create the file from the defaults if it does not exist.
|
| 34 |
rodolico |
53 |
|
| 48 |
rodolico |
54 |
Top-level keys
|
| 34 |
rodolico |
55 |
|
| 48 |
rodolico |
56 |
- `dryrun` (bool) — default: `0` — if true, actions that change state are not executed.
|
|
|
57 |
- `verbosity` (int) — default: `1` — controls logging verbosity.
|
|
|
58 |
- `status_file` (string) — path to status file used to track last replicated snapshots.
|
|
|
59 |
- `log_file` (string) — path to runtime log file.
|
| 34 |
rodolico |
60 |
|
| 48 |
rodolico |
61 |
`source` (hash)
|
| 34 |
rodolico |
62 |
|
| 48 |
rodolico |
63 |
- `hostname` (string) — hostname of the source server (used to detect running role)
|
|
|
64 |
- `poolname` (string) — zpool name on the source (default: `pool`)
|
|
|
65 |
- `report` (hash)
|
|
|
66 |
- `email` (string) — email to send the report to
|
|
|
67 |
- `subject` (string) — optional subject
|
|
|
68 |
- `targetDrive` (hash)
|
|
|
69 |
- `fstype` (string) — filesystem type of report drive (ufs/msdos)
|
|
|
70 |
- `check_interval` (int) — polling interval (seconds)
|
|
|
71 |
- `label` (string) — GPT label of the report drive
|
|
|
72 |
- `mount_point` (string) — optional mount point override
|
| 34 |
rodolico |
73 |
|
| 48 |
rodolico |
74 |
`target` (hash)
|
| 34 |
rodolico |
75 |
|
| 48 |
rodolico |
76 |
- `hostname` (string) — hostname of the target server
|
|
|
77 |
- `poolname` (string) — zpool name on the target (default: `backup`)
|
|
|
78 |
- `shutdown_after_replication` (bool) — default: `0` — if true, attempt to shutdown after completion
|
|
|
79 |
- `geli` (hash) — when present, instructs the script to decrypt/mount GELI-protected pool(s):
|
|
|
80 |
- `secureKey` (hash)
|
|
|
81 |
- `label` (string) — GPT label of the key disk (default: `replica`)
|
|
|
82 |
- `fstype` (string) — filesystem of key disk (default: `ufs`)
|
|
|
83 |
- `check_interval` (int) — polling interval for key disk
|
|
|
84 |
- `wait_timeout` (int) — how long to wait for the key disk
|
|
|
85 |
- `keyfile` (string) — filename of the remote binary key on the key disk (default: `geli.key`)
|
|
|
86 |
- `localKey` (string) — 64-hex-character 256-bit key string or path to file containing hex
|
|
|
87 |
- `target` (string) — path where the combined keyfile should be written (e.g. `/media/geli.key`)
|
|
|
88 |
- `poolname` (string) — pool name to import on target
|
|
|
89 |
- `diskList` (array) — optional list of device names to try (e.g. `['da0','da1']`)
|
| 34 |
rodolico |
90 |
|
| 48 |
rodolico |
91 |
`transport` (hash)
|
| 34 |
rodolico |
92 |
|
| 48 |
rodolico |
93 |
- `label` (string) — GPT label of the transport drive (default: `sneakernet`)
|
|
|
94 |
- `fstype` (string) — filesystem type for mounting (default: `ufs`)
|
|
|
95 |
- `mount_point` (string) — target mount point (default in sample: `/mnt/sneakernet`)
|
|
|
96 |
- `timeout` (int) — how long to wait for the transport device to appear (seconds)
|
|
|
97 |
- `check_interval` (int) — polling interval when waiting for the device (seconds)
|
|
|
98 |
- `encryption` (hash)
|
|
|
99 |
- `key` (string) — hex key used by `openssl enc -aes-256-cbc` for transport encryption
|
|
|
100 |
- `IV` (string) — hex IV used by encryption (defaults to zeros in sample)
|
| 34 |
rodolico |
101 |
|
| 48 |
rodolico |
102 |
`datasets` (hash)
|
| 34 |
rodolico |
103 |
|
| 48 |
rodolico |
104 |
- Keys are logical dataset names (user-defined blocks). Each dataset object contains:
|
|
|
105 |
- `source` — parent or root dataset on the source (string)
|
|
|
106 |
- `target` — parent or root dataset on the target (string)
|
|
|
107 |
- `dataset` — dataset name
|
| 34 |
rodolico |
108 |
|
| 48 |
rodolico |
109 |
Example minimal YAML snippet (derived from the script defaults):
|
| 34 |
rodolico |
110 |
|
| 48 |
rodolico |
111 |
```yaml
|
|
|
112 |
dryrun: 0
|
|
|
113 |
log_file: /path/to/sneakernet.log
|
|
|
114 |
source:
|
|
|
115 |
hostname: source-host
|
|
|
116 |
poolname: pool
|
|
|
117 |
target:
|
|
|
118 |
hostname: target-host
|
|
|
119 |
poolname: backup
|
|
|
120 |
geli:
|
|
|
121 |
secureKey:
|
|
|
122 |
label: replica
|
|
|
123 |
keyfile: geli.key
|
|
|
124 |
localKey: e98c66...bc9c
|
|
|
125 |
target: /media/geli.key
|
|
|
126 |
transport:
|
|
|
127 |
label: sneakernet
|
|
|
128 |
mount_point: /mnt/sneakernet
|
|
|
129 |
datasets:
|
|
|
130 |
files_share:
|
|
|
131 |
source: pool
|
|
|
132 |
target: backup
|
|
|
133 |
dataset: files_share
|
| 34 |
rodolico |
134 |
```
|
|
|
135 |
|
| 48 |
rodolico |
136 |
## Functions (script-level / documented)
|
| 34 |
rodolico |
137 |
|
| 48 |
rodolico |
138 |
The following functions are defined inside `sneakernet` and are documented here. Many
|
|
|
139 |
helpers used by the script are provided by `ZFS_Utils.pm` (imported at the top of the script).
|
| 34 |
rodolico |
140 |
|
| 48 |
rodolico |
141 |
- `getStatusFile($filename)`
|
|
|
142 |
- Returns: ARRAYREF of status lines (snapshot names). Reads `$filename` if present; returns
|
|
|
143 |
empty arrayref and logs an informational message if file missing or unreadable.
|
| 34 |
rodolico |
144 |
|
| 48 |
rodolico |
145 |
- `writeStatusFile($filename, $statusList)`
|
|
|
146 |
- Writes the provided ARRAYREF of status lines to `$filename`.
|
|
|
147 |
- Behavior: backs up an existing file to `$filename.bak` before writing. Dies on failure.
|
| 34 |
rodolico |
148 |
|
| 48 |
rodolico |
149 |
- `dirnameToFileName($string, $delimiter='/', $substitution='.')`
|
|
|
150 |
- Utility to turn dataset-like strings into filename-safe strings. Example: `pool/fs/sub` -> `pool.fs.sub`.
|
| 34 |
rodolico |
151 |
|
| 48 |
rodolico |
152 |
- `doSourceReplication($config, $statusList)`
|
|
|
153 |
- Performs replication on the source server.
|
|
|
154 |
- Behavior: enumerates source snapshots, builds zfs send commands with `makeReplicateCommands` (from `ZFS_Utils`),
|
|
|
155 |
optionally pipes through `openssl enc` (if transport encryption key is set), and writes send streams to files on the
|
|
|
156 |
transport mount point. Honors `$config->{dryrun}`.
|
|
|
157 |
- Returns: `$newStatus` ARRAYREF of updated status lines.
|
| 34 |
rodolico |
158 |
|
| 48 |
rodolico |
159 |
- `cleanup($config, $message)`
|
|
|
160 |
- Performs final cleanup and reporting actions.
|
|
|
161 |
- Behavior: logs disk usage and zpool list, attempts to unmount the transport drive, sends report via `sendReport`, and
|
|
|
162 |
optionally shuts down the machine if configured. Honors `dryrun`.
|
| 34 |
rodolico |
163 |
|
| 48 |
rodolico |
164 |
- `updateTarget($config)`
|
|
|
165 |
- Reads files from the transport disk and feeds them into `zfs receive` to update target datasets.
|
|
|
166 |
- Behavior: detects file->dataset mapping via the filename (uses `dirnameToFileName` reversal), optionally decrypts
|
|
|
167 |
with `openssl enc -d` if encryption was used, and calls `zfs receive -F` for each file.
|
| 34 |
rodolico |
168 |
|
| 48 |
rodolico |
169 |
## Main flow summary
|
| 34 |
rodolico |
170 |
|
| 48 |
rodolico |
171 |
1. Load YAML config using `ZFS_Utils::loadConfig` with the default config provided in the script.
|
|
|
172 |
2. Parse CLI flags (dryrun, verbose, help, version). CLI flags override config when present.
|
|
|
173 |
3. Determine whether the running host matches `source.hostname` or `target.hostname` and set `runningAs` accordingly.
|
|
|
174 |
4. Mount the transport drive (fatal error if not found) using `ZFS_Utils::mountDriveByLabel`.
|
|
|
175 |
5. If running as source:
|
|
|
176 |
- Clean transport directory (non-recursive), produce send streams and write to files on transport drive.
|
|
|
177 |
- Update status file with newest snapshots.
|
|
|
178 |
6. If running as target:
|
|
|
179 |
- If `target.geli` present, attempt to decrypt/mount GELI disks (via `ZFS_Utils::mountGeli`).
|
|
|
180 |
- Update target datasets by reading files from transport and running `zfs receive`.
|
|
|
181 |
7. Run `cleanup()` to unmount, send reports, and optionally shutdown.
|
| 34 |
rodolico |
182 |
|
| 48 |
rodolico |
183 |
## Logging & Reports
|
| 34 |
rodolico |
184 |
|
| 48 |
rodolico |
185 |
- The script uses `ZFS_Utils::logMsg` throughout. Default log path is set from `$config->{log_file}` and
|
|
|
186 |
the module exposes `$logFileName` and `$displayLogsOnConsole` for customizing behavior at runtime.
|
|
|
187 |
- Reports can be saved to a drive (via `mountDriveByLabel`) and/or emailed via `/usr/sbin/sendmail` using
|
|
|
188 |
`ZFS_Utils::sendReport`.
|
| 34 |
rodolico |
189 |
|
| 48 |
rodolico |
190 |
## Security notes
|
| 34 |
rodolico |
191 |
|
| 48 |
rodolico |
192 |
- GELI combined keys are created by XOR'ing a remote binary key and a local 256-bit hex key — the resulting
|
|
|
193 |
key is written with mode `0600`. Keep these files and the key disk physically secure.
|
|
|
194 |
- Transport encryption uses `openssl enc -aes-256-cbc`; manage the encryption key material carefully.
|
| 34 |
rodolico |
195 |
|
| 48 |
rodolico |
196 |
## Example quick-run checklist
|
| 34 |
rodolico |
197 |
|
| 48 |
rodolico |
198 |
1. Edit `sneakernet.conf.yaml` (create from defaults if necessary) and confirm `transport.mount_point` and `label`.
|
|
|
199 |
2. On source: run `perl sneakernet --dryrun --verbose` to validate the planned commands.
|
|
|
200 |
3. On source: run without `--dryrun` to execute replication.
|
|
|
201 |
4. Physically move the drive to the target, insert it, and run the script on the target host.
|
| 34 |
rodolico |
202 |
|
| 48 |
rodolico |
203 |
## Troubleshooting
|
| 34 |
rodolico |
204 |
|
| 48 |
rodolico |
205 |
- If the transport drive does not mount, check that the GPT label matches `transport.label` and that the filesystem
|
|
|
206 |
type matches `transport.fstype`.
|
|
|
207 |
- If GELI attach fails, verify the keyfile exists on the secure key disk and that the local key hex string is correct
|
|
|
208 |
(exactly 64 hex characters) and that the combined keyfile is created at the configured `target` path.
|
|
|
209 |
- Use `--dryrun` and `--verbose` to inspect command strings before running them.
|
| 34 |
rodolico |
210 |
|
| 48 |
rodolico |
211 |
---
|
| 34 |
rodolico |
212 |
|
| 48 |
rodolico |
213 |
Document last updated: 2025-12-15
|
| 34 |
rodolico |
214 |
|