From b627b58f8597e0617a77ddae5fd3a6cbed82d115 Mon Sep 17 00:00:00 2001 From: Bruno Goncalves Date: Thu, 27 Apr 2017 14:08:51 +0200 Subject: [PATCH] Initial test for baseruntime module --- tests/Makefile | 6 + tests/brtconfig.py | 77 ++++ tests/cleanup.py | 75 ++++ tests/config.yaml | 28 ++ tests/resources/base-runtime-mock.cfg | 34 ++ tests/resources/hello-world/Makefile | 4 + tests/resources/hello-world/README.md | 15 + tests/resources/hello-world/hello.c | 8 + tests/resources/hello-world/hello.sh | 7 + .../all_installed_pkgs_docker.txt | 111 ++++++ .../all_installed_pkgs_nspawn.txt | 144 +++++++ tests/resources/os_release/os_release.sh | 96 +++++ tests/setup.py | 195 +++++++++ tests/smoke.py | 371 ++++++++++++++++++ tests/teardown.py | 32 ++ 15 files changed, 1203 insertions(+) create mode 100644 tests/Makefile create mode 100644 tests/brtconfig.py create mode 100644 tests/cleanup.py create mode 100644 tests/config.yaml create mode 100644 tests/resources/base-runtime-mock.cfg create mode 100644 tests/resources/hello-world/Makefile create mode 100644 tests/resources/hello-world/README.md create mode 100644 tests/resources/hello-world/hello.c create mode 100755 tests/resources/hello-world/hello.sh create mode 100644 tests/resources/installed_packages/all_installed_pkgs_docker.txt create mode 100644 tests/resources/installed_packages/all_installed_pkgs_nspawn.txt create mode 100755 tests/resources/os_release/os_release.sh create mode 100755 tests/setup.py create mode 100644 tests/smoke.py create mode 100755 tests/teardown.py diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..ab9eb6d --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,6 @@ +CMD=MODULE=nspawn python -m avocado run smoke.py + +check: + $(CMD) + +all: check diff --git a/tests/brtconfig.py b/tests/brtconfig.py new file mode 100644 index 0000000..447102b --- /dev/null +++ b/tests/brtconfig.py @@ -0,0 +1,77 @@ +""" +get configuration parameters for base runtime smoke testing +""" + +import os +import logging +from moduleframework import module_framework + + +def get_mockcfg(self): + """ + Get the path to the base runtime mock configuration file + + This is provided by the avocado 'mockcfg' parameter if supplied, + otherwise it is set to "resources/base-runtime-mock.cfg" relative to + the test script directory. + """ + + script_dir = os.path.abspath(os.path.dirname(__file__)) + self.log.info("running script from directory: %s" % script_dir) + + mockcfg = self.params.get('mockcfg', default=os.path.join( + script_dir, "resources", "base-runtime-mock.cfg")) + mockcfg = str(mockcfg) + + if not mockcfg.endswith(".cfg"): + self.error("mock configuration file %s must have the extension '.cfg'" % + mockcfg) + + if not os.path.isfile(mockcfg): + self.error("mock configuration file %s does not exist" % + mockcfg) + + self.log.info("mock configuration file: %s" % mockcfg) + + return mockcfg + + +def get_compiler_test_dir(self): + """ + Get the path to the base runtime compiler test resource directory + + This is provided by the avocado 'compiler-test-dir' parameter if supplied, + otherwise it is set to "resources/hello-world" relative to + the test script directory. + """ + + script_dir = os.path.abspath(os.path.dirname(__file__)) + self.log.info("running script from directory: %s" % script_dir) + + compdir = self.params.get( + 'compiler-test-dir', default=os.path.join(script_dir, "resources", "hello-world")) + compdir = str(compdir) + + if not os.path.isdir(compdir): + self.error("Compiler test resource directory %s does not exist" % compdir) + + self.log.info("Compiler test resource directory: %s" % compdir) + + return compdir + + +def get_docker_image_name(self): + """ + Get the name to use for the base runtime docker image + """ + + container_helper = module_framework.ContainerHelper() + #It tries to get the name from URL env variable + #If URL is not defined it tries to get the name from config.yaml + image_name = container_helper.getDockerInstanceName() + if not image_name: + self.error("Could not find docker image name to use") + + self.log.info("base runtime image name: %s" % image_name) + + return image_name diff --git a/tests/cleanup.py b/tests/cleanup.py new file mode 100644 index 0000000..098737b --- /dev/null +++ b/tests/cleanup.py @@ -0,0 +1,75 @@ +""" +cleanup docker container/images and mock root for smoke testing +""" + +import logging +import subprocess +import re + + +log = logging.getLogger('avocado.test') + +def cleanup_docker_and_mock(mockcfg, img_name): + + # Clean-up old test artifacts (docker containers, image, mock root) + + docker_containerlist_cmdline = 'docker ps --filter=ancestor=%s -a -q' % img_name + try: + containerlist = subprocess.check_output(docker_containerlist_cmdline, + stderr = subprocess.STDOUT, shell = True) + except subprocess.CalledProcessError as e: + log.error("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + raise + else: + log.info("docker container list with '%s' succeeded with output:\n%s" % + (docker_containerlist_cmdline, containerlist)) + + if containerlist: + containers = re.sub('[\r\n]+', ' ', containerlist) + log.info("docker containers using image %s need to be removed: %s\n" % + (img_name, containers)); + docker_teardown_cmdline = 'docker rm -f %s' % containers + try: + docker_teardown_output = subprocess.check_output(docker_teardown_cmdline, + stderr = subprocess.STDOUT, shell = True) + except subprocess.CalledProcessError as e: + log.error("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + raise + else: + log.info("docker container teardown with '%s' succeeded with output:\n%s" % + (docker_teardown_cmdline, docker_teardown_output)) + else: + log.info("no docker containers are using image %s\n" % img_name) + + docker_teardown_cmdline = 'docker rmi %s' % img_name + try: + docker_teardown_output = subprocess.check_output(docker_teardown_cmdline, + stderr = subprocess.STDOUT, shell = True) + except subprocess.CalledProcessError as e: + if "No such image" not in e.output: + log.error("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + raise + else: + log.info("No existing docker image named %s" % img_name) + else: + log.info("docker teardown with '%s' succeeded with output:\n%s" % + (docker_teardown_cmdline, docker_teardown_output)) + + mock_teardown_cmdline = ['mock', '-r', mockcfg, '--scrub=all'] + try: + mock_teardown_output = subprocess.check_output(mock_teardown_cmdline, + stderr = subprocess.STDOUT) + except subprocess.CalledProcessError as e: + log.error("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + raise + log.info("mock teardown with '%s' succeeded with output:\n%s" % + (mock_teardown_cmdline, mock_teardown_output)) + + + + + diff --git a/tests/config.yaml b/tests/config.yaml new file mode 100644 index 0000000..d939680 --- /dev/null +++ b/tests/config.yaml @@ -0,0 +1,28 @@ +document: modularity-testing +version: 1 +name: baseruntime +modulemd-url: http://pkgs.fedoraproject.org/cgit/modules/base-runtime.git/plain/base-runtime.yaml +service: + port: +packages: + profiles: + - baseimage + rpms: +default_module: docker +module: + docker: + start: + labels: + description: "I dont know" + io.k8s.description: "I dont know too" + source: http://pkgs.fedoraproject.org/cgit/modules/base-runtime.git + container: docker=base-runtime-smoke + rpm: + start: + stop: + status: + repos: + - https://kojipkgs.stg.fedoraproject.org/compose/branched/jkaluza/latest-Boltron-26/compose/base-runtime/x86_64/os/ +test: + processrunnig: + - 'ls / | grep bin' diff --git a/tests/resources/base-runtime-mock.cfg b/tests/resources/base-runtime-mock.cfg new file mode 100644 index 0000000..ebe98c8 --- /dev/null +++ b/tests/resources/base-runtime-mock.cfg @@ -0,0 +1,34 @@ + +config_opts['root'] = 'base-runtime-docker' +config_opts['target_arch'] = 'x86_64' +config_opts['legal_host_arches'] = ('x86_64',) +config_opts['chroot_setup_cmd'] = 'install --setopt=tsflags=nodocs bash coreutils-single filesystem glibc-minimal-langpack libcrypt microdnf rpm shadow-utils util-linux' +config_opts['dist'] = '' +config_opts['extra_chroot_dirs'] = [ '/run/lock', ] +config_opts['releasever'] = '' +config_opts['package_manager'] = 'dnf' + +config_opts['yum.conf'] = """ +[main] +keepcache=1 +debuglevel=2 +reposdir=/dev/null +logfile=/var/log/yum.log +retries=20 +obsoletes=1 +gpgcheck=0 +assumeyes=1 +syslog_ident=mock +syslog_device= +install_weak_deps=0 +metadata_expire=3600 +mdpolicy=group:primary + +# repos + +[buildrepo] +name=base-runtime +baseurl=https://kojipkgs.stg.fedoraproject.org/compose/branched/jkaluza/latest-Boltron-26/compose/base-runtime/x86_64/os/ +enabled=1 + +""" diff --git a/tests/resources/hello-world/Makefile b/tests/resources/hello-world/Makefile new file mode 100644 index 0000000..71a429e --- /dev/null +++ b/tests/resources/hello-world/Makefile @@ -0,0 +1,4 @@ +default: hello + +hello: hello.c + gcc -Wall hello.c -o hello diff --git a/tests/resources/hello-world/README.md b/tests/resources/hello-world/README.md new file mode 100644 index 0000000..8b81058 --- /dev/null +++ b/tests/resources/hello-world/README.md @@ -0,0 +1,15 @@ +# compiler sanity smoke test for the Base Runtime docker image + +The internal workings of this test are as follows: + +1. Create a temporary directory. +2. Copy the `hello.sh` script from this resource directory into the temporary directory and make sure it is executable. +3. Place a gzipped tarball of `hello.c` and `Makefile` from this resource directory into the temporary directory with the name `hello.tgz`. + e.g., + `$ tar czf /tmp/random/hello.tgz hello.c Makefile` +4. Run the docker container binding the temporary directory as `/mnt` and run `/mnt/hello.sh`. + e.g., + `$ docker run -v /tmp/random:/mnt:z --rm base-runtime /bin/bash -c /mnt/hello.sh` +5. Clean up the temporary directory upon completion. + + diff --git a/tests/resources/hello-world/hello.c b/tests/resources/hello-world/hello.c new file mode 100644 index 0000000..020484e --- /dev/null +++ b/tests/resources/hello-world/hello.c @@ -0,0 +1,8 @@ +#include + +int +main (void) +{ + printf ("Hello, world!\n"); + return 0; +} diff --git a/tests/resources/hello-world/hello.sh b/tests/resources/hello-world/hello.sh new file mode 100755 index 0000000..b75b9f9 --- /dev/null +++ b/tests/resources/hello-world/hello.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e # exit immediately on any failure +microdnf install tar make gcc 1>&2 +cd /mnt +tar xzvf hello.tgz 1>&2 +make 1>&2 +./hello diff --git a/tests/resources/installed_packages/all_installed_pkgs_docker.txt b/tests/resources/installed_packages/all_installed_pkgs_docker.txt new file mode 100644 index 0000000..88199b0 --- /dev/null +++ b/tests/resources/installed_packages/all_installed_pkgs_docker.txt @@ -0,0 +1,111 @@ +audit-libs +basesystem +bash +bzip2-libs +ca-certificates +chkconfig +coreutils-single +cracklib +crypto-policies +curl +cyrus-sasl-lib +elfutils-libelf +expat +fedora-modular-release +filesystem +gawk +glib2 +glibc +glibc-common +glibc-minimal-langpack +gmp +gnupg2 +gnutls +gobject-introspection +gpgme +grep +gzip +info +keyutils-libs +krb5-libs +libacl +libarchive +libassuan +libattr +libblkid +libcap +libcap-ng +libcom_err +libcrypt +libcurl +libdb +libdb-utils +libdnf +libfdisk +libffi +libgcc +libgcrypt +libgpg-error +libidn2 +libksba +libmetalink +libmount +libnghttp2 +libpeas +libpsl +libpwquality +librepo +libselinux +libsemanage +libsepol +libsigsegv +libsmartcols +libsolv +libssh2 +libtasn1 +libunistring +libutempter +libuuid +libverto +libxml2 +lua-libs +lz4 +lz4-libs +lzo +microdnf +mpfr +ncurses +ncurses-base +ncurses-libs +nettle +npth +nspr +nss +nss-pem +nss-softokn +nss-softokn-freebl +nss-sysinit +nss-tools +nss-util +openldap +openssl-libs +p11-kit +p11-kit-trust +pam +pcre +popt +publicsuffix-list-dafsa +readline +rpm +rpm-libs +rpm-plugin-selinux +sed +setup +shadow-utils +sqlite-libs +systemd-libs +tzdata +ustr +util-linux +xz-libs +zlib diff --git a/tests/resources/installed_packages/all_installed_pkgs_nspawn.txt b/tests/resources/installed_packages/all_installed_pkgs_nspawn.txt new file mode 100644 index 0000000..bb225d3 --- /dev/null +++ b/tests/resources/installed_packages/all_installed_pkgs_nspawn.txt @@ -0,0 +1,144 @@ +acl +audit-libs +basesystem +bash +binutils +bzip2-libs +ca-certificates +chkconfig +coreutils-single +cpp +cracklib +cracklib-dicts +crypto-policies +cryptsetup-libs +curl +cyrus-sasl-lib +dbus +dbus-libs +device-mapper +device-mapper-libs +diffutils +elfutils-default-yama-scope +elfutils-libelf +elfutils-libs +emacs-filesystem +expat +fedora-modular-release +filesystem +gawk +gc +gcc +glib2 +glibc +glibc-common +glibc-devel +glibc-headers +glibc-minimal-langpack +gmp +gnupg2 +gnutls +gobject-introspection +gpgme +grep +guile +gzip +info +iptables-libs +isl +kernel-headers +keyutils-libs +kmod-libs +krb5-libs +libacl +libarchive +libassuan +libatomic_ops +libattr +libblkid +libcap +libcap-ng +libcom_err +libcrypt +libcurl +libdb +libdb-utils +libdnf +libfdisk +libffi +libgcc +libgcrypt +libgomp +libgpg-error +libidn +libidn2 +libksba +libmetalink +libmount +libmpc +libnghttp2 +libpcap +libpeas +libpsl +libpwquality +librepo +libseccomp +libselinux +libsemanage +libsepol +libsigsegv +libsmartcols +libsolv +libssh2 +libstdc++ +libtasn1 +libtool-ltdl +libunistring +libutempter +libuuid +libverto +libxml2 +lua-libs +lz4-libs +lzo +make +microdnf +mpfr +ncurses-base +ncurses-libs +nettle +npth +nspr +nss +nss-pem +nss-softokn +nss-softokn-freebl +nss-sysinit +nss-tools +nss-util +openldap +openssl-libs +p11-kit +p11-kit-trust +pam +pcre +popt +publicsuffix-list-dafsa +qrencode-libs +readline +rpm +rpm-libs +rpm-plugin-selinux +sed +setup +shadow-utils +sqlite-libs +systemd +systemd-libs +systemd-pam +tar +tzdata +ustr +util-linux +xz-libs +zlib diff --git a/tests/resources/os_release/os_release.sh b/tests/resources/os_release/os_release.sh new file mode 100755 index 0000000..39a009a --- /dev/null +++ b/tests/resources/os_release/os_release.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -e # exit immediately on any failure + +. /etc/os-release + +EXP_NAME="Fedora Modular" +EXP_VERSION="26 (Twenty Six)" +EXP_ID="fedora-modular" +EXP_ID_LIKE="fedora" +EXP_VERSION_ID="26" +EXP_PRETTY_NAME="Fedora Modular 26 (Twenty Six)" +EXP_ANSI_COLOR="0;34" +EXP_CPE_NAME="cpe:/o:fedoraproject:fedora-modular:26" +EXP_HOME_URL="https://fedoraproject.org/" +EXP_BUG_REPORT_URL="https://bugzilla.redhat.com/" +EXP_REDHAT_BUGZILLA_PRODUCT="Fedora" +EXP_REDHAT_BUGZILLA_PRODUCT_VERSION="26" +EXP_REDHAT_SUPPORT_PRODUCT="Fedora" +EXP_REDHAT_SUPPORT_PRODUCT_VERSION="26" +EXP_PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy" + +if [[ $NAME != $EXP_NAME ]]; then + echo "FAIL: Expected NAME to be '$EXP_NAME', but it is '$NAME'" + exit 1 +fi + +if [[ $VERSION != $EXP_VERSION ]]; then + echo "FAIL: Expected VERSION to be '$EXP_VERSION', but it is '$VERSION'" + exit 1 +fi + +if [[ $ID != $EXP_ID ]]; then + echo "FAIL: Expected ID to be '$EXP_ID', but it is '$ID'" + exit 1 +fi + +if [[ $ID_LIKE != $EXP_ID_LIKE ]]; then + echo "FAIL: Expected ID_LIKE to be '$EXP_ID_LIKE', but it is '$ID_LIKE'" + exit 1 +fi + +if [[ $VERSION_ID != $EXP_VERSION_ID ]]; then + echo "FAIL: Expected VERSION_ID to be '$EXP_VERSION_ID', but it is '$VERSION_ID'" + exit 1 +fi + +if [[ $PRETTY_NAME != $EXP_PRETTY_NAME ]]; then + echo "FAIL: Expected PRETTY_NAME to be '$EXP_PRETTY_NAME', but it is '$PRETTY_NAME'" + exit 1 +fi + +if [[ $ANSI_COLOR != $EXP_ANSI_COLOR ]]; then + echo "FAIL: Expected ANSI_COLOR to be '$EXP_ANSI_COLOR', but it is '$ANSI_COLOR'" + exit 1 +fi + +if [[ $CPE_NAME != $EXP_CPE_NAME ]]; then + echo "FAIL: Expected CPE_NAME to be '$EXP_CPE_NAME', but it is '$CPE_NAME'" + exit 1 +fi + +if [[ $HOME_URL != $EXP_HOME_URL ]]; then + echo "FAIL: Expected HOME_URL to be '$EXP_HOME_URL', but it is '$HOME_URL'" + exit 1 +fi + +if [[ $BUG_REPORT_URL != $EXP_BUG_REPORT_URL ]]; then + echo "FAIL: Expected BUG_REPORT_URL to be '$EXP_BUG_REPORT_URL', but it is '$BUG_REPORT_URL'" + exit 1 +fi + +if [[ $REDHAT_BUGZILLA_PRODUCT != $EXP_REDHAT_BUGZILLA_PRODUCT ]]; then + echo "FAIL: Expected REDHAT_BUGZILLA_PRODUCT to be '$EXP_REDHAT_BUGZILLA_PRODUCT', but it is '$REDHAT_BUGZILLA_PRODUCT'" + exit 1 +fi + +if [[ $REDHAT_BUGZILLA_PRODUCT_VERSION != $EXP_REDHAT_BUGZILLA_PRODUCT_VERSION ]]; then + echo "FAIL: Expected REDHAT_BUGZILLA_PRODUCT_VERSION to be '$EXP_REDHAT_BUGZILLA_PRODUCT_VERSION', but it is '$REDHAT_BUGZILLA_PRODUCT_VERSION'" + exit 1 +fi + +if [[ $REDHAT_SUPPORT_PRODUCT != $EXP_REDHAT_SUPPORT_PRODUCT ]]; then + echo "FAIL: Expected REDHAT_SUPPORT_PRODUCT to be '$EXP_REDHAT_SUPPORT_PRODUCT', but it is '$REDHAT_SUPPORT_PRODUCT'" + exit 1 +fi + +if [[ $REDHAT_SUPPORT_PRODUCT_VERSION != $EXP_REDHAT_SUPPORT_PRODUCT_VERSION ]]; then + echo "FAIL: Expected REDHAT_SUPPORT_PRODUCT_VERSION to be '$EXP_REDHAT_SUPPORT_PRODUCT_VERSION', but it is '$REDHAT_SUPPORT_PRODUCT_VERSION'" + exit 1 +fi + +if [[ $PRIVACY_POLICY_URL != $EXP_PRIVACY_POLICY_URL ]]; then + echo "FAIL: Expected PRIVACY_POLICY_URL to be '$EXP_PRIVACY_POLICY_URL', but it is '$PRIVACY_POLICY_URL'" + exit 1 +fi + diff --git a/tests/setup.py b/tests/setup.py new file mode 100755 index 0000000..13a6af0 --- /dev/null +++ b/tests/setup.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python + +import os +import subprocess +import re +import sys +import configparser +import tempfile + +from avocado import main +from avocado import Test +from moduleframework import module_framework + +import cleanup +import brtconfig + + +class BaseRuntimeSetupDocker(module_framework.CommonFunctions, Test): + + def setUp(self): + + self.mockcfg = brtconfig.get_mockcfg(self) + self.br_image_name = brtconfig.get_docker_image_name(self) + + def _process_mockcfg(self): + + mockcfg = self.mockcfg + + mock_root = '' + mockcfg_lines = [] + #Regex to get packages that are configured on mockcfg to be installed + chroot_setup_pkg_regex = re.compile("config_opts\s*\[\s*'chroot_setup_cmd'\s*\]\s*=" + "\s*'install --setopt=tsflags=nodocs\s*(.*)\s*'") + chroot_setup_pkgs = None + with open(mockcfg, 'r') as mock_cfgfile: + found_setup_cmd = False + for line in mock_cfgfile: + mockcfg_lines.append(line) + if re.match("config_opts\s*\[\s*'root'\s*\]", line) is not None: + mock_root = line.split('=')[1].split("'")[1] + if re.match("config_opts\s*\[\s*'chroot_setup_cmd'\s*\]", line) is not None: + found_setup_cmd = True + #Check if there are packages defined on chroot_setup_cmd + m = chroot_setup_pkg_regex.match(line) + if m: + chroot_setup_pkgs = sorted(m.group(1).split()) + if len(mock_root) == 0: + self.error("mock configuration file %s does not specify mock root" % + mockcfg) + self.log.info("mock root: %s" % mock_root) + self.mock_root = mock_root + + if not found_setup_cmd: + self.error("mock configuration file %s does not define chroot_setup_cmd" % mockcfg) + + #Need to get all packages that need to be installed + mod_yaml = self.getModulemdYamlconfig() + if not mod_yaml: + self.error("Could not read modulemd Yaml file") + + if "data" not in mod_yaml.keys(): + self.error("'data' key was not found in modulemd Yaml file") + + if "profiles" not in mod_yaml["data"].keys(): + self.error("'profiles' key was not found in 'data' section") + + if "baseimage" not in mod_yaml["data"]["profiles"].keys(): + self.error("'baseimage' key was not found in 'profiles' section") + + base_profile = mod_yaml["data"]["profiles"]["baseimage"] + if "rpms" not in base_profile.keys(): + self.error("'rpms' key was not found in 'baseimage' profile") + + req_pkgs = base_profile["rpms"] + if not req_pkgs: + self.error("Could not find any package to be installed in the image") + + #Only update mockcfg if the list of packages changed + if cmp(chroot_setup_pkgs, sorted(req_pkgs)): + #Need to change chroot_setup_cmd line on mockcfg file + setup_cmd = "install --setopt=tsflags=nodocs " + setup_cmd += " ".join(req_pkgs) + with open(mockcfg, 'w') as mock_cfgfile: + for line in mockcfg_lines: + if re.match("config_opts\s*\[\s*'chroot_setup_cmd'\s*\]", line) is not None: + line = "config_opts['chroot_setup_cmd'] = '%s'\n" % setup_cmd + mock_cfgfile.write(line) + + #Test will exit with WARN to inform the config file has changed + self.log.warning("List of packages to be installed by mock changed") + + def _run_command(self, cmd): + try: + cmd_output = subprocess.check_output( + cmd, stderr=subprocess.STDOUT, shell=True) + except subprocess.CalledProcessError as e: + self.error("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + else: + self.log.info("command '%s' succeeded with output:\n%s" % + (cmd, cmd_output)) + + def _configure_mock_microdnf(self): + """ + Configure mock chroot for microdnf so it carrys into the docker image + """ + + # fetch the dnf.conf file from the mock chroot that was conveniently + # created based on the yum.conf value in the mock configuration file + tmpdnfcfg = tempfile.NamedTemporaryFile(delete=False) + self._run_command('mock -r %s --copyout /etc/dnf/dnf.conf %s' % + (self.mockcfg, tmpdnfcfg.name)) + + with open(tmpdnfcfg.name, 'r') as dnffile: + contents = dnffile.read() + self.log.info( + "Contents of original dnf.conf generated by mock:\n%s" % contents) + + # load the dnf.conf file and remove the [main] section so only the + # repo section(s) remain + config = configparser.ConfigParser() + config.read(tmpdnfcfg.name) + self.log.info("Found the following configuration section(s): %s" % + ' '.join(config.sections())) + if 'main' in config: + del config['main'] + + # write out the cleaned up repo configuration + tmpyumcfg = tempfile.NamedTemporaryFile(delete=False) + with open(tmpyumcfg.name, 'w') as repofile: + config.write(repofile, space_around_delimiters=False) + + with open(tmpyumcfg.name, 'r') as yumfile: + contents = yumfile.read() + self.log.info("Contents of revised yum repo config:\n%s" % contents) + + # copy the new yum repo configuration file into the mock chroot + self._run_command( + 'mock -r %s --copyin %s /etc/yum.repos.d/build.repo' % (self.mockcfg, tmpyumcfg.name)) + self._run_command( + 'mock -r %s --chroot "chmod 644 /etc/yum.repos.d/build.repo"' % self.mockcfg) + + # remove the temporary files + os.remove(tmpdnfcfg.name) + os.remove(tmpyumcfg.name) + + # /etc/pki/rpm-gpg directory must exist or microdnf will explode + self._run_command( + 'mock -r %s --chroot "mkdir -p -m=755 /etc/pki/rpm-gpg"' % self.mockcfg) + + def testCreateDockerImage(self): + + self._process_mockcfg() + + # Clean-up any old test artifacts (docker containers, image, mock root) + # first: + try: + cleanup.cleanup_docker_and_mock(self.mockcfg, self.br_image_name) + except: + self.error("artifact cleanup failed") + else: + self.log.info("artifact cleanup successful") + + # Initialize chroot with mock + self._run_command('mock -r %s --init' % self.mockcfg) + + # Configure mock chroot for microdnf so it carrys into the docker image + self._configure_mock_microdnf() + + # check if "sudo" allows us to tar up the chroot without a password + # Note: this must be configured in "sudoers" to work! + tar_cmd = "tar -C /var/lib/mock/%s/root -c ." % self.mock_root + try: + cmd_output = subprocess.check_output( + "sudo -n %s >/dev/null" % tar_cmd, + stderr=subprocess.STDOUT, shell=True) + except subprocess.CalledProcessError as e: + # no luck using "sudo", warn and proceed as ordinary user without + # it + self.log.info("command '%s' returned exit status %d; output:\n%s" % + (e.cmd, e.returncode, e.output)) + self.log.warning("NO SUDO RIGHTS TO RUN COMMAND '%s' AS ROOT" % + tar_cmd) + self.log.warning("GENERATED DOCKER IMAGE '%s' MAY BE INCOMPLETE!" % + self.br_image_name) + else: + # "sudo" works, so use it + tar_cmd = "sudo -n " + tar_cmd + + # Import mock chroot as a docker image + self._run_command("%s | docker import - %s" % + (tar_cmd, self.br_image_name)) + +if __name__ == "__main__": + main() diff --git a/tests/smoke.py b/tests/smoke.py new file mode 100644 index 0000000..6b8a5a0 --- /dev/null +++ b/tests/smoke.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python + +import os +import subprocess +import re +import shutil +import stat +import tarfile +import tempfile + +from avocado import main +from moduleframework import module_framework + +import brtconfig + + +class BaseRuntimeSmokeTest(module_framework.AvocadoTest): + """ + :avocado: enable + """ + + def setUp(self): + super(self.__class__, self).setUp() + self.compiler_resource_dir = brtconfig.get_compiler_test_dir(self) + self.compiler_test_dir = None + + def _check_cmd_result(self, cmd, return_code, cmd_output, expect_pass=True): + """ + Check based on return code if command passed or failed as expected + """ + if return_code == 0 and expect_pass: + self.log.info("command '%s' succeeded with output:\n%s" % + (cmd, cmd_output)) + return True + elif return_code != 0 and not expect_pass: + self.log.info("command '%s' failed as expected with output:\n%s" % + (cmd, cmd_output)) + return True + self.error("command '%s' returned unexpected exit status %d; output:\n%s" % + (cmd, return_code, cmd_output)) + return False + + def testSmoke(self): + """ + Run several smoke tests + """ + + # TODO: fill this "placeholder" with actual, complete, smoke tests: + + smoke_pass = [ + "echo 'Hello, World!'", + "cat /etc/redhat-release", + "rpm -q glibc"] + + smoke_fail = [ + "exit 1"] + + for cmd in smoke_pass: + cmd_result = self.run("%s" % cmd, ignore_status=True) + cmd_output = cmd_result.stdout + cmd_result.stderr + self._check_cmd_result(cmd, cmd_result.exit_status, cmd_output) + + for cmd in smoke_fail: + cmd_result = self.run("%s" % cmd, ignore_status=True) + cmd_output = cmd_result.stdout + cmd_result.stderr + self._check_cmd_result(cmd, cmd_result.exit_status, cmd_output, expect_pass=False) + + + def _get_all_installed_pkgs(self): + try: + cmd_result = self.run("rpm -qa --qf='%{name}\n'") + except: + self.error("Could not get all installed packages") + output_list = cmd_result.stdout.split("\n") + #remove empty string from the list + return [item for item in output_list if item] + + def testRequiredPackages(self): + """ + Check if all required packages defined on yaml file are installed + """ + + mod_yaml = self.getModulemdYamlconfig() + if not mod_yaml: + self.error("Could not read modulemd Yaml file") + + if "data" not in mod_yaml.keys(): + self.error("'data' key was not found in modulemd Yaml file") + + if "profiles" not in mod_yaml["data"].keys(): + self.error("'profiles' key was not found in 'data' section") + + if "baseimage" not in mod_yaml["data"]["profiles"].keys(): + self.error("'baseimage' key was not found in 'profiles' section") + + base_profile = mod_yaml["data"]["profiles"]["baseimage"] + if "rpms" not in base_profile.keys(): + self.error("'rpms' key was not found in 'baseimage' profile") + + req_pkgs = base_profile["rpms"] + if not req_pkgs: + self.error("No rpm is defined for baseimage") + + installed_pkgs = self._get_all_installed_pkgs() + + for req_pkg in req_pkgs: + if req_pkg not in installed_pkgs: + self.error("Required package '%s' is not installed" % req_pkg) + + def testInstalledPackages(self): + """ + Check if only the expected packages are installed on module + """ + + expected_pkgs = None + + if not self.moduleType: + self.error("moduleType is not defined") + + all_installed_pkgs_path = ("resources/installed_packages/all_installed_pkgs_%s.txt" + % self.moduleType) + try: + with open(all_installed_pkgs_path) as f: + expected_pkgs = f.read().splitlines() + except: + self.error("Could not read the expected installed packages list") + + if not expected_pkgs: + self.error("List of expected installed packages is empty") + + installed_pkgs = self._get_all_installed_pkgs() + if not installed_pkgs: + self.error("It seems there is no package installed in the module") + + for pkg in installed_pkgs: + if pkg not in expected_pkgs: + self.error("Did not expect to have package '%s' installed" % pkg) + + def testUserManipulation(self): + """ + Check if can add, remove and modify user + """ + + #We want to run multiple commands using same docker container + new_user = "usertest" + pass_cmds = [] + #Create new user + pass_cmds.append("adduser %s" % new_user) + #Make sure user is created + pass_cmds.append("cat /etc/passwd | grep %s" % new_user) + pass_cmds.append("ls /home/%s" % new_user) + #set user password + pass_cmds.append("usermod --password testpassword %s" % new_user) + #Test new user functionality + pass_cmds.append('su - %s -c "touch ~/testfile.txt"' % new_user) + #Make sure the file was created by the correct user + pass_cmds.append("ls -allh /home/%s/testfile.txt | grep '%s %s'" % + (new_user, new_user, new_user)) + #Remove user + pass_cmds.append("userdel -r %s" % new_user) + for cmd in pass_cmds: + cmd_result = self.run("%s" % cmd, ignore_status=True) + cmd_output = cmd_result.stdout + cmd_result.stderr + self._check_cmd_result(cmd, cmd_result.exit_status, cmd_output) + + fail_cmds = [] + #Make sure user is removed + fail_cmds.append("ls /home/%s" % new_user) + fail_cmds.append("cat /etc/passwd | grep usertest") + #relying on __del__ from BaseRuntimeRunCmd to remove container + for cmd in fail_cmds: + cmd_result = self.run("%s" % cmd, ignore_status=True) + cmd_output = cmd_result.stdout + cmd_result.stderr + self._check_cmd_result(cmd, cmd_result.exit_status, cmd_output, expect_pass=False) + + def testOsRelease(self): + """ + Check if OS release information is correct + """ + + test_path = "resources/os_release/os_release.sh" + dest_path = "/tmp/os_release.sh" + try: + self.copyTo(test_path, dest_path) + except: + self.error("Could not copy test file from %s to module %s" % + (test_path, dest_path)) + + try: + self.run(dest_path) + except: + self.error("%s failed" % dest_path) + + try: + self.run("rm -f %s" % dest_path) + except: + self.error("Could not delete %s" % dest_path) + + def test_glibc_i18n(self): + """ + Test glibc support to internationalization + """ + + lang_default = { + #cmd : cmd_output + "ls /invalid_path" : "ls: cannot access '/invalid_path': No such file or directory", + "cp invalid_file tmp" : "cp: cannot stat 'invalid_file': No such file or directory", + "date -u -d \"2017-03-31\"" : "Fri Mar 31 00:00:00 UTC 2017", + "touch file; yes | rm -i file" : "rm: remove regular empty file 'file'?", + "numfmt --grouping 1234567890.98" : "1234567890.98" + } + + lang_english = { + "LC_ALL=en_US ls /invalid_path" : "ls: cannot access '/invalid_path': No such file or directory", + "LC_ALL=en_US cp invalid_file tmp" : "cp: cannot stat 'invalid_file': No such file or directory", + "LC_ALL=en_US date -u -d \"2017-03-31\"" : "Fri Mar 31 00:00:00 UTC 2017", + "touch file; yes | LC_ALL=en_US rm -i file" : "rm: remove regular empty file 'file'?", + "LC_ALL=en_US numfmt --grouping 1234567890.98" : "1,234,567,890.98" + } + + lang_spanish = { + "LC_ALL=es_ES ls /invalid_path" : "No existe el fichero o el directorio", + "LC_ALL=es_ES cp invalid_file tmp" : "No existe el fichero o el directorio", + "LC_ALL=es_ES date -u -d \"2017-03-31\"" : "vie mar 31 00:00:00 UTC 2017", + "LC_ALL=es_ES numfmt --grouping 1234567890,98" : "1.234.567.890,98" + } + + langs = {} + langs["default"] = { + "pkg" : "glibc-minimal-langpack", + "cmds" : lang_default + } + + langs["english"] = { + "pkg" : "glibc-langpack-en", + "cmds" : lang_english + } + + langs["spanish"] = { + "pkg" : "glibc-langpack-es", + "cmds" : lang_spanish + } + + + for i18n in langs.keys(): + lang = langs[i18n] + self.log.info("Testing %s" % lang["pkg"]) + + install_package = True + # glibc-minimal-langpack is installed by default + if lang["pkg"] == "glibc-minimal-langpack": + install_package = False + + if install_package: + try: + self.run("microdnf install %s" % lang["pkg"]) + except: + self.error("Could not install %s" % lang["pkg"]) + + for cmd in lang["cmds"].keys(): + cmd_result = self.run("%s" % cmd, ignore_status=True) + output = cmd_result.stdout + output += cmd_result.stderr + output = output.strip() + #search for pattern as Spanish might have special characters + if not re.search(lang["cmds"][cmd], output): + self.error("'%s'expected output '%s', but got '%s'" % + (cmd, lang["cmds"][cmd], output)) + + if install_package: + try: + self.run("microdnf remove %s" % lang["pkg"]) + except: + self.error("Could not remove %s" % lang["pkg"]) + + + def _prepare_compiler_test_directory(self): + + # create a temporary directory + tmpdir = tempfile.mkdtemp() + + self.log.info("Compiler test temporary directory is %s" % tmpdir) + + # Copy the `hello.sh` script from this resource directory into the + # temporary directory + src = os.path.join(self.compiler_resource_dir, "hello.sh") + dest = os.path.join(tmpdir, "hello.sh") + try: + shutil.copy(src, dest) + except shutil.Error as e: + self.log.info('Error: %s' % e) + except IOError as e: + self.log.info('Error: %s' % e.strerror) + + # make sure destination script is executable + st = os.stat(dest) + os.chmod(dest, st.st_mode | stat.S_IEXEC) + + # Place a gzipped tarball of `hello.c` and `Makefile` from the + # resource directory into the temporary directory with the name + # `hello.tgz`. + dest = os.path.join(tmpdir, "hello.tgz") + tar = tarfile.open(dest, "w:gz") + for f in ["hello.c", "Makefile"]: + src = os.path.join(self.compiler_resource_dir, f) + tar.add(src, arcname=f) + tar.close() + + self.compiler_test_dir = tmpdir + + def _cleanup_compiler_test_directory(self): + + # clean up the temporary directory + if self.compiler_test_dir: + self.log.info("cleaning up compiler test directory") + shutil.rmtree(self.compiler_test_dir, ignore_errors=True) + + def testCompiler(self): + """ + Run a basic C compiler test on our docker image. + + This actually tests the integration of several things, including the + ability to install packages, extract a gzipped tarball, run make to + compile a very simple C program, and run the compiled executable. + """ + + self._prepare_compiler_test_directory() + + #The test dir should be the same one used on hello.sh + mod_compiler_test_dir = "/mnt" + + #Make sure there is a container running + #TODO: Remove start() once https://pagure.io/modularity-testing-framework/issue/8 is fixed + self.start() + + try: + self.copyTo("%s/." % self.compiler_test_dir, mod_compiler_test_dir) + except: + self.error("Could not copy test files from %s to module %s" % + (self.compiler_test_dir, mod_compiler_test_dir)) + + cmdline = "%s/hello.sh" % mod_compiler_test_dir + cmd_result = self.run("%s" % cmdline, ignore_status=True) + test_stdout = cmd_result.stdout + test_stderr = cmd_result.stderr + if cmd_result.exit_status: + self.error("command '%s' returned exit status %d; output:\n%s\nstderr:\n%s" % + (cmdline, cmd_result.exit_status, test_stdout, test_stderr)) + + self.log.info("command '%s' succeeded with output:\n%s\nstderr:\n%s" % + (cmdline, test_stdout, test_stderr)) + + # make sure we get exactly what we expect on stdout + # (all other output from commands in the script were sent to stderr) + expected_stdout = 'Hello, world!\n' + self.log.info("checking that compiler test returned expected output: %s" % + repr(expected_stdout)) + if test_stdout != expected_stdout: + self.error("compiler test did not return unexpected output: %s" % + repr(test_stdout)) + + def tearDown(self): + """ + Tear-down + """ + super(self.__class__, self).tearDown() + + self._cleanup_compiler_test_directory() + +if __name__ == "__main__": + main() diff --git a/tests/teardown.py b/tests/teardown.py new file mode 100755 index 0000000..3db11fc --- /dev/null +++ b/tests/teardown.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import os +import subprocess +import re + +from avocado import main +from avocado import Test + +import cleanup +import brtconfig + + +class BaseRuntimeTeardownDocker(Test): + + def setUp(self): + + self.mockcfg = brtconfig.get_mockcfg(self) + self.br_image_name = brtconfig.get_docker_image_name(self) + + def testRemoveDockerImage(self): + + # Clean-up old test artifacts (docker containers, image, mock root) + try: + cleanup.cleanup_docker_and_mock(self.mockcfg, self.br_image_name) + except: + self.error("artifact cleanup failed") + else: + self.log.info("artifact cleanup successful") + +if __name__ == "__main__": + main()