From aa3e4961ceae5a5c5b5cf13221b5f6721991fe75 Mon Sep 17 00:00:00 2001 From: ahosmanmsft Date: Tue, 26 Nov 2019 11:36:00 -0700 Subject: cloud_tests: add azure platform support to integration tests Added Azure to cloud tests supporting upstream integration testing. Implement the inherited platform classes, Azure configurations to release/platform, and docs on how to run Azure CI. --- tests/cloud_tests/platforms/azurecloud/platform.py | 232 +++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 tests/cloud_tests/platforms/azurecloud/platform.py (limited to 'tests/cloud_tests/platforms/azurecloud/platform.py') diff --git a/tests/cloud_tests/platforms/azurecloud/platform.py b/tests/cloud_tests/platforms/azurecloud/platform.py new file mode 100644 index 00000000..77f159eb --- /dev/null +++ b/tests/cloud_tests/platforms/azurecloud/platform.py @@ -0,0 +1,232 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +"""Base Azure Cloud class.""" + +import os +import base64 +import traceback +from datetime import datetime +from tests.cloud_tests import LOG + +# pylint: disable=no-name-in-module +from azure.common.credentials import ServicePrincipalCredentials +# pylint: disable=no-name-in-module +from azure.mgmt.resource import ResourceManagementClient +# pylint: disable=no-name-in-module +from azure.mgmt.network import NetworkManagementClient +# pylint: disable=no-name-in-module +from azure.mgmt.compute import ComputeManagementClient +# pylint: disable=no-name-in-module +from azure.mgmt.storage import StorageManagementClient +from msrestazure.azure_exceptions import CloudError + +from .image import AzureCloudImage +from .instance import AzureCloudInstance +from ..platforms import Platform + +from cloudinit import util as c_util + + +class AzureCloudPlatform(Platform): + """Azure Cloud test platforms.""" + + platform_name = 'azurecloud' + + def __init__(self, config): + """Set up platform.""" + super(AzureCloudPlatform, self).__init__(config) + self.tag = '%s-%s' % ( + config['tag'], datetime.now().strftime('%Y%m%d%H%M%S')) + self.storage_sku = config['storage_sku'] + self.vm_size = config['vm_size'] + self.location = config['region'] + + try: + self.credentials, self.subscription_id = self._get_credentials() + + self.resource_client = ResourceManagementClient( + self.credentials, self.subscription_id) + self.compute_client = ComputeManagementClient( + self.credentials, self.subscription_id) + self.network_client = NetworkManagementClient( + self.credentials, self.subscription_id) + self.storage_client = StorageManagementClient( + self.credentials, self.subscription_id) + + self.resource_group = self._create_resource_group() + self.public_ip = self._create_public_ip_address() + self.storage = self._create_storage_account(config) + self.vnet = self._create_vnet() + self.subnet = self._create_subnet() + self.nic = self._create_nic() + except CloudError: + raise RuntimeError('failed creating a resource:\n{}'.format( + traceback.format_exc())) + + def create_instance(self, properties, config, features, + image_id, user_data=None): + """Create an instance + + @param properties: image properties + @param config: image configuration + @param features: image features + @param image_id: string of image id + @param user_data: test user-data to pass to instance + @return_value: cloud_tests.instances instance + """ + user_data = str(base64.b64encode( + user_data.encode('utf-8')), 'utf-8') + + return AzureCloudInstance(self, properties, config, features, + image_id, user_data) + + def get_image(self, img_conf): + """Get image using specified image configuration. + + @param img_conf: configuration for image + @return_value: cloud_tests.images instance + """ + ss_region = self.azure_location_to_simplestreams_region() + + filters = [ + 'arch=%s' % 'amd64', + 'endpoint=https://management.core.windows.net/', + 'region=%s' % ss_region, + 'release=%s' % img_conf['release'] + ] + + LOG.debug('finding image using streams') + image = self._query_streams(img_conf, filters) + + try: + image_id = image['id'] + LOG.debug('found image: %s', image_id) + if image_id.find('__') > 0: + image_id = image_id.split('__')[1] + LOG.debug('image_id shortened to %s', image_id) + except KeyError: + raise RuntimeError('no images found for %s' % img_conf['release']) + + return AzureCloudImage(self, img_conf, image_id) + + def destroy(self): + """Delete all resources in resource group.""" + LOG.debug("Deleting resource group: %s", self.resource_group.name) + delete = self.resource_client.resource_groups.delete( + self.resource_group.name) + delete.wait() + + def azure_location_to_simplestreams_region(self): + """Convert location to simplestreams region""" + location = self.location.lower().replace(' ', '') + LOG.debug('finding location %s using simple streams', location) + regions_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'regions.json') + region_simplestreams_map = c_util.load_json( + c_util.load_file(regions_file)) + return region_simplestreams_map.get(location, location) + + def _get_credentials(self): + """Get credentials from environment""" + LOG.debug('getting credentials from environment') + cred_file = os.path.expanduser('~/.azure/credentials.json') + try: + azure_creds = c_util.load_json( + c_util.load_file(cred_file)) + subscription_id = azure_creds['subscriptionId'] + credentials = ServicePrincipalCredentials( + client_id=azure_creds['clientId'], + secret=azure_creds['clientSecret'], + tenant=azure_creds['tenantId']) + return credentials, subscription_id + except KeyError: + raise RuntimeError('Please configure Azure service principal' + ' credentials in %s' % cred_file) + + def _create_resource_group(self): + """Create resource group""" + LOG.debug('creating resource group') + resource_group_name = self.tag + resource_group_params = { + 'location': self.location + } + resource_group = self.resource_client.resource_groups.create_or_update( + resource_group_name, resource_group_params) + return resource_group + + def _create_storage_account(self, config): + LOG.debug('creating storage account') + storage_account_name = 'storage%s' % datetime.now().\ + strftime('%Y%m%d%H%M%S') + storage_params = { + 'sku': { + 'name': config['storage_sku'] + }, + 'kind': "Storage", + 'location': self.location + } + storage_account = self.storage_client.storage_accounts.create( + self.resource_group.name, storage_account_name, storage_params) + return storage_account.result() + + def _create_public_ip_address(self): + """Create public ip address""" + LOG.debug('creating public ip address') + public_ip_name = '%s-ip' % self.resource_group.name + public_ip_params = { + 'location': self.location, + 'public_ip_allocation_method': 'Dynamic' + } + ip = self.network_client.public_ip_addresses.create_or_update( + self.resource_group.name, public_ip_name, public_ip_params) + return ip.result() + + def _create_vnet(self): + """create virtual network""" + LOG.debug('creating vnet') + vnet_name = '%s-vnet' % self.resource_group.name + vnet_params = { + 'location': self.location, + 'address_space': { + 'address_prefixes': ['10.0.0.0/16'] + } + } + vnet = self.network_client.virtual_networks.create_or_update( + self.resource_group.name, vnet_name, vnet_params) + return vnet.result() + + def _create_subnet(self): + """create sub-network""" + LOG.debug('creating subnet') + subnet_name = '%s-subnet' % self.resource_group.name + subnet_params = { + 'address_prefix': '10.0.0.0/24' + } + subnet = self.network_client.subnets.create_or_update( + self.resource_group.name, self.vnet.name, + subnet_name, subnet_params) + return subnet.result() + + def _create_nic(self): + """Create network interface controller""" + LOG.debug('creating nic') + nic_name = '%s-nic' % self.resource_group.name + nic_params = { + 'location': self.location, + 'ip_configurations': [{ + 'name': 'ipconfig', + 'subnet': { + 'id': self.subnet.id + }, + 'publicIpAddress': { + 'id': "/subscriptions/%s" + "/resourceGroups/%s/providers/Microsoft.Network" + "/publicIPAddresses/%s" % ( + self.subscription_id, self.resource_group.name, + self.public_ip.name), + } + }] + } + nic = self.network_client.network_interfaces.create_or_update( + self.resource_group.name, nic_name, nic_params) + return nic.result() -- cgit v1.2.3 From ecffd25df840277ab1fa7d5372659abe833cacbe Mon Sep 17 00:00:00 2001 From: Ryan Harper Date: Thu, 13 Feb 2020 14:11:17 -0600 Subject: azurecloud: fix issues with instances not starting (#205) The azurecloud platform did not always start instances during collect runs. This was a result of two issues. First the image class _instance method did not invoke the start() method which then allowed collect stage to attempt to run scripts without an endpoint. Second, azurecloud used the image_id as both an instance handle (which is typically vmName in azure api) as well as an image handle (for image capture). Resolve this by adding a .vm_name property to the AzureCloudInstance and reference this property in AzureCloudImage. Also in this branch - Fix error encoding user-data when value is None - Add additional logging in AzureCloud platform - Update logging format to print pathname,funcName and line number This greatly eases debugging. LP: #1861921 --- tests/cloud_tests/__init__.py | 3 +- tests/cloud_tests/platforms/azurecloud/image.py | 32 ++++++++++++++-------- tests/cloud_tests/platforms/azurecloud/instance.py | 15 ++++++---- tests/cloud_tests/platforms/azurecloud/platform.py | 5 ++-- tests/cloud_tests/setup_image.py | 2 +- 5 files changed, 36 insertions(+), 21 deletions(-) (limited to 'tests/cloud_tests/platforms/azurecloud/platform.py') diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py index dd436989..6c632f99 100644 --- a/tests/cloud_tests/__init__.py +++ b/tests/cloud_tests/__init__.py @@ -22,7 +22,8 @@ def _initialize_logging(): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + '%(asctime)s - %(pathname)s:%(funcName)s:%(lineno)s ' + '[%(levelname)s]: %(message)s') console = logging.StreamHandler() console.setLevel(logging.DEBUG) diff --git a/tests/cloud_tests/platforms/azurecloud/image.py b/tests/cloud_tests/platforms/azurecloud/image.py index 96a946f3..aad2bca1 100644 --- a/tests/cloud_tests/platforms/azurecloud/image.py +++ b/tests/cloud_tests/platforms/azurecloud/image.py @@ -21,26 +21,26 @@ class AzureCloudImage(Image): @param image_id: image id used to boot instance """ super(AzureCloudImage, self).__init__(platform, config) - self.image_id = image_id self._img_instance = None + self.image_id = image_id @property def _instance(self): """Internal use only, returns a running instance""" - LOG.debug('creating instance') if not self._img_instance: self._img_instance = self.platform.create_instance( self.properties, self.config, self.features, self.image_id, user_data=None) + self._img_instance.start(wait=True, wait_for_cloud_init=True) return self._img_instance def destroy(self): """Delete the instance used to create a custom image.""" - LOG.debug('deleting VM that was used to create image') if self._img_instance: - LOG.debug('Deleting instance %s', self._img_instance.name) + LOG.debug('Deleting backing instance %s', + self._img_instance.vm_name) delete_vm = self.platform.compute_client.virtual_machines.delete( - self.platform.resource_group.name, self.image_id) + self.platform.resource_group.name, self._img_instance.vm_name) delete_vm.wait() super(AzureCloudImage, self).destroy() @@ -48,7 +48,7 @@ class AzureCloudImage(Image): def _execute(self, *args, **kwargs): """Execute command in image, modifying image.""" LOG.debug('executing commands on image') - self._instance.start() + self._instance.start(wait=True) return self._instance._execute(*args, **kwargs) def push_file(self, local_path, remote_path): @@ -72,21 +72,26 @@ class AzureCloudImage(Image): Otherwise runs the clean script, deallocates, generalizes and creates custom image from instance. """ - LOG.debug('creating image from VM') + LOG.debug('creating snapshot of image') if not self._img_instance: + LOG.debug('No existing image, snapshotting base image') return AzureCloudSnapshot(self.platform, self.properties, self.config, self.features, - self.image_id, delete_on_destroy=False) + self._instance.vm_name, + delete_on_destroy=False) + LOG.debug('creating snapshot from instance: %s', self._img_instance) if self.config.get('boot_clean_script'): self._img_instance.run_script(self.config.get('boot_clean_script')) + LOG.debug('deallocating instance %s', self._instance.vm_name) deallocate = self.platform.compute_client.virtual_machines.deallocate( - self.platform.resource_group.name, self.image_id) + self.platform.resource_group.name, self._instance.vm_name) deallocate.wait() + LOG.debug('generalizing instance %s', self._instance.vm_name) self.platform.compute_client.virtual_machines.generalize( - self.platform.resource_group.name, self.image_id) + self.platform.resource_group.name, self._instance.vm_name) image_params = { "location": self.platform.location, @@ -96,13 +101,16 @@ class AzureCloudImage(Image): } } } + LOG.debug('updating resource group image %s', self._instance.vm_name) self.platform.compute_client.images.create_or_update( - self.platform.resource_group.name, self.image_id, + self.platform.resource_group.name, self._instance.vm_name, image_params) + LOG.debug('destroying self') self.destroy() + LOG.debug('snapshot complete') return AzureCloudSnapshot(self.platform, self.properties, self.config, - self.features, self.image_id) + self.features, self._instance.vm_name) # vi: ts=4 expandtab diff --git a/tests/cloud_tests/platforms/azurecloud/instance.py b/tests/cloud_tests/platforms/azurecloud/instance.py index 3d77a1a7..f1e28a96 100644 --- a/tests/cloud_tests/platforms/azurecloud/instance.py +++ b/tests/cloud_tests/platforms/azurecloud/instance.py @@ -41,6 +41,7 @@ class AzureCloudInstance(Instance): self.ssh_ip = None self.instance = None self.image_id = image_id + self.vm_name = 'ci-azure-i-%s' % self.platform.tag self.user_data = user_data self.ssh_key_file = os.path.join( platform.config['data_dir'], platform.config['private_key']) @@ -74,16 +75,18 @@ class AzureCloudInstance(Instance): self.image_id ) image_exists = True - LOG.debug('image found, launching instance') + LOG.debug('image found, launching instance, image_id=%s', + self.image_id) except CloudError: - LOG.debug( - 'image not found, launching instance with base image') + LOG.debug(('image not found, launching instance with base image, ' + 'image_id=%s'), self.image_id) pass vm_params = { + 'name': self.vm_name, 'location': self.platform.location, 'os_profile': { - 'computer_name': 'CI', + 'computer_name': 'CI-%s' % self.platform.tag, 'admin_username': self.ssh_username, "customData": self.user_data, "linuxConfiguration": { @@ -129,7 +132,9 @@ class AzureCloudInstance(Instance): try: self.instance = self.platform.compute_client.virtual_machines.\ create_or_update(self.platform.resource_group.name, - self.image_id, vm_params) + self.vm_name, vm_params) + LOG.debug('creating instance %s from image_id=%s', self.vm_name, + self.image_id) except CloudError: raise RuntimeError('failed creating instance:\n{}'.format( traceback.format_exc())) diff --git a/tests/cloud_tests/platforms/azurecloud/platform.py b/tests/cloud_tests/platforms/azurecloud/platform.py index 77f159eb..cb62a74b 100644 --- a/tests/cloud_tests/platforms/azurecloud/platform.py +++ b/tests/cloud_tests/platforms/azurecloud/platform.py @@ -74,8 +74,9 @@ class AzureCloudPlatform(Platform): @param user_data: test user-data to pass to instance @return_value: cloud_tests.instances instance """ - user_data = str(base64.b64encode( - user_data.encode('utf-8')), 'utf-8') + if user_data is not None: + user_data = str(base64.b64encode( + user_data.encode('utf-8')), 'utf-8') return AzureCloudInstance(self, properties, config, features, image_id, user_data) diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py index a8aaba15..69e66e3f 100644 --- a/tests/cloud_tests/setup_image.py +++ b/tests/cloud_tests/setup_image.py @@ -229,7 +229,7 @@ def setup_image(args, image): except Exception as e: info = "N/A (%s)" % e - LOG.info('setting up %s (%s)', image, info) + LOG.info('setting up image %s (info %s)', image, info) res = stage.run_stage( 'set up for {}'.format(image), calls, continue_after_error=False) return res -- cgit v1.2.3