On Thu, Oct 13, 2022 at 09:26:21PM -0400, Daniel Kahn Gillmor wrote:

> What's the best approach?  Should someone who wanted to add an nspawn
> isolation container try to copy ./virt/autopkgtest-virt-lxc and
> ./tools/autopkgtest-build-lxc (and their associated manpages) and tweak
> them to use nspawn instead of lxc?

I couldn't think of anything better, so attached to this mail is a
start.

This is based on 
https://github.com/Truelite/nspawn-runner/blob/main/nspawn-runner
and on https://github.com/ARPA-SIMC/moncic-ci/blob/main/moncic/container.py#L463

This is what currently happens with it:

  autopkgtest-virt-nspawn: DBG: executing open
  autopkgtest-virt-nspawn: DBG: will start a container (with 
isolation-container capability)
  autopkgtest-virt-nspawn: DBG: using container name 
8a087ee0-a8c1-4c87-a2f1-e43709e11184
  autopkgtest-virt-nspawn: DBG: execute-timeout: sudo systemd-run --quiet 
--property=KillMode=mixed --property=Type=notify 
--property=RestartForceExitStatus=133 --property=SuccessExitStatus=133 
--property=Slice=machine.slice --property=Delegate=yes 
--property=TasksMax=16384 --property=WatchdogSec=3min systemd-nspawn --quiet 
--directory=/var/lib/machines/sid-build 
--machine=8a087ee0-a8c1-4c87-a2f1-e43709e11184 --boot --notify-ready=yes 
--resolv-conf=replace-host --volatile=overlay --read-only --suppress-sync=yes 
systemd.hostname=8a087ee0-a8c1-4c87-a2f1-e43709e11184
  autopkgtest-virt-nspawn: DBG: container started
  autopkgtest-virt-nspawn: DBG: execute-timeout: sudo systemd-run --quiet 
--machine=8a087ee0-a8c1-4c87-a2f1-e43709e11184 -- sh -c getent passwd | sort 
-t: -nk3 | awk -F: '{if ($3 >= 1000 && $3 <= 59999) { print $1; exit } }'
  autopkgtest-virt-nspawn: DBG: determine_normal_user: no uid in [1000,59999] 
available
  autopkgtest-virt-nspawn: DBG: auxverb = ['sudo', 'systemd-run', '--quiet', 
'--machine=8a087ee0-a8c1-4c87-a2f1-e43709e11184', '--', 'env', '-i', 'bash', 
'-c', 'set -a; [ -r /etc/environment ] && . /etc/environment 2>/dev/null || 
true; [ -r /etc/default/locale ] && . /etc/default/locale 2>/dev/null || true; 
[ -r /etc/profile ] && . /etc/profile 2>/dev/null || true; set +a;"$@"; RC=$?; 
[ $RC != 255 ] || RC=253; set -e;myout=$(readlink 
/proc/$$/fd/1);myerr=$(readlink /proc/$$/fd/2);myout="${myout/[/\\\\[}"; 
myout="${myout/]/\\\\]}";myerr="${myerr/[/\\\\[}"; 
myerr="${myerr/]/\\\\]}";PS=$(ls -l /proc/[0-9]*/fd/* 2>/dev/null | sed -nr 
\'\\#(\'"$myout"\'|\'"$myerr"\')# { s#^.*/proc/([0-9]+)/.*$#\\1#; p}\'|sort 
-u);KILL="";for pid in $PS; do    [ $pid -ne $$ ] && [ $pid -ne $PPID ] || 
continue;    KILL="$KILL $pid";done;[ -z "$KILL" ] || kill -9 $KILL >/dev/null 
2>&1 || true;exit $RC', '--'], downtmp = None
  autopkgtest-virt-nspawn: DBG: execute-timeout: sudo systemd-run --quiet 
--machine=8a087ee0-a8c1-4c87-a2f1-e43709e11184 -- env -i bash -c set -a; [ -r 
/etc/environment ] && . /etc/environment 2>/dev/null || true; [ -r 
/etc/default/locale ] && . /etc/default/locale 2>/dev/null || true; [ -r 
/etc/profile ] && . /etc/profile 2>/dev/null || true; set +a;"$@"; RC=$?; [ $RC 
!= 255 ] || RC=253; set -e;myout=$(readlink /proc/$$/fd/1);myerr=$(readlink 
/proc/$$/fd/2);myout="${myout/[/\\[}"; 
myout="${myout/]/\\]}";myerr="${myerr/[/\\[}"; myerr="${myerr/]/\\]}";PS=$(ls 
-l /proc/[0-9]*/fd/* 2>/dev/null | sed -nr '\#('"$myout"'|'"$myerr"')# { 
s#^.*/proc/([0-9]+)/.*$#\1#; p}'|sort -u);KILL="";for pid in $PS; do    [ $pid 
-ne $$ ] && [ $pid -ne $PPID ] || continue;    KILL="$KILL $pid";done;[ -z 
"$KILL" ] || kill -9 $KILL >/dev/null 2>&1 || true;exit $RC -- mktemp 
--directory --tmpdir autopkgtest.XXXXXX
  autopkgtest-virt-nspawn: DBG: execute-timeout: sudo systemd-run --quiet 
--machine=8a087ee0-a8c1-4c87-a2f1-e43709e11184 -- env -i bash -c set -a; [ -r 
/etc/environment ] && . /etc/environment 2>/dev/null || true; [ -r 
/etc/default/locale ] && . /etc/default/locale 2>/dev/null || true; [ -r 
/etc/profile ] && . /etc/profile 2>/dev/null || true; set +a;"$@"; RC=$?; [ $RC 
!= 255 ] || RC=253; set -e;myout=$(readlink /proc/$$/fd/1);myerr=$(readlink 
/proc/$$/fd/2);myout="${myout/[/\\[}"; 
myout="${myout/]/\\]}";myerr="${myerr/[/\\[}"; myerr="${myerr/]/\\]}";PS=$(ls 
-l /proc/[0-9]*/fd/* 2>/dev/null | sed -nr '\#('"$myout"'|'"$myerr"')# { 
s#^.*/proc/([0-9]+)/.*$#\1#; p}'|sort -u);KILL="";for pid in $PS; do    [ $pid 
-ne $$ ] && [ $pid -ne $PPID ] || continue;    KILL="$KILL $pid";done;[ -z 
"$KILL" ] || kill -9 $KILL >/dev/null 2>&1 || true;exit $RC -- chmod 1777 
  autopkgtest: DBG: got reply from testbed: ok 
  autopkgtest: DBG: TestbedFailure sent `open', got `ok ' (0 result 
parameters), expected 1 result parameters
  autopkgtest: DBG: testbed stop
  autopkgtest: DBG: testbed close, scratch=None
  autopkgtest: DBG: sending command to testbed: quit
  autopkgtest-virt-nspawn: DBG: executing quit
  autopkgtest-virt-nspawn: DBG: cleanup...
  autopkgtest [13:56:31]: ERROR: testbed failure: sent `open', got `ok ' (0 
