Audit backup recovery coverage and preserve separator container isolation

This commit is contained in:
Mikei386 committed 2026-10-01 17:10:17 +02:00
1 parent bd5aea7656
commit 356abbeb9d
4 files changed
+118 -2

No files matched your search

+47
View File
@@ -73,6 +73,43 @@ class RestoreTests(unittest.TestCase):
with patch.object(self.server.catalog,'start',side_effect=download):
summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password');self.server.backup.start(dict(id=summary['id'],services=[],confirm=True,restore_credentials=True));job=self.wait()
self.assertEqual(job['state'],'complete',job);self.assertEqual(self.server.profiles.rows[0]['model_id'],ident)
def test_all_separator_packages_have_pinned_sources_and_restore(self):
from separator_runtime import SOURCES
doc=self.server.backup.snapshot(include_history=False)
doc['runtimes']['separator']={'installed':True,'models':list(SOURCES),'sources':copy.deepcopy(SOURCES)}
validate(doc)
runtime=Mock();runtime.job=None;runtime.status.return_value={'installed':True,'version':'0.47.0','job':{'state':'complete'},'models':[]};runtime.model_ready.return_value=False
self.server.separator_runtime=runtime
summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password')
self.server.backup.start(dict(id=summary['id'],services=[],confirm=True,restore_credentials=False));job=self.wait()
self.assertEqual(job['state'],'complete',job)
self.assertEqual([c.args[0] for c in runtime.download.call_args_list],list(SOURCES))
bad=copy.deepcopy(doc);name=next(iter(SOURCES));bad['runtimes']['separator']['sources'][name]['recipe_version']=999
with self.assertRaises(ValueError):validate(bad)
def test_local_source_restore_preserves_profile_reference(self):
ident=self.model();self.profile(ident);doc=self.server.backup.snapshot(include_history=False)
model=doc['models'][0];data=(self.root/'models'/ident/'model.gguf').read_bytes()
model.update(repo='local/athena-router',revision=model['sha256'],source='local',download_source={'repo':'test/original','revision':'b'*40,'file':'original.gguf'})
import shutil
shutil.rmtree(self.root/'models'/ident)
source_id=hashlib.sha256(('test/original'+'b'*40+'original.gguf').encode()).hexdigest()
def download(repo,filename,revision,kind):
self.assertEqual((repo,filename,revision),('test/original','original.gguf','b'*40))
p=self.root/'models'/source_id;p.mkdir();(p/'model.gguf').write_bytes(data)
entry=dict(repo=repo,revision=revision,file=filename,size=len(data),sha256=model['sha256'],kind=kind,state='downloaded');(p/'entry.json').write_text(json.dumps(entry));self.server.catalog.job={'state':'complete'}
with patch.object(self.server.catalog,'start',side_effect=download):
summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password');self.assertFalse(summary['blockers'])
self.server.backup.start(dict(id=summary['id'],services=[],confirm=True,restore_credentials=False));job=self.wait()
self.assertEqual(job['state'],'complete',job)
self.assertEqual(self.server.profiles.rows[0]['model_id'],ident)
self.assertEqual(json.loads((self.root/'models'/ident/'entry.json').read_text())['download_source'],model['download_source'])
def test_buildable_local_container_does_not_warn_about_missing_source(self):
doc=self.server.backup.snapshot(include_history=False);c=ContainerTests().config()
c.update(name='athena-ladypoly',image='athena-ladypoly:3960afc-deck1',registry=None,network='host',build_recipe='ladypoly')
doc['services']=[c]
summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password')
self.assertTrue(summary['services'][0]['restorable'])
self.assertFalse(any('Buildquelle fehlt' in w for w in summary['warnings']))
def test_rejects_unsafe_paths_and_bad_references_before_changes(self):
ident=self.model();self.profile(ident);doc=self.server.backup.snapshot(include_history=False)
for change in (lambda d:d['models'][0].update(file='../secret'),lambda d:d['settings']['profiles.json'][0].update(model_id='f'*64),lambda d:d.update(version=99)):
@@ -100,6 +137,16 @@ class ContainerTests(unittest.TestCase):
for change in (lambda x:x.update(name='other-ui'),lambda x:x.update(image='untrusted:1'),lambda x:x['mounts'][0].update(target='/other')):
bad=copy.deepcopy(c);change(bad)
with self.assertRaises(ValueError):validate_container(bad)
def test_separator_restore_keeps_readonly_root_and_writable_tmpfs(self):
c=self.config();c.update(name='athena-separator-web',image='athena-separator-web:0.1.0',registry=None,network='host',ports={},build_recipe='separator-web',running=False)
with tempfile.TemporaryDirectory() as d:
root=Path(d);policy=root/'backup-policy.json';policy.write_text(json.dumps({'deck_state':d}))
calls=[]
with patch('deploy.docker_backup.subprocess.run',return_value=Mock(returncode=1)),patch('deploy.docker_backup.command',side_effect=lambda args,*extra,**kw:calls.append(args) or ''),patch('deploy.docker_backup.Path.stat',return_value=Mock(st_uid=0,st_mode=0o100600)):
result=restore_container(Mock(state=root),c)
self.assertEqual(result['state'],'complete')
create=next(args for args in calls if args[0]=='create')
self.assertIn('--read-only',create);self.assertIn('/tmp:rw,noexec,nosuid,size=1g,uid=65534,gid=65534',create)
def test_never_replaces_existing_foreign_container(self):
with tempfile.TemporaryDirectory() as d:
manager=Mock(state=Path(d));(Path(d)/'backup-policy.json').write_text(json.dumps({'deck_state':d}))