This script is provided “as is”, without warranty of any kind.
By using this script, you agree that I am not liable for any data loss, system damage, service interruption, or other issues that may occur as a result of running it.
This script performs destructive operations, including but not limited to:
- Creating ZFS snapshots
- Destroying ZFS snapshots
- Deleting log files (
.log,.err) - Executing system-level commands (
zfs,mail)
This project is AI-assisted / vibe-coded software created as a hobby project. It has not been professionally audited and may contain bugs, unsafe behavior, data-loss issues, security problems, or incorrect assumptions.
You are responsible for reviewing the code, testing it in a safe environment, making backups, and understanding what it does before using it on real data. The author is not responsible for damage, data loss, broken systems, security issues, or other problems caused by using this software.
This application can perform destructive operations, including deleting ZFS snapshots, and backup data. Always test with dry-runs first, check the generated plans, and keep a separate working backup.
SnapBeforeWatchTower is a Python 3 utility for managing ZFS snapshots using a safe naming convention and retention policy.
It supports:
- Creating ZFS snapshots with a configurable
--name+--note - Pruning snapshots by age (
--older-than) and/or count (--retain-count) - Dry-run mode (show what would be deleted)
- Log retention cleanup using the same age/count rules
- Optional email notifications (errors only, or errors + success)
The script must be run as root for any ZFS actions.
❗ ZFS is required
❗ A dataset file is required (--datasets-file)
❗ A snapshot name token is required (--name)
❗ Run as root (sudo)
If any mandatory requirement is missing or misconfigured, the script fails safely.
- ✅ ZFS snapshot creation
- ✅ Safe snapshot deletion with strict name + timestamp matching
- ✅ Retention rules: keep newest N, optionally delete only older than cutoff
- ✅ Dataset-driven operation (one dataset per line)
- ✅ Dry-run mode (no destructive changes)
- ✅ Structured logging to
.log+.err - ✅ Automatic writable log folder selection (script
./logsor/tmp/...fallback) - ✅ Optional email notifications
- ✅ Cleans empty
.errfiles automatically
- Linux with ZFS available
- Python 3.9+ recommended (uses modern typing)
The following must be available in $PATH:
zfsmail(only required if email notifications are used)
The dataset file must exist and contain one ZFS dataset per line.
Example datasets.txt:
tank/data
tank/docker
tank/vms
- Blank lines are ignored
- If a dataset is invalid or ZFS fails, the run will fail (and can send mail if enabled)
Snapshots created by this script look like:
<dataset>@<Name>_<Note>_<YYYY-MM-DD_HH_MM_SS>
Example:
tank/data@SnapBeforeWatchTower_run_2026-01-13_20_15_01
When pruning, the script will only consider snapshots that:
- Are on the target dataset, and
- Start with
@<Name>_, and - End with a valid timestamp
_<YYYY-MM-DD_HH_MM_SS>
The <Note> token is ignored for pruning decisions (but still must exist in the snapshot name).
Anything not matching this exact pattern is never deleted.
Retention is applied per dataset:
- The script lists snapshots for the dataset and selects only those matching the pattern above
- Snapshots are sorted by timestamp (newest first)
- The newest
--retain-countsnapshots are always kept - Remaining snapshots are deleted depending on
--older-than:- If
--older-thanis set: delete only snapshots older than the cutoff - If
--older-thanis NOT set: delete everything not retained
- If
--older-than (e.g. 9999d).
Each run generates files with the same timestamp:
.log→ main log (INFO to console + DEBUG to file).err→ errors only (removed automatically if empty)
The script picks a writable log folder in this order:
<script_dir>/logs(preferred if writable/creatable)/tmp/<script_name>/(fallback)
Note: Even if you run without root, logs can still be written.
However, if you are not root the script will:
- Log the error
- Optionally send a failure email (if
--send-mailis set) - Exit without touching ZFS
Enable mail with:
-m / --send-mail you@example.com
If a run fails, it sends a mail with:
- Latest
.log - Latest
.erronly if non-empty
If you also pass:
--mail-on-success
…then it will send a mail on successful completion too (same attachment rules).
sudo ./SnapBeforeWatchTower.py \
--command create \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--note runsudo ./SnapBeforeWatchTower.py \
--command create+delete \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--note run \
--older-than 7d \
--retain-count 10sudo ./SnapBeforeWatchTower.py \
--command delete-only \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--older-than 7d \
--retain-count 10sudo ./SnapBeforeWatchTower.py \
--command dry-run \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--older-than 7d \
--retain-count 10sudo ./SnapBeforeWatchTower.py \
--command create+delete \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--older-than 7d \
--retain-count 10 \
--send-mail you@example.comsudo ./SnapBeforeWatchTower.py \
--command create+delete \
--datasets-file datasets.txt \
--name SnapBeforeWatchTower \
--older-than 7d \
--retain-count 10 \
--send-mail you@example.com \
--mail-on-success| Flag | Description |
|---|---|
-c, --command |
One of: create, create+delete, delete-only, dry-run |
-d, --datasets-file |
Path to dataset list file (required) |
-n, --name |
Snapshot Name token (required) |
--note |
Snapshot Note token (default: run) |
-o, --older-than |
Age cutoff for deletion: Nd, Nw, Nm (days/weeks/30-day months) |
-r, --retain-count |
Always keep this many newest snapshots (default: 0) |
-l, --log-prefix |
Prefix for log filenames (default: script filename) |
-m, --send-mail EMAIL |
Send email report (failure by default) |
-mos, --mail-on-success |
Also email on success (requires --send-mail) |
- ❗ This script destroys ZFS snapshots
- ❗ Must run as root for ZFS actions
- ❗ Use
--command dry-runto preview deletions - ❗ Prune matching is strict (name + timestamp required)
- ❗ Retention without
--older-thanwill delete all non-retained snapshots
No license is implied unless explicitly added.
Use, modify, and run this script entirely at your own risk.