summaryrefslogtreecommitdiff
path: root/tests/cloud_tests/util.py
diff options
context:
space:
mode:
authorScott Moser <smoser@brickies.net>2017-11-06 17:39:00 -0500
committerScott Moser <smoser@brickies.net>2017-11-06 17:39:00 -0500
commit8622491c29f30862a1a1d7ad2cba023981acc8ce (patch)
tree597b08c4ca66575cc9567263030876814999beeb /tests/cloud_tests/util.py
parentbe8e3d3c5b5d3d6a3d222383a58fd5feecead7b7 (diff)
downloadvyos-cloud-init-8622491c29f30862a1a1d7ad2cba023981acc8ce.tar.gz
vyos-cloud-init-8622491c29f30862a1a1d7ad2cba023981acc8ce.zip
tests: integration test cleanup and full pass of nocloud-kvm.
Integration test harness changes: * Enable collection of console log in nocloud-kvm and lxd. * Collect the console log to results for all test runs. * change 'tmpfile' to pick name locally instead of using 'mktemp'. * drop the 'instance' attribute from nocloud-kvm Image and demote LXDImage.instance to a private attribute. This is because Images do not actually have instances. (LXDImage internally uses a booted system to modify the image). * Add 'TargetBase' as a superclass of Image and Instance providing implementations of execute, read_data, write_data, pull_file, and push_file. These all depend on an implementation of _execute. * Improve '_execute' implementations to support accepting stdin. * execute supports 'rcs=False' meaning 'do not raise exception'. * Drop support for pylxd < 2.2. older versions cannot determine exit code of 'execute', which makes them unusable. * make NoCloudKVMInstance._execute run as root via sudo. This required some changes so that 'hostname' could be reverse-looked up in order to avoid sudo taking a long time (~20 seconds). * re-use existing ssh connection in nocloud-kvm. Test changes here: * do not use /tmp, but rather /var/tmp (LP: #1707222) * make keys_to_console assertions more strict. * change user test cases to always add default (ubuntu) user so that nocloud-kvm's execute which operates over ssh can work.
Diffstat (limited to 'tests/cloud_tests/util.py')
-rw-r--r--tests/cloud_tests/util.py162
1 files changed, 154 insertions, 8 deletions
diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
index 4357fbb0..92c31c3a 100644
--- a/tests/cloud_tests/util.py
+++ b/tests/cloud_tests/util.py
@@ -7,6 +7,7 @@ import copy
import glob
import os
import random
+import shlex
import shutil
import string
import subprocess
@@ -285,20 +286,165 @@ def shell_pack(cmd):
return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64
+def shell_quote(cmd):
+ if isinstance(cmd, (tuple, list)):
+ return ' '.join([shlex.quote(x) for x in cmd])
+ return shlex.quote(cmd)
+
+
+class TargetBase(object):
+ _tmp_count = 0
+
+ def execute(self, command, stdin=None, env=None,
+ rcs=None, description=None):
+ """Execute command in instance, recording output, error and exit code.
+
+ Assumes functional networking and execution as root with the
+ target filesystem being available at /.
+
+ @param command: the command to execute as root inside the image
+ if command is a string, then it will be executed as:
+ ['sh', '-c', command]
+ @param stdin: bytes content for standard in
+ @param env: environment variables
+ @param rcs: return codes.
+ None (default): non-zero exit code will raise exception.
+ False: any is allowed (No execption raised).
+ list of int: any rc not in the list will raise exception.
+ @param description: purpose of command
+ @return_value: tuple containing stdout data, stderr data, exit code
+ """
+ if isinstance(command, str):
+ command = ['sh', '-c', command]
+
+ if rcs is None:
+ rcs = (0,)
+
+ if description:
+ LOG.debug('Executing "%s"', description)
+ else:
+ LOG.debug("Executing command: %s", shell_quote(command))
+
+ out, err, rc = self._execute(command=command, stdin=stdin, env=env)
+
+ # False means accept anything.
+ if (rcs is False or rc in rcs):
+ return out, err, rc
+
+ raise InTargetExecuteError(out, err, rc, command, description)
+
+ def _execute(self, command, stdin=None, env=None):
+ """Execute command in inside, return stdout, stderr and exit code.
+
+ Assumes functional networking and execution as root with the
+ target filesystem being available at /.
+
+ @param stdin: bytes content for standard in
+ @param env: environment variables
+ @return_value: tuple containing stdout data, stderr data, exit code
+
+ This is intended to be implemented by the Image or Instance.
+ Many callers will use the higher level 'execute'."""
+ raise NotImplementedError
+
+ def read_data(self, remote_path, decode=False):
+ """Read data from instance filesystem.
+
+ @param remote_path: path in instance
+ @param decode: decode data before returning.
+ @return_value: content of remote_path as bytes if 'decode' is False,
+ and as string if 'decode' is True.
+ """
+ # when sh is invoked with '-c', then the first argument is "$0"
+ # which is commonly understood as the "program name".
+ # 'read_data' is the program name, and 'remote_path' is '$1'
+ stdout, stderr, rc = self._execute(
+ ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path])
+ if rc != 0:
+ raise RuntimeError("Failed to read file '%s'" % remote_path)
+
+ if decode:
+ return stdout.decode()
+ return stdout
+
+ def write_data(self, remote_path, data):
+ """Write data to instance filesystem.
+
+ @param remote_path: path in instance
+ @param data: data to write in bytes
+ """
+ # when sh is invoked with '-c', then the first argument is "$0"
+ # which is commonly understood as the "program name".
+ # 'write_data' is the program name, and 'remote_path' is '$1'
+ _, _, rc = self._execute(
+ ["sh", "-c", 'exec cat >"$1"', 'write_data', remote_path],
+ stdin=data)
+
+ if rc != 0:
+ raise RuntimeError("Failed to write to '%s'" % remote_path)
+ return
+
+ def pull_file(self, remote_path, local_path):
+ """Copy file at 'remote_path', from instance to 'local_path'.
+
+ @param remote_path: path on remote instance
+ @param local_path: path on local instance
+ """
+ with open(local_path, 'wb') as fp:
+ fp.write(self.read_data(remote_path))
+
+ def push_file(self, local_path, remote_path):
+ """Copy file at 'local_path' to instance at 'remote_path'.
+
+ @param local_path: path on local instance
+ @param remote_path: path on remote instance"""
+ with open(local_path, "rb") as fp:
+ self.write_data(remote_path, data=fp.read())
+
+ def run_script(self, script, rcs=None, description=None):
+ """Run script in target and return stdout.
+
+ @param script: script contents
+ @param rcs: allowed return codes from script
+ @param description: purpose of script
+ @return_value: stdout from script
+ """
+ # Just write to a file, add execute, run it, then remove it.
+ shblob = '; '.join((
+ 'set -e',
+ 's="$1"',
+ 'shift',
+ 'cat > "$s"',
+ 'trap "rm -f $s" EXIT',
+ 'chmod +x "$s"',
+ '"$s" "$@"'))
+ return self.execute(
+ ['sh', '-c', shblob, 'runscript', self.tmpfile()],
+ stdin=script, description=description, rcs=rcs)
+
+ def tmpfile(self):
+ """Get a tmp file in the target.
+
+ @return_value: path to new file in target
+ """
+ path = "/tmp/%s-%04d" % (type(self).__name__, self._tmp_count)
+ self._tmp_count += 1
+ return path
+
+
class InTargetExecuteError(c_util.ProcessExecutionError):
"""Error type for in target commands that fail."""
- default_desc = 'Unexpected error while running command in target instance'
+ default_desc = 'Unexpected error while running command.'
- def __init__(self, stdout, stderr, exit_code, cmd, instance,
- description=None):
+ def __init__(self, stdout, stderr, exit_code, cmd, description=None,
+ reason=None):
"""Init error and parent error class."""
- if isinstance(cmd, (tuple, list)):
- cmd = ' '.join(cmd)
super(InTargetExecuteError, self).__init__(
- stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd,
- reason="Instance: {}".format(instance),
- description=description if description else self.default_desc)
+ stdout=stdout, stderr=stderr, exit_code=exit_code,
+ cmd=shell_quote(cmd),
+ description=description if description else self.default_desc,
+ reason=reason)
class TempDir(object):