fix(oci): preserve private access to stack frontends across lifecycle operations

This commit is contained in:
Jonatan
2026-10-28 11:26:59 +01:00
parent a7786a6874
commit 05790b494e
16 changed files with 892 additions and 10 deletions
@@ -42,6 +42,7 @@ class JobInstallIntegrationTests(unittest.TestCase):
staged=Path(command[1]).parent/'oci_compose_jobs.py'
self.assertEqual(staged.read_bytes(),(ROOT/'remote/oci_compose_jobs.py').read_bytes())
self.assertEqual((staged.parent/'oci_carried_record.py').read_bytes(),(ROOT/'remote/oci_carried_record.py').read_bytes())
self.assertEqual((staged.parent/'oci_stack_public_routes.py').read_bytes(),(ROOT/'remote/oci_stack_public_routes.py').read_bytes())
return None
with patch.object(installer,'_stream_process',side_effect=inspect):
installer._run_remote_install(ROOT,template,{'deployment_kind':'generic-multi-lxc-stack'},'local',True)
@@ -54,6 +55,7 @@ class JobInstallIntegrationTests(unittest.TestCase):
with tarfile.open(fileobj=io.BytesIO(data),mode='r:gz') as archive:
self.assertEqual(archive.extractfile('oci_compose_jobs.py').read(),(ROOT/'remote/oci_compose_jobs.py').read_bytes())
self.assertEqual(archive.extractfile('oci_carried_record.py').read(),(ROOT/'remote/oci_carried_record.py').read_bytes())
self.assertEqual(archive.extractfile('oci_stack_public_routes.py').read(),(ROOT/'remote/oci_stack_public_routes.py').read_bytes())
return None
with patch.object(installer,'_stream_process',side_effect=inspect):
installer._run_remote_install(ROOT,template,{'deployment_kind':'generic-multi-lxc-stack'},'test-node',True)
@@ -0,0 +1,68 @@
"""Persist route contracts before startup without losing other LXC includes."""
import copy
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
import uuid
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'remote'))
import install_generic_stack as runtime
import oci_stack_public_routes as routes
class PublicRouteInstallTests(unittest.TestCase):
def test_private_members_route_to_all_static_frontends_with_real_identities(self):
services = [
{'name':'web','vmid':4810,'main':True,'ip':'10.77.0.30','frontend_ipv4':'10.0.1.246/24','deployment':{}},
{'name':'mail','vmid':4811,'main':False,'frontend':True,'ip':'10.77.0.31','frontend_ipv4':'10.0.1.245/24','deployment':{}},
{'name':'worker','vmid':4812,'main':False,'ip':'10.77.0.32','deployment':{}},
]
identities = {service['vmid']: {'installation_id':str(uuid.uuid4())} for service in services}
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / '4812.conf'
config.write_text('hostname: worker\nlxc.include: /existing/sysctl.conf\n')
def path(value):
return config if str(value) == '/etc/pve/lxc/4812.conf' else Path(value)
def install(deployment, vmid, identity):
routes.validate(deployment,vmid,identity)
return f'lxc.include: {routes.include_path(vmid)}'
with patch.object(runtime,'Path',side_effect=path), patch.object(runtime.oci_instances,'read',side_effect=lambda root,vmid:identities[vmid]), patch.object(routes,'install',side_effect=install), patch.object(routes,'check',wraps=lambda text,plan,vmid,identity=None,**kw:routes.validate(plan,vmid,identity)):
runtime.install_public_routes(services,4810,'vmbr10')
self.assertIn('lxc.include: /existing/sysctl.conf\n',config.read_text())
spec=services[2]['deployment']['stack_public_routes']
self.assertEqual(spec['installation_id'],identities[4812]['installation_id'])
self.assertEqual(spec['stack_id'],identities[4810]['installation_id'])
self.assertEqual(spec['primary_vmid'],4810)
self.assertEqual(spec['routes'],[
{'destination':'10.0.1.245','via':'10.77.0.31','target_vmid':4811,'target_installation_id':identities[4811]['installation_id']},
{'destination':'10.0.1.246','via':'10.77.0.30','target_vmid':4810,'target_installation_id':identities[4810]['installation_id']}])
self.assertNotIn('stack_public_routes',services[0]['deployment'])
self.assertNotIn('stack_public_routes',services[1]['deployment'])
def test_dhcp_without_static_targets_does_not_invent_routes(self):
services=[{'main':True,'frontend_ipv4':'dhcp','deployment':{}}]
with patch.object(runtime.oci_instances,'read') as read:
runtime.install_public_routes(services,4810,'vmbr10')
read.assert_not_called()
def test_duplicate_static_ip_rejected_before_read_or_write(self):
services=[{'main':True,'frontend_ipv4':'10.0.1.246/24'}, {'frontend':True,'frontend_ipv4':'10.0.1.246/24'}]
with patch.object(runtime.oci_instances,'read') as read:
with self.assertRaisesRegex(RuntimeError,'Duplicate'):runtime.install_public_routes(services,4810,'vmbr10')
read.assert_not_called()
def test_recreate_route_include_is_last_after_other_managed_includes(self):
source=(ROOT/'remote/install_oci.sh').read_text()
flow=source[source.index('prepare_managed_format_directories "/var/lib/lxc/'):]
self.assertLess(flow.index('apply_host_monitor\n'),flow.index('ROUTE_INCLUDE=$(python3'))
self.assertLess(flow.index('apply_kept_settings\n'),flow.index('ROUTE_INCLUDE=$(python3'))
def test_routes_installed_before_jobs_and_main_start(self):
source=(ROOT/'remote/install_generic_stack.py').read_text()
main=source[source.index('def main('):]
self.assertLess(main.index('install_public_routes(services, primary_id, bridge)'),main.index('bind_completed_jobs(services)'))
self.assertLess(main.index('install_public_routes(services, primary_id, bridge)'),main.index("run('pct','start',primary_id"))
if __name__ == '__main__':unittest.main()
+199
View File
@@ -0,0 +1,199 @@
"""Managed routes cannot configure an unowned guest or the host namespace."""
import copy
from contextlib import ExitStack
import json
import os
from pathlib import Path
import stat
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'remote'))
import oci_stack_public_routes as routes
OWNER = '11111111-1111-4111-8111-111111111111'
PRIMARY = '22222222-2222-4222-8222-222222222222'
MAC = '02:11:22:33:44:55'
def fixture():
return {'stack_public_routes': {'schema': 1, 'stack_id': PRIMARY, 'primary_vmid': 4900,
'vmid': 4901, 'installation_id': OWNER, 'private_bridge': 'vmbr15',
'private_address': '10.77.5.31/24', 'routes': [
{'destination': '172.30.249.90', 'via': '10.77.5.30',
'target_vmid': 4900, 'target_installation_id': PRIMARY}]}}
class PublicRouteTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
root = Path(self.temp.name)
self.stack = ExitStack()
self.addCleanup(self.stack.close)
for key, value in {'DIRECTORY': root/'owned', 'HELPER': root/'helper.py',
'CONFIGS': root/'configs', 'ROOT_UID': os.getuid()}.items():
self.stack.enter_context(patch.object(routes, key, value))
routes.CONFIGS.mkdir()
routes.HELPER.write_text(Path(routes.__file__).read_text())
routes.HELPER.chmod(0o644)
self.deployment = fixture()
self.spec = self.deployment['stack_public_routes']
def config(self, vmid, identity, private, public=None, mac=MAC):
text = f'# proxmenux-instance={identity}\nnet0: name=eth0,bridge=vmbr15,ip={private},hwaddr={mac}\n'
if public:
text += f'net1: name=eth1,bridge=vmbr909,ip={public}\n'
if vmid == 4901:
text += f'lxc.include: {routes.include_path(vmid)}\n'
path = routes.CONFIGS/f'{vmid}.conf'
path.write_text(text)
path.chmod(0o600)
return text
def install(self):
return routes.install(self.deployment, 4901, OWNER)
def test_install_check_canonical_include_and_root_owned_contract(self):
line = self.install()
self.assertEqual(line, f'lxc.include: {routes.include_path(4901)}')
self.assertNotIn('lxc.hook.version', routes.include_path(4901).read_text())
self.assertIn(' -I ', routes.include_content())
self.assertEqual(routes.check(line, self.deployment, 4901, OWNER), self.spec)
self.assertEqual(routes.contract_path(4901).stat().st_mode & 0o777, 0o600)
self.assertEqual(routes.HELPER.stat().st_mode & 0o777, 0o644)
def test_restore_can_validate_without_assets_but_not_a_foreign_include(self):
line = f'lxc.include: {routes.include_path(4901)}\n'
self.assertEqual(routes.check(line, self.deployment, 4901, OWNER, require_host_assets=False), self.spec)
with self.assertRaises(routes.RouteError):
routes.check(line.replace('4901.conf','4902.conf'), self.deployment, 4901, OWNER, require_host_assets=False)
self.assertEqual(routes.renumber_line(line.encode(),4901,5901), f'lxc.include: {routes.include_path(5901)}\n'.encode())
def test_reused_vmid_cannot_remove_another_installation_contract(self):
self.install()
with self.assertRaises(routes.RouteError):routes.remove(4901, PRIMARY)
self.assertTrue(routes.contract_path(4901).exists())
self.assertTrue(routes.remove(4901, OWNER))
self.assertFalse(routes.include_path(4901).exists())
def test_bad_identity_and_unsafe_routes_fail_before_commands(self):
changes = [lambda s:s.update(vmid=4902), lambda s:s.update(installation_id=PRIMARY),
lambda s:s['routes'][0].update(destination='0.0.0.0/0'),
lambda s:s['routes'][0].update(destination='127.0.0.1'),
lambda s:s['routes'][0].update(via='172.30.249.1'),
lambda s:s['routes'][0].update(destination=True),
lambda s:s['routes'].append(copy.deepcopy(s['routes'][0]))]
with patch.object(routes.subprocess,'run') as run:
for change in changes:
value=copy.deepcopy(self.deployment);change(value['stack_public_routes'])
with self.subTest(value=value),self.assertRaises(routes.RouteError):routes.validate(value,4901,OWNER)
run.assert_not_called()
def test_symlink_and_unsafe_helper_fail_closed(self):
routes.DIRECTORY.mkdir()
target=Path(self.temp.name)/'other';target.write_text('do not touch')
routes.include_path(4901).symlink_to(target)
with self.assertRaises(routes.RouteError):self.install()
self.assertEqual(target.read_text(),'do not touch')
routes.include_path(4901).unlink()
routes.HELPER.chmod(0o666)
with self.assertRaises(routes.RouteError):self.install()
def test_symlink_directory_and_foreign_file_owner_are_rejected(self):
outside=Path(self.temp.name)/'outside';outside.mkdir()
routes.DIRECTORY.symlink_to(outside, target_is_directory=True)
with self.assertRaises(routes.RouteError):self.install()
unsafe=SimpleNamespace(lstat=lambda:SimpleNamespace(st_mode=stat.S_IFREG|0o600,st_uid=routes.ROOT_UID+1,st_size=10))
with self.assertRaises(routes.RouteError):routes._read(unsafe)
def test_live_identity_and_frontend_addresses_are_verified(self):
self.install()
self.config(4901,OWNER,'10.77.5.31/24')
self.config(4900,PRIMARY,'10.77.5.30/24','172.30.249.90/24')
self.assertEqual(routes.live_contract(self.spec),MAC)
self.config(4900,OWNER,'10.77.5.30/24','172.30.249.90/24')
with self.assertRaises(routes.RouteError):routes.live_contract(self.spec)
self.config(4900,PRIMARY,'10.77.5.30/24','172.30.249.91/24')
with self.assertRaises(routes.RouteError):routes.live_contract(self.spec)
def test_tampered_or_world_writable_contract_is_rejected(self):
self.install();routes.contract_path(4901).chmod(0o666)
with self.assertRaises(routes.RouteError):routes.check(f'lxc.include: {routes.include_path(4901)}',self.deployment,4901,OWNER)
def test_namespace_is_a_net_namespace_and_never_the_host(self):
with patch.object(routes.os,'open',return_value=17),patch.object(routes.os,'close') as close, \
patch.object(routes.fcntl,'ioctl',return_value=routes.CLONE_NEWNET), \
patch.object(routes.os,'fstat',return_value=SimpleNamespace(st_dev=1,st_ino=9)), \
patch.object(routes.os,'stat',return_value=SimpleNamespace(st_dev=1,st_ino=9)):
with self.assertRaisesRegex(routes.RouteError,'host network'):routes.namespace_fd('/proc/123/fd/4')
close.assert_called_once_with(17)
with patch.object(routes.os,'open',return_value=17),patch.object(routes.os,'close'), \
patch.object(routes.fcntl,'ioctl',return_value=0):
with self.assertRaisesRegex(routes.RouteError,'not a network'):routes.namespace_fd('/proc/123/fd/4')
with patch.object(routes.os,'open') as opened:
for value in (None,'/proc/self/ns/net','/etc/passwd','/proc/123/ns/mnt'):
with self.assertRaises(routes.RouteError):routes.namespace_fd(value)
opened.assert_not_called()
def test_routes_are_only_32_in_verified_namespace_with_no_shell(self):
interface=[{'address':MAC,'addr_info':[{'family':'inet','local':'10.77.5.31','prefixlen':24}]}]
with patch.object(routes,'_namespace_command',side_effect=[json.dumps(interface).encode(),b'']) as command:
routes.apply(self.spec,17,MAC)
self.assertEqual(command.call_args_list[0].args,(17,'-j','address','show','dev','eth0'))
self.assertEqual(command.call_args.args,(17,'-4','route','replace','172.30.249.90/32','via','10.77.5.30','dev','eth0','src','10.77.5.31'))
with patch.object(routes.subprocess,'run',return_value=SimpleNamespace(stdout=b'ok')) as run:
routes._namespace_command(17,'-4','route','show')
self.assertEqual(run.call_args.args[0][:3],[routes.NSENTER,'--net=/proc/self/fd/17','--'])
self.assertEqual(run.call_args.kwargs['pass_fds'],(17,));self.assertNotIn('shell',run.call_args.kwargs)
def test_engine_helper_can_change_normally_without_stale_snapshot_adoption(self):
routes.HELPER.write_text('# trusted engine update, same stable path\n')
self.install()
self.assertEqual(routes.HELPER.read_text(),'# trusted engine update, same stable path\n')
def test_namespace_with_wrong_owner_address_cannot_receive_routes(self):
for value in ([], [{'address':'02:aa:bb:cc:dd:ee','addr_info':[{'family':'inet','local':'10.77.5.31','prefixlen':24}]}]):
with patch.object(routes,'_namespace_command',return_value=json.dumps(value).encode()) as command:
with self.assertRaises(routes.RouteError):routes.apply(self.spec,17,MAC)
self.assertEqual(command.call_count,1)
def test_ip_failure_propagates_so_startup_cannot_report_success(self):
interface=[{'address':MAC,'addr_info':[{'family':'inet','local':'10.77.5.31','prefixlen':24}]}]
with patch.object(routes,'_namespace_command',side_effect=[json.dumps(interface).encode(),subprocess.CalledProcessError(2,['ip'])]):
with self.assertRaises(subprocess.CalledProcessError):routes.apply(self.spec,17,MAC)
def test_start_uses_only_the_runtime_pid_namespace(self):
self.install()
with patch.dict(os.environ,{'LXC_NAME':'4901','LXC_PID':'12345'},clear=True), \
patch.object(routes.os,'geteuid',return_value=0), \
patch.object(routes,'live_contract',return_value=MAC), \
patch.object(routes,'namespace_fd',return_value=17) as namespace, \
patch.object(routes,'apply') as apply, patch.object(routes.os,'close') as close:
routes.start()
namespace.assert_called_once_with('/proc/12345/ns/net')
apply.assert_called_once_with(self.spec,17,MAC)
close.assert_called_once_with(17)
def test_default_hook_arguments_must_match_owner_and_phase(self):
with patch.dict(os.environ,{'LXC_NAME':'4901'},clear=True), \
patch.object(routes.os,'geteuid',return_value=0),patch.object(routes,'start') as start:
with patch.object(sys,'argv',['helper','start','4901','lxc','start-host']):routes.main()
start.assert_called_once()
start.reset_mock()
with patch.object(sys,'argv',['helper','start','4902','lxc','start-host']), self.assertRaises(routes.RouteError):routes.main()
with patch.object(sys,'argv',['helper','start','4901','lxc','post-stop']), self.assertRaises(routes.RouteError):routes.main()
start.assert_not_called()
def test_missing_lxc_pid_is_not_replaced_by_a_host_fallback(self):
self.install()
with patch.dict(os.environ,{'LXC_NAME':'4901'},clear=True),patch.object(routes.os,'geteuid',return_value=0),patch.object(routes,'live_contract',return_value=MAC),patch.object(routes,'apply') as apply:
with self.assertRaises(routes.RouteError):routes.start()
apply.assert_not_called()
if __name__=='__main__':unittest.main()
@@ -0,0 +1,131 @@
"""Managed frontend /32 routes remain bounded and portable across lifecycle APIs."""
import copy
import contextlib
import json
import os
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'remote'))
import oci_instance_transaction as tx
import oci_restore_recovery as recovery
import oci_stack_plan as plans
import oci_stack_public_routes as routes
import oci_remove as removal
MAIN='11111111-1111-4111-8111-111111111111'
WORKER='22222222-2222-4222-8222-222222222222'
FOREIGN='33333333-3333-4333-8333-333333333333'
def deployment():
return {'vmid':4901,'mounts':[],'stack_public_routes':{'schema':1,'stack_id':MAIN,
'primary_vmid':4900,'vmid':4901,'installation_id':WORKER,'private_bridge':'vmbr15',
'private_address':'10.77.5.31/24','routes':[{'destination':'192.0.2.10','via':'10.77.5.30',
'target_vmid':4900,'target_installation_id':MAIN}]}}
def records():
return {4900:{'vmid':4900,'installation_id':MAIN,'deployment':{},'stack':{'members':[
{'vmid':4900,'installation_id':MAIN},{'vmid':4901,'installation_id':WORKER}]}},
4901:{'vmid':4901,'installation_id':WORKER,'deployment':deployment()}}
class ManagedRouteLifecycleTests(unittest.TestCase):
@contextlib.contextmanager
def assets(self,folder):
with patch.multiple(routes,DIRECTORY=Path(folder)/'private',HELPER=Path(folder)/'canonical.py',ROOT_UID=os.geteuid()):
# The installed engine is trusted; guest-carried code is never used.
routes.HELPER.write_bytes(Path(routes.__file__).read_bytes())
routes.HELPER.chmod(0o755)
yield
def test_managed_include_coexists_with_sysctl_validation_and_rejects_unknown_include(self):
with tempfile.TemporaryDirectory() as folder,self.assets(folder):
plan=deployment();routes.install(plan,4901,WORKER)
config=(f'description: proxmenux-instance={WORKER}\nrootfs: local-zfs:subvol-4901-disk-0,size=8G\n'
f'lxc.include: {routes.include_path(4901)}\n').encode()
tx.check_runtime_mounts(config,plan)
with self.assertRaisesRegex(ValueError,'sysctl include'):
tx.check_runtime_mounts(config+b'lxc.include: /unknown/profile.conf\n',plan)
with self.assertRaisesRegex(ValueError,'differs from the contract'):
tx.check_runtime_mounts(config,{'vmid':4901,'mounts':[]})
def test_plan_rejects_foreign_target_even_if_supplied_in_record_inventory(self):
good=records();plans.validate_public_route_members(4900,good)
bad=copy.deepcopy(good);bad[4999]={'vmid':4999,'installation_id':FOREIGN,'deployment':{}}
route=bad[4901]['deployment']['stack_public_routes']['routes'][0]
route.update(target_vmid=4999,target_installation_id=FOREIGN)
with self.assertRaisesRegex(ValueError,'Invalid stack contract'):
plans.validate_public_route_members(4900,bad)
def test_plan_rejects_changed_target_and_stack_identities(self):
for field in ('target_installation_id','stack_id'):
bad=records();spec=bad[4901]['deployment']['stack_public_routes']
if field=='stack_id':spec[field]=FOREIGN
else:spec['routes'][0][field]=FOREIGN
with self.subTest(field=field),self.assertRaisesRegex(ValueError,'Invalid stack contract'):
plans.validate_public_route_members(4900,bad)
def test_restore_renumbers_only_managed_paths_and_all_route_member_ids(self):
with tempfile.TemporaryDirectory() as folder,self.assets(folder):
mapped=recovery.renumber(records(),{4900:4800,4901:4801})
# Mapping dictionary keys are not VMID-valued fields: select records by their new vmid.
mapped={value['vmid']:value for value in mapped.values()}
plans.validate_public_route_members(4800,mapped)
spec=mapped[4801]['deployment']['stack_public_routes']
self.assertEqual((spec['vmid'],spec['primary_vmid'],spec['routes'][0]['target_vmid']),(4801,4800,4800))
self.assertEqual(spec['installation_id'],WORKER)
original=f'lxc.include: {routes.include_path(4901)}\nlxc.include: /unrelated/4901.conf\n'
changed=recovery.renumbered_lines(original,4901,4801)
self.assertIn(f'lxc.include: {routes.include_path(4801)}',changed)
self.assertIn('lxc.include: /unrelated/4901.conf',changed)
routes.check(changed.splitlines()[0]+'\n',mapped[4801]['deployment'],4801,WORKER,require_host_assets=False)
def test_restore_republishes_only_canonical_host_code_before_start(self):
with tempfile.TemporaryDirectory() as folder,self.assets(folder):
plan=recovery.renumber(deployment(),{4900:4800,4901:4801})
entry={'vmid':4801,'installation_id':WORKER,'config':f'lxc.include: {routes.include_path(4801)}\n',
'copy':{'record':{'deployment':plan},'untrusted_script':'do not execute'}}
with patch.object(recovery,'renumber_config'):
recovery.restore_host_files(entry)
self.assertEqual(routes.HELPER.read_bytes(),Path(routes.__file__).read_bytes())
self.assertEqual(json.loads(routes.contract_path(4801).read_text())['routes'][0]['target_vmid'],4800)
routes.check(entry['config'],plan,4801,WORKER)
def test_restore_plan_refuses_foreign_route_before_host_assets_are_created(self):
bad=records();bad[4901]['deployment']['stack_public_routes']['routes'][0]['target_vmid']=4999
plan={'primary':4900,'members':[{'vmid':v} for v in bad],'restored':[
{'vmid':v,'copy':{'record':record}} for v,record in bad.items()],'blockers':[]}
recovery.check_public_route_members(Path('/unused'),plan)
self.assertTrue(any('Invalid stack contract' in value for value in plan['blockers']))
def test_recreate_keeps_managed_routes_outside_editable_desired_state(self):
old={'vmid':4901,'installation_id':WORKER,'status':'installed',
'template':{'id':'fixture','proxmox':{'installer_profile':{}},
'first_run':{'endpoints':[{'scheme':'http','port':80,'path':'/'}]}},
'deployment':deployment()}
candidate=copy.deepcopy(old);candidate['deployment'].pop('stack_public_routes')
candidate['deployment']['resources']={'cores':2,'memory_mb':1024}
result=tx.candidate_contract(old,'recreate',{'operation':'recreate','candidate':candidate})
self.assertEqual(result['deployment']['stack_public_routes'],old['deployment']['stack_public_routes'])
result['deployment']['stack_public_routes']['routes'][0]['destination']='192.0.2.11'
self.assertEqual(old['deployment']['stack_public_routes']['routes'][0]['destination'],'192.0.2.10')
def test_orphan_sweep_cleans_only_absent_owned_routes_and_keeps_pending_or_reused_ids(self):
for case in ('absent','pending','reused','changed'):
with self.subTest(case=case),tempfile.TemporaryDirectory() as folder,self.assets(folder):
root=Path(folder)/'records';root.mkdir()
routes.install(deployment(),4901,WORKER)
if case=='changed':routes.include_path(4901).write_text('foreign hook')
with patch.object(removal.instances,'guest_exists',return_value=case=='reused'), \
patch.object(removal.instances,'has_contract',return_value=case=='pending'), \
patch.object(removal.instances,'read',return_value={'installation_id':WORKER}), \
patch.object(removal.instances,'release_orphan',return_value=False), \
patch.object(removal,'remove_host_state'), \
patch.object(removal.oci_console,'remove_log'), \
patch.object(removal,'release_shared_host_files'):
cleaned=removal.sweep_orphans(root)
self.assertEqual(cleaned,[4901] if case=='absent' else [])
self.assertEqual(routes.contract_path(4901).exists(),case!='absent')
self.assertEqual(routes.include_path(4901).exists(),case!='absent')