← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~wesley-wiedenmeier/cloud-init:integration-testing-invocation-cleanup into cloud-init:master

 

Wesley Wiedenmeier has proposed merging ~wesley-wiedenmeier/cloud-init:integration-testing-invocation-cleanup into cloud-init:master.

Requested reviews:
  Server Team CI bot (server-team-bot): continuous-integration
  Joshua Powers (powersj)
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~wesley-wiedenmeier/cloud-init/+git/cloud-init/+merge/314496

Integration Testing: provide commands to run tests from current tree

 - Add 'bddeb' command to build a deb from the current working tree
   cleanly in a container, so deps don't have to be installed on host
 - Add 'tree_collect' and 'tree_run' commands, to build deb from
   current working tree and call collect or run with deb
 - Fix bug in instance.pull_file()
-- 
Your team cloud init development team is requested to review the proposed merge of ~wesley-wiedenmeier/cloud-init:integration-testing-invocation-cleanup into cloud-init:master.
diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
index 099c357..7959bd9 100644
--- a/tests/cloud_tests/__init__.py
+++ b/tests/cloud_tests/__init__.py
@@ -6,6 +6,7 @@ import os
 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')
 TEST_CONF_DIR = os.path.join(BASE_DIR, 'configs')
+TREE_BASE = os.sep.join(BASE_DIR.split(os.sep)[:-2])
 
 
 def _initialize_logging():
diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py
index ef7d187..74f29f6 100644
--- a/tests/cloud_tests/__main__.py
+++ b/tests/cloud_tests/__main__.py
@@ -2,11 +2,9 @@
 
 import argparse
 import logging
-import shutil
 import sys
-import tempfile
 
-from tests.cloud_tests import (args, collect, manage, verify)
+from tests.cloud_tests import args, bddeb, collect, manage, run_funcs, verify
 from tests.cloud_tests import LOG
 
 
@@ -22,28 +20,6 @@ def configure_log(args):
     LOG.setLevel(level)
 
 
-def run(args):
-    """
-    run full test suite
-    """
-    failed = 0
-    args.data_dir = tempfile.mkdtemp(prefix='cloud_test_data_')
-    LOG.debug('using tmpdir %s', args.data_dir)
-    try:
-        failed += collect.collect(args)
-        failed += verify.verify(args)
-    except Exception:
-        failed += 1
-        raise
-    finally:
-        # TODO: make this configurable via environ or cmdline
-        if failed:
-            LOG.warn('some tests failed, leaving data in %s', args.data_dir)
-        else:
-            shutil.rmtree(args.data_dir)
-    return failed
-
-
 def main():
     """
     entry point for cloud test suite
@@ -80,9 +56,12 @@ def main():
     # run handler
     LOG.debug('running with args: %s\n', parsed)
     return {
+        'bddeb': bddeb.bddeb,
         'collect': collect.collect,
         'create': manage.create,
-        'run': run,
+        'run': run_funcs.run,
+        'tree_collect': run_funcs.tree_collect,
+        'tree_run': run_funcs.tree_run,
         'verify': verify.verify,
     }[parsed.subcmd](parsed)
 
diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
index b68cc98..d051737 100644
--- a/tests/cloud_tests/args.py
+++ b/tests/cloud_tests/args.py
@@ -3,9 +3,24 @@
 import os
 
 from tests.cloud_tests import config, util
-from tests.cloud_tests import LOG
+from tests.cloud_tests import LOG, TREE_BASE
 
 ARG_SETS = {
+    'BDDEB': (
+        (('--bddeb-args',),
+         {'help': 'args to pass through to bddeb',
+          'action': 'store', 'default': None, 'required': False}),
+        (('--build-os',),
+         {'help': 'OS to use as build system (default is xenial)',
+          'action': 'store', 'choices': config.list_enabled_distros(),
+          'default': 'xenial', 'required': False}),
+        (('--build-platform',),
+         {'help': 'platform to use for build system (default is lxd)',
+          'action': 'store', 'choices': config.list_enabled_platforms(),
+          'default': 'lxd', 'required': False}),
+        (('--cloud-init',),
+         {'help': 'path to base of cloud-init tree', 'metavar': 'DIR',
+          'action': 'store', 'required': False, 'default': TREE_BASE}),),
     'COLLECT': (
         (('-p', '--platform'),
          {'help': 'platform(s) to run tests on', 'metavar': 'PLATFORM',
@@ -42,6 +57,10 @@ ARG_SETS = {
         (('-d', '--data-dir'),
          {'help': 'directory to store test data in',
           'action': 'store', 'metavar': 'DIR', 'required': True}),),
+    'OUTPUT_DEB': (
+        (('--deb',),
+         {'help': 'path to write output deb to', 'metavar': 'FILE',
+          'action': 'store', 'required': True}),),
     'RESULT': (
         (('-r', '--result'),
          {'help': 'file to write results to',
@@ -66,10 +85,16 @@ ARG_SETS = {
 }
 
 SUBCMDS = {
+    'bddeb': ('build cloud-init deb from tree',
+              ('BDDEB', 'OUTPUT_DEB', 'INTERFACE')),
     'collect': ('collect test data',
                 ('COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT', 'SETUP')),
     'create': ('create new test case', ('CREATE', 'INTERFACE')),
     'run': ('run test suite', ('COLLECT', 'INTERFACE', 'RESULT', 'SETUP')),
+    'tree_collect': ('collect using current working tree',
+                     ('BDDEB', 'COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT')),
+    'tree_run': ('run using current working tree',
+                 ('BDDEB', 'COLLECT', 'INTERFACE', 'RESULT')),
     'verify': ('verify test data', ('INTERFACE', 'OUTPUT', 'RESULT')),
 }
 
@@ -81,6 +106,20 @@ def _empty_normalizer(args):
     return args
 
 
+def normalize_bddeb_args(args):
+    """
+    normalize BDDEB arguments
+    args: parsed args
+    return_value: updated args, or None if errors encountered
+    """
+    # make sure cloud-init dir is accessible
+    if not (args.cloud_init and os.path.isdir(args.cloud_init)):
+        LOG.error('invalid cloud-init tree path')
+        return None
+
+    return args
+
+
 def normalize_create_args(args):
     """
     normalize CREATE arguments
