mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
feat(oci): update and modify every application, and keep its address
- The backup taken before an update or a modification is made on another storage when it does not fit on the system disk; an operation without room says so before anything is stopped. - Privileged containers, relaxed AppArmor or seccomp, process limits, the host monitors Glances and Netdata and single-file host mounts are rebuilt and recovered like any other application. - HAOS One installs the docker-haos image: port 8123, host time zone, unprivileged, watchdog proposed by default; it is updated and modified with Home Assistant's own check on the rebuilt container. - A container that reboots itself gets its DHCP client back. - The installer takes the address of the container's own interface, and the notes of a suite or stack container show its LAN address. - Seerr installs: its volume belongs to the image user and no-new-privileges goes through the include of the container. - Removing an application deletes its seccomp profile.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Home Assistant OS is updated and modified like any other application."""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'remote'))
|
||||
import oci_instance_transaction as transaction
|
||||
|
||||
HAOS = json.loads((ROOT / 'catalog/curated/haos-one.json').read_text())
|
||||
CONFIG = (b'net0: name=eth0,bridge=vmbr0,firewall=1,host-managed=1,hwaddr=BC:24:11:D5:0F:FD,ip=dhcp,type=veth\n'
|
||||
b'ostype: unmanaged\n')
|
||||
ETH0 = b'2: eth0 inet 192.168.0.100/24 brd 192.168.0.255 scope global eth0\\ valid_lft forever\n'
|
||||
|
||||
|
||||
class HaosCheckTests(TestCase):
|
||||
def test_the_recipe_has_a_check_so_the_operation_is_not_refused(self):
|
||||
check = transaction.effective_healthcheck(HAOS)
|
||||
self.assertEqual(check, {'type': 'haos', 'timeout_seconds': 1200})
|
||||
|
||||
def test_the_check_is_left_to_the_installer_profile(self):
|
||||
contract = transaction.with_default_healthcheck({'template': json.loads(json.dumps(HAOS))})
|
||||
profile = contract['template']['proxmox']['installer_profile']
|
||||
self.assertNotIn('startup_healthcheck', profile)
|
||||
self.assertEqual(profile['haos_healthcheck'], {'timeout_seconds': 1200})
|
||||
|
||||
def run_commands(self, *args):
|
||||
if args[:2] == ('pct', 'config'):
|
||||
return CONFIG
|
||||
if args[:2] == ('pct', 'status'):
|
||||
return b'status: running\n'
|
||||
if args[0] == 'lxc-attach':
|
||||
return ETH0
|
||||
raise AssertionError(args)
|
||||
|
||||
def test_the_container_answers_on_its_own_interface(self):
|
||||
with patch.object(transaction, 'run', side_effect=self.run_commands):
|
||||
self.assertEqual(transaction.own_addresses(121), ['192.168.0.100'])
|
||||
|
||||
def test_a_restored_container_passes_the_check_of_home_assistant(self):
|
||||
import haos_healthcheck
|
||||
urls = [{'label': 'Home Assistant', 'url': 'http://192.168.0.100:8123/'}]
|
||||
with patch.object(transaction, 'run', side_effect=self.run_commands), \
|
||||
patch.object(transaction, 'log'), \
|
||||
patch.object(haos_healthcheck, 'wait_ready', return_value=urls) as wait:
|
||||
self.assertEqual(transaction.healthcheck(121, HAOS), 'http://192.168.0.100:8123/')
|
||||
wait.assert_called_once_with(121, '192.168.0.100', 1200)
|
||||
|
||||
def test_a_restored_container_that_does_not_pass_is_not_confirmed(self):
|
||||
import haos_healthcheck
|
||||
with patch.object(transaction, 'run', side_effect=self.run_commands), \
|
||||
patch.object(transaction, 'log'), \
|
||||
patch.object(haos_healthcheck, 'wait_ready', side_effect=RuntimeError('time is up')):
|
||||
with self.assertRaises(ValueError):
|
||||
transaction.healthcheck(121, HAOS)
|
||||
|
||||
|
||||
class InstallerAddressTests(TestCase):
|
||||
"""The installer takes the address of an interface of the container, not
|
||||
the one of a bridge Docker created inside it."""
|
||||
|
||||
def address(self, listed, attach):
|
||||
script = (ROOT / 'remote/install_oci.sh').read_text()
|
||||
start = script.index('own_ipv4() {')
|
||||
function = script[start:script.index('\n}\n', start) + 3]
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name, body in (('lxc-info', f'printf "{listed}"'),
|
||||
('pct', 'echo "net0: name=eth0,bridge=vmbr0,ip=dhcp,type=veth"'),
|
||||
('lxc-attach', attach)):
|
||||
path = Path(tmp) / name
|
||||
path.write_text(f'#!/bin/bash\n{body}\n')
|
||||
path.chmod(0o755)
|
||||
result = subprocess.run(['bash', '-c', f'PATH={tmp}:$PATH; VMID=121; {function}\nown_ipv4; echo "rc=$?"'],
|
||||
capture_output=True, text=True)
|
||||
return result.stdout.split()
|
||||
|
||||
def test_the_inner_bridges_are_skipped(self):
|
||||
attach = 'echo "2: eth0 inet 192.168.0.100/24 brd 192.168.0.255 scope global eth0"'
|
||||
self.assertEqual(self.address('172.30.232.1\\n172.30.32.1\\n192.168.0.100\\n', attach), ['192.168.0.100', 'rc=0'])
|
||||
|
||||
def test_no_address_yet_is_waited_for(self):
|
||||
self.assertEqual(self.address('172.30.232.1\\n', 'true'), ['rc=0'])
|
||||
|
||||
def test_interfaces_that_cannot_be_read_leave_it_to_the_caller(self):
|
||||
self.assertEqual(self.address('192.168.0.100\\n', 'exit 1'), ['rc=1'])
|
||||
self.assertEqual(self.address('', 'true'), ['rc=1'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import unittest
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user