diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 74562774..72dd4afc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,7 +24,7 @@ steps: AZURE_CLIENT_ID: $(SPAPPID) AZURE_CLIENT_SECRET: $(SPPW) - script: | - python3 test/avere_template_deploy.py --skip-az-ops + python3 test/test_avere_template_deploy.py --skip-az-ops displayName: 'Test Avere vFXT template-based deployment' env: AVERE_ADMIN_PW: $(controllerpassword) diff --git a/test/avere_template_deploy.py b/test/avere_template_deploy.py index 80f3bb3d..91fc6def 100644 --- a/test/avere_template_deploy.py +++ b/test/avere_template_deploy.py @@ -1,228 +1,110 @@ #!/usr/bin/python3 """ -Test template-based Avere vFXT deployment. +Class used for testing template-based deployment of the Avere vFXT product. -Assumptions: - 1. The caller/script is able to write to the current working directory. - 2. Azure secrets are stored in the following environment variables: - * AVERE_ADMIN_PW - * AVERE_CONTROLLER_PW - * AZURE_CLIENT_ID - * AZURE_CLIENT_SECRET - * AZURE_TENANT_ID - * AZURE_SUBSCRIPTION_ID +Objects require the following environment variables at instantiation: + * AVERE_ADMIN_PW + * AVERE_CONTROLLER_PW + * AZURE_CLIENT_ID + * AZURE_CLIENT_SECRET + * AZURE_TENANT_ID + * AZURE_SUBSCRIPTION_ID """ -import argparse import json import os -import random -import sys -import time +from pprint import pformat +from random import choice from string import ascii_lowercase, digits -from urllib.request import urlretrieve +import requests from azure.common.credentials import ServicePrincipalCredentials from azure.mgmt.resource import ResourceManagementClient from azure.mgmt.resource.resources.models import DeploymentMode -# GLOBAL VARIABLES ############################################################ -DEFAULT_LOCATION = 'eastus2' -DEPLOY_PARAMS = {} -RESOURCE_GROUP_CREATED = False -SCRIPT_ARGS = None -TEMPLATE_URL = 'https://raw.githubusercontent.com/Azure/Avere/master/src/vfxt/azuredeploy-auto.json' -TEMPLATE_LOCAL_FILE = os.path.basename(TEMPLATE_URL) +class AvereTemplateDeploy: + def __init__(self, deploy_params={}, resource_group=None, + location='eastus2', debug=True): + """Initialize, authenticate to Azure, generate deploy params.""" + self._template_url = 'https://raw.githubusercontent.com/Azure/Avere/master/src/vfxt/azuredeploy-auto.json' + self.debug = debug -# FUNCTIONS ################################################################### + self.deploy_params = deploy_params + self.resource_group = self.deploy_params.pop('resourceGroup', + resource_group) + self.location = self.deploy_params.pop('location', location) -def load_credentials(): - """Loads Azure credentials from environment variables.""" - print('> Load Azure credentials.') - return ResourceManagementClient( - credentials=ServicePrincipalCredentials( - client_id=os.environ['AZURE_CLIENT_ID'], - secret=os.environ['AZURE_CLIENT_SECRET'], - tenant=os.environ['AZURE_TENANT_ID'] - ), - subscription_id=os.environ['AZURE_SUBSCRIPTION_ID'] - ) + self._debug('> Loading Azure credentials') + self.rm_client = ResourceManagementClient( + credentials=ServicePrincipalCredentials( + client_id=os.environ['AZURE_CLIENT_ID'], + secret=os.environ['AZURE_CLIENT_SECRET'], + tenant=os.environ['AZURE_TENANT_ID'] + ), + subscription_id=os.environ['AZURE_SUBSCRIPTION_ID'] + ) -def load_params(): - """ - Loads the parameters needed in this script (e.g., resource group name). - - If the user specified a parameters file, load those values into - DEPLOY_PARAMS. Otherwise, generate the parameter values and store those - values for re-use. The generated parameter values are stored in the current - working directory as .params.json. - """ - global DEPLOY_PARAMS - if SCRIPT_ARGS.param_file: # Open user-specified params file. - with open(SCRIPT_ARGS.param_file) as config_file: - DEPLOY_PARAMS = json.load(config_file) - else: # Generate and store params. - random_id = 'av' + \ - ''.join(random.choice(ascii_lowercase + digits) for _ in range(6)) - rg_name = 'aapipe-' + random_id + '-rg' - DEPLOY_PARAMS = { - 'resource-group': rg_name, - 'parameters': { - 'virtualNetworkResourceGroup': rg_name, + if not self.deploy_params: + random_id = 'av' + \ + ''.join(choice(ascii_lowercase + digits) for _ in range(6)) + self.resource_group = 'aapipe-' + random_id + '-rg' + self.deploy_params = { + 'virtualNetworkResourceGroup': self.resource_group, 'virtualNetworkName': random_id + '-vnet', 'virtualNetworkSubnetName': random_id + '-subnet', 'avereBackedStorageAccountName': random_id + 'sa', 'controllerName': random_id + '-con', 'controllerAuthenticationType': 'password' } - } - with open(DEPLOY_PARAMS['resource-group'] + '.params.json', 'w') as pf: - json.dump(DEPLOY_PARAMS, pf) + self._debug('> Generated deploy parameters: \n{}'.format( + json.dumps(self.deploy_params, indent=4))) - # Set location/region. Precedence: command-line > params file > default - if not SCRIPT_ARGS.location: - SCRIPT_ARGS.location = DEPLOY_PARAMS.pop('location', DEFAULT_LOCATION) - _debug('SCRIPT_ARGS.location = {}'.format(SCRIPT_ARGS.location)) - _debug('DEPLOY_PARAMS (before secrets): \n{}'.format( - json.dumps(DEPLOY_PARAMS, indent=4))) - - # Add secrets to the parameters for template deployment. - secrets = { - 'adminPassword': os.environ['AVERE_ADMIN_PW'], - 'controllerPassword': os.environ['AVERE_CONTROLLER_PW'], - 'servicePrincipalAppId': os.environ['AZURE_CLIENT_ID'], - 'servicePrincipalPassword': os.environ['AZURE_CLIENT_SECRET'], - 'servicePrincipalTenant': os.environ['AZURE_TENANT_ID'] - } - DEPLOY_PARAMS['parameters'] = {**DEPLOY_PARAMS['parameters'], **secrets} - -def create_resource_group(rm_client): - """Creates an Azure resource group.""" - global RESOURCE_GROUP_CREATED - print('> Creating resource group: ' + DEPLOY_PARAMS['resource-group']) - if not SCRIPT_ARGS.skip_az_ops: - rg = rm_client.resource_groups.create_or_update( - DEPLOY_PARAMS['resource-group'], - {'location': SCRIPT_ARGS.location} + def create_resource_group(self): + """Creates the Azure resource group for this deployment.""" + self._debug('> Creating resource group: ' + self.resource_group) + return self.rm_client.resource_groups.create_or_update( + self.resource_group, + {'location': self.location} ) - _debug('Resource Group = {}'.format(rg)) - RESOURCE_GROUP_CREATED = True -def deploy_template(rm_client): - """Deploys the Avere vFXT template.""" - print('> Deploying template') + def delete_resource_group(self): + """Deletes the Azure resource group for this deployment.""" + self._debug('> Deleting resource group: ' + self.resource_group) + return self.rm_client.resource_groups.delete(self.resource_group) - # Prepare parameters. - parameters = {k: {'value': v} for k, v in DEPLOY_PARAMS['parameters'].items()} + def deploy(self): + """Deploys the Avere vFXT template.""" + self._debug('> Deploying template') - if not SCRIPT_ARGS.skip_az_ops: - op = rm_client.deployments.create_or_update( - resource_group_name=DEPLOY_PARAMS['resource-group'], + deploy_secrets = { + 'adminPassword': os.environ['AVERE_ADMIN_PW'], + 'controllerPassword': os.environ['AVERE_CONTROLLER_PW'], + 'servicePrincipalAppId': os.environ['AZURE_CLIENT_ID'], + 'servicePrincipalPassword': os.environ['AZURE_CLIENT_SECRET'], + 'servicePrincipalTenant': os.environ['AZURE_TENANT_ID'] + } + params = {**self.deploy_params, **deploy_secrets} + + return self.rm_client.deployments.create_or_update( + resource_group_name=self.resource_group, deployment_name='azuredeploy-auto', properties={ 'mode': DeploymentMode.incremental, - 'template': _load_template(), - 'parameters': parameters + 'parameters': {k: {'value': v} for k, v in params.items()}, + 'template': requests.get(self._template_url).json() } ) - _wait_for_op(op) -def delete_resource_group(rm_client): - """Deletes the resource group.""" - print('> Deleting resource group: ' + DEPLOY_PARAMS['resource-group']) - if not SCRIPT_ARGS.skip_az_ops: - op = rm_client.resource_groups.delete(DEPLOY_PARAMS['resource-group']) - _wait_for_op(op) + def _debug(self, s): + """Prints the passed string, with a DEBUG header, if debug is on.""" + if self.debug: + print('[DEBUG]: {}'.format(s)) -def cleanup(rm_client): - """ - Performs multiple cleanup activities. - 1. Deletes the downloaded template file. - 2. Deletes the resource group. - """ - print('> Cleaning up') - if os.path.isfile(TEMPLATE_LOCAL_FILE): - os.remove(TEMPLATE_LOCAL_FILE) - - if not SCRIPT_ARGS.skip_rg_cleanup and RESOURCE_GROUP_CREATED: - delete_resource_group(rm_client) - -# HELPER FUNCTIONS ############################################################ - -def _load_template(): - """Downloads the Avere vFXT deployment template.""" - _debug('Downloading template: ' + TEMPLATE_URL) - urlretrieve(TEMPLATE_URL, filename=TEMPLATE_LOCAL_FILE) - with open(TEMPLATE_LOCAL_FILE, 'r') as template_file_fd: - template = json.load(template_file_fd) - return template - -def _wait_for_op(op, timeout_sec=60): - """ - Wait for a long-running operation (op) for timeout_sec seconds. - - op is an AzureOperationPoller object. - """ - time_start = time.time() - while not op.done(): - op.wait(timeout=timeout_sec) - print('>> operation status: {0} ({1} sec)'.format( - op.status(), int(time.time() - time_start))) - result = op.result() - if result: - print('>> operation result: {}'.format(result)) - -def _debug(s): - """Prints the passed string, with a DEBUG header, if debug is on.""" - if SCRIPT_ARGS.debug: - print('[DEBUG]: {}'.format(s)) - -# MAIN ######################################################################## - -def main(): - """Main script driver.""" - rm_client = load_credentials() - load_params() - retcode = 0 # PASS - try: - create_resource_group(rm_client) - deploy_template(rm_client) - except Exception as ex: - print('\n' + ('><' * 40)) - print('> TEST FAILED') - print('> EXCEPTION TEXT: {}'.format(ex)) - print(('><' * 40) + '\n') - retcode = 1 # FAIL - raise - except: - retcode = 2 # FAIL - raise - finally: - cleanup(rm_client) - print('> SCRIPT COMPLETE. Resource Group: {} (region: {})'.format( - DEPLOY_PARAMS['resource-group'], SCRIPT_ARGS.location)) - print('> RESULT: ' + ('FAIL' if retcode else 'PASS')) - sys.exit(retcode) + def __str__(self): + return pformat(vars(self), indent=4) if __name__ == '__main__': - arg_parser = argparse.ArgumentParser( - description='Test template-based Avere vFXT deployment.') - - arg_parser.add_argument('-p', '--param-file', default=None, - help='Full path to JSON params file. ' + - 'Default: None (generate new params)') - arg_parser.add_argument('-l', '--location', default=None, - help='Azure location (region short name) to use for deployment. ' + - 'Default: ' + DEFAULT_LOCATION) - arg_parser.add_argument('-xc', '--skip-rg-cleanup', action='store_true', - help='Do NOT delete the resource group during cleanup.') - arg_parser.add_argument('-xo', '--skip-az-ops', action='store_true', - help='Do NOT actually run any of the Azure operations.') - arg_parser.add_argument('-d', '--debug', action='store_true', - help='Turn on script debugging.') - SCRIPT_ARGS = arg_parser.parse_args() - - main() + pass diff --git a/test/requirements.txt b/test/requirements.txt index 3cc3cb7f..90d9dbc6 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1 +1,2 @@ azure.mgmt.resource +requests \ No newline at end of file diff --git a/test/test_avere_template_deploy.py b/test/test_avere_template_deploy.py new file mode 100644 index 00000000..da4dde40 --- /dev/null +++ b/test/test_avere_template_deploy.py @@ -0,0 +1,114 @@ +#!/usr/bin/python3 + +""" +Driver for testing template-based deployment of the Avere vFXT product. +""" + +import argparse +import json +import sys +import time + +from avere_template_deploy import AvereTemplateDeploy + + +# HELPER FUNCTIONS ############################################################ + +def wait_for_op(op, timeout_sec=60): + """ + Wait for a long-running operation (op) for timeout_sec seconds. + + op is an AzureOperationPoller object. + """ + time_start = time.time() + while not op.done(): + op.wait(timeout=timeout_sec) + print('>> operation status: {0} ({1} sec)'.format( + op.status(), int(time.time() - time_start))) + result = op.result() + if result: + print('>> operation result: {}'.format(result)) + + +# MAIN ######################################################################## + +def main(script_args): + def debug(s): + """Prints the passed string, with a DEBUG header, if debug is on.""" + if script_args.debug: + print('[DEBUG]: {}'.format(s)) + + """Main script driver.""" + retcode = 0 # PASS + try: + deploy_params = {} + if script_args.param_file: # Open user-specified params file. + with open(script_args.param_file) as pfile: + deploy_params = json.load(pfile) + + atd = AvereTemplateDeploy( + debug=script_args.debug, + location=script_args.location, + deploy_params=deploy_params + ) + debug('atd = \n{}'.format(atd)) + + if not script_args.param_file: + dparams = { + **atd.deploy_params, + 'resourceGroup': atd.resource_group + } + with open(atd.resource_group + '.params.json', 'w') as pfile: + json.dump(dparams, pfile) + + print('> Creating resource group: ' + atd.resource_group) + if not script_args.skip_az_ops: + rg = atd.create_resource_group() + debug('Resource Group = {}'.format(rg)) + + print('> Deploying template') + if not script_args.skip_az_ops: + wait_for_op(atd.deploy()) + except Exception as ex: + print('\n' + ('><' * 40)) + print('> TEST FAILED') + print('> EXCEPTION TEXT: {}'.format(ex)) + print(('><' * 40) + '\n') + retcode = 1 # FAIL + raise + except: + retcode = 2 # FAIL + raise + finally: + if not script_args.skip_rg_cleanup: + print('> Deleting resource group: ' + atd.resource_group) + if not script_args.skip_az_ops: + wait_for_op(atd.delete_resource_group()) + + print('> SCRIPT COMPLETE. Resource Group: {} (region: {})'.format( + atd.resource_group, atd.location)) + print('> RESULT: ' + ('FAIL' if retcode else 'PASS')) + sys.exit(retcode) + + +if __name__ == '__main__': + default_location = 'eastus2' + + arg_parser = argparse.ArgumentParser( + description='Test template-based Avere vFXT deployment.') + + arg_parser.add_argument('-p', '--param-file', default=None, + help='Full path to JSON params file. ' + + 'Default: None (generate new params)') + arg_parser.add_argument('-l', '--location', default=default_location, + help='Azure location (region short name) to use for deployment. ' + + 'Default: ' + default_location) + arg_parser.add_argument('-xc', '--skip-rg-cleanup', action='store_true', + help='Do NOT delete the resource group during cleanup.') + arg_parser.add_argument('-xo', '--skip-az-ops', action='store_true', + help='Do NOT actually run any of the Azure operations.') + arg_parser.add_argument('-d', '--debug', action='store_true', + help='Turn on script debugging.') + script_args = arg_parser.parse_args() + + main(script_args)