# -*- coding: utf-8 -*-
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:textwidth=0:
# License: GPL2 or later see COPYING
# Written by Michael Brown
# Sections by Seth Vidal
# Sections taken from Mach by Thomas Vander Stichele
# Copyright (C) 2007 Michael E Brown <mebrown@michaels-house.net>
from __future__ import print_function

import atexit
import contextlib
import ctypes
import errno
import fcntl
from glob import glob
import logging
import os
import os.path
import re
import select
import signal
import shlex
import shutil
import struct
import subprocess
import sys
import tempfile
# pylint: disable=wrong-import-order
import termios
from textwrap import dedent
import time
import uuid

import distro

from . import exception
from . import file_util
from . import text
from .constants import DEFAULT_UMASK
from .trace_decorator import getLog, traceLog
from .uid import setresuid
from pyroute2 import IPRoute

_libc = ctypes.cdll.LoadLibrary(None)
_libc.personality.argtypes = [ctypes.c_ulong]
_libc.personality.restype = ctypes.c_int
_libc.unshare.argtypes = [ctypes.c_int]
_libc.unshare.restype = ctypes.c_int
_libc.sethostname.argtypes = [ctypes.c_char_p, ctypes.c_int]
_libc.sethostname.restype = ctypes.c_int

# See linux/include/sched.h
CLONE_NEWNS = 0x00020000
CLONE_NEWUTS = 0x04000000
CLONE_NEWPID = 0x20000000
CLONE_NEWNET = 0x40000000
CLONE_NEWIPC = 0x08000000

# taken from sys/personality.h
PER_LINUX32 = 0x0008
PER_LINUX = 0x0000
personality_defs = {
    'x86_64': PER_LINUX, 'ppc64': PER_LINUX, 'sparc64': PER_LINUX,
    'i386': PER_LINUX32, 'i586': PER_LINUX32, 'i686': PER_LINUX32,
    'armv7': PER_LINUX32, 'armv7l': PER_LINUX32, 'armv7hl': PER_LINUX32,
    'armv7hnl': PER_LINUX32, 'armv7hcnl': PER_LINUX32,
    'armv7b': PER_LINUX32, 'armv7hb': PER_LINUX32,
    'armv7hnb': PER_LINUX32, 'armv7hcnb': PER_LINUX32,
    'armv8': PER_LINUX32, 'armv8l': PER_LINUX32, 'armv8hl': PER_LINUX32,
    'armv8hnl': PER_LINUX32, 'armv8hcnl': PER_LINUX32,
    'armv8b': PER_LINUX32, 'armv8hb': PER_LINUX32,
    'armv8hnb': PER_LINUX32, 'armv8hcnb': PER_LINUX32,
    'ppc': PER_LINUX32, 'sparc': PER_LINUX32, 'sparcv9': PER_LINUX32,
    'ia64': PER_LINUX, 'alpha': PER_LINUX,
    's390': PER_LINUX32, 's390x': PER_LINUX,
    'mips': PER_LINUX32, 'mipsel': PER_LINUX32,
    'mipsr6': PER_LINUX32, 'mipsr6el': PER_LINUX32,
    'mips64': PER_LINUX, 'mips64el': PER_LINUX,
    'mips64r6': PER_LINUX, 'mips64r6el': PER_LINUX,
    'riscv64': PER_LINUX, 'riscv32': PER_LINUX32,
}

USE_NSPAWN = False
USE_NSPAWN_SECCOMP = False

_NSPAWN_HELP_OUTPUT = None

RHEL_CLONES = ['centos', 'deskos', 'ol', 'rhel', 'scientific']

_OPS_TIMEOUT = 0


def cmd_pretty(cmd, env=None):
    if isinstance(cmd, list):
        return ' '.join(shlex.quote(arg) for arg in cmd)
    if env:
        variables = [f"{k}={shlex.quote(v)}" for k, v in env.items()]
        cmd = " ".join(variables) + " " + cmd
    return cmd


@traceLog()
def get_proxy_environment(config):
    env = {}
    for proto in ('http', 'https', 'ftp', 'no'):
        key = '%s_proxy' % proto
        value = config.get(key)
        if value:
            env[key] = value
        elif os.getenv(key):
            env[key] = os.getenv(key)
    return env


