Subversion Repositories zfs_utils

Rev

Rev 34 | Rev 51 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 34 Rev 48
Line 1... Line 1...
1
# ZFS_Utils Module Documentation
1
# ZFS_Utils Module Documentation
2
 
2
 
3
Perl module providing utilities for ZFS management, GELI encryption, and sneakernet-based replication on FreeBSD systems.
3
Perl module providing utilities for ZFS management, GELI encryption, and sneakernet-based replication on FreeBSD systems.
4
 
4
 
5
**Version:** 0.2  
5
**Version:** 1.0 
6
**Copyright:** 2024–2025 Daily Data Inc.  
6
**Copyright:** 2024–2025 Daily Data Inc.  
7
**License:** Simplified BSD License (FreeBSD License)
7
**License:** Simplified BSD License (FreeBSD License)
8
 
8
 
9
---
9
---
10
 
10
 
Line 24... Line 24...
24
 
24
 
25
**Returns:**
25
**Returns:**
26
 
26
 
27
- In scalar context: full command output as a string
27
- In scalar context: full command output as a string
28
- In list context: output split into lines  
28
- In list context: output split into lines  
29
- Empty string on failure
-
 
30
 
29
 
31
**Behavior:**
30
### Full API (functions & exported variables)
32
 
31
 
33
- Logs the command execution via `logMsg()`
-
 
34
- If `$merge_stderr` is true, stderr is merged with stdout
32
The table below lists all public functions defined in `ZFS_Utils.pm` and whether they are available for import via `@EXPORT_OK`. Private/internal helper functions are also listed for completeness.
35
- Returns exit status and logs on command failure
-
 
36
- Calls `die` on fatal signal
-
 
37
 
33
 
-
 
34
| Symbol | Type | Exported? | Description |
-
 
35
|---|---:|:---:|---|
-
 
36
| `loadConfig($filename, $default?)` | function | Yes | Load a YAML config file into a HASHREF. If file missing and `$default` is a HASHREF, writes the default to disk using `YAML::XS` or `YAML::Tiny` and returns the default. Returns hashref on success. |
-
 
37
| `runCmd(@cmd_parts)` | function | Yes | Run a shell command (joined with spaces). Captures exit status in `$lastRunError`. Returns scalar output or list of lines depending on context. Honors `$merge_stderr`. |
-
 
38
| `logMsg($message, $filename?, $time_fmt?)` | function | Yes | Timestamped logging helper. Writes to `$filename` (defaults to `$logFileName`) and optionally to console if `$displayLogsOnConsole` is true. |
-
 
39
| `shredFile($path)` | function | Yes | Calls `/usr/local/bin/gshred -u -f -s 32 $path` to attempt secure deletion. Note: ineffective on ZFS due to COW. |
-
 
40
| `mountDriveByLabel($driveInfo)` | function | Yes | Find a device by GPT/msdos label and mount it. `$driveInfo` is a HASHREF with keys `label`, `fstype` (ufs/msdos), `mountPath`, `timeout`, `check_interval`. Returns mount path or empty string. |
-
 
41
| `unmountDriveByLabel($driveInfo)` | function | Yes | Unmount a drive previously mounted by label and remove the mountpoint if empty. Returns mount path on success or empty string. |
-
 
42
| `mountGeli($geliConfig)` | function | Yes | High-level orchestrator: mounts key disk, creates combined GELI key (via `makeGeliKey`), then decrypts disks and imports/mounts the zpool via `decryptAndMountGeli`. Returns pool name on success. |
38
**Example:**
43
| `makeReplicateCommands($sourceSnapsRef,$targetSnapsRef,$dataset,$sourceParent,$targetParent,$newStatusRef)` | function | Yes | Build zfs send command strings based on source and target snapshot lists and prior status. Returns HASHREF of dataset -> send command. |
-
 
44
| `sendReport($reportConfig, $message, $logFile)` | function | Yes | High-level report sender. Can save report to a target drive (via `mountDriveByLabel`) and/or email it. Delegates to `copyReportToDrive` and `sendEmailReport`. |
-
 
45
| `fatalError($message,$config?,$cleanupRoutine?)` | function | Yes | Log a fatal error, optionally run a cleanup CODE ref, and die. |
-
 
46
| `getDirectoryList($dirname)` | function | Yes | Return ARRAYREF of regular files (non-recursive) under `$dirname`. Returns 0 on error. |
-
 
47
| `cleanDirectory($dirname)` | function | Yes | Remove all regular files from `$dirname` (non-recursive). Returns 1. |
-
 
48
| `$logFileName` | scalar | Yes | Path to default log file (default: `/tmp/zfs_utils.log`). Can be overridden by caller. |
-
 
