Restoring without NBackup
NBackup’s artifacts are plain GNU-tar archives, compressed (and optionally
encrypted) with stock tools. Recovery never requires NBackup — this page is
the by-hand procedure for when the binary, config, or catalog are all gone. For
normal restores use nb recover; to rehearse this exact bare-tools path (and have
the commands printed for you), run nb drill --tier stock.
The on-disk layout is described in Concepts:
each archive is a clean NNNNNN-<dle>-L<n>.tar.<ext> payload with a .hdr
sidecar, plus a small commit footer and member index per archive.
A single full
The disk payload is a clean tar.<ext> — no header to skip:
zstd -dc 000000-app01-home-L0.tar.zst | tar -xf -
(gzip -dc for a .tar.gz; a none scheme is a plain .tar.)
A full + its incrementals
Replay one archive per level — the newest of each, from the last full forward
— in level order. Each level is cumulative since the one below, so only its newest
dump is needed. Replaying an older same-level rerun re-applies GNU tar’s
rename/delete directives and aborts (tar: Cannot rename …).
Order runs by date then same-day sequence. A plain glob mis-sorts a .2
rerun before its own date (since '.' < '/'), so normalize first:
dle=app01-home
runs=$(ls -d runs/run-* | sed -E 's#(/run-[0-9-]+)$#\1.1#' \
| sort -t. -k1,1 -k2,2n | sed -E 's#\.1$##')
# keep only the runs from this DLE's most recent full onward:
full=$(for d in $runs; do ls "$d"/0*-"$dle"-L0*.tar* 2>/dev/null; done | tail -1)
chain=$(printf '%s\n' "$runs" | sed -n "\#^$(dirname "$full")\$#,\$p")
for lvl in $(seq 0 9); do
a=$(for d in $chain; do ls "$d"/0*-"$dle"-L"$lvl"*.tar* 2>/dev/null; done | tail -1)
[ -n "$a" ] && zstd -dc "$a" | tar --extract --listed-incremental=/dev/null
done
(The L<n>*.tar* glob — a wildcard before .tar, not just after — matches both a
plain …-L0.tar.zst and an encrypted …-L0.p000.tar.zst.gpg. An encrypted level
still needs the atom loop below in place of the bare zstd -dc "$a" line.)
Encrypted archives
An encrypted archive is always stored as one or more atoms, even when it
is a single, unsplit archive: gpg refuses to decrypt concatenated messages, so no
encrypted payload can be the plain …-L0.tar.<ext> a full’s name otherwise is.
Every part — one in the common case — carries a .pNNN part-index suffix BEFORE
the extensions (…-L0.p000.tar.zst.gpg — a valid .gpg file, unlike a slice’s
….tar.zst.gpg.p000), signalling that it is ciphertext. The recipe is always the
same file loop — decrypt each atom, concatenate the plaintexts (whole compressed
frames: one valid stream), then decompress and untar once — whether there is one
atom or many:
# public-key (the private key is in the operator's keyring):
for p in 00*-app01-home-L0.p*.tar.zst.gpg; do gpg -d "$p"; done | zstd -dc | tar -xf -
# symmetric (passphrase_file) — supply the passphrase non-interactively, or a bare
# `gpg -d` blocks on a pinentry prompt:
for p in 00*-app01-home-L0.p*.tar.zst.gpg; do
gpg --batch --pinentry-mode loopback --passphrase-file /etc/nbackup/secret -d "$p"
done | zstd -dc | tar -xf -
A public-key dump restores on any host with the private key in its keyring; a
symmetric (passphrase_file) dump needs the same passphrase supplied to gpg.
The loop needs no index — the boundaries are the file boundaries, and ls | sort
already yields part order. Re-cutting an atom needs the key, so copies carry atoms
unchanged to every medium; an nb repack (decrypt → re-encrypt at a new atom size)
is deliberately not built — retention ages old atom sizes out on its own.
From tape
Tape frames each payload with a fixed 32 KB inline header — skip it first. On an
emulated library each volume is a slot-NN/ directory (or, with a bucket-URL
dir:, key prefix) of plain files, so a regular dd reads one — download the
object first if it lives in a bucket:
dd bs=32k skip=1 < file | zstd -dc | tar -xf -
On a real drive (/dev/nst0) the backend writes in variable-block mode, so position
to the file with mt and read with a block size at least the medium’s block_size
(the records are that big — bs=256k covers the 256k ceiling):
mt -f /dev/nst0 asf 1 # position to tape file 1 (file 0 is the label)
dd if=/dev/nst0 bs=256k skip=1 | zstd -dc | tar -xf - # skip the 32 KB header record
A spanned archive is split into parts written across several volumes. On a
dir-backed library each volume is a slot-NN/ directory whose file 000000 is the
volume’s identity label; the data files follow as 000001, 000002, …. `nb run