From 8e08868e1a99a6cf85a5e743b378c048a4282215 Mon Sep 17 00:00:00 2001
From: Sergio Oliveira Campos <seocam@redhat.com>
Date: Wed, 24 Jun 2020 15:49:07 -0300
Subject: [PATCH] Allow to run tests in Docker

* Adapted tests/test_playbook_runs.py script to allow tests to be
  executed from a docker container.
* Added molecule scenarios to create/destroy test containers and
  respective documentation in tests/README.md.
---
 molecule/centos-7-build/molecule.yml          | 20 +++++
 molecule/centos-7/molecule.yml                | 20 +++++
 molecule/centos-8-build/molecule.yml          | 20 +++++
 molecule/centos-8/molecule.yml                | 20 +++++
 molecule/default                              |  1 +
 molecule/resources/playbooks/library          |  1 +
 molecule/resources/playbooks/module_utils     |  1 +
 .../resources/playbooks/prepare-build.yml     | 54 ++++++++++++
 molecule/resources/playbooks/prepare.yml      | 18 ++++
 molecule/resources/playbooks/roles            |  1 +
 pytest.ini                                    |  1 +
 tests/README.md                               | 47 +++++++++-
 tests/test_playbook_runs.py                   | 86 ++++++++++++++++---
 13 files changed, 276 insertions(+), 14 deletions(-)
 create mode 100644 molecule/centos-7-build/molecule.yml
 create mode 100644 molecule/centos-7/molecule.yml
 create mode 100644 molecule/centos-8-build/molecule.yml
 create mode 100644 molecule/centos-8/molecule.yml
 create mode 120000 molecule/default
 create mode 120000 molecule/resources/playbooks/library
 create mode 120000 molecule/resources/playbooks/module_utils
 create mode 100644 molecule/resources/playbooks/prepare-build.yml
 create mode 100644 molecule/resources/playbooks/prepare.yml
 create mode 120000 molecule/resources/playbooks/roles