49
| `$displayLogsOnConsole` | scalar | Yes | If true (non-zero), `logMsg` also prints to STDOUT. |
-
 
50
| `$lastRunError` | scalar | Yes | Contains the last command exit code set by `runCmd` (Perl `$?` value). |
39
 
51
 
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`)
52
### Internal / non-exported helpers (not in `@EXPORT_OK`)
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
 
53
 
230
---
-
 
231
 
-
 
232
### Exported Variables
54
| Symbol | Type | Exported? | Description |
-
 
55
|---|---:|:---:|---|
-
 
56
| `findGeliDisks()` | function | No | Discover disks that appear free for GELI/ZFS use (excludes disks with partitions and those referenced by zpools). Returns list of device names. |
-
 
57
| `decryptAndMountGeli($geliConfig)` | function | No | Attach (`geli attach`) encrypted devices using the combined key and import the specified zpool, then run `zfs mount -a`. Returns pool name or empty string. |
233
 
58
| `makeGeliKey($geliConfig)` | function | No | Create the combined 32-byte GELI key by XOR'ing the remote binary keyfile and the local 256-bit hex key. Writes the binary key to `$geliConfig->{target}` with mode 0600. Returns 1 on success; dies on unrecoverable errors. |
234
#### `$logFileName`
59
| `copyReportToDrive($logFile,$mountPoint)` | function | No | Helper used by `sendReport` to copy the log file to a mounted drive using `File::Copy`. |
-
 
60
| `sendEmailReport($to,$subject,$message,$logFile)` | function | No | Helper to send plain-text email via `/usr/sbin/sendmail -t` including the message and appended log contents. |
235
 
61
 
236
Path to the log file for `logMsg()` output.
62
## Notes about exporting
237
 
63
 
238
**Default:** `/tmp/zfs_utils.log`  
-
 
239
**Type:** Scalar string  
-
 
240
**Usage:** Can be overridden by caller before calling any function
64
- The module uses `our @EXPORT_OK = qw(...)` so callers must explicitly import symbols they need, e.g.:
241
 
-
 
242
**Example:**
-
 
243
 
65
 
244
```perl
66
```perl
245
use ZFS_Utils qw(logMsg $logFileName);
67
use ZFS_Utils qw(loadConfig runCmd logMsg);
246
$logFileName = '/var/log/myapp.log';
-
 
247
logMsg("This goes to /var/log/myapp.log");
-
 
248
```
68
```
249
 
69
 
250
---
-
 
251
 
-
 
252
#### `$displayLogsOnConsole`
-
 
-
 
70
- `$merge_stderr` is an internal variable controlling whether `runCmd` merges stderr into stdout; it is not exported by default. If you need to change it, modify it in the module or add an explicit export.
253
 
71
 
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:**
72
## Example usage
261
 
73
 
262
```perl
74
```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:**
75
use FindBin;
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()`
76
use lib "$FindBin::Bin/..";
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
77
use ZFS_Utils qw(loadConfig runCmd logMsg);
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
 
78
 
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
79
my $cfg = loadConfig('/usr/local/etc/sneakernet.yaml', { dryrun => 1 });
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'
80
logMsg("Loaded config");
360
);
-
 
361
my @old_status = ('tank/home@snap0');
81
my @out = runCmd('zpool', 'list');
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
```
82
```
370
 
83
 
371
---
-
 
372
 
-
 
373
## Module Variables (Internal)
84
## Dependencies (summary)
374
 
-
 
-
 
85
- Perl core modules: strict, warnings, Exporter, Data::Dumper, POSIX, File::Path
375
- `$VERSION` - Module version (0.2)
86
- Optional CPAN: YAML::XS (preferred) or YAML::Tiny (fallback) for `loadConfig`
376
- `$merge_stderr` - Global flag for `runCmd()` stderr handling (default: 0, set to 1 in runCmd)
87
- System utilities (FreeBSD): `geli`, `zfs`, `zpool`, `geom`, `gpart`, `mount`, `/usr/sbin/sendmail`
377
 
88
 
378
---
89
---
379
 
90
 
380
## Usage Examples
-
 
381
 
-
 
382
### Basic Logging
-
 
383
 
-
 
384
```perl
-
 
385
use ZFS_Utils qw(logMsg $logFileName $displayLogsOnConsole);
91
Update notes: This document describes the API as implemented in `ZFS_Utils.pm` (v1.0). If you add new exported symbols, please update `@EXPORT_OK` in the module and this documentation accordingly.
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
 
92
 
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
 
93
 
451
## Dependencies
94
## Dependencies
452
 
95
 
453
- **Core:** Perl 5.10+, strict, warnings, Exporter, Data::Dumper, POSIX, File::Path
96
- **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()`)
97
- **External (optional):** YAML::XS or YAML::Tiny (at least one required for `loadConfig()`)