From f576b2a24b8014e91087933d19a7a0d396787c30 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 26 Jan 2018 16:00:10 -0500 Subject: tests: add support for logs with lxd from snap and future lxd 3. This puts in place detection for if 'show-log' will work with lxc client, and uses that if present. The 'lxc console --show-log' is not expected to work until lxd/liblxc3.0. That should come in a few months. The hope is that when that function arrives, this code will move over to using it. For other scenarios (all current lxd installs) this will now support getting logs from a snap installed lxd or a package installed lxd via the old 'lxc.console.logfile'. If installed from snap, a platform error will be raised until the user does: sudo mkdir --mode=1777 -p /var/snap/lxd/common/consoles LP: #1745663 --- tests/cloud_tests/platforms/lxd/instance.py | 132 ++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 26 deletions(-) (limited to 'tests/cloud_tests/platforms') diff --git a/tests/cloud_tests/platforms/lxd/instance.py b/tests/cloud_tests/platforms/lxd/instance.py index d2d2a1fd..0488da57 100644 --- a/tests/cloud_tests/platforms/lxd/instance.py +++ b/tests/cloud_tests/platforms/lxd/instance.py @@ -6,7 +6,9 @@ import os import shutil from tempfile import mkdtemp -from cloudinit.util import subp, ProcessExecutionError +from cloudinit.util import load_yaml, subp, ProcessExecutionError, which +from tests.cloud_tests import LOG +from tests.cloud_tests.util import PlatformError from ..instances import Instance @@ -15,6 +17,8 @@ class LXDInstance(Instance): """LXD container backed instance.""" platform_name = "lxd" + _console_log_method = None + _console_log_file = None def __init__(self, platform, name, properties, config, features, pylxd_container): @@ -30,8 +34,8 @@ class LXDInstance(Instance): super(LXDInstance, self).__init__( platform, name, properties, config, features) self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name)) - self._setup_console_log() self.name = name + self._setup_console_log() @property def pylxd_container(self): @@ -39,21 +43,6 @@ class LXDInstance(Instance): self._pylxd_container.sync() return self._pylxd_container - def _setup_console_log(self): - logf = os.path.join(self.tmpd, "console.log") - - # doing this ensures we can read it. Otherwise it ends up root:root. - with open(logf, "w") as fp: - fp.write("# %s\n" % self.name) - - cfg = "lxc.console.logfile=%s" % logf - orig = self._pylxd_container.config.get('raw.lxc', "") - if orig: - orig += "\n" - self._pylxd_container.config['raw.lxc'] = orig + cfg - self._pylxd_container.save() - self._console_log_file = logf - def _execute(self, command, stdin=None, env=None): if env is None: env = {} @@ -97,19 +86,80 @@ class LXDInstance(Instance): """ self.pylxd_container.files.put(remote_path, data) + @property + def console_log_method(self): + if self._console_log_method is not None: + return self._console_log_method + + client = which('lxc') + if not client: + raise PlatformError("No 'lxc' client.") + + elif _has_proper_console_support(): + self._console_log_method = 'show-log' + elif client.startswith("/snap"): + self._console_log_method = 'logfile-snap' + else: + self._console_log_method = 'logfile-tmp' + + LOG.debug("Set console log method to %s", self._console_log_method) + return self._console_log_method + + def _setup_console_log(self): + method = self.console_log_method + if not method.startswith("logfile-"): + return + + if method == "logfile-snap": + log_dir = "/var/snap/lxd/common/consoles" + if not os.path.exists(log_dir): + raise PlatformError( + "Unable to log with snap lxc. Please run:\n" + " sudo mkdir --mode=1777 -p %s" % log_dir) + elif method == "logfile-tmp": + log_dir = "/tmp" + else: + raise PlatformError( + "Unexpected value for console method: %s" % method) + + # doing this ensures we can read it. Otherwise it ends up root:root. + log_file = os.path.join(log_dir, self.name) + with open(log_file, "w") as fp: + fp.write("# %s\n" % self.name) + + cfg = "lxc.console.logfile=%s" % log_file + orig = self._pylxd_container.config.get('raw.lxc', "") + if orig: + orig += "\n" + self._pylxd_container.config['raw.lxc'] = orig + cfg + self._pylxd_container.save() + self._console_log_file = log_file + def console_log(self): """Console log. - @return_value: bytes of this instance’s console + @return_value: bytes of this instance's console """ - if not os.path.exists(self._console_log_file): - raise NotImplementedError( - "Console log '%s' does not exist. If this is a remote " - "lxc, then this is really NotImplementedError. If it is " - "A local lxc, then this is a RuntimeError." - "https://github.com/lxc/lxd/issues/1129") - with open(self._console_log_file, "rb") as fp: - return fp.read() + + if self._console_log_file: + if not os.path.exists(self._console_log_file): + raise NotImplementedError( + "Console log '%s' does not exist. If this is a remote " + "lxc, then this is really NotImplementedError. If it is " + "A local lxc, then this is a RuntimeError." + "https://github.com/lxc/lxd/issues/1129") + with open(self._console_log_file, "rb") as fp: + return fp.read() + + try: + stdout, stderr = subp( + ['lxc', 'console', '--show-log', self.name], decode=False) + return stdout + except ProcessExecutionError as e: + raise PlatformError( + "console log", + "Console log failed [%d]: stdout=%s stderr=%s" % ( + e.exit_code, e.stdout, e.stderr)) def reboot(self, wait=True): """Reboot instance.""" @@ -146,7 +196,37 @@ class LXDInstance(Instance): if self.platform.container_exists(self.name): raise OSError('container {} was not properly removed' .format(self.name)) + if self._console_log_file and os.path.exists(self._console_log_file): + os.unlink(self._console_log_file) shutil.rmtree(self.tmpd) super(LXDInstance, self).destroy() + +def _has_proper_console_support(): + stdout, _ = subp(['lxc', 'info']) + info = load_yaml(stdout) + reason = None + if 'console' not in info.get('api_extensions', []): + reason = "LXD server does not support console api extension" + else: + dver = info.get('environment', {}).get('driver_version', "") + if dver.startswith("2.") or dver.startwith("1."): + reason = "LXD Driver version not 3.x+ (%s)" % dver + else: + try: + stdout, stderr = subp(['lxc', 'console', '--help'], + decode=False) + if not (b'console' in stdout and b'log' in stdout): + reason = "no '--log' in lxc console --help" + except ProcessExecutionError as e: + reason = "no 'console' command in lxc client" + + if reason: + LOG.debug("no console-support: %s", reason) + return False + else: + LOG.debug("console-support looks good") + return True + + # vi: ts=4 expandtab -- cgit v1.2.3 From bef2f2c945fdee4a1141c3177b3e48b1537027e4 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 15 Mar 2018 11:45:12 -0400 Subject: tests: Fix some warnings in tests that popped up with newer python. When running 'tox -e pylint' on a bionic system (python 3.6.4) I started seeing errors today like: tests/cloud_tests/platforms/__init__.py:5: [E0401(import-error), ] Unable to import 'tests.cloud_tests.platforms.ec2' The fix for those first errors was simply to create the __init__.py. The second set of changes fixes fallout found from actually now having pylint properly run on more of the cloud_tests. --- .pylintrc | 12 +++++++++++- tests/cloud_tests/platforms/ec2/__init__.py | 0 tests/cloud_tests/platforms/lxd/__init__.py | 0 tests/cloud_tests/platforms/lxd/platform.py | 4 ---- tests/cloud_tests/platforms/nocloudkvm/__init__.py | 0 tests/cloud_tests/platforms/nocloudkvm/instance.py | 2 +- tests/cloud_tests/platforms/nocloudkvm/platform.py | 4 ---- 7 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 tests/cloud_tests/platforms/ec2/__init__.py create mode 100644 tests/cloud_tests/platforms/lxd/__init__.py create mode 100644 tests/cloud_tests/platforms/nocloudkvm/__init__.py (limited to 'tests/cloud_tests/platforms') diff --git a/.pylintrc b/.pylintrc index 05a086d9..0bdfa59d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -46,7 +46,17 @@ reports=no # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. -ignored-modules=six.moves,pkg_resources,httplib,http.client,paramiko,simplestreams +ignored-modules= + http.client, + httplib, + pkg_resources, + six.moves, + # cloud_tests requirements. + boto3, + botocore, + paramiko, + pylxd, + simplestreams # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of diff --git a/tests/cloud_tests/platforms/ec2/__init__.py b/tests/cloud_tests/platforms/ec2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cloud_tests/platforms/lxd/__init__.py b/tests/cloud_tests/platforms/lxd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cloud_tests/platforms/lxd/platform.py b/tests/cloud_tests/platforms/lxd/platform.py index 6a016929..f7251a07 100644 --- a/tests/cloud_tests/platforms/lxd/platform.py +++ b/tests/cloud_tests/platforms/lxd/platform.py @@ -101,8 +101,4 @@ class LXDPlatform(Platform): """ return self.client.images.get_by_alias(alias) - def destroy(self): - """Clean up platform data.""" - super(LXDPlatform, self).destroy() - # vi: ts=4 expandtab diff --git a/tests/cloud_tests/platforms/nocloudkvm/__init__.py b/tests/cloud_tests/platforms/nocloudkvm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cloud_tests/platforms/nocloudkvm/instance.py b/tests/cloud_tests/platforms/nocloudkvm/instance.py index 932dc0fa..33ff3f24 100644 --- a/tests/cloud_tests/platforms/nocloudkvm/instance.py +++ b/tests/cloud_tests/platforms/nocloudkvm/instance.py @@ -109,7 +109,7 @@ class NoCloudKVMInstance(Instance): if self.pid: try: c_util.subp(['kill', '-9', self.pid]) - except util.ProcessExectuionError: + except c_util.ProcessExecutionError: pass if self.pid_file: diff --git a/tests/cloud_tests/platforms/nocloudkvm/platform.py b/tests/cloud_tests/platforms/nocloudkvm/platform.py index a7e6f5de..85933463 100644 --- a/tests/cloud_tests/platforms/nocloudkvm/platform.py +++ b/tests/cloud_tests/platforms/nocloudkvm/platform.py @@ -21,10 +21,6 @@ class NoCloudKVMPlatform(Platform): platform_name = 'nocloud-kvm' - def __init__(self, config): - """Set up platform.""" - super(NoCloudKVMPlatform, self).__init__(config) - def get_image(self, img_conf): """Get image using specified image configuration. -- cgit v1.2.3 From 2f5d4cebb243cf2f30c665f034668ba4b14178f9 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 15 Mar 2018 12:39:32 -0400 Subject: tests: fix run_tree and bddeb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was broken probably when we inserted the ssh keys into Platform.   tox -e citest tree_run and   tox -e citest bddeb would fail with KeyError in Platform.init due to lack of a data_dir. Also here are a few fixes found from attempting to make it work. --- tests/cloud_tests/bddeb.py | 2 +- tests/cloud_tests/platforms/platforms.py | 14 ++++++++++++-- tests/cloud_tests/util.py | 6 +++++- 3 files changed, 18 insertions(+), 4 deletions(-) (limited to 'tests/cloud_tests/platforms') diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py index a6d5069f..b9cfcfa6 100644 --- a/tests/cloud_tests/bddeb.py +++ b/tests/cloud_tests/bddeb.py @@ -16,7 +16,7 @@ pre_reqs = ['devscripts', 'equivs', 'git', 'tar'] def _out(cmd_res): """Get clean output from cmd result.""" - return cmd_res[0].strip() + return cmd_res[0].decode("utf-8").strip() def build_deb(args, instance): diff --git a/tests/cloud_tests/platforms/platforms.py b/tests/cloud_tests/platforms/platforms.py index 1542b3be..abbfebba 100644 --- a/tests/cloud_tests/platforms/platforms.py +++ b/tests/cloud_tests/platforms/platforms.py @@ -2,12 +2,15 @@ """Base platform class.""" import os +import shutil from simplestreams import filters, mirrors from simplestreams import util as s_util from cloudinit import util as c_util +from tests.cloud_tests import util + class Platform(object): """Base class for platforms.""" @@ -17,7 +20,14 @@ class Platform(object): def __init__(self, config): """Set up platform.""" self.config = config - self._generate_ssh_keys(config['data_dir']) + self.tmpdir = util.mkdtemp() + if 'data_dir' in config: + self.data_dir = config['data_dir'] + else: + self.data_dir = os.path.join(self.tmpdir, "data_dir") + os.mkdir(self.data_dir) + + self._generate_ssh_keys(self.data_dir) def get_image(self, img_conf): """Get image using specified image configuration. @@ -29,7 +39,7 @@ class Platform(object): def destroy(self): """Clean up platform data.""" - pass + shutil.rmtree(self.tmpdir) def _generate_ssh_keys(self, data_dir): """Generate SSH keys to be used with image.""" diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py index 6ff285e7..3dd4996d 100644 --- a/tests/cloud_tests/util.py +++ b/tests/cloud_tests/util.py @@ -460,6 +460,10 @@ class PlatformError(IOError): IOError.__init__(self, message) +def mkdtemp(prefix='cloud_test_data'): + return tempfile.mkdtemp(prefix=prefix) + + class TempDir(object): """Configurable temporary directory like tempfile.TemporaryDirectory.""" @@ -480,7 +484,7 @@ class TempDir(object): @return_value: tempdir path """ if not self.tmpdir: - self.tmpdir = tempfile.mkdtemp(prefix=self.prefix) + self.tmpdir = mkdtemp(prefix=self.prefix) LOG.debug('using tmpdir: %s', self.tmpdir) return self.tmpdir -- cgit v1.2.3