diff --git a/Makefile b/Makefile index 17c2496..c19f016 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,9 @@ lint: @flake8 --exclude hooks/charmhelpers hooks tests unit_tests @charm proof -unit_test: - @echo Unit tests... - @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests - -test: unit_test +test: @# Bundletester expects unit tests here. + @$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests functional_test: @echo Starting all functional, lint and unit tests... diff --git a/tests/README b/tests/README index ad0921b..ee4dc11 100644 --- a/tests/README +++ b/tests/README @@ -1,6 +1,29 @@ This directory provides Amulet tests that focus on verification of heat deployments. +test_* methods are called in lexical sort order. + +Test name convention to ensure desired test order: + 1xx service and endpoint checks + 2xx relation checks + 3xx config checks + 4xx functional checks + 9xx restarts and other final checks + +Common uses of heat relations in deployments: + - [ heat, mysql ] + - [ heat, keystone ] + - [ heat, rabbitmq-server ] + +More detailed relations of heat service in a common deployment: + relations: + amqp: + - rabbitmq-server + identity-service: + - keystone + shared-db: + - mysql + In order to run tests, you'll need charm-tools installed (in addition to juju, of course): sudo add-apt-repository ppa:juju/stable diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 3312e46..aa462e7 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -2,34 +2,9 @@ """ Basic heat functional test. - -test_* methods are called in lexical sort order. - -Convention to ensure desired test order: - 1xx service and endpoint checks - 2xx relation checks - 3xx config checks - 4xx functional checks - 9xx restarts and other final checks - -Common relation definitions: - - [ heat, mysql ] - - [ heat, keystone ] - - [ heat, rabbitmq-server ] - -Resultant relations of heat service: - relations: - amqp: - - rabbitmq-server - identity-service: - - keystone - shared-db: - - mysql """ import amulet -import os import time -from heatclient.openstack.common.py3kcompat import urlutils from heatclient.common import template_utils from charmhelpers.contrib.openstack.amulet.deployment import ( @@ -153,26 +128,140 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): # Authenticate admin with heat endpoint self.heat = u.authenticate_heat_admin(self.keystone) - def file_url(self, file_rel_path): - """Return file:// url for a file expressed as a relative path.""" - file_abs_path = os.path.abspath(file_rel_path) - file_url = urlutils.urljoin('file:', - urlutils.pathname2url(file_abs_path)) - return file_url + def _image_create(self): + """Create an image to be used by the heat template, verify it exists""" + u.log.debug('Creating glance image ({})...'.format(IMAGE_NAME)) - def create_or_get_keypair(self, keypair_name="testkey"): - """Create a new keypair, or return pointer if it already exists.""" + # Create a new image + image_new = u.create_cirros_image(self.glance, IMAGE_NAME) + + # Confirm image is created and has status of 'active' + if not image_new: + message = 'glance image create failed' + amulet.raise_status(amulet.FAIL, msg=message) + + # Verify new image name + images_list = list(self.glance.images.list()) + if images_list[0].name != IMAGE_NAME: + message = ('glance image create failed or unexpected ' + 'image name {}'.format(images_list[0].name)) + amulet.raise_status(amulet.FAIL, msg=message) + + def _keypair_create(self): + """Create a keypair to be used by the heat template, + or get a keypair if it exists.""" + self.keypair = u.create_or_get_keypair(self.nova, + keypair_name=KEYPAIR_NAME) + if not self.keypair: + msg = 'Failed to create or get keypair.' + amulet.raise_status(amulet.FAIL, msg=msg) + u.log.debug("Keypair: {} {}".format(self.keypair.id, + self.keypair.fingerprint)) + + def _stack_create(self): + """Create a heat stack from a basic heat template, verify its status""" + u.log.debug('Creating heat stack...') + + t_url = u.file_to_url(TEMPLATE_REL_PATH) + r_req = self.heat.http_client.raw_request + u.log.debug('template url: {}'.format(t_url)) + + t_files, template = template_utils.get_template_contents(t_url, r_req) + env_files, env = template_utils.process_environment_and_files( + env_path=None) + + fields = { + 'stack_name': STACK_NAME, + 'timeout_mins': '15', + 'disable_rollback': False, + 'parameters': { + 'admin_pass': 'Ubuntu', + 'key_name': KEYPAIR_NAME, + 'image': IMAGE_NAME + }, + 'template': template, + 'files': dict(list(t_files.items()) + list(env_files.items())), + 'environment': env + } + + # Create the stack. try: - _keypair = self.nova.keypairs.get(keypair_name) - u.log.debug('Keypair ({}) already exists, ' - 'using it.'.format(keypair_name)) - return _keypair - except: - u.log.debug('Keypair ({}) does not exist, ' - 'creating it.'.format(keypair_name)) + _stack = self.heat.stacks.create(**fields) + u.log.debug('Stack data: {}'.format(_stack)) + _stack_id = _stack['stack']['id'] + u.log.debug('Creating new stack, ID: {}'.format(_stack_id)) + except Exception as e: + # Generally, an api or cloud config error if this is hit. + msg = 'Failed to create heat stack: {}'.format(e) + amulet.raise_status(amulet.FAIL, msg=msg) - _keypair = self.nova.keypairs.create(name=keypair_name) - return _keypair + # Confirm stack reaches COMPLETE status. + # /!\ Heat stacks reach a COMPLETE status even when nova cannot + # find resources (a valid hypervisor) to fit the instance, in + # which case the heat stack self-deletes! Confirm anyway... + ret = u.resource_reaches_status(self.heat.stacks, _stack_id, + expected_stat="COMPLETE", + msg="Stack status wait") + _stacks = list(self.heat.stacks.list()) + u.log.debug('All stacks: {}'.format(_stacks)) + if not ret: + msg = 'Heat stack failed to reach expected state.' + amulet.raise_status(amulet.FAIL, msg=msg) + + # Confirm stack still exists. + try: + _stack = self.heat.stacks.get(STACK_NAME) + except Exception as e: + # Generally, a resource availability issue if this is hit. + msg = 'Failed to get heat stack: {}'.format(e) + amulet.raise_status(amulet.FAIL, msg=msg) + + # Confirm stack name. + u.log.debug('Expected, actual stack name: {}, ' + '{}'.format(STACK_NAME, _stack.stack_name)) + if STACK_NAME != _stack.stack_name: + msg = 'Stack name mismatch, {} != {}'.format(STACK_NAME, + _stack.stack_name) + amulet.raise_status(amulet.FAIL, msg=msg) + + def _stack_resource_compute(self): + """Confirm that the stack has created a subsequent nova + compute resource, and confirm its status.""" + u.log.debug('Confirming heat stack resource status...') + + # Confirm existence of a heat-generated nova compute resource. + _resource = self.heat.resources.get(STACK_NAME, RESOURCE_TYPE) + _server_id = _resource.physical_resource_id + if _server_id: + u.log.debug('Heat template spawned nova instance, ' + 'ID: {}'.format(_server_id)) + else: + msg = 'Stack failed to spawn a nova compute resource (instance).' + amulet.raise_status(amulet.FAIL, msg=msg) + + # Confirm nova instance reaches ACTIVE status. + ret = u.resource_reaches_status(self.nova.servers, _server_id, + expected_stat="ACTIVE", + msg="nova instance") + if not ret: + msg = 'Nova compute instance failed to reach expected state.' + amulet.raise_status(amulet.FAIL, msg=msg) + + def _stack_delete(self): + """Delete a heat stack, verify.""" + u.log.debug('Deleting heat stack...') + u.delete_resource(self.heat.stacks, STACK_NAME, msg="heat stack") + + def _image_delete(self): + """Delete that image.""" + u.log.debug('Deleting glance image...') + image = self.nova.images.find(name=IMAGE_NAME) + u.delete_resource(self.nova.images, image, msg="glance image") + + def _keypair_delete(self): + """Delete that keypair.""" + u.log.debug('Deleting keypair...') + u.delete_resource(self.nova.keypairs, KEYPAIR_NAME, msg="nova keypair") def test_100_services(self): """Verify the expected services are running on the corresponding @@ -263,6 +352,23 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): message = u.relation_error('heat:mysql shared-db', ret) amulet.raise_status(amulet.FAIL, msg=message) + def test_201_mysql_heat_shared_db_relation(self): + """Verify the mysql:heat shared-db relation data""" + u.log.debug('Checking mysql:heat shared-db relation data...') + unit = self.mysql_sentry + relation = ['shared-db', 'heat:shared-db'] + expected = { + 'private-address': u.valid_ip, + 'db_host': u.valid_ip, + 'heat_allowed_units': 'heat/0', + 'heat_password': u.not_null + } + + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('mysql:heat shared-db', ret) + amulet.raise_status(amulet.FAIL, msg=message) + def test_202_heat_keystone_identity_relation(self): """Verify the heat:keystone identity-service relation data""" u.log.debug('Checking heat:keystone identity-service relation data...') @@ -285,6 +391,30 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): message = u.relation_error('heat:keystone identity-service', ret) amulet.raise_status(amulet.FAIL, msg=message) + def test_203_keystone_heat_identity_relation(self): + """Verify the keystone:heat identity-service relation data""" + u.log.debug('Checking keystone:heat identity-service relation data...') + unit = self.keystone_sentry + relation = ['identity-service', 'heat:identity-service'] + expected = { + 'service_protocol': 'http', + 'service_tenant': 'services', + 'admin_token': 'ubuntutesting', + 'service_password': u.not_null, + 'service_port': '5000', + 'auth_port': '35357', + 'auth_protocol': 'http', + 'private-address': u.valid_ip, + 'auth_host': u.valid_ip, + 'service_username': 'heat-cfn_heat', + 'service_tenant_id': u.not_null, + 'service_host': u.valid_ip + } + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('keystone:heat identity-service', ret) + amulet.raise_status(amulet.FAIL, msg=message) + def test_204_heat_rmq_amqp_relation(self): """Verify the heat:rabbitmq-server amqp relation data""" u.log.debug('Checking heat:rabbitmq-server amqp relation data...') @@ -301,6 +431,22 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): message = u.relation_error('heat:rabbitmq-server amqp', ret) amulet.raise_status(amulet.FAIL, msg=message) + def test_205_rmq_heat_amqp_relation(self): + """Verify the rabbitmq-server:heat amqp relation data""" + u.log.debug('Checking rabbitmq-server:heat amqp relation data...') + unit = self.rabbitmq_sentry + relation = ['amqp', 'heat:amqp'] + expected = { + 'private-address': u.valid_ip, + 'password': u.not_null, + 'hostname': u.valid_ip, + } + + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('rabbitmq-server:heat amqp', ret) + amulet.raise_status(amulet.FAIL, msg=message) + def test_300_heat_config(self): """Verify the data in the heat config file.""" u.log.debug('Checking heat config file data...') @@ -338,6 +484,10 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): 'environment_dir': '/etc/heat/environment.d', 'deferred_auth_method': 'password', 'host': 'heat', + 'rabbit_userid': 'heat', + 'rabbit_virtual_host': 'openstack', + 'rabbit_password': rmq_rel['password'], + 'rabbit_host': rmq_rel['hostname'] }, 'keystone_authtoken': { 'auth_uri': auth_uri, @@ -363,15 +513,6 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): }, } - expected['DEFAULT'].update( - { - 'rabbit_userid': 'heat', - 'rabbit_virtual_host': 'openstack', - 'rabbit_password': rmq_rel['password'], - 'rabbit_host': rmq_rel['hostname'] - } - ) - for section, pairs in expected.iteritems(): ret = u.validate_config_data(unit, conf, section, pairs) if ret: @@ -379,7 +520,8 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): amulet.raise_status(amulet.FAIL, msg=message) def test_400_heat_resource_types_list(self): - """Check default heat resource list functionality.""" + """Check default heat resource list behavior, also confirm + heat functionality.""" u.log.debug('Checking default heat resouce list...') try: types = list(self.heat.resource_types.list()) @@ -390,7 +532,7 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): u.log.error('{}'.format(msg)) raise if len(types) > 0: - u.log.debug('Resource type list length is non-zero ' + u.log.debug('Resource type list is populated ' '({}, ok).'.format(len(types))) else: msg = 'Resource type list length is zero!' @@ -402,7 +544,8 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): raise def test_402_heat_stack_list(self): - """Check default heat stack list functionality.""" + """Check default heat stack list behavior, also confirm + heat functionality.""" u.log.debug('Checking default heat stack list...') try: stacks = list(self.heat.stacks.list()) @@ -417,139 +560,16 @@ class HeatBasicDeployment(OpenStackAmuletDeployment): u.log.error(msg) raise - def test_410_image_create(self): - """Create an image to be used by the heat template, verify it exists""" - u.log.debug('Creating glance image ({})...'.format(IMAGE_NAME)) - - # Create a new image - image_new = u.create_cirros_image(self.glance, IMAGE_NAME) - - # Confirm image is created and has status of 'active' - if not image_new: - message = 'glance image create failed' - amulet.raise_status(amulet.FAIL, msg=message) - - # Verify new image name - images_list = list(self.glance.images.list()) - if images_list[0].name != IMAGE_NAME: - message = ('glance image create failed or unexpected ' - 'image name {}'.format(images_list[0].name)) - amulet.raise_status(amulet.FAIL, msg=message) - - def test_411_nova_keypair_create(self): - """Create a keypair to be used by the heat template, - or get a keypair if it exists.""" - self.keypair = self.create_or_get_keypair(keypair_name=KEYPAIR_NAME) - if not self.keypair: - msg = 'Failed to create or get keypair.' - amulet.raise_status(amulet.FAIL, msg=msg) - u.log.debug("Keypair: {} {}".format(self.keypair.id, - self.keypair.fingerprint)) - - def test_412_heat_stack_create(self): - """Create a heat stack from a basic heat template, verify its status""" - u.log.debug('Creating heat stack...') - - t_url = self.file_url(TEMPLATE_REL_PATH) - r_req = self.heat.http_client.raw_request - u.log.debug('template url: {}'.format(t_url)) - - t_files, template = template_utils.get_template_contents(t_url, r_req) - env_files, env = template_utils.process_environment_and_files( - env_path=None) - - fields = { - 'stack_name': STACK_NAME, - 'timeout_mins': '15', - 'disable_rollback': False, - 'parameters': { - 'admin_pass': 'Ubuntu', - 'key_name': KEYPAIR_NAME, - 'image': IMAGE_NAME - }, - 'template': template, - 'files': dict(list(t_files.items()) + list(env_files.items())), - 'environment': env - } - - # Create the stack. - try: - _stack = self.heat.stacks.create(**fields) - u.log.debug('Stack data: {}'.format(_stack)) - _stack_id = _stack['stack']['id'] - u.log.debug('Creating new stack, ID: {}'.format(_stack_id)) - except Exception as e: - # Generally, an api or cloud config error if this is hit. - msg = 'Failed to create heat stack: {}'.format(e) - amulet.raise_status(amulet.FAIL, msg=msg) - - # Confirm stack reaches COMPLETE status. - # /!\ Heat stacks reach a COMPLETE status even when nova cannot - # find resources (a valid hypervisor) to fit the instance, in - # which case the heat stack self-deletes! Confirm anyway... - ret = u.resource_reaches_status(self.heat.stacks, _stack_id, - expected_stat="COMPLETE", - msg="Stack status wait") - _stacks = list(self.heat.stacks.list()) - u.log.debug('All stacks: {}'.format(_stacks)) - if not ret: - msg = 'Heat stack failed to reach expected state.' - amulet.raise_status(amulet.FAIL, msg=msg) - - # Confirm stack still exists. - try: - _stack = self.heat.stacks.get(STACK_NAME) - except Exception as e: - # Generally, a resource availability issue if this is hit. - msg = 'Failed to get heat stack: {}'.format(e) - amulet.raise_status(amulet.FAIL, msg=msg) - - # Confirm stack name. - u.log.debug('Expected, actual stack name: {}, ' - '{}'.format(STACK_NAME, _stack.stack_name)) - if STACK_NAME != _stack.stack_name: - msg = 'Stack name mismatch, {} != {}'.format(STACK_NAME, - _stack.stack_name) - amulet.raise_status(amulet.FAIL, msg=msg) - - def test_413_heat_stack_resource_compute(self): - """Confirm that the stack has created a subsequent nova - compute resource, and confirm its status.""" - u.log.debug('Confirming heat stack resource status...') - - # Confirm existence of a heat-generated nova compute resource. - _resource = self.heat.resources.get(STACK_NAME, RESOURCE_TYPE) - _server_id = _resource.physical_resource_id - if _server_id: - u.log.debug('Heat template spawned nova instance, ' - 'ID: {}'.format(_server_id)) - else: - msg = 'Stack failed to spawn a nova compute resource (instance).' - amulet.raise_status(amulet.FAIL, msg=msg) - - # Confirm nova instance reaches ACTIVE status. - ret = u.resource_reaches_status(self.nova.servers, _server_id, - expected_stat="ACTIVE", - msg="nova instance") - if not ret: - msg = 'Nova compute instance failed to reach expected state.' - amulet.raise_status(amulet.FAIL, msg=msg) - - def test_490_heat_stack_delete(self): - """Delete a heat stack, verify.""" - u.log.debug('Deleting heat stack...') - u.delete_resource(self.heat.stacks, STACK_NAME, msg="heat stack") - - def test_491_image_delete(self): - """Delete that image.""" - u.log.debug('Deleting glance image...') - image = self.nova.images.find(name=IMAGE_NAME) - u.delete_resource(self.nova.images, image, msg="glance image") - - def test_492_keypair_delete(self): - """Delete that keypair.""" - u.log.debug('Deleting keypair...') - u.delete_resource(self.nova.keypairs, KEYPAIR_NAME, msg="nova keypair") + def test_410_heat_stack_create_delete(self): + """Create a heat stack from template, confirm that a corresponding + nova compute resource is spawned, delete stack.""" + self._image_create() + self._keypair_create() + self._stack_create() + self._stack_resource_compute() + self._stack_delete() + self._image_delete() + self._keypair_delete() def test_900_heat_restart_on_config_change(self): """Verify that the specified services are restarted when the config diff --git a/tests/charmhelpers/contrib/amulet/utils.py b/tests/charmhelpers/contrib/amulet/utils.py index 1d4fe4d..b47d4ea 100644 --- a/tests/charmhelpers/contrib/amulet/utils.py +++ b/tests/charmhelpers/contrib/amulet/utils.py @@ -18,10 +18,12 @@ import ConfigParser import distro_info import io import logging +import os import re import six import sys import time +import urlparse class AmuletUtils(object): @@ -72,7 +74,11 @@ class AmuletUtils(object): return False def get_ubuntu_release_from_sentry(self, sentry_unit): - """Get Ubuntu release codename from sentry unit""" + """Get Ubuntu release codename from sentry unit. + + :param sentry_unit: amulet sentry/service unit pointer + :returns: list of strings - release codename, failure message + """ msg = None cmd = 'lsb_release -cs' release, code = sentry_unit.run(cmd) @@ -88,86 +94,40 @@ class AmuletUtils(object): "({})".format(release, self.ubuntu_releases)) return release, msg - def normalize_service_check_command(self, series, cmd): - """Normalize a service check command with init system logic, - providing backward compatibility for tests which presume - a specific init system is present. - """ - # NOTE(beisner): this work-around is intended to be a temporary - # unblocker of vivid, wily and later tests. See deprecation - # warning on validate_services(). - systemd_switch = self.ubuntu_releases.index('vivid') - - # Preserve sudo usage and strip it out if present - if cmd.startswith('sudo '): - sudo_if_sudo, cmd = cmd[:5], cmd[5:] - else: - sudo_if_sudo = '' - - # Guess the service name - cmd_words = list(set(cmd.split())) - for remove_items in ['status', 'service']: - if remove_items in cmd_words: - cmd_words.remove(remove_items) - service_name = cmd_words[0] - self.log.debug('Service name: {}'.format(service_name)) - - if (cmd.startswith('status') and - self.ubuntu_releases.index(series) >= systemd_switch): - # systemd init expected, but upstart command found - self.log.debug('Correcting for an upstart command ' - 'on a systemd release') - return '{}{} {} {}'.format(sudo_if_sudo, 'service', - service_name, 'status') - elif (cmd.startswith('service') and - self.ubuntu_releases.index(series) < systemd_switch): - # upstart init expected, but systemd command found - self.log.debug('Correcting for a systemd command on ' - 'an upstart release') - return '{}{} {}'.format(sudo_if_sudo, 'status', service_name) - return cmd - def validate_services(self, commands): - """Validate services. - - Verify the specified services are running on the corresponding + """Validate that lists of commands succeed on service units. Can be + used to verify system services are running on the corresponding service units. - """ + + :param command: dict with sentry keys and arbitrary command list values + :returns: None if successful, Failure string message otherwise + """ self.log.debug('Checking status of system services...') # /!\ DEPRECATION WARNING (beisner): - # This method is present to preserve functionality - # of older tests which presume upstart init system, until they are - # rewritten to use validate_services_by_name(). + # New and existing tests should be rewritten to use + # validate_services_by_name() as it is aware of init systems. self.log.warn('/!\\ DEPRECATION WARNING: use ' 'validate_services_by_name instead of validate_services ' 'due to init system differences.') for k, v in six.iteritems(commands): for cmd in v: - - # Ask unit for its Ubuntu release codename - release, ret = self.get_ubuntu_release_from_sentry(k) - if ret: - return ret - - # Conditionally correct for init system assumptions - cmd_normalized = self.normalize_service_check_command(release, - cmd) - self.log.debug('Command, normalized with init logic: ' - '{}'.format(cmd_normalized)) - - output, code = k.run(cmd_normalized) + output, code = k.run(cmd) self.log.debug('{} `{}` returned ' '{}'.format(k.info['unit_name'], - cmd_normalized, code)) + cmd, code)) if code != 0: return "command `{}` returned {}".format(cmd, str(code)) return None def validate_services_by_name(self, sentry_services): """Validate system service status by service name, automatically - detecting init system based on Ubuntu release codename.""" + detecting init system based on Ubuntu release codename. + + :param sentry_resources: dict with sentry keys and svc list values + :returns: None if successful, Failure string message otherwise + """ self.log.debug('Checking status of system services...') # Point at which systemd became a thing @@ -441,3 +401,8 @@ class AmuletUtils(object): _release_list = _d.all self.log.debug('Ubuntu release list: {}'.format(_release_list)) return _release_list + + def file_to_url(self, file_rel_path): + """Convert a relative file path to a file URL.""" + _abs_path = os.path.abspath(file_rel_path) + return urlparse.urlparse(_abs_path, scheme='file').geturl() diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py index 461a702..98437ba 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -110,7 +110,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): (self.precise_essex, self.precise_folsom, self.precise_grizzly, self.precise_havana, self.precise_icehouse, self.trusty_icehouse, self.trusty_juno, self.utopic_juno, - self.trusty_kilo, self.vivid_kilo) = range(10) + self.trusty_kilo, self.vivid_kilo, self.trusty_liberty, + self.wily_liberty) = range(12) releases = { ('precise', None): self.precise_essex, @@ -121,8 +122,11 @@ class OpenStackAmuletDeployment(AmuletDeployment): ('trusty', None): self.trusty_icehouse, ('trusty', 'cloud:trusty-juno'): self.trusty_juno, ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo, + ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty, ('utopic', None): self.utopic_juno, - ('vivid', None): self.vivid_kilo} + ('vivid', None): self.vivid_kilo, + ('wily', None): self.wily_liberty} + return releases[(self.series, self.openstack)] def _get_openstack_release_string(self): @@ -138,6 +142,7 @@ class OpenStackAmuletDeployment(AmuletDeployment): ('trusty', 'icehouse'), ('utopic', 'juno'), ('vivid', 'kilo'), + ('wily', 'liberty'), ]) if self.openstack: os_origin = self.openstack.split(':')[1] diff --git a/tests/charmhelpers/contrib/openstack/amulet/utils.py b/tests/charmhelpers/contrib/openstack/amulet/utils.py index 97beeda..576bf0b 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/utils.py +++ b/tests/charmhelpers/contrib/openstack/amulet/utils.py @@ -325,6 +325,20 @@ class OpenStackAmuletUtils(AmuletUtils): return True + def create_or_get_keypair(self, nova, keypair_name="testkey"): + """Create a new keypair, or return pointer if it already exists.""" + try: + _keypair = nova.keypairs.get(keypair_name) + self.log.debug('Keypair ({}) already exists, ' + 'using it.'.format(keypair_name)) + return _keypair + except: + self.log.debug('Keypair ({}) does not exist, ' + 'creating it.'.format(keypair_name)) + + _keypair = nova.keypairs.create(name=keypair_name) + return _keypair + def delete_resource(self, resource, resource_id, msg="resource", max_wait=120): """Delete one openstack resource, such as one instance, keypair, diff --git a/tests/files/hot_hello_world.yaml b/tests/files/hot_hello_world.yaml index d23ef26..f799a2a 100644 --- a/tests/files/hot_hello_world.yaml +++ b/tests/files/hot_hello_world.yaml @@ -18,7 +18,6 @@ parameters: type: string description: Flavor for the server to be created default: m1.tiny -# default: m1.small constraints: - custom_constraint: nova.flavor image: