lxc: code cleanup and docs polish from review

Code:
- Fix check_ssh_config_include() to do a case-insensitive line match
  (read_text().splitlines() instead of filter(None, map(..., open()))).
- Drop the default help_text= from _add_name_args(); callers now must
  supply an explicit string.
- Expand the DNSContainer class docstring.
- Fix sysctl comment: note that incus provides net.* virtualization
  so the sysctl only affects the container's network namespace.

Docs (doc/source/lxc.rst):
- Remove double blank line after page title; fix missing comma.
- Replace the plain-text root-access note with a .. caution:: block.
- Tighten the Quick-start section and lxc-test CLI entry.
This commit is contained in:
holger krekel
2026-03-08 18:12:49 +01:00
parent e2ec0cf2c5
commit 04ac2cf700
4 changed files with 36 additions and 44 deletions

View File

@@ -430,14 +430,8 @@ def _deploy_status(ct, local_hash, ix):
return f"IN-SYNC ({short})" return f"IN-SYNC ({short})"
def _add_name_args(parser, help_text=None): def _add_name_args(parser, help_text):
"""Add optional positional NAME arguments.""" parser.add_argument("names", nargs="*", metavar="NAME", help=help_text)
parser.add_argument(
"names",
nargs="*",
metavar="NAME",
help=help_text or "Relay name(s) to operate on.",
)
def _run_cmdeploy(subcmd, ct, ix, out, extra=None, **kwargs): def _run_cmdeploy(subcmd, ct, ix, out, extra=None, **kwargs):

View File

@@ -95,8 +95,9 @@ class Incus:
user_ssh_config = Path.home() / ".ssh" / "config" user_ssh_config = Path.home() / ".ssh" / "config"
if not user_ssh_config.exists(): if not user_ssh_config.exists():
return False return False
lines = filter(None, map(str.strip, user_ssh_config.open("r"))) lines = user_ssh_config.read_text().splitlines()
return f"Include {self.ssh_config_path}" in lines target = f"include {self.ssh_config_path}".lower()
return any(line.strip().lower() == target for line in lines)
def run(self, args, check=True, capture=True, input=None): def run(self, args, check=True, capture=True, input=None):
"""Run an incus command. """Run an incus command.
@@ -190,7 +191,7 @@ class Incus:
def delete_images(self): def delete_images(self):
"""Delete the cached base and relay images.""" """Delete the cached base and relay images."""
for alias in (RELAY_IMAGE_ALIAS, BASE_IMAGE_ALIAS): for alias in (RELAY_IMAGE_ALIAS, BASE_IMAGE_ALIAS):
self.run(["image", "delete", alias], check=False) self.run(["image", "delete", alias], check=False) # ok if absent
def list_managed(self): def list_managed(self):
"""Return list of dicts with name, ip, ipv6, domain, status, memory_usage.""" """Return list of dicts with name, ip, ipv6, domain, status, memory_usage."""
@@ -433,7 +434,8 @@ class RelayContainer(Container):
def disable_ipv6(self): def disable_ipv6(self):
"""Disable IPv6 inside the container via sysctl.""" """Disable IPv6 inside the container via sysctl."""
# incus provides net.* virtalization # incus provides net.* virtualization for LXC containers so that
# these sysctls only affect the container's network namespace.
self.bash("""\ self.bash("""\
sysctl -w net.ipv6.conf.all.disable_ipv6=1 sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1 sysctl -w net.ipv6.conf.default.disable_ipv6=1
@@ -485,7 +487,7 @@ class RelayContainer(Container):
return shell(cmd, timeout=15).returncode == 0 return shell(cmd, timeout=15).returncode == 0
def configure_dns(self, dns_ip): def configure_dns(self, dns_ip):
"""Point this container's resolver at *dns_ip* and verify it works.""" """Point this container's resolver at *dns_ip* and verify DNS is reachable."""
self.bash(f"""\ self.bash(f"""\
systemctl disable --now systemd-resolved 2>/dev/null || true systemctl disable --now systemd-resolved 2>/dev/null || true
rm -f /etc/resolv.conf rm -f /etc/resolv.conf
@@ -535,7 +537,11 @@ class RelayContainer(Container):
class DNSContainer(Container): class DNSContainer(Container):
"""Container handle for the PowerDNS name server.""" """Container handle for the PowerDNS name server.
Manages the authoritative and recursive DNS services required for
name resolution in the local testing environment.
"""
def __init__(self, incus): def __init__(self, incus):
super().__init__(incus, DNS_CONTAINER_NAME, domain=DNS_DOMAIN) super().__init__(incus, DNS_CONTAINER_NAME, domain=DNS_DOMAIN)

View File

@@ -12,7 +12,7 @@ from cmdeploy.util import Out
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
not shutil.which("incus"), not shutil.which("incus"),
reason="incus/lxc not installed", reason="incus not installed",
) )

