From 404f0a4a6542cdc721901d149ac981a81199aa79 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Mon, 26 Oct 2020 11:16:20 -0500 Subject: refactor integration testing infrastructure (#610) * Separated IntegrationClient into separate cloud and instance abstractions. This makes it easier to control the lifetime of the pycloudlib's cloud and instance abstractions separately. * Created new cloud-specific subclasses accordingly * Moved platform parsing and initialization code into its own file * Created new session-wide autorun fixture to automatically initialize and destroy the dynamic cloud --- tests/integration_tests/clouds.py | 126 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/integration_tests/clouds.py (limited to 'tests/integration_tests/clouds.py') diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py new file mode 100644 index 00000000..06f1c623 --- /dev/null +++ b/tests/integration_tests/clouds.py @@ -0,0 +1,126 @@ +# This file is part of cloud-init. See LICENSE file for license information. +from abc import ABC, abstractmethod +import logging + +from pycloudlib import EC2, GCE, Azure, OCI, LXD + +from tests.integration_tests import integration_settings +from tests.integration_tests.instances import ( + IntegrationEc2Instance, + IntegrationGceInstance, + IntegrationAzureInstance, IntegrationInstance, + IntegrationOciInstance, + IntegrationLxdContainerInstance, +) + +try: + from typing import Optional +except ImportError: + pass + + +log = logging.getLogger('integration_testing') + + +class IntegrationCloud(ABC): + datasource = None # type: Optional[str] + integration_instance_cls = IntegrationInstance + + def __init__(self, settings=integration_settings): + self.settings = settings + self.cloud_instance = self._get_cloud_instance() + self.image_id = self._get_initial_image() + + @abstractmethod + def _get_cloud_instance(self): + raise NotImplementedError + + def _get_initial_image(self): + image_id = self.settings.OS_IMAGE + try: + image_id = self.cloud_instance.released_image( + self.settings.OS_IMAGE) + except (ValueError, IndexError): + pass + return image_id + + def launch(self, user_data=None, launch_kwargs=None, + settings=integration_settings): + if self.settings.EXISTING_INSTANCE_ID: + log.info( + 'Not launching instance due to EXISTING_INSTANCE_ID. ' + 'Instance id: %s', self.settings.EXISTING_INSTANCE_ID) + self.instance = self.cloud_instance.get_instance( + self.settings.EXISTING_INSTANCE_ID + ) + return + launch_kwargs = { + 'image_id': self.image_id, + 'user_data': user_data, + 'wait': False, + } + launch_kwargs.update(launch_kwargs) + pycloudlib_instance = self.cloud_instance.launch(**launch_kwargs) + pycloudlib_instance.wait(raise_on_cloudinit_failure=False) + log.info('Launched instance: %s', pycloudlib_instance) + return self.get_instance(pycloudlib_instance, settings) + + def get_instance(self, cloud_instance, settings=integration_settings): + return self.integration_instance_cls(self, cloud_instance, settings) + + def destroy(self): + pass + + def snapshot(self, instance): + return self.cloud_instance.snapshot(instance, clean=True) + + +class Ec2Cloud(IntegrationCloud): + datasource = 'ec2' + integration_instance_cls = IntegrationEc2Instance + + def _get_cloud_instance(self): + return EC2(tag='ec2-integration-test') + + +class GceCloud(IntegrationCloud): + datasource = 'gce' + integration_instance_cls = IntegrationGceInstance + + def _get_cloud_instance(self): + return GCE( + tag='gce-integration-test', + project=self.settings.GCE_PROJECT, + region=self.settings.GCE_REGION, + zone=self.settings.GCE_ZONE, + ) + + +class AzureCloud(IntegrationCloud): + datasource = 'azure' + integration_instance_cls = IntegrationAzureInstance + + def _get_cloud_instance(self): + return Azure(tag='azure-integration-test') + + def destroy(self): + self.cloud_instance.delete_resource_group() + + +class OciCloud(IntegrationCloud): + datasource = 'oci' + integration_instance_cls = IntegrationOciInstance + + def _get_cloud_instance(self): + return OCI( + tag='oci-integration-test', + compartment_id=self.settings.OCI_COMPARTMENT_ID + ) + + +class LxdContainerCloud(IntegrationCloud): + datasource = 'lxd_container' + integration_instance_cls = IntegrationLxdContainerInstance + + def _get_cloud_instance(self): + return LXD(tag='lxd-integration-test') -- cgit v1.2.3 From d619f5171ac0ce5b626ef4575ad5f4468e94c987 Mon Sep 17 00:00:00 2001 From: Daniel Watkins Date: Mon, 2 Nov 2020 13:40:10 -0500 Subject: integration_tests: various launch improvements (#638) * integration_tests: fix passing launch_kwargs to session_cloud.launch * integration_tests: log the launch_kwargs before launching instances * integration_tests: add support for specifying instance name for tests Co-authored-by: Rick Harding --- tests/integration_tests/clouds.py | 11 ++++++++--- tests/integration_tests/conftest.py | 10 +++++++++- tox.ini | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) (limited to 'tests/integration_tests/clouds.py') diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index 06f1c623..08c86198 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -54,13 +54,18 @@ class IntegrationCloud(ABC): self.settings.EXISTING_INSTANCE_ID ) return - launch_kwargs = { + kwargs = { 'image_id': self.image_id, 'user_data': user_data, 'wait': False, } - launch_kwargs.update(launch_kwargs) - pycloudlib_instance = self.cloud_instance.launch(**launch_kwargs) + kwargs.update(launch_kwargs) + log.info( + "Launching instance with launch_kwargs:\n{}".format( + "\n".join("{}={}".format(*item) for item in kwargs.items()) + ) + ) + pycloudlib_instance = self.cloud_instance.launch(**kwargs) pycloudlib_instance.wait(raise_on_cloudinit_failure=False) log.info('Launched instance: %s', pycloudlib_instance) return self.get_instance(pycloudlib_instance, settings) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 9163ac66..34e674e9 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -111,7 +111,15 @@ def _client(request, fixture_utils, session_cloud): """ user_data = fixture_utils.closest_marker_first_arg_or( request, 'user_data', None) - with session_cloud.launch(user_data=user_data) as instance: + name = fixture_utils.closest_marker_first_arg_or( + request, 'instance_name', None + ) + launch_kwargs = {} + if name is not None: + launch_kwargs = {"name": name} + with session_cloud.launch( + user_data=user_data, launch_kwargs=launch_kwargs + ) as instance: yield instance diff --git a/tox.ini b/tox.ini index a8681197..816e6e8e 100644 --- a/tox.ini +++ b/tox.ini @@ -158,3 +158,4 @@ markers = oci: test will only run on OCI platform lxd_container: test will only run in LXD container user_data: the user data to be passed to the test instance + instance_name: the name to be used for the test instance -- cgit v1.2.3 From f42359741a6446d3c4ef25266ec290ed6f925b3a Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 4 Nov 2020 11:54:20 -0600 Subject: Fix launch_kwargs bug in integration tests (#654) Integration test instance launch would previously fail if provided launch_kwargs is None --- tests/integration_tests/clouds.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests/integration_tests/clouds.py') diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index 08c86198..71d4e85d 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -59,7 +59,8 @@ class IntegrationCloud(ABC): 'user_data': user_data, 'wait': False, } - kwargs.update(launch_kwargs) + if launch_kwargs: + kwargs.update(launch_kwargs) log.info( "Launching instance with launch_kwargs:\n{}".format( "\n".join("{}={}".format(*item) for item in kwargs.items()) -- cgit v1.2.3 From bfaee8cc9b8fd23dbc118ae548fc2ca695a0d707 Mon Sep 17 00:00:00 2001 From: Daniel Watkins Date: Thu, 19 Nov 2020 12:42:49 -0500 Subject: integration_tests: restore emission of settings to log (#657) --- tests/integration_tests/clouds.py | 11 +++++++++++ tests/integration_tests/conftest.py | 1 + tests/integration_tests/instances.py | 11 ----------- 3 files changed, 12 insertions(+), 11 deletions(-) (limited to 'tests/integration_tests/clouds.py') diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index 71d4e85d..fe89c0c6 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -31,6 +31,17 @@ class IntegrationCloud(ABC): self.cloud_instance = self._get_cloud_instance() self.image_id = self._get_initial_image() + def emit_settings_to_log(self) -> None: + log.info( + "\n".join( + ["Settings:"] + + [ + "{}={}".format(key, getattr(self.settings, key)) + for key in sorted(self.settings.current_settings) + ] + ) + ) + @abstractmethod def _get_cloud_instance(self): raise NotImplementedError diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 34e674e9..eacb2ae2 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -62,6 +62,7 @@ def session_cloud(): ) cloud = platforms[integration_settings.PLATFORM]() + cloud.emit_settings_to_log() yield cloud cloud.destroy() diff --git a/tests/integration_tests/instances.py b/tests/integration_tests/instances.py index d64c1ab2..0db7c07b 100644 --- a/tests/integration_tests/instances.py +++ b/tests/integration_tests/instances.py @@ -29,17 +29,6 @@ class IntegrationInstance: self.instance = instance self.settings = settings - def emit_settings_to_log(self) -> None: - log.info( - "\n".join( - ["Settings:"] - + [ - "{}={}".format(key, getattr(self.settings, key)) - for key in sorted(self.settings.current_settings) - ] - ) - ) - def destroy(self): self.instance.delete() -- cgit v1.2.3 From 9707a08a82161cd4129f6cdd10978cde50bea747 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Thu, 19 Nov 2020 11:54:35 -0600 Subject: Make mount in place for tests work (#667) IMAGE_SOURCE = 'IN_PLACE' wasn't working previously. Replaced LXD launch with an init, then mount, then start. --- tests/integration_tests/clouds.py | 44 ++++++++++++++++++++++++++++++++++-- tests/integration_tests/conftest.py | 2 +- tests/integration_tests/instances.py | 17 -------------- 3 files changed, 43 insertions(+), 20 deletions(-) (limited to 'tests/integration_tests/clouds.py') diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index fe89c0c6..2841261b 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -4,6 +4,8 @@ import logging from pycloudlib import EC2, GCE, Azure, OCI, LXD +import cloudinit +from cloudinit.subp import subp from tests.integration_tests import integration_settings from tests.integration_tests.instances import ( IntegrationEc2Instance, @@ -55,6 +57,11 @@ class IntegrationCloud(ABC): pass return image_id + def _perform_launch(self, launch_kwargs): + pycloudlib_instance = self.cloud_instance.launch(**launch_kwargs) + pycloudlib_instance.wait(raise_on_cloudinit_failure=False) + return pycloudlib_instance + def launch(self, user_data=None, launch_kwargs=None, settings=integration_settings): if self.settings.EXISTING_INSTANCE_ID: @@ -77,8 +84,9 @@ class IntegrationCloud(ABC): "\n".join("{}={}".format(*item) for item in kwargs.items()) ) ) - pycloudlib_instance = self.cloud_instance.launch(**kwargs) - pycloudlib_instance.wait(raise_on_cloudinit_failure=False) + + pycloudlib_instance = self._perform_launch(kwargs) + log.info('Launched instance: %s', pycloudlib_instance) return self.get_instance(pycloudlib_instance, settings) @@ -141,3 +149,35 @@ class LxdContainerCloud(IntegrationCloud): def _get_cloud_instance(self): return LXD(tag='lxd-integration-test') + + def _perform_launch(self, launch_kwargs): + launch_kwargs['inst_type'] = launch_kwargs.pop('instance_type', None) + launch_kwargs.pop('wait') + + pycloudlib_instance = self.cloud_instance.init( + launch_kwargs.pop('name', None), + launch_kwargs.pop('image_id'), + **launch_kwargs + ) + if self.settings.CLOUD_INIT_SOURCE == 'IN_PLACE': + self._mount_source(pycloudlib_instance) + pycloudlib_instance.start(wait=False) + pycloudlib_instance.wait(raise_on_cloudinit_failure=False) + return pycloudlib_instance + + def _mount_source(self, instance): + container_path = '/usr/lib/python3/dist-packages/cloudinit' + format_variables = { + 'name': instance.name, + 'cloudinit_path': cloudinit.__path__[0], + 'container_path': container_path, + } + log.info( + 'Mounting source {cloudinit_path} directly onto LXD container/vm ' + 'named {name} at {container_path}'.format(**format_variables)) + command = ( + 'lxc config device add {name} host-cloud-init disk ' + 'source={cloudinit_path} ' + 'path={container_path}' + ).format(**format_variables) + subp(command.split()) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index eacb2ae2..e31a9192 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -81,7 +81,7 @@ def setup_image(session_cloud): if session_cloud.datasource != 'lxd_container': raise ValueError( 'IN_PLACE as CLOUD_INIT_SOURCE only works for LXD') - # The mount needs to happen after the instance is launched, so + # The mount needs to happen after the instance is created, so # no further action needed here elif integration_settings.CLOUD_INIT_SOURCE == 'PROPOSED': client = session_cloud.launch() diff --git a/tests/integration_tests/instances.py b/tests/integration_tests/instances.py index 0db7c07b..67a6fb92 100644 --- a/tests/integration_tests/instances.py +++ b/tests/integration_tests/instances.py @@ -5,8 +5,6 @@ from tempfile import NamedTemporaryFile from pycloudlib.instance import BaseInstance -import cloudinit -from cloudinit.subp import subp from tests.integration_tests import integration_settings try: @@ -129,18 +127,3 @@ class IntegrationOciInstance(IntegrationInstance): class IntegrationLxdContainerInstance(IntegrationInstance): use_sudo = False - - def __init__(self, cloud: 'IntegrationCloud', instance: BaseInstance, - settings=integration_settings): - super().__init__(cloud, instance, settings) - if self.settings.CLOUD_INIT_SOURCE == 'IN_PLACE': - self._mount_source() - - def _mount_source(self): - command = ( - 'lxc config device add {name} host-cloud-init disk ' - 'source={cloudinit_path} ' - 'path=/usr/lib/python3/dist-packages/cloudinit' - ).format( - name=self.instance.name, cloudinit_path=cloudinit.__path__[0]) - subp(command.split()) -- cgit v1.2.3 From 8a493bf08d8b09d4f3a35dae725756d157844201 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Mon, 23 Nov 2020 12:12:52 -0600 Subject: LXD VM support in integration tests (#678) --- integration-requirements.txt | 2 +- tests/integration_tests/clouds.py | 78 +++++++++++++++++++++++++----------- tests/integration_tests/conftest.py | 4 +- tests/integration_tests/instances.py | 2 +- tox.ini | 1 + 5 files changed, 61 insertions(+), 26 deletions(-) (limited to 'tests/integration_tests/clouds.py') diff --git a/integration-requirements.txt b/integration-requirements.txt index 61d2e504..e8ddb648 100644 --- a/integration-requirements.txt +++ b/integration-requirements.txt @@ -1,5 +1,5 @@ # PyPI requirements for cloud-init integration testing # https://cloudinit.readthedocs.io/en/latest/topics/integration_tests.html # -pycloudlib @ git+https://github.com/canonical/pycloudlib.git@e6b2b3732a2a17d48bdf1167f56eb14576215d3c +pycloudlib @ git+https://github.com/canonical/pycloudlib.git@9211c0e5b34794595565d4626bc41ddbe14994f2 pytest diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index 2841261b..88ac4408 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -2,7 +2,8 @@ from abc import ABC, abstractmethod import logging -from pycloudlib import EC2, GCE, Azure, OCI, LXD +from pycloudlib import EC2, GCE, Azure, OCI, LXDContainer, LXDVirtualMachine +from pycloudlib.lxd.instance import LXDInstance import cloudinit from cloudinit.subp import subp @@ -12,7 +13,7 @@ from tests.integration_tests.instances import ( IntegrationGceInstance, IntegrationAzureInstance, IntegrationInstance, IntegrationOciInstance, - IntegrationLxdContainerInstance, + IntegrationLxdInstance, ) try: @@ -143,20 +144,48 @@ class OciCloud(IntegrationCloud): ) -class LxdContainerCloud(IntegrationCloud): - datasource = 'lxd_container' - integration_instance_cls = IntegrationLxdContainerInstance +class _LxdIntegrationCloud(IntegrationCloud): + integration_instance_cls = IntegrationLxdInstance def _get_cloud_instance(self): - return LXD(tag='lxd-integration-test') + return self.pycloudlib_instance_cls(tag=self.instance_tag) + + @staticmethod + def _get_or_set_profile_list(release): + return None + + @staticmethod + def _mount_source(instance: LXDInstance): + target_path = '/usr/lib/python3/dist-packages/cloudinit' + format_variables = { + 'name': instance.name, + 'source_path': cloudinit.__path__[0], + 'container_path': target_path, + } + log.info( + 'Mounting source {source_path} directly onto LXD container/vm ' + 'named {name} at {container_path}'.format(**format_variables)) + command = ( + 'lxc config device add {name} host-cloud-init disk ' + 'source={source_path} ' + 'path={container_path}' + ).format(**format_variables) + subp(command.split()) def _perform_launch(self, launch_kwargs): launch_kwargs['inst_type'] = launch_kwargs.pop('instance_type', None) launch_kwargs.pop('wait') + release = launch_kwargs.pop('image_id') + + try: + profile_list = launch_kwargs['profile_list'] + except KeyError: + profile_list = self._get_or_set_profile_list(release) pycloudlib_instance = self.cloud_instance.init( launch_kwargs.pop('name', None), - launch_kwargs.pop('image_id'), + release, + profile_list=profile_list, **launch_kwargs ) if self.settings.CLOUD_INIT_SOURCE == 'IN_PLACE': @@ -165,19 +194,22 @@ class LxdContainerCloud(IntegrationCloud): pycloudlib_instance.wait(raise_on_cloudinit_failure=False) return pycloudlib_instance - def _mount_source(self, instance): - container_path = '/usr/lib/python3/dist-packages/cloudinit' - format_variables = { - 'name': instance.name, - 'cloudinit_path': cloudinit.__path__[0], - 'container_path': container_path, - } - log.info( - 'Mounting source {cloudinit_path} directly onto LXD container/vm ' - 'named {name} at {container_path}'.format(**format_variables)) - command = ( - 'lxc config device add {name} host-cloud-init disk ' - 'source={cloudinit_path} ' - 'path={container_path}' - ).format(**format_variables) - subp(command.split()) + +class LxdContainerCloud(_LxdIntegrationCloud): + datasource = 'lxd_container' + pycloudlib_instance_cls = LXDContainer + instance_tag = 'lxd-container-integration-test' + + +class LxdVmCloud(_LxdIntegrationCloud): + datasource = 'lxd_vm' + pycloudlib_instance_cls = LXDVirtualMachine + instance_tag = 'lxd-vm-integration-test' + _profile_list = None + + def _get_or_set_profile_list(self, release): + if self._profile_list: + return self._profile_list + self._profile_list = self.cloud_instance.build_necessary_profiles( + release) + return self._profile_list diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 54867096..73b44bfc 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -12,6 +12,7 @@ from tests.integration_tests.clouds import ( AzureCloud, OciCloud, LxdContainerCloud, + LxdVmCloud, ) @@ -25,6 +26,7 @@ platforms = { 'azure': AzureCloud, 'oci': OciCloud, 'lxd_container': LxdContainerCloud, + 'lxd_vm': LxdVmCloud, } @@ -87,7 +89,7 @@ def setup_image(session_cloud): if integration_settings.CLOUD_INIT_SOURCE == 'NONE': pass # that was easy elif integration_settings.CLOUD_INIT_SOURCE == 'IN_PLACE': - if session_cloud.datasource != 'lxd_container': + if session_cloud.datasource not in ['lxd_container', 'lxd_vm']: raise ValueError( 'IN_PLACE as CLOUD_INIT_SOURCE only works for LXD') # The mount needs to happen after the instance is created, so diff --git a/tests/integration_tests/instances.py b/tests/integration_tests/instances.py index 67a6fb92..ca0b38d5 100644 --- a/tests/integration_tests/instances.py +++ b/tests/integration_tests/instances.py @@ -125,5 +125,5 @@ class IntegrationOciInstance(IntegrationInstance): pass -class IntegrationLxdContainerInstance(IntegrationInstance): +class IntegrationLxdInstance(IntegrationInstance): use_sudo = False diff --git a/tox.ini b/tox.ini index f08e0f03..30b11398 100644 --- a/tox.ini +++ b/tox.ini @@ -167,6 +167,7 @@ markers = azure: test will only run on Azure platform oci: test will only run on OCI platform lxd_container: test will only run in LXD container + lxd_vm: test will only run in LXD VM no_container: test cannot run in a container user_data: the user data to be passed to the test instance instance_name: the name to be used for the test instance -- cgit v1.2.3