diff -Nru ols-vms-1.3.0/debian/bzr-builder.manifest ols-vms-1.3.1/debian/bzr-builder.manifest --- ols-vms-1.3.0/debian/bzr-builder.manifest 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/debian/bzr-builder.manifest 2018-01-25 19:29:29.000000000 +0000 @@ -1,2 +1,2 @@ -# bzr-builder format 0.3 deb-version {debupstream}-0~317 -lp:~vila/ols-vms/trunk revid:v.ladeuil+lp@free.fr-20171219143953-z2pjwu4ej7jyofnb +# bzr-builder format 0.3 deb-version {debupstream}-0~324 +lp:~vila/ols-vms/trunk revid:v.ladeuil+lp@free.fr-20180125130031-csb3nhio7hoyyjxg diff -Nru ols-vms-1.3.0/debian/changelog ols-vms-1.3.1/debian/changelog --- ols-vms-1.3.0/debian/changelog 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/debian/changelog 2018-01-25 19:29:29.000000000 +0000 @@ -1,8 +1,37 @@ -ols-vms (1.3.0-0~317~ubuntu18.04.1) bionic; urgency=low +ols-vms (1.3.1-0~324~ubuntu18.04.1) bionic; urgency=low * Auto build. - -- Vincent Ladeuil Tue, 19 Dec 2017 14:59:27 +0000 + -- Vincent Ladeuil Thu, 25 Jan 2018 19:29:29 +0000 + +ols-vms (1.3.1) unstable; urgency=medium + + * Fix debian support for ephemeral-lxd. + + * Add a 'version' command. + + * 'ols-vms config' now expand options in a file when using '@' + as the option name. This is not (yet) documented as the API may change + in the future. + + * 'teardown' now accepts a '--force' parameter which stops the vm if it's + running. The default is to raise an error. + + * Support more setup for ephemeral (setup_over_ssh() which is installing + packages and running additional setup). + + * Add a 'vm.setup.hook' configuration option to execute a command on the + *host* or a script if prefixed with '@'. + + * 'teardown --force' has been re-implemented to give more freedom to + backend implementations. The 'scaleway' backend has a way to terminate a + server when stopping it which benefits from the new implementation. + + * A new 'scaleway' backend has been implemented as well as a + 'bootstrap-scaleway-image' script to create up to date images including + cloud-init. + + -- Vincent Ladeuil Thu, 25 Jan 2018 13:57:18 +0100 ols-vms (1.3.0) unstable; urgency=medium diff -Nru ols-vms-1.3.0/debian/control ols-vms-1.3.1/debian/control --- ols-vms-1.3.0/debian/control 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/debian/control 2018-01-25 19:29:29.000000000 +0000 @@ -2,7 +2,7 @@ Section: python Priority: optional Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Vincent Ladeuil +XSBC-Original-Maintainer: Vincent Ladeuil Build-Depends: debhelper (>= 9), dh-python, openssh-client, @@ -10,11 +10,13 @@ python-setuptools, python-ols-config, python-ols-tests, + python-requests, python-yaml, python3-all, python3-setuptools, python3-ols-config, python3-ols-tests, + python3-requests, python3-yaml Standards-Version: 3.9.4 @@ -22,6 +24,7 @@ Architecture: all Depends: openssh-client, python-ols-config, + python-requests, python-yaml, ${misc:Depends}, ${python:Depends} @@ -31,6 +34,7 @@ Architecture: all Depends: openssh-client, python3-ols-config, + python3-requests, python3-yaml, ${misc:Depends}, ${python3:Depends} diff -Nru ols-vms-1.3.0/NEWS.rst ols-vms-1.3.1/NEWS.rst --- ols-vms-1.3.0/NEWS.rst 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/NEWS.rst 2018-01-25 19:29:29.000000000 +0000 @@ -4,8 +4,8 @@ Overview of changes to ols-vms in reverse chronological order. -dev -=== +1.3.1 +===== * Fix debian support for ephemeral-lxd. @@ -24,6 +24,14 @@ * Add a 'vm.setup.hook' configuration option to execute a command on the *host* or a script if prefixed with '@'. + * 'teardown --force' has been re-implemented to give more freedom to + backend implementations. The 'scaleway' backend has a way to terminate a + server when stopping it which benefits from the new implementation. + + * A new 'scaleway' backend has been implemented as well as a + 'bootstrap-scaleway-image' script to create up to date images including + cloud-init. + 1.3.0 ===== diff -Nru ols-vms-1.3.0/olsvms/commands.py ols-vms-1.3.1/olsvms/commands.py --- ols-vms-1.3.0/olsvms/commands.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/commands.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -32,6 +33,7 @@ errors, ) from olsvms.vms import ( + scaleway, libvirt, lxd, ) @@ -48,6 +50,8 @@ 'Lxd virtual machine') config.vm_class_registry.register('ephemeral-lxd', lxd.EphemeralLxd, 'Linux container ephemeral virtual machine') +config.vm_class_registry.register('scaleway', scaleway.Scaleway, + 'Scaleway server') if sys.version_info < (3,): try: from olsvms.vms import nova @@ -606,15 +610,9 @@ state = self.vm.state() if state is None: raise errors.VmUnknown(self.vm_name) - # FIXME: states need to be defined uniquely across the various vms - # implementations -- vila 2014-01-17 - if state in ('running', 'RUNNING'): - if self.options.force: - self.vm.stop() - state = self.vm.state() - else: - raise errors.VmRunning(self.vm_name) - self.vm.teardown() + if state in ('running', 'RUNNING') and not self.options.force: + raise errors.VmRunning(self.vm_name) + self.vm.teardown(force=self.options.force) return 0 diff -Nru ols-vms-1.3.0/olsvms/config.py ols-vms-1.3.1/olsvms/config.py --- ols-vms-1.3.0/olsvms/config.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/config.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -140,7 +141,8 @@ super(VmStack, self).__init__( section_getters, self.user_store, mutable_section_id=name) - # FIXME: This should be a DictOption -- vila 2016-01-05 + # FIXME: This should be a DictOption or a NameSpaceOption + # -- vila 2018-01-08 def get_nova_creds(self): """Get nova credentials from a config. @@ -157,6 +159,25 @@ creds[opt_name] = self.get(opt_name) return creds + def get_scaleway_creds(self): + """Get nova credentials from a config. + + This defines the set of options needed to authenticate against scaleway + in a single place. + + :raises: olsconfig.errors.OptionMandatoryValueError if one of the + options is not set. + + """ + # In theory the token is region agnostic, in practice you can't use + # compute without a region (the api host name includes the region + # name). + creds = {} + for k in ('access_key', 'token', 'region_name'): + opt_name = 'scaleway.{}'.format(k) + creds[opt_name] = self.get(opt_name) + return creds + class ExistingVmStack(stacks.Stack): """Internal stack for defined vms.""" @@ -419,7 +440,6 @@ register(options.ListOption('vm.setup.digest.options', default='vm.name, vm.class,' 'vm.distribution, vm.release, vm.architecture,' - 'vm.image,' 'vm.user, {vm.distribution}.user,' 'vm.fqdn, vm.manage_etc_hosts,' 'vm.cpus, vm.ram_size, vm.disk_size,' @@ -469,6 +489,9 @@ If prefixed with '@', it's a script where options are expanded before execution. +FIXME: The above is misleading as @ is not required to use a script as a +command -- vila 2018-01-10 + Note: This runs before 'vm.setup_scripts'. ''')) register(options.ListOption('vm.setup_scripts', default=None, @@ -486,6 +509,121 @@ none is provided to address compatibility with Ubuntu precise.''')) ###################################################################### +# scaleway options +###################################################################### +register(options.Option('scaleway.access_key', + default=options.MANDATORY, + help_string='''The scaleway access key. + +See https://cloud.scaleway.com/#/credentials for details. +''')) +register(options.Option('scaleway.token', + default=options.MANDATORY, + help_string='''The scaleway authorization token. + +See https://cloud.scaleway.com/#/credentials for valid ones. +''')) +register(options.Option('scaleway.region_name', + default=options.MANDATORY, + help_string='''The scaleway region name. +See https://scaleway.com. +''')) +register(options.Option('scaleway.flavor', + default=options.MANDATORY, + help_string='''\ +A scaleway commercial type for the server. + +See https://scaleway.com. +''')) +register(options.Option('scaleway.image.bootstrap', + from_unicode=options.bool_from_store, + default='false', + help_string='''\ +Disable cloud-init check when creating scaleway images. + +Images provided by scaleway don't include cloud-init. This option disables the +checks for cloud-init completion. +''')) +# FIXME: scaleway.bootscript is missing -- vila 2018-01-24 +register(options.Option( + 'scaleway.image', + default='{vm.distribution}/{vm.release}/{vm.architecture}', + help_string='''\ +The image to boot from. + +See https://www.scaleway.com/imagehub/. +''')) +register(options.Option('scaleway.poweron_timeout', default='900', + from_unicode=options.float_from_store, + help_string='''\ +Max time to power on a scaleway server (in seconds).''')) +register(options.Option('scaleway.poweroff_timeout', default='600', + from_unicode=options.float_from_store, + help_string='''\ +Max time to power off a scaleway server (in seconds).''')) +register(options.Option('scaleway.terminate_timeout', default='300', + from_unicode=options.float_from_store, + help_string='''\ +Max time to terminate a scaleway server (in seconds).''')) +# FIXME: It takes around 3 minutes to boot and get a dynamic IP for C1, check +# whether it's faster with a static one -- vila 2018-01-14 +register(options.ListOption('scaleway.setup_ip_timeouts', + default='60, 180, 20', + help_string='''\ +A timeouts tuple to use when waiting for scaleway to setup an IP. + +(first, up_to, retries): +- first: seconds to wait after the first attempt +- up_to: seconds after which to give up +- retries: how many attempts after the first try +''')) +register(options.ListOption('scaleway.setup_ssh_timeouts', + default='0, 180, 20', + help_string='''\ +A timeouts tuple to use when waiting for scaleway to setup ssh. + +(first, up_to, retries): +- first: seconds to wait after the first attempt +- up_to: seconds after which to give up +- retries: how many attempts after the first try +''')) +register(options.ListOption('scaleway.cloud_init_timeouts', + default='0, 240, 20', + help_string='''\ +A timeouts tuple to use when waiting for cloud-init completion. + +(first, up_to, retries): +- first: seconds to wait after the first attempt +- up_to: seconds after which to give up +- retries: how many attempts after the first try +''')) +register(options.ListOption('scaleway.setup.digest.options', + default='scaleway.flavor, scaleway.image,' + 'scaleway.bootscript, scaleway.isntance_id', + help_string='''\ +A list of scaleway related options that are used to define a vm. + +The values for these options are hashed to produce 'vm.setup.digest'. +''')) +register(options.Option( + 'scaleway.compute.url', + default='https://cp-{scaleway.region_name}.scaleway.com', + help_string='''The scaleway compute api url. + +See https://developer.scaleway.com. +''')) +register(options.ListOption('scaleway.compute.timeouts', + default='2, 30, 10', + help_string='''\ +A timeouts tuple to use when talking to the scaleway compute server. + +(first, up_to, retries): +- first: seconds to wait after the first attempt +- up_to: seconds after which to give up +- retries: how many attempts after the first try +''')) + +###################################################################### # libvirt options ###################################################################### @@ -724,7 +862,7 @@ # enough.. -- vila 2016-09-07 register(options.ListOption('apt.options', default='--option=Dpkg::Options::=--force-confold,' - ' --option=Dpkg::options::=--force-unsafe-io,' + ' --option=Dpkg::Options::=--force-unsafe-io,' ' --assume-yes, --quiet, --no-install-recommends', help_string='''apt-get install options.''')) register(options.Option('apt.proxy', default=None, invalid='error', @@ -749,6 +887,14 @@ When apt-get update fails on hash sum mismatches, retry after the specified timeouts. More values mean more retries. ''')) +register(options.ListOption('apt.upgrade.timeouts', + default='15.0, 90.0, 240.0', + help_string='''\ +apt-get dist-upgrade timeouts in seconds. + +When apt-get fails with retcode 100 (Could not get lock /var/lib/dpkg/lock), +retry after the specified timeouts. More values mean more retries. +''')) register(options.ListOption('apt.setup.digest.options', default='apt.options, apt.sources', help_string='''\ diff -Nru ols-vms-1.3.0/olsvms/__init__.py ols-vms-1.3.1/olsvms/__init__.py --- ols-vms-1.3.0/olsvms/__init__.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/__init__.py 2018-01-25 19:29:29.000000000 +0000 @@ -19,7 +19,7 @@ # the release level is 'dev' or 'final'. The # version_info value corresponding to the olsvms version 2.0 is (2, 0, 0, # 'final', 0). -__version__ = (1, 3, 1, 'dev', 0) +__version__ = (1, 3, 1, 'final', 0) def version(ver=None): diff -Nru ols-vms-1.3.0/olsvms/subprocesses.py ols-vms-1.3.1/olsvms/subprocesses.py --- ols-vms-1.3.0/olsvms/subprocesses.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/subprocesses.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -24,15 +25,19 @@ logger = logging.getLogger(__name__) -def run(args, cmd_input=None): +def run(args, cmd_input=None, raise_on_error=True): """Run the specified command capturing output and errors. :param args: A list of a command and its arguments. :param cmd_input: A unicode string to feed the command with. + :param raise_on_error: A boolean controlling whether or not an exception is + raised if the command fails. + :return: A tuple of the return code, the output and the errors as unicode strings. + """ stdin = None if cmd_input is not None: @@ -46,7 +51,7 @@ out, err = proc.communicate(cmd_input) out = out.decode('utf8') err = err.decode('utf8') - if proc.returncode: + if raise_on_error and proc.returncode: raise errors.CommandError(args, proc.returncode, out, err) return proc.returncode, out, err diff -Nru ols-vms-1.3.0/olsvms/tests/features.py ols-vms-1.3.1/olsvms/tests/features.py --- ols-vms-1.3.0/olsvms/tests/features.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/features.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -153,6 +154,19 @@ return 'A valid set of nova credentials' +class ScalewayCredentials(features.Feature): + + def _probe(self): + try: + config.VmStack('ols-vms-tests-scaleway').get_scaleway_creds() + except errors.OptionMandatoryValueError: + return False + return True + + def feature_name(self): + return 'A valid set of scaleway credentials' + + # We can't use PathOption below as '~' won't be expanded properly once the test # is isolated from disk. config.register(config.options.Option( @@ -212,5 +226,6 @@ ssh_feature = SshFeature() nova_compute = NovaCompute() nova_creds = NovaCredentials() +scaleway_creds = ScalewayCredentials() tests_config = TestsConfig() lxd_nesting_1 = LxdNesting(1) diff -Nru ols-vms-1.3.0/olsvms/tests/fixtures.py ols-vms-1.3.1/olsvms/tests/fixtures.py --- ols-vms-1.3.0/olsvms/tests/fixtures.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/fixtures.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -27,6 +28,13 @@ ) from olsvms.tests import features +try: + if sys.version_info < (3,): + # novaclient doesn't support python3 (yet) + from olsvms.vms import nova +except ImportError: + pass + # Useful shorcuts patch = fixtures.patch @@ -147,3 +155,49 @@ # Install the new level, restoring the actual one after the test test.addCleanup(root_logger.setLevel, root_logger.level) root_logger.setLevel(level) + + +def per_backend_release_arch_setup(test): + """Prepare setting up vm for parmetrization by backend, release and arch. + + This is used by the per_vm tests. + """ + # Some classes require additional features + required_features = { + 'lxd': features.lxd_client_feature, + 'nova': features.nova_creds, + 'scaleway': features.scaleway_creds, + } + if test.kls in required_features: + f = required_features[test.kls] + if not f.available(): + test.skipTest('{} is not available'.format(f.feature_name())) + # Create a shared config + conf = config.VmStack(None) + conf.store._load_from_string(''' +[{vm_name}] +vm.name = {vm_name} +vm.class = {kls} +'''.format(vm_name=test.vm_name, kls=test.kls)) + conf.set('vm.release', test.series) + conf.set('vm.architecture', test.arch) + # Some classes require additional and specific setup + + def nova_setup(test, conf): + image_id = nova.ols_image_name('cloudimg', test.series, test.arch) + conf.set('nova.image', image_id) + + def scaleway_setup(test, conf): + conf.set('scaleway.flavor', 'VC1S') + # conf.set('scaleway.flavor', 'C1') + # conf.set('vm.architecture', 'armhf') + + specific_setups = { + 'nova': nova_setup, + 'scaleway': scaleway_setup, + } + if test.kls in specific_setups: + setup = specific_setups[test.kls] + setup(test, conf) + conf.store.save() + conf.store.unload() diff -Nru ols-vms-1.3.0/olsvms/tests/per_vm/test_install_packages.py ols-vms-1.3.1/olsvms/tests/per_vm/test_install_packages.py --- ols-vms-1.3.0/olsvms/tests/per_vm/test_install_packages.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/per_vm/test_install_packages.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -18,7 +19,6 @@ import io import os import unittest -import sys from olstests import scenarii from olsvms import ( @@ -31,14 +31,6 @@ ) -try: - if sys.version_info < (3,): - # novaclient doesn't support python3 (yet) - from olsvms.vms import nova -except ImportError: - pass - - load_tests = scenarii.load_tests_with_scenarios @@ -48,58 +40,19 @@ scenarios = [(k, dict(kls=k, series='xenial', arch='amd64')) for k in config.vm_class_registry.keys() - # ephemeral-lxd don't have setup which is assumed in the - # following tests. - # something is broken with libvirt (console owned by root is - # back /o\) + # - ephemeral-lxd don't have setup which is assumed in the + # following tests. + # - something is broken with libvirt (console owned by root is + # back /o\) if k not in ('libvirt', 'ephemeral-lxd')] def setUp(self): super(TestInstallPackages, self).setUp() fixtures.setup_tests_config(self) fixtures.set_uniq_vm_name(self) - # Some classes require additional features - required_features = { - 'libvirt': features.use_sudo_for_tests_feature, - 'lxd': features.lxd_client_feature, - 'nova': features.nova_creds, - } - if self.kls in required_features: - f = required_features[self.kls] - if not f.available(): - self.skipTest('{} is not available'.format(f.feature_name())) - # Create a shared config - conf = config.VmStack(None) - conf.store._load_from_string(''' -[{vm_name}] -vm.name = {vm_name} -vm.class = {kls} -'''.format(vm_name=self.vm_name, kls=self.kls)) - # Some classes require additional setup - specific_setups = { - 'libvirt': self.libvirt_setup, - 'lxd': self.lxd_setup, - 'nova': self.nova_setup, - } - if self.kls in specific_setups: - setup = specific_setups[self.kls] - setup(conf) - conf.store.save() - conf.store.unload() self.log_stream = io.StringIO() fixtures.override_logging(self, self.log_stream) - - def libvirt_setup(self, conf): - conf.set('vm.release', self.series) - conf.set('vm.architecture', self.arch) - - def lxd_setup(self, conf): - conf.set('vm.release', self.series) - conf.set('vm.architecture', self.arch) - - def nova_setup(self, conf): - image_id = nova.ols_image_name('cloudimg', self.series, self.arch) - conf.set('nova.image', image_id) + fixtures.per_backend_release_arch_setup(self) def prepare_vm(self, packages): """Prepare a vm to be setup with a list of packages as a string. @@ -112,13 +65,14 @@ vm = vm_class(conf) vm.conf.set('vm.packages', packages) self.addCleanup(self.post_teardown_checks, vm) - self.addCleanup(vm.teardown) - self.addCleanup(vm.stop) + self.addCleanup(vm.teardown, True) return vm def post_teardown_checks(self, vm): # Ensure the config directory was removed - self.assertFalse(os.path.exists(vm.config_dir_path())) + path = vm.config_dir_path() + self.assertFalse(os.path.exists(path), + '{} still exist'.format(path)) def test_install_nothing(self): vm = self.prepare_vm('') diff -Nru ols-vms-1.3.0/olsvms/tests/per_vm/test_publish.py ols-vms-1.3.1/olsvms/tests/per_vm/test_publish.py --- ols-vms-1.3.0/olsvms/tests/per_vm/test_publish.py 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/per_vm/test_publish.py 2018-01-25 19:29:29.000000000 +0000 @@ -0,0 +1,71 @@ +# This file is part of Online Services virtual machine tools. +# +# Copyright 2018 Vincent Ladeuil. +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3, as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . +from __future__ import unicode_literals + +import io +import unittest + +from olstests import scenarii +from olsvms import config +from olsvms.tests import ( + features, + fixtures, +) + + +load_tests = scenarii.load_tests_with_scenarios + + +@features.requires(features.tests_config) +class TestPublish(unittest.TestCase): + + scenarios = [(k, dict(kls=k, series='xenial', arch='amd64')) + for k in config.vm_class_registry.keys() + # - ephemeral lxcs are not meant to install packages + # - something is broken with libvirt (console owned by root is + # back /o\) + # - nova doesn't implement publish + if k not in ('nova', 'libvirt', 'ephemeral-lxd',)] + + def setUp(self): + super(TestPublish, self).setUp() + fixtures.setup_tests_config(self) + fixtures.set_uniq_vm_name(self) + self.log_stream = io.StringIO() + fixtures.override_logging(self, self.log_stream) + fixtures.per_backend_release_arch_setup(self) + + def prepare_vm(self, packages): + """Prepare a vm to be setup with a list of packages as a string. + + It's assumed that tests will call vm.setup() so we prepare cleanup + here. + """ + conf = config.VmStack(self.vm_name) + vm_class = conf.get('vm.class') + vm = vm_class(conf) + vm.conf.set('vm.packages', packages) + # We don't need an up-to-date image + vm.conf.set('vm.update', 'false') + self.addCleanup(vm.teardown, True) + return vm + + def test_publish(self): + vm = self.prepare_vm('') + vm.setup() + vm.stop() + self.addCleanup(vm.unpublish) + vm.publish() diff -Nru ols-vms-1.3.0/olsvms/tests/per_vm/test_setup_digest.py ols-vms-1.3.1/olsvms/tests/per_vm/test_setup_digest.py --- ols-vms-1.3.0/olsvms/tests/per_vm/test_setup_digest.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/per_vm/test_setup_digest.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -17,7 +18,6 @@ import io import unittest -import sys from olstests import scenarii from olsvms import config @@ -27,14 +27,6 @@ ) -try: - if sys.version_info < (3,): - # novaclient doesn't support python3 (yet) - from olsvms.vms import nova -except ImportError: - pass - - load_tests = scenarii.load_tests_with_scenarios @@ -44,57 +36,18 @@ scenarios = [(k, dict(kls=k, series='xenial', arch='amd64')) for k in config.vm_class_registry.keys() - # ephemeral lxcs are not meant to install packages - # something is broken with libvirt (console owned by root is - # back /o\) + # - ephemeral lxcs are not meant to install packages + # - something is broken with libvirt (console owned by root is + # back /o\) if k not in ('libvirt', 'ephemeral-lxd')] def setUp(self): super(TestSetupDigest, self).setUp() fixtures.setup_tests_config(self) fixtures.set_uniq_vm_name(self) - # Some classes require additional features - required_features = { - 'libvirt': features.use_sudo_for_tests_feature, - 'lxd': features.lxd_client_feature, - 'nova': features.nova_creds, - } - if self.kls in required_features: - f = required_features[self.kls] - if not f.available(): - self.skipTest('{} is not available'.format(f.feature_name())) - # Create a shared config - conf = config.VmStack(None) - conf.store._load_from_string(''' -[{vm_name}] -vm.name = {vm_name} -vm.class = {kls} -'''.format(vm_name=self.vm_name, kls=self.kls)) - # Some classes require additional setup - specific_setups = { - 'libvirt': self.libvirt_setup, - 'lxd': self.lxd_setup, - 'nova': self.nova_setup, - } - if self.kls in specific_setups: - setup = specific_setups[self.kls] - setup(conf) - conf.store.save() - conf.store.unload() self.log_stream = io.StringIO() fixtures.override_logging(self, self.log_stream) - - def libvirt_setup(self, conf): - conf.set('vm.release', self.series) - conf.set('vm.architecture', self.arch) - - def lxd_setup(self, conf): - conf.set('vm.release', self.series) - conf.set('vm.architecture', self.arch) - - def nova_setup(self, conf): - image_id = nova.ols_image_name('cloudimg', self.series, self.arch) - conf.set('nova.image', image_id) + fixtures.per_backend_release_arch_setup(self) def prepare_vm(self, packages): """Prepare a vm to be setup with a list of packages as a string. @@ -106,8 +59,7 @@ vm_class = conf.get('vm.class') vm = vm_class(conf) vm.conf.set('vm.packages', packages) - self.addCleanup(vm.teardown) - self.addCleanup(vm.stop) + self.addCleanup(vm.teardown, True) return vm def test_hash_setup(self): diff -Nru ols-vms-1.3.0/olsvms/tests/per_vm/test_status.py ols-vms-1.3.1/olsvms/tests/per_vm/test_status.py --- ols-vms-1.3.0/olsvms/tests/per_vm/test_status.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/per_vm/test_status.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -50,7 +51,7 @@ self.conf = config.VmStack('foo') self.conf.set('vm.name', 'foo') self.conf.set('vm.class', self.kls) - self.conf.set('vm.release', 'trusty') + self.conf.set('vm.release', 'xenial') self.conf.set('vm.architecture', 'amd64') def test_unknown_vm(self): diff -Nru ols-vms-1.3.0/olsvms/tests/per_vm/test_update.py ols-vms-1.3.1/olsvms/tests/per_vm/test_update.py --- ols-vms-1.3.0/olsvms/tests/per_vm/test_update.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/per_vm/test_update.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -18,7 +19,6 @@ import io import os import unittest -import sys from olstests import scenarii from olsvms import config @@ -28,14 +28,6 @@ ) -try: - if sys.version_info < (3,): - # novaclient doesn't support python3 (yet) - from olsvms.vms import nova -except ImportError: - pass - - load_tests = scenarii.load_tests_with_scenarios @@ -46,50 +38,17 @@ scenarios = [(k, dict(kls=k, series='xenial', arch='amd64')) for k in config.vm_class_registry.keys() # - ephemeral lxcs are not meant to install packages - # - something is broken with libvirt + # - something is broken with libvirt (console owned by root is + # back /o\) if k not in ('libvirt', 'ephemeral-lxd',)] def setUp(self): super(TestUpdate, self).setUp() fixtures.setup_tests_config(self) fixtures.set_uniq_vm_name(self) - # Some classes require additional features - required_features = { - 'lxd': features.lxd_client_feature, - 'nova': features.nova_creds, - } - if self.kls in required_features: - f = required_features[self.kls] - if not f.available(): - self.skipTest('{} is not available'.format(f.feature_name())) - # Create a shared config - conf = config.VmStack(None) - conf.store._load_from_string(''' -[{vm_name}] -vm.name = {vm_name} -vm.class = {kls} -vm.update = true -'''.format(vm_name=self.vm_name, kls=self.kls)) - # Some classes require additional setup - specific_setups = { - 'nova': self.nova_setup, - 'lxd': self.lxd_setup, - } - if self.kls in specific_setups: - setup = specific_setups[self.kls] - setup(conf) - conf.store.save() - conf.store.unload() self.log_stream = io.StringIO() fixtures.override_logging(self, self.log_stream) - - def lxd_setup(self, conf): - conf.set('vm.release', self.series) - conf.set('vm.architecture', self.arch) - - def nova_setup(self, conf): - image_id = nova.ols_image_name('cloudimg', self.series, self.arch) - conf.set('nova.image', image_id) + fixtures.per_backend_release_arch_setup(self) def prepare_vm(self, packages): """Prepare a vm to be setup with a list of packages as a string. @@ -101,17 +60,17 @@ vm_class = conf.get('vm.class') vm = vm_class(conf) vm.conf.set('vm.packages', packages) - self.addCleanup(vm.teardown) - self.addCleanup(vm.stop) + vm.conf.set('vm.update', 'true') + self.addCleanup(vm.teardown, True) return vm def test_usable_vm(self): vm = self.prepare_vm('') # There is more than meet the eye in the following test (a single call - # to setup(). What is really tested here is that from any image, a full - # dist-upgrade can be performed successfully. So depending on the image - # freshness, it is expected that the test fail and help writing more - # focused tests depending on the encountered errors. + # to setup()). What is really tested here is that from any image, a + # full dist-upgrade can be performed successfully. So depending on the + # image freshness, it is expected that the test fail and help writing + # more focused tests depending on the encountered errors. vm.setup() def test_setup_scripts(self): diff -Nru ols-vms-1.3.0/olsvms/tests/test_commands.py ols-vms-1.3.1/olsvms/tests/test_commands.py --- ols-vms-1.3.0/olsvms/tests/test_commands.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/test_commands.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -150,7 +151,7 @@ def publish(self): self.publish_called = True - def teardown(self): + def teardown(self, force=False): self.teardown_called = True diff -Nru ols-vms-1.3.0/olsvms/tests/test_lxd.py ols-vms-1.3.1/olsvms/tests/test_lxd.py --- ols-vms-1.3.0/olsvms/tests/test_lxd.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/test_lxd.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2015, 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -309,37 +310,6 @@ vm.check_subids_for_mounts(888, 999, self.etc_dir) -@features.requires(features.tests_config) -@features.requires(features.lxd_client_feature) -class TestPublish(unittest.TestCase): - - def setUp(self): - super(TestPublish, self).setUp() - fixtures.setup_tests_config(self) - fixtures.set_uniq_vm_name(self) - # Create a shared config - conf = config.VmStack(None) - conf.store._load_from_string(''' -[{vm_name}] -vm.name = {vm_name} -vm.class = lxd -vm.release = xenial -vm.architecture = amd64 -'''.format(vm_name=self.vm_name)) - conf.store.save() - conf.store.unload() - - def test_publish(self): - vm = lxd.Lxd(config.VmStack(self.vm_name)) - self.addCleanup(vm.teardown) - # FIXME: This is slow, investigate using an existing container or an - # 'alpine' one even -- vila 2016-05-23 - vm.setup() - vm.stop() - self.addCleanup(vm.unpublish) - vm.publish() - - # FIXME: parametrized by distribution -- vila 2017-12-06 @features.requires(features.tests_config) class TestEphemeralLXD(unittest.TestCase): diff -Nru ols-vms-1.3.0/olsvms/tests/test_ssh.py ols-vms-1.3.1/olsvms/tests/test_ssh.py --- ols-vms-1.3.0/olsvms/tests/test_ssh.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/test_ssh.py 2018-01-25 19:29:29.000000000 +0000 @@ -102,7 +102,6 @@ vms_fixtures.setup_tests_config(self) vms_features.requires_existing_vm(self, 'ols-vms-tests-lxd-debian') vms_fixtures.set_uniq_vm_name(self) - config_dir = os.path.join(self.uniq_dir, 'config') # Create a shared config conf = config.VmStack(None) conf.store._load_from_string(''' @@ -110,7 +109,7 @@ vm.name = {vm_name} vm.class = ephemeral-lxd vm.backing = ols-vms-tests-lxd-debian -'''.format(config_dir=config_dir, vm_name=self.vm_name)) +'''.format(vm_name=self.vm_name)) conf.store.save() conf.store.unload() diff -Nru ols-vms-1.3.0/olsvms/tests/test_subprocesses.py ols-vms-1.3.1/olsvms/tests/test_subprocesses.py --- ols-vms-1.3.0/olsvms/tests/test_subprocesses.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/test_subprocesses.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -51,6 +52,13 @@ self.assertEqual('', cm.exception.out) self.assertTrue('I-dont-exist' in cm.exception.err) + def test_raise_on_error(self): + retcode, out, err = subprocesses.run( + ['ls', 'I-dont-exist'], raise_on_error=False) + self.assertEqual(2, retcode) + self.assertEqual('', out) + self.assertTrue('I-dont-exist' in err) + def test_error(self): # python2 doesn't have FileNotFoundError with self.assertRaises(OSError) as cm: diff -Nru ols-vms-1.3.0/olsvms/tests/test_vms.py ols-vms-1.3.1/olsvms/tests/test_vms.py --- ols-vms-1.3.0/olsvms/tests/test_vms.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/tests/test_vms.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -221,7 +222,7 @@ self.vm.conf.set('apt.update.timeouts', '0.1, 0.1') self.nb_calls = 0 - def failing_update(): + def failing_update(command): self.nb_calls += 1 if self.nb_calls > 1: return 0, 'stdout success', 'stderr success' @@ -229,21 +230,24 @@ # Fake a failed apt-get update raise errors.CommandError(['boo!'], 1, '', 'I failed') - self.vm.do_apt_get_update = failing_update + self.vm.do_apt_get = failing_update self.vm.apt_get_update() self.assertEqual(2, self.nb_calls) def test_apt_get_update_fails(self): self.vm.conf.set('apt.update.timeouts', '0.1, 0.1') + self.nb_calls = 0 - def failing_update(): + def failing_update(command): + self.nb_calls += 1 raise errors.CommandError(['boo!'], 1, '', 'I failed') - self.vm.do_apt_get_update = failing_update + self.vm.do_apt_get = failing_update with self.assertRaises(errors.OlsVmsError) as cm: self.vm.apt_get_update() self.assertEqual('apt-get update never succeeded', str(cm.exception)) + self.assertEqual(2, self.nb_calls) class TestWaitForIp(unittest.TestCase): diff -Nru ols-vms-1.3.0/olsvms/vms/__init__.py ols-vms-1.3.1/olsvms/vms/__init__.py --- ols-vms-1.3.0/olsvms/vms/__init__.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/vms/__init__.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2014-2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -64,7 +65,7 @@ def setup(self): raise NotImplementedError(self.setup) - def teardown(self): + def teardown(self, force=False): self.cleanup_configs() def start(self): @@ -78,7 +79,7 @@ def save_existing_config(self): conf = self.conf - for opt in ('vm.name', 'vm.class', 'vm.distribution', 'vm.ip'): + for opt in ('vm.name', 'vm.class', 'vm.distribution'): # We need the raw values value = conf.get(opt, convert=False) if value is not None: @@ -199,16 +200,7 @@ return logger.info('Updating...') self.apt_get_update() - upgrade_command = ['sudo', 'apt-get', 'dist-upgrade'] - opts = self.conf.get('apt.options') - if opts: - upgrade_command += opts - ssh_command = self.get_ssh_command(*upgrade_command) - try: - subprocesses.run(ssh_command) - except errors.CommandError: - logger.debug('Failed to upgrade', exc_info=True) - raise + self.apt_get_upgrade() # FIXME: cloud-init checks /var/run/reboot-required and act # accordingly, this is missing for now -- vila 2016-06-24 @@ -217,14 +209,9 @@ if not packages: return 0 logger.info('Installing packages {}'.format(' '.join(packages))) - install_command = ['sudo', 'apt-get', 'install'] - opts = self.conf.get('apt.options') - if opts: - install_command += opts - install_command += packages - ssh_command = self.get_ssh_command(*install_command) + install_command = ['install'] + self.conf.get('vm.packages') try: - subprocesses.run(ssh_command) + self.do_apt_get(install_command) except errors.CommandError: logger.debug('Failed to install packages', exc_info=True) raise @@ -318,17 +305,19 @@ raise errors.CommandError(send_command, send.returncode, out.decode('utf8'), err.decode('utf8')) - def do_apt_get_update(self): - update_command = ['sudo', 'apt-get', 'update'] + def do_apt_get(self, command): + apt_command = ['sudo', 'DEBIAN_FRONTEND=noninteractive', + 'apt-get'] opts = self.conf.get('apt.options') if opts: - update_command += opts - return self.shell_captured(*update_command) + apt_command.extend(opts) + apt_command.extend(command) + return self.shell_captured(*apt_command) def apt_get_update(self): for timeout in self.conf.get('apt.update.timeouts'): try: - self.do_apt_get_update() + self.do_apt_get(['update']) return # We're done except errors.CommandError: msg = 'apt-get update failed, will sleep for {} seconds' @@ -336,6 +325,17 @@ time.sleep(float(timeout)) raise errors.OlsVmsError('apt-get update never succeeded') + def apt_get_upgrade(self): + for timeout in self.conf.get('apt.upgrade.timeouts'): + try: + self.do_apt_get(['dist-upgrade']) + return # We're done + except errors.CommandError: + msg = 'apt-get dist-upgrade failed, will sleep for {} seconds' + logger.debug(msg.format(float(timeout)), exc_info=True) + time.sleep(float(timeout)) + raise errors.OlsVmsError('apt-get dist-upgrade never succeeded') + def ensure_dir(self, path): try: os.makedirs(path) @@ -510,8 +510,13 @@ first = float(first) up_to = float(up_to) retries = int(retries) - for sleep in timeouts.ExponentialBackoff(first, up_to, retries): + me = self.wait_for_ip.__name__ + for attempt, sleep in enumerate(timeouts.ExponentialBackoff( + first, up_to, retries)): try: + if attempt > 1: + logger.debug('Re-trying {} {}/{}'.format( + me, attempt, retries)) ip = self.discover_ip() self.econf.set('vm.ip', ip) logger.info('{} IP is {}'.format(self.conf.get('vm.name'), @@ -522,7 +527,9 @@ 'IP not yet available for {}'.format( self.conf.get('vm.name')), exc_info=True) - # FIXME: metric -- vila 2015-06-25 + # FIXME: metric -- vila 2015-06-25 + logger.debug('Sleeping {} seconds for {} {}/{}'.format( + sleep, me, attempt, retries)) time.sleep(sleep) raise errors.OlsVmsError('{} never received an IP'.format( self.conf.get('vm.name'))) @@ -533,10 +540,14 @@ first = float(first) up_to = float(up_to) retries = int(retries) + me = self.wait_for_ssh.__name__ exc = None for attempt, sleep in enumerate(timeouts.ExponentialBackoff( first, up_to, retries), start=1): try: + if attempt > 1: + logger.debug('Re-trying {} {}/{}'.format( + me, attempt, retries)) ret, out, err = self.shell_captured('whoami') # Success, clear the exception exc = None @@ -546,6 +557,8 @@ exc = e # FIXME: metric -- vila 2015-06-25 pass + logger.debug('Sleeping {} seconds for {} {}/{}'.format( + sleep, me, attempt, retries)) time.sleep(sleep) if exc is not None: # Re-raise the last seen exception diff -Nru ols-vms-1.3.0/olsvms/vms/libvirt.py ols-vms-1.3.1/olsvms/vms/libvirt.py --- ols-vms-1.3.0/olsvms/vms/libvirt.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/vms/libvirt.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -327,7 +328,9 @@ subprocesses.run( ['sudo', 'virsh', 'destroy', self.conf.get('vm.name')]) - def teardown(self): + def teardown(self, force=False): + if force and self.state().upper() == 'RUNNING': + self.stop() try: subprocesses.run( ['sudo', 'virsh', 'undefine', self.conf.get('vm.name')]) diff -Nru ols-vms-1.3.0/olsvms/vms/lxd.py ols-vms-1.3.1/olsvms/vms/lxd.py --- ols-vms-1.3.0/olsvms/vms/lxd.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/vms/lxd.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2015, 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -285,6 +286,14 @@ logger.info('Publishing lxd image...') publish_command = ['lxc', 'publish', self.conf.get('vm.name'), '--alias', self.conf.get('vm.published_as')] + # FIXME: Seen failing with: + # command: lxc publish TestPublish-test-publish-7621 \ + # --alias TestPublish-test-publish-7621 + # retcode: 1 + # output: + # error: error: websocket: close 1006 (abnormal closure): \ + # unexpected EOF + # -- vila 2018-01-17 subprocesses.run(publish_command) def unpublish(self): @@ -293,8 +302,10 @@ self.conf.get('vm.published_as')] subprocesses.run(unpublish_command) - def teardown(self): + def teardown(self, force=False): logger.info('Tearing down lxd container...') + if force and self.state() == 'RUNNING': + self.stop() teardown_command = ['lxc', 'delete', '--force', self.conf.get('vm.name')] subprocesses.run(teardown_command) diff -Nru ols-vms-1.3.0/olsvms/vms/nova.py ols-vms-1.3.1/olsvms/vms/nova.py --- ols-vms-1.3.0/olsvms/vms/nova.py 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/olsvms/vms/nova.py 2018-01-25 19:29:29.000000000 +0000 @@ -1,5 +1,6 @@ # This file is part of Online Services virtual machine tools. # +# Copyright 2018 Vincent Ladeuil. # Copyright 2015, 2016, 2017 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify it under @@ -296,6 +297,7 @@ raise NovaServerException(msg) def nova_id_path(self): + # FIXME: This should be in self.econf -- vila 2018-01-14 return os.path.join(self.config_dir_path(), 'nova_id') def create_nova_id_file(self, nova_id): @@ -359,8 +361,10 @@ # FIXME: Should wait for the instance to be shut off -- vila 2016-07-01 # With a wait parameter defaulting to True ? -- vila 2017-01-20 - def teardown(self): + def teardown(self, force=False): logger.info('Tearing down nova server...') + if force and self.state() == 'RUNNING': + self.stop() if self.instance is not None: logger.info('Deleting instance {}'.format(self.instance.id)) self.nova.delete_server(self.instance.id) diff -Nru ols-vms-1.3.0/olsvms/vms/scaleway.py ols-vms-1.3.1/olsvms/vms/scaleway.py --- ols-vms-1.3.0/olsvms/vms/scaleway.py 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.3.1/olsvms/vms/scaleway.py 2018-01-25 19:29:29.000000000 +0000 @@ -0,0 +1,517 @@ +# This file is part of Online Services virtual machine tools. +# +# Copyright 2018 Vincent Ladeuil. +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3, as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . +from __future__ import unicode_literals + +import hashlib +import json +import logging +import sys +import time + +try: + import urlparse # python2 +except: + from urllib import parse as urlparse + +import olsvms +from olsvms import ( + errors, + timeouts, + vms, +) + +# FIXME: These are dependencies that need to remain optional so the scaleway +# backend requires them but ols-vms doesn't -- vila 2018-01-14 +import requests + +logger = logging.getLogger(__name__) + + +# scaleway uses a different nomenclature. Probably closer to linux than ubuntu +# or lxd. +scaleway_architectures = dict( + armhf='arm', + amd64='x86_64', + arm64='arm64', +) + + +class ScalewayComputeException(errors.OlsVmsError): + pass + + +class Client(object): + """A client for the scaleway API. + + This is a simple wrapper around requests.Session so we inherit all good + bits while providing a simple point for tests to override when needed. + """ + + user_agent = 'ols-vms-{} python {}'.format(olsvms.version(), + sys.version.split()[0]) + + def __init__(self, conf, root_url, timeouts): + self.conf = conf + self.root_url = root_url + self.timeouts = timeouts + self.session = requests.Session() + + def request(self, method, url, params=None, headers=None, **kwargs): + """Overriding base class to handle the root url.""" + # Note that url may be absolute in which case 'root_url' is ignored by + # urljoin. + sent_headers = {'User-Agent': self.user_agent, + 'X-Auth-Token': self.conf.get('scaleway.token'), + 'Content-Type': 'application/json'} + if headers is not None: + sent_headers.update(headers) + final_url = urlparse.urljoin(self.root_url, + self.conf.expand_options(url)) + response = self.session.request( + method, final_url, headers=sent_headers, params=params, **kwargs) + return response + + def get(self, url, **kwargs): + return self.request('GET', url, **kwargs) + + def post(self, url, data=None, **kwargs): + if data is not None: + data = json.dumps(data) + return self.request('POST', url, data=data, **kwargs) + + def patch(self, url, **kwargs): + return self.request('PATCH', url, **kwargs) + + def delete(self, url, **kwargs): + return self.request('DELETE', url, **kwargs) + + def close(self): + self.session.close() + + def retry(self, func, url, *args, **kwargs): + req_path = '{} {}'.format(func.__name__.upper(), url) + no_404_retry = kwargs.pop('no_404_retry', False) + first, up_to, retries = self.timeouts + first = float(first) + up_to = float(up_to) + retries = int(retries) + sleeps = timeouts.ExponentialBackoff(first, up_to, retries) + for attempt, sleep in enumerate(sleeps, start=1): + try: + response = None + if attempt > 1: + logger.debug('Re-trying {} {}/{}'.format( + req_path, attempt, retries)) + response = func(url, *args, **kwargs) + except requests.ConnectionError: + # Most common transient failure: the API server is unreachable + # (server, network or client may be the cause). + msg = 'Connection error for {}, will sleep for {} seconds' + logger.warning(msg.format(req_path, sleep)) + except: + # All other exceptions are raised + logger.error('{} failed'.format(req_path), exc_info=True) + raise ScalewayComputeException('{} failed'.format(req_path)) + # If the request succeeded return the response (also for the 404 + # special case as instructed). + if (response is not None and + (response.ok or (response.status_code == 404 and + no_404_retry))): + return response + if not response.ok: + if response.status_code == 429: + msg = ('Rate limit reached for {},' + ' will sleep for {} seconds') + # This happens rarely but breaks badly if not caught. elmo + # recommended a 30 seconds nap in that case. + sleep += 30 + logger.warning(msg.format(req_path, sleep)) + else: + # Nice spot to debug the API usage + # All other errors are raised + msg = '{} failed {}: {}'.format(req_path, + response.status_code, + response.json()) + logger.error(msg, exc_info=True) + # FIXME: Debug, should not land -- vila 2018-01-18 + sys.stderr.write(msg + '\n') + raise ScalewayComputeException(msg) + # Take a nap before retrying + logger.debug('Sleeping {} seconds for {} {}/{}'.format( + sleep, req_path, attempt, retries)) + time.sleep(sleep) + # Raise if we didn't succeed at all + raise ScalewayComputeException( + "Failed to '{}' after {} retries".format(req_path, attempt)) + + +class ComputeClient(Client): + """Client for the scaleway compute API.""" + + def create_server(self, image_id): + response = self.retry( + self.post, 'servers', + data=dict(name=self.conf.get('vm.name'), + organization=self.conf.get('scaleway.access_key'), + image=image_id, + commercial_type=self.conf.get('scaleway.flavor'), + dynamic_ip_required=True, + volumes={})) + return response.json()['server'] + + def get_server_details(self): + server_id = self.conf.get('scaleway.server_id') + if server_id is None: + # We can't ask about a server that doesn't exist yet + return None + response = self.retry(self.get, 'servers/{}'.format(server_id), + no_404_retry=True) + if response.status_code == 404: + return dict(state='UNKNOWN') + return response.json()['server'] + + def set_user_data(self, key, data): + return self.retry( + self.patch, + 'servers/{}/user_data/{}'.format( + self.conf.get('scaleway.server_id'), key), + headers={'Content-Type': 'text/plain'}, + data=data) + + def start_server(self): + self.action('poweron') + + def stop_server(self): + # FIXME: Attempting to stop a stopped server 400's. + self.action('poweroff') + + def terminate_server(self): + # FIXME: Attempting to stop a stopped server 400's. + self.action('terminate') + + def action(self, action): + server_id = self.conf.get('scaleway.server_id') + if server_id is None: + # We can't act on a server that doesn't exist + # FIXME: Building a 404 seems overkill -- vila 2018-01-15 + return None + response = self.retry(self.post, 'servers/{}/action'.format(server_id), + data=dict(action=action)) + return response + + def delete_server(self): + server_id = self.conf.get('scaleway.server_id') + if server_id is None: + # We can't delete a server that doesn't exist + # FIXME: Building a 404 seems overkill -- vila 2018-01-15 + return None + response = self.retry(self.delete, 'servers/{}'.format(server_id)) + return response + + def delete_volume(self, volume_id): + # FIXME: no_404_retry ? -- vila 2018-01-17 + response = self.retry(self.delete, 'volumes/{}'.format(volume_id)) + return response + + def create_snapshot(self, volume_id): + response = self.retry( + self.post, 'snapshots', + data=dict(name=self.conf.get('vm.name'), + organization=self.conf.get('scaleway.access_key'), + volume_id=volume_id)) + return response.json()['snapshot'] + + def delete_snapshot(self, snapshot_id): + # FIXME: no_404_retry ? -- vila 2018-01-23 + response = self.retry(self.delete, 'snapshots/{}'.format(snapshot_id)) + return response + + # FIXME: IWBNI this was a generator, especially since callers will stop + # iterating on first match -- vila 2018-01-24 + def images_list(self): + images = [] + response = self.retry(self.get, 'images') + while True: + images.extend(response.json()['images']) + next_link = response.links.get('next', None) + if next_link is None: + break + response = self.retry(self.get, next_link['url']) + return images + + def create_image(self, name, arch, snapshot_id): + response = self.retry( + self.post, 'images', + data=dict(name=name, + arch=arch, + organization=self.conf.get('scaleway.access_key'), + root_volume=snapshot_id)) + return response.json()['image'] + + def delete_image(self, image_id): + # FIXME: no_404_retry ? -- vila 2018-01-23 + response = self.retry(self.delete, 'images/{}'.format(image_id)) + return response + + +class Scaleway(vms.VM): + + setup_ip_timeouts = 'scaleway.setup_ip_timeouts' + setup_ssh_timeouts = 'scaleway.setup_ssh_timeouts' + + def __init__(self, conf): + super(Scaleway, self).__init__(conf) + self.compute = ComputeClient( + self.conf, + self.conf.get('scaleway.compute.url'), + self.conf.get('scaleway.compute.timeouts')) + self.server_id = None + + def state(self): + # FIXME: The possible values need to be discovered and mapped + # -- vila 20180-01-14 + # scw inspect can fail IRL just before the server starts and after it + # shuts down (and can't be used while it's off :-/) + # Seen IRL: + # "state": "starting", + # "state_detail": "provisioning node", + # "state": "starting", + # "state_detail": "booting kernel", + # "state": "running", + # "state_detail": "booted", + # "state": "stopping", + # "state_detail": "rebooting", + # "state": "stopping", + # "state_detail": "stopping", + # curl to the rescue: + # "state": "stopped" + # "state_detail": "" + server = self.compute.get_server_details() + if server is None: + return 'UNKNOWN' + else: + return server['state'].upper() + + def find_scaleway_image(self, image_name, arch): + logger.debug('Searching for image {}...'.format(image_name)) + # First match wins + existing_images = self.compute.images_list() + for existing in existing_images: + if image_name == existing['name'] and arch == existing['arch']: + logger.debug('Found image {}'.format(existing['id'])) + # More recent images come first. Picking the first match should + # be the Right Thing. + return existing + raise ScalewayComputeException( + 'Image "{}" cannot be found'.format(image_name)) + + def create(self): + logger.debug('Creating scaleway server {}'.format( + self.conf.get('vm.name'))) + arch = scaleway_architectures[self.conf.get('vm.architecture')] + image = self.find_scaleway_image(self.conf.get('scaleway.image'), arch) + server = self.compute.create_server(image['id']) + self.econf.set('scaleway.server_id', server['id']) + + def set_cloud_init_config(self): + self.create_user_data() + logger.debug('Configuring scaleway container') + self.compute.set_user_data('cloud-init', self.ci_user_data.dump()) + + def setup(self): + logger.info('Setting up scaleway server {}...'.format( + self.conf.get('vm.name'))) + self.create() + self.set_cloud_init_config() + self.econf.set('scaleway.region_name', + self.conf.get('scaleway.region_name')) + self.start() + self.econf.store.save_changes() + # MISSINGTEST: with bootstrap (though building the images ount as a + # manual but mandatory test to get anywhere -- vila 2018-01-24 + if not self.conf.get('scaleway.image.bootstrap'): + self.wait_for_cloud_init() + self.setup_over_ssh() + + def hash_setup(self): + hasher = hashlib.md5() + # Seed with the digest from the base class to capture common options + hasher.update(super(Scaleway, self).hash_setup().encode('utf8')) + options = self.conf.get('scaleway.setup.digest.options') + if options: + for opt in options: + value = self.conf.get(opt, convert=False) + self.hash_value(hasher, value) + return hasher.hexdigest() + + def discover_ip(self): + ip = None + server = self.compute.get_server_details() + state = server['state'].upper() + public_ip = server.get('public_ip') + if public_ip: + ip = public_ip.get('address') + if state != 'RUNNING' or not ip: + raise errors.OlsVmsError( + 'scaleway server {} has not provided an IP yet: {}'.format( + self.conf.get('vm.name'), state)) + return ip + + def wait_for_server_reaching(self, expected_states, how_long): + logger.debug('Waiting for the server to reach {} state...'.format( + expected_states)) + # An amd64 (but this seems to be the same for all arches) server is in + # the 'starting' state including 'allocating node' (up to a min), + # 'provisioning node' (up to a min), 'booting kernel' (10s), + # 'kernel-started' (5s), 'booted'. It then reaches the 'running' state. + server = self.compute.get_server_details() + timeout_limit = time.time() + how_long + while (time.time() < timeout_limit and + server['state'].upper() not in expected_states): + logger.debug( + 'Server state: {state}/{state_detail}'.format(**server)) + time.sleep(5) + server = self.compute.get_server_details() + if server['state'].upper() not in expected_states: + msg = ('Server {id} never reach state {expected_states}' + ' (last status: {state}/{state_detail})'.format( + expected_states=expected_states, **server)) + raise ScalewayComputeException(msg) + return server + + def wait_for_cloud_init(self): + logger.info('Waiting for cloud-init...') + # Now let's track cloud-init completion + # We know ssh is up and running + first, up_to, retries = self.conf.get('scaleway.cloud_init_timeouts') + first = float(first) + up_to = float(up_to) + retries = int(retries) + me = self.wait_for_cloud_init.__name__ + exc = None + # boot-finished is deleted when cloud-init starts and created once it + # finishes. + check_cloud_init_command = ['test', '-f', + '/var/lib/cloud/instance/boot-finished'] + for attempt, sleep in enumerate(timeouts.ExponentialBackoff( + first, up_to, retries), start=1): + try: + if attempt > 1: + logger.debug('Re-trying {} {}/{}'.format( + me, attempt, retries)) + ret, out, err = self.shell_captured(*check_cloud_init_command) + # Success, clear the exception + exc = None + break + except errors.CommandError as e: + logger.debug('ssh is not up yet', exc_info=True) + exc = e + # FIXME: metric -- vila 2015-06-25 + logger.debug('Sleeping {:.1f} seconds for {} {}/{}'.format( + sleep, me, attempt, retries)) + time.sleep(sleep) + if exc is not None: + # Re-raise the last seen exception + raise exc + # FIXME: We need to check if cloud-init failed or not but + # /var/log/cloud-init[-output].log contain all executions :-/ So + # checking only for the last execution is not trivial + # -- vila 2018-01-12 + + def start(self): + logger.info('Starting scaleway server {}...'.format( + self.conf.get('vm.name'))) + self.econf.store.save_changes() # Recovery checkpoint + how_long = self.conf.get('scaleway.poweron_timeout') + timeout_limit = time.time() + how_long + server = self.compute.get_server_details() + attempts = 0 + while time.time() < timeout_limit: + attempts += 1 + self.compute.start_server() + server = self.wait_for_server_reaching( + ['RUNNING', 'STOPPED'], how_long) + if server['state'].upper() == 'STOPPED': + # It may happen that the 'allocating node' step ends without + # starting the server (it's just stopped: allocation failed + # silently). Try again. + logger.warning( + 'Server {} allocation failed silently, re-trying'.format( + self.conf.get('vm.name'))) + continue + else: + break + if server['state'].upper() != 'RUNNING': + msg = ('Server {id} never reach state RUNNING' + ' (last status: {state}/{state_detail})' + ' after {attempts} attempts'.format(attempts=attempts, + **server)) + raise ScalewayComputeException(msg) + + self.wait_for_ip() + self.econf.store.save_changes() # Recovery checkpoint + self.wait_for_ssh() + self.save_existing_config() + + def stop(self): + logger.info('Stopping scaleway server...') + self.compute.stop_server() + self.wait_for_server_reaching( + ['STOPPED'], self.conf.get('scaleway.poweroff_timeout')) + + # FIXME: Hmm, volumes are archived by default but when tearing down + # it's worth skipping the volume archive step as it takes a significant + # time compared to 'stop --terminate'. In any case, it's probably a + # good idea to keep track of the existing volumes in self.econf to be + # able to clean them up by default (the only workflow that needs to + # keep a boot volume is publish' AFAICS) -- vila 2018-01-14 + + def publish(self): + image_name = self.conf.get('vm.published_as') + logger.info('Publishing scaleway image {}...'.format(image_name)) + server = self.compute.get_server_details() + vol0 = server['volumes']['0'] + snapshot = self.compute.create_snapshot(vol0['id']) + arch = scaleway_architectures[self.conf.get('vm.architecture')] + image = self.compute.create_image(image_name, arch, snapshot['id']) + return image + + def unpublish(self): + image_name = self.conf.get('vm.published_as') + logger.info('Un-publishing scaleway image {}...'.format(image_name)) + arch = scaleway_architectures[self.conf.get('vm.architecture')] + image = self.find_scaleway_image(image_name, arch) + snapshot_id = image['root_volume']['id'] + self.compute.delete_image(image['id']) + self.compute.delete_snapshot(snapshot_id) + + def teardown(self, force=False): + logger.info('Tearing down scaleway server...') + if force and self.state() == 'RUNNING': + self.compute.terminate_server() + self.wait_for_server_reaching( + ['UNKNOWN'], self.conf.get('scaleway.terminate_timeout')) + else: + # Get the server description before deletion + server = self.compute.get_server_details() + self.compute.delete_server() + # Clean up the volumes + # MISSINGTEST: race with server deletion ? what if server deletion + # somehow deleted some volumes ? -- vila 2018-01-17 + for volume in server['volumes'].values(): + self.compute.delete_volume(volume['id']) + super(Scaleway, self).teardown() + # FIXME: Beware of volume leaks -- vila 2018-01-14 diff -Nru ols-vms-1.3.0/ols-vms.conf ols-vms-1.3.1/ols-vms.conf --- ols-vms-1.3.0/ols-vms.conf 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/ols-vms.conf 2018-01-25 19:29:29.000000000 +0000 @@ -8,7 +8,7 @@ vm.update = True vm.poweroff = True -[image-builder] +[image-builder-nova] # Openstack credentials need to be set (or sourced) before dealing with this # vm # Once the vm is setup, ols-vms2 shell to it and run: @@ -25,9 +25,10 @@ vm.packages = python-glanceclient vm.setup_scripts = scripts/get-nova-cloud-images, scripts/create-glance-images vm.poweroff = True -[image-builder1] +# For canonistack, each region need its own images +[nova-image-builder-nova1] nova.region_name = lcy01 -[image-builder2] +[nova-image-builder-nova2] nova.region_name = lcy02 [ols-vms-tests-lxd] vm.class = lxd @@ -53,3 +54,52 @@ vm.class = lxd vm.distribution = debian vm.release = sid + +# ad-hoc testing for scaleway +[scw] +vm.class = scaleway +vm.distribution = ubuntu +vm.release = xenial +vm.update = True +apt.sources = ppa:vila/ci +vm.packages = python3-ols-tests +[scw-amd64] +vm.architecture = amd64 +scaleway.flavor = VC1S +[scw-armhf] +vm.architecture = armhf +scaleway.flavor = C1 +[scw-arm64] +vm.architecture = arm64 +scaleway.flavor = ARM64-2GB +[image-builder-scw] +# Produces an image named ubuntu/xenial/amd64, override +# 'vm.{distribution,release,architecture}' to produce images for other +# distributions or architectures. +vm.class = scaleway +vm.distribution = ubuntu +vm.release = xenial +vm.architecture = amd64 +vm.published_as = {vm.distribution}/{vm.release}/{vm.architecture} +vm.update = true +vm.packages = software-properties-common, cloud-init, haveged +vm.poweroff = true +scaleway.flavor = {{vm.architecture}.flavor} +scaleway.image.bootstrap = true +# /!\ ssh.key should be a key registered at +# https://cloud.scaleway.com/#/credentials and available from the ssh agent. +vm.user = root # Because cloud-init may not be there (yet) +scaleway.image = Ubuntu Xenial (16.04 latest) +vm.setup_scripts = scripts/transform-scaleway-image +# Scaleway specific mappings +# scaleway mirrors are the fastest, use them +# FIXME: But they only carry ubuntu :-/ -- 2018-01-24 +amd64.architecture = x86_64 +amd64.mirror = http://mirror.scaleway.com/{vm.distribution} +amd64.flavor = VC1S +armhf.architecture = arm +armhf.mirror = http://mirror.scaleway.com/{vm.distribution}-ports +armhf.flavor = C1 +arm64.architecture = arm64 +arm64.mirror = {armhf.mirror} +arm64.flavor = ARM64-2GB \ No newline at end of file diff -Nru ols-vms-1.3.0/scripts/bootstrap-scaleway-image ols-vms-1.3.1/scripts/bootstrap-scaleway-image --- ols-vms-1.3.0/scripts/bootstrap-scaleway-image 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.3.1/scripts/bootstrap-scaleway-image 2018-01-25 19:29:29.000000000 +0000 @@ -0,0 +1,9 @@ +#!/bin/sh -ex + +# Setup a temporary vm to create an arch-specific image suitable for ols-vms + +ARCH=${1:-amd64} + +./ols-vms setup -Ovm.architecture=$ARCH --force image-builder-scw-$ARCH +./ols-vms publish -Ovm.architecture=$ARCH image-builder-scw-$ARCH +./ols-vms teardown -Ovm.architecture=$ARCH image-builder-scw-$ARCH diff -Nru ols-vms-1.3.0/scripts/transform-scaleway-image ols-vms-1.3.1/scripts/transform-scaleway-image --- ols-vms-1.3.0/scripts/transform-scaleway-image 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.3.1/scripts/transform-scaleway-image 2018-01-25 19:29:29.000000000 +0000 @@ -0,0 +1,68 @@ +#!/bin/sh -ex + +# Create a scaleway image usable with ols-vms from a close-enough bootable +# image. This is mainly installing and configuring cloud-init. + +# Cleanup +APT_OPTIONS=$(echo {apt.options} | sed -e 's/,/ /g') +apt-get $APT_OPTIONS autoremove + +# At this point we have cloud-init >= 17.1 with scaleway userdata support. + +# Which means /etc/cloud/90_dpkg.cfg (which can be updated by running +# dpkg-reconfigure cloud-init) should contain: + +# datasource_list: [ NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, +# AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, +# Scaleway, AliYun, Ec2, CloudStack, None ] + +# Very slow boots can be explained by wrongly configured datasources +# running 'cloud-init analyze show' will help to spot them. +# RL example (where Ec2 and CloudStack accounted for 4 mins): +# |`->no network data found from DataSourceEc2 @12.90500s +120.22400s +# |`->no network data found from DataSourceCloudStack @133.13200s +126.56400s +# Finished stage: (init-network) 260.33700 seconds + +# Setup cloud-init config with the bare minimum: Scaleway and the fallback +# None. 'NoCloud' and 'Openstack' are nice to have for more usages. +# Use a name that sorts after 90_dpkg.cfg to override +cat < /etc/cloud/cloud.cfg.d/99_olsvms.cfg +# Generated by ols-vms at $(date) +# to update this file, run dpkg-reconfigure cloud-init +datasource_list: [ NoCloud, OpenStack, Scaleway, None] +apt_preserve_sources_list: true +EOC +chmod 644 /etc/cloud/cloud.cfg.d/99_olsvms.cfg + +# Setup apt sources.list for cases where it's not correctly set +# FIXME: cloud-init probably provides a better way but for now, let's stick +# with the simplest approach of forcing a reduced set of sources (and +# telling cloud-init to apt_preserve_sources_list above) -- vila 2018-01-24 +cat < /etc/apt/sources.list +deb {{vm.architecture}.mirror} {vm.release} main universe +deb {{vm.architecture}.mirror} {vm.release}-security main universe +deb {{vm.architecture}.mirror} {vm.release}-updates main universe +EOS + +chmod 644 /etc/apt/sources.list + +# Disable unattended upgrades at they cause a race with cloud-init/ols-vms +# when apt-get updat'ing. The list of options below was established from +# /usr/lib/apt/apt.systemd.daily +cat < /etc/apt/apt.conf.d/99olsvms +// Generated by ols-vms at $(date) +// Avoid races with apt-get cloud-init/ols-vms uses +// Delete this file to restore unattended upgrades +APT::Periodic::Update-Package-Lists "0"; +APT::Periodic::Download-Upgradeable-Packages "0"; +APT::Periodic::AutocleanInterval "0"; +APT::Periodic::Unattended-Upgrade "0"; +APT::Periodic::AutocleanInterval "0"; +APT::Periodic::CleanInterval "0"; +EOA + +# The incoming shutdown may lose our last modifications, make sure they hit +# the disk. Do it twice because better safe than sorry ;-) (And the second +# one should be extremely fast anyway). +sync +sync diff -Nru ols-vms-1.3.0/TODO.rst ols-vms-1.3.1/TODO.rst --- ols-vms-1.3.0/TODO.rst 2017-12-19 14:59:27.000000000 +0000 +++ ols-vms-1.3.1/TODO.rst 2018-01-25 19:29:29.000000000 +0000 @@ -1,11 +1,32 @@ +* Have a look at the various '@' uses and settle on a consistent one. + +* Implement --definition that takes an importable python file that can be + used to register additional options. + +* Implement --stack that takes a class name to be used (or would it be + better to make that a name in a dedicated stack registry + (which --definition can populate) ?). + +* scaleway and ubuntu use different names for the architectures. Since there + is no collision, the most user-friendly would be to accept any synonym as + valid but use the one expected by the backend internally. Search across + image repositories to see what names are used. + +* Report @scaleway 'wget: can't connect to remote host (169.254.42.42): + Connection timed out' ? Seen during the 'booting kernel' step ?? Followed + by an automatic reboot ? + +* Look at places where 'raise_on_error=False' can be used for to + subprocesses.run(). + * Add per_distro or blackbox tests against all backends. * add a 'push' and a 'pull' command with sane defaults (carry mode bits, default user), list of files, target dir (or one file, one target). -* /etc/ols-vms.conf should be replaced by /etc/ols-vms/*.conf to support - more use cases (some sites or packages may want to supply options and - having to deal with a single file to share is a pain). +* Add support for /etc/ols-vms.conf.d/ to complement /etc/ols-vms/*.conf so + more use cases can be supported (some sites or packages may want to supply + options and having to deal with a single file to share is a pain). * turn all _scripts options into _commands options supporting @. @@ -18,15 +39,6 @@ Alternatively, just write the doc from the existing config option docstrings. -* an upgrade can fail transiently :-/ IRL, open-iscsi open-iscsi amd64 - 2.0.873+git0.3b4b4500-14ubuntu3.2 amd64 2.0.873+git0.3b4b4500-14ubuntu3.2 - on xenial - https://jenkins.ols.canonical.com/online-services/job/build-snapcraft-io-setup-jenkins-slave-8/ (up to 5 times in a row) - but seen for previous versions. This should be addressed in two ways: the - caller can repeat the setup until success (up to an hour). ols-vms should - collect all intermediate failures and give them back on the final failure - (there are internal timeouts already). - * a vm can be setup again just from the existing-vms.conf definition. This is wrong (or at least would be very confusing since the proper definition is not seen).