#!/usr/pkg/bin/python3.11
#
# autopkgtest-virt-lxd is part of autopkgtest
# autopkgtest is a tool for testing Debian binary packages
#
# autopkgtest is Copyright (C) 2006-2015 Canonical Ltd.
#
# Author: Martin Pitt <martin.pitt@ubuntu.com>
#
# 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 string
import random
import subprocess
import time
import argparse

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
    os.path.abspath(__file__))), 'lib'))

from reprotest.lib import VirtSubproc
from reprotest.lib import adtlog


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

args = None
container_name = None
normal_user = None


def parse_args():
    global args

    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--debug', action='store_true',
                        help='Enable debugging output')
    parser.add_argument('-r', '--remote', default='',
                        help='Run container on given remote host instead of '
                        'locally; see "lxc remote list"')
    parser.add_argument('image', help='LXD image name')
    parser.add_argument('lxcargs', nargs=argparse.REMAINDER,
                        help='Additional arguments to pass to lxc launch ')
    args = parser.parse_args()
    if args.debug:
        adtlog.verbosity = 2
    if args.remote and not args.remote.endswith(':'):
        args.remote += ':'


def get_available_container_name():
    '''Return a container name that isn't already taken'''

    while True:
        # generate random container name
        rnd = [random.choice(string.ascii_lowercase) for i in range(6)]
        candidate = 'autopkgtest-lxd-' + ''.join(rnd)

        rc = VirtSubproc.execute_timeout(None, 10, ['lxc', 'info', candidate],
                                         stdout=subprocess.DEVNULL,
                                         stderr=subprocess.STDOUT)[0]
        if rc != 0:
            return candidate


def wait_booted():
    '''Wait until the container has sufficiently booted to interact with it

    Do this by checking that the runlevel is someting numeric, i. e. not
    "unknown" or "S".
    '''
    timeout = 60
    while timeout > 0:
        timeout -= 1
        time.sleep(1)
        (rc, out, _) = VirtSubproc.execute_timeout(
            None, 10, ['lxc', 'exec', container_name, 'runlevel'],
            stdout=subprocess.PIPE)
        if rc != 0:
            adtlog.debug('wait_booted: lxc exec failed, retrying...')
            continue
        out = out.strip()
        if out.split()[-1].isdigit():
            return

        adtlog.debug('wait_booted: runlevel "%s", retrying...' % out)

    VirtSubproc.bomb('timed out waiting for container %s to start; '
                     'last runlevel "%s"' % (container_name, out))


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

    global capabilities, normal_user

    # get the first UID >= 500
    cmd = ['lxc', 'exec', container_name, '--', 'sh', '-c',
           'getent passwd | sort -t: -nk3 | '
           "awk -F: '{if ($3 >= 500) { 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 >= 500 available')


def hook_open():
    global args, container_name

    container_name = args.remote + get_available_container_name()
    adtlog.debug('using container name %s' % container_name)
    VirtSubproc.check_exec(['lxc', 'launch', '--ephemeral', args.image, container_name] + args.lxcargs,
                           outp=True, timeout=600)
    try:
        adtlog.debug('waiting for container start')
        wait_booted()
        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 = [
            'lxc', 'exec', 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:
        # Clean up on failure
        VirtSubproc.execute_timeout(None, 300, ['lxc', 'delete', '--force', container_name])
        raise


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


def hook_revert():
    hook_cleanup()
    hook_open()


def get_uptime():
    try:
        (rc, out, _) = VirtSubproc.execute_timeout(
            None, 10, ['lxc', 'exec', container_name, '--', 'cat', '/proc/uptime'],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        if rc != 0:
            return

        return float(out.split()[0])
    except IndexError:
        return


def hook_wait_reboot():
    adtlog.debug('hook_wait_reboot: waiting for container to shut down...')
    # "lxc exec" exits with 0 when the container stops, so just wait longer
    # than our timeout
    initial_uptime = get_uptime()

    adtlog.debug('hook_wait_reboot: container up for %s, waiting for reboot' % initial_uptime)

    for retry in range(20):
        time.sleep(5)

        current_uptime = get_uptime()

        # container is probably in the very late stages of shutting down, just
        # keep trying, if this persists we'll bomb out later on
        if not current_uptime:
            continue

        if current_uptime < initial_uptime:
            adtlog.debug('hook_wait_reboot: container now up for %s - has rebooted (initial uptime %s)' % (current_uptime, initial_uptime))
            break
        else:
            adtlog.debug('hook_wait_reboot: container now up for %s - has not rebooted (initial uptime %s)' % (current_uptime, initial_uptime))
    else:
        VirtSubproc.bomb('timed out waiting for container %s to restart' % container_name)

    adtlog.debug('hook_wait_reboot: container restarted, waiting for boot to finish')
    wait_booted()


def hook_cleanup():
    VirtSubproc.downtmp_remove()
    VirtSubproc.check_exec(['lxc', 'delete', '--force', container_name], timeout=600)


def hook_capabilities():
    return capabilities


parse_args()
VirtSubproc.main()
