diff --git a/Makefile b/Makefile index 01db0a79..9a332cba 100644 --- a/Makefile +++ b/Makefile @@ -6,3 +6,6 @@ lint: sync: @charm-helper-sync -c charm-helpers-sync.yaml + +test: + @nosetests -svd tests/ diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/glance_relations.py b/hooks/glance_relations.py index fb2fbb40..b9ab9b0d 100755 --- a/hooks/glance_relations.py +++ b/hooks/glance_relations.py @@ -13,7 +13,6 @@ from glance_utils import ( PACKAGES, SERVICES, CHARM, - SERVICE_NAME, GLANCE_REGISTRY_CONF, GLANCE_REGISTRY_PASTE_INI, GLANCE_API_CONF, @@ -22,12 +21,13 @@ from glance_utils import ( CEPH_CONF, ) from charmhelpers.core.hookenv import ( - config as charm_conf, + config, Hooks, log as juju_log, relation_get, relation_set, relation_ids, + service_name, unit_get, UnregisteredHookError, ) @@ -58,13 +58,12 @@ hooks = Hooks() CONFIGS = register_configs() -config = charm_conf() @hooks.hook('install') def install_hook(): juju_log('Installing glance packages') - src = config['openstack-origin'] + src = config('openstack-origin') if (lsb_release()['DISTRIB_CODENAME'] == 'precise' and src == 'distro'): src = 'cloud:precise-folsom' @@ -82,7 +81,7 @@ def install_hook(): @hooks.hook('shared-db-relation-joined') def db_joined(): - relation_set(database=config['database'], username=config['database-user'], + relation_set(database=config('database'), username=config('database-user'), hostname=unit_get('private-address')) @@ -123,7 +122,7 @@ def image_service_joined(relation_id=None): host = unit_get('private-address') if is_clustered(): - host = config["vip"] + host = config("vip") relation_data = { 'glance-api-server': "%s://%s:9292" % (scheme, host), } @@ -164,7 +163,7 @@ def ceph_changed(): juju_log('ceph relation incomplete. Peer not ready?') return - if not ensure_ceph_keyring(service=SERVICE_NAME): + if not ensure_ceph_keyring(service=service_name()): juju_log('Could not create ceph keyring: peer not ready?') return @@ -172,7 +171,7 @@ def ceph_changed(): CONFIGS.write(CEPH_CONF) if eligible_leader(CLUSTER_RES): - ensure_ceph_pool(service=SERVICE_NAME) + ensure_ceph_pool(service=service_name()) @hooks.hook('identity-service-relation-joined') @@ -187,13 +186,13 @@ def keystone_joined(relation_id=None): host = unit_get('private-address') if is_clustered(): - host = config["vip"] + host = config("vip") url = "%s://%s:9292" % (scheme, host) relation_data = { 'service': 'glance', - 'region': config['region'], + 'region': config('region'), 'public_url': url, 'admin_url': url, 'internal_url': url, } @@ -228,7 +227,7 @@ def keystone_changed(): def config_changed(): # Determine whether or not we should do an upgrade, based on whether or not # the version offered in openstack-origin is greater than what is installed - install_src = config["openstack-origin"] + install_src = config("openstack-origin") available = get_os_codename_install_source(install_src) installed = get_os_codename_package("glance-common") @@ -241,7 +240,7 @@ def config_changed(): configure_https() - env_vars = {'OPENSTACK_PORT_MCASTPORT': config["ha-mcastport"], + env_vars = {'OPENSTACK_PORT_MCASTPORT': config("ha-mcastport"), 'OPENSTACK_SERVICE_API': "glance-api", 'OPENSTACK_SERVICE_REGISTRY': "glance-registry"} save_script_rc(**env_vars) @@ -261,11 +260,11 @@ def upgrade_charm(): @hooks.hook('ha-relation-joined') def ha_relation_joined(): - corosync_bindiface = config["ha-bindiface"] - corosync_mcastport = config["ha-mcastport"] - vip = config["vip"] - vip_iface = config["vip_iface"] - vip_cidr = config["vip_cidr"] + corosync_bindiface = config("ha-bindiface") + corosync_mcastport = config("ha-mcastport") + vip = config("vip") + vip_iface = config("vip_iface") + vip_cidr = config("vip_cidr") #if vip and vip_iface and vip_cidr and \ # corosync_bindiface and corosync_mcastport: @@ -299,7 +298,7 @@ def ha_relation_changed(): juju_log('glance subordinate is not fully clustered.') return if eligible_leader(CLUSTER_RES): - host = config["vip"] + host = config("vip") scheme = "http" if 'https' in CONFIGS.complete_contexts(): scheme = "https" @@ -309,7 +308,7 @@ def ha_relation_changed(): for r_id in relation_ids('identity-service'): relation_set(relation_id=r_id, service="glance", - region=config["region"], + region=config("region"), public_url=url, admin_url=url, internal_url=url) diff --git a/hooks/glance_utils.py b/hooks/glance_utils.py index c6a10104..440ccd10 100755 --- a/hooks/glance_utils.py +++ b/hooks/glance_utils.py @@ -16,8 +16,7 @@ from charmhelpers.core.hookenv import ( log as juju_log, relation_get, relation_ids, - related_units, - service_name, ) + related_units, ) from charmhelpers.contrib.openstack import ( templating, @@ -48,7 +47,6 @@ SERVICES = [ "glance-api", "glance-registry", ] CHARM = "glance" -SERVICE_NAME = service_name() GLANCE_REGISTRY_CONF = "/etc/glance/glance-registry.conf" GLANCE_REGISTRY_PASTE_INI = "/etc/glance/glance-registry-paste.ini" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..afaed60c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append('hooks/') diff --git a/tests/test_glance_relations.py b/tests/test_glance_relations.py new file mode 100644 index 00000000..34e4064e --- /dev/null +++ b/tests/test_glance_relations.py @@ -0,0 +1,111 @@ +from mock import call, patch, MagicMock + +from tests.test_utils import CharmTestCase + +import hooks.glance_utils as utils + +_reg = utils.register_configs +_map = utils.restart_map + +utils.register_configs = MagicMock() +utils.restart_map = MagicMock() + +import hooks.glance_relations as relations + +utils.register_configs = _reg +utils.restart_map = _map + +TO_PATCH = [ + # charmhelpers.core.hookenv + 'Hooks', + 'config', + 'juju_log', + 'relation_ids', + 'relation_set', + 'service_name', + 'unit_get', + # charmhelpers.core.host + 'apt_install', + 'apt_update', + 'restart_on_change', + #charmhelpers.contrib.openstack.utils + 'configure_installation_source', + 'get_os_codename_package', + # charmhelpers.contrib.hahelpers.cluster_utils + 'eligible_leader', + 'is_clustered', + # glance_utils + 'PACKAGES', + 'restart_map', + 'register_configs', + 'do_openstack_upgrade', + 'migrate_database', + # glance_relations + 'configure_https', + #'GLANCE_REGISTRY_CONF', + #'GLANCE_API_CONF', +] + + +class GlanceRelationTests(CharmTestCase): + def setUp(self): + super(GlanceRelationTests, self).setUp(relations, TO_PATCH) + self.config.side_effect = self.test_config.get + +# def test_install_hook(self): +# repo = 'cloud:precise-grizzly' +# self.test_config.set('openstack-origin', repo) +# self.PACKAGES.return_value = ['foo', 'bar'] +# relations.install_hook() +# self.configure_installation_source.assert_called_with(repo) +# self.assertTrue(self.apt_update.called) +# self.apt_install.assert_called_with(['foo', 'bar'], fatal=True) + + def test_db_joined(self): + self.unit_get.return_value = 'glance.foohost.com' + relations.db_joined() + self.relation_set.assert_called_with(database='glance', username='glance', + hostname='glance.foohost.com') + self.unit_get.assert_called_with('private-address') + + @patch.object(relations, 'CONFIGS') + def test_db_changed_missing_relation_data(self, configs): + configs.complete_contexts = MagicMock() + configs.complete_contexts.return_value = [] + relations.db_changed() + self.juju_log.assert_called_with( + 'shared-db relation incomplete. Peer not ready?' + ) + + def _shared_db_test(self, configs): + configs.complete_contexts = MagicMock() + configs.complete_contexts.return_value = ['shared-db'] + configs.write = MagicMock() + relations.db_changed() + + @patch.object(relations, 'CONFIGS') + def test_db_changed_on_essex(self, configs): + self._shared_db_test(configs) + self.assertEquals([call('/etc/glance/glance-registry.conf'), + call('/etc/glance/glance-api.conf')], + configs.write.call_args_list) + self.juju_log.assert_called_with( + 'Cluster leader, performing db sync' + ) + self.migrate_database.assert_called_with() + + #@patch.object(relations, 'CONFIGS') + #def test_db_changed_no_essex(self, configs): + # self.get_os_codename_package.return_value = "other" + # self._shared_db_test(configs) + # self.assertEquals([call('/etc/glance/glance-registry.conf')], + # configs.write.call_args_list) + + @patch.object(relations, 'CONFIGS') + def test_image_service_joined(self, configs): + # look at compute joined + configs.complete_contexts = MagicMock() + configs.complete_contexts.return_value = ['https'] + configs.write = MagicMock() + relations.image_service_joined() + self.assertTrue(self.eligible_leader.called) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..a981c7c4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,118 @@ +import logging +import unittest +import os +import yaml + +from contextlib import contextmanager +from mock import patch, MagicMock + + +def load_config(): + ''' + Walk backwords from __file__ looking for config.yaml, load and return the + 'options' section' + ''' + config = None + f = __file__ + while config is None: + d = os.path.dirname(f) + if os.path.isfile(os.path.join(d, 'config.yaml')): + config = os.path.join(d, 'config.yaml') + break + f = d + + if not config: + logging.error('Could not find config.yaml in any parent directory ' + 'of %s. ' % file) + raise Exception + + return yaml.safe_load(open(config).read())['options'] + + +def get_default_config(): + ''' + Load default charm config from config.yaml return as a dict. + If no default is set in config.yaml, its value is None. + ''' + default_config = {} + config = load_config() + for k, v in config.iteritems(): + if 'default' in v: + default_config[k] = v['default'] + else: + default_config[k] = None + return default_config + + +class CharmTestCase(unittest.TestCase): + def setUp(self, obj, patches): + super(CharmTestCase, self).setUp() + self.patches = patches + self.obj = obj + self.test_config = TestConfig() + self.test_relation = TestRelation() + self.patch_all() + + def patch(self, method): + _m = patch.object(self.obj, method) + mock = _m.start() + self.addCleanup(_m.stop) + return mock + + def patch_all(self): + for method in self.patches: + setattr(self, method, self.patch(method)) + + +class TestConfig(object): + def __init__(self): + self.config = get_default_config() + + def get(self, attr=None): + if not attr: + return self.get_all() + try: + return self.config[attr] + except KeyError: + return None + + def get_all(self): + return self.config + + def set(self, attr, value): + if attr not in self.config: + raise KeyError + self.config[attr] = value + + +class TestRelation(object): + def __init__(self, relation_data={}): + self.relation_data = relation_data + + def set(self, relation_data): + self.relation_data = relation_data + + def get(self, attr=None, unit=None, rid=None): + if attr == None: + return self.relation_data + elif attr in self.relation_data: + return self.relation_data[attr] + return None + + +@contextmanager +def patch_open(): + '''Patch open() to allow mocking both open() itself and the file that is + yielded. + + Yields the mock for "open" and "file", respectively.''' + mock_open = MagicMock(spec=open) + mock_file = MagicMock(spec=file) + + @contextmanager + def stub_open(*args, **kwargs): + mock_open(*args, **kwargs) + yield mock_file + + with patch('__builtin__.open', stub_open): + yield mock_open, mock_file