def _safe_check_output(*args):
    # this can be done in one call in python3, but python2 requires this hack
    try:
        output = subprocess.check_output(*args, shell=False, stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
        output = str(e.output)
    return output


@traceLog()
def get_machinectl_uuid(chroot_path):
    """ Get UUID from machinectl. This function does not check if NSPAWN is used """
    # we will ignore errors in machinectl, it sometimes fails for various errors (cannot find IP addr...)
    # we do not care about exit code, we just want the output
    vm_list = _safe_check_output(["/bin/machinectl", "list", "--no-legend", "--no-pager"])
    if (isinstance(vm_list, bytes)):
        vm_list = vm_list.decode("utf-8")
    for name in vm_list.split("\n"):
        if len(name) > 0:
            m_uuid = name.split()[0]
            try:
                vm_root = _safe_check_output(["/bin/machinectl", "show", "-pRootDirectory", m_uuid])
                if (isinstance(vm_root, bytes)):
                    vm_root = vm_root.decode("utf-8")
            except subprocess.CalledProcessError:
                continue
            vm_root = '='.join(vm_root.rstrip().split('=')[1:])
            if vm_root == chroot_path:
                return m_uuid
    # we should never get here
    return None


def compare_two_paths_cached(path1, path2, path_cache):
    """ compare two files on dev/ino pairs """
    def file_dev_ino(path):
        """ Return dev/ino pair for path, and cache results """
        if path in path_cache:
            return path_cache[path]
        stat_val = os.stat(os.path.realpath(path))
        ret = path_cache[path] = stat_val.st_dev, stat_val.st_ino
        return ret
    return file_dev_ino(path1) == file_dev_ino(path2)


def get_pid_cmdline(pid):
    """
    For given PID return the command-line arguments from /proc/PID/cmdline
    """
    cmd_file = f"/proc/{pid}/cmdline"
    try:
        with open(cmd_file, "rb") as cmdline_file:
            cmdline = cmdline_file.read().decode('utf-8').split('\0')
            cmdline.pop()  # last string is always empty in 0-terminated file
            return ' '.join([shlex.quote(x) for x in cmdline])
    except OSError as e:
        return f"ERROR: Can not read {pid} file {e}"


@traceLog()
def orphansKill(rootToKill, manual_forced=False):
    """
    Kill off anything that is still chrooted.

    When USE_NSPAWN==False, this method manually detects the running processes
    in chroot by reading the /proc file-system.  When USE_NSPAWN==True, it just
    relies on '/bin/machinectl terminate' call.

    When manual_forced==True, the manual kill based on /proc is enforced.
    """
    getLog().debug("kill orphans in chroot %s", rootToKill)
    if USE_NSPAWN is False or manual_forced:
        path_cache = {}
        for killsig in [signal.SIGTERM, signal.SIGKILL]:
            for fn in [d for d in os.listdir("/proc") if d.isdigit()]:
                try:
                    root = os.readlink("/proc/%s/root" % fn)
                    if compare_two_paths_cached(root, rootToKill, path_cache):
                        pid = int(fn, 10)
                        getLog().warning("Leftover process %s is being killed with signal %s: %s",
                                         pid, killsig, get_pid_cmdline(pid))
                        os.kill(pid, killsig)
                        os.waitpid(pid, 0)
                except OSError:
                    pass
    else:
        m_uuid = get_machinectl_uuid(rootToKill)
        if m_uuid:
            getLog().warning("Machine %s still running. Killing...", m_uuid)
            os.system("/bin/machinectl terminate %s" % m_uuid)


@traceLog()
def yieldSrpmHeaders(srpms, plainRpmOk=0):
    # pylint: disable=import-outside-toplevel
    import rpm
    ts = rpm.TransactionSet('/')
    # When RPM > 4.14.90 is common we can use RPMVSF_MASK_NOSIGNATURES, RPMVSF_MASK_NODIGESTS
    # pylint: disable=protected-access
    flags = (rpm._RPMVSF_NOSIGNATURES | rpm._RPMVSF_NODIGESTS)
    ts.setVSFlags(flags)
    for srpm in srpms:
        srpm = host_file(srpm)
        try:
            fd = os.open(srpm, os.O_RDONLY)
        except OSError as e:
            raise exception.Error("Cannot find/open srpm: %s. Error: %s"
                                  % (srpm, e))
        try:
            hdr = ts.hdrFromFdno(fd)
        except rpm.error as e:
            raise exception.Error(
                "Cannot find/open srpm: %s. Error: %s" % (srpm, e))
        finally:
            os.close(fd)

        if not plainRpmOk and hdr[rpm.RPMTAG_SOURCEPACKAGE] != 1:
            raise exception.Error("File is not an srpm: %s." % srpm)

        yield hdr


@traceLog()
def checkSrpmHeaders(srpms, plainRpmOk=0):
    for dummy in yieldSrpmHeaders(srpms, plainRpmOk):
        pass


@traceLog()
def getNEVRA(hdr):
    # pylint: disable=import-outside-toplevel
    import rpm
    name = hdr[rpm.RPMTAG_NAME]
    ver = hdr[rpm.RPMTAG_VERSION]
    rel = hdr[rpm.RPMTAG_RELEASE]
    epoch = hdr[rpm.RPMTAG_EPOCH]
    arch = hdr[rpm.RPMTAG_ARCH]
    if epoch is None:
        epoch = 0
    ret = (name, epoch, ver, rel, arch)
    return tuple(text._to_text(x) if i != 1 else x for i, x in enumerate(ret))


@traceLog()
def cmpKernelVer(str1, str2):
    'compare two kernel version strings and return -1, 0, 1 for less, equal, greater'
    # pylint: disable=import-outside-toplevel
    import rpm
    return rpm.labelCompare(('', str1, ''), ('', str2, ''))


@traceLog()
def getAddtlReqs(hdr, conf):
    # Add the 'more_buildreqs' for this SRPM (if defined in config file)
    # pylint: disable=unused-variable
    (name, epoch, ver, rel, arch) = getNEVRA(hdr)
    reqlist = []
    for this_srpm in ['-'.join([name, ver, rel]),
                      '-'.join([name, ver]),
                      '-'.join([name])]:
        if this_srpm in conf:
            more_reqs = conf[this_srpm]
            if isinstance(more_reqs, str):
                reqlist.append(more_reqs)
            else:
                reqlist.extend(more_reqs)
            break

    return set(reqlist)


@traceLog()
def unshare(flags):
    #getLog().debug("Unsharing. Flags: %s", flags)
    try:
        res = _libc.unshare(flags)
        if res:
            raise exception.UnshareFailed(os.strerror(ctypes.get_errno()))
    except AttributeError:
        pass


def sethostname(hostname):
    getLog().info("Setting hostname: %s", hostname)
    hostname = hostname.encode('utf-8')
    if _libc.sethostname(hostname, len(hostname)) != 0:
        raise OSError('Failed to sethostname %s' % hostname)


# these are called in child process, so no logging
def condChroot(chrootPath):
    if chrootPath is not None:
        saved = {"ruid": os.getuid(), "euid": os.geteuid()}
        setresuid(0, 0, 0)
        os.chdir(chrootPath)
        os.chroot(chrootPath)
        setresuid(saved['ruid'], saved['euid'])


def condChdir(cwd):
    if cwd is not None:
        os.chdir(cwd)


def condDropPrivs(uid, gid):
    if gid is not None:
        os.setregid(gid, gid)
    if uid is not None:
        os.setreuid(uid, uid)


def condPersonality(per=None):
    if per is None or per in ('noarch',):
        return
    if personality_defs.get(per, None) is None:
        return
    res = _libc.personality(personality_defs[per])
    if res == -1:
        raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))


def condEnvironment(env=None):
    if not env:
        return
    os.environ.clear()
    for k in list(env.keys()):
        os.putenv(k, env[k])


def condUnshareIPC(unshare_ipc=True):
    if unshare_ipc:
        try:
            unshare(CLONE_NEWIPC)
        except exception.UnshareFailed:
            # IPC and UTS ns are supported since the same kernel version. If this
            # fails, there had to be a warning already
            pass


def condUnshareNet(unshare_net=True):
    if USE_NSPAWN and unshare_net:
        try:
            unshare(CLONE_NEWNET)
            # Set up loopback interface and add default route via loopback in the namespace.
            # Missing default route may confuse some software, see
            # https://github.com/rpm-software-management/mock/issues/113
            ipr = IPRoute()
            dev = ipr.link_lookup(ifname='lo')[0]

            ipr.link('set', index=dev, state='up')
            ipr.route("add", dst="default", gateway="127.0.0.1")
        except exception.UnshareFailed:
            # IPC and UTS ns are supported since the same kernel version. If this
            # fails, there had to be a warning already
            pass
        except Exception as e: # pylint: disable=broad-except
            getLog().warning("network namespace setup failed: %s", e)


def process_input(line):
    out = []
    for char in line.rstrip('\r'):
        if char == '\r':
            out = []
        elif char == '\b':
            out.pop()
        else:
            out.append(char)
    return ''.join(out)


