Recover aborted maintenance without fabricated drain or foreign fence changes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Legacy IndeeHub maintenance controller
|
# Legacy IndeeHub maintenance controller
|
||||||
|
|
||||||
Status: isolated source implementation. Ten pure Python fake-runtime regressions
|
Status: isolated source implementation. Twelve pure Python fake-runtime regressions
|
||||||
pass; no live invocation or production qualification. The controller is not part
|
pass; no live invocation or production qualification. The controller is not part
|
||||||
of the already signed private app candidate and needs no new app image/API.
|
of the already signed private app candidate and needs no new app image/API.
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ prove compatibility with newly changed data. No automatic DB/media restore exist
|
|||||||
|
|
||||||
## Qualification and remaining integration
|
## Qualification and remaining integration
|
||||||
|
|
||||||
`python3 tests/regression/test_indeehub_maintenance_controller.py` passes ten
|
`python3 tests/regression/test_indeehub_maintenance_controller.py` passes twelve
|
||||||
fake-runtime cases in temporary directories, without services/network/containers.
|
fake-runtime cases in temporary directories, without services/network/containers.
|
||||||
Source nginx template guard coverage also passes its parser check. Production
|
Source nginx template guard coverage also passes its parser check. Production
|
||||||
adapter compilation, actual Podman event format/systemd clean-exit behavior,
|
adapter compilation, actual Podman event format/systemd clean-exit behavior,
|
||||||
@@ -71,3 +71,8 @@ The backend must refuse missing/mismatched prerequisites before snapshots/stops.
|
|||||||
Native AppGate + nginx guards are separate node source changes owned by the
|
Native AppGate + nginx guards are separate node source changes owned by the
|
||||||
supervised updater agent. The signed app catalog/private image receipts remain
|
supervised updater agent. The signed app catalog/private image receipts remain
|
||||||
unchanged. Existing live stop/uninstall intent must not be rewritten as maintenance.
|
unchanged. Existing live stop/uninstall intent must not be rewritten as maintenance.
|
||||||
|
|
||||||
|
A pre-acquire snapshot/preflight failure may leave no controller journal. An
|
||||||
|
Aborted node journal with target_startup_began=false then permits idempotent
|
||||||
|
no-op acknowledgement, without touching any other operation’s admission fence.
|
||||||
|
A matching fence without its controller journal requires recovery investigation.
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ class Controller:
|
|||||||
return {'operation_id':self.operation,'state':'held'}
|
return {'operation_id':self.operation,'state':'held'}
|
||||||
def release(self, outcome):
|
def release(self, outcome):
|
||||||
require(outcome in ('committed','restored','aborted'),'Invalid release outcome')
|
require(outcome in ('committed','restored','aborted'),'Invalid release outcome')
|
||||||
|
if self.record is None and outcome=='aborted':
|
||||||
|
runtime=json.loads((self.data/'update-transactions'/'supervised'/(self.operation+'.json')).read_text())
|
||||||
|
require(runtime.get('phase')=='Aborted' and runtime.get('target_startup_began') is False,'Untouched abort evidence required')
|
||||||
|
if self.fence.exists():
|
||||||
|
require(not self.fence.is_symlink() and self.fence.read_text()!=self.operation,'Matching fence without journal requires recovery')
|
||||||
|
return {'operation_id':self.operation,'state':'released'}
|
||||||
require(self.record is not None,'Unknown maintenance operation')
|
require(self.record is not None,'Unknown maintenance operation')
|
||||||
if self.record['phase']=='Released':
|
if self.record['phase']=='Released':
|
||||||
require(self.record.get('outcome')==outcome,'Maintenance outcome changed')
|
require(self.record.get('outcome')==outcome,'Maintenance outcome changed')
|
||||||
|
|||||||
@@ -82,4 +82,17 @@ class MaintenanceTests(unittest.TestCase):
|
|||||||
self.assertEqual(module.validate_nginx_guards('\n'.join(blocks)),3)
|
self.assertEqual(module.validate_nginx_guards('\n'.join(blocks)),3)
|
||||||
with self.assertRaisesRegex(RuntimeError,'missing its maintenance guard'):module.validate_nginx_guards('\n'.join(blocks).replace(guard,'',1))
|
with self.assertRaisesRegex(RuntimeError,'missing its maintenance guard'):module.validate_nginx_guards('\n'.join(blocks).replace(guard,'',1))
|
||||||
with self.assertRaisesRegex(RuntimeError,'Unrecognized direct'):module.validate_nginx_guards('\n'.join(blocks)+'\nlocation /other/ {\n proxy_pass http://127.0.0.1:7778/;\n}')
|
with self.assertRaisesRegex(RuntimeError,'Unrecognized direct'):module.validate_nginx_guards('\n'.join(blocks)+'\nlocation /other/ {\n proxy_pass http://127.0.0.1:7778/;\n}')
|
||||||
|
def test_pre_acquire_abort_acknowledges_without_mutating_foreign_fence(self):
|
||||||
|
c=self.controller;runtime=c.data/'update-transactions'/'supervised'/(self.operation+'.json')
|
||||||
|
module.atomic(runtime,{'phase':'Aborted','target_startup_began':False})
|
||||||
|
self.assertEqual(c.release('aborted')['state'],'released')
|
||||||
|
c.fence.parent.mkdir(parents=True);foreign=str(uuid.uuid4());c.fence.write_text(foreign)
|
||||||
|
self.assertEqual(c.release('aborted')['state'],'released');self.assertEqual(c.fence.read_text(),foreign)
|
||||||
|
module.atomic(runtime,{'phase':'Aborted','target_startup_began':True})
|
||||||
|
with self.assertRaisesRegex(RuntimeError,'Untouched abort'):c.release('aborted')
|
||||||
|
def test_matching_fence_without_journal_is_not_an_untouched_abort(self):
|
||||||
|
c=self.controller;module.atomic(c.data/'update-transactions'/'supervised'/(self.operation+'.json'),{'phase':'Aborted','target_startup_began':False})
|
||||||
|
c.fence.parent.mkdir(parents=True);c.fence.write_text(self.operation)
|
||||||
|
with self.assertRaisesRegex(RuntimeError,'without journal'):c.release('aborted')
|
||||||
|
self.assertTrue(c.fence.exists())
|
||||||
if __name__=='__main__':unittest.main()
|
if __name__=='__main__':unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user