View File

@@ -1,21 +1,15 @@
Local testing with LXC/Incus Local testing with LXC/Incus
============================ ============================
.. warning::
cmdeploy LXC support is geared towards local testing and CI, only.
Do not base production setups on it.
The ``cmdeploy`` tool includes support for running The ``cmdeploy`` tool includes support for running
chatmail relays inside local chatmail relays inside local
`Incus <https://linuxcontainers.org/incus/>`_ LXC containers. `Incus <https://linuxcontainers.org/incus/>`_ LXC containers.
This is useful for development, testing, and CI This is meant for development, testing, and CI
without requiring a remote server. without requiring a remote server.
LXC system containers behave like lightweight virtual machines. LXC system containers are lightweight virtual machines
They share the host's kernel but run their own init system that share the host's kernel but run their own init system,
(systemd), package manager, and network stack, package manager, and network stack,
so the cmdeploy deployment scripts work exactly so the cmdeploy deployment scripts work pretty much
as they would on a real Debian server or cloud VPS. as they would on a real Debian server or cloud VPS.
Prerequisites Prerequisites
@@ -32,6 +26,16 @@ After installing incus, initialise and grant yourself access::
sudo incus admin init --minimal sudo incus admin init --minimal
sudo usermod -aG incus-admin $USER sudo usermod -aG incus-admin $USER
.. caution::
Adding yourself to ``incus-admin`` grants effective root access
to the host: any member can mount host directories into a container
and manipulate them as root.
This is fine for local testing of your own relay branches,
but do **not** use it for production setups
or for testing untrusted relay branches from others.
.. warning:: .. warning::
You **must now log out and back in** (or run ``newgrp incus-admin``) You **must now log out and back in** (or run ``newgrp incus-admin``)
@@ -53,11 +57,13 @@ Quick start
source venv/bin/activate # activate venv source venv/bin/activate # activate venv
cmdeploy lxc-test # create containers, deploy, test cmdeploy lxc-test # create containers, deploy, test
The ``lxc-test`` command provides an automated way
The ``lxc-test`` command executes each ``cmdeploy`` subprocess command to run the full deployment and test pipeline.
so you can copy-paste and run them individually. It executes several ``cmdeploy`` subcommands in sequential steps.
If a step fails, you can copy-paste the printed command
and run it manually to debug.
No host DNS delegation or ``~/.ssh/config`` changes are needed No host DNS delegation or ``~/.ssh/config`` changes are needed
because lxc-test passes ssh-related CLI options to "cmdeploy run" and "cmdeploy test" commands. because ``lxc-test`` passes the required SSH and DNS options directly.
CLI reference CLI reference
@@ -86,20 +92,6 @@ CLI reference
User containers are **never** destroyed unless named explicitly. User containers are **never** destroyed unless named explicitly.
``lxc-test [--one]`` ``lxc-test [--one]``
Idempotent full pipeline:
1. ``lxc-start``: create ``test0`` + ``test1``
containers, minimal DNS
2. ``cmdeploy run``: deploy chatmail services on each relay
3. locally cache ``localchat-relay`` image after first successful deploy
4. ``cmdeploy dns --zonefile``: generate standard
BIND-format zone files, load full DNS records
5. ``cmdeploy test``: run full test suite
By default creates, deploys, and tests both ``test0`` and ``test1`` By default creates, deploys, and tests both ``test0`` and ``test1``
for dual-domain federation testing (sets ``CHATMAIL_DOMAIN2=_test1.localchat``). for dual-domain federation testing (sets ``CHATMAIL_DOMAIN2=_test1.localchat``).
test0 runs dual-stack (IPv4 + IPv6) while test1 runs IPv4-only (``disable_ipv6 = True``). test0 runs dual-stack (IPv4 + IPv6) while test1 runs IPv4-only (``disable_ipv6 = True``).