@microck/raft 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/assets/raft-logo.png +0 -0
- package/LICENSE +21 -0
- package/README.md +162 -2
- package/deploy/benchmark-boot.py +150 -0
- package/deploy/build-image.py +95 -0
- package/deploy/deploy.py +176 -0
- package/deploy/incus-network.conf +4 -0
- package/deploy/install-incus.sh +22 -0
- package/deploy/raft-expire.py +62 -0
- package/deploy/raft-expire.service +8 -0
- package/deploy/raft-expire.timer +8 -0
- package/deploy/raft-network.service +12 -0
- package/deploy/raft-network.sh +40 -0
- package/deploy/verify-controls.py +241 -0
- package/deploy/verify-features.py +461 -0
- package/deploy/verify-fresh-host.py +301 -0
- package/deploy/verify-image-tests.py +139 -0
- package/deploy/verify-image.py +95 -0
- package/deploy/verify-limits.py +110 -0
- package/deploy/verify-live.py +903 -0
- package/deploy/verify-release.py +132 -0
- package/deploy/verify-tools.py +105 -0
- package/docs/benchmarks/amd64-native-ci.json +137 -0
- package/docs/benchmarks/arm64-baseline-a.json +108 -0
- package/docs/benchmarks/arm64-baseline-b.json +108 -0
- package/docs/benchmarks/arm64-candidate-a.json +109 -0
- package/docs/benchmarks/arm64-desktop-local.json +22 -0
- package/docs/benchmarks/arm64-native-ci.json +137 -0
- package/docs/boat-capabilities.md +64 -0
- package/docs/boat-parity-design.md +26 -0
- package/docs/boot-performance.mdx +102 -0
- package/docs/contract.md +93 -0
- package/docs/first-box.mdx +82 -0
- package/docs/guides/background-jobs.mdx +92 -0
- package/docs/guides/backup-recovery.mdx +111 -0
- package/docs/guides/desktop-services.mdx +87 -0
- package/docs/guides/docker.mdx +84 -0
- package/docs/guides/files.mdx +91 -0
- package/docs/guides/meta.json +14 -0
- package/docs/guides/snapshots-forks.mdx +107 -0
- package/docs/guides/troubleshooting.mdx +127 -0
- package/docs/incus.example.json +3 -0
- package/docs/index.mdx +36 -0
- package/docs/meta.json +21 -0
- package/docs/operations.mdx +224 -0
- package/docs/publishing.md +48 -0
- package/docs/reference/capacity-limits.mdx +83 -0
- package/docs/reference/cli.md +279 -0
- package/docs/reference/configuration.mdx +77 -0
- package/docs/reference/meta.json +10 -0
- package/docs/releases/0.2.0.md +22 -0
- package/docs/requirements.mdx +47 -0
- package/docs/reviews/code-review-2026-10-06.md +168 -0
- package/docs/site-development.md +80 -0
- package/docs/verification.mdx +231 -0
- package/images/incus/build.sh +57 -0
- package/images/incus/desktop.sh +21 -0
- package/images/incus/raft-desktop.service +7 -0
- package/package.json +46 -4
- package/raft.py +955 -0
- package/raft_files.py +134 -0
- package/skills/raft-cli/SKILL.md +117 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Enforce running deadlines and preview or delete explicitly selected old boxes."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
PROTOCOL = 1
|
|
10
|
+
|
|
11
|
+
PREFIX = ["incus", "--project", "raft"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(*argv):
|
|
15
|
+
return subprocess.run([*PREFIX, *argv], check=True, capture_output=True, text=True).stdout
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
cli = argparse.ArgumentParser(description=__doc__)
|
|
20
|
+
cli.add_argument("--prune-days", type=int)
|
|
21
|
+
cli.add_argument("--yes", action="store_true")
|
|
22
|
+
args = cli.parse_args()
|
|
23
|
+
if args.prune_days is not None and args.prune_days < 1:
|
|
24
|
+
cli.error("Retention must be at least one day")
|
|
25
|
+
if args.yes and args.prune_days is None:
|
|
26
|
+
cli.error("--yes requires --prune-days")
|
|
27
|
+
now = int(time.time())
|
|
28
|
+
# The service, gc and prune all hold the same lifecycle lock. Never select
|
|
29
|
+
# unrelated Incus instances or use creation time as a guessed stop time.
|
|
30
|
+
for box in json.loads(run("list", "--fast", "--format", "json")):
|
|
31
|
+
config = box["config"]
|
|
32
|
+
if config.get("user.raft") != "true":
|
|
33
|
+
continue
|
|
34
|
+
name = box["name"]
|
|
35
|
+
if args.prune_days is not None:
|
|
36
|
+
stopped_at = config.get("user.raft.stopped-at")
|
|
37
|
+
if box["status"] != "Stopped" or not stopped_at:
|
|
38
|
+
continue
|
|
39
|
+
if int(stopped_at) > now - args.prune_days * 86400:
|
|
40
|
+
continue
|
|
41
|
+
if args.yes:
|
|
42
|
+
run("delete", name)
|
|
43
|
+
print(json.dumps({"box": name, "action": "deleted" if args.yes else "would-delete"}))
|
|
44
|
+
continue
|
|
45
|
+
deadline = config.get("user.raft.expires")
|
|
46
|
+
if not deadline or int(deadline) > now:
|
|
47
|
+
continue
|
|
48
|
+
if box["status"] not in ["Running", "Stopped"]:
|
|
49
|
+
raise RuntimeError(f"Unexpected state for expired box {name}")
|
|
50
|
+
if config.get("user.raft.disposable") == "true":
|
|
51
|
+
run("delete", name, "--force")
|
|
52
|
+
print(f"Deleted expired disposable box {name}", flush=True)
|
|
53
|
+
continue
|
|
54
|
+
if box["status"] == "Running":
|
|
55
|
+
run("stop", name, "--timeout", "30")
|
|
56
|
+
run("config", "set", name, f"user.raft.stopped-at={now}")
|
|
57
|
+
run("config", "unset", name, "user.raft.expires")
|
|
58
|
+
print(f"Stopped expired box {name}; disk retained", flush=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Private Raft bridge firewall rules
|
|
3
|
+
After=network-online.target docker.service nftables.service
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
Before=incus.service incus-startup.service raft-expire.service
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
ExecStart=/usr/local/lib/raft/network
|
|
9
|
+
ExecReload=/usr/local/lib/raft/network
|
|
10
|
+
RemainAfterExit=yes
|
|
11
|
+
[Install]
|
|
12
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
# Peer frames can bypass IP hooks when bridge-netfilter is absent. Filter them
|
|
4
|
+
# at the bridge hook, scoped to Raft, without changing host-wide bridge sysctls.
|
|
5
|
+
nft -f - <<'RULES'
|
|
6
|
+
add table bridge raft
|
|
7
|
+
add chain bridge raft peers { type filter hook forward priority -200; policy accept; }
|
|
8
|
+
flush chain bridge raft peers
|
|
9
|
+
add rule bridge raft peers meta ibrname "rfbr0" meta obrname "rfbr0" counter drop
|
|
10
|
+
RULES
|
|
11
|
+
# Replace the owned IPv4 chains in one transaction. Existing jumps continue to
|
|
12
|
+
# enforce the old complete policy until the new complete policy commits.
|
|
13
|
+
iptables-restore --noflush <<'RULES'
|
|
14
|
+
*filter
|
|
15
|
+
:RAFT-INPUT - [0:0]
|
|
16
|
+
:RAFT-FORWARD - [0:0]
|
|
17
|
+
-F RAFT-INPUT
|
|
18
|
+
-F RAFT-FORWARD
|
|
19
|
+
-A RAFT-INPUT -p udp --dport 67 -j ACCEPT
|
|
20
|
+
-A RAFT-INPUT -d 10.232.0.1 -p udp --dport 53 -j ACCEPT
|
|
21
|
+
-A RAFT-INPUT -d 10.232.0.1 -p tcp --dport 53 -j ACCEPT
|
|
22
|
+
-A RAFT-INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
23
|
+
-A RAFT-INPUT -j REJECT
|
|
24
|
+
-A RAFT-FORWARD -d 10.0.0.0/8 -j REJECT
|
|
25
|
+
-A RAFT-FORWARD -d 100.64.0.0/10 -j REJECT
|
|
26
|
+
-A RAFT-FORWARD -d 169.254.0.0/16 -j REJECT
|
|
27
|
+
-A RAFT-FORWARD -d 172.16.0.0/12 -j REJECT
|
|
28
|
+
-A RAFT-FORWARD -d 192.168.0.0/16 -j REJECT
|
|
29
|
+
-A RAFT-FORWARD -j ACCEPT
|
|
30
|
+
COMMIT
|
|
31
|
+
RULES
|
|
32
|
+
iptables -C INPUT -i rfbr0 -j RAFT-INPUT 2>/dev/null || iptables -I INPUT 1 -i rfbr0 -j RAFT-INPUT
|
|
33
|
+
iptables -C FORWARD -i rfbr0 -j RAFT-FORWARD 2>/dev/null || iptables -I FORWARD 1 -i rfbr0 -j RAFT-FORWARD
|
|
34
|
+
iptables -C FORWARD -o rfbr0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -o rfbr0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
35
|
+
|
|
36
|
+
iptables -C FORWARD -o rfbr0 -m conntrack --ctstate NEW -j REJECT 2>/dev/null || iptables -I FORWARD 1 -o rfbr0 -m conntrack --ctstate NEW -j REJECT
|
|
37
|
+
# IPv6 is not provided on this bridge; reject link-local peer traffic too.
|
|
38
|
+
ip6tables -C INPUT -i rfbr0 -j REJECT 2>/dev/null || ip6tables -I INPUT 1 -i rfbr0 -j REJECT
|
|
39
|
+
ip6tables -C FORWARD -i rfbr0 -j REJECT 2>/dev/null || ip6tables -I FORWARD 1 -i rfbr0 -j REJECT
|
|
40
|
+
ip6tables -C FORWARD -o rfbr0 -j REJECT 2>/dev/null || ip6tables -I FORWARD 1 -o rfbr0 -j REJECT
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Verify portable recovery between real hosts using a Debian fixture or full image."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
|
+
from raft import ADMISSION, address, incus, inventory, parse_handle, transaction # noqa: E402
|
|
13
|
+
import importlib.util # noqa: E402
|
|
14
|
+
|
|
15
|
+
spec = importlib.util.spec_from_file_location(
|
|
16
|
+
"verify_live", Path(__file__).with_name("verify-live.py")
|
|
17
|
+
)
|
|
18
|
+
live = importlib.util.module_from_spec(spec)
|
|
19
|
+
spec.loader.exec_module(live)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def wait_address(box):
|
|
23
|
+
# Incus Running precedes DHCP readiness. Use the canonical managed-NIC
|
|
24
|
+
# lookup, not hostname -I, which can include guest Docker bridge addresses.
|
|
25
|
+
location, name = parse_handle(box)
|
|
26
|
+
deadline = time.monotonic() + 30
|
|
27
|
+
while time.monotonic() < deadline:
|
|
28
|
+
try:
|
|
29
|
+
return address(location, name)
|
|
30
|
+
except ValueError:
|
|
31
|
+
time.sleep(0.2)
|
|
32
|
+
raise RuntimeError("Resumed workspace has no managed IPv4 address after 30 seconds")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def verify_admission(location, directory):
|
|
36
|
+
fixtures = []
|
|
37
|
+
saved = len(inventory(location))
|
|
38
|
+
if saved >= 4:
|
|
39
|
+
raise RuntimeError("Admission test needs at least one free slot on its target")
|
|
40
|
+
try:
|
|
41
|
+
for _ in range(4 - saved):
|
|
42
|
+
box = live.raft(
|
|
43
|
+
"new", "--location", location, "--memory", "1GiB", "--ttl", "600"
|
|
44
|
+
).stdout.strip()
|
|
45
|
+
fixtures.append(box)
|
|
46
|
+
live.wait_running(box)
|
|
47
|
+
live.raft("stop", box)
|
|
48
|
+
rejected = live.raft("new", "--location", location, check=False)
|
|
49
|
+
if rejected.returncode == 0:
|
|
50
|
+
fixtures.append(rejected.stdout.strip())
|
|
51
|
+
assert rejected.returncode != 0 and "already holds four boxes" in rejected.stderr
|
|
52
|
+
rejected = live.raft("fork", fixtures[0], "--ttl", "600", check=False)
|
|
53
|
+
if rejected.returncode == 0:
|
|
54
|
+
fixtures.append(rejected.stdout.strip())
|
|
55
|
+
assert rejected.returncode != 0 and "already holds four boxes" in rejected.stderr
|
|
56
|
+
with tempfile.TemporaryDirectory(dir=directory, prefix="raft-admission-") as work:
|
|
57
|
+
# Admission must reject before native archive parsing, without creating a box.
|
|
58
|
+
archive = Path(work) / "rejected.tar.gz"
|
|
59
|
+
archive.write_bytes(b"not-an-incus-archive")
|
|
60
|
+
rejected = live.raft("recover", str(archive), "--location", location, check=False)
|
|
61
|
+
if rejected.returncode == 0:
|
|
62
|
+
fixtures.append(rejected.stdout.strip())
|
|
63
|
+
assert rejected.returncode != 0 and "already holds four boxes" in rejected.stderr
|
|
64
|
+
assert len(inventory(location)) == 4
|
|
65
|
+
report = json.loads(live.raft("limits", "--location", location, "--json").stdout)
|
|
66
|
+
assert report["savedBoxes"] == 4 and report["savedSlots"] == 0
|
|
67
|
+
assert all(size["newBoxes"] == 0 for size in report["recommendations"])
|
|
68
|
+
print(
|
|
69
|
+
f"{location}: stopped boxes count toward admission; new/fork/recover reject a fifth box",
|
|
70
|
+
flush=True,
|
|
71
|
+
)
|
|
72
|
+
finally:
|
|
73
|
+
for box in reversed(fixtures):
|
|
74
|
+
live.raft("destroy", box)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def verify(source, target, directory, development_image=False):
|
|
78
|
+
fixtures = []
|
|
79
|
+
name = "rf-" + uuid.uuid4().hex[:16]
|
|
80
|
+
box = source + ":" + name
|
|
81
|
+
try:
|
|
82
|
+
if development_image:
|
|
83
|
+
box = live.raft("new", "--location", source, "--ttl", "1800").stdout.strip()
|
|
84
|
+
fixtures.append(box)
|
|
85
|
+
live.wait_running(box)
|
|
86
|
+
else:
|
|
87
|
+
transaction(
|
|
88
|
+
source,
|
|
89
|
+
ADMISSION + 'incus --project raft launch images:debian/13 "$1" '
|
|
90
|
+
"-c user.raft=true -c limits.cpu=1 -c limits.memory=1GiB; "
|
|
91
|
+
'incus --project raft config set "$1" user.raft.expires="$(( $(date +%s) + 600 ))"',
|
|
92
|
+
name,
|
|
93
|
+
)
|
|
94
|
+
fixtures.append(box)
|
|
95
|
+
incus(source, "exec", name, "--", "mkdir", "-p", "/workspace")
|
|
96
|
+
live.execute(
|
|
97
|
+
box,
|
|
98
|
+
"bash",
|
|
99
|
+
"-ec",
|
|
100
|
+
"apt-get update -qq; apt-get install -y --no-install-recommends python3",
|
|
101
|
+
)
|
|
102
|
+
inventories = None
|
|
103
|
+
if development_image:
|
|
104
|
+
inventories = live.execute(
|
|
105
|
+
box, "sha256sum", "/opt/raft/packages.tsv", "/opt/raft/npm-packages.json"
|
|
106
|
+
)
|
|
107
|
+
print(f"{source}: fixture ready", flush=True)
|
|
108
|
+
live.execute(
|
|
109
|
+
box,
|
|
110
|
+
"bash",
|
|
111
|
+
"-ec",
|
|
112
|
+
"printf raft-backup-proof > /workspace/proof; chmod 640 /workspace/proof",
|
|
113
|
+
)
|
|
114
|
+
job = live.raft(
|
|
115
|
+
"exec",
|
|
116
|
+
box,
|
|
117
|
+
"--detach",
|
|
118
|
+
"--",
|
|
119
|
+
"bash",
|
|
120
|
+
"-ec",
|
|
121
|
+
"sleep 600 & echo $! > /workspace/child-pid; wait",
|
|
122
|
+
).stdout.strip()
|
|
123
|
+
deadline = time.monotonic() + 10
|
|
124
|
+
while live.raft(
|
|
125
|
+
"exec", box, "--", "test", "-f", "/workspace/child-pid", check=False
|
|
126
|
+
).returncode:
|
|
127
|
+
if time.monotonic() > deadline:
|
|
128
|
+
raise RuntimeError("Detached job did not start")
|
|
129
|
+
time.sleep(0.2)
|
|
130
|
+
child = live.execute(box, "cat", "/workspace/child-pid")
|
|
131
|
+
assert "ActiveState=active" in live.raft("status", box, job).stdout
|
|
132
|
+
live.raft("extend", box, "--ttl", "1200")
|
|
133
|
+
extended = json.loads(live.raft("info", box).stdout)
|
|
134
|
+
assert (
|
|
135
|
+
int(extended["config"]["user.raft.expires"])
|
|
136
|
+
> int(live.execute(box, "date", "+%s")) + 1100
|
|
137
|
+
)
|
|
138
|
+
assert "ActiveState=active" in live.raft("status", box, job).stdout
|
|
139
|
+
# Exercise journal retention without changing any host service or other box.
|
|
140
|
+
live.execute(box, "journalctl", "--rotate", "--vacuum-size=1")
|
|
141
|
+
assert "ActiveState=active" in live.raft("status", box, job).stdout
|
|
142
|
+
live.raft("cancel", box, job)
|
|
143
|
+
assert "ActiveState=inactive" in live.raft("status", box, job).stdout
|
|
144
|
+
assert live.raft("logs", box, job).returncode == 0
|
|
145
|
+
assert (
|
|
146
|
+
live.raft("exec", box, "--", "test", "-e", "/proc/" + child, check=False).returncode
|
|
147
|
+
!= 0
|
|
148
|
+
)
|
|
149
|
+
for command in ["status", "logs", "cancel"]:
|
|
150
|
+
missing = live.raft(command, box, "rfcmd-" + "0" * 32, check=False)
|
|
151
|
+
assert missing.returncode == 1 and "Detached job not found" in missing.stderr
|
|
152
|
+
usage = json.loads(live.raft("usage", box).stdout)
|
|
153
|
+
assert 0 < usage["memoryBytes"] < int(usage["memoryLimit"])
|
|
154
|
+
assert usage["filesystemScope"] == "shared Raft pool, not per-box usage"
|
|
155
|
+
with tempfile.TemporaryDirectory(dir=directory, prefix="raft-controls-") as work:
|
|
156
|
+
archive = Path(work) / "workspace with spaces.tar.gz"
|
|
157
|
+
assert live.raft("backup", box, str(archive), check=False).returncode != 0
|
|
158
|
+
assert not archive.exists()
|
|
159
|
+
live.raft("stop", box)
|
|
160
|
+
uploaded = Path(work) / "stopped upload.bin"
|
|
161
|
+
downloaded = Path(work) / "stopped download.bin"
|
|
162
|
+
uploaded.write_bytes(bytes(range(256)))
|
|
163
|
+
live.raft("upload", box, str(uploaded), "/workspace/stopped file.bin")
|
|
164
|
+
live.raft("download", box, "/workspace/stopped file.bin", str(downloaded))
|
|
165
|
+
assert downloaded.read_bytes() == uploaded.read_bytes()
|
|
166
|
+
assert live.raft("extend", box, "--ttl", "600", check=False).returncode != 0
|
|
167
|
+
live.raft("snapshot", box, "checkpoint")
|
|
168
|
+
print(f"{source}: exporting stopped workspace and snapshot", flush=True)
|
|
169
|
+
live.raft("backup", box, str(archive))
|
|
170
|
+
size = archive.stat().st_size
|
|
171
|
+
assert size > 0 and archive.stat().st_mode & 0o077 == 0
|
|
172
|
+
print(f"{source}: archive completed ({size} bytes)", flush=True)
|
|
173
|
+
assert live.raft("backup", box, str(archive), check=False).returncode != 0
|
|
174
|
+
assert archive.stat().st_size == size
|
|
175
|
+
restored = live.raft("recover", str(archive), "--location", target).stdout.strip()
|
|
176
|
+
fixtures.append(restored)
|
|
177
|
+
print(f"{target}: recovered stopped workspace", flush=True)
|
|
178
|
+
metadata = json.loads(live.raft("info", restored).stdout)
|
|
179
|
+
assert metadata["status"] == "Stopped"
|
|
180
|
+
assert not metadata["config"].get("user.raft.expires")
|
|
181
|
+
assert "checkpoint" in live.raft("snapshots", restored).stdout
|
|
182
|
+
live.raft("resume", restored, "--ttl", "600")
|
|
183
|
+
if development_image:
|
|
184
|
+
live.wait_running(restored)
|
|
185
|
+
assert (
|
|
186
|
+
live.execute(
|
|
187
|
+
restored,
|
|
188
|
+
"sha256sum",
|
|
189
|
+
"/opt/raft/packages.tsv",
|
|
190
|
+
"/opt/raft/npm-packages.json",
|
|
191
|
+
)
|
|
192
|
+
== inventories
|
|
193
|
+
)
|
|
194
|
+
import subprocess
|
|
195
|
+
|
|
196
|
+
subprocess.run(
|
|
197
|
+
[sys.executable, str(Path(__file__).with_name("verify-tools.py")), restored],
|
|
198
|
+
check=True,
|
|
199
|
+
)
|
|
200
|
+
recovered_config = json.loads(live.raft("info", restored).stdout)["config"]
|
|
201
|
+
source_config = json.loads(live.raft("info", box).stdout)["config"]
|
|
202
|
+
assert recovered_config["volatile.eth0.hwaddr"] != source_config["volatile.eth0.hwaddr"]
|
|
203
|
+
assert live.execute(restored, "cat", "/workspace/proof") == "raft-backup-proof"
|
|
204
|
+
assert live.execute(restored, "stat", "-c", "%a", "/workspace/proof") == "640"
|
|
205
|
+
live.execute(restored, "bash", "-ec", "printf changed > /workspace/proof")
|
|
206
|
+
live.raft("stop", restored)
|
|
207
|
+
live.raft("restore", restored, "checkpoint")
|
|
208
|
+
live.raft("resume", restored, "--ttl", "600")
|
|
209
|
+
assert live.execute(restored, "cat", "/workspace/proof") == "raft-backup-proof"
|
|
210
|
+
restored_config = json.loads(live.raft("info", restored).stdout)["config"]
|
|
211
|
+
assert restored_config["volatile.eth0.hwaddr"] != source_config["volatile.eth0.hwaddr"]
|
|
212
|
+
if source == target:
|
|
213
|
+
live.raft("resume", box, "--ttl", "600")
|
|
214
|
+
original_ip = wait_address(box)
|
|
215
|
+
recovered_ip = wait_address(restored)
|
|
216
|
+
assert original_ip != recovered_ip
|
|
217
|
+
assert live.execute(box, "cat", "/workspace/proof") == "raft-backup-proof"
|
|
218
|
+
print(
|
|
219
|
+
f"{source} -> {target}: cancellation, usage, portable recovery and snapshots passed ({size} bytes)",
|
|
220
|
+
flush=True,
|
|
221
|
+
)
|
|
222
|
+
finally:
|
|
223
|
+
for fixture in reversed(fixtures):
|
|
224
|
+
live.raft("destroy", fixture)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
229
|
+
parser.add_argument("--source", required=True)
|
|
230
|
+
parser.add_argument("--target", required=True)
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
"--directory", default=None, help="Controller directory with room for the archive"
|
|
233
|
+
)
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--development-image",
|
|
236
|
+
action="store_true",
|
|
237
|
+
help="Export the complete configured development image rather than a small fixture",
|
|
238
|
+
)
|
|
239
|
+
args = parser.parse_args()
|
|
240
|
+
verify_admission(args.target, args.directory)
|
|
241
|
+
verify(args.source, args.target, args.directory, args.development_image)
|