summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/cloud_tests/__main__.py5
-rw-r--r--tests/cloud_tests/args.py4
-rw-r--r--tests/cloud_tests/collect.py3
-rw-r--r--tests/cloud_tests/config.py1
-rw-r--r--tests/cloud_tests/images/nocloudkvm.py88
-rw-r--r--tests/cloud_tests/instances/base.py2
-rw-r--r--tests/cloud_tests/instances/nocloudkvm.py216
-rw-r--r--tests/cloud_tests/platforms.yaml4
-rw-r--r--tests/cloud_tests/platforms/__init__.py2
-rw-r--r--tests/cloud_tests/platforms/nocloudkvm.py90
-rw-r--r--tests/cloud_tests/releases.yaml19
-rw-r--r--tests/cloud_tests/setup_image.py20
-rw-r--r--tests/cloud_tests/snapshots/nocloudkvm.py74
-rw-r--r--tests/cloud_tests/util.py43
14 files changed, 564 insertions, 7 deletions
diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py
index 260ddb3f..7ee29cad 100644
--- a/tests/cloud_tests/__main__.py
+++ b/tests/cloud_tests/__main__.py
@@ -4,6 +4,7 @@
import argparse
import logging
+import os
import sys
from tests.cloud_tests import args, bddeb, collect, manage, run_funcs, verify
@@ -50,7 +51,7 @@ def main():
return -1
# run handler
- LOG.debug('running with args: %s\n', parsed)
+ LOG.debug('running with args: %s', parsed)
return {
'bddeb': bddeb.bddeb,
'collect': collect.collect,
@@ -63,6 +64,8 @@ def main():
if __name__ == "__main__":
+ if os.geteuid() == 0:
+ sys.exit('Do not run as root')
sys.exit(main())
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
index 369d60db..c6c1877b 100644
--- a/tests/cloud_tests/args.py
+++ b/tests/cloud_tests/args.py
@@ -170,9 +170,9 @@ def normalize_collect_args(args):
@param args: parsed args
@return_value: updated args, or None if errors occurred
"""
- # platform should default to all supported
+ # platform should default to lxd
if len(args.platform) == 0:
- args.platform = config.ENABLED_PLATFORMS
+ args.platform = ['lxd']
args.platform = util.sorted_unique(args.platform)
# os name should default to all enabled
diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
index b44e8bdd..4a2422ed 100644
--- a/tests/cloud_tests/collect.py
+++ b/tests/cloud_tests/collect.py
@@ -120,6 +120,7 @@ def collect_image(args, platform, os_name):
os_config = config.load_os_config(
platform.platform_name, os_name, require_enabled=True,
feature_overrides=args.feature_override)
+ LOG.debug('os config: %s', os_config)
component = PlatformComponent(
partial(images.get_image, platform, os_config))
@@ -144,6 +145,8 @@ def collect_platform(args, platform_name):
platform_config = config.load_platform_config(
platform_name, require_enabled=True)
+ platform_config['data_dir'] = args.data_dir
+ LOG.debug('platform config: %s', platform_config)
component = PlatformComponent(
partial(platforms.get_platform, platform_name, platform_config))
diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
index 4d5dc801..52fc2bda 100644
--- a/tests/cloud_tests/config.py
+++ b/tests/cloud_tests/config.py
@@ -112,6 +112,7 @@ def load_os_config(platform_name, os_name, require_enabled=False,
feature_conf = main_conf['features']
feature_groups = conf.get('feature_groups', [])
overrides = merge_config(get(conf, 'features'), feature_overrides)
+ conf['arch'] = c_util.get_architecture()
conf['features'] = merge_feature_groups(
feature_conf, feature_groups, overrides)
diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
new file mode 100644
index 00000000..a7af0e59
--- /dev/null
+++ b/tests/cloud_tests/images/nocloudkvm.py
@@ -0,0 +1,88 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""NoCloud KVM Image Base Class."""
+
+from tests.cloud_tests.images import base
+from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot
+
+
+class NoCloudKVMImage(base.Image):
+ """NoCloud KVM backed image."""
+
+ platform_name = "nocloud-kvm"
+
+ def __init__(self, platform, config, img_path):
+ """Set up image.
+
+ @param platform: platform object
+ @param config: image configuration
+ @param img_path: path to the image
+ """
+ self.modified = False
+ self._instance = None
+ self._img_path = img_path
+
+ super(NoCloudKVMImage, self).__init__(platform, config)
+
+ @property
+ def instance(self):
+ """Returns an instance of an image."""
+ if not self._instance:
+ if not self._img_path:
+ raise RuntimeError()
+
+ self._instance = self.platform.create_image(
+ self.properties, self.config, self.features, self._img_path,
+ image_desc=str(self), use_desc='image-modification')
+ return self._instance
+
+ @property
+ def properties(self):
+ """Dictionary containing: 'arch', 'os', 'version', 'release'."""
+ return {
+ 'arch': self.config['arch'],
+ 'os': self.config['family'],
+ 'release': self.config['release'],
+ 'version': self.config['version'],
+ }
+
+ def execute(self, *args, **kwargs):
+ """Execute command in image, modifying image."""
+ return self.instance.execute(*args, **kwargs)
+
+ def push_file(self, local_path, remote_path):
+ """Copy file at 'local_path' to instance at 'remote_path'."""
+ return self.instance.push_file(local_path, remote_path)
+
+ def run_script(self, *args, **kwargs):
+ """Run script in image, modifying image.
+
+ @return_value: script output
+ """
+ return self.instance.run_script(*args, **kwargs)
+
+ def snapshot(self):
+ """Create snapshot of image, block until done."""
+ if not self._img_path:
+ raise RuntimeError()
+
+ instance = self.platform.create_image(
+ self.properties, self.config, self.features,
+ self._img_path, image_desc=str(self), use_desc='snapshot')
+
+ return nocloud_kvm_snapshot.NoCloudKVMSnapshot(
+ self.platform, self.properties, self.config,
+ self.features, instance)
+
+ def destroy(self):
+ """Unset path to signal image is no longer used.
+
+ The removal of the images and all other items is handled by the
+ framework. In some cases we want to keep the images, so let the
+ framework decide whether to keep or destroy everything.
+ """
+ self._img_path = None
+ self._instance.destroy()
+ super(NoCloudKVMImage, self).destroy()
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
index 58f45b14..9bdda608 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -90,7 +90,7 @@ class Instance(object):
return self.execute(
['/bin/bash', script_path], rcs=rcs, description=description)
finally:
- self.execute(['rm', script_path], rcs=rcs)
+ self.execute(['rm', '-f', script_path], rcs=rcs)
def tmpfile(self):
"""Get a tmp file in the target.
diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
new file mode 100644
index 00000000..7abfe737
--- /dev/null
+++ b/tests/cloud_tests/instances/nocloudkvm.py
@@ -0,0 +1,216 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base NoCloud KVM instance."""
+
+import os
+import paramiko
+import shlex
+import socket
+import subprocess
+import time
+
+from cloudinit import util as c_util
+from tests.cloud_tests.instances import base
+from tests.cloud_tests import util
+
+
+class NoCloudKVMInstance(base.Instance):
+ """NoCloud KVM backed instance."""
+
+ platform_name = "nocloud-kvm"
+
+ def __init__(self, platform, name, properties, config, features,
+ user_data, meta_data):
+ """Set up instance.
+
+ @param platform: platform object
+ @param name: image path
+ @param properties: dictionary of properties
+ @param config: dictionary of configuration values
+ @param features: dictionary of supported feature flags
+ """
+ self.user_data = user_data
+ self.meta_data = meta_data
+ self.ssh_key_file = os.path.join(platform.config['data_dir'],
+ platform.config['private_key'])
+ self.ssh_port = None
+ self.pid = None
+ self.pid_file = None
+
+ super(NoCloudKVMInstance, self).__init__(
+ platform, name, properties, config, features)
+
+ def destroy(self):
+ """Clean up instance."""
+ if self.pid:
+ try:
+ c_util.subp(['kill', '-9', self.pid])
+ except util.ProcessExectuionError:
+ pass
+
+ if self.pid_file:
+ 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.pid:
+ return self.ssh(command)
+ else:
+ return self.mount_image_callback(command) + (0,)
+
+ def mount_image_callback(self, cmd):
+ """Run mount-image-callback."""
+ mic = ('sudo mount-image-callback --system-mounts --system-resolvconf '
+ '%s -- chroot _MOUNTPOINT_ ' % self.name)
+
+ out, err = c_util.subp(shlex.split(mic) + cmd)
+
+ return out, err
+
+ def generate_seed(self, tmpdir):
+ """Generate nocloud seed from user-data"""
+ seed_file = os.path.join(tmpdir, '%s_seed.img' % self.name)
+ user_data_file = os.path.join(tmpdir, '%s_user_data' % self.name)
+
+ with open(user_data_file, "w") as ud_file:
+ ud_file.write(self.user_data)
+
+ c_util.subp(['cloud-localds', seed_file, user_data_file])
+
+ return seed_file
+
+ def get_free_port(self):
+ """Get a free port assigned by the kernel."""
+ s = socket.socket()
+ s.bind(('', 0))
+ num = s.getsockname()[1]
+ 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:
+ cmd = ("sudo mount-image-callback --system-mounts "
+ "--system-resolvconf %s -- chroot _MOUNTPOINT_ "
+ "/bin/sh -c 'cat - > %s'" % (self.name, remote_path))
+ local_file = open(local_path)
+ p = subprocess.Popen(shlex.split(cmd),
+ 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):
+ """Run a command via SSH."""
+ client = self._ssh_connect()
+
+ 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
+
+ def _ssh_connect(self, hostname='localhost', username='ubuntu',
+ banner_timeout=120, retry_attempts=30):
+ """Connect via SSH."""
+ private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ while retry_attempts:
+ try:
+ client.connect(hostname=hostname, username=username,
+ port=self.ssh_port, pkey=private_key,
+ banner_timeout=banner_timeout)
+ return client
+ except (paramiko.SSHException, TypeError):
+ time.sleep(1)
+ retry_attempts = retry_attempts - 1
+
+ error_desc = 'Failed command to: %s@%s:%s' % (username, hostname,
+ self.ssh_port)
+ raise util.InTargetExecuteError('', '', -1, 'ssh connect',
+ self.name, error_desc)
+
+ def start(self, wait=True, wait_for_cloud_init=False):
+ """Start instance."""
+ tmpdir = self.platform.config['data_dir']
+ seed = self.generate_seed(tmpdir)
+ self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
+ self.ssh_port = self.get_free_port()
+
+ cmd = ('./tools/xkvm --disk %s,cache=unsafe --disk %s,cache=unsafe '
+ '--netdev user,hostfwd=tcp::%s-:22 '
+ '-- -pidfile %s -vnc none -m 2G -smp 2'
+ % (self.name, seed, self.ssh_port, self.pid_file))
+
+ subprocess.Popen(shlex.split(cmd), close_fds=True,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ while not os.path.exists(self.pid_file):
+ time.sleep(1)
+
+ with open(self.pid_file, 'r') as pid_f:
+ self.pid = pid_f.readlines()[0].strip()
+
+ 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)
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
index b91834ab..fa4f845e 100644
--- a/tests/cloud_tests/platforms.yaml
+++ b/tests/cloud_tests/platforms.yaml
@@ -59,6 +59,10 @@ platforms:
{{ config_get("user.user-data", properties.default) }}
cloud-init-vendor.tpl: |
{{ config_get("user.vendor-data", properties.default) }}
+ nocloud-kvm:
+ enabled: true
+ private_key: id_rsa
+ public_key: id_rsa.pub
ec2: {}
azure: {}
diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py
index 443f6d44..3490fe87 100644
--- a/tests/cloud_tests/platforms/__init__.py
+++ b/tests/cloud_tests/platforms/__init__.py
@@ -3,8 +3,10 @@
"""Main init."""
from tests.cloud_tests.platforms import lxd
+from tests.cloud_tests.platforms import nocloudkvm
PLATFORMS = {
+ 'nocloud-kvm': nocloudkvm.NoCloudKVMPlatform,
'lxd': lxd.LXDPlatform,
}
diff --git a/tests/cloud_tests/platforms/nocloudkvm.py b/tests/cloud_tests/platforms/nocloudkvm.py
new file mode 100644
index 00000000..f1f81877
--- /dev/null
+++ b/tests/cloud_tests/platforms/nocloudkvm.py
@@ -0,0 +1,90 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base NoCloud KVM platform."""
+import glob
+import os
+
+from simplestreams import filters
+from simplestreams import mirrors
+from simplestreams import objectstores
+from simplestreams import util as s_util
+
+from cloudinit import util as c_util
+from tests.cloud_tests.images import nocloudkvm as nocloud_kvm_image
+from tests.cloud_tests.instances import nocloudkvm as nocloud_kvm_instance
+from tests.cloud_tests.platforms import base
+from tests.cloud_tests import util
+
+
+class NoCloudKVMPlatform(base.Platform):
+ """NoCloud KVM test platform."""
+
+ platform_name = 'nocloud-kvm'
+
+ def get_image(self, img_conf):
+ """Get image using specified image configuration.
+
+ @param img_conf: configuration for image
+ @return_value: cloud_tests.images instance
+ """
+ (url, path) = s_util.path_from_mirror_url(img_conf['mirror_url'], None)
+
+ filter = filters.get_filters(['arch=%s' % c_util.get_architecture(),
+ 'release=%s' % img_conf['release'],
+ 'ftype=disk1.img'])
+ mirror_config = {'filters': filter,
+ 'keep_items': False,
+ 'max_items': 1,
+ 'checksumming_reader': True,
+ 'item_download': True
+ }
+
+ def policy(content, path):
+ return s_util.read_signed(content, keyring=img_conf['keyring'])
+
+ smirror = mirrors.UrlMirrorReader(url, policy=policy)
+ tstore = objectstores.FileStore(img_conf['mirror_dir'])
+ tmirror = mirrors.ObjectFilterMirror(config=mirror_config,
+ objectstore=tstore)
+ tmirror.sync(smirror, path)
+
+ search_d = os.path.join(img_conf['mirror_dir'], '**',
+ img_conf['release'], '**', '*.img')
+
+ images = []
+ for fname in glob.iglob(search_d, recursive=True):
+ images.append(fname)
+
+ if len(images) != 1:
+ raise Exception('No unique images found')
+
+ image = nocloud_kvm_image.NoCloudKVMImage(self, img_conf, images[0])
+ if img_conf.get('override_templates', False):
+ image.update_templates(self.config.get('template_overrides', {}),
+ self.config.get('template_files', {}))
+ return image
+
+ def create_image(self, properties, config, features,
+ src_img_path, image_desc=None, use_desc=None,
+ user_data=None, meta_data=None):
+ """Create an image
+
+ @param src_img_path: image path to launch from
+ @param properties: image properties
+ @param config: image configuration
+ @param features: image features
+ @param image_desc: description of image being launched
+ @param use_desc: description of container's use
+ @return_value: cloud_tests.instances instance
+ """
+ name = util.gen_instance_name(image_desc=image_desc, use_desc=use_desc)
+ img_path = os.path.join(self.config['data_dir'], name + '.qcow2')
+ c_util.subp(['qemu-img', 'create', '-f', 'qcow2',
+ '-b', src_img_path, img_path])
+
+ return nocloud_kvm_instance.NoCloudKVMInstance(self, img_path,
+ properties, config,
+ features, user_data,
+ meta_data)
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
index c8dd1427..ec7e2d5b 100644
--- a/tests/cloud_tests/releases.yaml
+++ b/tests/cloud_tests/releases.yaml
@@ -27,7 +27,12 @@ default_release_config:
# features groups and additional feature settings
feature_groups: []
features: {}
-
+ nocloud-kvm:
+ mirror_url: https://cloud-images.ubuntu.com/daily
+ mirror_dir: '/srv/citest/nocloud-kvm'
+ keyring: /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg
+ setup_overrides: null
+ override_templates: false
# lxd specific default configuration options
lxd:
# default sstreams server to use for lxd image retrieval
@@ -121,6 +126,9 @@ releases:
# EOL: Jul 2018
default:
enabled: true
+ release: artful
+ version: 17.10
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -134,6 +142,9 @@ releases:
# EOL: Jan 2018
default:
enabled: true
+ release: zesty
+ version: 17.04
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -147,6 +158,9 @@ releases:
# EOL: Apr 2021
default:
enabled: true
+ release: xenial
+ version: 16.04
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -160,6 +174,9 @@ releases:
# EOL: Apr 2019
default:
enabled: true
+ release: trusty
+ version: 14.04
+ family: ubuntu
feature_groups:
- base
- debian_base
diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
index 3c0fff62..6672ffb3 100644
--- a/tests/cloud_tests/setup_image.py
+++ b/tests/cloud_tests/setup_image.py
@@ -5,6 +5,7 @@
from functools import partial
import os
+from cloudinit import util as c_util
from tests.cloud_tests import LOG
from tests.cloud_tests import stage, util
@@ -19,7 +20,7 @@ def installed_package_version(image, package, ensure_installed=True):
"""
os_family = util.get_os_family(image.properties['os'])
if os_family == 'debian':
- cmd = ['dpkg-query', '-W', "--showformat='${Version}'", package]
+ cmd = ['dpkg-query', '-W', "--showformat=${Version}", package]
elif os_family == 'redhat':
cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package]
else:
@@ -53,7 +54,7 @@ def install_deb(args, image):
image.execute(cmd, description=msg)
# check installed deb version matches package
- fmt = ['-W', "--showformat='${Version}'"]
+ fmt = ['-W', "--showformat=${Version}"]
(out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
expected_version = out.strip()
found_version = installed_package_version(image, 'cloud-init')
@@ -191,6 +192,20 @@ def enable_repo(args, image):
image.execute(cmd, description=msg)
+def generate_ssh_keys(data_dir):
+ """Generate SSH keys to be used with image."""
+ LOG.info('generating SSH keys')
+ filename = os.path.join(data_dir, 'id_rsa')
+
+ if os.path.exists(filename):
+ c_util.del_file(filename)
+
+ c_util.subp(['ssh-keygen', '-t', 'rsa', '-b', '4096',
+ '-f', filename, '-P', '',
+ '-C', 'ubuntu@cloud_test'],
+ capture=True)
+
+
def setup_image(args, image):
"""Set up image as specified in args.
@@ -226,6 +241,7 @@ def setup_image(args, image):
'set up for {}'.format(image), calls, continue_after_error=False)
LOG.debug('after setup complete, installed cloud-init version is: %s',
installed_package_version(image, 'cloud-init'))
+ generate_ssh_keys(args.data_dir)
return res
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/snapshots/nocloudkvm.py b/tests/cloud_tests/snapshots/nocloudkvm.py
new file mode 100644
index 00000000..09998349
--- /dev/null
+++ b/tests/cloud_tests/snapshots/nocloudkvm.py
@@ -0,0 +1,74 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base NoCloud KVM snapshot."""
+import os
+
+from tests.cloud_tests.snapshots import base
+
+
+class NoCloudKVMSnapshot(base.Snapshot):
+ """NoCloud KVM image copy backed snapshot."""
+
+ platform_name = "nocloud-kvm"
+
+ def __init__(self, platform, properties, config, features,
+ instance):
+ """Set up snapshot.
+
+ @param platform: platform object
+ @param properties: image properties
+ @param config: image config
+ @param features: supported feature flags
+ """
+ self.instance = instance
+
+ super(NoCloudKVMSnapshot, self).__init__(
+ platform, properties, config, features)
+
+ def launch(self, user_data, meta_data=None, block=True, start=True,
+ use_desc=None):
+ """Launch instance.
+
+ @param user_data: user-data for the instance
+ @param instance_id: instance-id for the instance
+ @param block: wait until instance is created
+ @param start: start instance and wait until fully started
+ @param use_desc: description of snapshot instance use
+ @return_value: an Instance
+ """
+ key_file = os.path.join(self.platform.config['data_dir'],
+ self.platform.config['public_key'])
+ user_data = self.inject_ssh_key(user_data, key_file)
+
+ instance = self.platform.create_image(
+ self.properties, self.config, self.features,
+ self.instance.name, image_desc=str(self), use_desc=use_desc,
+ user_data=user_data, meta_data=meta_data)
+
+ if start:
+ instance.start()
+
+ return instance
+
+ def inject_ssh_key(self, user_data, key_file):
+ """Inject the authorized key into the user_data."""
+ with open(key_file) as f:
+ value = f.read()
+
+ key = 'ssh_authorized_keys:'
+ value = ' - %s' % value.strip()
+ user_data = user_data.split('\n')
+ if key in user_data:
+ user_data.insert(user_data.index(key) + 1, '%s' % value)
+ else:
+ user_data.insert(-1, '%s' % key)
+ user_data.insert(-1, '%s' % value)
+
+ return '\n'.join(user_data)
+
+ def destroy(self):
+ """Clean up snapshot data."""
+ self.instance.destroy()
+ super(NoCloudKVMSnapshot, self).destroy()
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
index 2bbe21c7..4357fbb0 100644
--- a/tests/cloud_tests/util.py
+++ b/tests/cloud_tests/util.py
@@ -2,12 +2,14 @@
"""Utilities for re-use across integration tests."""
+import base64
import copy
import glob
import os
import random
import shutil
import string
+import subprocess
import tempfile
import yaml
@@ -242,6 +244,47 @@ def update_user_data(user_data, updates, dump_to_yaml=True):
if dump_to_yaml else user_data)
+def shell_safe(cmd):
+ """Produce string safe shell string.
+
+ Create a string that can be passed to:
+ set -- <string>
+ to produce the same array that cmd represents.
+
+ Internally we utilize 'getopt's ability/knowledge on how to quote
+ strings to be safe for shell. This implementation could be changed
+ to be pure python. It is just a matter of correctly escaping
+ or quoting characters like: ' " ^ & $ ; ( ) ...
+
+ @param cmd: command as a list
+ """
+ out = subprocess.check_output(
+ ["getopt", "--shell", "sh", "--options", "", "--", "--"] + list(cmd))
+ # out contains ' -- <data>\n'. drop the ' -- ' and the '\n'
+ return out[4:-1].decode()
+
+
+def shell_pack(cmd):
+ """Return a string that can shuffled through 'sh' and execute cmd.
+
+ In Python subprocess terms:
+ check_output(cmd) == check_output(shell_pack(cmd), shell=True)
+
+ @param cmd: list or string of command to pack up
+ """
+
+ if isinstance(cmd, str):
+ cmd = [cmd]
+ else:
+ cmd = list(cmd)
+
+ stuffed = shell_safe(cmd)
+ # for whatever reason b64encode returns bytes when it is clearly
+ # representable as a string by nature of being base64 encoded.
+ b64 = base64.b64encode(stuffed.encode()).decode()
+ return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64
+
+
class InTargetExecuteError(c_util.ProcessExecutionError):
"""Error type for in target commands that fail."""