def logOutput(fdout, fderr, logger, returnOutput=1, start=0, timeout=0, printOutput=False,
              child=None, chrootPath=None, pty=False, returnStderr=True):
    output = ""
    done = False
    fds = [fdout, fderr]

    # set all fds to nonblocking
    for fd in fds:
        flags = fcntl.fcntl(fd, fcntl.F_GETFL)
        if not fd.closed:
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

    mockbuild_logger = logging.getLogger('mockbuild')
    stored_propagate = mockbuild_logger.propagate
    if printOutput:
        # prevent output being printed twice when log propagates to stdout
        mockbuild_logger.propagate = 0
        sys.stdout.flush()
    try:
        tail = ""
        ansi_escape = re.compile(r'\x1b\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]\x0f?')
        while not done:
            if (time.time() - start) > timeout and timeout != 0:
                done = True
                break

            i_rdy, o_rdy, e_rdy = select.select(fds, [], [], 1)

            if not i_rdy and not o_rdy and not e_rdy:
                if child and child.poll() is not None:
                    logger.info("Child pid '%s' is dead", child.pid)
                    done = True
                    if chrootPath:
                        logger.info("Child dead, killing orphans")
                        orphansKill(chrootPath)
                    continue

            for s in i_rdy:
                # slurp as much input as is ready
                raw = s.read()
                if not raw:
                    done = True
                    break
                if printOutput:
                    if hasattr(sys.stdout, 'buffer'):
                        # python3 would print binary strings ugly
                        # pylint: disable=no-member
                        sys.stdout.buffer.write(raw)
                    else:
                        print(raw, end='')
                    sys.stdout.flush()

                if returnStderr is False and s == fderr:
                    continue

                txt_input = raw.decode(text.encoding, 'replace')
                lines = txt_input.split("\n")
                if tail:
                    lines[0] = tail + lines[0]
                # we may not have all of the last line
                tail = lines.pop()
                if not lines:
                    continue
                if pty:
                    lines = [process_input(line) for line in lines]
                processed_input = '\n'.join(lines) + '\n'
                if "mock_stderr_line_prefix" in dir(mockbuild_logger):
                    mock_stderr_line_prefix = mockbuild_logger.mock_stderr_line_prefix
                else:
                    mock_stderr_line_prefix = ""
                if logger is not None:
                    for line in lines:
                        if line != '':
                            line = ansi_escape.sub('', line)
                            if fderr is s and not line.startswith('+ '):
                                logger.debug("%s%s", mock_stderr_line_prefix, line)
                            else:
                                logger.debug(line)
                    for h in logger.handlers:
                        h.flush()
                if returnOutput:
                    output += processed_input

        if tail:
            if pty:
                tail = process_input(tail) + '\n'
            if logger is not None:
                logger.debug(tail)
            if returnOutput:
                output += tail
    finally:
        mockbuild_logger.propagate = stored_propagate

    return output


@traceLog()
def selinuxEnabled():
    """Check if SELinux is enabled (enforcing or permissive)."""
    with open("/proc/mounts") as f:
        for mount in f.readlines():
            (fstype, mountpoint, _) = mount.split(None, 2)
            if fstype == "selinuxfs":
                selinux_mountpoint = mountpoint
                break
        else:
            selinux_mountpoint = "/selinux"

    try:
        enforce_filename = os.path.join(selinux_mountpoint, "enforce")
        with open(enforce_filename) as f:
            if f.read().strip() in ("1", "0"):
                return True
    # pylint: disable=bare-except
    except:
        pass
    return False


def resize_pty(pty):
    try:
        winsize = struct.pack('HHHH', 0, 0, 0, 0)
        winsize = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, winsize)
        fcntl.ioctl(pty, termios.TIOCSWINSZ, winsize)
    except IOError:
        # Nice to have, but not necessary
        pass


def do(*args, **kargs):
    """ returns output of the command. Arguments are the same as for do_with_status() """
    return do_with_status(*args, **kargs)[0]