diff --git a/molecule/centos-7-build/molecule.yml b/molecule/centos-7-build/molecule.yml
new file mode 100644
index 00000000..0360b8cc
--- /dev/null
+++ b/molecule/centos-7-build/molecule.yml
@@ -0,0 +1,20 @@
+---
+dependency:
+  name: galaxy
+driver:
+  name: docker
+platforms:
+  - name: centos-7-build
+    image: centos/systemd
+    pre_build_image: true
+    hostname: ipaserver.test.local
+    dns_servers:
+      - 8.8.8.8
+    volumes:
+      - /sys/fs/cgroup:/sys/fs/cgroup:ro
+    command: /usr/sbin/init
+    privileged: true
+provisioner:
+  name: ansible
+  playbooks:
+    prepare: ../resources/playbooks/prepare-build.yml
diff --git a/molecule/centos-7/molecule.yml b/molecule/centos-7/molecule.yml
new file mode 100644
index 00000000..0603e267
--- /dev/null
+++ b/molecule/centos-7/molecule.yml
@@ -0,0 +1,20 @@
+---
+dependency:
+  name: galaxy
+driver:
+  name: docker
+platforms:
+  - name: centos-7
+    image: quay.io/ansible-freeipa/upstream-tests:centos-7
+    pre_build_image: true
+    hostname: ipaserver.test.local
+    dns_servers:
+      - 127.0.0.1
+    volumes:
+      - /sys/fs/cgroup:/sys/fs/cgroup:ro
+    command: /usr/sbin/init
+    privileged: true
+provisioner:
+  name: ansible
+  playbooks:
+    prepare: ../resources/playbooks/prepare.yml
diff --git a/molecule/centos-8-build/molecule.yml b/molecule/centos-8-build/molecule.yml
new file mode 100644
index 00000000..a7ffacdf
--- /dev/null
+++ b/molecule/centos-8-build/molecule.yml
@@ -0,0 +1,20 @@
+---
+dependency:
+  name: galaxy
+driver:
+  name: docker
+platforms:
+  - name: centos-8-build
+    image: centos:8
+    pre_build_image: true
+    hostname: ipaserver.test.local
+    dns_servers:
+      - 8.8.8.8
+    volumes:
+      - /sys/fs/cgroup:/sys/fs/cgroup:ro
+    command: /usr/sbin/init
+    privileged: true
+provisioner:
+  name: ansible
+  playbooks:
+    prepare: ../resources/playbooks/prepare-build.yml
diff --git a/molecule/centos-8/molecule.yml b/molecule/centos-8/molecule.yml
new file mode 100644
index 00000000..4e1ab793
--- /dev/null
+++ b/molecule/centos-8/molecule.yml
@@ -0,0 +1,20 @@
+---
+dependency:
+  name: galaxy
+driver:
+  name: docker
+platforms:
+  - name: centos-8
+    image: quay.io/ansible-freeipa/upstream-tests:centos-8
+    pre_build_image: true
+    hostname: ipaserver.test.local
+    dns_servers:
+      - 127.0.0.1
+    volumes:
+      - /sys/fs/cgroup:/sys/fs/cgroup:ro
+    command: /usr/sbin/init
+    privileged: true
+provisioner:
+  name: ansible
+  playbooks:
+    prepare: ../resources/playbooks/prepare.yml
diff --git a/molecule/default b/molecule/default
new file mode 120000
index 00000000..e0a54605
--- /dev/null
+++ b/molecule/default
@@ -0,0 +1 @@
+/home/scampos/src/ansible-freeipa/molecule/centos-8
\ No newline at end of file
diff --git a/molecule/resources/playbooks/library b/molecule/resources/playbooks/library
new file mode 120000
index 00000000..b30cd095
--- /dev/null
+++ b/molecule/resources/playbooks/library
@@ -0,0 +1 @@
+/home/scampos/src/ansible-freeipa/plugins/modules/
\ No newline at end of file
diff --git a/molecule/resources/playbooks/module_utils b/molecule/resources/playbooks/module_utils
new file mode 120000
index 00000000..4a88d1e6
--- /dev/null
+++ b/molecule/resources/playbooks/module_utils
@@ -0,0 +1 @@
+/home/scampos/src/ansible-freeipa/plugins/module_utils/
\ No newline at end of file
diff --git a/molecule/resources/playbooks/prepare-build.yml b/molecule/resources/playbooks/prepare-build.yml
new file mode 100644
index 00000000..784fe7e3
--- /dev/null
+++ b/molecule/resources/playbooks/prepare-build.yml
@@ -0,0 +1,54 @@
+---
+- name: Converge
+  hosts: all
+  tasks:
+  - name: Ensure IPv6 is ENABLED
+    sysctl:
+      name: "{{ item.name }}"
+      value: "{{ item.value }}"
+      sysctl_set: yes
+      state: present
+      reload: yes
+    with_items :
+      - name: net.ipv6.conf.all.disable_ipv6
+        value: 0
+      - name: net.ipv6.conf.lo.disable_ipv6
+        value: 0
+      - name: net.ipv6.conf.eth0.disable_ipv6
+        value: 1
+
+  - name: stat protected_regular
+    stat:
+      path: /proc/sys/fs/protected_regular
+    register: result
+
+  - name: Ensure fs.protected_regular is disabled
+    sysctl:
+      name: fs.protected_regular
+      value: 0
+      sysctl_set: yes
+      state: present
+      reload: yes
+    when: result.stat.exists
+
+  - name: Ensure sudo package is installed
+    package:
+      name: sudo
+
+  - name: Ensure nss package is updated
+    package:
+      name: nss
+      state: latest
+
+  - include_role:
+      name: ipaserver
+    vars:
+      ipaserver_setup_dns: yes
+      ipaserver_setup_kra: yes
+      ipaserver_auto_forwarders: yes
+      ipaserver_no_dnssec_validation: yes
+      ipaserver_auto_reverse: yes
+      ipaadmin_password: SomeADMINpassword
+      ipadm_password: SomeDMpassword
+      ipaserver_domain: test.local
+      ipaserver_realm: TEST.LOCAL
diff --git a/molecule/resources/playbooks/prepare.yml b/molecule/resources/playbooks/prepare.yml
new file mode 100644
index 00000000..fc564d01
--- /dev/null
+++ b/molecule/resources/playbooks/prepare.yml
@@ -0,0 +1,18 @@
+---
+- name: Converge
+  hosts: all
+  tasks:
+  - name: Ensure lock dirs for DS exists
+    file:
+      state: directory
+      owner: dirsrv
+      group: dirsrv
+      path: "{{ item }} "
+    loop:
+      - /var/lock/dirsrv/
+      - /var/lock/dirsrv/slapd-TEST-LOCAL/
+
+  - name: Ensure IPA server is up an running
+    service:
+      name: ipa
+      state: started
diff --git a/molecule/resources/playbooks/roles b/molecule/resources/playbooks/roles
new file mode 120000
index 00000000..a0123be0
--- /dev/null
+++ b/molecule/resources/playbooks/roles
@@ -0,0 +1 @@
+/home/scampos/src/ansible-freeipa/roles
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
index 0ee949b8..1e6a1921 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +1,3 @@
 [pytest]
 python_files = test_*.py
