| 34 |
rodolico |
1 |
# ZFS_Utils Module Documentation
|
|
|
2 |
|
|
|
3 |
Perl module providing utilities for ZFS management, GELI encryption, and sneakernet-based replication on FreeBSD systems.
|
|
|
4 |
|
|
|
5 |
**Version:** 0.2
|
|
|
6 |
**Copyright:** 2024–2025 Daily Data Inc.
|
|
|
7 |
**License:** Simplified BSD License (FreeBSD License)
|
|
|
8 |
|
|
|
9 |
---
|
|
|
10 |
|
|
|
11 |
## Exported Functions & Variables
|
|
|
12 |
|
|
|
13 |
Functions and variables listed in `@EXPORT_OK` and available via `use ZFS_Utils qw(...)`
|
|
|
14 |
|
|
|
15 |
### Functions
|
|
|
16 |
|
|
|
17 |
#### `runCmd(@args)`
|
|
|
18 |
|
|
|
19 |
Execute a shell command and return output.
|
|
|
20 |
|
|
|
21 |
**Parameters:**
|
|
|
22 |
|
|
|
23 |
- `@args` - Command and arguments (joined with spaces)
|
|
|
24 |
|
|
|
25 |
**Returns:**
|
|
|
26 |
|
|
|
27 |
- In scalar context: full command output as a string
|
|
|
28 |
- In list context: output split into lines
|
|
|
29 |
- Empty string on failure
|
|
|
30 |
|
|
|
31 |
**Behavior:**
|
|
|
32 |
|
|
|
33 |
- Logs the command execution via `logMsg()`
|
|
|
34 |
- If `$merge_stderr` is true, stderr is merged with stdout
|
|
|
35 |
- Returns exit status and logs on command failure
|
|
|
36 |
- Calls `die` on fatal signal
|
|
|
37 |
|
|
|
38 |
**Example:**
|
|
|
39 |
|
|
|
40 |
```perl
|
|
|
41 |
my @files = runCmd('ls', '/tmp');
|
|
|
42 |
my $output = runCmd('grep pattern /var/log/file');
|
|
|
43 |
```
|
|
|
44 |
|
|
|
45 |
---
|
|
|
46 |
|
|
|
47 |
#### `shredFile($filename)`
|
|
|
48 |
|
|
|
49 |
Securely overwrite and delete a file using `gshred`.
|
|
|
50 |
|
|
|
51 |
**Parameters:**
|
|
|
52 |
|
|
|
53 |
- `$filename` - Path to file to securely delete
|
|
|
54 |
|
|
|
55 |
**Returns:** nothing
|
|
|
56 |
|
|
|
57 |
**Notes:**
|
|
|
58 |
|
|
|
59 |
- Uses `/usr/local/bin/gshred -u -f -s 32` (3-pass overwrite)
|
|
|
60 |
- Silently does nothing if file does not exist
|
|
|
61 |
- **Ineffective on COW filesystems** (e.g., ZFS)
|
|
|
62 |
- Best used with UFS or ramdisk storage
|
|
|
63 |
|
|
|
64 |
---
|
|
|
65 |
|
|
|
66 |
#### `logMsg($message, $filename?, $timeStampFormat?)`
|
|
|
67 |
|
|
|
68 |
Log a message to file and/or console with timestamp.
|
|
|
69 |
|
|
|
70 |
**Parameters:**
|
|
|
71 |
|
|
|
72 |
- `$message` - Message to log (required)
|
|
|
73 |
- `$filename` - Path to log file (optional; defaults to `$logFileName`)
|
|
|
74 |
- `$timeStampFormat` - strftime format string (optional; defaults to `'%Y-%m-%d %H:%M:%S'`)
|
|
|
75 |
|
|
|
76 |
**Returns:** nothing
|
|
|
77 |
|
|
|
78 |
**Behavior:**
|
|
|
79 |
|
|
|
80 |
- Appends timestamped message to log file if `$filename` is set and non-empty
|
|
|
81 |
- Prints to console if `$displayLogsOnConsole` is true
|
|
|
82 |
- Format: `TIMESTAMP\tMESSAGE`
|
|
|
83 |
|
|
|
84 |
**Example:**
|
|
|
85 |
|
|
|
86 |
```perl
|
|
|
87 |
use ZFS_Utils qw(logMsg);
|
|
|
88 |
logMsg("Operation started");
|
|
|
89 |
logMsg("Debug info", '/var/log/custom.log', '%H:%M:%S');
|
|
|
90 |
```
|
|
|
91 |
|
|
|
92 |
---
|
|
|
93 |
|
|
|
94 |
#### `mountDriveByLabel($label, $mountPath?, $timeout?, $checkEvery?, $filesystem?, $devPath?)`
|
|
|
95 |
|
|
|
96 |
Wait for a labeled device to appear and mount it. Works with gpt and msdos labels
|
|
|
97 |
|
|
|
98 |
**Parameters:**
|
|
|
99 |
|
|
|
100 |
- `$label` - GPT label name (required; alphanumeric, hyphen, underscore only)
|
|
|
101 |
- `$mountPath` - Mount destination (optional; defaults to `/mnt/$label`)
|
|
|
102 |
- `$timeout` - Seconds to wait (optional; defaults to 600)
|
|
|
103 |
- `$checkEvery` - Polling interval in seconds (optional; defaults to 15)
|
|
|
104 |
- `$filesystem` - Filesystem type for mount (optional; defaults to `'ufs'`)
|
|
|
105 |
- `$devPath` - Path to GPT devices (optional; defaults to `/dev/gpt/`)
|
|
|
106 |
|
|
|
107 |
**Returns:**
|
|
|
108 |
|
|
|
109 |
- Mount path on success
|
|
|
110 |
- Empty string on timeout or error
|
|
|
111 |
|
|
|
112 |
**Behavior:**
|
|
|
113 |
|
|
|
114 |
- Validates label format (alphanumeric, hyphen, underscore)
|
|
|
115 |
- Checks if already mounted; returns path immediately if so
|
|
|
116 |
- Polls for device appearance with `$checkEvery` second intervals
|
|
|
117 |
- Creates mount point if needed (via `make_path`)
|
|
|
118 |
- Logs all operations and errors
|
|
|
119 |
- Prints "Waiting for drive labeled..." during polling
|
|
|
120 |
|
|
|
121 |
**Example:**
|
|
|
122 |
|
|
|
123 |
```perl
|
|
|
124 |
use ZFS_Utils qw(mountDriveByLabel);
|
|
|
125 |
my $mp = mountDriveByLabel('replica', '/mnt/backup', 300, 10);
|
|
|
126 |
die "Failed to mount" unless $mp;
|
|
|
127 |
```
|
|
|
128 |
|
|
|
129 |
---
|
|
|
130 |
|
|
|
131 |
#### `loadConfig($filename, $default?)`
|
|
|
132 |
|
|
|
133 |
Load a YAML configuration file into a hashref.
|
|
|
134 |
|
|
|
135 |
**Parameters:**
|
|
|
136 |
|
|
|
137 |
- `$filename` - Path to YAML config file (required)
|
|
|
138 |
- `$default` - Default hashref to use if file missing (optional)
|
|
|
139 |
|
|
|
140 |
**Returns:** hashref loaded from YAML file, or `$default` if provided and file is missing
|
|
|
141 |
|
|
|
142 |
**Behavior:**
|
|
|
143 |
|
|
|
144 |
- Dies if no filename provided
|
|
|
145 |
- If file missing and `$default` provided, writes `$default` to file in YAML format
|
|
|
146 |
- Tries `YAML::XS` first, falls back to `YAML::Tiny`
|
|
|
147 |
- Logs which YAML module was used
|
|
|
148 |
- Dies if file exists but does not contain a hashref
|
|
|
149 |
- Returns empty hashref if both file is missing and no default is provided
|
|
|
150 |
|
|
|
151 |
**Dependencies:** YAML::XS or YAML::Tiny (at least one required)
|
|
|
152 |
|
|
|
153 |
**Example:**
|
|
|
154 |
|
|
|
155 |
```perl
|
|
|
156 |
use ZFS_Utils qw(loadConfig);
|
|
|
157 |
my $cfg = loadConfig(
|
|
|
158 |
'/etc/app.yaml',
|
|
|
159 |
{ debug => 0, port => 8080 }
|
|
|
160 |
);
|
|
|
161 |
```
|
|
|
162 |
|
|
|
163 |
---
|
|
|
164 |
|
|
|
165 |
#### `mountGeli($geliConfig)`
|
|
|
166 |
|
|
|
167 |
Prepare and decrypt GELI-encrypted disks, then mount the ZFS pool.
|
|
|
168 |
|
|
|
169 |
**Parameters:**
|
|
|
170 |
|
|
|
171 |
- `$geliConfig` - hashref with GELI configuration
|
|
|
172 |
|
|
|
173 |
**Expected keys in `$geliConfig`:**
|
|
|
174 |
|
|
|
175 |
- `localKey` - Path to local hex key (or hex string)
|
|
|
176 |
- `keydiskname` - GPT label of disk holding remote binary keyfile
|
|
|
177 |
- `keyfile` - Filename on key disk
|
|
|
178 |
- `target` - Path to write combined GELI key
|
|
|
179 |
- `diskList` - Arrayref of disk paths to decrypt (passed to `decryptAndMountGeli`)
|
|
|
180 |
- `poolname` - Name of ZFS pool to mount (passed to `decryptAndMountGeli`)
|
|
|
181 |
|
|
|
182 |
**Returns:**
|
|
|
183 |
|
|
|
184 |
- Pool name on success
|
|
|
185 |
- Empty string on error
|
|
|
186 |
|
|
|
187 |
**Workflow:**
|
|
|
188 |
|
|
|
189 |
1. Validates local key file exists
|
|
|
190 |
2. Mounts key disk via `mountDriveByLabel`
|
|
|
191 |
3. Creates combined GELI key via `makeGeliKey`
|
|
|
192 |
4. Decrypts disks and mounts pool via `decryptAndMountGeli`
|
|
|
193 |
|
|
|
194 |
---
|
|
|
195 |
|
|
|
196 |
#### `makeGeliKey($remote_keyfile, $localKeyHexOrPath, $target)`
|
|
|
197 |
|
|
|
198 |
Create a GELI key by XOR-ing a remote binary keyfile with a local hex key.
|
|
|
199 |
|
|
|
200 |
**Parameters:**
|
|
|
201 |
|
|
|
202 |
- `$remote_keyfile` - Path to binary keyfile (32 bytes; required)
|
|
|
203 |
- `$localKeyHexOrPath` - Hex string (64 hex chars) or path to file containing hex (required)
|
|
|
204 |
- `$target` - Path where to write resulting 32-byte binary key (required)
|
|
|
205 |
|
|
|
206 |
**Returns:** 1 on success; dies on error
|
|
|
207 |
|
|
|
208 |
**Behavior:**
|
|
|
209 |
|
|
|
210 |
- Reads exactly 32 bytes from `$remote_keyfile` in binary mode
|
|
|
211 |
- Accepts `$localKeyHexOrPath` as direct hex string or file path
|
|
|
212 |
- Cleans hex: removes `0x` prefix and whitespace
|
|
|
213 |
- Validates local key is exactly 64 hex characters (256-bit)
|
|
|
214 |
- XORs remote and local buffers byte-by-byte
|
|
|
215 |
- Creates target directory if needed
|
|
|
216 |
- Writes result with 0600 permissions
|
|
|
217 |
- Dies with descriptive error on any failure
|
|
|
218 |
|
|
|
219 |
**Example:**
|
|
|
220 |
|
|
|
221 |
```perl
|
|
|
222 |
use ZFS_Utils qw(makeGeliKey);
|
|
|
223 |
makeGeliKey(
|
|
|
224 |
'/mnt/key_disk/geli.key',
|
|
|
225 |
'a1b2c3d4...(64 hex chars)',
|
|
|
226 |
'/root/combined.key'
|
|
|
227 |
);
|
|
|
228 |
```
|
|
|
229 |
|
|
|
230 |
---
|
|
|
231 |
|
|
|
232 |
### Exported Variables
|
|
|
233 |
|
|
|
234 |
#### `$logFileName`
|
|
|
235 |
|
|
|
236 |
Path to the log file for `logMsg()` output.
|
|
|
237 |
|
|
|
238 |
**Default:** `/tmp/zfs_utils.log`
|
|
|
239 |
**Type:** Scalar string
|
|
|
240 |
**Usage:** Can be overridden by caller before calling any function
|
|
|
241 |
|
|
|
242 |
**Example:**
|
|
|
243 |
|
|
|
244 |
```perl
|
|
|
245 |
use ZFS_Utils qw(logMsg $logFileName);
|
|
|
246 |
$logFileName = '/var/log/myapp.log';
|
|
|
247 |
logMsg("This goes to /var/log/myapp.log");
|
|
|
248 |
```
|
|
|
249 |
|
|
|
250 |
---
|
|
|
251 |
|
|
|
252 |
#### `$displayLogsOnConsole`
|
|
|
253 |
|
|
|
254 |
Flag to enable/disable console output in `logMsg()`.
|
|
|
255 |
|
|
|
256 |
**Default:** 1 (enabled)
|
|
|
257 |
**Type:** Integer (0 = disabled, non-zero = enabled)
|
|
|
258 |
**Usage:** Can be toggled at runtime
|
|
|
259 |
|
|
|
260 |
**Example:**
|
|
|
261 |
|
|
|
262 |
```perl
|
|
|
263 |
use ZFS_Utils qw(logMsg $displayLogsOnConsole);
|
|
|
264 |
$displayLogsOnConsole = 0; # suppress console output
|
|
|
265 |
logMsg("Silent operation");
|
|
|
266 |
```
|
|
|
267 |
|
|
|
268 |
---
|
|
|
269 |
|
|
|
270 |
## Non-Exported Functions
|
|
|
271 |
|
|
|
272 |
Functions not in `@EXPORT_OK`; use full package path or import explicitly.
|
|
|
273 |
|
|
|
274 |
### `decryptAndMountGeli($geliConfig)`
|
|
|
275 |
|
|
|
276 |
Decrypt each GELI disk and import/mount the ZFS pool.
|
|
|
277 |
|
|
|
278 |
**Parameters:**
|
|
|
279 |
|
|
|
280 |
- `$geliConfig` - hashref with GELI configuration
|
|
|
281 |
|
|
|
282 |
**Expected keys:**
|
|
|
283 |
|
|
|
284 |
- `poolname` - ZFS pool name (required; dies if missing)
|
|
|
285 |
- `diskList` - Arrayref of disk paths; auto-discovered if missing
|
|
|
286 |
- `target` - Path to GELI keyfile
|
|
|
287 |
|
|
|
288 |
**Returns:**
|
|
|
289 |
|
|
|
290 |
- Pool name on success
|
|
|
291 |
- Empty string on error (logs instead of dying)
|
|
|
292 |
|
|
|
293 |
**Behavior:**
|
|
|
294 |
|
|
|
295 |
- Dies if no pool name specified
|
|
|
296 |
- Auto-discovers available disks via `findGeliDisks()` if no `diskList` provided
|
|
|
297 |
- Decrypts each disk via `geli attach -k <keyfile> <disk>`
|
|
|
298 |
- Logs failures but continues with remaining disks
|
|
|
299 |
- Imports pool via `zpool import`
|
|
|
300 |
- Mounts all filesystems via `zfs mount -a`
|
|
|
301 |
- Logs all operations and errors
|
|
|
302 |
|
|
|
303 |
**Internal Use:** Called by `mountGeli()`
|
|
|
304 |
|
|
|
305 |
---
|
|
|
306 |
|
|
|
307 |
### `findGeliDisks()`
|
|
|
308 |
|
|
|
309 |
Find all disks available for GELI/ZFS use.
|
|
|
310 |
|
|
|
311 |
**Parameters:** none
|
|
|
312 |
|
|
|
313 |
**Returns:** List of disk names (e.g., `('da0', 'da1')`)
|
|
|
314 |
|
|
|
315 |
**Behavior:**
|
|
|
316 |
|
|
|
317 |
- Lists all disks from `geom disk list`
|
|
|
318 |
- Filters out disks with existing partitions
|
|
|
319 |
- Filters out disks already in zpools
|
|
|
320 |
- Returns only unpartitioned, unused disks
|
|
|
321 |
- Logs the search operation
|
|
|
322 |
|
|
|
323 |
**Internal Use:** Called by `decryptAndMountGeli()` if no disk list provided
|
|
|
324 |
|
|
|
325 |
---
|
|
|
326 |
|
|
|
327 |
### `makeReplicateCommands($sourceSnapsRef, $statusRef?, $newStatusRef?)`
|
|
|
328 |
|
|
|
329 |
Generate ZFS send commands for replication based on snapshot lists.
|
|
|
330 |
|
|
|
331 |
**Parameters:**
|
|
|
332 |
|
|
|
333 |
- `$sourceSnapsRef` - Arrayref of snapshot lines (required)
|
|
|
334 |
- `$statusRef` - Arrayref of previously replicated snapshots (optional)
|
|
|
335 |
- `$newStatusRef` - Arrayref to populate with newly replicated snapshots (optional)
|
|
|
336 |
|
|
|
337 |
**Expected format of snapshot lines:**
|
|
|
338 |
|
|
|
339 |
- First token (space-separated) is the full snapshot name: `pool/filesystem@snapshot [extra tokens...]`
|
|
|
340 |
|
|
|
341 |
**Returns:** Hashref of `{ filesystem => 'zfs send command' }`
|
|
|
342 |
|
|
|
343 |
**Behavior:**
|
|
|
344 |
|
|
|
345 |
- Parses snapshot names from input lines
|
|
|
346 |
- Identifies root filesystem (first snapshot's fs)
|
|
|
347 |
- Looks up last replicated snapshot per filesystem from `$statusRef`
|
|
|
348 |
- Attempts recursive send if all child snapshots share same name
|
|
|
349 |
- Falls back to per-filesystem incremental or full sends
|
|
|
350 |
- Populates `$newStatusRef` with new snapshot names for status tracking
|
|
|
351 |
|
|
|
352 |
**Replaces missing `from` snapshots with full sends**
|
|
|
353 |
|
|
|
354 |
**Example:**
|
|
|
355 |
|
|
|
356 |
```perl
|
|
|
357 |
my @snaps = (
|
|
|
358 |
'tank/home@snap1 some extra info',
|
|
|
359 |
'tank/home/user@snap1 another field'
|
|
|
360 |
);
|
|
|
361 |
my @old_status = ('tank/home@snap0');
|
|
|
362 |
my @new_status = ();
|
|
|
363 |
|
|
|
364 |
my $cmds = makeReplicateCommands(\@snaps, \@old_status, \@new_status);
|
|
|
365 |
foreach my $fs (keys %$cmds) {
|
|
|
366 |
print "For $fs: " . $cmds->{$fs} . "\n";
|
|
|
367 |
}
|
|
|
368 |
# @new_status now contains the new snapshot names
|
|
|
369 |
```
|
|
|
370 |
|
|
|
371 |
---
|
|
|
372 |
|
|
|
373 |
## Module Variables (Internal)
|
|
|
374 |
|
|
|
375 |
- `$VERSION` - Module version (0.2)
|
|
|
376 |
- `$merge_stderr` - Global flag for `runCmd()` stderr handling (default: 0, set to 1 in runCmd)
|
|
|
377 |
|
|
|
378 |
---
|
|
|
379 |
|
|
|
380 |
## Usage Examples
|
|
|
381 |
|
|
|
382 |
### Basic Logging
|
|
|
383 |
|
|
|
384 |
```perl
|
|
|
385 |
use ZFS_Utils qw(logMsg $logFileName $displayLogsOnConsole);
|
|
|
386 |
|
|
|
387 |
$logFileName = '/var/log/backup.log';
|
|
|
388 |
$displayLogsOnConsole = 1;
|
|
|
389 |
|
|
|
390 |
logMsg("Backup started");
|
|
|
391 |
logMsg("Backup completed");
|
|
|
392 |
```
|
|
|
393 |
|
|
|
394 |
### Loading Configuration
|
|
|
395 |
|
|
|
396 |
```perl
|
|
|
397 |
use ZFS_Utils qw(loadConfig);
|
|
|
398 |
|
|
|
399 |
my $config = loadConfig('/etc/backup.yaml', {
|
|
|
400 |
pool => 'tank',
|
|
|
401 |
retention_days => 30,
|
|
|
402 |
});
|
|
|
403 |
```
|
|
|
404 |
|
|
|
405 |
### Mounting an Encrypted Pool
|
|
|
406 |
|
|
|
407 |
```perl
|
|
|
408 |
use ZFS_Utils qw(mountGeli);
|
|
|
409 |
|
|
|
410 |
my $result = mountGeli({
|
|
|
411 |
localKey => '/root/.key/local.hex',
|
|
|
412 |
keydiskname => 'key_disk',
|
|
|
413 |
keyfile => 'geli.key',
|
|
|
414 |
target => '/tmp/combined.key',
|
|
|
415 |
diskList => ['/dev/gpt/encrypted1', '/dev/gpt/encrypted2'],
|
|
|
416 |
poolname => 'backup',
|
|
|
417 |
});
|
|
|
418 |
|
|
|
419 |
die "Failed to mount" unless $result;
|
|
|
420 |
print "Pool $result mounted\n";
|
|
|
421 |
```
|
|
|
422 |
|
|
|
423 |
### Creating a GELI Key
|
|
|
424 |
|
|
|
425 |
```perl
|
|
|
426 |
use ZFS_Utils qw(makeGeliKey);
|
|
|
427 |
|
|
|
428 |
makeGeliKey(
|
|
|
429 |
'/mnt/usb/remote.bin',
|
|
|
430 |
'deadbeefcafebabe' . ('0' x 48), # 64 hex chars
|
|
|
431 |
'/root/.key/geli.key'
|
|
|
432 |
);
|
|
|
433 |
```
|
|
|
434 |
|
|
|
435 |
### Running Commands with Output
|
|
|
436 |
|
|
|
437 |
```perl
|
|
|
438 |
use ZFS_Utils qw(runCmd);
|
|
|
439 |
|
|
|
440 |
my @lines = runCmd('zfs', 'list', '-H');
|
|
|
441 |
foreach my $line (@lines) {
|
|
|
442 |
print "Pool: $line\n";
|
|
|
443 |
}
|
|
|
444 |
|
|
|
445 |
my $output = runCmd('df', '-h');
|
|
|
446 |
print $output;
|
|
|
447 |
```
|
|
|
448 |
|
|
|
449 |
---
|
|
|
450 |
|
|
|
451 |
## Dependencies
|
|
|
452 |
|
|
|
453 |
- **Core:** Perl 5.10+, strict, warnings, Exporter, Data::Dumper, POSIX, File::Path
|
|
|
454 |
- **External (optional):** YAML::XS or YAML::Tiny (at least one required for `loadConfig()`)
|
|
|
455 |
- **System (FreeBSD):** gshred, geli, zfs, zpool, geom, gpart, mount
|
|
|
456 |
|
|
|
457 |
---
|
|
|
458 |
|
|
|
459 |
## Notes
|
|
|
460 |
|
|
|
461 |
- All functions use `logMsg()` for diagnostics; configure logging before use
|
|
|
462 |
- Functions prefer returning empty strings or undef over dying (except `makeGeliKey`)
|
|
|
463 |
- `mountGeli()` and `decryptAndMountGeli()` require FreeBSD with GELI and ZFS
|
|
|
464 |
- Binary key operations use raw `:raw` mode for safe byte handling
|
|
|
465 |
- XOR operations assume 256-bit (32-byte) keys
|
|
|
466 |
|
|
|
467 |
---
|
|
|
468 |
|
|
|
469 |
## License
|
|
|
470 |
|
|
|
471 |
Simplified BSD License (FreeBSD License) – see module header for full text.
|