# logger =
# output = [1|0]
# chrootPath
#
# The "Not-as-complicated" version
#
@traceLog()
# pylint: disable=unused-argument
def do_with_status(command, shell=False, chrootPath=None, cwd=None, timeout=0, raiseExc=True,
                   returnOutput=0, uid=None, gid=None, user=None, personality=None,
                   printOutput=False, env=None, pty=False, nspawn_args=None, unshare_net=False,
                   returnStderr=True, *_, **kargs):
    logger = kargs.get("logger", getLog())
    if timeout == 0:
        timeout = _OPS_TIMEOUT
    output = ""
    start = time.time()
    if pty:
        lead_pty, sub_pty = os.openpty()
        resize_pty(sub_pty)
        reader = os.fdopen(lead_pty, 'rb')
    preexec = ChildPreExec(personality, chrootPath, cwd, uid, gid,
                           unshare_ipc=bool(chrootPath), unshare_net=unshare_net)
    if env is None:
        env = clean_env()
    stdout = None

    if isinstance(command, list):
        # convert int args to strings
        command = [str(x) for x in command]

    try:
        child = None
        if chrootPath and USE_NSPAWN:
            logger.debug("Using nspawn with args %s", nspawn_args)
            command = _prepare_nspawn_command(chrootPath, user, command,
                                              nspawn_args=nspawn_args,
                                              env=env, cwd=cwd, shell=shell)
            shell = False
        logger.debug("Executing command: %s with env %s and shell %s", command, env, shell)
        with open(os.devnull, "r") as stdin:
            child = subprocess.Popen(
                command,
                shell=shell,
                env=env,
                bufsize=0, close_fds=True,
                stdin=stdin,
                stdout=sub_pty if pty else subprocess.PIPE,
                stderr=subprocess.PIPE,
                preexec_fn=preexec,
            )
            if not pty:
                stdout = child.stdout
            with child.stderr:
                # use select() to poll for output so we dont block
                output = logOutput(
                    reader if pty else child.stdout, child.stderr,
                    logger, returnOutput, start, timeout, pty=pty,
                    printOutput=printOutput, child=child,
                    chrootPath=chrootPath, returnStderr=returnStderr)
    except:
        # kill children if they arent done
        if child is not None and child.returncode is None:
            os.killpg(child.pid, 9)
        try:
            if child is not None:
                os.waitpid(child.pid, 0)
        except: # pylint: disable=bare-except
            pass
        raise
    finally:
        if pty:
            os.close(sub_pty)
            reader.close()
        if stdout:
            stdout.close()

    # wait until child is done, kill it if it passes timeout
    niceExit = 1
    while child.poll() is None:
        if (time.time() - start) > timeout and timeout != 0:
            niceExit = 0
            os.killpg(child.pid, 15)
        if (time.time() - start) > (timeout + 1) and timeout != 0:
            niceExit = 0
            os.killpg(child.pid, 9)

    if not niceExit:
        raise exception.commandTimeoutExpired("Timeout(%s) expired for command:\n # %s\n%s" %
                                              (timeout, cmd_pretty(command, env), output))

    logger.debug("Child return code was: %s", child.returncode)
    if raiseExc and child.returncode:
        raise exception.Error("Command failed: \n # %s\n%s" % (cmd_pretty(command, env), output), child.returncode)

    return (output, child.returncode)


class ChildPreExec(object):
    def __init__(self, personality, chrootPath, cwd, uid, gid, env=None,
                 shell=False, unshare_ipc=False, unshare_net=False,
                 no_setsid=False):
        """
        Params:
        - no_setsid - assure we don't call os.setsid(), as the process we run
            calls that itself
        """
        self.personality = personality
        self.chrootPath = chrootPath
        self.cwd = cwd
        self.uid = uid
        self.gid = gid
        self.env = env
        self.shell = shell
        self.unshare_ipc = unshare_ipc
        self.unshare_net = unshare_net
        self.no_setsid = no_setsid
        getLog().debug("child environment: %s", env)

    def __call__(self, *args, **kargs):
        if not self.shell and not self.no_setsid:
            os.setsid()
        os.umask(DEFAULT_UMASK)
        condUnshareNet(self.unshare_net)
        condPersonality(self.personality)
        condEnvironment(self.env)
        # Even if nspawn is allowed to be used, it won't be used unless there
        # is a chrootPath set
        if not USE_NSPAWN or not self.chrootPath:
            condChroot(self.chrootPath)
            condDropPrivs(self.uid, self.gid)
            condChdir(self.cwd)
        condUnshareIPC(self.unshare_ipc)
        reset_sigpipe()


def setup_operations_timeout(config_opts):
    global _OPS_TIMEOUT
    _OPS_TIMEOUT = config_opts.get('opstimeout', 0)


def set_use_nspawn(value, config_opts):
    global USE_NSPAWN
    global USE_NSPAWN_SECCOMP
    USE_NSPAWN = value
    USE_NSPAWN_SECCOMP = config_opts["seccomp"]


class BindMountedFile(str):
    'see host_file() doc'
    def __new__(cls, value, on_host=None):
        the_string = str.__new__(cls, value)
        the_string.on_host = on_host if on_host else value
        return the_string


def host_file(file):
    """
    Some functions accept arguments which may be either str() or
    BindMountedFile();  we use this helper to work with those transparently.
    TODO: all the code parts which need this should be fixed so they
    are executed _inside_ bootstrap chroot, not on host.
    """
    return file.on_host if hasattr(file, 'on_host') else file


def reset_sigpipe():
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)


def _nspawnTempResolvAtExit(path):
    """Remove nspawn temporary resolv.conf from host."""
    try:
        os.remove(path)
    except OSError as e:
        if e.errno not in [errno.ENOENT, errno.EPERM]:
            getLog().warning("unable to delete temporary resolv.conf (%s): %s", path, e)


