summaryrefslogtreecommitdiff
path: root/tests/cloud_tests/instances
diff options
context:
space:
mode:
Diffstat (limited to 'tests/cloud_tests/instances')
-rw-r--r--tests/cloud_tests/instances/base.py81
-rw-r--r--tests/cloud_tests/instances/lxd.py104
-rw-r--r--tests/cloud_tests/instances/nocloudkvm.py143
3 files changed, 112 insertions, 216 deletions
diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
index 9bdda608..8c59d62c 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -2,8 +2,10 @@
"""Base instance."""
+from ..util import TargetBase
-class Instance(object):
+
+class Instance(TargetBase):
"""Base instance object."""
platform_name = None
@@ -22,82 +24,7 @@ class Instance(object):
self.properties = properties
self.config = config
self.features = features
-
- def execute(self, command, stdout=None, stderr=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 stdout, stderr: file handles to write output and error to
- @param env: environment variables
- @param rcs: allowed return codes from command
- @param description: purpose of command
- @return_value: tuple containing stdout data, stderr data, exit code
- """
- raise NotImplementedError
-
- def read_data(self, remote_path, decode=False):
- """Read data from instance filesystem.
-
- @param remote_path: path in instance
- @param decode: return as string
- @return_value: data as str or bytes
- """
- raise NotImplementedError
-
- def write_data(self, remote_path, data):
- """Write data to instance filesystem.
-
- @param remote_path: path in instance
- @param data: data to write, either str or bytes
- """
- raise NotImplementedError
-
- 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, 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
- """
- script_path = self.tmpfile()
- try:
- self.write_data(script_path, script)
- return self.execute(
- ['/bin/bash', script_path], rcs=rcs, description=description)
- finally:
- self.execute(['rm', '-f', script_path], rcs=rcs)
-
- def tmpfile(self):
- """Get a tmp file in the target.
-
- @return_value: path to new file in target
- """
- return self.execute(['mktemp'])[0].strip()
+ self._tmp_count = 0
def console_log(self):
"""Instance console.
diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
index a43918c2..3b035d86 100644
--- a/tests/cloud_tests/instances/lxd.py
+++ b/tests/cloud_tests/instances/lxd.py
@@ -2,8 +2,11 @@
"""Base LXD instance."""
-from tests.cloud_tests.instances import base
-from tests.cloud_tests import util
+from . import base
+
+import os
+import shutil
+from tempfile import mkdtemp
class LXDInstance(base.Instance):
@@ -24,6 +27,8 @@ class LXDInstance(base.Instance):
self._pylxd_container = pylxd_container
super(LXDInstance, self).__init__(
platform, name, properties, config, features)
+ self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name))
+ self._setup_console_log()
@property
def pylxd_container(self):
@@ -31,74 +36,69 @@ class LXDInstance(base.Instance):
self._pylxd_container.sync()
return self._pylxd_container
- def execute(self, command, stdout=None, stderr=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 stdout: file handler to write output
- @param stderr: file handler to write error
- @param env: environment variables
- @param rcs: allowed return codes from command
- @param description: purpose of command
- @return_value: tuple containing stdout data, stderr data, exit code
- """
+ 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 = {}
- if isinstance(command, str):
- command = ['sh', '-c', command]
+ if stdin is not None:
+ # pylxd does not support input to execute.
+ # https://github.com/lxc/pylxd/issues/244
+ #
+ # The solution here is write a tmp file in the container
+ # and then execute a shell that sets it standard in to
+ # be from that file, removes it, and calls the comand.
+ tmpf = self.tmpfile()
+ self.write_data(tmpf, stdin)
+ ncmd = 'exec <"{tmpf}"; rm -f "{tmpf}"; exec "$@"'
+ command = (['sh', '-c', ncmd.format(tmpf=tmpf), 'stdinhack'] +
+ list(command))
# ensure instance is running and execute the command
self.start()
+ # execute returns a ContainerExecuteResult, named tuple
+ # (exit_code, stdout, stderr)
res = self.pylxd_container.execute(command, environment=env)
# get out, exit and err from pylxd return
- if hasattr(res, 'exit_code'):
- # pylxd 2.2 returns ContainerExecuteResult, named tuple of
- # (exit_code, out, err)
- (exit, out, err) = res
- else:
+ if not hasattr(res, 'exit_code'):
# pylxd 2.1.3 and earlier only return out and err, no exit
- # LOG.warning('using pylxd version < 2.2')
- (out, err) = res
- exit = 0
-
- # write data to file descriptors if needed
- if stdout:
- stdout.write(out)
- if stderr:
- stderr.write(err)
-
- # if the command exited with a code not allowed in rcs, then fail
- if exit not in (rcs if rcs else (0,)):
- error_desc = ('Failed command to: {}'.format(description)
- if description else None)
- raise util.InTargetExecuteError(
- out, err, exit, command, self.name, error_desc)
+ raise RuntimeError(
+ "No 'exit_code' in pylxd.container.execute return.\n"
+ "pylxd > 2.2 is required.")
- return (out, err, exit)
+ return res.stdout, res.stderr, res.exit_code
def read_data(self, remote_path, decode=False):
"""Read data from instance filesystem.
@param remote_path: path in instance
- @param decode: return as string
- @return_value: data as str or bytes
+ @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.
"""
data = self.pylxd_container.files.get(remote_path)
- return data.decode() if decode and isinstance(data, bytes) else data
+ return data.decode() if decode else data
def write_data(self, remote_path, data):
"""Write data to instance filesystem.
@param remote_path: path in instance
- @param data: data to write, either str or bytes
+ @param data: data to write in bytes
"""
self.pylxd_container.files.put(remote_path, data)
@@ -107,7 +107,14 @@ class LXDInstance(base.Instance):
@return_value: bytes of this instance’s console
"""
- raise NotImplementedError
+ 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()
def reboot(self, wait=True):
"""Reboot instance."""
@@ -144,6 +151,7 @@ class LXDInstance(base.Instance):
if self.platform.container_exists(self.name):
raise OSError('container {} was not properly removed'
.format(self.name))
+ shutil.rmtree(self.tmpd)
super(LXDInstance, self).destroy()
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
index 8a0e5319..fbb04aa2 100644
--- a/tests/cloud_tests/instances/nocloudkvm.py
+++ b/tests/cloud_tests/instances/nocloudkvm.py
@@ -12,11 +12,19 @@ from cloudinit import util as c_util
from tests.cloud_tests.instances import base
from tests.cloud_tests import util
+# This domain contains reverse lookups for hostnames that are used.
+# The primary reason is so sudo will return quickly when it attempts
+# to look up the hostname. i9n is just short for 'integration'.
+# use i9n.brickies.net until i9n.cloud-init.io is popualted:
+# https://portal.admin.canonical.com/107125
+CI_DOMAIN = "i9n.brickies.net"
+
class NoCloudKVMInstance(base.Instance):
"""NoCloud KVM backed instance."""
platform_name = "nocloud-kvm"
+ _ssh_client = None
def __init__(self, platform, name, properties, config, features,
user_data, meta_data):
@@ -35,6 +43,7 @@ class NoCloudKVMInstance(base.Instance):
self.ssh_port = None
self.pid = None
self.pid_file = None
+ self.console_file = None
super(NoCloudKVMInstance, self).__init__(
platform, name, properties, config, features)
@@ -51,43 +60,18 @@ class NoCloudKVMInstance(base.Instance):
os.remove(self.pid_file)
self.pid = None
- super(NoCloudKVMInstance, self).destroy()
-
- def execute(self, command, stdout=None, stderr=None, env=None,
- rcs=None, description=None):
- """Execute command in instance.
-
- 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 stdout, stderr: file handles to write output and error to
- @param env: environment variables
- @param rcs: allowed return codes from command
- @param description: purpose of command
- @return_value: tuple containing stdout data, stderr data, exit code
- """
- if env is None:
- env = {}
-
- if isinstance(command, str):
- command = ['sh', '-c', command]
+ if self._ssh_client:
+ self._ssh_client.close()
+ self._ssh_client = None
- if self.pid:
- return self.ssh(command)
- else:
- return self.mount_image_callback(command) + (0,)
+ super(NoCloudKVMInstance, self).destroy()
- def mount_image_callback(self, cmd):
- """Run mount-image-callback."""
- out, err = c_util.subp(['sudo', 'mount-image-callback',
- '--system-mounts', '--system-resolvconf',
- self.name, '--', 'chroot',
- '_MOUNTPOINT_'] + cmd)
+ def _execute(self, command, stdin=None, env=None):
+ env_args = []
+ if env:
+ env_args = ['env'] + ["%s=%s" for k, v in env.items()]
- return out, err
+ return self.ssh(['sudo'] + env_args + list(command), stdin=stdin)
def generate_seed(self, tmpdir):
"""Generate nocloud seed from user-data"""
@@ -109,57 +93,31 @@ class NoCloudKVMInstance(base.Instance):
s.close()
return num
- def push_file(self, local_path, remote_path):
- """Copy file at 'local_path' to instance at 'remote_path'.
-
- If we have a pid then SSH is up, otherwise, use
- mount-image-callback.
-
- @param local_path: path on local instance
- @param remote_path: path on remote instance
- """
- if self.pid:
- super(NoCloudKVMInstance, self).push_file()
- else:
- local_file = open(local_path)
- p = subprocess.Popen(['sudo', 'mount-image-callback',
- '--system-mounts', '--system-resolvconf',
- self.name, '--', 'chroot', '_MOUNTPOINT_',
- '/bin/sh', '-c', 'cat - > %s' % remote_path],
- stdin=local_file,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- p.wait()
-
- def sftp_put(self, path, data):
- """SFTP put a file."""
- client = self._ssh_connect()
- sftp = client.open_sftp()
-
- with sftp.open(path, 'w') as f:
- f.write(data)
-
- client.close()
-
- def ssh(self, command):
+ def ssh(self, command, stdin=None):
"""Run a command via SSH."""
client = self._ssh_connect()
+ cmd = util.shell_pack(command)
try:
- _, out, err = client.exec_command(util.shell_pack(command))
- except paramiko.SSHException:
- raise util.InTargetExecuteError('', '', -1, command, self.name)
-
- exit = out.channel.recv_exit_status()
- out = ''.join(out.readlines())
- err = ''.join(err.readlines())
- client.close()
-
- return out, err, exit
+ fp_in, fp_out, fp_err = client.exec_command(cmd)
+ channel = fp_in.channel
+ if stdin is not None:
+ fp_in.write(stdin)
+ fp_in.close()
+
+ channel.shutdown_write()
+ rc = channel.recv_exit_status()
+ return (fp_out.read(), fp_err.read(), rc)
+ except paramiko.SSHException as e:
+ raise util.InTargetExecuteError(
+ b'', b'', -1, command, self.name, reason=e)
def _ssh_connect(self, hostname='localhost', username='ubuntu',
banner_timeout=120, retry_attempts=30):
"""Connect via SSH."""
+ if self._ssh_client:
+ return self._ssh_client
+
private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -168,6 +126,7 @@ class NoCloudKVMInstance(base.Instance):
client.connect(hostname=hostname, username=username,
port=self.ssh_port, pkey=private_key,
banner_timeout=banner_timeout)
+ self._ssh_client = client
return client
except (paramiko.SSHException, TypeError):
time.sleep(1)
@@ -183,15 +142,19 @@ class NoCloudKVMInstance(base.Instance):
tmpdir = self.platform.config['data_dir']
seed = self.generate_seed(tmpdir)
self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
+ self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name)
self.ssh_port = self.get_free_port()
- subprocess.Popen(['./tools/xkvm',
- '--disk', '%s,cache=unsafe' % self.name,
- '--disk', '%s,cache=unsafe' % seed,
- '--netdev',
- 'user,hostfwd=tcp::%s-:22' % self.ssh_port,
- '--', '-pidfile', self.pid_file, '-vnc', 'none',
- '-m', '2G', '-smp', '2'],
+ cmd = ['./tools/xkvm',
+ '--disk', '%s,cache=unsafe' % self.name,
+ '--disk', '%s,cache=unsafe' % seed,
+ '--netdev', ','.join(['user',
+ 'hostfwd=tcp::%s-:22' % self.ssh_port,
+ 'dnssearch=%s' % CI_DOMAIN]),
+ '--', '-pidfile', self.pid_file, '-vnc', 'none',
+ '-m', '2G', '-smp', '2', '-nographic',
+ '-serial', 'file:' + self.console_file]
+ subprocess.Popen(cmd,
close_fds=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -206,12 +169,10 @@ class NoCloudKVMInstance(base.Instance):
if wait:
self._wait_for_system(wait_for_cloud_init)
- def write_data(self, remote_path, data):
- """Write data to instance filesystem.
-
- @param remote_path: path in instance
- @param data: data to write, either str or bytes
- """
- self.sftp_put(remote_path, data)
+ def console_log(self):
+ if not self.console_file:
+ return b''
+ with open(self.console_file, "rb") as fp:
+ return fp.read()
# vi: ts=4 expandtab