feat(station): Rettung folgt dem Bildsatz-Umschalter, Server meldet Lebenszeichen
server.py: image_path() liefert jetzt aus rescue_dir() (nach dem Umschalter ohne-wlan/mit-wlan), genau wie migrate_dir() es fuer die Migration schon tut. Bewusst kein Fallback auf den alten flachen Bestand in images/ - ein Pi mit altem Bestand muss einmal per deploy.sh neu bestueckt werden, sonst wuerde "ohne-wlan" den alten Zustand (immer mit Zugangsdaten) stillschweigend fortsetzen. Station.last_activity/"last_request_ts" in status.json: Zeitpunkt jeder Anfrage (auch erfolgloser), damit die Anzeige ein Lebenszeichen zeigen kann, statt nur am Servierzaehler abzulesen, ob ueberhaupt etwas ankommt.
This commit is contained in:
+64
-3
@@ -86,6 +86,21 @@ def trailer_version(path: Path) -> str:
|
|||||||
IMAGE_SET_DIRS = {"ohne-wlan": "migrate-nocreds", "mit-wlan": "migrate-wlan"}
|
IMAGE_SET_DIRS = {"ohne-wlan": "migrate-nocreds", "mit-wlan": "migrate-wlan"}
|
||||||
DEFAULT_IMAGE_SET = "ohne-wlan"
|
DEFAULT_IMAGE_SET = "ohne-wlan"
|
||||||
|
|
||||||
|
# Derselbe Umschalter gilt jetzt auch fuer die RETTUNG (nicht nur die Migration): der
|
||||||
|
# Eigentuemer hat das ausdruecklich verlangt, denn bisher bekam eine Rettung IMMER ein
|
||||||
|
# Abbild MIT Zugangsdaten (die flachen Abzuege in images/ stammten aus build/). Die
|
||||||
|
# Rettungsabzuege OHNE Zugangsdaten kommen aus tools/build_nocreds.sh, genau wie ihr
|
||||||
|
# Migrations-Gegenstueck - deshalb dasselbe Namensschema, nur mit "rescue-" statt
|
||||||
|
# "migrate-" (deploy.sh legt sie entsprechend ab, siehe dort).
|
||||||
|
#
|
||||||
|
# Zwei getrennte Verzeichnispaare (migrate-*/rescue-*) statt einem gemeinsamen: die
|
||||||
|
# Migrationsabzuege sind signiert und tragen ein eigenes manifest.json, die
|
||||||
|
# Rettungsabzuege sind die gewoehnlichen _ota.img mit ninlight-Trailer. Sie
|
||||||
|
# unterscheiden sich in Form und Herstellung; ein gemeinsames Verzeichnis wuerde beide
|
||||||
|
# Herstellungswege ununterscheidbar ineinander mischen (siehe auch: "migrate-wlan/ und
|
||||||
|
# migrate-nocreds/ werden bei der Rettung nie angefasst" im Auftrag).
|
||||||
|
RESCUE_SET_DIRS = {"ohne-wlan": "rescue-nocreds", "mit-wlan": "rescue-wlan"}
|
||||||
|
|
||||||
|
|
||||||
def read_image_set(path: Path | None) -> str:
|
def read_image_set(path: Path | None) -> str:
|
||||||
"""Der eingestellte Bildsatz, oder DEFAULT_IMAGE_SET, wenn die Datei fehlt, unlesbar
|
"""Der eingestellte Bildsatz, oder DEFAULT_IMAGE_SET, wenn die Datei fehlt, unlesbar
|
||||||
@@ -117,6 +132,13 @@ class Station:
|
|||||||
self.since = now_iso()
|
self.since = now_iso()
|
||||||
self.served = 0
|
self.served = 0
|
||||||
self.recent: list[dict] = []
|
self.recent: list[dict] = []
|
||||||
|
# Zeitpunkt der letzten Anfrage JEDER Art (erfolgreich oder nicht) - das
|
||||||
|
# Lebenszeichen fuer die Anzeige (display.py:_recent_request()). Anders als
|
||||||
|
# "served" zaehlt hier auch eine erfolglose Anfrage: der Eigentuemer wollte
|
||||||
|
# gerade nicht erst am Servierzaehler raten muessen, ob ueberhaupt etwas
|
||||||
|
# anklopft (Bedienerwunsch 2026-09-23). None, solange seit dem Start der
|
||||||
|
# Station noch keine einzige Anfrage einging.
|
||||||
|
self.last_activity: str | None = None
|
||||||
# Zuletzt erkanntes Modell im Migrationsmodus, auch wenn (noch) nichts ausgeliefert
|
# Zuletzt erkanntes Modell im Migrationsmodus, auch wenn (noch) nichts ausgeliefert
|
||||||
# wurde - das ist es, was die Anzeige gross darstellt (Spec 5.2).
|
# wurde - das ist es, was die Anzeige gross darstellt (Spec 5.2).
|
||||||
self.last_seen: dict | None = None
|
self.last_seen: dict | None = None
|
||||||
@@ -148,8 +170,36 @@ class Station:
|
|||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
self.write_state()
|
self.write_state()
|
||||||
|
|
||||||
|
def rescue_dir(self) -> Path:
|
||||||
|
"""Das Verzeichnis des EINGESTELLTEN Bildsatzes fuer die RETTUNG. Dieselbe Regel
|
||||||
|
wie migrate_dir() (dort steht die ausfuehrliche Begruendung): der Umschalter
|
||||||
|
entscheidet, nicht eine Abfrage je Rettung, und es wird nie stillschweigend auf
|
||||||
|
den jeweils anderen Satz ausgewichen.
|
||||||
|
|
||||||
|
Bestandskompatibilitaet bewusst NICHT gegeben: vor diesem Umschalter lagen
|
||||||
|
Rettungsabzuege FLACH in image_dir (immer MIT Zugangsdaten, aus build/). Ein
|
||||||
|
Pi mit diesem alten Bestand wuerde, faende image_path() dort noch etwas, bei der
|
||||||
|
Vorgabe "ohne-wlan" weiterhin klaglos ein Abbild MIT Zugangsdaten ausliefern -
|
||||||
|
also genau die alte, jetzt unerwuenschte Grundhaltung stillschweigend
|
||||||
|
fortsetzen, die dieser Umschalter gerade abstellen soll. Ein Pi mit altem
|
||||||
|
Bestand muss deshalb einmal per deploy.sh neu bestueckt werden (das raeumt den
|
||||||
|
flachen Altbestand auch auf); bis dahin liefert die Rettung in der Vorgabe
|
||||||
|
"ohne-wlan" sichtbar nichts aus (Status "notfound"), statt einen Fehlgriff zu
|
||||||
|
verstecken."""
|
||||||
|
return self.image_dir / RESCUE_SET_DIRS[self.image_set()]
|
||||||
|
|
||||||
|
def rescue_set_present(self) -> bool:
|
||||||
|
"""Liegt der eingestellte Rettungs-Bildsatz ueberhaupt auf dem Pi? Dieselbe Regel
|
||||||
|
wie migrate_set_present() (Verzeichnis vorhanden UND mindestens ein Image
|
||||||
|
darin) - Panel und CLI sollen fuer die Rettung nach demselben Massstab
|
||||||
|
urteilen wie fuer die Migration."""
|
||||||
|
try:
|
||||||
|
return any(p.name.endswith(".img") for p in self.rescue_dir().iterdir())
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
def image_path(self, target: str) -> Path:
|
def image_path(self, target: str) -> Path:
|
||||||
return self.image_dir / f"ninlight_{target}_ota.img"
|
return self.rescue_dir() / f"ninlight_{target}_ota.img"
|
||||||
|
|
||||||
def image_set(self) -> str:
|
def image_set(self) -> str:
|
||||||
"""Der gerade eingestellte Bildsatz ("ohne-wlan"/"mit-wlan")."""
|
"""Der gerade eingestellte Bildsatz ("ohne-wlan"/"mit-wlan")."""
|
||||||
@@ -326,7 +376,11 @@ class Station:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
if status == "ok":
|
if status == "ok":
|
||||||
self.served += 1
|
self.served += 1
|
||||||
self.recent.append({"ts": now_iso(), "ip": ip, "target": target,
|
ts = now_iso()
|
||||||
|
# Jede Anfrage zaehlt als Lebenszeichen, nicht nur eine erfolgreiche - siehe
|
||||||
|
# self.last_activity oben.
|
||||||
|
self.last_activity = ts
|
||||||
|
self.recent.append({"ts": ts, "ip": ip, "target": target,
|
||||||
"version": version, "bytes": nbytes, "status": status})
|
"version": version, "bytes": nbytes, "status": status})
|
||||||
del self.recent[:-RECENT_MAX]
|
del self.recent[:-RECENT_MAX]
|
||||||
self.write_state()
|
self.write_state()
|
||||||
@@ -347,6 +401,9 @@ class Station:
|
|||||||
"variant_source": variant_source,
|
"variant_source": variant_source,
|
||||||
"product_id": product_id, "lamp_version": lamp_version,
|
"product_id": product_id, "lamp_version": lamp_version,
|
||||||
"version": version, "bytes": nbytes, "status": status}
|
"version": version, "bytes": nbytes, "status": status}
|
||||||
|
# Jede Anfrage zaehlt als Lebenszeichen, auch "listen"/"unknown"/"notfound"/
|
||||||
|
# "noimageset" - siehe self.last_activity oben und record()'s Gegenstueck.
|
||||||
|
self.last_activity = entry["ts"]
|
||||||
self.recent.append(entry)
|
self.recent.append(entry)
|
||||||
del self.recent[:-RECENT_MAX]
|
del self.recent[:-RECENT_MAX]
|
||||||
if target is not None:
|
if target is not None:
|
||||||
@@ -359,7 +416,11 @@ class Station:
|
|||||||
def write_state(self) -> None:
|
def write_state(self) -> None:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
payload = {"since": self.since, "mode": self.mode, "served": self.served,
|
payload = {"since": self.since, "mode": self.mode, "served": self.served,
|
||||||
"recent": list(self.recent)}
|
"recent": list(self.recent),
|
||||||
|
# In BEIDEN Modi (nicht nur migration, siehe unten) - das
|
||||||
|
# Lebenszeichen auf dem Panel gilt fuer die Rettung genauso wie
|
||||||
|
# fuer die Migration (display.py:_recent_request()).
|
||||||
|
"last_request_ts": self.last_activity}
|
||||||
if self.mode == "migration":
|
if self.mode == "migration":
|
||||||
phase, armed_target = self.read_control()
|
phase, armed_target = self.read_control()
|
||||||
payload["phase"] = phase
|
payload["phase"] = phase
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ class Station:
|
|||||||
def __init__(self, tmp_path, *extra):
|
def __init__(self, tmp_path, *extra):
|
||||||
self.image_dir = tmp_path / "images"
|
self.image_dir = tmp_path / "images"
|
||||||
self.image_dir.mkdir()
|
self.image_dir.mkdir()
|
||||||
|
# image_path() folgt jetzt dem Bildsatz-Umschalter, genauso wie migrate_dir() es
|
||||||
|
# tut - ohne --imageset-file gilt die Vorgabe "ohne-wlan" (server.py,
|
||||||
|
# DEFAULT_IMAGE_SET), also landen Rettungsabbilder in image_dir/rescue-nocreds.
|
||||||
|
# Die meisten Tests hier pruefen nur die Ausliefermechanik, nicht den
|
||||||
|
# Bildsatz-Umschalter selbst - die Bildsatz-spezifischen Tests weiter unten legen
|
||||||
|
# ihre Abbilder gezielt in andere Verzeichnisse.
|
||||||
|
self.rescue_dir = self.image_dir / "rescue-nocreds"
|
||||||
|
self.rescue_dir.mkdir()
|
||||||
self.state = tmp_path / "status.json"
|
self.state = tmp_path / "status.json"
|
||||||
self.port = free_port()
|
self.port = free_port()
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
@@ -140,7 +148,7 @@ def station(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_serves_a_known_image(station):
|
def test_serves_a_known_image(station):
|
||||||
data = make_image(station.image_dir / "ninlight_evk_ota.img", "evk", "1.2.3", b"payload" * 500)
|
data = make_image(station.rescue_dir / "ninlight_evk_ota.img", "evk", "1.2.3", b"payload" * 500)
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", "/ninlight_evk_ota.img")
|
c.request("GET", "/ninlight_evk_ota.img")
|
||||||
r = c.getresponse()
|
r = c.getresponse()
|
||||||
@@ -157,7 +165,7 @@ def test_missing_image_is_404(station):
|
|||||||
|
|
||||||
|
|
||||||
def test_unknown_target_is_404(station):
|
def test_unknown_target_is_404(station):
|
||||||
make_image(station.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"x" * 100)
|
make_image(station.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"x" * 100)
|
||||||
for path in ("/ninlight_nosuch_ota.img", "/ninlight_evk_ota.img.bak", "/", "/images/"):
|
for path in ("/ninlight_nosuch_ota.img", "/ninlight_evk_ota.img.bak", "/", "/images/"):
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", path)
|
c.request("GET", path)
|
||||||
@@ -216,7 +224,7 @@ def test_log_error_is_not_silenced_by_log_message(capsys):
|
|||||||
|
|
||||||
|
|
||||||
def test_range_request_returns_206_and_the_right_slice(station):
|
def test_range_request_returns_206_and_the_right_slice(station):
|
||||||
data = make_image(station.image_dir / "ninlight_e27_rgb_ota.img", "e27_rgb", "2.0.0", b"abc" * 2000)
|
data = make_image(station.rescue_dir / "ninlight_e27_rgb_ota.img", "e27_rgb", "2.0.0", b"abc" * 2000)
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", "/ninlight_e27_rgb_ota.img", headers={"Range": "bytes=1000-"})
|
c.request("GET", "/ninlight_e27_rgb_ota.img", headers={"Range": "bytes=1000-"})
|
||||||
r = c.getresponse()
|
r = c.getresponse()
|
||||||
@@ -227,7 +235,7 @@ def test_range_request_returns_206_and_the_right_slice(station):
|
|||||||
|
|
||||||
|
|
||||||
def test_state_file_records_a_delivery(station):
|
def test_state_file_records_a_delivery(station):
|
||||||
make_image(station.image_dir / "ninlight_evk_ota.img", "evk", "0.3.4", b"z" * 1000)
|
make_image(station.rescue_dir / "ninlight_evk_ota.img", "evk", "0.3.4", b"z" * 1000)
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", "/ninlight_evk_ota.img")
|
c.request("GET", "/ninlight_evk_ota.img")
|
||||||
c.getresponse().read()
|
c.getresponse().read()
|
||||||
@@ -241,6 +249,21 @@ def test_state_file_records_a_delivery(station):
|
|||||||
assert entry["bytes"] == 1000 + TRAILER_SIZE
|
assert entry["bytes"] == 1000 + TRAILER_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_file_carries_a_last_request_timestamp_for_the_display(station):
|
||||||
|
"""display.py zeigt einen kleinen Lebenszeichen-Punkt, sobald eine Anfrage jeder Art
|
||||||
|
hereinkommt (auch ein 404) - dafuer muss status.json den Zeitpunkt tragen, nicht nur
|
||||||
|
den Erfolgszaehler. Vor der ersten Anfrage ist das Feld da, aber None."""
|
||||||
|
initial = station.read_state()
|
||||||
|
assert "last_request_ts" in initial
|
||||||
|
assert initial["last_request_ts"] is None
|
||||||
|
|
||||||
|
c = station.conn()
|
||||||
|
c.request("GET", "/ninlight_g95_cw_ota.img") # existiert nicht - zaehlt trotzdem
|
||||||
|
c.getresponse().read()
|
||||||
|
state = station.read_state(lambda s: s["last_request_ts"] is not None)
|
||||||
|
assert isinstance(state["last_request_ts"], str) and state["last_request_ts"]
|
||||||
|
|
||||||
|
|
||||||
def test_state_file_records_a_missing_image(station):
|
def test_state_file_records_a_missing_image(station):
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", "/ninlight_g95_cw_ota.img")
|
c.request("GET", "/ninlight_g95_cw_ota.img")
|
||||||
@@ -266,7 +289,7 @@ def test_state_file_is_world_readable_even_under_a_restrictive_umask(tmp_path):
|
|||||||
finally:
|
finally:
|
||||||
os.umask(old_umask)
|
os.umask(old_umask)
|
||||||
try:
|
try:
|
||||||
make_image(s.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"w" * 100)
|
make_image(s.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"w" * 100)
|
||||||
|
|
||||||
def world_readable() -> bool:
|
def world_readable() -> bool:
|
||||||
mode = stat.S_IMODE(s.state.stat().st_mode)
|
mode = stat.S_IMODE(s.state.stat().st_mode)
|
||||||
@@ -289,7 +312,7 @@ def test_state_file_is_world_readable_even_under_a_restrictive_umask(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_recent_is_capped_at_eight(station):
|
def test_recent_is_capped_at_eight(station):
|
||||||
make_image(station.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"q" * 10)
|
make_image(station.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"q" * 10)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
c = station.conn()
|
c = station.conn()
|
||||||
c.request("GET", "/ninlight_evk_ota.img")
|
c.request("GET", "/ninlight_evk_ota.img")
|
||||||
@@ -302,7 +325,7 @@ def test_recent_is_capped_at_eight(station):
|
|||||||
def test_aborted_transfer_is_recorded(tmp_path):
|
def test_aborted_transfer_is_recorded(tmp_path):
|
||||||
s = Station(tmp_path, "--stall-after", "256")
|
s = Station(tmp_path, "--stall-after", "256")
|
||||||
try:
|
try:
|
||||||
make_image(s.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 100000)
|
make_image(s.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 100000)
|
||||||
sock = socket.create_connection(("127.0.0.1", s.port), timeout=5)
|
sock = socket.create_connection(("127.0.0.1", s.port), timeout=5)
|
||||||
sock.sendall(b"GET /ninlight_evk_ota.img HTTP/1.1\r\nHost: x\r\n\r\n")
|
sock.sendall(b"GET /ninlight_evk_ota.img HTTP/1.1\r\nHost: x\r\n\r\n")
|
||||||
sock.recv(4096)
|
sock.recv(4096)
|
||||||
@@ -326,7 +349,7 @@ def test_aborted_transfer_is_recorded(tmp_path):
|
|||||||
def test_stall_after_stops_sending_but_keeps_the_connection(tmp_path):
|
def test_stall_after_stops_sending_but_keeps_the_connection(tmp_path):
|
||||||
s = Station(tmp_path, "--stall-after", "512")
|
s = Station(tmp_path, "--stall-after", "512")
|
||||||
try:
|
try:
|
||||||
make_image(s.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 200000)
|
make_image(s.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 200000)
|
||||||
sock = socket.create_connection(("127.0.0.1", s.port), timeout=5)
|
sock = socket.create_connection(("127.0.0.1", s.port), timeout=5)
|
||||||
sock.sendall(b"GET /ninlight_evk_ota.img HTTP/1.1\r\nHost: x\r\n\r\n")
|
sock.sendall(b"GET /ninlight_evk_ota.img HTTP/1.1\r\nHost: x\r\n\r\n")
|
||||||
got = b""
|
got = b""
|
||||||
@@ -369,7 +392,7 @@ def test_stall_after_budget_survives_a_resumed_request(tmp_path):
|
|||||||
"""
|
"""
|
||||||
s = Station(tmp_path, "--stall-after", "1000")
|
s = Station(tmp_path, "--stall-after", "1000")
|
||||||
try:
|
try:
|
||||||
make_image(s.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 200000)
|
make_image(s.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"y" * 200000)
|
||||||
|
|
||||||
# First request: burns through the whole --stall-after budget, then the server
|
# First request: burns through the whole --stall-after budget, then the server
|
||||||
# holds the connection open and sends nothing further.
|
# holds the connection open and sends nothing further.
|
||||||
@@ -419,7 +442,7 @@ def test_concurrent_downloads_are_all_recorded(station):
|
|||||||
concurrent requests can lose increments (two threads read the same old value) or have
|
concurrent requests can lose increments (two threads read the same old value) or have
|
||||||
one thread's status.json write clobber another's.
|
one thread's status.json write clobber another's.
|
||||||
"""
|
"""
|
||||||
make_image(station.image_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"c" * 5000)
|
make_image(station.rescue_dir / "ninlight_evk_ota.img", "evk", "1.0.0", b"c" * 5000)
|
||||||
expected_len = 5000 + TRAILER_SIZE
|
expected_len = 5000 + TRAILER_SIZE
|
||||||
n_workers = 20
|
n_workers = 20
|
||||||
errors = []
|
errors = []
|
||||||
@@ -1092,6 +1115,75 @@ def test_the_version_comes_from_the_manifest_of_the_selected_set(migration_serve
|
|||||||
)["recent"][-1]["version"] == "0.4.0-wlan"
|
)["recent"][-1]["version"] == "0.4.0-wlan"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Derselbe Umschalter gilt jetzt auch fuer die RETTUNG ---------------------------
|
||||||
|
#
|
||||||
|
# Der Eigentuemer hat das ausdruecklich verlangt: bisher bekam eine Rettung IMMER ein
|
||||||
|
# Abbild MIT Zugangsdaten (die flachen Abzuege in images/ kamen aus build/). image_path()
|
||||||
|
# folgt jetzt demselben Umschalter wie migrate_dir() - dieselben Tests wie oben fuer die
|
||||||
|
# Migration, nur gegen den Rettungspfad "/ninlight_<target>_ota.img".
|
||||||
|
|
||||||
|
def _both_rescue_sets(imgdir):
|
||||||
|
(imgdir / "rescue-nocreds").mkdir(exist_ok=True)
|
||||||
|
make_image(imgdir / "rescue-nocreds" / "ninlight_g95_cw_ota.img", "g95_cw", "1.0.0",
|
||||||
|
b"OHNE" * 100)
|
||||||
|
(imgdir / "rescue-wlan").mkdir(exist_ok=True)
|
||||||
|
make_image(imgdir / "rescue-wlan" / "ninlight_g95_cw_ota.img", "g95_cw", "1.0.0",
|
||||||
|
b"MITW" * 100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rescue_default_image_set_is_the_one_without_wifi_credentials(server):
|
||||||
|
"""Wie bei der Migration: ohne Umschalterdatei gilt die sichere Vorgabe - ein
|
||||||
|
Fehlgriff darf niemandem das eigene WLAN-Passwort in ein fremdes Geraet schreiben.
|
||||||
|
Vor diesem Umschalter bekam eine Rettung IMMER das Abbild MIT Zugangsdaten; das war
|
||||||
|
genau die Nebenwirkung, die der Eigentuemer sichtbar gemacht haben wollte."""
|
||||||
|
srv, imgdir = server
|
||||||
|
_both_rescue_sets(imgdir)
|
||||||
|
r = srv.get("/ninlight_g95_cw_ota.img")
|
||||||
|
assert r.status == 200
|
||||||
|
assert r.body.startswith(b"OHNE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_rescue_switch_decides_which_file_is_actually_delivered(server):
|
||||||
|
srv, imgdir = server
|
||||||
|
_both_rescue_sets(imgdir)
|
||||||
|
srv.set_image_set("mit-wlan")
|
||||||
|
assert srv.get("/ninlight_g95_cw_ota.img").body.startswith(b"MITW")
|
||||||
|
# ... und wieder zurueck, ohne Dienstneustart.
|
||||||
|
srv.set_image_set("ohne-wlan")
|
||||||
|
assert srv.get("/ninlight_g95_cw_ota.img").body.startswith(b"OHNE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_rescue_missing_selected_set_delivers_nothing(server):
|
||||||
|
"""Der gewaehlte Rettungs-Bildsatz liegt nicht auf dem Pi: NICHTS geht raus -
|
||||||
|
insbesondere nicht das gleichnamige Abbild des anderen, danebenliegenden Satzes.
|
||||||
|
Auf den anderen Satz auszuweichen waere genau der Fehlgriff, gegen den der
|
||||||
|
Umschalter da ist (siehe rescue_dir() in server.py)."""
|
||||||
|
srv, imgdir = server
|
||||||
|
(imgdir / "rescue-nocreds").mkdir(exist_ok=True)
|
||||||
|
make_image(imgdir / "rescue-nocreds" / "ninlight_g95_cw_ota.img", "g95_cw", "1.0.0",
|
||||||
|
b"OHNE" * 100)
|
||||||
|
srv.set_image_set("mit-wlan") # rescue-wlan/ existiert nicht
|
||||||
|
|
||||||
|
r = srv.get("/ninlight_g95_cw_ota.img")
|
||||||
|
assert r.status == 404
|
||||||
|
assert b"OHNE" not in r.body
|
||||||
|
st = srv.status(lambda s: len(s["recent"]) >= 1)
|
||||||
|
assert st["served"] == 0
|
||||||
|
assert st["recent"][-1]["status"] == "notfound"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rescue_old_flat_layout_is_no_longer_served(server):
|
||||||
|
"""Rueckwaertskompatibilitaet bewusst NICHT gegeben (server.py, rescue_dir()): ein Pi
|
||||||
|
mit dem alten, flachen Bestand (vor diesem Umschalter, immer MIT Zugangsdaten aus
|
||||||
|
build/) muss einmal per deploy.sh neu bestueckt werden. Wuerde image_path() den
|
||||||
|
flachen Altbestand noch lesen, wuerde die Vorgabe "ohne-wlan" NICHTS daran aendern -
|
||||||
|
genau die alte, jetzt unerwuenschte Grundhaltung stillschweigend fortgesetzt."""
|
||||||
|
srv, imgdir = server
|
||||||
|
make_image(imgdir / "ninlight_g95_cw_ota.img", "g95_cw", "1.0.0", b"ALTBESTAND" * 50)
|
||||||
|
r = srv.get("/ninlight_g95_cw_ota.img")
|
||||||
|
assert r.status == 404
|
||||||
|
|
||||||
|
|
||||||
def test_read_image_set_defaults_without_a_file(tmp_path):
|
def test_read_image_set_defaults_without_a_file(tmp_path):
|
||||||
module = _load_server_module()
|
module = _load_server_module()
|
||||||
assert module.read_image_set(None) == "ohne-wlan"
|
assert module.read_image_set(None) == "ohne-wlan"
|
||||||
|
|||||||
Reference in New Issue
Block a user