From 56c8d60dcdb405a7c60b4b6844dc329baeac6298 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Wed, 30 May 2018 12:23:02 +0100 Subject: [PATCH 1/4] Add tests for testing the glance charm * Add OpenStackAPITest class which can be used by OpenStack API charms. It provides the framework for common tests like pause and resume. It also provides lower level entites like an authenticated keystone session. * Add generic openstack resource managment functions to zaza.utilities.openstack. These are based on existing functions in charmhelpers. Main difference is that they use tenacity to manage retry logic and throw AssertionError if then required state is not reached rather than returning True/False * Add image management functions to zaza.utilities.openstack. * Add set of glance setup/configuration/tests. These are equivalent to the existing glance amulet tests with all the introspection tests removed (see below for more detail). Tests replicated here: test_410_glance_image_create_delete test_411_set_disk_format test_900_glance_restart_on_config_change test_901_pause_resume Tests removed test_100_services test_102_service_catalog test_104_glance_endpoint test_106_keystone_endpoint test_110_users test_115_memcache test_200_mysql_glance_db_relation test_201_glance_mysql_db_relation test_202_keystone_glance_id_relation test_203_glance_keystone_id_relation test_204_rabbitmq_glance_amqp_relation test_205_glance_rabbitmq_amqp_relation test_300_glance_api_default_config test_302_glance_registry_default_config --- setup.py | 4 + .../test_zaza_utilities_openstack.py | 156 ++++++++++++++ zaza/charm_tests/glance/__init__.py | 0 zaza/charm_tests/glance/setup.py | 7 + zaza/charm_tests/glance/tests.py | 52 +++++ zaza/charm_tests/test_utils.py | 142 ++++++++++++- zaza/utilities/openstack.py | 198 +++++++++++++++++- 7 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 zaza/charm_tests/glance/__init__.py create mode 100644 zaza/charm_tests/glance/setup.py create mode 100644 zaza/charm_tests/glance/tests.py diff --git a/setup.py b/setup.py index e1689f3..d1c6970 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,10 @@ install_require = [ 'juju-wait', 'PyYAML', 'tenacity', + 'oslo.config', + 'python-keystoneclient', + 'python-novaclient', + 'python-neutronclient', ] tests_require = [ diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 016fd17..75d657a 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -1,5 +1,7 @@ import copy import mock +import tenacity + import unit_tests.utils as ut_utils from zaza.utilities import openstack as openstack_utils @@ -171,3 +173,157 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): openstack_utils.get_undercloud_keystone_session() self.get_keystone_session.assert_called_once_with(_auth, scope=_scope) + + def test_get_urllib_opener(self): + self.patch_object(openstack_utils.urllib.request, "ProxyHandler") + self.patch_object(openstack_utils.urllib.request, "HTTPHandler") + self.patch_object(openstack_utils.urllib.request, "build_opener") + self.patch_object(openstack_utils.os, "getenv") + self.getenv.return_value = None + HTTPHandler_mock = mock.MagicMock() + self.HTTPHandler.return_value = HTTPHandler_mock + openstack_utils.get_urllib_opener() + self.build_opener.assert_called_once_with(HTTPHandler_mock) + self.HTTPHandler.assert_called_once_with() + + def test_get_urllib_opener_proxy(self): + self.patch_object(openstack_utils.urllib.request, "ProxyHandler") + self.patch_object(openstack_utils.urllib.request, "HTTPHandler") + self.patch_object(openstack_utils.urllib.request, "build_opener") + self.patch_object(openstack_utils.os, "getenv") + self.getenv.return_value = 'http://squidy' + ProxyHandler_mock = mock.MagicMock() + self.ProxyHandler.return_value = ProxyHandler_mock + openstack_utils.get_urllib_opener() + self.build_opener.assert_called_once_with(ProxyHandler_mock) + self.ProxyHandler.assert_called_once_with({'http': 'http://squidy'}) + + def test_find_cirros_image(self): + urllib_opener_mock = mock.MagicMock() + self.patch_object(openstack_utils, "get_urllib_opener") + self.get_urllib_opener.return_value = urllib_opener_mock + urllib_opener_mock.open().read.return_value = b'12' + self.assertEqual( + openstack_utils.find_cirros_image('aarch64'), + 'http://download.cirros-cloud.net/12/cirros-12-aarch64-disk.img') + + def test_download_image(self): + urllib_opener_mock = mock.MagicMock() + self.patch_object(openstack_utils, "get_urllib_opener") + self.get_urllib_opener.return_value = urllib_opener_mock + self.patch_object(openstack_utils.urllib.request, "install_opener") + self.patch_object(openstack_utils.urllib.request, "urlretrieve") + openstack_utils.download_image('http://cirros/c.img', '/tmp/c1.img') + self.install_opener.assert_called_once_with(urllib_opener_mock) + self.urlretrieve.assert_called_once_with( + 'http://cirros/c.img', '/tmp/c1.img') + + def test_resource_reaches_status(self): + resource_mock = mock.MagicMock() + resource_mock.get.return_value = mock.MagicMock(status='available') + openstack_utils.resource_reaches_status(resource_mock, 'e01df65a') + + def test_resource_reaches_status_fail(self): + openstack_utils.resource_reaches_status.retry.wait = \ + tenacity.wait_none() + resource_mock = mock.MagicMock() + resource_mock.get.return_value = mock.MagicMock(status='unavailable') + with self.assertRaises(AssertionError): + openstack_utils.resource_reaches_status( + resource_mock, + 'e01df65a') + + def test_resource_reaches_status_bespoke(self): + resource_mock = mock.MagicMock() + resource_mock.get.return_value = mock.MagicMock(status='readyish') + openstack_utils.resource_reaches_status( + resource_mock, + 'e01df65a', + 'readyish') + + def test_resource_reaches_status_bespoke_fail(self): + openstack_utils.resource_reaches_status.retry.wait = \ + tenacity.wait_none() + resource_mock = mock.MagicMock() + resource_mock.get.return_value = mock.MagicMock(status='available') + with self.assertRaises(AssertionError): + openstack_utils.resource_reaches_status( + resource_mock, + 'e01df65a', + 'readyish') + + def test_resource_removed(self): + resource_mock = mock.MagicMock() + resource_mock.list.return_value = [mock.MagicMock(id='ba8204b0')] + openstack_utils.resource_removed(resource_mock, 'e01df65a') + + def test_resource_removed_fail(self): + openstack_utils.resource_reaches_status.retry.wait = \ + tenacity.wait_none() + resource_mock = mock.MagicMock() + resource_mock.list.return_value = [mock.MagicMock(id='e01df65a')] + with self.assertRaises(AssertionError): + openstack_utils.resource_removed(resource_mock, 'e01df65a') + + def test_delete_resource(self): + resource_mock = mock.MagicMock() + self.patch_object(openstack_utils, "resource_removed") + openstack_utils.delete_resource(resource_mock, 'e01df65a') + resource_mock.delete.assert_called_once_with('e01df65a') + self.resource_removed.assert_called_once_with( + resource_mock, + 'e01df65a', + 'resource') + + def test_delete_image(self): + self.patch_object(openstack_utils, "delete_resource") + glance_mock = mock.MagicMock() + openstack_utils.delete_image(glance_mock, 'b46c2d83') + self.delete_resource.assert_called_once_with( + glance_mock.images, + 'b46c2d83', + msg="glance image") + + def test_upload_image_to_glance(self): + self.patch_object(openstack_utils, "resource_reaches_status") + glance_mock = mock.MagicMock() + image_mock = mock.MagicMock(id='9d1125af') + glance_mock.images.create.return_value = image_mock + m = mock.mock_open() + with mock.patch('zaza.utilities.openstack.open', m, create=False) as f: + openstack_utils.upload_image_to_glance( + glance_mock, + '/tmp/im1.img', + 'bob') + glance_mock.images.create.assert_called_once_with( + name='bob', + disk_format='qcow2', + visibility='public', + container_format='bare') + glance_mock.images.upload.assert_called_once_with( + '9d1125af', + f(), + ) + self.resource_reaches_status.assert_called_once_with( + glance_mock.images, + '9d1125af', + expected_stat='active', + msg='Image status wait') + + def test_create_image(self): + glance_mock = mock.MagicMock() + self.patch_object(openstack_utils.os.path, "exists") + self.patch_object(openstack_utils, "download_image") + self.patch_object(openstack_utils, "upload_image_to_glance") + openstack_utils.create_image( + glance_mock, + 'http://cirros/c.img', + 'bob') + self.exists.return_value = False + self.download_image.assert_called_once_with( + 'http://cirros/c.img', + 'tests/c.img') + self.upload_image_to_glance.assert_called_once_with( + glance_mock, + 'tests/c.img', + 'bob') diff --git a/zaza/charm_tests/glance/__init__.py b/zaza/charm_tests/glance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zaza/charm_tests/glance/setup.py b/zaza/charm_tests/glance/setup.py new file mode 100644 index 0000000..a7f0819 --- /dev/null +++ b/zaza/charm_tests/glance/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + + +def basic_setup(): + """Glance setup for testing glance is currently part of glance functional + tests. Image setup for other tests to use should go here""" + pass diff --git a/zaza/charm_tests/glance/tests.py b/zaza/charm_tests/glance/tests.py new file mode 100644 index 0000000..9bd9709 --- /dev/null +++ b/zaza/charm_tests/glance/tests.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import logging + +import zaza.utilities.openstack as openstack_utils +import zaza.charm_tests.test_utils as test_utils + + +class GlanceTest(test_utils.OpenStackAPITest): + + @classmethod + def setUpClass(cls): + super(GlanceTest, cls).setUpClass() + cls.glance_client = openstack_utils.get_glance_session_client( + cls.keystone_session) + + def test_410_glance_image_create_delete(self): + """Create an image and then delete it""" + image_url = openstack_utils.find_cirros_image(arch='x86_64') + image = openstack_utils.create_image( + self.glance_client, + image_url, + 'cirrosimage') + openstack_utils.delete_image(self.glance_client, image.id) + + def test_411_set_disk_format(self): + """Change disk format and assert then change propagates to the correct + file and that services are restarted as a result""" + # Expected default and alternate values + set_default = { + 'disk-formats': 'ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar'} + set_alternate = {'disk-formats': 'qcow2'} + + # Config file affected by juju set config change + conf_file = '/etc/glance/glance-api.conf' + + # Make config change, check for service restarts + logging.debug('Setting disk format glance...') + self.restart_on_changed( + conf_file, + set_default, + set_alternate, + {'image_format': { + 'disk_formats': [ + 'ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar']}}, + {'image_format': {'disk_formats': ['qcow2']}}, + ['glance-api']) + + def test_901_pause_resume(self): + """Pause service and check services are stopped then resume and check + they are started""" + self.pause_resume(['glance-api']) diff --git a/zaza/charm_tests/test_utils.py b/zaza/charm_tests/test_utils.py index 81bbf6d..3060c26 100644 --- a/zaza/charm_tests/test_utils.py +++ b/zaza/charm_tests/test_utils.py @@ -1,13 +1,18 @@ import logging +import unittest import zaza.model -import zaza.charm_lifecycle.utils as utils +import zaza.model as model +import zaza.charm_lifecycle.utils as lifecycle_utils +import zaza.utilities.openstack as openstack_utils def skipIfNotHA(service_name): def _skipIfNotHA_inner_1(f): def _skipIfNotHA_inner_2(*args, **kwargs): - ips = zaza.model.get_app_ips(utils.get_juju_model(), service_name) + ips = zaza.model.get_app_ips( + lifecycle_utils.get_juju_model(), + service_name) if len(ips) > 1: return f(*args, **kwargs) else: @@ -16,3 +21,136 @@ def skipIfNotHA(service_name): return _skipIfNotHA_inner_2 return _skipIfNotHA_inner_1 + + +class OpenStackAPITest(unittest.TestCase): + """Generic helpers for testing OpenStack API charms""" + + @classmethod + def setUpClass(cls): + cls.keystone_session = openstack_utils.get_overcloud_keystone_session() + cls.model_name = lifecycle_utils.get_juju_model() + cls.test_config = lifecycle_utils.get_charm_config() + cls.application_name = cls.test_config['charm_name'] + + def restart_on_changed(self, config_file, default_config, alternate_config, + default_entry, alternate_entry, services): + """Test that changing config results in config file being updates and + services restarted. Return config to default_config afterwards + + :param config_file: Config file to check for settings + :type config_file: str + :param default_config: Dict of charm settings to set on completion + :type default_config: dict + :param alternate_config: Dict of charm settings to change to + :type alternate_config: dict + :param default_entry: Config file entries that correspond to + default_config + :type default_entry: dict + :param alternate_entry: Config file entries that correspond to + :type alternate_entry: alternate_config + :param services: Services expected to be restarted when config_file is + changed. + :type services: list + """ + # first_unit is only useed to grab a timestamp, the assumption being + # that all the units times are in sync. + first_unit = model.get_first_unit_name( + self.model_name, + self.application_name) + logging.debug('First unit is {}'.format(first_unit)) + + mtime = model.get_unit_time(self.model_name, first_unit) + logging.debug('Remote unit timestamp {}'.format(mtime)) + + logging.debug('Changing charm setting to {}'.format(alternate_config)) + model.set_application_config( + self.model_name, + self.application_name, + alternate_config) + + logging.debug( + 'Waiting for updates to propagate to {}'.format(config_file)) + model.block_until_oslo_config_entries_match( + self.model_name, + self.application_name, + config_file, + alternate_entry) + + logging.debug( + 'Waiting for units to reach target states'.format(config_file)) + model.wait_for_application_states( + self.model_name, + self.test_config.get('target_deploy_status', {})) + + # Config update has occured and hooks are idle. Any services should + # have been restarted by now: + logging.debug( + 'Waiting for services ({}) to be restarted'.format(services)) + model.block_until_services_restarted( + self.model_name, + self.application_name, + mtime, + services) + + logging.debug('Restoring charm setting to {}'.format(default_config)) + model.set_application_config( + self.model_name, + self.application_name, + default_config) + + logging.debug( + 'Waiting for updates to propagate to '.format(config_file)) + model.block_until_oslo_config_entries_match( + self.model_name, + self.application_name, + config_file, + default_entry) + + logging.debug( + 'Waiting for units to reach target states'.format(config_file)) + model.wait_for_application_states( + self.model_name, + self.test_config.get('target_deploy_status', {})) + + def pause_resume(self, services): + """Pause and then resume a unit checking that services are in the + required state after each action + + :param services: Services expected to be restarted when config_file is + changed. + :type services: list + """ + first_unit = model.get_first_unit_name( + self.model_name, + self.application_name) + model.block_until_service_status( + self.model_name, + first_unit, + services, + 'running') + model.block_until_unit_wl_status( + self.model_name, + first_unit, + 'active') + model.run_action(self.model_name, first_unit, 'pause', {}) + model.block_until_unit_wl_status( + self.model_name, + first_unit, + 'maintenance') + model.block_until_all_units_idle(self.model_name) + model.block_until_service_status( + self.model_name, + first_unit, + services, + 'stopped') + model.run_action(self.model_name, first_unit, 'resume', {}) + model.block_until_unit_wl_status( + self.model_name, + first_unit, + 'active') + model.block_until_all_units_idle(self.model_name) + model.block_until_service_status( + self.model_name, + first_unit, services, + 'running') diff --git a/zaza/utilities/openstack.py b/zaza/utilities/openstack.py index 696b74b..090bf61 100644 --- a/zaza/utilities/openstack.py +++ b/zaza/utilities/openstack.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python - from .os_versions import ( OPENSTACK_CODENAMES, SWIFT_CODENAMES, PACKAGE_CODENAMES, ) +from glanceclient import Client as GlanceClient + from keystoneclient.v3 import client as keystoneclient_v3 from keystoneauth1 import session from keystoneauth1.identity import ( @@ -16,12 +16,14 @@ from novaclient import client as novaclient_client from neutronclient.v2_0 import client as neutronclient from neutronclient.common import exceptions as neutronexceptions +import juju_wait import logging import os import re import six import sys -import juju_wait +import tenacity +import urllib from zaza import model from zaza.charm_lifecycle import utils as lifecycle_utils @@ -31,6 +33,9 @@ from zaza.utilities import ( juju as juju_utils, ) +CIRROS_RELEASE_URL = 'http://download.cirros-cloud.net/version/released' +CIRROS_IMAGE_URL = 'http://download.cirros-cloud.net' + CHARM_TYPES = { 'neutron': { 'pkg': 'neutron-common', @@ -116,6 +121,17 @@ def get_ks_creds(cloud_creds, scope='PROJECT'): return auth +def get_glance_session_client(session): + """Return glanceclient authenticated by keystone session + + :param session: Keystone session object + :type session: keystoneauth1.session.Session object + :returns: Authenticated glanceclient + :rtype: glanceclient.Client + """ + return GlanceClient('2', session=session) + + def get_nova_session_client(session): """Return novaclient authenticated by keystone session @@ -1269,3 +1285,179 @@ def get_overcloud_auth(): 'API_VERSION': 3, } return auth_settings + + +def get_urllib_opener(): + """Create a urllib opener taking into account proxy settings + + Using urllib.request.urlopen will automatically handle proxies so none + of this function is needed except we are currently specifying proxies + via AMULET_HTTP_PROXY rather than http_proxy so a ProxyHandler is needed + explicitly stating the proxies. + + :returns: An opener which opens URLs via BaseHandlers chained together + :rtype: urllib.request.OpenerDirector + """ + http_proxy = os.getenv('AMULET_HTTP_PROXY') + logging.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy)) + + if http_proxy: + handler = urllib.request.ProxyHandler({'http': http_proxy}) + else: + handler = urllib.request.HTTPHandler() + return urllib.request.build_opener(handler) + + +def find_cirros_image(arch): + """Return the url for the latest cirros image for the given architecture + + :param arch: aarch64, arm, i386, x86_64 etc + :type arch: str + :returns: Unit matching given name + :rtype: juju.unit.Unit or None + """ + opener = get_urllib_opener() + f = opener.open(CIRROS_RELEASE_URL) + version = f.read().strip().decode() + cirros_img = 'cirros-{}-{}-disk.img'.format(version, arch) + return '{}/{}/{}'.format(CIRROS_IMAGE_URL, version, cirros_img) + + +def download_image(image_url, target_file): + """Download the image from the given url to the specified file + + :param image_url: URL to download image from + :type image_url: str + :param target_file: Local file to savee image to + :type target_file: str + """ + opener = get_urllib_opener() + urllib.request.install_opener(opener) + urllib.request.urlretrieve(image_url, target_file) + + +@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), + reraise=True, stop=tenacity.stop_after_attempt(8)) +def resource_reaches_status(resource, resource_id, + expected_stat='available', + msg='resource'): + """Wait for an openstack resources status to reach an expected status + within a specified time. Useful to confirm that nova instances, cinder + vols, snapshots, glance images, heat stacks and other resources + eventually reach the expected status. + + :param resource: pointer to os resource type, ex: heat_client.stacks + :type resource: str + :param resource_id: unique id for the openstack resource + :type resource_id: str + :param expected_stat: status to expect resource to reach + :type expected_stat: str + :param msg: text to identify purpose in logging + :type msy: str + :raises: AssertionError + """ + resource_stat = resource.get(resource_id).status + assert resource_stat == expected_stat, ( + "Resource in {} state, waiting for {}" .format(resource_stat, + expected_stat,)) + + +@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), + reraise=True, stop=tenacity.stop_after_attempt(2)) +def resource_removed(resource, resource_id, msg="resource"): + """Wait for an openstack resource to no longer be present + + :param resource: pointer to os resource type, ex: heat_client.stacks + :type resource: str + :param resource_id: unique id for the openstack resource + :type resource_id: str + :param msg: text to identify purpose in logging + :type msy: str + :raises: AssertionError + """ + matching = [r for r in resource.list() if r.id == resource_id] + logging.debug("Resource {} still present".format(resource_id)) + assert len(matching) == 0, "Resource {} still present".format(resource_id) + + +def delete_resource(resource, resource_id, msg="resource"): + """Delete an openstack resource, such as one instance, keypair, + image, volume, stack, etc., and confirm deletion within max wait time. + + :param resource: pointer to os resource type, ex:glance_client.images + :type resource: str + :param resource_id: unique name or id for the openstack resource + :type resource_id: str + :param msg: text to identify purpose in logging + :type msg: str + """ + logging.debug('Deleting OpenStack resource ' + '{} ({})'.format(resource_id, msg)) + resource.delete(resource_id) + resource_removed(resource, resource_id, msg) + + +def delete_image(glance, img_id): + """Delete the given image from glance + + :param glance: Authenticated glanceclient + :type glance: glanceclient.Client + :param img_id: unique name or id for the openstack resource + :type img_id: str + """ + delete_resource(glance.images, img_id, msg="glance image") + + +def upload_image_to_glance(glance, local_path, image_name): + """Upload the given image to glance and apply the given label + + :param glance: Authenticated glanceclient + :type glance: glanceclient.Client + :param local_path: Path to local image + :type local_path: str + :param image_name: The label to give the image in glance + :type image_name: str + """ + # Create glance image + image = glance.images.create( + name=image_name, + disk_format='qcow2', + visibility='public', + container_format='bare') + glance.images.upload(image.id, open(local_path, 'rb')) + + resource_reaches_status( + glance.images, + image.id, + expected_stat='active', + msg='Image status wait') + + return image + + +def create_image(glance, image_url, image_name, image_cache_dir='tests'): + """Download the latest cirros image and upload it to glance, + validate and return a resource pointer. + + :param glance: pointer to authenticated glance connection + :type glance: glanceclient.Client + :param image_url: URL to download image from + :type image_url: str + :param image_name: display name for new image + :type image_name: str + :param image_cache_dir: Directory to store image in before uploading + :type image_cache_dir: str + :returns: glance image pointer + :rtype: juju.unit.Unit or None + """ + logging.debug('Creating glance cirros image ' + '({})...'.format(image_name)) + + img_name = os.path.basename(urllib.parse.urlparse(image_url).path) + local_path = os.path.join(image_cache_dir, img_name) + + if not os.path.exists(local_path): + download_image(image_url, local_path) + + image = upload_image_to_glance(glance, local_path, image_name) + return image From 12317054d202274e092cca6e9676664605f08a3d Mon Sep 17 00:00:00 2001 From: Liam Young Date: Thu, 31 May 2018 09:06:03 +0100 Subject: [PATCH 2/4] Lint fixes --- zaza/charm_tests/glance/tests.py | 2 +- zaza/charm_tests/test_utils.py | 35 +++++++++++++-------------- zaza/utilities/openstack.py | 41 ++++++++++++++++++++------------ 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/zaza/charm_tests/glance/tests.py b/zaza/charm_tests/glance/tests.py index 9bd9709..5861e12 100644 --- a/zaza/charm_tests/glance/tests.py +++ b/zaza/charm_tests/glance/tests.py @@ -6,7 +6,7 @@ import zaza.utilities.openstack as openstack_utils import zaza.charm_tests.test_utils as test_utils -class GlanceTest(test_utils.OpenStackAPITest): +class GlanceTest(test_utils.OpenStackBaseTest): @classmethod def setUpClass(cls): diff --git a/zaza/charm_tests/test_utils.py b/zaza/charm_tests/test_utils.py index 3060c26..b587a83 100644 --- a/zaza/charm_tests/test_utils.py +++ b/zaza/charm_tests/test_utils.py @@ -23,7 +23,7 @@ def skipIfNotHA(service_name): return _skipIfNotHA_inner_1 -class OpenStackAPITest(unittest.TestCase): +class OpenStackBaseTest(unittest.TestCase): """Generic helpers for testing OpenStack API charms""" @classmethod @@ -32,6 +32,10 @@ class OpenStackAPITest(unittest.TestCase): cls.model_name = lifecycle_utils.get_juju_model() cls.test_config = lifecycle_utils.get_charm_config() cls.application_name = cls.test_config['charm_name'] + cls.first_unit = model.get_first_unit_name( + cls.model_name, + cls.application_name) + logging.debug('First unit is {}'.format(cls.first_unit)) def restart_on_changed(self, config_file, default_config, alternate_config, default_entry, alternate_entry, services): @@ -48,19 +52,16 @@ class OpenStackAPITest(unittest.TestCase): default_config :type default_entry: dict :param alternate_entry: Config file entries that correspond to - :type alternate_entry: alternate_config + alternate_config + :type alternate_entry: dict :param services: Services expected to be restarted when config_file is changed. :type services: list """ # first_unit is only useed to grab a timestamp, the assumption being # that all the units times are in sync. - first_unit = model.get_first_unit_name( - self.model_name, - self.application_name) - logging.debug('First unit is {}'.format(first_unit)) - mtime = model.get_unit_time(self.model_name, first_unit) + mtime = model.get_unit_time(self.model_name, self.first_unit) logging.debug('Remote unit timestamp {}'.format(mtime)) logging.debug('Changing charm setting to {}'.format(alternate_config)) @@ -121,36 +122,34 @@ class OpenStackAPITest(unittest.TestCase): changed. :type services: list """ - first_unit = model.get_first_unit_name( - self.model_name, - self.application_name) model.block_until_service_status( self.model_name, - first_unit, + self.first_unit, services, 'running') model.block_until_unit_wl_status( self.model_name, - first_unit, + self.first_unit, 'active') - model.run_action(self.model_name, first_unit, 'pause', {}) + model.run_action(self.model_name, self.first_unit, 'pause', {}) model.block_until_unit_wl_status( self.model_name, - first_unit, + self.first_unit, 'maintenance') model.block_until_all_units_idle(self.model_name) model.block_until_service_status( self.model_name, - first_unit, + self.first_unit, services, 'stopped') - model.run_action(self.model_name, first_unit, 'resume', {}) + model.run_action(self.model_name, self.first_unit, 'resume', {}) model.block_until_unit_wl_status( self.model_name, - first_unit, + self.first_unit, 'active') model.block_until_all_units_idle(self.model_name) model.block_until_service_status( self.model_name, - first_unit, services, + self.first_unit, + services, 'running') diff --git a/zaza/utilities/openstack.py b/zaza/utilities/openstack.py index 090bf61..73f0ee6 100644 --- a/zaza/utilities/openstack.py +++ b/zaza/utilities/openstack.py @@ -1313,8 +1313,8 @@ def find_cirros_image(arch): :param arch: aarch64, arm, i386, x86_64 etc :type arch: str - :returns: Unit matching given name - :rtype: juju.unit.Unit or None + :returns: URL for latest cirros image + :rtype: str """ opener = get_urllib_opener() f = opener.open(CIRROS_RELEASE_URL) @@ -1339,7 +1339,7 @@ def download_image(image_url, target_file): @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), reraise=True, stop=tenacity.stop_after_attempt(8)) def resource_reaches_status(resource, resource_id, - expected_stat='available', + expected_status='available', msg='resource'): """Wait for an openstack resources status to reach an expected status within a specified time. Useful to confirm that nova instances, cinder @@ -1350,16 +1350,16 @@ def resource_reaches_status(resource, resource_id, :type resource: str :param resource_id: unique id for the openstack resource :type resource_id: str - :param expected_stat: status to expect resource to reach - :type expected_stat: str + :param expected_status: status to expect resource to reach + :type expected_status: str :param msg: text to identify purpose in logging :type msy: str :raises: AssertionError """ - resource_stat = resource.get(resource_id).status - assert resource_stat == expected_stat, ( - "Resource in {} state, waiting for {}" .format(resource_stat, - expected_stat,)) + resource_status = resource.get(resource_id).status + assert resource_status == expected_status, ( + "Resource in {} state, waiting for {}" .format(resource_status, + expected_status,)) @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), @@ -1408,7 +1408,8 @@ def delete_image(glance, img_id): delete_resource(glance.images, img_id, msg="glance image") -def upload_image_to_glance(glance, local_path, image_name): +def upload_image_to_glance(glance, local_path, image_name, disk_format='qcow2', + visibility='public', container_format='bare'): """Upload the given image to glance and apply the given label :param glance: Authenticated glanceclient @@ -1417,13 +1418,23 @@ def upload_image_to_glance(glance, local_path, image_name): :type local_path: str :param image_name: The label to give the image in glance :type image_name: str + :param disk_format: The of the underlying disk image. + :type disk_format: str + :param visibility: Who can access image + :type visibility: str (public, private, shared or community) + :param container_format: Whether the virtual machine image is in a file + format that also contains metadata about the + actual virtual machine. + :type container_format: str + :returns: glance image pointer + :rtype: glanceclient.common.utils.RequestIdProxy """ # Create glance image image = glance.images.create( name=image_name, - disk_format='qcow2', - visibility='public', - container_format='bare') + disk_format=disk_format, + visibility=visibility, + container_format=container_format) glance.images.upload(image.id, open(local_path, 'rb')) resource_reaches_status( @@ -1439,7 +1450,7 @@ def create_image(glance, image_url, image_name, image_cache_dir='tests'): """Download the latest cirros image and upload it to glance, validate and return a resource pointer. - :param glance: pointer to authenticated glance connection + :param glance: Authenticated glanceclient :type glance: glanceclient.Client :param image_url: URL to download image from :type image_url: str @@ -1448,7 +1459,7 @@ def create_image(glance, image_url, image_name, image_cache_dir='tests'): :param image_cache_dir: Directory to store image in before uploading :type image_cache_dir: str :returns: glance image pointer - :rtype: juju.unit.Unit or None + :rtype: glanceclient.common.utils.RequestIdProxy """ logging.debug('Creating glance cirros image ' '({})...'.format(image_name)) From 76c539bae8732e04253175546f4e89af671dab30 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Thu, 31 May 2018 09:23:40 +0100 Subject: [PATCH 3/4] Fix typo --- zaza/utilities/openstack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zaza/utilities/openstack.py b/zaza/utilities/openstack.py index 73f0ee6..25ab78e 100644 --- a/zaza/utilities/openstack.py +++ b/zaza/utilities/openstack.py @@ -1440,7 +1440,7 @@ def upload_image_to_glance(glance, local_path, image_name, disk_format='qcow2', resource_reaches_status( glance.images, image.id, - expected_stat='active', + expected_status='active', msg='Image status wait') return image From 6c12a8104d7d7eee0ceed28919e50e528b424c7b Mon Sep 17 00:00:00 2001 From: Liam Young Date: Thu, 31 May 2018 10:30:28 +0100 Subject: [PATCH 4/4] Fix unit test --- unit_tests/utilities/test_zaza_utilities_openstack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 75d657a..b183e1b 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -307,7 +307,7 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.resource_reaches_status.assert_called_once_with( glance_mock.images, '9d1125af', - expected_stat='active', + expected_status='active', msg='Image status wait') def test_create_image(self):