result parameters), expected 1 result parameters
  autopkgtest: DBG: testbed stop

I know a thing or three about using nspawn containers, but I don't know
much about autopkgtest, and I'd love to team up with someone with the
opposite kind of knowledge to make this work.


Enrico

-- 
GPG key: 4096R/634F4BD1E7AD5568 2009-05-08 Enrico Zini <enr...@enricozini.org>
#!/usr/bin/python3
#
# autopkgtest-virt-nspawn is part of autopkgtest
# autopkgtest is a tool for testing Debian binary packages
#
# autopkgtest is Copyright (C) 2006-2015 Canonical Ltd.
#
# Author: Enrico Zini <enr...@enricozini.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# See the file CREDITS for a full list of credits information (often
# installed as /usr/share/doc/autopkgtest/CREDITS).

import sys
import os
import subprocess
import time
import argparse
import uuid
import signal
import errno
import shlex
from typing import Dict

sys.path.insert(0, '/usr/share/autopkgtest/lib')
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
    os.path.abspath(__file__))), 'lib'))

import VirtSubproc
import adtlog


capabilities = [
    'revert',
    'revert-full-system',
    'root-on-testbed',
]

args = None
container_name = None
normal_user = None
container_properties: Dict[str, str] = {}


def parse_args():
    global args

    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--debug', action='store_true',
                        help='Enable debugging output')
    parser.add_argument('-r', '--gain-root', metavar='COMMAND',
                        help='can become root by prefixing commands with 
COMMAND')
    parser.add_argument('image', help='nspawn ostree name from under 
/var/lib/machines/')
    parser.add_argument('nspawnargs', nargs=argparse.REMAINDER,
                        help='Additional arguments to pass to systemd-nspawn')
    args = parser.parse_args()
    if args.debug:
        adtlog.verbosity = 2


def get_available_container_name():
    '''Return a container name that isn't already taken'''
    return str(uuid.uuid4())


def determine_normal_user():
    '''Check for a normal user to run tests as.'''

    global capabilities, normal_user

    # get the first UID in the Debian Policy ยง9.2.2 "dynamically allocated
    # user account" range
    cmd = []
    if args.gain_root:
        cmd += shlex.split(args.gain_root)
    cmd += ['systemd-run', '--quiet', f'--machine={container_name}', '--', 
'sh', '-c',
            'getent passwd | sort -t: -nk3 | '
            "awk -F: '{if ($3 >= 1000 && $3 <= 59999) { print $1; exit } }'"]
    out = VirtSubproc.execute_timeout(None, 10, cmd,
                                      stdout=subprocess.PIPE)[1].strip()
    if out:
        normal_user = out
        capabilities.append('suggested-normal-user=' + normal_user)
        adtlog.debug('determine_normal_user: got user "%s"' % normal_user)
    else:
        adtlog.debug('determine_normal_user: no uid in [1000,59999] available')


def stop_container():
    """
    Stop the running container
    """
    cmd = []
    if args.gain_root:
        cmd += shlex.split(args.gain_root)
    cmd += ["machinectl", "terminate", container_name]
    VirtSubproc.execute_timeout(None, 300, cmd)
    # We cannot run this code because we are likely not root ourselves.
    # Consider execing ourselves with the gain_root command instead of 
    # prepending it to all systemd-run commands
    #
    # # See https://github.com/systemd/systemd/issues/6458
    # leader_pid = int(container_properties["Leader"])
    # os.kill(leader_pid, signal.SIGRTMIN + 4)
    # while True:
    #     try:
    #         os.kill(leader_pid, 0)
    #     except OSError as e:
    #         if e.errno == errno.ESRCH:
    #             break
    #         raise
    #     time.sleep(0.1)


def hook_open():
    global args, container_name, capabilities

    unit_config = [
        'KillMode=mixed',
        'Type=notify',
        'RestartForceExitStatus=133',
        'SuccessExitStatus=133',
        'Slice=machine.slice',
        'Delegate=yes',
        'TasksMax=16384',
        'WatchdogSec=3min',
    ]

    systemd_run_cmd = []
    if args.gain_root:
        systemd_run_cmd += shlex.split(args.gain_root)

    systemd_run_cmd += ["systemd-run", "--quiet"]
    for c in unit_config:
        systemd_run_cmd.append(f"--property={c}")

    extra_nspawnargs = args.nspawnargs
    adtlog.debug('will start a container (with isolation-container capability)')
    capabilities.append('isolation-container')

    container_name = get_available_container_name()
    adtlog.debug('using container name %s' % container_name)

    ostree = os.path.join("/var/lib/machines", args.image)

    systemd_run_cmd += [
        "systemd-nspawn",
        "--quiet",
        f"--directory={ostree}",
        f"--machine={container_name}",
        "--boot",
        "--notify-ready=yes",
        "--resolv-conf=replace-host",
    ]

    if False:  # TODO: check if /var/lib/machines is on BTRFS
        systemd_run_cmd.append("--ephemeral")
    else:
        systemd_run_cmd.append("--volatile=overlay")
        systemd_run_cmd.append("--read-only")

    # If systemd_version >= 250:
    systemd_run_cmd.append("--suppress-sync=yes")

    systemd_run_cmd.append(f"systemd.hostname={container_name}")

    systemd_run_cmd += extra_nspawnargs

    # This will only return after the container is fully started
    VirtSubproc.check_exec(
        systemd_run_cmd,
        outp=True,
        timeout=600
    )

    # Read machine properties
    res = subprocess.run(
            ["machinectl", "show", container_name],
            capture_output=True, text=True, check=True)
    container_properties = {}
    for line in res.stdout.splitlines():
        key, value = line.split('=', 1)
        container_properties[key] = value

    try:
        adtlog.debug('container started')
        determine_normal_user()
        # provide a minimal and clean environment in the container
        # We also want to avoid exiting with 255 as that's auxverb's exit code
        # if the auxverb itself failed; so we translate that to 253.
        # Tests or builds sometimes leak background processes which might still
        # be connected to lxc exec's stdout/err; we need to kill these after the
        # main program (build or test script) finishes, otherwise we get
        # eternal hangs.
        VirtSubproc.auxverb = []
        if args.gain_root:
            VirtSubproc.auxverb += shlex.split(args.gain_root)

        VirtSubproc.auxverb += [
            'systemd-run', "--quiet", f'--machine={container_name}', '--',
            'env', '-i', 'bash', '-c',
            'set -a; '
            '[ -r /etc/environment ] && . /etc/environment 2>/dev/null || true; 
'
            '[ -r /etc/default/locale ] && . /etc/default/locale 2>/dev/null || 
true; '
            '[ -r /etc/profile ] && . /etc/profile 2>/dev/null || true; '
            'set +a;'
            '"$@"; RC=$?; [ $RC != 255 ] || RC=253; '
            'set -e;'
            'myout=$(readlink /proc/$$/fd/1);'
            'myerr=$(readlink /proc/$$/fd/2);'
            'myout="${myout/[/\\\\[}"; myout="${myout/]/\\\\]}";'
            'myerr="${myerr/[/\\\\[}"; myerr="${myerr/]/\\\\]}";'
            'PS=$(ls -l /proc/[0-9]*/fd/* 2>/dev/null | sed -nr 
\'\\#(\'"$myout"\'|\'"$myerr"\')# { s#^.*/proc/([0-9]+)/.*$#\\1#; p}\'|sort 
-u);'
            'KILL="";'
            'for pid in $PS; do'
            '    [ $pid -ne $$ ] && [ $pid -ne $PPID ] || continue;'
            '    KILL="$KILL $pid";'
            'done;'
            '[ -z "$KILL" ] || kill -9 $KILL >/dev/null 2>&1 || true;'
            'exit $RC', '--'
        ]
    except Exception:
        # Clean up on failure
        stop_container()
        raise


def hook_downtmp(path):
    return VirtSubproc.downtmp_mktemp(path)


def hook_revert():
    hook_cleanup()
    hook_open()


def hook_cleanup():
    VirtSubproc.downtmp_remove()
    stop_container()


def hook_capabilities():
    return capabilities


parse_args()
VirtSubproc.main()

Attachment: signature.asc
Description: PGP signature

Reply via email to