+junit_family = xunit1
diff --git a/tests/README.md b/tests/README.md
index d187575e..2c00436a 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -29,7 +29,13 @@ environment variable. For example:
 ANSIBLE_REMOTE_USER=root IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest
 ```
 
-To select which tests to run use the option `-k`. For example:
+To run a single test use the full path with the following format:
+
+```
+IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest tests/test_playbook_runs.py::sudorule::test_sudorule
+```
+
+To select which tests to run based on search use the option `-k`. For example:
 
 ```
 IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest -k dnszone
@@ -50,6 +56,45 @@ IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest -rs
 For a complete list of options check `pytest --help`.
 
 
+## Running tests in a docker container
+
+It's also possible to run the tests in a container.
+
+### Creating a container to run the tests
+
+Before setting up a container you will need to install molecule framework:
+
+```
+pip install molecule[docker]>=3
+```
+
+Now you can start a test container using the following command:
+```
+molecule create -s centos-8
+```
+
+Note: Currently the containers available for running the tests are:
+ * centos-7
+ * centos-8
+
+### Running the tests inside the container
+
+To run the tests you will use pytest (works the same as for VMs).
+
+```
+RUN_TESTS_IN_DOCKER=1 IPA_SERVER_HOST=centos-8 pytest
+```
+
+### Cleaning up after tests
+
+After running the tests you should probably destroy the test container using:
+
+```
+molecule destroy -s centos-8
+```
+
+See [Running the tests](#running-the-tests) section for more information on available options.
+
 ## Upcoming/desired improvements:
 
 * A script to pre-config the complete test environment using virsh.
diff --git a/tests/test_playbook_runs.py b/tests/test_playbook_runs.py
index e44f4785..86e77be4 100644
--- a/tests/test_playbook_runs.py
+++ b/tests/test_playbook_runs.py
@@ -4,7 +4,7 @@ import os
 import functools
 import tempfile
 
-from subprocess import Popen
+import subprocess
 
 from unittest import TestCase
 
@@ -13,12 +13,53 @@ import pytest
 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
+def is_docker_env():
+    if os.getenv("RUN_TESTS_IN_DOCKER", "0") == "0":
+        return False
+    return True
+
+
+def get_server_host():
+    return os.getenv("IPA_SERVER_HOST")
+
+
+def get_molecule_scenario():
+    return get_server_host()
+
+
 def get_inventory_content():
-    ipa_server_host = os.getenv("IPA_SERVER_HOST")
-    return "[ipaserver]\n{}".format(ipa_server_host).encode("utf8")
+    ipa_server_host = get_server_host()
+
+    if is_docker_env():
+        ipa_server_host += " ansible_connection=docker"
+
+    lines = [
+        "[ipaserver]",
+        ipa_server_host,
+        "[ipaserver:vars]",
+        "ipaserver_domain=test.local",
+        "ipaserver_realm=TEST.LOCAL",
+    ]
+    return "\n".join(lines).encode("utf8")
 
 
-def run_playbook(playbook):
+def write_logs(result, test_name):
+    log_dir = os.path.join(SCRIPT_DIR, "logs")
+    if not os.path.exists(log_dir):
+        os.makedirs(log_dir)
+
+    # Write stdout log for test
+    log_path = os.path.join(log_dir, test_name + ".log")
+    with open(log_path, "w") as log_file:
+        log_file.write(result.stdout.decode("utf-8"))
+
+    # Write stderr log for test
+    error_log_path = os.path.join(log_dir, test_name + "-error.log")
+    with open(error_log_path, "w") as log_file:
+        log_file.write(result.stderr.decode("utf-8"))
+
+
+def run_playbook(playbook, test_name):
     with tempfile.NamedTemporaryFile() as inventory_file:
         inventory_file.write(get_inventory_content())
         inventory_file.flush()
@@ -28,15 +69,17 @@ def run_playbook(playbook):
             inventory_file.name,
             playbook,
         ]