def systemd_nspawn_help_output():
    """ Get (cached, so we don't re-run) systemd-nspawn --help output. """
    global _NSPAWN_HELP_OUTPUT  # pylint: disable=global-statement
    if _NSPAWN_HELP_OUTPUT is not None:
        return _NSPAWN_HELP_OUTPUT

    _NSPAWN_HELP_OUTPUT = subprocess.check_output(
        'systemd-nspawn --help || true',
        shell=True)
    _NSPAWN_HELP_OUTPUT = _NSPAWN_HELP_OUTPUT.decode('utf-8', errors='ignore')
    return _NSPAWN_HELP_OUTPUT


def _check_nspawn_pipe_option():
    """
    Detect whether host's systemd-nspawn supports --pipe argument and if we can
    use it for non-interactive commands.  Before --pipe was implemented in
    nspawn the default behavior was to detect tty => and use 'interactive' vs.
    'pipe'.  Later the default was changed to 'interactive' vs. 'read-only'
    (systemd commit de40a3037).
    """
    output = systemd_nspawn_help_output()
    return '--pipe' in output and '--console' in output


def _check_nspawn_resolv_conf():
    """
    Detect that --resolv-conf= option is supported in systemd-nspawn, and if
    yes - switch the default value 'auto' to 'off' so nspawn doesn't override
    our pre-generated resolv.conf file.
    """
    return '--resolv-conf' in systemd_nspawn_help_output()


def check_nspawn_has_chdir_option():
    """
    Older systemd-nspawn versions don't have --chdir option, and sometimes we
    need to know we work with such version.
    """
    return '--chdir' in systemd_nspawn_help_output()

def check_nspawn_has_suppress_sync_option():
    """
    Older systemd-nspawn versions don't have --suppress-sync option, and sometimes we
    need to know we work with such version.
    This is here becase RHEL 8 and older. When systemd 250 is everywhere we can
    remove this.
    """
    return '--suppress-sync' in systemd_nspawn_help_output()

def _prepare_nspawn_command(chrootPath, user, cmd, nspawn_args=None, env=None,
                            cwd=None, interactive=False, shell=False):
    nspawn_argv = ['/usr/bin/systemd-nspawn', '-q', '-M', uuid.uuid4().hex, '-D', chrootPath]
    distro_label = distro.id()
    try:
        distro_version = float(distro.version() or 0)
    except ValueError:
        distro_version = 0
    if distro_label not in RHEL_CLONES or distro_version >= 7.5:
        # EL < 7.5 does not support the nspawn -a option. See BZ 1417387
        nspawn_argv += ['-a']

    if user:
        # user can be either id or name
        nspawn_argv += ['-u', str(user)]

    if nspawn_args:
        nspawn_argv.extend(nspawn_args)

    if _check_nspawn_pipe_option():
        if not interactive or not (sys.stdin.isatty() and sys.stdout.isatty()):
            nspawn_argv += ['--console=pipe']

    if cwd:
        nspawn_argv.append('--chdir={0}'.format(cwd))

    assert env is not None

    # Those variables are expected to be set _inside_ the container
    for k, v in env.items():
        nspawn_argv.append('--setenv={0}={1}'.format(k, v))

    # And these need to be set outside the container (processed by nspawn)
    env['SYSTEMD_NSPAWN_TMPFS_TMP'] = '0'
    if not USE_NSPAWN_SECCOMP:
        env['SYSTEMD_SECCOMP'] = '0'

    if _check_nspawn_resolv_conf():
        nspawn_argv.append("--resolv-conf=off")

    # The '/bin/sh -c' wrapper is explicitly requested (--shell).  In this case
    # we shrink the list of arguments into one shell command, so the command is
    # completely shell-expanded.
    if shell and isinstance(cmd, list):
        cmd = ' '.join(cmd)

    # HACK!  No matter if --shell/--chroot is used, we have documented that we
    # shell-expand the CMD if there are no ARGS.  This is historical
    # requirement that other people probably depend on.
    if isinstance(cmd, str):
        cmd = ['/bin/sh', '-c', cmd]

    return nspawn_argv + cmd

