Multiple printers, printer/slot in Spoolman, kept in sync
- One worker per printer (name, IP, CC2 Control URL, own pinned host key); a spool sits in at most one slot of one printer, moves between printers - Spoolman: location "<printer> Slot N" plus extra fields "drucker"/"slot" (created by the bridge), cleared and "Lager" on removal - Full comparison every minute: the printer wins over manual edits of loaded spools; stale printer locations go to storage; own locations untouched - Automatic migration of v1 settings, state and host key - Status hint when a tag is still in a tray but unmapped (e.g. after "Take out" in CC2 Control) - SSH test: connection drops during exec give a clean error, not a 500 - Mock environment with several printers; tests for all of the above Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
1 parent
bf32667f77
commit
a9041eb538
15 files changed
+1400
-684
No files matched your search
+25
-9
@@ -18,6 +18,8 @@ RECORD = record(10001, "", 1000, 10008)
|
||||
|
||||
|
||||
class Server(paramiko.ServerInterface):
|
||||
reject_exec = False
|
||||
|
||||
def __init__(self, allowed: paramiko.PKey):
|
||||
self.allowed = allowed
|
||||
self.commands = []
|
||||
@@ -34,6 +36,8 @@ class Server(paramiko.ServerInterface):
|
||||
|
||||
def check_channel_exec_request(self, channel, command):
|
||||
self.commands.append(command)
|
||||
if Server.reject_exec:
|
||||
return False
|
||||
|
||||
def reply(): # forced command: always the canvas record
|
||||
channel.sendall(json.dumps(RECORD).encode())
|
||||
@@ -102,13 +106,13 @@ def test_rejects_garbage_key(tmp_path):
|
||||
def test_first_contact_requires_confirmation(tmp_path, printer):
|
||||
keys = KeyStore(tmp_path)
|
||||
keys.save_key(printer["client_key_text"])
|
||||
reader = CanvasRecordReader(keys, "127.0.0.1", printer["port"])
|
||||
reader = CanvasRecordReader(keys, "p1", "127.0.0.1", printer["port"])
|
||||
with pytest.raises(HostKeyUnknown):
|
||||
reader.read()
|
||||
result = reader.test()
|
||||
assert result["host_key_pinned"] is False
|
||||
assert result["record"]["channels0"]["weight"] == "10001"
|
||||
keys.pin_host_key("127.0.0.1", printer["host_key"])
|
||||
keys.pin_host_key("p1", "127.0.0.1", printer["host_key"])
|
||||
rec = reader.read()
|
||||
assert rec["channels3"]["weight"] == "10008"
|
||||
# transport is reused between reads
|
||||
@@ -121,8 +125,8 @@ def test_changed_host_key_is_rejected(tmp_path, printer):
|
||||
keys = KeyStore(tmp_path)
|
||||
keys.save_key(printer["client_key_text"])
|
||||
other = paramiko.Ed25519Key.from_private_key(io.StringIO(_new_key_text()))
|
||||
keys.pin_host_key("127.0.0.1", other)
|
||||
reader = CanvasRecordReader(keys, "127.0.0.1", printer["port"])
|
||||
keys.pin_host_key("p1", "127.0.0.1", other)
|
||||
reader = CanvasRecordReader(keys, "p1", "127.0.0.1", printer["port"])
|
||||
with pytest.raises(HostKeyMismatch):
|
||||
reader.read()
|
||||
with pytest.raises(HostKeyMismatch):
|
||||
@@ -132,16 +136,16 @@ def test_changed_host_key_is_rejected(tmp_path, printer):
|
||||
def test_wrong_client_key_fails_cleanly(tmp_path, printer):
|
||||
keys = KeyStore(tmp_path)
|
||||
keys.save_key(_new_key_text())
|
||||
keys.pin_host_key("127.0.0.1", printer["host_key"])
|
||||
keys.pin_host_key("p1", "127.0.0.1", printer["host_key"])
|
||||
with pytest.raises(SSHError, match="abgelehnt|fehlgeschlagen"):
|
||||
CanvasRecordReader(keys, "127.0.0.1", printer["port"]).read()
|
||||
CanvasRecordReader(keys, "p1", "127.0.0.1", printer["port"]).read()
|
||||
|
||||
|
||||
def test_reconnects_after_drop(tmp_path, printer):
|
||||
keys = KeyStore(tmp_path)
|
||||
keys.save_key(printer["client_key_text"])
|
||||
keys.pin_host_key("127.0.0.1", printer["host_key"])
|
||||
reader = CanvasRecordReader(keys, "127.0.0.1", printer["port"])
|
||||
keys.pin_host_key("p1", "127.0.0.1", printer["host_key"])
|
||||
reader = CanvasRecordReader(keys, "p1", "127.0.0.1", printer["port"])
|
||||
reader.read()
|
||||
for t, _ in printer["servers"]:
|
||||
t.close()
|
||||
@@ -156,4 +160,16 @@ def test_unreachable_printer(tmp_path, printer):
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
with pytest.raises(SSHError, match="nicht erreichbar"):
|
||||
CanvasRecordReader(keys, "127.0.0.1", port, timeout=2).read()
|
||||
CanvasRecordReader(keys, "p1", "127.0.0.1", port, timeout=2).read()
|
||||
|
||||
|
||||
def test_exec_failure_is_a_clean_error(tmp_path, printer, monkeypatch):
|
||||
monkeypatch.setattr(Server, "reject_exec", True)
|
||||
keys = KeyStore(tmp_path)
|
||||
keys.save_key(printer["client_key_text"])
|
||||
reader = CanvasRecordReader(keys, "p1", "127.0.0.1", printer["port"])
|
||||
with pytest.raises(SSHError, match="Lesefehler"):
|
||||
reader.test()
|
||||
keys.pin_host_key("p1", "127.0.0.1", printer["host_key"])
|
||||
with pytest.raises(SSHError, match="Lesefehler"):
|
||||
reader.read()
|
||||
Reference in new issue
Block a user