-        process = Popen(cmd, cwd=SCRIPT_DIR)
-        process.wait()
+        process = subprocess.run(
+            cmd, cwd=SCRIPT_DIR, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+    write_logs(process, test_name)
 
     return process
 
 
 def list_test_yaml(dir_path):
     yamls = []
-    for yaml_name in os.listdir(dir_path):
+    for yaml_name in sorted(os.listdir(dir_path)):
         if yaml_name.startswith("test_") and yaml_name.endswith(".yml"):
             yamls.append(
                 {
@@ -50,7 +93,7 @@ def list_test_yaml(dir_path):
 def get_test_groups():
     test_dirs = os.listdir(SCRIPT_DIR)
     groups = {}
-    for test_group_dir in test_dirs:
+    for test_group_dir in sorted(test_dirs):
         group_dir_path = os.path.join(SCRIPT_DIR, test_group_dir)
         if not os.path.isdir(group_dir_path):
             continue
@@ -65,6 +108,7 @@ def prepare_test(test_name, test_path):
         @functools.wraps(func)
         def wrapper(*args, **kwargs):
             kwargs["test_path"] = test_path
+            kwargs["test_name"] = test_name
             return func(*args, **kwargs)
 
         return wrapper
@@ -82,13 +126,29 @@ for group_name, group_tests in get_test_groups().items():
         test_path = test_config["path"]
 
         @pytest.mark.skipif(
-            os.getenv("IPA_SERVER_HOST") is None,
+            not get_server_host(),
             reason="Environment variable IPA_SERVER_HOST must be set",
         )
         @prepare_test(test_name, test_path)
-        def method(self, test_path):
-            result = run_playbook(test_path)
-            assert result.returncode == 0
+        def method(self, test_path, test_name):
+            result = run_playbook(test_path, test_name)
+            status_code_msg = "ansible-playbook return code: {}".format(
+                result.returncode
+            )
+            assert_msg = "\n".join(
+                [
+                    "",
+                    "-" * 30 + " Captured stdout " + "-" * 30,
+                    result.stdout.decode("utf8"),
+                    "-" * 30 + " Captured stderr " + "-" * 30,
+                    result.stderr.decode("utf8"),
+                    "-" * 30 + " Playbook Return Code " + "-" * 30,
+                    status_code_msg,
+                ]
+            )
+            # Need to get the last bytes of msg otherwise Azure
+            #   will cut it out.
+            assert result.returncode == 0, assert_msg[-2500:]
 
         _tests[test_name] = method
-    globals()[group_name] = type(group_name, (TestCase,), _tests)
+    globals()[group_name] = type(group_name, tuple([TestCase]), _tests,)
-- 
GitLab