def doshell(chrootPath=None, environ=None, uid=None, gid=None, cmd=None,
            cwd=None,
            nspawn_args=None,
            unshare_ipc=True,
            unshare_net=False):
    log = getLog()
    log.debug("doshell: chrootPath:%s, uid:%d, gid:%d", chrootPath, uid, gid)
    if environ is None:
        environ = clean_env()
    if 'PROMPT_COMMAND' not in environ:
        environ['PROMPT_COMMAND'] = r'printf "\033]0;<mock-chroot>\007"'
    if 'PS1' not in environ:
        environ['PS1'] = r'<mock-chroot> \s-\v\$ '
    if 'SHELL' not in environ:
        environ['SHELL'] = '/bin/sh'
    log.debug("doshell environment: %s", environ)

    no_setsid = False
    shell = True
    if not cmd:
        cmd = ["/bin/sh", "-i", "-l"]
        shell = False
        no_setsid = True
    elif isinstance(cmd, list):
        cmd = ' '.join(cmd)

    preexec = ChildPreExec(personality=None, chrootPath=chrootPath, cwd=cwd,
                           uid=uid, gid=gid, env=environ, shell=shell,
                           unshare_ipc=unshare_ipc, unshare_net=unshare_net,
                           no_setsid=no_setsid)

    if USE_NSPAWN:
        # nspawn cannot set gid
        log.debug("Using nspawn with args %s", nspawn_args)
        cmd = _prepare_nspawn_command(chrootPath, uid, cmd, nspawn_args=nspawn_args, env=environ,
                                      interactive=True, cwd=cwd)
        shell = False

    log.debug("doshell: command: %s", cmd_pretty(cmd))
    return subprocess.call(cmd, preexec_fn=preexec, env=environ, shell=shell)


def run(cmd, isShell=True):
    log = getLog()
    log.debug("run: cmd = %s", cmd_pretty(cmd))
    return subprocess.call(cmd, shell=isShell)


def clean_env():
    return {
        'TERM': 'vt100',
        'SHELL': '/bin/sh',
        'HOME': '/builddir',
        'HOSTNAME': 'mock',
        'PATH': '/usr/bin:/bin:/usr/sbin:/sbin',
        'LANG': 'C.UTF-8',
    }


@traceLog()
def setup_host_resolv(config_opts):
    log = getLog()
    if not config_opts['use_host_resolv']:
        # If we don't copy host's resolv.conf, we at least want to resolve
        # our own hostname.  See commit 28027fc26d.
        if 'etc/hosts' not in config_opts['files']:
            config_opts['files']['etc/hosts'] = dedent('''\
                127.0.0.1 localhost localhost.localdomain
                ::1       localhost localhost.localdomain localhost6 localhost6.localdomain6
                ''')

    if not USE_NSPAWN:
        # Not using nspawn -> don't touch /etc/resolv.conf; we already have
        # a valid file prepared by Buildroot._init() (if user requested).
        return

    if config_opts['rpmbuild_networking'] and not config_opts['use_host_resolv']:
        # keep the default systemd-nspawn's /etc/resolv.conf
        return

    # Either we want to have empty resolv.conf to speedup name resolution
    # failure (rpmbuild_networking is off, see commit 3f939785bb), or we want
    # to copy hosts resolv.conf file.

    resolv_path = (tempfile.mkstemp(prefix="mock-resolv."))[1]
    atexit.register(_nspawnTempResolvAtExit, resolv_path)

    # make sure that anyone in container can read resolv.conf file
    os.chmod(resolv_path, 0o644)

    if config_opts['use_host_resolv']:
        try:
            shutil.copyfile('/etc/resolv.conf', resolv_path)
        except FileNotFoundError:
            log.warning("Non-existing resolv.conf on host, using an empty one")

    config_opts['nspawn_args'] += ['--bind={0}:/etc/resolv.conf'.format(resolv_path)]


def pretty_getcwd():
    try:
        return os.getcwd()
    except OSError:
        if ORIGINAL_CWD is not None:
            return ORIGINAL_CWD
        else:
            return file_util.find_non_nfs_dir()


ORIGINAL_CWD = None
ORIGINAL_CWD = pretty_getcwd()


@traceLog()
def find_btrfs_in_chroot(mockdir, chroot_path):
    """
    Find a btrfs subvolume inside the chroot.

    Example btrfs output:
    ID 258 gen 32689 top level 5 path root
    ID 493 gen 32682 top level 258 path var/lib/mock/fedora-rawhide-x86_64/root/var/lib/machines

    The subvolume's path will always be the 9th field of the output and
    will not contain a leading '/'. The output will also contain additional
    newline at the end, which should not be parsed.
    """

    try:
        output = do(["btrfs", "subv", "list", mockdir], returnOutput=1, printOutput=False)
    except OSError as e:
        # btrfs utility does not exist, nothing we can do about it
        if e.errno == errno.ENOENT:
            return None
        raise e
    except Exception as e: # pylint: disable=broad-except
        # it is not btrfs volume
        log = getLog()
        log.debug("Please ignore the error above about btrfs.")
        return None

    for l in output[:-1].splitlines():
        subv = l.split()[8]
        if subv.startswith(chroot_path[1:]):
            return subv
    return None


