Files
ProxMenux/oci/tests/test_removal_cleanup.py
T

178 lines
8.9 KiB
Python

"""Removing an OCI application leaves nothing of it on the host, and nothing another guest still uses is touched."""
import json
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_remove
import oci_runtime_settings as runtime_settings
HOOK = """#!/usr/bin/env bash
inside=/data/mounts/drive
published={shared}/rw/drive
published_ro={shared}/ro/drive
"""
class RemovalCleanupTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.root = Path(tmp.name)
self.nodes = self.root / "nodes"
(self.nodes / "amd/lxc").mkdir(parents=True)
(self.nodes / "pve2/lxc").mkdir(parents=True)
self.snippets = self.root / "snippets"
self.snippets.mkdir()
self.apps = self.root / "apps"
self.apps.mkdir()
self.cluster = self.root / "proxmenux"
self.cluster.mkdir()
self.legacy = self.root / "legacy"
self.legacy.mkdir()
self.host_monitor = (self.cluster / "host-monitor", self.legacy / "proxmenux-host-monitor")
patches = [
patch.object(oci_remove, "CLUSTER_NODES", self.nodes),
patch.object(oci_remove, "SNIPPETS", self.snippets),
patch.object(oci_remove, "MONITOR_APPS", self.apps),
patch.object(oci_remove, "HOST_MONITOR_INCLUDES", self.host_monitor),
patch.object(runtime_settings, "include_path", lambda vmid: self.cluster / f"{vmid}.sysctls"),
patch.object(runtime_settings, "legacy_include_path", lambda vmid: self.legacy / f"{vmid}.proxmenux-sysctls"),
patch.object(oci_remove.subprocess, "run"),
]
for item in patches:
item.start()
self.addCleanup(item.stop)
def test_every_file_of_the_container_is_removed(self):
(self.cluster / "113.sysctls").write_text("lxc.sysctl.net.ipv4.ip_unprivileged_port_start = 0\n")
(self.legacy / "113.proxmenux-sysctls").write_text("x\n")
shared = self.root / "shared"
for view in ("rw/drive", "ro/drive"):
(shared / view).mkdir(parents=True)
(self.snippets / "proxmenux-rclone-113-fuse-hook.sh").write_text(HOOK.format(shared=shared))
(self.apps / "113.json").write_text("{}")
(self.apps / ".oci-dismissed.json").write_text(json.dumps({"113": "a", "112": "b"}))
oci_remove.remove_host_state(113)
self.assertFalse((self.cluster / "113.sysctls").exists())
self.assertFalse((self.legacy / "113.proxmenux-sysctls").exists())
self.assertFalse((self.snippets / "proxmenux-rclone-113-fuse-hook.sh").exists())
self.assertFalse((shared / "rw/drive").exists() or (shared / "ro/drive").exists())
self.assertFalse((self.apps / "113.json").exists())
self.assertEqual(json.loads((self.apps / ".oci-dismissed.json").read_text()), {"112": "b"})
def test_a_published_view_with_content_is_kept(self):
shared = self.root / "shared"
(shared / "rw/drive").mkdir(parents=True)
(shared / "rw/drive/file").write_text("data")
(self.snippets / "proxmenux-rclone-113-fuse-hook.sh").write_text(HOOK.format(shared=shared))
oci_remove.remove_host_state(113)
self.assertTrue((shared / "rw/drive/file").exists())
def test_other_containers_are_not_touched(self):
(self.cluster / "114.sysctls").write_text("x\n")
(self.apps / "114.json").write_text("{}")
oci_remove.remove_host_state(113)
self.assertTrue((self.cluster / "114.sysctls").exists() and (self.apps / "114.json").exists())
def test_shared_host_monitor_include_is_kept_while_a_guest_uses_it(self):
self.host_monitor[0].write_text("lxc.namespace.share.net = 1\n")
(self.nodes / "pve2/lxc/120.conf").write_text(f"arch: amd64\nlxc.include: {self.host_monitor[0]}\n")
oci_remove.release_shared_host_files([])
self.assertTrue(self.host_monitor[0].exists())
(self.nodes / "pve2/lxc/120.conf").unlink()
oci_remove.release_shared_host_files([])
self.assertFalse(self.host_monitor[0].exists())
def test_stack_hookscript_is_removed_only_when_no_guest_uses_it(self):
hook = self.snippets / "proxmenux-stack-dependencies.sh"
hook.write_text("#!/bin/sh\n")
volume = "local:snippets/proxmenux-stack-dependencies.sh"
with patch.object(oci_remove.instances, "command", return_value=f"{hook}\n".encode()):
(self.nodes / "amd/lxc/130.conf").write_text(f"hookscript: {volume}\n")
oci_remove.release_shared_host_files([volume])
self.assertTrue(hook.exists())
(self.nodes / "amd/lxc/130.conf").unlink()
oci_remove.release_shared_host_files([volume, "local:snippets/someone-else.sh"])
self.assertFalse(hook.exists())
def sweep(self, existing=()):
registry = self.root / "instances"
registry.mkdir(exist_ok=True)
logs = self.root / "logs"
logs.mkdir(exist_ok=True)
with patch.object(oci_remove.oci_console, "LOG_DIR", logs), \
patch.object(oci_remove.instances, "guest_exists", lambda vmid: vmid in existing), \
patch.object(oci_remove.instances, "has_contract", lambda root, vmid: (root / str(vmid) / "oci-compose.json").exists()), \
patch.object(oci_remove.instances, "release_orphan", self.retire):
return oci_remove.sweep_orphans(registry), registry, logs
def retire(self, root, vmid):
record = root / str(vmid) / "oci-compose.json"
if json.loads(record.read_text()).get("pending_transaction"):
return False
record.replace(record.with_name("retired-x.json"))
return True
def test_sweep_removes_what_is_left_of_containers_that_no_longer_exist(self):
registry = self.root / "instances"
(registry / "151").mkdir(parents=True)
(registry / "151/oci-compose.json").write_text("{}")
(self.legacy / "9901.proxmenux-sysctls").write_text("x\n")
(self.cluster / "100.sysctls").write_text("x\n")
cleaned, registry, logs = self.sweep(existing={100})
self.assertEqual(cleaned, [151, 9901])
self.assertTrue((registry / "151/retired-x.json").exists())
self.assertFalse((self.legacy / "9901.proxmenux-sysctls").exists())
# A container that exists keeps everything.
self.assertTrue((self.cluster / "100.sysctls").exists())
# Nothing left: the next visit cleans and reports nothing.
self.assertEqual(self.sweep(existing={100})[0], [])
def test_sweep_finds_oci_registrations_of_the_app_tab_only(self):
(self.apps / "113.json").write_text(json.dumps({"apps": [{"installed_via": "oci_image"}]}))
(self.apps / "114.json").write_text(json.dumps({"apps": [{"installed_via": "dpkg"}]}))
self.assertEqual(self.sweep()[0], [113])
self.assertFalse((self.apps / "113.json").exists())
self.assertTrue((self.apps / "114.json").exists())
def test_sweep_keeps_an_operation_left_halfway(self):
registry = self.root / "instances"
(registry / "152").mkdir(parents=True)
(registry / "152/oci-compose.json").write_text(json.dumps({"pending_transaction": "x"}))
(self.cluster / "152.sysctls").write_text("x\n")
self.assertEqual(self.sweep()[0], [])
self.assertTrue((self.cluster / "152.sysctls").exists())
def test_private_network_of_a_stack_and_of_an_arr_suite_application(self):
stack = {"stack": {"deployment": {"network": {"private_bridge": "vmbr10", "private_subnet": "10.77.0.0/24"}}}}
suite = {"deployment": {"network": {"bridge": "vmbr11", "ipv4": "10.77.1.31/24"}}}
lan = {"deployment": {"network": {"bridge": "vmbr0", "ipv4": "192.168.0.40/24"}}}
dhcp = {"deployment": {"network": {"bridge": "vmbr0", "ipv4": "dhcp"}}}
self.assertEqual(oci_remove.private_bridge(stack), "vmbr10")
self.assertEqual(oci_remove.private_bridge(suite), "vmbr11")
self.assertIsNone(oci_remove.private_bridge(lan))
self.assertIsNone(oci_remove.private_bridge(dhcp))
def test_the_suite_network_is_kept_while_another_application_uses_it(self):
(self.nodes / "amd/lxc/180.conf").write_text("net0: name=eth0,bridge=vmbr11,ip=10.77.1.30/24\n")
(self.nodes / "amd/lxc/181.conf").write_text("net0: name=eth0,bridge=vmbr11,ip=10.77.1.31/24\n")
with patch.object(oci_remove, "Path", lambda value: self.nodes if value == "/etc/pve/nodes" else Path(value)):
self.assertTrue(oci_remove.bridge_in_use("vmbr11", {181}))
self.assertFalse(oci_remove.bridge_in_use("vmbr11", {180, 181}))
def test_a_container_on_another_node_is_found(self):
(self.nodes / "pve2/lxc/113.conf").write_text("arch: amd64\n")
self.assertEqual(oci_remove.guest_node(113), "pve2")
self.assertIsNone(oci_remove.guest_node(114))
if __name__ == "__main__":
unittest.main()