Restore without the recovery kit: the key from the location, with a passphrase
On a server without a backup key, backup:target:add finds the backups at the location and takes their key from its config with one of the old server's passphrases. Backups then wait until a new recovery kit is confirmed. The deletion delay and the repository id are now stored on the location, sealed with the key, so the delay survives a restore without a kit and --repair-config can write a lost config again from the kit. The key page warns against creating a new key when restoring a server. New test: tests/integration/recovery-scenarios.sh, and a no-kit mode in full-restore-roundtrip.sh.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\NgBackup\Tests\Unit\Repository;
|
||||
|
||||
use OCA\NgBackup\Backend\LocalBackend;
|
||||
use OCA\NgBackup\Crypto\CryptoException;
|
||||
use OCA\NgBackup\Crypto\KeyRing;
|
||||
use OCA\NgBackup\Repository\Repository;
|
||||
use OCA\NgBackup\Tests\Unit\TempDirTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** Disaster recovery without the recovery kit: the location gives its key with a passphrase. */
|
||||
class KeyFromLocationTest extends TestCase {
|
||||
use TempDirTrait;
|
||||
|
||||
private string $dir;
|
||||
private LocalBackend $backend;
|
||||
private KeyRing $keys;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->dir = $this->tempDir();
|
||||
$this->backend = new LocalBackend($this->dir);
|
||||
$this->keys = KeyRing::generate();
|
||||
$slots = ['slots' => [
|
||||
['slot' => 1, 'label' => 'A', 'created' => 0, 'key' => $this->keys->wrap('first passphrase')],
|
||||
['slot' => 2, 'label' => 'B', 'created' => 0, 'key' => $this->keys->wrap('second passphrase')],
|
||||
]];
|
||||
Repository::initWithKey($this->backend, $this->keys, $slots)->recordSettings(['delete_delay_days' => 30]);
|
||||
}
|
||||
|
||||
public function testEachPassphraseGivesTheKeyThatOpensTheBackups(): void {
|
||||
foreach (['first passphrase', 'second passphrase'] as $passphrase) {
|
||||
$keys = KeyRing::unwrapAny(Repository::wrappedKeyAt($this->backend), $passphrase);
|
||||
$this->assertTrue(Repository::madeWithKey($this->backend, $keys));
|
||||
$this->assertSame($this->keys->exportMasterKey(), $keys->exportMasterKey());
|
||||
}
|
||||
}
|
||||
|
||||
public function testAWrongPassphraseGivesNothing(): void {
|
||||
$this->expectException(CryptoException::class);
|
||||
KeyRing::unwrapAny(Repository::wrappedKeyAt($this->backend), 'not the passphrase');
|
||||
}
|
||||
|
||||
public function testAnotherKeyIsNotTakenForTheRightOne(): void {
|
||||
$this->assertFalse(Repository::madeWithKey($this->backend, KeyRing::generate()));
|
||||
}
|
||||
|
||||
public function testTheSettingsAreSealedAndWrittenOnce(): void {
|
||||
$this->assertSame(30, Repository::settingsAt($this->backend, $this->keys)['delete_delay_days']);
|
||||
Repository::openWithKey($this->backend, $this->keys)->recordSettings(['delete_delay_days' => 1]);
|
||||
$this->assertSame(30, Repository::settingsAt($this->backend, $this->keys)['delete_delay_days']);
|
||||
$this->assertSame([], Repository::settingsAt($this->backend, KeyRing::generate()));
|
||||
}
|
||||
|
||||
public function testAlteredSettingsAreNotBelieved(): void {
|
||||
$file = $this->dir . '/settings';
|
||||
$raw = file_get_contents($file);
|
||||
$raw[strlen($raw) - 1] = $raw[strlen($raw) - 1] ^ "\x01";
|
||||
file_put_contents($file, $raw);
|
||||
$this->assertSame([], Repository::settingsAt($this->backend, $this->keys));
|
||||
}
|
||||
|
||||
public function testTheKitWritesALostConfigAgainAndTheBackupsOpen(): void {
|
||||
$src = $this->tempDir();
|
||||
file_put_contents("$src/a", 'still here');
|
||||
$repo = Repository::openWithKey($this->backend, $this->keys);
|
||||
$id = $repo->id();
|
||||
$snapshot = $repo->backupDirectory($src)['snapshot'];
|
||||
unlink($this->dir . '/config');
|
||||
$this->assertTrue(Repository::holdsData($this->backend));
|
||||
|
||||
Repository::repairConfig($this->backend, $this->keys, $this->keys->wrap('kit passphrase'));
|
||||
$repo = Repository::openWithKey($this->backend, $this->keys);
|
||||
$this->assertSame($id, $repo->id());
|
||||
$this->assertSame([$snapshot], $repo->listSnapshots());
|
||||
$out = $this->tempDir();
|
||||
$repo->restore($snapshot, $out);
|
||||
$this->assertSame('still here', file_get_contents("$out/a"));
|
||||
$this->assertSame($this->keys->exportMasterKey(), KeyRing::unwrapAny(Repository::wrappedKeyAt($this->backend), 'kit passphrase')->exportMasterKey());
|
||||
}
|
||||
|
||||
public function testAnotherKeyWritesNoConfig(): void {
|
||||
unlink($this->dir . '/config');
|
||||
$other = KeyRing::generate();
|
||||
try {
|
||||
Repository::repairConfig($this->backend, $other, $other->wrap('x'));
|
||||
$this->fail('a config was written with another key');
|
||||
} catch (\OCA\NgBackup\Repository\RepositoryException $e) {
|
||||
$this->assertStringContainsString('another server', $e->getMessage());
|
||||
}
|
||||
$this->assertFileDoesNotExist($this->dir . '/config');
|
||||
}
|
||||
|
||||
public function testAConfigThatIsThereIsNeverRepaired(): void {
|
||||
$before = file_get_contents($this->dir . '/config');
|
||||
try {
|
||||
Repository::repairConfig($this->backend, $this->keys, $this->keys->wrap('x'));
|
||||
$this->fail('an existing config was replaced');
|
||||
} catch (\OCA\NgBackup\Repository\RepositoryException) {
|
||||
}
|
||||
$this->assertSame($before, file_get_contents($this->dir . '/config'));
|
||||
}
|
||||
|
||||
public function testALocationWithoutSettingsHasNone(): void {
|
||||
$other = new LocalBackend($this->tempDir());
|
||||
Repository::initWithKey($other, $this->keys, $this->keys->wrap('x'));
|
||||
$this->assertSame([], Repository::settingsAt($other, $this->keys));
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
# Disaster recovery: occ backup:key:import + backup:target:add + backup:restore:full on a
|
||||
# SECOND, fresh Nextcloud instance that never saw the first one, sharing only the backup
|
||||
# location (a local directory, bind-mounted into both containers) -- see PLAN.md 3.7.
|
||||
# Usage: full-restore-roundtrip.sh <nextcloud-image-tag> [sqlite|pgsql|mysql] [legacy|sftp|salvage|salvage-files|to-pgsql|to-mysql|to-sqlite]
|
||||
# Usage: full-restore-roundtrip.sh <nextcloud-image-tag> [sqlite|pgsql|mysql] [legacy|sftp|salvage|salvage-files|to-pgsql|to-mysql|to-sqlite|no-kit]
|
||||
# e.g. full-restore-roundtrip.sh 35-apache pgsql
|
||||
# With "no-kit" the recovery kit is gone: backup:target:add takes the key from the location with
|
||||
# the passphrase, and backups wait until a new kit is confirmed.
|
||||
# With "salvage-files" one pack is damaged and another is gone from the location. The restore must
|
||||
# stop and point at --salvage; run again as the guide says, it restores the database and every file
|
||||
# that can be read, and lists the rest (exit 2). NGB_FRT_DATA_MB sets the amount of data (default 200).
|
||||
@@ -93,7 +95,10 @@ run_on ngb-frt-origin app:enable ng_backup >/dev/null
|
||||
ok "origin instance up ($DB${LEGACY:+, NG Backup 0.9.7})"
|
||||
|
||||
docker exec ngb-frt-origin sh -c "printf 'full restore roundtrip passphrase 2026\n' > /tmp/pass"
|
||||
run_on ngb-frt-origin backup:key:init --passphrase-file=/tmp/pass --label=CI >/dev/null || fail "key init"
|
||||
DELAY_OPT=()
|
||||
[ "$MODE" = no-kit ] && DELAY_OPT=(--delete-delay-days=21)
|
||||
run_on ngb-frt-origin backup:key:init --passphrase-file=/tmp/pass --label=CI "${DELAY_OPT[@]}" >/dev/null || fail "key init"
|
||||
ORIGIN_FP=$(run_on ngb-frt-origin backup:status | grep -o 'Key: [0-9a-f-]*' | cut -d' ' -f2)
|
||||
run_on ngb-frt-origin backup:key:kit --output=/tmp/kit.json >/dev/null
|
||||
CODE=$(run_on ngb-frt-origin backup:key:kit | php -r 'echo json_decode(stream_get_contents(STDIN), true)["confirmation_code"];')
|
||||
run_on ngb-frt-origin backup:key:confirm -u admin --code="$CODE" --confirm="Yes, I confirm" >/dev/null || fail "confirm"
|
||||
@@ -187,8 +192,29 @@ docker exec ngb-frt-restore chmod 644 /tmp/kit.json
|
||||
docker cp /tmp/ngb-frt-pass ngb-frt-restore:/tmp/pass
|
||||
ok "fresh restore instance up"
|
||||
|
||||
run_on ngb-frt-restore backup:key:import /tmp/kit.json --passphrase-file=/tmp/pass >/dev/null || fail "key import"
|
||||
ADD_OUT=$(run_on ngb-frt-restore backup:target:add offsite "${LOCATION[@]}" --path=repo 2>&1) || { printf '%s\n' "$ADD_OUT"; fail "target add on the restore instance"; }
|
||||
if [ "$MODE" = no-kit ]; then
|
||||
# No recovery kit: backup:target:add takes the key from the location with a passphrase
|
||||
docker exec ngb-frt-restore rm -f /tmp/kit.json
|
||||
docker exec ngb-frt-restore sh -c "printf 'not the passphrase of the old server\n' > /tmp/wrongpass && chmod 644 /tmp/wrongpass"
|
||||
OUT=$(run_on ngb-frt-restore backup:target:add offsite "${LOCATION[@]}" --path=repo --passphrase-file=/tmp/wrongpass 2>&1) \
|
||||
&& { printf '%s\n' "$OUT"; fail "a wrong passphrase was accepted"; }
|
||||
printf '%s\n' "$OUT" | grep -q "Nothing was changed" || { printf '%s\n' "$OUT"; fail "a wrong passphrase did not say that nothing was changed"; }
|
||||
STATUS_OUT=$(run_on ngb-frt-restore backup:status 2>&1 || true)
|
||||
printf '%s\n' "$STATUS_OUT" | grep -q "Key: not created" || { printf '%s\n' "$STATUS_OUT"; fail "a wrong passphrase left a key behind"; }
|
||||
[ -z "$(run_on ngb-frt-restore backup:target:list 2>&1 | grep offsite)" ] || fail "a wrong passphrase left a location behind"
|
||||
OUT=$(run_on ngb-frt-restore backup:target:add other "${LOCATION[@]}" --path=nothing-here --passphrase-file=/tmp/pass 2>&1) \
|
||||
&& { printf '%s\n' "$OUT"; fail "a folder without backups was added without a key"; }
|
||||
printf '%s\n' "$OUT" | grep -q "no backups at this location" || { printf '%s\n' "$OUT"; fail "a folder without backups did not say so"; }
|
||||
ok "without a kit: a wrong passphrase and a folder without backups change nothing"
|
||||
ADD_OUT=$(run_on ngb-frt-restore backup:target:add offsite "${LOCATION[@]}" --path=repo --passphrase-file=/tmp/pass 2>&1) \
|
||||
|| { printf '%s\n' "$ADD_OUT"; fail "target add with the passphrase on the restore instance"; }
|
||||
printf '%s\n' "$ADD_OUT" | grep -q "fingerprint $ORIGIN_FP" || { printf '%s\n' "$ADD_OUT"; fail "the key from the location is not the origin's ($ORIGIN_FP)"; }
|
||||
printf '%s\n' "$ADD_OUT" | grep -q "21 days (from the location)" || { printf '%s\n' "$ADD_OUT"; fail "the deletion delay did not come from the location"; }
|
||||
ok "without a kit: the key and the deletion delay came from the location"
|
||||
else
|
||||
run_on ngb-frt-restore backup:key:import /tmp/kit.json --passphrase-file=/tmp/pass >/dev/null || fail "key import"
|
||||
ADD_OUT=$(run_on ngb-frt-restore backup:target:add offsite "${LOCATION[@]}" --path=repo 2>&1) || { printf '%s\n' "$ADD_OUT"; fail "target add on the restore instance"; }
|
||||
fi
|
||||
LIST_OUT=$(run_on ngb-frt-restore backup:list offsite 2>&1)
|
||||
printf '%s\n' "$LIST_OUT" | grep -q "$SNAP" || {
|
||||
printf 'target:add said:\n%s\nbackup:list said:\n%s\n' "$ADD_OUT" "$LIST_OUT"
|
||||
@@ -196,7 +222,8 @@ printf '%s\n' "$LIST_OUT" | grep -q "$SNAP" || {
|
||||
docker exec ngb-frt-restore sh -c 'ls -la /mnt/shared/repo /mnt/shared/repo/snapshots' || true
|
||||
fail "the origin's snapshot is not listed after connecting the location"
|
||||
}
|
||||
ok "recovery kit imported, existing repository adopted, snapshot visible"
|
||||
if [ "$MODE" = no-kit ]; then KEY_FROM="key from the location"; else KEY_FROM="recovery kit imported"; fi
|
||||
ok "$KEY_FROM, existing repository adopted, snapshot visible"
|
||||
|
||||
if [ "$MODE" = salvage ]; then
|
||||
set +e
|
||||
@@ -371,6 +398,13 @@ if [ "$MODE" = sftp ]; then
|
||||
ok "over SFTP: read ahead on connections of its own ($LOGINS SFTP logins during the restore), large file identical, no spool left"
|
||||
fi
|
||||
|
||||
if [ "$MODE" = no-kit ]; then
|
||||
OUT=$(run_on ngb-frt-restore backup:run offsite -l post-restore-check 2>&1) && { printf '%s\n' "$OUT"; fail "a backup ran without a confirmed recovery kit"; }
|
||||
printf '%s\n' "$OUT" | grep -q "Confirm the recovery kit" || { printf '%s\n' "$OUT"; fail "the refused backup does not point at the recovery kit"; }
|
||||
CODE=$(run_on ngb-frt-restore backup:key:kit | php -r 'echo json_decode(stream_get_contents(STDIN), true)["confirmation_code"];')
|
||||
run_on ngb-frt-restore backup:key:confirm -u admin --code="$CODE" --confirm="Yes, I confirm" >/dev/null || fail "confirm a new kit after the restore"
|
||||
ok "without a kit: backups wait for a new recovery kit to be confirmed"
|
||||
fi
|
||||
run_on ngb-frt-restore backup:run offsite -l post-restore-check >/dev/null || fail "a new backup after the restore fails"
|
||||
ok "the restored instance can run a new backup of its own"
|
||||
|
||||
|
||||
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
# Getting the backup key onto a new server, with and without the recovery kit, and what the
|
||||
# administrator is told in each case. Every scenario starts on a fresh Nextcloud (SQLite) with a
|
||||
# copy of the origin's location; nothing is restored (full-restore-roundtrip.sh does that).
|
||||
# Usage: recovery-scenarios.sh [nextcloud-image-tag] e.g. recovery-scenarios.sh 35-apache
|
||||
#
|
||||
# kit kit + passphrase: backup:key:import, then backup:target:add asks nothing
|
||||
# kit-wrong kit with a wrong passphrase: refused, no key left behind
|
||||
# no-kit passphrase only: target:add asks for it (or --passphrase-file), a wrong one
|
||||
# changes nothing; backups wait for a new kit
|
||||
# no-kit-old as no-kit, on a location backed up only before 1.1.0 (no settings file):
|
||||
# the deletion delay is the default, and the admin is told so
|
||||
# no-config-no-kit the location's config is gone and there is no kit: told that only the kit helps
|
||||
# no-config-kit config gone, kit there: --repair-config writes it again, the backups open
|
||||
# other-kit the kit of another server: refused, told to use the old server's kit
|
||||
# empty nothing at the location: told to check the details, not to make a new key
|
||||
set -euo pipefail
|
||||
IMAGE="nextcloud:${1:-35-apache}"
|
||||
APP=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
SHARED=$(mktemp -d)
|
||||
chmod 777 "$SHARED"
|
||||
PASS='recovery scenarios passphrase 2026'
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
ok() { echo "ok $*"; }
|
||||
|
||||
cleanup() {
|
||||
docker rm -fv ngb-rs-origin ngb-rs-other ngb-rs-new >/dev/null 2>&1 || true
|
||||
docker run --rm -v "$SHARED:/target" alpine sh -c 'rm -rf /target/*' >/dev/null 2>&1 || true
|
||||
rmdir "$SHARED" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
run_on() { docker exec -u www-data "$1" php occ "${@:2}"; }
|
||||
# Output of a command that must fail; the scenario fails when it succeeds.
|
||||
must_fail() { # container what args...
|
||||
local out
|
||||
if out=$(run_on "$1" "${@:3}" 2>&1); then printf '%s\n' "$out"; fail "$2: the command succeeded"; fi
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
expect() { # output text what
|
||||
printf '%s\n' "$1" | grep -qF -- "$2" || { printf '%s\n' "$1"; fail "$3: the output does not say \"$2\""; }
|
||||
}
|
||||
not_expect() {
|
||||
printf '%s\n' "$1" | grep -qF -- "$2" && { printf '%s\n' "$1"; fail "$3: the output says \"$2\""; }
|
||||
return 0
|
||||
}
|
||||
|
||||
start() { # name
|
||||
docker rm -fv "$1" >/dev/null 2>&1 || true
|
||||
docker run -d --name "$1" -e SQLITE_DATABASE=nextcloud -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin \
|
||||
-v "$APP:/var/www/html/custom_apps/ng_backup:ro" -v "$SHARED:/mnt/shared" "$IMAGE" >/dev/null
|
||||
for i in $(seq 1 90); do run_on "$1" status 2>/dev/null | grep -q "installed: true" && break; sleep 3; done
|
||||
docker exec "$1" chown www-data:www-data /var/www/html/custom_apps
|
||||
run_on "$1" app:enable ng_backup >/dev/null
|
||||
docker exec "$1" sh -c "printf '%s\n' '$PASS' > /tmp/pass; printf 'not the passphrase at all\n' > /tmp/wrongpass; chmod 644 /tmp/pass /tmp/wrongpass"
|
||||
}
|
||||
key_of() { run_on "$1" backup:status 2>&1 | sed -n 's/^Key: \([0-9a-f-]*\),.*/\1/p'; }
|
||||
no_key() { # container what
|
||||
local s
|
||||
s=$(run_on "$1" backup:status 2>&1 || true)
|
||||
expect "$s" "Key: not created" "$2: a key was left behind"
|
||||
[ -z "$(run_on "$1" backup:target:list 2>&1 | grep offsite || true)" ] || fail "$2: a location was left behind"
|
||||
}
|
||||
location() { echo "local -a null::null -o datadir=/mnt/shared/$1 --path=repo"; }
|
||||
# A fresh server with its own copy of the origin's location
|
||||
scenario() { # name
|
||||
docker exec ngb-rs-origin sh -c "rm -rf /mnt/shared/$1 && mkdir -p /mnt/shared/$1 && cp -a /mnt/shared/origin/repo /mnt/shared/$1/ && chown -R www-data /mnt/shared/$1"
|
||||
start ngb-rs-new
|
||||
docker exec -i ngb-rs-new sh -c 'cat > /tmp/kit.json && chmod 644 /tmp/kit.json' < "$SHARED/kit.json"
|
||||
}
|
||||
|
||||
# The origin: a key with a deletion delay of its own, a kit, one backup
|
||||
start ngb-rs-origin
|
||||
run_on ngb-rs-origin backup:key:init --passphrase-file=/tmp/pass --label=Safe --delete-delay-days=21 >/dev/null || fail "key init"
|
||||
run_on ngb-rs-origin backup:key:kit --output=/tmp/kit.json >/dev/null
|
||||
CODE=$(run_on ngb-rs-origin backup:key:kit | php -r 'echo json_decode(stream_get_contents(STDIN), true)["confirmation_code"];')
|
||||
run_on ngb-rs-origin backup:key:confirm -u admin --code="$CODE" --confirm="Yes, I confirm" >/dev/null || fail "confirm"
|
||||
docker exec ngb-rs-origin sh -c 'mkdir -p /mnt/shared/origin && chown www-data /mnt/shared/origin'
|
||||
# shellcheck disable=SC2046
|
||||
run_on ngb-rs-origin backup:target:add offsite $(location origin) >/dev/null || fail "target add on the origin"
|
||||
run_on ngb-rs-origin backup:run offsite >/dev/null || fail "backup on the origin"
|
||||
SNAP=$(run_on ngb-rs-origin backup:list offsite | grep -o '[0-9a-f]\{32\}' | head -1)
|
||||
FP=$(key_of ngb-rs-origin)
|
||||
docker exec ngb-rs-origin cat /tmp/kit.json > "$SHARED/kit.json"
|
||||
docker exec ngb-rs-origin test -f /mnt/shared/origin/repo/settings || fail "the origin's location has no settings file"
|
||||
ok "origin: key $FP, kit, snapshot $SNAP, settings on the location"
|
||||
|
||||
# kit: the passphrase once, for the kit; target:add asks nothing more
|
||||
scenario kit
|
||||
OUT=$(run_on ngb-rs-new backup:key:import /tmp/kit.json --passphrase-file=/tmp/pass 2>&1) || { printf '%s\n' "$OUT"; fail "kit: key import"; }
|
||||
expect "$OUT" "fingerprint $FP" "kit"
|
||||
expect "$OUT" "21 days (from the kit)" "kit"
|
||||
expect "$OUT" "backup:target:add" "kit: the next step"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(run_on ngb-rs-new backup:target:add offsite $(location kit) --no-interaction 2>&1) || { printf '%s\n' "$OUT"; fail "kit: target add"; }
|
||||
not_expect "$OUT" "Passphrase" "kit: target:add asked for a passphrase"
|
||||
expect "$OUT" "found and adopted" "kit"
|
||||
run_on ngb-rs-new backup:list offsite | grep -q "$SNAP" || fail "kit: the snapshot is not listed"
|
||||
run_on ngb-rs-new backup:run offsite >/dev/null || fail "kit: a backup after the kit import"
|
||||
ok "kit: imported with the passphrase, location added without asking again, backups run"
|
||||
|
||||
# kit-wrong
|
||||
scenario kit-wrong
|
||||
OUT=$(must_fail ngb-rs-new "kit-wrong" backup:key:import /tmp/kit.json --passphrase-file=/tmp/wrongpass)
|
||||
expect "$OUT" "Wrong passphrase" "kit-wrong"
|
||||
no_key ngb-rs-new "kit-wrong"
|
||||
ok "kit-wrong: refused, nothing left behind"
|
||||
|
||||
# no-kit
|
||||
scenario no-kit
|
||||
docker exec ngb-rs-new rm /tmp/kit.json
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "no-kit without a passphrase" backup:target:add offsite $(location no-kit) --no-interaction)
|
||||
expect "$OUT" "this location holds backups" "no-kit: explains where the key comes from"
|
||||
expect "$OUT" "--passphrase-file" "no-kit: says how to give the passphrase"
|
||||
no_key ngb-rs-new "no-kit without a passphrase"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "no-kit wrong passphrase" backup:target:add offsite $(location no-kit) --passphrase-file=/tmp/wrongpass)
|
||||
expect "$OUT" "Wrong passphrase" "no-kit wrong passphrase"
|
||||
expect "$OUT" "Nothing was changed" "no-kit wrong passphrase"
|
||||
no_key ngb-rs-new "no-kit wrong passphrase"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(run_on ngb-rs-new backup:target:add offsite $(location no-kit) --passphrase-file=/tmp/pass 2>&1) || { printf '%s\n' "$OUT"; fail "no-kit: target add"; }
|
||||
expect "$OUT" "fingerprint $FP" "no-kit"
|
||||
expect "$OUT" "21 days (from the location)" "no-kit"
|
||||
expect "$OUT" "download a new recovery kit" "no-kit: asks for a new kit"
|
||||
run_on ngb-rs-new backup:list offsite | grep -q "$SNAP" || fail "no-kit: the snapshot is not listed"
|
||||
OUT=$(must_fail ngb-rs-new "no-kit backup before a new kit" backup:run offsite)
|
||||
expect "$OUT" "Confirm the recovery kit" "no-kit: a backup before a new kit"
|
||||
CODE=$(run_on ngb-rs-new backup:key:kit | php -r 'echo json_decode(stream_get_contents(STDIN), true)["confirmation_code"];')
|
||||
run_on ngb-rs-new backup:key:confirm -u admin --code="$CODE" --confirm="Yes, I confirm" >/dev/null || fail "no-kit: confirm the new kit"
|
||||
run_on ngb-rs-new backup:run offsite >/dev/null || fail "no-kit: a backup after the new kit"
|
||||
ok "no-kit: passphrase asked, wrong one changes nothing, key and delay from the location, backups wait for a new kit"
|
||||
|
||||
# no-kit-old: a location from before 1.1.0 has no settings file
|
||||
scenario no-kit-old
|
||||
docker exec ngb-rs-new rm /tmp/kit.json /mnt/shared/no-kit-old/repo/settings
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(run_on ngb-rs-new backup:target:add offsite $(location no-kit-old) --passphrase-file=/tmp/pass 2>&1) || { printf '%s\n' "$OUT"; fail "no-kit-old: target add"; }
|
||||
expect "$OUT" "fingerprint $FP" "no-kit-old"
|
||||
expect "$OUT" "7 days (the default" "no-kit-old: the default delay and why"
|
||||
docker exec ngb-rs-new test -f /mnt/shared/no-kit-old/repo/settings || fail "no-kit-old: the settings were not written when the location was added"
|
||||
ok "no-kit-old: key from the location, default delay with the reason, settings written now"
|
||||
|
||||
# no-config-no-kit
|
||||
scenario no-config-no-kit
|
||||
docker exec ngb-rs-new rm /tmp/kit.json /mnt/shared/no-config-no-kit/repo/config
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "no-config-no-kit" backup:target:add offsite $(location no-config-no-kit) --passphrase-file=/tmp/pass)
|
||||
expect "$OUT" "its config file is gone" "no-config-no-kit"
|
||||
expect "$OUT" "only the recovery kit has the key" "no-config-no-kit"
|
||||
expect "$OUT" "--repair-config" "no-config-no-kit"
|
||||
no_key ngb-rs-new "no-config-no-kit"
|
||||
docker exec ngb-rs-new test ! -f /mnt/shared/no-config-no-kit/repo/config || fail "no-config-no-kit: a config was written"
|
||||
ok "no-config-no-kit: told the kit is needed, nothing written"
|
||||
|
||||
# no-config-kit
|
||||
scenario no-config-kit
|
||||
docker exec ngb-rs-new rm /mnt/shared/no-config-kit/repo/config
|
||||
run_on ngb-rs-new backup:key:import /tmp/kit.json --passphrase-file=/tmp/pass >/dev/null || fail "no-config-kit: key import"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "no-config-kit without --repair-config" backup:target:add offsite $(location no-config-kit))
|
||||
expect "$OUT" "--repair-config" "no-config-kit: points at --repair-config"
|
||||
docker exec ngb-rs-new test ! -f /mnt/shared/no-config-kit/repo/config || fail "no-config-kit: a config was written without --repair-config"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(run_on ngb-rs-new backup:target:add offsite $(location no-config-kit) --repair-config 2>&1) || { printf '%s\n' "$OUT"; fail "no-config-kit: --repair-config"; }
|
||||
expect "$OUT" "written again" "no-config-kit"
|
||||
expect "$OUT" "found and adopted" "no-config-kit"
|
||||
run_on ngb-rs-new backup:list offsite | grep -q "$SNAP" || fail "no-config-kit: the snapshot is not listed after the repair"
|
||||
run_on ngb-rs-new backup:browse offsite "$SNAP" >/dev/null 2>&1 || fail "no-config-kit: the snapshot cannot be read after the repair"
|
||||
ok "no-config-kit: refused without --repair-config, config written again with it, the backups open"
|
||||
|
||||
# other-kit: the kit of another server
|
||||
start ngb-rs-other
|
||||
run_on ngb-rs-other backup:key:init --passphrase-file=/tmp/pass >/dev/null || fail "other: key init"
|
||||
run_on ngb-rs-other backup:key:kit --output=/tmp/other.json >/dev/null
|
||||
docker exec ngb-rs-other cat /tmp/other.json > "$SHARED/other.json"
|
||||
docker rm -fv ngb-rs-other >/dev/null
|
||||
scenario other-kit
|
||||
docker exec ngb-rs-new rm /tmp/kit.json
|
||||
docker exec -i ngb-rs-new sh -c 'cat > /tmp/other.json && chmod 644 /tmp/other.json' < "$SHARED/other.json"
|
||||
run_on ngb-rs-new backup:key:import /tmp/other.json --passphrase-file=/tmp/pass >/dev/null || fail "other-kit: key import"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "other-kit" backup:target:add offsite $(location other-kit))
|
||||
expect "$OUT" "different key" "other-kit"
|
||||
expect "$OUT" "must be the old server's" "other-kit: points at the right kit"
|
||||
[ -z "$(run_on ngb-rs-new backup:target:list 2>&1 | grep offsite || true)" ] || fail "other-kit: a location was left behind"
|
||||
# shellcheck disable=SC2046
|
||||
OUT=$(must_fail ngb-rs-new "other-kit repair" backup:target:add offsite $(location other-kit) --repair-config)
|
||||
expect "$OUT" "leave out --repair-config" "other-kit: a config that is there is not repaired"
|
||||
ok "other-kit: refused, told to use the old server's kit, the config untouched"
|
||||
|
||||
# empty
|
||||
scenario empty
|
||||
docker exec ngb-rs-new sh -c 'mkdir -p /mnt/shared/nothing && chown www-data /mnt/shared/nothing'
|
||||
OUT=$(must_fail ngb-rs-new "empty" backup:target:add offsite local -a null::null -o datadir=/mnt/shared/nothing --path=repo --passphrase-file=/tmp/pass)
|
||||
expect "$OUT" "no backups at this location" "empty"
|
||||
expect "$OUT" "Check the connection details and --path" "empty"
|
||||
no_key ngb-rs-new "empty"
|
||||
ok "empty: told to check the details, no key made"
|
||||
|
||||
echo "RECOVERY SCENARIOS PASS"
|
||||
Reference in New Issue
Block a user