cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #03082
[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:
Server Team CI bot (server-team-bot): continuous-integration
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.
Note, there are a couple logging drive-by changes that I made in order to
make the verbose output easier to view and more complete.
Note, I also added a check that this is not ran as root.
--
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/args.py b/tests/cloud_tests/args.py
index 369d60d..c6c1877 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/bddeb.py b/tests/cloud_tests/bddeb.py
index fe80535..90d7534 100644
--- a/tests/cloud_tests/bddeb.py
+++ b/tests/cloud_tests/bddeb.py
@@ -28,15 +28,14 @@ def build_deb(args, instance):
# update remote system package list and install build deps
LOG.debug('installing pre-reqs')
pkgs = ' '.join(pre_reqs)
- cmd = 'apt-get update && apt-get install --yes {}'.format(pkgs)
- instance.execute(['/bin/sh', '-c', cmd])
+ instance.execute('apt-get update && apt-get install --yes {}'.format(pkgs))
# local tmpfile that must be deleted
local_tarball = tempfile.NamedTemporaryFile().name
# paths to use in remote system
output_link = '/root/cloud-init_all.deb'
- remote_tarball = _out(instance.execute(['mktemp']))
+ remote_tarball = _out(instance.execute('mktemp'))
extract_dir = '/root'
bddeb_path = os.path.join(extract_dir, 'packages', 'bddeb')
git_env = {'GIT_DIR': os.path.join(extract_dir, '.git'),
@@ -49,18 +48,17 @@ def build_deb(args, instance):
instance.push_file(local_tarball, remote_tarball)
LOG.debug('extracting tarball in remote system at: %s', extract_dir)
- instance.execute(['tar', 'xf', remote_tarball, '-C', extract_dir])
- instance.execute(['git', 'commit', '-a', '-m', 'tmp', '--allow-empty'],
- env=git_env)
+ instance.execute('tar xf %s -C %s' % (remote_tarball, extract_dir))
+ instance.execute('git commit -a -m tmp --allow-empty', env=git_env)
LOG.debug('installing deps')
deps_path = os.path.join(extract_dir, 'tools', 'read-dependencies')
- instance.execute([deps_path, '--install', '--test-distro',
- '--distro', 'ubuntu', '--python-version', '3'])
+ instance.execute('%s --install --test-distro --distro ubuntu '
+ '--python-version 3' % deps_path)
LOG.debug('building deb in remote system at: %s', output_link)
- bddeb_args = args.bddeb_args.split() if args.bddeb_args else []
- instance.execute([bddeb_path, '-d'] + bddeb_args, env=git_env)
+ bddeb_args = args.bddeb_args if args.bddeb_args else ''
+ instance.execute('%s -d %s' % (bddeb_path, bddeb_args), env=git_env)
# copy the deb back to the host system
LOG.debug('copying built deb to host at: %s', args.deb)
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..af6c896 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -85,17 +85,17 @@ class Instance(object):
script_path = self.tmpfile()
try:
self.write_data(script_path, script)
- return self.execute(
- ['/bin/bash', script_path], rcs=rcs, description=description)
+ return self.execute('/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..45b30ae
--- /dev/null
+++ b/tests/cloud_tests/instances/kvm.py
@@ -0,0 +1,196 @@
+# 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
+
+
+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
+ self.pid_file = 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])
+ os.remove(self.pid_file)
+ 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:
+ return self.ssh(command)
+ else:
+ return self.mount_image_callback(command)
+
+ def mount_image_callback(self, cmd):
+ """Run mount-image-callback."""
+ mic = ('sudo mount-image-callback --system-mounts --system-resolvconf '
+ '%s -- chroot _MOUNTPOINT_ /bin/sh' % self.name)
+
+ out, err = c_util.subp(shlex.split(mic), data=cmd)
+
+ return out, err, 0
+
+ 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)
+
+ 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:
+ 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()
+ _, 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']
+ 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 --disk %s '
+ '--netdev user,hostfwd=tcp::%s-:22 '
+ '-- -pidfile %s -vnc none'
+ % (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/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..70808f2
--- /dev/null
+++ b/tests/cloud_tests/platforms/kvm.py
@@ -0,0 +1,98 @@
+# 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):
+ """KVM 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
+ """
+ (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 = 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 c8dd142..14b7352 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: {}
-
+ kvm:
+ mirror_url: https://cloud-images.ubuntu.com/daily
+ 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 +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 8053a09..d9c1cc5 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 --show --showformat=\${Version} %s' % package
elif os_family == 'redhat':
- cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package]
+ cmd = 'rpm -q --queryformat \'%{VERSION}\' %s' % package
else:
raise NotImplementedError
@@ -49,16 +50,16 @@ def install_deb(args, image):
LOG.debug(msg)
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)
+ cmd = 'dpkg -i {}; apt-get install --yes -f'.format(remote_path)
+ 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 --show --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:
- raise OSError('install deb version "{}" does not match expected "{}"'
+ raise OSError('install deb version {} does not match expected {}'
.format(found_version, expected_version))
LOG.debug('successfully installed: %s, version: %s', args.deb,
@@ -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
diff --git a/tools/xkvm b/tools/xkvm
new file mode 100755
index 0000000..a30ba91
--- /dev/null
+++ b/tools/xkvm
@@ -0,0 +1,664 @@
+#!/bin/bash
+
+set -f
+
+VERBOSITY=0
+KVM_PID=""
+DRY_RUN=false
+TEMP_D=""
+DEF_BRIDGE="virbr0"
+TAPDEVS=( )
+# OVS_CLEANUP gets populated with bridge:devname pairs used with ovs
+OVS_CLEANUP=( )
+MAC_PREFIX="52:54:00:12:34"
+KVM="kvm"
+declare -A KVM_DEVOPTS
+
+error() { echo "$@" 1>&2; }
+fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
+
+bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; }
+randmac() {
+ # return random mac addr within final 3 tokens
+ local random=""
+ random=$(printf "%02x:%02x:%02x" \
+ "$((${RANDOM}%256))" "$((${RANDOM}%256))" "$((${RANDOM}%256))")
+ padmac "$random"
+}
+
+cleanup() {
+ [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
+ [ -z "${KVM_PID}" ] || kill "$KVM_PID"
+ if [ ${#TAPDEVS[@]} -ne 0 ]; then
+ local name item
+ for item in "${TAPDEVS[@]}"; do
+ [ "${item}" = "skip" ] && continue
+ debug 1 "removing" "$item"
+ name="${item%:*}"
+ if $DRY_RUN; then
+ error ip tuntap del mode tap "$name"
+ else
+ ip tuntap del mode tap "$name"
+ fi
+ [ $? -eq 0 ] || error "failed removal of $name"
+ done
+ if [ ${#OVS_CLEANUP[@]} -ne 0 ]; then
+ # with linux bridges, there seems to be no harm in just deleting
+ # the device (not detaching from the bridge). However, with
+ # ovs, you have to remove them from the bridge, or later it
+ # will refuse to add the same name.
+ error "cleaning up ovs ports: ${OVS_CLEANUP[@]}"
+ if ${DRY_RUN}; then
+ error sudo "$0" tap-control ovs-cleanup "${OVS_CLEANUP[@]}"
+ else
+ sudo "$0" tap-control ovs-cleanup "${OVS_CLEANUP[@]}"
+ fi
+ fi
+ fi
+}
+
+debug() {
+ local level=${1}; shift;
+ [ "${level}" -gt "${VERBOSITY}" ] && return
+ error "${@}"
+}
+
+Usage() {
+ cat <<EOF
+Usage: ${0##*/} [ options ] -- kvm-args [ ... ]
+
+ run kvm with a tap interface.
+
+ options:
+ -n | --netdev NETDEV netdev can be 'user' or a bridge.
+ default is to bridge to $DEF_BRIDGE
+ -d | --disk DISK.img attach DISK.img as a disk (via virtio)
+ --dry-run only report what would be done
+
+ --uefi boot with efi
+ --uefi-nvram=FILE boot with efi, using nvram settings in FILE
+ if FILE not present, copy from defaults.
+
+ NETDEV:
+ Above, 'NETDEV' is a comma delimited string
+ The first field must be
+ * bridge name: (br0 or virbr0): attach a device to this bridge
+ * literal 'user': use qemu user networking
+
+ Additional fields are optional, and can be anything that is acceptable
+ to kvm either as:
+ * '-device virtio-net-pci' option (see 'kvm -device virtio-net-pci,?')
+ * '-net [user|tap]' option
+
+ Example:
+ * xkvm --netdev br0,macaddr=:05 -- -drive file=disk.img,if=virtio -curses
+ attach a tap device to bridge 'br0' with mac address
+ '${MAC_PREFIX}:05'
+
+ * xkvm --netdev user,mac=random --netdev br1,model=e1000,mac=auto -- -curses
+ attach virtio user networking nic with random mac address
+ attach tap device to br1 bridge as e1000 with unspecified mac
+
+ * xkvm --disk disk1.img
+EOF
+}
+
+isdevopt() {
+ local model="$1" input="${2%%=*}"
+ local out="" opt="" opts=()
+ if [ -z "${KVM_DEVOPTS[$model]}" ]; then
+ out=$($KVM -device "$model,?" 2>&1) &&
+ out=$(echo "$out" | sed -e "s,[^.]*[.],," -e 's,=.*,,') &&
+ KVM_DEVOPTS[$model]="$out" ||
+ { error "bad device model $model?"; exit 1; }
+ fi
+ opts=( ${KVM_DEVOPTS[$model]} )
+ for opt in "${opts[@]}"; do
+ [ "$input" = "$opt" ] && return 0
+ done
+ return 1
+}
+
+padmac() {
+ # return a full mac, given a subset.
+ # assume whatever is input is the last portion to be
+ # returned, and fill it out with entries from MAC_PREFIX
+ local mac="$1" num="$2" prefix="${3:-$MAC_PREFIX}" itoks="" ptoks=""
+ # if input is empty set to :$num
+ [ -n "$mac" ] || mac=$(printf "%02x" "$num") || return
+ itoks=( ${mac//:/ } )
+ ptoks=( ${prefix//:/ } )
+ rtoks=( )
+ for r in ${ptoks[@]:0:6-${#itoks[@]}} ${itoks[@]}; do
+ rtoks[${#rtoks[@]}]="0x$r"
+ done
+ _RET=$(printf "%02x:%02x:%02x:%02x:%02x:%02x" "${rtoks[@]}")
+}
+
+make_nics_Usage() {
+ cat <<EOF
+Usage: ${0##*/} tap-control make-nics [options] bridge [bridge [..]]
+
+ make a tap device on each of bridges requested
+ outputs: 'tapname:type' for each input, or 'skip' if nothing needed.
+
+ type is one of 'brctl' or 'ovs'
+EOF
+}
+
+make_nics() {
+ # takes input of list of bridges to create a tap device on
+ # and echos either 'skip' or
+ # <tapname>:<type> for each tap created
+ # type is one of "ovs" or "brctl"
+ local short_opts="v"
+ local long_opts="--verbose"
+ local getopt_out=""
+ getopt_out=$(getopt --name "${0##*/} make-nics" \
+ --options "${short_opts}" --long "${long_opts}" -- "$@") &&
+ eval set -- "${getopt_out}" || { make_nics_Usage 1>&2; return 1; }
+
+ local cur="" next=""
+ while [ $# -ne 0 ]; do
+ cur=${1}; next=${2};
+ case "$cur" in
+ -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
+ --) shift; break;;
+ esac
+ shift;
+ done
+
+ [ $# -ne 0 ] || {
+ make_nics_Usage 1>&2; error "must give bridge";
+ return 1;
+ }
+
+ local owner="" ovsbrs="" tap="" tapnum="0" brtype="" bridge=""
+ [ "$(id -u)" = "0" ] || { error "must be root for make-nics"; return 1; }
+ owner="${SUDO_USER:-root}"
+ ovsbrs=""
+ if command -v ovs-vsctl >/dev/null 2>&1; then
+ out=$(ovs-vsctl list-br)
+ out=$(echo "$out" | sed "s/\n/,/")
+ ovsbrs=",$out,"
+ fi
+ for bridge in "$@"; do
+ [ "$bridge" = "user" ] && echo skip && continue
+ [ "${ovsbrs#*,${bridge},}" != "$ovsbrs" ] &&
+ btype="ovs" || btype="brctl"
+ tapnum=0;
+ while [ -e /sys/class/net/tapvm$tapnum ]; do tapnum=$(($tapnum+1)); done
+ tap="tapvm$tapnum"
+ debug 1 "creating $tap:$btype on $bridge" 1>&2
+ ip tuntap add mode tap user "$owner" "$tap" ||
+ { error "failed to create tap '$tap' for '$owner'"; return 1; }
+ ip link set "$tap" up 1>&2 || {
+ error "failed to bring up $tap";
+ ip tuntap del mode tap "$tap";
+ return 1;
+ }
+ if [ "$btype" = "ovs" ]; then
+ ovs-vsctl add-port "$bridge" "$tap" 1>&2 || {
+ error "failed: ovs-vsctl add-port $bridge $tap";
+ ovs-vsctl del-port "$bridge" "$tap"
+ return 1;
+ }
+ else
+ ip link set "$tap" master "$bridge" 1>&2 || {
+ error "failed to add tap '$tap' to '$bridge'"
+ ip tuntap del mode tap "$tap";
+ return 1
+ }
+ fi
+ echo "$tap:$btype"
+ done
+}
+
+ovs_cleanup() {
+ [ "$(id -u)" = "0" ] ||
+ { error "must be root for ovs-cleanup"; return 1; }
+ local item="" errors=0
+ # TODO: if get owner (SUDO_USERNAME) and if that isn't
+ # the owner, then do not delete.
+ for item in "$@"; do
+ name=${item#*:}
+ bridge=${item%:*}
+ ovs-vsctl del-port "$bridge" "$name" || errors=$((errors+1))
+ done
+ return $errors
+}
+
+quote_cmd() {
+ local quote='"' x="" vline=""
+ for x in "$@"; do
+ if [ "${x#* }" != "${x}" ]; then
+ if [ "${x#*$quote}" = "${x}" ]; then
+ x="\"$x\""
+ else
+ x="'$x'"
+ fi
+ fi
+ vline="${vline} $x"
+ done
+ echo "$vline"
+}
+
+get_bios_opts() {
+ # get_bios_opts(bios, uefi, nvram)
+ # bios is a explicit bios to boot.
+ # uefi is boolean indicating uefi
+ # nvram is optional and indicates that ovmf vars should be copied
+ # to that file if it does not exist. if it exists, use it.
+ local bios="$1" uefi="${2:-false}" nvram="$3"
+ local ovmf_dir="/usr/share/OVMF"
+ local bios_opts="" pflash_common="if=pflash,format=raw"
+ unset _RET
+ _RET=( )
+ if [ -n "$bios" ]; then
+ _RET=( -drive "${pflash_common},file=$bios" )
+ return 0
+ elif ! $uefi; then
+ return 0
+ fi
+
+ # ovmf in older releases (14.04) shipped only a single file
+ # /usr/share/ovmf/OVMF.fd
+ # newer ovmf ships split files
+ # /usr/share/OVMF/OVMF_CODE.fd
+ # /usr/share/OVMF/OVMF_VARS.fd
+ # with single file, pass only one file and read-write
+ # with split, pass code as readonly and vars as read-write
+ local joined="/usr/share/ovmf/OVMF.fd"
+ local code="/usr/share/OVMF/OVMF_CODE.fd"
+ local vars="/usr/share/OVMF/OVMF_VARS.fd"
+ local split="" nvram_src=""
+ if [ -e "$code" -o -e "$vars" ]; then
+ split=true
+ nvram_src="$vars"
+ elif [ -e "$joined" ]; then
+ split=false
+ nvram_src="$joined"
+ elif [ -n "$nvram" -a -e "$nvram" ]; then
+ error "WARN: nvram given, but did not find expected ovmf files."
+ error " assuming this is code and vars (OVMF.fd)"
+ split=false
+ else
+ error "uefi support requires ovmf bios: apt-get install -qy ovmf"
+ return 1
+ fi
+
+ if [ -n "$nvram" ]; then
+ if [ ! -f "$nvram" ]; then
+ cp "$nvram_src" "$nvram" ||
+ { error "failed copy $nvram_src to $nvram"; return 1; }
+ debug 1 "copied $nvram_src to $nvram"
+ fi
+ else
+ debug 1 "uefi without --uefi-nvram storage." \
+ "nvram settings likely will not persist."
+ nvram="${nvram_src}"
+ fi
+
+ if [ ! -w "$nvram" ]; then
+ debug 1 "nvram file ${nvram} is readonly"
+ nvram_ro="readonly"
+ fi
+
+ if $split; then
+ # to ensure bootability firmware must be first, then variables
+ _RET=( -drive "${pflash_common},file=$code,readonly" )
+ fi
+ _RET=( "${_RET[@]}"
+ -drive "${pflash_common},file=$nvram${nvram_ro:+,${nvram_ro}}" )
+}
+
+main() {
+ local short_opts="hd:n:v"
+ local long_opts="bios:,help,dowait,disk:,dry-run,kvm:,no-dowait,netdev:,uefi,uefi-nvram:,verbose"
+ local getopt_out=""
+ getopt_out=$(getopt --name "${0##*/}" \
+ --options "${short_opts}" --long "${long_opts}" -- "$@") &&
+ eval set -- "${getopt_out}" || { bad_Usage; return 1; }
+
+ local bridge="$DEF_BRIDGE" oifs="$IFS"
+ local netdevs="" need_tap="" ret="" p="" i="" pt="" cur="" conn=""
+ local kvm="" kvmcmd="" archopts=""
+ local def_disk_driver=${DEF_DISK_DRIVER:-"virtio-blk"}
+ local def_netmodel=${DEF_NETMODEL:-"virtio-net-pci"}
+ local bios="" uefi=false uefi_nvram=""
+
+ archopts=( )
+ kvmcmd=( )
+ netdevs=( )
+ addargs=( )
+ diskdevs=( )
+ diskargs=( )
+
+ # dowait: run qemu-system with a '&' and then 'wait' on the pid.
+ # the reason to do this or not do this has to do with interactivity
+ # if detached with &, then user input will not go to xkvm.
+ # if *not* detached, then signal handling is blocked until
+ # the foreground subprocess returns. which means we can't handle
+ # a sigterm and kill the qemu-system process.
+ # We default to dowait=false if input and output are a terminal
+ local dowait=""
+ [ -t 0 -a -t 1 ] && dowait=false || dowait=true
+ while [ $# -ne 0 ]; do
+ cur=${1}; next=${2};
+ case "$cur" in
+ -h|--help) Usage; exit 0;;
+ -d|--disk)
+ diskdevs[${#diskdevs[@]}]="$next"; shift;;
+ --dry-run) DRY_RUN=true;;
+ --kvm) kvm="$next"; shift;;
+ -n|--netdev)
+ netdevs[${#netdevs[@]}]=$next; shift;;
+ -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
+ --dowait) dowait=true;;
+ --no-dowait) dowait=false;;
+ --bios) bios="$next"; shift;;
+ --uefi) uefi=true;;
+ --uefi-nvram) uefi=true; uefi_nvram="$next"; shift;;
+ --) shift; break;;
+ esac
+ shift;
+ done
+
+ [ ${#netdevs[@]} -eq 0 ] && netdevs=( "${DEF_BRIDGE}" )
+ pt=( "$@" )
+
+ local kvm_pkg="" virtio_scsi_bus="virtio-scsi-pci"
+ [ -n "$kvm" ] && kvm_pkg="none"
+ case $(uname -m) in
+ i?86)
+ [ -n "$kvm" ] ||
+ { kvm="qemu-system-i386"; kvm_pkg="qemu-system-x86"; }
+ ;;
+ x86_64)
+ [ -n "$kvm" ] ||
+ { kvm="qemu-system-x86_64"; kvm_pkg="qemu-system-x86"; }
+ ;;
+ s390x)
+ [ -n "$kvm" ] ||
+ { kvm="qemu-system-s390x"; kvm_pkg="qemu-system-misc"; }
+ def_netmodel=${DEF_NETMODEL:-"virtio-net-ccw"}
+ virtio_scsi_bus="virtio-scsi-ccw"
+ ;;
+ ppc64*)
+ [ -n "$kvm" ] ||
+ { kvm="qemu-system-ppc64"; kvm_pkg="qemu-system-ppc"; }
+ def_netmodel="virtio-net-pci"
+ # virtio seems functional on in 14.10, but might want scsi here
+ #def_diskif="scsi"
+ archopts=( "${archopts[@]}" -machine pseries,usb=off )
+ archopts=( "${archopts[@]}" -device spapr-vscsi )
+ ;;
+ *) kvm=qemu-system-$(uname -m);;
+ esac
+ KVM="$kvm"
+ kvmcmd=( $kvm -enable-kvm )
+
+ local bios_opts=""
+ if [ -n "$bios" ] && $uefi; then
+ error "--uefi (or --uefi-nvram) is incompatible with --bios"
+ return 1
+ fi
+ get_bios_opts "$bios" "$uefi" "$uefi_nvram" ||
+ { error "failed to get bios opts"; return 1; }
+ bios_opts=( "${_RET[@]}" )
+
+ local out="" fmt="" bus="" unit="" index="" serial="" driver="" devopts=""
+ local busorindex="" driveopts="" cur="" val="" file=""
+ for((i=0;i<${#diskdevs[@]};i++)); do
+ cur=${diskdevs[$i]}
+ IFS=","; set -- $cur; IFS="$oifs"
+ driver=""
+ id=$(printf "disk%02d" "$i")
+ file=""
+ fmt=""
+ bus=""
+ unit=""
+ index=""
+ serial=""
+ for tok in "$@"; do
+ [ "${tok#*=}" = "${tok}" -a -f "${tok}" -a -z "$file" ] && file="$tok"
+ val=${tok#*=}
+ case "$tok" in
+ driver=*) driver=$val;;
+ if=virtio) driver=virtio-blk;;
+ if=scsi) driver=scsi-hd;;
+ if=pflash) driver=;;
+ if=sd|if=mtd|floppy) fail "do not know what to do with $tok on $cur";;
+ id=*) id=$val;;
+ file=*) file=$val;;
+ fmt=*|format=*) fmt=$val;;
+ serial=*) serial=$val;;
+ bus=*) bus=$val;;
+ unit=*) unit=$val;;
+ index=*) index=$val;;
+ esac
+ done
+ [ -z "$file" ] && fail "did not read a file from $cur"
+ if [ -f "$file" -a -z "$fmt" ]; then
+ out=$(LANG=C qemu-img info "$file") &&
+ fmt=$(echo "$out" | awk '$0 ~ /^file format:/ { print $3 }') ||
+ { error "failed to determine format of $file"; return 1; }
+ else
+ fmt=raw
+ fi
+ if [ -z "$driver" ]; then
+ driver="$def_disk_driver"
+ fi
+ if [ -z "$serial" ]; then
+ serial="${file##*/}"
+ fi
+
+ # make sure we add either bus= or index=
+ if [ -n "$bus" -o "$unit" ] && [ -n "$index" ]; then
+ fail "bus and index cant be specified together: $cur"
+ elif [ -z "$bus" -a -z "$unit" -a -z "$index" ]; then
+ index=$i
+ elif [ -n "$bus" -a -z "$unit" ]; then
+ unit=$i
+ fi
+
+ busorindex="${bus:+bus=$bus,unit=$unit}${index:+index=${index}}"
+ diskopts="file=${file},id=$id,if=none,format=$fmt,$busorindex"
+ devopts="$driver,drive=$id${serial:+,serial=${serial}}"
+ for tok in "$@"; do
+ case "$tok" in
+ id=*|if=*|driver=*|$file|file=*) continue;;
+ fmt=*|format=*) continue;;
+ serial=*|bus=*|unit=*|index=*) continue;;
+ esac
+ isdevopt "$driver" "$tok" && devopts="${devopts},$tok" ||
+ diskopts="${diskopts},${tok}"
+ done
+
+ diskargs=( "${diskargs[@]}" -drive "$diskopts" -device "$devopts" )
+ done
+
+ local mnics_vflag=""
+ for((i=0;i<${VERBOSITY}-1;i++)); do mnics_vflag="${mnics_vflag}v"; done
+ [ -n "$mnics_vflag" ] && mnics_vflag="-${mnics_vflag}"
+
+ # now go through and split out options
+ # -device virtio-net-pci,netdev=virtnet0,mac=52:54:31:15:63:02
+ # -netdev type=tap,id=virtnet0,vhost=on,script=/etc/kvm/kvm-ifup.br0,downscript=no
+ local netopts="" devopts="" id="" need_taps=0 model=""
+ local device_args netdev_args
+ device_args=( )
+ netdev_args=( )
+ connections=( )
+ for((i=0;i<${#netdevs[@]};i++)); do
+ id=$(printf "net%02d" "$i")
+ netopts="";
+ devopts=""
+ # mac=auto is 'unspecified' (let qemu assign one)
+ mac="auto"
+ #vhost="off"
+
+ IFS=","; set -- ${netdevs[$i]}; IFS="$oifs"
+ bridge=$1; shift;
+ if [ "$bridge" = "user" ]; then
+ netopts="type=user"
+ ntype="user"
+ connections[$i]="user"
+ else
+ need_taps=1
+ ntype="tap"
+ netopts="type=tap"
+ connections[$i]="$bridge"
+ fi
+ netopts="${netopts},id=$id"
+ [ "$ntype" = "tap" ] && netopts="${netopts},script=no,downscript=no"
+
+ model="${def_netmodel}"
+ for tok in "$@"; do
+ [ "${tok#model=}" = "${tok}" ] && continue
+ case "${tok#model=}" in
+ virtio) model=virtio-net-pci;;
+ *) model=${tok#model=};;
+ esac
+ done
+
+ for tok in "$@"; do
+ case "$tok" in
+ mac=*) mac="${tok#mac=}"; continue;;
+ macaddr=*) mac=${tok#macaddr=}; continue;;
+ model=*) continue;;
+ esac
+
+ isdevopt "$model" "$tok" && devopts="${devopts},$tok" ||
+ netopts="${netopts},${tok}"
+ done
+ devopts=${devopts#,}
+ netopts=${netopts#,}
+
+ if [ "$mac" != "auto" ]; then
+ [ "$mac" = "random" ] && randmac && mac="$_RET"
+ padmac "$mac" "$i"
+ devopts="${devopts:+${devopts},}mac=$_RET"
+ fi
+ devopts="$model,netdev=$id${devopts:+,${devopts}}"
+ #netopts="${netopts},vhost=${vhost}"
+
+ device_args[$i]="$devopts"
+ netdev_args[$i]="$netopts"
+ done
+
+ trap cleanup EXIT
+
+ reqs=( "$kvm" )
+ pkgs=( "$kvm_pkg" )
+ for((i=0;i<${#reqs[@]};i++)); do
+ req=${reqs[$i]}
+ pkg=${pkgs[$i]}
+ [ "$pkg" = "none" ] && continue
+ command -v "$req" >/dev/null || {
+ missing="${missing:+${missing} }${req}"
+ missing_pkgs="${missing_pkgs:+${missing_pkgs} }$pkg"
+ }
+ done
+ if [ -n "$missing" ]; then
+ local reply cmd=""
+ cmd=( sudo apt-get --quiet install ${missing_pkgs} )
+ error "missing prereqs: $missing";
+ error "install them now with the following?: ${cmd[*]}"
+ read reply && [ "$reply" = "y" -o "$reply" = "Y" ] ||
+ { error "run: apt-get install ${missing_pkgs}"; return 1; }
+ "${cmd[@]}" || { error "failed to install packages"; return 1; }
+ fi
+
+ if [ $need_taps -ne 0 ]; then
+ local missing="" missing_pkgs="" reqs="" req="" pkgs="" pkg=""
+ for i in "${connections[@]}"; do
+ [ "$i" = "user" -o -e "/sys/class/net/$i" ] ||
+ missing="${missing} $i"
+ done
+ [ -z "$missing" ] || {
+ error "cannot create connection on: ${missing# }."
+ error "bridges do not exist.";
+ return 1;
+ }
+ error "creating tap devices: ${connections[*]}"
+ if $DRY_RUN; then
+ error "sudo $0 tap-control make-nics" \
+ $mnics_vflag "${connections[@]}"
+ taps=""
+ for((i=0;i<${#connections[@]};i++)); do
+ if [ "${connections[$i]}" = "user" ]; then
+ taps="${taps} skip"
+ else
+ taps="${taps} dryruntap$i:brctl"
+ fi
+ done
+ else
+ taps=$(sudo "$0" tap-control make-nics \
+ ${mnics_vflag} "${connections[@]}") ||
+ { error "$failed to make-nics ${connections[*]}"; return 1; }
+ fi
+ TAPDEVS=( ${taps} )
+ for((i=0;i<${#TAPDEVS[@]};i++)); do
+ cur=${TAPDEVS[$i]}
+ [ "${cur#*:}" = "ovs" ] || continue
+ conn=${connections[$i]}
+ OVS_CLEANUP[${#OVS_CLEANUP[@]}]="${conn}:${cur%:*}"
+ done
+
+ debug 2 "tapdevs='${TAPDEVS[@]}'"
+ [ ${#OVS_CLEANUP[@]} -eq 0 ] || error "OVS_CLEANUP='${OVS_CLEANUP[*]}'"
+
+ for((i=0;i<${#TAPDEVS[@]};i++)); do
+ cur=${TAPDEVS[$i]}
+ [ "$cur" = "skip" ] && continue
+ netdev_args[$i]="${netdev_args[$i]},ifname=${cur%:*}";
+ done
+ fi
+
+ netargs=()
+ for((i=0;i<${#device_args[@]};i++)); do
+ netargs=( "${netargs[@]}" -device "${device_args[$i]}"
+ -netdev "${netdev_args[$i]}")
+ done
+
+ local bus_devices
+ bus_devices=( -device "$virtio_scsi_bus,id=virtio-scsi-xkvm" )
+ cmd=( "${kvmcmd[@]}" "${archopts[@]}"
+ "${bios_opts[@]}"
+ "${bus_devices[@]}"
+ "${netargs[@]}"
+ "${diskargs[@]}" "${pt[@]}" )
+ local pcmd=$(quote_cmd "${cmd[@]}")
+ error "$pcmd"
+ ${DRY_RUN} && return 0
+
+ if $dowait; then
+ "${cmd[@]}" &
+ KVM_PID=$!
+ debug 1 "kvm pid=$KVM_PID. my pid=$$"
+ wait
+ ret=$?
+ KVM_PID=""
+ else
+ "${cmd[@]}"
+ ret=$?
+ fi
+ return $ret
+}
+
+
+if [ "$1" = "tap-control" ]; then
+ shift
+ mode=$1
+ shift || fail "must give mode to tap-control"
+ case "$mode" in
+ make-nics) make_nics "$@";;
+ ovs-cleanup) ovs_cleanup "$@";;
+ *) fail "tap mode must be either make-nics or ovs-cleanup";;
+ esac
+else
+ main "$@"
+fi
+
+# vi: ts=4 expandtab
Follow ups
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Scott Moser, 2017-09-08
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Scott Moser, 2017-09-08
-
[Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Joshua Powers, 2017-09-08
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Joshua Powers, 2017-09-08
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Chad Smith, 2017-09-06
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Ryan Harper, 2017-09-06
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Server Team CI bot, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Server Team CI bot, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Joshua Powers, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Chad Smith, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Chad Smith, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Chad Smith, 2017-08-18
-
Re: [Merge] ~powersj/cloud-init:cii-kvm into cloud-init:master
From: Server Team CI bot, 2017-08-16