@traceLog()
def createrepo(config_opts, path):
    """ Create repository in given path. """
    cmd = shlex.split(config_opts["createrepo_command"])
    if os.path.exists(os.path.join(path, 'repodata/repomd.xml')):
        cmd.append('--update')
    cmd.append(path)
    return do(cmd)


REPOS_ID = []


@traceLog()
def generate_repo_id(baseurl):
    """ generate repository id for yum.conf out of baseurl """
    repoid = baseurl

    # drop proto:// suffix
    proto_split = baseurl.split('://')
    if len(proto_split) > 1:
        repoid = "/".join(proto_split[1:])
    else:
        repoid = baseurl

    repoid = repoid.replace('/', '_')
    repoid = re.sub(r'[^a-zA-Z0-9_]', '', repoid)
    suffix = ''
    i = 1
    while repoid + suffix in REPOS_ID:
        suffix = str(i)
        i += 1
    repoid = repoid + suffix
    REPOS_ID.append(repoid)
    return repoid


@traceLog()
def add_local_repo(config_opts, baseurl, repoid=None, bootstrap=None):
    if not repoid:
        repoid = generate_repo_id(baseurl)
    else:
        REPOS_ID.append(repoid)
    localyumrepo = """

[{repoid}]
name={baseurl}
baseurl={baseurl}
enabled=1
skip_if_unavailable=0
metadata_expire=0
gpgcheck=0
cost=1
best=1
module_hotfixes=true
""".format(repoid=repoid, baseurl=baseurl)

    def _fix_cfg(cfg):
        cfg['dnf.conf'] += localyumrepo

    _fix_cfg(config_opts)

    if bootstrap is None:
        return

    _fix_cfg(bootstrap.config)


def subscription_redhat_init(opts, uidManager):
    if not opts['redhat_subscription_required']:
        return

    if 'redhat_subscription_key_id' in opts:
        return

    ent_path = '/etc/pki/entitlement'
    if not os.path.isdir(ent_path):
        raise exception.ConfigError(ent_path + " is not a directory "
                                    "is subscription-manager installed?")

    key_pattern = os.path.join(ent_path, "*-key.pem")
    keys = glob(key_pattern)
    if not keys:
        hostdir = "/etc/pki/entitlement-host"
        if os.path.isdir(hostdir):
            # running in a Podman container
            host_keys = glob(os.path.join(hostdir, "*-key.pem"))
            if host_keys:
                with uidManager.elevated_privileges():
                    for file in glob(os.path.join(hostdir, "*.pem")):
                        basename = os.path.basename(file)
                        target = os.path.join(ent_path, basename)
                        shutil.copy2(file, target)

    keys = glob(key_pattern)
    if not keys:
        raise exception.ConfigError(
            "No key found in /etc/pki/entitlement directory.  It means "
            "this machine is not subscribed.  Please use \n"
            "  subscription-manager register\n"
            "If you don't have Red Hat subscription yet, consider "
            "getting subscription:\n"
            "  https://access.redhat.com/solutions/253273\n"
            "You can have a free developer subscription:\n"
            "  https://developers.redhat.com/faq/"
        )

    # Use the first available key.
    key_file_name = os.path.basename(keys[0])
    opts['redhat_subscription_key_id'] = key_file_name.split('-')[0]


def is_host_rh_family():
    distro_name = distro.id()
    return distro_name in RHEL_CLONES + ['fedora']

def mock_host_environment_type():
    """
    Detect if we run in Docker.
    """
    if hasattr(mock_host_environment_type, "cached_retval"):
        return mock_host_environment_type.cached_retval

    def _cache(retval):
        mock_host_environment_type.cached_retval = retval
        getLog().info("Guessed host environment type: %s", retval)
        return retval

    # Docker container has different cgroup than PID 1 of host.
    # And have "docker" in that tree.
    with open('/proc/self/cgroup', encoding="utf8") as f:
        for line in f:
            items = line.split(':')
            if 'docker' in items[2]:
                return _cache("docker")
    # For containers with cgroupv2
    with open('/proc/self/mountinfo', encoding='utf8') as f:
        for line in f:
            if '/docker/containers/' in line and "/etc/hosts" in line:
                return _cache("docker")

    return _cache("unknown")


@contextlib.contextmanager
def nullcontext():
    """
    contextlib.nullcontext is not available in Python 3.6, but we are still
    Python 3.6+ compatible because of EL 8
    """
    yield None

@contextlib.contextmanager
def env_var_override(name, value):
    """
    Temporary set environment variable NAME to VALUE, revert to the previous
    state.
    """

    oldval = None
    if name in os.environ:
        oldval = os.environ[name]

    os.environ[name] = value
    yield

    if oldval is None:
        del os.environ[name]
    else:
        os.environ[name] = oldval
