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,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Give its DHCP address back to an OCI container that restarted itself.
|
||||
|
||||
Proxmox runs the DHCP client of a host-managed interface from the host and
|
||||
starts it only when it starts the container. A container that reboots from
|
||||
inside, as Home Assistant OS does from its settings, is started again by
|
||||
Proxmox without that client and comes back with no address. The start hook of
|
||||
every OCI container queues this check: once the container is up, the client
|
||||
that is missing is started the way Proxmox starts it.
|
||||
|
||||
The client lives where the container was started from: one started from the
|
||||
Monitor ends when the Monitor is restarted, and the address would be lost
|
||||
when its lease runs out. The Monitor asks for every container when it starts.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
CONFIGS = Path('/etc/pve/lxc')
|
||||
HOOKS = Path('/var/lib/lxc')
|
||||
PROC = Path('/proc')
|
||||
LOCKS = Path('/run/proxmenux')
|
||||
# Proxmox starts its own client right after the container; one that just
|
||||
# started is left to it, and to the check its start hook queued.
|
||||
SETTLE = 30
|
||||
NAME_RE = re.compile(r'[A-Za-z0-9_.-]{1,15}')
|
||||
START = 'PVE::LXC::manage_dhclient("start", $ARGV[0], $ARGV[1], $ARGV[2], "/proc/$ARGV[3]/root")'
|
||||
|
||||
|
||||
def managed(config):
|
||||
"""The host-managed interfaces of a container configuration that take
|
||||
their address by DHCP: [(name, ip version)]."""
|
||||
found = []
|
||||
for line in config.split('\n[', 1)[0].splitlines():
|
||||
match = re.match(r'net\d+:\s*(.*)', line)
|
||||
if not match:
|
||||
continue
|
||||
options = dict(part.split('=', 1) for part in match.group(1).split(',') if '=' in part)
|
||||
name = options.get('name', '')
|
||||
if options.get('host-managed') != '1' or options.get('link_down') == '1' or not NAME_RE.fullmatch(name):
|
||||
continue
|
||||
found += [(name, version) for key, version in (('ip', 4), ('ip6', 6)) if options.get(key) == 'dhcp']
|
||||
return found
|
||||
|
||||
|
||||
def client_running(vmid, name, version):
|
||||
"""Whether a DHCP client of the interface runs, with or without a lease yet."""
|
||||
mark = f'{HOOKS}/{vmid}/hook/dhclient{version}-{name}.pid'.encode()
|
||||
for entry in PROC.glob('[0-9]*/cmdline'):
|
||||
try:
|
||||
if mark in entry.read_bytes().split(b'\0'):
|
||||
return True
|
||||
except OSError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def init_pid(vmid):
|
||||
result = subprocess.run(['lxc-info', '-n', str(vmid), '-pH'], capture_output=True, text=True, check=False, timeout=30)
|
||||
pid = result.stdout.strip()
|
||||
return int(pid) if result.returncode == 0 and pid.isdigit() and int(pid) > 0 else None
|
||||
|
||||
|
||||
def running_for(pid):
|
||||
"""Seconds since the init of the container started."""
|
||||
try:
|
||||
return time.time() - (PROC / str(pid)).stat().st_ctime
|
||||
except OSError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def containers():
|
||||
"""The containers of this node with a host-managed interface on DHCP."""
|
||||
found = []
|
||||
for path in sorted(CONFIGS.glob('*.conf')):
|
||||
try:
|
||||
if path.stem.isdigit() and managed(path.read_text(errors='replace')):
|
||||
found.append(int(path.stem))
|
||||
except OSError:
|
||||
continue
|
||||
return found
|
||||
|
||||
|
||||
def start_client(vmid, name, version, pid):
|
||||
# In a scope of its own: the client stays when this check ends.
|
||||
result = subprocess.run(['systemd-run', '--scope', '--quiet', '--collect', 'perl', '-MPVE::LXC', '-e', START,
|
||||
str(vmid), str(version), name, str(pid)],
|
||||
capture_output=True, text=True, check=False, timeout=180)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def repair(vmid, settle=0):
|
||||
"""Start the DHCP clients a running container is missing. Returns the
|
||||
interfaces that got one: [(name, ip version)]."""
|
||||
try:
|
||||
config = (CONFIGS / f'{vmid}.conf').read_text(errors='replace')
|
||||
except OSError:
|
||||
return []
|
||||
wanted = managed(config)
|
||||
if not wanted:
|
||||
return []
|
||||
pid = init_pid(vmid)
|
||||
if pid is None or running_for(pid) < settle:
|
||||
return []
|
||||
started = []
|
||||
for name, version in wanted:
|
||||
if not client_running(vmid, name, version) and start_client(vmid, name, version, pid):
|
||||
started.append((name, version))
|
||||
return started
|
||||
|
||||
|
||||
def check(vmid, settle=0):
|
||||
with (LOCKS / f'oci-dhcp-lease-{vmid}.lock').open('w') as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
return
|
||||
for name, version in repair(vmid, settle):
|
||||
print(f'CT {vmid}: DHCP client of {name} (IPv{version}) started again', flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2 or not (sys.argv[1].isdigit() or sys.argv[1] == '--all'):
|
||||
print('usage: oci_dhcp_lease.py <vmid> | --all', file=sys.stderr)
|
||||
return 2
|
||||
LOCKS.mkdir(parents=True, exist_ok=True)
|
||||
if sys.argv[1] == '--all':
|
||||
for vmid in containers():
|
||||
check(vmid, SETTLE)
|
||||
else:
|
||||
check(int(sys.argv[1]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user