From 6f2e6d79d76d9f5d791b099f1ea6d401a9fb66f7 Mon Sep 17 00:00:00 2001
From: Susant Sahani <ssahani@redhat.com>
Date: Mar 19 2019 14:07:13 +0000
Subject: inventory: Inroduce Podman


This works allows to run tests in podman container.

How to test?

```
$ export TEST_SUBJECTS=registry.fedoraproject.org/fedora:29
$ ansible-playbook -vvv -i ./standard-inventory-podman tests.yml
```

---

diff --git a/inventory/standard-inventory-podman b/inventory/standard-inventory-podman
new file mode 100755
index 0000000..c017b51
--- /dev/null
+++ b/inventory/standard-inventory-podman
@@ -0,0 +1,231 @@
+#!/usr/bin/python3
+# SPDX Licence identifier MIT
+# Copyright (c) 2019 Red Hat Inc.
+
+import argparse
+import distutils.util
+import errno
+import json
+import logging
+import os
+import sys
+import shlex
+import signal
+import subprocess
+import time
+from distutils import dir_util
+
+EMPTY_INVENTORY = {}
+LOG_FILE = "default_provisioners.log"
+CONTAINER_TEMP_ARTIFACTS = "/tmp/artifacts"
+PODMAN_BIN = "/usr/bin/podman"
+
+
+def print_bad_inventory():
+    """Print bad inventory on any uncatched exception. This will prevent
+    running playbook on localhost.
+    """
+    fake_host = "fake_host"
+    fake_hostname = "standard-inventory-qcow2_failed_check_logs"
+    hosts = [fake_host]
+    bad_inv = {"localhost": {"hosts": hosts, "vars": {}},
+               "subjects": {"hosts": hosts, "vars": {}},
+               "_meta": {"hostvars": {fake_host: {"ansible_host": fake_hostname}}}}
+
+    sys.stdout.write(json.dumps(bad_inv, indent=4, separators=(',', ': ')))
+
+
+def get_artifact_path(path=""):
+    """Return path to an artifact file in artifacts directory. If path == ""
+    than return path artifacts dir.  Create artifacts dir if necessary.
+    """
+    artifacts = os.environ.get("TEST_ARTIFACTS", os.path.join(os.getcwd(), "artifacts"))
+    try:
+        os.makedirs(artifacts)
+    except OSError as exc:
+        if exc.errno != errno.EEXIST or not os.path.isdir(artifacts):
+            raise
+    return os.path.join(artifacts, path)
+
+
+def copy_artifacts_from_container(name):
+    container_path = subprocess.check_output([PODMAN_BIN, "mount", name]).rstrip().decode('utf-8')
+    container_artifacts_path = container_path + CONTAINER_TEMP_ARTIFACTS
+
+    dir_util.copy_tree(container_artifacts_path, CONTAINER_TEMP_ARTIFACTS)
+
+    subprocess.call([PODMAN_BIN, "umount", name])
+
+
+def inv_list(subjects):
+    hosts = []
+    variables = {}
+
+    for subject in subjects:
+        name, host_vars = inv_host(subject)
+        if host_vars:
+            hosts.append(name)
+            variables[name] = host_vars
+
+    if not hosts:
+        return EMPTY_INVENTORY
+
+    return {"localhost": {"hosts": hosts, "vars": {}},
+            "subjects": {"hosts": hosts, "vars": {}},
+            "_meta": {"hostvars": variables}}
+
+
+def inv_host(subject):
+    words = subject.split(":")
+    image = words[0]
+    name = words[1]
+
+    null = open(os.devnull, 'w')
+    try:
+        tty = os.open("/dev/tty", os.O_WRONLY)
+        os.dup2(tty, 2)
+    except OSError:
+        tty = None
+        pass
+
+    logger.info("Launching Podman container for {0}".format(image))
+
+    # Make sure the podman service is running
+    try:
+        subprocess.check_call(["/usr/bin/systemctl", "start", "io.podman.service"])
+    except subprocess.CalledProcessError:
+        raise RuntimeError("Could not start podman service")
+
+    try:
+        subprocess.check_call([PODMAN_BIN, "start", name], stdout=sys.stderr.fileno())
+    except subprocess.CalledProcessError:
+        cmd = [PODMAN_BIN, "run", "-d", "--name", format(name), "--entrypoint=/bin/sh", image, "-c", "sleep infinity"]
+        try:
+            subprocess.check_call(cmd, stdout=sys.stderr.fileno())
+        except subprocess.CalledProcessError:
+            raise RuntimeError("Could not start container image: {0}".format(image))
+
+    # Directory to place artifacts
+    artifacts = os.environ.get("TEST_ARTIFACTS", os.path.join(os.getcwd(), "artifacts"))
+
+    # The variables
+    variables = {
+        "ansible_connection": "podman",
+        "ansible_python_interpreter": "/usr/bin/python3"
+    }
+
+    ppid = os.getppid()
+    pid = os.fork()
+    if pid > 0:
+        return name, variables
+
+    # Daemonize and watch the processes
+    os.chdir("/")
+    os.setsid()
+    os.umask(0)
+    if tty is None:
+        tty = null.fileno()
+
+    # Duplicate standard input to standard output and standard error.
+    os.dup2(null.fileno(), 0)
+    os.dup2(tty, 1)
+    os.dup2(tty, 2)
+
+    # Now wait for the parent process to go away, then kill the VM
+    while True:
+        time.sleep(3)
+        try:
+            os.kill(ppid, 0)
+        except OSError:
+            break  # Either of the processes no longer exist
+
+    if diagnose:
+        def _signal_handler(*args):
+            logger.info("Diagnose ending.")
+
+        logger.info("kill {0} # when finished".format(os.getpid()))
+        signal.signal(signal.SIGTERM, _signal_handler)
+        signal.pause()
+
+    # Dump the container logs
+    os.makedirs(artifacts, exist_ok=True)
+    log = os.path.join(artifacts, "{0}.log".format(os.path.basename(image)))
+
+    with open(log, "w") as f:
+        subprocess.call([PODMAN_BIN, "logs", name], stdout=f.fileno())
+
+    # Fetch logs from container
+    copy_artifacts_from_container(name)
+    dir_util.copy_tree(CONTAINER_TEMP_ARTIFACTS, artifacts)
+
+    # Stop the container
+    subprocess.check_call([PODMAN_BIN, "stop", name])
+
+    sys.exit(0)
+
+
+def main(argv):
+    global logger
+    global diagnose
+    logger = logging.getLogger(__name__)
+    logger.setLevel(logging.DEBUG)
+
+    # stderr output
+    conhandler = logging.StreamHandler()
+
+    # Print to strerr by default messages with level >= warning, can be changed
+    # with setting TEST_DEBUG=1.
+    try:
+        diagnose = distutils.util.strtobool(os.getenv("TEST_DEBUG", "0"))
+    except ValueError:
+        diagnose = 0
+    conhandler.setLevel(logging.WARNING)
+
+    if diagnose:
+        # Collect all messages with any log level to stderr.
+        conhandler.setLevel(logging.NOTSET)
+
+    # Log format for stderr.
+    log_format = "[%(levelname)-5.5s] {}: %(message)s".format(os.path.basename(__file__))
+    formatter = logging.Formatter(log_format)
+    conhandler.setFormatter(formatter)
+    logger.addHandler(conhandler)
+
+    parser = argparse.ArgumentParser(description="Inventory for a container image in a registry")
+    parser.add_argument("--list", action="store_true", help="Verbose output")
+    parser.add_argument('--host', help="Get host variables")
+    parser.add_argument("subjects", nargs="*", default=shlex.split(os.environ.get("TEST_SUBJECTS", "")))
+    opts = parser.parse_args()
+
+    # Send logs to common logfile for all default provisioners.
+    log_file = get_artifact_path(LOG_FILE)
+    fhandler = logging.FileHandler(log_file)
+
+    # Collect all messages with any log level to log file.
+    fhandler.setLevel(logging.NOTSET)
+    log_format = ("%(asctime)s [{}/%(threadName)-12.12s] [%(levelname)-5.5s]:"
+                  "%(message)s").format(os.path.basename(__file__))
+    logFormatter = logging.Formatter(log_format)
+    fhandler.setFormatter(logFormatter)
+    logger.addHandler(fhandler)
+
+    logger.info("Start provisioner.")
+    if opts.host:
+        _, data = inv_host(opts.host)
+    else:
+        data = inv_list(opts.subjects)
+
+    sys.stdout.write(json.dumps(data, indent=4, separators=(',', ': ')))
+
+
+if __name__ == '__main__':
+    ret = -1
+    try:
+        main(sys.argv)
+        ret = 0
+    except Exception:
+        print_bad_inventory()
+        # Backtrace stack goes to log file. If TEST_DEBUG == 1, it goes to stderr too.
+        logger.info("Fatal error in provision script.", exc_info=True)
+
+    sys.exit(ret)
diff --git a/roles/str-common-final/tasks/main.yml b/roles/str-common-final/tasks/main.yml
index 53989d4..a8ace93 100644
--- a/roles/str-common-final/tasks/main.yml
+++ b/roles/str-common-final/tasks/main.yml
@@ -6,7 +6,9 @@
     src: "{{ remote_artifacts }}/"
     mode: pull
     ssh_args: "-o UserKnownHostsFile=/dev/null"
-  when: artifacts|default("") != ""
+  when:
+      - artifacts|default("") != ""
+      - ansible_connection|default("") != "podman"
 
 - name: Report role result
   vars:
diff --git a/roles/str-common-init/tasks/main.yml b/roles/str-common-init/tasks/main.yml
index 9529f24..583648d 100644
--- a/roles/str-common-init/tasks/main.yml
+++ b/roles/str-common-init/tasks/main.yml
@@ -25,12 +25,19 @@
 - name: Make artifacts directory
   file: path={{ remote_artifacts }} state=directory owner=root mode=755 recurse=yes
 
+- name: Copy files to local podman container
+  copy:
+    src: "{{ playbook_dir }}/"
+    dest: "{{ tenv_workdir }}"
+  when: ansible_connection|default("") == "podman"
+
 # Next task requires rsync on test environment
 - name: Copy tests to test environment
   synchronize:
     src: "{{ playbook_dir }}/"
     dest: "{{ tenv_workdir }}"
     ssh_args: "-o UserKnownHostsFile=/dev/null"
+  when: ansible_connection|default("") != "podman"
 
 - name: Start services defined in `required_services`
   service:

