cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #02898
[Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
Joshua Powers has proposed merging ~powersj/cloud-init:cii-kvm into cloud-init:master.
Commit message:
test: Enable the KVM platform on integration tests
The KVM platform includes:
* Ubuntu images from daily stream
* Image setup via mount-image-callback
* Generation and injection of SSH key pair (RSA 4096)
* Launching via QEMU CLI and execution via SSH
Requested reviews:
cloud-init commiters (cloud-init-dev)
For more details, see:
https://code.launchpad.net/~powersj/cloud-init/+git/cloud-init/+merge/327646
Would like an initial review, please!
Example command:
$ python3 -m tests.cloud_tests run --verbose --platform kvm --os-name xenial -t modules/locale
Note, not all existing tests work with the kvm. This is because lxd runs with the root user, whereas kvm runs as a normal 'ubuntu' user. Certain assumptions were made with lxd that do not apply to kvm. As such updated test cases will come in a later merge.
--
Your team cloud-init commiters is requested to review the proposed merge of ~powersj/cloud-init:cii-kvm into cloud-init:master.
diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py
index 260ddb3..7ee29ca 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/collect.py b/tests/cloud_tests/collect.py
index b44e8bd..4a2422e 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 4d5dc80..52fc2bd 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/kvm.py b/tests/cloud_tests/images/kvm.py
new file mode 100644
index 0000000..008acef
--- /dev/null
+++ b/tests/cloud_tests/images/kvm.py
@@ -0,0 +1,75 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""KVM Image Base Class."""
+
+from tests.cloud_tests.images import base
+from tests.cloud_tests.snapshots import kvm as kvm_snapshot
+
+
+class KVMImage(base.Image):
+ """KVM backed image."""
+
+ platform_name = "kvm"
+
+ def __init__(self, platform, config, img_path):
+ """Set up image.
+
+ @param platform: platform object
+ @param config: image configuration
+ """
+ self.modified = False
+ self._instance = None
+ self._img_path = img_path
+
+ super(KVMImage, self).__init__(platform, config)
+
+ @property
+ def instance(self):
+ """Property function."""
+ if not self._instance:
+ 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):
+ """{} 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."""
+ instance = self.platform.create_image(
+ self.properties, self.config, self.features,
+ self._img_path, image_desc=str(self), use_desc='snapshot')
+
+ return kvm_snapshot.KVMSnapshot(
+ self.platform, self.properties, self.config,
+ self.features, instance)
+
+ def destroy(self):
+ """Clean up data associated with image."""
+ self._img_path = None
+ super(KVMImage, self).destroy()
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
index 959e9cc..d02f809 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -86,16 +86,16 @@ class Instance(object):
try:
self.write_data(script_path, script)
return self.execute(
- ['/bin/bash', script_path], rcs=rcs, description=description)
+ '/bin/bash %s' % script_path, rcs=rcs, description=description)
finally:
- self.execute(['rm', script_path], rcs=rcs)
+ self.execute('rm %s' % 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()
+ return self.execute('mktemp')[0].strip()
def console_log(self):
"""Instance console.
@@ -137,9 +137,8 @@ class Instance(object):
tests.append(self.config['cloud_init_ready_script'])
formatted_tests = ' && '.join(clean_test(t) for t in tests)
- test_cmd = ('for ((i=0;i<{time};i++)); do {test} && exit 0; sleep 1; '
- 'done; exit 1;').format(time=time, test=formatted_tests)
- cmd = ['/bin/bash', '-c', test_cmd]
+ cmd = ('for ((i=0;i<{time};i++)); do {test} && exit 0; sleep 1; '
+ 'done; exit 1;').format(time=time, test=formatted_tests)
if self.execute(cmd, rcs=(0, 1))[-1] != 0:
raise OSError('timeout: after {}s system not started'.format(time))
diff --git a/tests/cloud_tests/instances/kvm.py b/tests/cloud_tests/instances/kvm.py
new file mode 100644
index 0000000..cb8b4cc
--- /dev/null
+++ b/tests/cloud_tests/instances/kvm.py
@@ -0,0 +1,220 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base 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 KVMInstance(base.Instance):
+ """KVM backed instance."""
+
+ platform_name = "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: image properties
+ @param config: image config
+ @param features: 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
+
+ super(KVMInstance, self).__init__(
+ platform, name, properties, config, features)
+
+ def destroy(self):
+ """Clean up instance."""
+ if self.pid:
+ c_util.subp(['kill', '-9', self.pid])
+ super(KVMInstance, self).destroy()
+
+ def execute(self, command, stdout=None, stderr=None, env={},
+ 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
+ @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 self.pid:
+ out, err, exit = self.ssh(command)
+ else:
+ out, err, exit = self.mount_image_callback(command)
+
+ # write data to file descriptors if needed
+ if stdout:
+ stdout.write(out.readlines())
+ if stderr:
+ stderr.write(err.readlines())
+
+ # 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)
+
+ return (out, err, exit)
+
+ def mount_image_callback(self, command,):
+ """Run mount-image-callback."""
+ img_shell = subprocess.Popen(['sudo', 'mount-image-callback',
+ '--system-mounts', '--system-resolvconf',
+ self.name, 'chroot', '_MOUNTPOINT_',
+ '/bin/sh'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = img_shell.communicate(command.encode())
+ exit = img_shell.returncode
+ img_shell.stdin.close()
+ img_shell.wait()
+
+ return out, err, exit
+
+ def generate_seed(self, tmpdir):
+ """Generate nocloud seed from user-data"""
+ seed_file = os.path.join(tmpdir, 'seed.img')
+ user_data_file = os.path.join(tmpdir, 'user_data')
+
+ if os.path.exists(seed_file):
+ os.remove(seed_file)
+ if os.path.exists(user_data_file):
+ os.remove(user_data_file)
+
+ 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, description=''):
+ """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(KVMInstance, self).push_file()
+ else:
+ command = ('sudo mount-image-callback --system-mounts '
+ '--system-resolvconf %s -- chroot _MOUNTPOINT_ '
+ '/bin/sh -ec "cat > %s" < %s'
+ % (self.name, remote_path, local_path))
+ img_shell = subprocess.Popen(command, shell=True,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = img_shell.communicate()
+ img_shell.stdin.close()
+ img_shell.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()
+ _, out, err = client.exec_command(command)
+ 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)
+ break
+ except paramiko.SSHException:
+ time.sleep(1)
+ retry_attempts = retry_attempts - 1
+
+ return client
+
+ def start(self, wait=True, wait_for_cloud_init=False):
+ """Start instance."""
+ tmpdir = self.platform.config['data_dir']
+ pid_file = os.path.join(tmpdir, 'pid')
+ seed = self.generate_seed(tmpdir)
+ self.ssh_port = self.get_free_port()
+
+ cmd = ('qemu-system-x86_64 -enable-kvm -m 1024 '
+ '-pidfile %s -vnc none '
+ '-drive file=%s,format=qcow2,if=virtio '
+ '-drive file=%s,format=raw,if=virtio '
+ '-device virtio-net-pci,netdev=net00 '
+ '-netdev type=user,id=net00,hostfwd=tcp::%s-:22'
+ % (pid_file, self.name, seed, self.ssh_port))
+
+ subprocess.Popen(shlex.split(cmd), close_fds=True)
+ while not os.path.exists(pid_file):
+ time.sleep(1)
+
+ with open(pid_file, 'r') as pid_f:
+ self.pid = pid_f.readlines()[0].strip()
+
+ time.sleep(10)
+
+ 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/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
index b9c2cc6..c3a7c13 100644
--- a/tests/cloud_tests/instances/lxd.py
+++ b/tests/cloud_tests/instances/lxd.py
@@ -48,6 +48,7 @@ class LXDInstance(base.Instance):
"""
# ensure instance is running and execute the command
self.start()
+ command = ['/bin/bash', '-c'] + [command]
res = self.pylxd_container.execute(command, environment=env)
# get out, exit and err from pylxd return
diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
index b91834a..7bdc1f5 100644
--- a/tests/cloud_tests/platforms.yaml
+++ b/tests/cloud_tests/platforms.yaml
@@ -8,6 +8,10 @@ default_platform_config:
create_instance_timeout: 60
platforms:
+ kvm:
+ enabled: true
+ private_key: id_rsa
+ public_key: id_rsa.pub
lxd:
enabled: true
# overrides for image templates
diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py
index 443f6d4..7890dae 100644
--- a/tests/cloud_tests/platforms/__init__.py
+++ b/tests/cloud_tests/platforms/__init__.py
@@ -2,10 +2,12 @@
"""Main init."""
+from tests.cloud_tests.platforms import kvm
from tests.cloud_tests.platforms import lxd
PLATFORMS = {
'lxd': lxd.LXDPlatform,
+ 'kvm': kvm.KVMPlatform,
}
diff --git a/tests/cloud_tests/platforms/kvm.py b/tests/cloud_tests/platforms/kvm.py
new file mode 100644
index 0000000..83ca4d6
--- /dev/null
+++ b/tests/cloud_tests/platforms/kvm.py
@@ -0,0 +1,96 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base KVM platform."""
+import glob
+import os
+import shutil
+
+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 kvm as kvm_image
+from tests.cloud_tests.instances import kvm as kvm_instance
+from tests.cloud_tests.platforms import base
+from tests.cloud_tests import util
+
+
+class KVMPlatform(base.Platform):
+ """LXD test platform."""
+
+ platform_name = 'kvm'
+
+ def __init__(self, config):
+ """Set up platform."""
+ super(KVMPlatform, self).__init__(config)
+
+ def get_image(self, img_conf):
+ """Get image using specified image configuration.
+
+ @param img_conf: configuration for image
+ @return_value: cloud_tests.images instance
+ """
+ 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(img_conf['mirror_url'],
+ policy=policy)
+ tstore = objectstores.FileStore(img_conf['mirror_dir'])
+ tmirror = mirrors.ObjectFilterMirror(config=mirror_config,
+ objectstore=tstore)
+ tmirror.sync(smirror, img_conf['mirror_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 = kvm_image.KVMImage(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_image_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')
+ shutil.copy2(src_img_path, img_path)
+
+ # create copy of the latest image, return that as an instance
+ return kvm_instance.KVMInstance(self, img_path, properties, config,
+ features, user_data, meta_data)
+
+ def destroy(self):
+ """Clean up platform data."""
+ super(KVMPlatform, self).destroy()
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
index 45deb58..02993d6 100644
--- a/tests/cloud_tests/releases.yaml
+++ b/tests/cloud_tests/releases.yaml
@@ -27,7 +27,13 @@ default_release_config:
# features groups and additional feature settings
feature_groups: []
features: {}
-
+ kvm:
+ mirror_url: https://cloud-images.ubuntu.com/daily
+ mirror_path: streams/v1/index.sjson
+ mirror_dir: '/tmp/cloud_test_mirror'
+ 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 +127,9 @@ releases:
# EOL: Jul 2018
default:
enabled: true
+ release: artful
+ version: 17.10
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -134,6 +143,9 @@ releases:
# EOL: Jan 2018
default:
enabled: true
+ release: zesty
+ version: 17.04
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -147,6 +159,9 @@ releases:
# EOL: Jul 2017
default:
enabled: true
+ release: yakkety
+ version: 16.10
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -160,6 +175,9 @@ releases:
# EOL: Apr 2021
default:
enabled: true
+ release: xenial
+ version: 16.04
+ family: ubuntu
feature_groups:
- base
- debian_base
@@ -173,6 +191,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 8053a09..557b4b5 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,9 +20,9 @@ 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}\' %s' % package
elif os_family == 'redhat':
- cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package]
+ cmd = 'rpm -q --queryformat \'%{VERSION}\' %s' % package
else:
raise NotImplementedError
@@ -50,11 +51,11 @@ def install_deb(args, image):
remote_path = os.path.join('/tmp', os.path.basename(args.deb))
image.push_file(args.deb, remote_path)
cmd = 'dpkg -i {} || apt-get install --yes -f'.format(remote_path)
- image.execute(['/bin/sh', '-c', cmd], description=msg)
+ image.execute(cmd, description=msg)
# check installed deb version matches package
- fmt = ['-W', "--showformat='${Version}'"]
- (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
+ cmd = 'dpkg-deb -W --showformat=\'${Version}\' %s' % remote_path
+ (out, err, exit) = image.execute(cmd)
expected_version = out.strip()
found_version = installed_package_version(image, 'cloud-init')
if expected_version != found_version:
@@ -82,10 +83,10 @@ def install_rpm(args, image):
LOG.debug(msg)
remote_path = os.path.join('/tmp', os.path.basename(args.rpm))
image.push_file(args.rpm, remote_path)
- image.execute(['rpm', '-U', remote_path], description=msg)
+ image.execute('rpm -U %s' % remote_path, description=msg)
- fmt = ['--queryformat', '"%{VERSION}"']
- (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path])
+ cmd = 'rpm -q --queryformat \'${VERSION}\' %s' % remote_path
+ (out, err, exit) = image.execute(cmd)
expected_version = out.strip()
found_version = installed_package_version(image, 'cloud-init')
if expected_version != found_version:
@@ -113,7 +114,7 @@ def upgrade(args, image):
msg = 'upgrading cloud-init'
LOG.debug(msg)
- image.execute(['/bin/sh', '-c', cmd], description=msg)
+ image.execute(cmd, description=msg)
def upgrade_full(args, image):
@@ -134,7 +135,7 @@ def upgrade_full(args, image):
msg = 'full system upgrade'
LOG.debug(msg)
- image.execute(['/bin/sh', '-c', cmd], description=msg)
+ image.execute(cmd, description=msg)
def run_script(args, image):
@@ -165,7 +166,7 @@ def enable_ppa(args, image):
msg = 'enable ppa: "{}" in target'.format(ppa)
LOG.debug(msg)
cmd = 'add-apt-repository --yes {} && apt-get update'.format(ppa)
- image.execute(['/bin/sh', '-c', cmd], description=msg)
+ image.execute(cmd, description=msg)
def enable_repo(args, image):
@@ -188,7 +189,16 @@ def enable_repo(args, image):
msg = 'enable repo: "{}" in target'.format(args.repo)
LOG.debug(msg)
- image.execute(['/bin/sh', '-c', cmd], description=msg)
+ image.execute(cmd, description=msg)
+
+
+def generate_ssh_keys(args, image):
+ """Generate SSH keys to be used with image."""
+ LOG.info('generating SSH keys')
+ c_util.subp(['ssh-keygen', '-t', 'rsa', '-b', '4096',
+ '-f', '%s/id_rsa' % args.data_dir, '-P', '',
+ '-C', 'ubuntu@cloud_test'],
+ capture=True)
def setup_image(args, image):
@@ -226,6 +236,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, image)
return res
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/snapshots/kvm.py b/tests/cloud_tests/snapshots/kvm.py
new file mode 100644
index 0000000..c15f834
--- /dev/null
+++ b/tests/cloud_tests/snapshots/kvm.py
@@ -0,0 +1,74 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base KVM snapshot."""
+import os
+
+from tests.cloud_tests.snapshots import base
+
+
+class KVMSnapshot(base.Snapshot):
+ """KVM image copy backed snapshot."""
+
+ platform_name = "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(KVMSnapshot, 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(KVMSnapshot, self).destroy()
+
+# vi: ts=4 expandtab
Follow ups