From d7e515315dbb7d315d537860332c58b864dcd9dc Mon Sep 17 00:00:00 2001 From: Stefan Caraiman Date: Thu, 16 Mar 2017 15:18:46 +0000 Subject: [PATCH] Added base Azure support for Argus Adds support for Argus testing on Azure cloud. This commit adds an Azure backend, an Azure recipe and a scenario (ScenarioAzure) that verifies that no errors are found in the cloudbase-init logs. --- argus/backends/azure/__init__.py | 0 argus/backends/azure/azure_backend.py | 118 +++++++ argus/backends/azure/client.py | 288 ++++++++++++++++++ argus/config/azure.py | 53 ++++ argus/config/factory.py | 1 + argus/recipes/cloud/windows.py | 96 ++++++ .../unit_tests/recipes/cloud/test_windows.py | 2 +- argus/util.py | 32 ++ ci/tests.py | 7 + requirements.txt | 11 + 10 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 argus/backends/azure/__init__.py create mode 100644 argus/backends/azure/azure_backend.py create mode 100644 argus/backends/azure/client.py create mode 100644 argus/config/azure.py diff --git a/argus/backends/azure/__init__.py b/argus/backends/azure/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/argus/backends/azure/azure_backend.py b/argus/backends/azure/azure_backend.py new file mode 100644 index 00000000..d24afb3c --- /dev/null +++ b/argus/backends/azure/azure_backend.py @@ -0,0 +1,118 @@ +# Copyright 2017 Cloudbase Solutions Srl +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import six + +from argus.backends import base +from argus.backends import windows +from argus.backends.azure import client as azure_client + +from argus import config as argus_config +from argus import log as argus_log + +CONFIG = argus_config.CONFIG + +RETRY_COUNT = 50 +RETRY_DELAY = 10 + +LOG = argus_log.LOG + + +# pylint: disable=abstract-method +@six.add_metaclass(abc.ABCMeta) +class BaseAzureBackend(base.CloudBackend): + """A back-end which uses Azure as the driving core.""" + + def __init__(self, name=None, userdata=None, metadata=None, + availability_zone=None): + super(BaseAzureBackend, self).__init__( + name=name, userdata=userdata, metadata=metadata, + availability_zone=availability_zone) + self._azure_client = azure_client.AzureClient( + CONFIG.azure.subscription_id, + CONFIG.azure.username, + CONFIG.azure.password, + CONFIG.azure.resource_group_name, + CONFIG.azure.storage_account_name, + CONFIG.azure.resource_group_location, + CONFIG.azure.image_vhd_path, + CONFIG.azure.vm_username, + CONFIG.azure.vm_password) + + def setup_instance(self): + super(BaseAzureBackend, self).setup_instance() + LOG.info("Creating Azure resource group") + self._azure_client.create_resource_group() + LOG.info("Creating Azure storage account") + self._azure_client.create_storage_account() + LOG.info("Creating Azure VM") + self._azure_client.create_vm() + + def cleanup(self): + LOG.info("Destroying Azure VM") + self._azure_client.destroy_vm() + + def internal_instance_id(self): + """Get the underlying instance ID. + + Gets the instance ID depending on the internals of the back-end. + """ + return self._azure_client.vm_name + + def floating_ip(self): + """Get the underlying floating IP.""" + floating_ip = self._azure_client.get_floating_ip() + LOG.info("Floating ip is {}".format(floating_ip)) + return floating_ip + + def instance_output(self, limit=6500): + """Get the console output, sent from the instance.""" + return "" + + def reboot_instance(self): + """Reboot the underlying instance.""" + pass + + def instance_password(self): + """Get the underlying instance password, if any.""" + return self._azure_client.vm_password + + def private_key(self): + """Get the underlying private key.""" + pass + + def public_key(self): + """Get the underlying public key.""" + pass + + def instance_server(self): + """Get the instance server object.""" + return { + 'name': self._azure_client.vm_name, + 'id': self._azure_client.vm_name + } + + def get_image_by_ref(self): + """Get the image object by its reference id.""" + return { + 'image': { + 'OS-EXT-IMG-SIZE:size': self._azure_client.vm_disk_size + } + } + + +class WindowsAzureBackend(windows.WindowsBackendMixin, BaseAzureBackend): + """Azure back-end tailored to work with Windows platforms.""" diff --git a/argus/backends/azure/client.py b/argus/backends/azure/client.py new file mode 100644 index 00000000..68a10ed5 --- /dev/null +++ b/argus/backends/azure/client.py @@ -0,0 +1,288 @@ +# Copyright 2017 Cloudbase Solutions Srl +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import abc +import random +import six + +from argus import util +from azure.common.credentials import UserPassCredentials +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.storage import StorageManagementClient +from azure.mgmt.network import NetworkManagementClient +from azure.mgmt.compute import ComputeManagementClient + +PROVISIONING_STATE_CREATING = 'Creating' + + +@six.add_metaclass(abc.ABCMeta) +class BaseAzureClient(object): + """Class for managing Azure instances + + """ + def __init__(self, subscription_id=None, username=None, password=None, + resource_group_name=None, storage_account_name=None, + availability_zone=None, image_vhd_path=None, vm_username=None, + vm_password=None): + self.vm_disk_size = 50000 + self._subscription_id = subscription_id + self._username = username + self._password = password + self._resource_group_name = resource_group_name + self._storage_account_name = storage_account_name + self._availability_zone = availability_zone + self._image_vhd_path = image_vhd_path + self._vm_username = vm_username + self.vm_password = vm_password + self._creds = UserPassCredentials(username, password) + self.res_client = ResourceManagementClient( + self._creds, subscription_id) + self._stor_client = StorageManagementClient( + self._creds, subscription_id) + self._vm_client = ComputeManagementClient( + self._creds, subscription_id) + self.net_client = NetworkManagementClient( + self._creds, subscription_id) + + @abc.abstractmethod + def create_vm(self): + pass + + @abc.abstractmethod + def destroy_vm(self): + pass + + @abc.abstractmethod + def create_resource_group(self): + pass + + @abc.abstractmethod + def create_storage_account(self): + pass + + @abc.abstractmethod + def create_nic(self): + pass + + @abc.abstractmethod + def get_floating_ip(self): + pass + + @abc.abstractmethod + def get_vm_state(self): + pass + + +class AzureClient(BaseAzureClient): + """Class for managing Azure instances + """ + def __init__(self, subscription_id=None, username=None, password=None, + resource_group_name=None, storage_account_name=None, + availability_zone=None, image_vhd_path=None, + vm_username=None, vm_password=None): + super(AzureClient, self).__init__( + subscription_id, username, password, resource_group_name, + storage_account_name, availability_zone, image_vhd_path, + vm_username, vm_password) + + self.vm_name = "argusvm-" + AzureClient._get_random_id() + self._os_disk_name = "argusdisk-" + AzureClient._get_random_id() + self._vnet_name = "argusvnet-" + AzureClient._get_random_id() + self._sub_net_name = "argussubnet-" + AzureClient._get_random_id() + self._nic_name = "argusnic-" + AzureClient._get_random_id() + self._vip_name = "argusvip-" + AzureClient._get_random_id() + self._ip_config_name = "argusip-" + AzureClient._get_random_id() + self._sec_group_name = 'argussecgrp-' + AzureClient._get_random_id() + + @staticmethod + def _get_random_id(): + return str(random.randint(0, 10000)) + + def create_vm(self): + nic = self.create_nic() + vm_parameters = self._create_vm_parameters( + self.vm_name, self._vm_username, self.vm_password, + self._os_disk_name, nic.id) + self._vm_client.virtual_machines.create_or_update( + self._resource_group_name, self.vm_name, + vm_parameters) + + self._vm_client.virtual_machines.start( + self._resource_group_name, self.vm_name) + + def destroy_vm(self): + self._vm_client.virtual_machines.delete( + self._resource_group_name, self.vm_name).result() + self.net_client.network_interfaces.delete( + self._resource_group_name, self._nic_name).result() + self.net_client.virtual_networks.delete( + self._resource_group_name, self._vnet_name).result() + self.net_client.network_security_groups.delete( + self._resource_group_name, self._sec_group_name).result() + self.net_client.public_ip_addresses.delete( + self._resource_group_name, self._vip_name).result() + + def create_resource_group(self): + self.res_client.resource_groups.create_or_update( + self._resource_group_name, + { + 'location': self._availability_zone + }) + + def create_storage_account(self): + async_create = self._stor_client.storage_accounts.create( + self._resource_group_name, + self._storage_account_name, + { + 'location': self._availability_zone, + 'sku': {'name': 'standard_lrs'}, + 'kind': 'storage' + }) + async_create.wait() + + def create_security_group_rule(self, sec_group_name, + rule_name, port, priority, direction): + self.net_client.security_rules.create_or_update( + self._resource_group_name, + sec_group_name, rule_name, + { + 'access': 'allow', + 'protocol': 'Tcp', + 'direction': direction, + 'source_address_prefix': '*', + 'destination_address_prefix': '*', + 'source_port_range': '*', + 'destination_port_range': port, + 'priority': priority + }).wait() + + def create_nic(self): + """Create a Network Interface for a VM. + """ + + # pylint: disable=maybe-no-member + sec_gr_id = self.net_client.network_security_groups.create_or_update( + self._resource_group_name, + self._sec_group_name, + { + 'location': self._availability_zone + }).result().id + self.create_security_group_rule( + self._sec_group_name, 'secgrrule-1', '3389', 100, 'inbound') + self.create_security_group_rule( + self._sec_group_name, 'secgrrule-2', '5985', 101, 'inbound') + self.create_security_group_rule( + self._sec_group_name, 'secgrrule-3', '5986', 102, 'inbound') + self.create_security_group_rule( + self._sec_group_name, 'secgrrule-4', '*', 103, 'outbound') + self.net_client.virtual_networks.create_or_update( + self._resource_group_name, + self._vnet_name, + { + 'location': self._availability_zone, + 'address_space': { + 'address_prefixes': ['10.0.0.0/16'] + } + }).wait() + + # pylint: disable=maybe-no-member + subnet_id = self.net_client.subnets.create_or_update( + self._resource_group_name, + self._vnet_name, + self._sub_net_name, + { + 'address_prefix': '10.0.0.0/24', + 'network_security_group': + { + 'id': sec_gr_id + } + }).result().id + + # pylint: disable=maybe-no-member + vip_id = self.net_client.public_ip_addresses.create_or_update( + self._resource_group_name, + self._vip_name, + { + 'location': self._availability_zone, + 'public_ip_allocation_method': 'dynamic' + }).result().id + + return self.net_client.network_interfaces.create_or_update( + self._resource_group_name, + self._nic_name, + { + 'location': self._availability_zone, + 'ip_configurations': [{ + 'name': self._ip_config_name, + 'subnet': { + 'id': subnet_id + }, + 'public_ip_address': { + 'id': vip_id + }, + }] + }).result() + + def _create_vm_parameters(self, vm_name, vm_username, vm_password, + os_disk_name, nic_id): + """Create the VM parameters structure. + """ + vhd_uri = 'https://{}.blob.core.windows.net/vhds/{}.vhd'.format( + self._storage_account_name, vm_name) + return { + 'location': self._availability_zone, + 'os_profile': { + 'computer_name': vm_name, + 'admin_username': vm_username, + 'admin_password': vm_password + }, + 'hardware_profile': { + 'vm_size': 'Standard_D1_v2' + }, + 'storage_profile': { + 'os_disk': { + 'name': os_disk_name, + 'os_type': 'Windows', + 'image': {'uri': self._image_vhd_path}, + 'caching': 'None', + 'create_option': 'fromImage', + 'vhd': { + 'uri': vhd_uri + } + } + }, + 'network_profile': { + 'network_interfaces': [{ + 'id': nic_id, + }] + }, + } + + @util.retry_on_error(max_attempts=30, sleep_seconds=8) + def get_floating_ip(self): + # pylint: disable=maybe-no-member + vm_state = self.get_vm_state() + if vm_state != PROVISIONING_STATE_CREATING: + raise Exception('VM is not in creating state') + floating_ip = self.net_client.public_ip_addresses.get( + self._resource_group_name, self._vip_name).ip_address + if not floating_ip: + raise Exception('Floating IP not available') + return floating_ip + + @util.retry_on_error(max_attempts=5, sleep_seconds=5) + def get_vm_state(self): + # pylint: disable=maybe-no-member + return self._vm_client.virtual_machines.get( + self._resource_group_name, self.vm_name).provisioning_state diff --git a/argus/config/azure.py b/argus/config/azure.py new file mode 100644 index 00000000..cf8e7c0f --- /dev/null +++ b/argus/config/azure.py @@ -0,0 +1,53 @@ +# Copyright 2017 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Config options available for the Azure backend.""" + +from oslo_config import cfg + +from argus.config import base as conf_base + + +class AzureOptions(conf_base.Options): + + """Config options available for the OpenStack setup.""" + + def __init__(self, config): + super(AzureOptions, self).__init__(config, group="azure") + self._options = [ + cfg.StrOpt( + "subscription_id", default=None, + help="The subscription id for the Azure account" + "used for tests."), + cfg.StrOpt( + "username", default=None, + help="The Azure account username."), + cfg.StrOpt("password", default=None), + cfg.StrOpt("storage_account_name", default=None), + cfg.StrOpt("resource_group_name", default=None), + cfg.StrOpt("resource_group_location", default=None), + cfg.StrOpt("image_vhd_path", default=None), + cfg.StrOpt("vm_username", default=None), + cfg.StrOpt("vm_password", default=None), + ] + + def register(self): + """Register the current options to the global ConfigOpts object.""" + group = cfg.OptGroup(self.group_name, title='Azure Options') + self._config.register_group(group) + self._config.register_opts(self._options, group=group) + + def list(self): + """Return a list which contains all the available options.""" + return self._options diff --git a/argus/config/factory.py b/argus/config/factory.py index 505a388f..d304b0b6 100644 --- a/argus/config/factory.py +++ b/argus/config/factory.py @@ -15,6 +15,7 @@ _OPT_PATHS = ( 'argus.config.ci.ArgusOptions', 'argus.config.cloudbaseinit.CloudbaseInitOptions', + 'argus.config.azure.AzureOptions', 'argus.config.openstack.OpenStackOptions', 'argus.config.mock_cloudstack.MockCloudStackOptions', 'argus.config.mock_ec2.MockEC2Options', diff --git a/argus/recipes/cloud/windows.py b/argus/recipes/cloud/windows.py index c06e17ca..d4fe7f00 100644 --- a/argus/recipes/cloud/windows.py +++ b/argus/recipes/cloud/windows.py @@ -222,6 +222,11 @@ def replace_code(self): location=_CBINIT_TARGET_LOCATION), command_type=util.CMD) + command = '"{folder}" -m pip install {location}' + self._execute(command.format(folder=python, + location=_CBINIT_TARGET_LOCATION), + command_type=util.CMD) + def pre_sysprep(self): # Patch the installation of Cloudbase-Init in order to create # a file when the execution ends. We're doing this instead of @@ -572,6 +577,97 @@ def prepare_cbinit_config(self, service_type): ".TrimConfigPlugin") +class CloudbaseinitAzureRecipe(CloudbaseinitRecipe): + """Recipe for testing Azure configuration.""" + + def prepare_cbinit_config(self, service_type): + super(CloudbaseinitAzureRecipe, self).prepare_cbinit_config( + service_type) + self._cbinit_conf.set_conf_value( + name='metadata_services', + value='cloudbaseinit.metadata.services.azureservice.AzureService') + self._cbinit_unattend_conf.set_conf_value( + name='metadata_services', + value='cloudbaseinit.metadata.services.azureservice.AzureService') + self._cbinit_unattend_conf.set_conf_value( + name='allow_reboot', value='false') + self._cbinit_unattend_conf.set_conf_value( + name='san_policy', value='OnlineAll') + + self._cbinit_conf.set_conf_value( + name="plugins", + value="cloudbaseinit.plugins.windows.createuser.CreateUserPlugin," + "cloudbaseinit.plugins.common.setuserpassword." + "SetUserPasswordPlugin," + "cloudbaseinit.plugins.windows.certificates." + "ServerCertificatesPlugin," + "cloudbaseinit.plugins.windows.winrmlistener." + "ConfigWinRMListenerPlugin," + "cloudbaseinit.plugins.windows.updates." + "WindowsAutoUpdatesPlugin," + "cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin," + "cloudbaseinit.plugins.windows.licensing." + "WindowsLicensingPlugin," + "cloudbaseinit.plugins.windows.rdp.RDPSettingsPlugin," + "cloudbaseinit.plugins.windows.rdp." + "RDPPostCertificateThumbprintPlugin," + "cloudbaseinit.plugins.windows.bootconfig.BCDConfigPlugin," + "cloudbaseinit.plugins.windows.userdata.UserDataPlugin," + "cloudbaseinit.plugins.windows.displayidletimeout." + "DisplayIdleTimeoutConfigPlugin," + "cloudbaseinit.plugins.windows.winrmcertificateauth." + "ConfigWinRMCertificateAuthPlugin," + "cloudbaseinit.plugins.windows.azureguestagent." + "AzureGuestAgentPlugin") + self._cbinit_conf.set_conf_value( + name='metadata_report_provisioning_started', value='false') + self._cbinit_conf.set_conf_value( + name='metadata_report_provisioning_completed', value='true') + self._cbinit_conf.set_conf_value( + name='rename_admin_user', value='true') + self._cbinit_conf.set_conf_value( + name='winrm_enable_basic_auth', value='true') + self._cbinit_conf.set_conf_value( + name='first_logon_behaviour', value='no') + self._cbinit_conf.set_conf_value( + name='username', value='admin1') + self._cbinit_conf.set_conf_value( + name='display_idle_timeout', value='120') + self._cbinit_conf.set_conf_value( + name='bcd_enable_auto_recovery', value='true') + self._cbinit_conf.set_conf_value( + name='page_file_volume_labels', value='Temporary Storage') + self._cbinit_conf.set_conf_value( + name='trim_enabled', value='true') + self._cbinit_conf.set_conf_value( + name='userdata_save_path', value=r'C:\userdata.ps1') + self._cbinit_conf.set_conf_value( + name='bcd_boot_status_policy', value='ignoreallfailures') + self._cbinit_conf.set_conf_value( + name='activate_windows', value='true') + + self._cbinit_unattend_conf.set_conf_value( + name="plugins", + value="cloudbaseinit.plugins.windows.bootconfig." + "BootStatusPolicyPlugin," + "cloudbaseinit.plugins.windows.sanpolicy.SANPolicyPlugin," + "cloudbaseinit.plugins.windows.mtu.MTUPlugin," + "cloudbaseinit.plugins.windows.pagefiles.PageFilesPlugin," + "cloudbaseinit.plugins.common.trim.TrimConfigPlugin," + "cloudbaseinit.plugins.windows.displayidletimeout." + "DisplayIdleTimeoutConfigPlugin," + "cloudbaseinit.plugins.windows.extendvolumes." + "ExtendVolumesPlugin," + "cloudbaseinit.plugins.common.sethostname." + "SetHostNamePlugin") + self._cbinit_unattend_conf.set_conf_value( + name='metadata_report_provisioning_started', value='true') + self._cbinit_unattend_conf.set_conf_value( + name='metadata_report_provisioning_completed', value='false') + self._cbinit_unattend_conf.set_conf_value( + name='trim_enabled', value='true') + + class CloudbaseinitIndependentPlugins(CloudbaseinitEnableTrim): """Recipe for independent plugins.""" diff --git a/argus/unit_tests/recipes/cloud/test_windows.py b/argus/unit_tests/recipes/cloud/test_windows.py index c220355e..ccecdcfd 100644 --- a/argus/unit_tests/recipes/cloud/test_windows.py +++ b/argus/unit_tests/recipes/cloud/test_windows.py @@ -313,7 +313,7 @@ def _test_replace_code(self, mock_get_python_dir, mock_execute, python_dir, "Lib", "site-packages", "cloudbaseinit") self.assertEqual(mock_execute.call_count, 1) else: - self.assertEqual(mock_execute.call_count, 4) + self.assertEqual(mock_execute.call_count, 5) self.assertEqual(mock_join.call_count, 2) def test_replace_code_no_git(self): diff --git a/argus/util.py b/argus/util.py index f450f848..46935665 100644 --- a/argus/util.py +++ b/argus/util.py @@ -16,6 +16,7 @@ import base64 import collections import contextlib +import functools import logging import pkgutil import random @@ -28,6 +29,9 @@ import six from argus import exceptions +from argus import log as argus_log + +LOG = argus_log.LOG CMD = "cmd" BAT_SCRIPT = "bat" @@ -362,3 +366,31 @@ def get_command(command, command_type=None): True: WINDOWS_NANO } } + + +def retry_on_error(max_attempts=5, sleep_seconds=0, + terminal_exceptions=[]): + def _retry_on_error(func): + @functools.wraps(func) + def _exec_retry(*args, **kwargs): + i = 0 + while True: + try: + return func(*args, **kwargs) + except KeyboardInterrupt as ex: + LOG.debug("Got a KeyboardInterrupt, skip retrying") + LOG.exception(ex) + raise + except Exception as ex: + if any([isinstance(ex, tex) + for tex in terminal_exceptions]): + raise + + i += 1 + if i < max_attempts: + LOG.debug("Exception occurred, retrying: %s", ex) + time.sleep(sleep_seconds) + else: + raise + return _exec_retry + return _retry_on_error diff --git a/ci/tests.py b/ci/tests.py index ee590a2a..5bdc8daf 100644 --- a/ci/tests.py +++ b/ci/tests.py @@ -16,6 +16,7 @@ import unittest from argus.backends.heat import heat_backend +from argus.backends.azure import azure_backend from argus.backends.tempest import manager from argus.backends.tempest import cloud as tempest_cloud_backend from argus.backends.tempest import tempest_backend @@ -54,6 +55,12 @@ class ScenarioSmoke(BaseWindowsScenario): test_classes = (test_smoke.TestSmoke, ) +class ScenarioAzure(BaseWindowsScenario): + + test_classes = (smoke.TestNoError, ) + backend_type = azure_backend.WindowsAzureBackend + recipe_type = recipe.CloudbaseinitAzureRecipe + class ScenarioSmokeHeat(BaseWindowsScenario): test_classes = (test_smoke.TestSmoke, test_smoke.TestHeatUserdata) diff --git a/requirements.txt b/requirements.txt index de298269..8197eced 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,14 @@ oslo.config python-keystoneclient python-heatclient cherrypy +oslo.messaging +azure-common==1.1.4 +azure-mgmt-compute==0.30.0rc5 +azure-mgmt-network==0.30.0rc5 +azure-mgmt-nspkg==1.0.0 +azure-mgmt-resource==0.30.0rc5 +azure-mgmt-storage==0.30.0rc5 +azure-nspkg==1.0.0 +azure-storage==0.32.0 +msrest==0.4.1 +msrestazure==0.4.1