@@ -185,6 +224,17 @@ def normalize_output_args(args):
     return args
 
 
+def normalize_output_deb_args(args):
+    """
+    normalize OUTPUT_DEB arguments
+    args: parsed args
+    return_value: updated args, or None if erros occurred
+    """
+    # make sure to use abspath for deb
+    args.deb = os.path.abspath(args.deb)
+    return args
+
+
 def normalize_setup_args(args):
     """
     normalize SETUP arguments
@@ -210,10 +260,12 @@ def normalize_setup_args(args):
 
 
 NORMALIZERS = {
+    'BDDEB': normalize_bddeb_args,
     'COLLECT': normalize_collect_args,
     'CREATE': normalize_create_args,
     'INTERFACE': _empty_normalizer,
     'OUTPUT': normalize_output_args,
+    'OUTPUT_DEB': normalize_output_deb_args,
     'RESULT': _empty_normalizer,
     'SETUP': normalize_setup_args,
 }
diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py
new file mode 100644
index 0000000..3b79cd2
--- /dev/null
+++ b/tests/cloud_tests/bddeb.py
@@ -0,0 +1,124 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from tests.cloud_tests import (config, LOG)
+from tests.cloud_tests import (platforms, images, snapshots, instances)
+from tests.cloud_tests.stage import (PlatformComponent, run_stage, run_single)
+
+from cloudinit import util as c_util
+
+from functools import partial
+import os
+
+build_deps = ['devscripts', 'equivs', 'git', 'tar']
+
+
+def _out(cmd_res):
+    """
+    get clean output from cmd result
+    """
+    return cmd_res[0].strip()
+
+
+def build_deb(args, instance):
+    """
+    build deb on system and copy out to location at args.deb
+    args: cmdline arguments
+    return_value: tuple of results and fail count
+    """
+    # update remote system package list and install build deps
+    LOG.debug('installing build deps')
+    pkgs = ' '.join(build_deps)
+    cmd = 'apt-get update && apt-get install --yes {}'.format(pkgs)
+    instance.execute(['/bin/sh', '-c', cmd])
+    instance.execute(['mk-build-deps', '--install', '-t',
+                      'apt-get --no-install-recommends --yes', 'cloud-init'])
+
+    # local tmpfile that must be deleted
+    local_tarball = _out(c_util.subp(['mktemp'], capture=True))
+
+    try:
+        # paths to use in remote system
+        remote_tarball = _out(instance.execute(['mktemp']))
+        extract_dir = _out(instance.execute(['mktemp', '--directory']))
+        bddeb_path = os.path.join(extract_dir, 'packages', 'bddeb')
+        output_link = '/cloud-init_all.deb'
+        git_env = {'GIT_DIR': os.path.join(extract_dir, '.git'),
+                   'GIT_WORK_TREE': extract_dir}
+
+        # create a tarball of cloud init tree and copy to remote system
+        LOG.debug('creating tarball of cloud-init at: %s', local_tarball)
+        c_util.subp(['tar', 'cf', local_tarball, '--owner', 'root',
+                     '--group', 'root', '-C', args.cloud_init, '.'])
+        LOG.debug('copying to remote system at: %s', remote_tarball)
+        instance.push_file(local_tarball, remote_tarball)
+
+        # extract tarball in remote system and commit anything uncommitted
+        LOG.debug('extracting tarball in remote system at: %s', extract_dir)
+        instance.execute(['tar', 'xf', remote_tarball, '-C', extract_dir])
+        instance.execute(['git', 'commit', '-a', '-m', 'tmp', '--allow-empty'],
+                         env=git_env)
+
+        # build the deb, ignoring missing deps (flake8)
+        LOG.debug('building deb in remote system at: %s', output_link)
+        bddeb_args = args.bddeb_args.split() if args.bddeb_args else []
+        instance.execute([bddeb_path, '-d'] + bddeb_args, env=git_env)
+
+        # copy the deb back to the host system
+        LOG.debug('copying built deb to host at: %s', args.deb)
+        instance.pull_file(output_link, args.deb)
+
+    finally:
+        os.remove(local_tarball)
+
+
+def setup_build(args):
+    """
+    set build system up then run build
+    args: cmdline arguments
+    return_value: tuple of results and fail count
+    """
+    res = ({}, 1)
+
+    # set up platform
+    LOG.info('setting up platform: %s', args.build_platform)
+    platform_config = config.load_platform_config(args.build_platform)
+    platform_call = partial(platforms.get_platform, args.build_platform,
+                            platform_config)
+    with PlatformComponent(platform_call) as platform:
+
+        # set up image
+        LOG.info('acquiring image for os: %s', args.build_os)
+        img_conf = config.load_os_config(args.build_os)
+        image_call = partial(images.get_image, platform, img_conf)
+        with PlatformComponent(image_call) as image:
+
+            # set up snapshot
+            snapshot_call = partial(snapshots.get_snapshot, image)
+            with PlatformComponent(snapshot_call) as snapshot:
+
+                # create instance with cloud-config to set it up
+                LOG.info('creating instance to build deb in')
+                empty_cloud_config = "#cloud-config\n{}"
+                instance_call = partial(
+                    instances.get_instance, snapshot, empty_cloud_config,
+                    use_desc='build cloud-init deb')
+                with PlatformComponent(instance_call) as instance:
+
+                    # build the deb
+                    res = run_single('build deb on system',
+                                     partial(build_deb, args, instance))
+
+    return res
+
+
+def bddeb(args):
+    """
+    entry point for build deb
+    args: cmdline arguments
+    return_value: fail count
+    """
+    LOG.info('preparing to build cloud-init deb')
+    (res, failed) = run_stage('build deb', [partial(setup_build, args)])
+    return failed
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
index 9559d28..1782c1d 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -51,7 +51,7 @@ class Instance(object):
         copy file at 'remote_path', from instance to 'local_path'
         """
         with open(local_path, 'wb') as fp:
-            fp.write(self.read_data(remote_path), encode=True)
+            fp.write(self.read_data(remote_path))
 
     def push_file(self, local_path, remote_path):
         """
diff --git a/tests/cloud_tests/run_funcs.py b/tests/cloud_tests/run_funcs.py
new file mode 100644
index 0000000..683a3f6
--- /dev/null
+++ b/tests/cloud_tests/run_funcs.py
@@ -0,0 +1,65 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from tests.cloud_tests import bddeb, collect, util, verify
+
+import os
+
+
+def tree_collect(args):
+    """
+    collect data using deb build from current tree
+    args: cmdline args
+    return_value: fail count
+    """
+    failed = 0
+
+    with util.TempDir(args) as tmpdir:
+        args.deb = os.path.join(tmpdir, 'cloud-init.deb')
+        try:
+            failed += bddeb.bddeb(args)
+            failed += collect.collect(args)
+        except Exception:
+            failed += 1
+            raise
+
+    return failed
+
+
+def tree_run(args):
+    """
+    run test suite using deb build from current tree
+    args: cmdline args
+    return_value: fail count
+    """
+    failed = 0
+
+    with util.TempDir(args) as tmpdir:
+        args.deb = os.path.join(tmpdir, 'cloud-init.deb')
+        try:
+            failed += bddeb.bddeb(args)
+            failed += run(args)
+        except Exception:
+            failed += 1
+            raise
+
+    return failed
+
+
+def run(args):
+    """
+    run test suite
+    """
+    failed = 0
+
+    with util.TempDir(args) as tmpdir:
+        args.data_dir = tmpdir
+        try:
+            failed += collect.collect(args)
+            failed += verify.verify(args)
+        except Exception:
+            failed += 1
+            raise
+
+    return failed
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
index 64a8667..18f54b4 100644
--- a/tests/cloud_tests/util.py
+++ b/tests/cloud_tests/util.py
@@ -3,6 +3,7 @@
 import glob
 import os
 import random
+import shutil
 import string
 import tempfile
 import yaml
@@ -160,4 +161,36 @@ def write_file(*args, **kwargs):
     """
     c_util.write_file(*args, **kwargs)
 
+
+class TempDir(object):
+    """
+    temporary directory like tempfile.TemporaryDirectory, but configurable
+    """
+
+    def __init__(self, args):
+        """
+        setup and store args
+        args: cmdline arguments
+        """
+        self.args = args
+        self.tmpdir = None
+
+    def __enter__(self):
+        """
+        create tempdir
+        return_value: tempdir path
+        """
+        self.tmpdir = tempfile.mkdtemp(prefix='cloud_test_')
+        LOG.debug('using tmpdir: %s', self.tmpdir)
+        return self.tmpdir
+
+    def __exit__(self, etype, value, trace):
+        """
+        destroy tempdir if no errors occurred
+        """
+        if etype:
+            LOG.warn('erros occurred, leaving data in %s', self.tmpdir)
+        else:
+            shutil.rmtree(self.tmpdir)
+
 # vi: ts=4 expandtab