diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ed08ec9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + if __name__ == .__main__.: +include= + hooks/ceilometer_* diff --git a/.pydevproject b/.pydevproject index c0701ac..a338b81 100644 --- a/.pydevproject +++ b/.pydevproject @@ -2,6 +2,7 @@ /ceilometer/hooks +/ceilometer/unit_tests python 2.7 Default diff --git a/Makefile b/Makefile index 17f7bee..1fd305c 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ lint: @flake8 --exclude hooks/charmhelpers hooks + @flake8 unit_tests @charm proof sync: diff --git a/hooks/ceilometer_contexts.py b/hooks/ceilometer_contexts.py index bcf9ba7..78d0709 100644 --- a/hooks/ceilometer_contexts.py +++ b/hooks/ceilometer_contexts.py @@ -60,3 +60,15 @@ class CeilometerContext(OSContextGenerator): 'metering_secret': get_shared_secret() } return ctxt + + +class CeilometerServiceContext(OSContextGenerator): + interfaces = ['ceilometer-service'] + + def __call__(self): + for relid in relation_ids('ceilometer-service'): + for unit in related_units(relid): + conf = relation_get(unit=unit, rid=relid) + if context_complete(conf): + return conf + return {} diff --git a/hooks/ceilometer_utils.py b/hooks/ceilometer_utils.py index 09368a4..a1844f8 100644 --- a/hooks/ceilometer_utils.py +++ b/hooks/ceilometer_utils.py @@ -61,7 +61,7 @@ def register_configs(): # just default to earliest supported release. configs dont get touched # till post-install, anyway. release = get_os_codename_package('ceilometer-common', fatal=False) \ - or 'grizzly' + or 'grizzly' configs = templating.OSConfigRenderer(templates_dir=TEMPLATES, openstack_release=release) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..bb0670f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] +verbosity=1 +with-coverage=1 +cover-erase=1 +cover-package=hooks + diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..f80aab3 --- /dev/null +++ b/unit_tests/__init__.py @@ -0,0 +1,2 @@ +import sys +sys.path.append('hooks') diff --git a/unit_tests/test_ceilometer_contexts.py b/unit_tests/test_ceilometer_contexts.py new file mode 100644 index 0000000..c512729 --- /dev/null +++ b/unit_tests/test_ceilometer_contexts.py @@ -0,0 +1,89 @@ +from mock import patch + +import ceilometer_contexts as contexts + +from test_utils import CharmTestCase, mock_open + +TO_PATCH = [ + 'relation_get', + 'relation_ids', + 'related_units', + 'config' +] + + +class CeilometerContextsTest(CharmTestCase): + + def setUp(self): + super(CeilometerContextsTest, self).setUp(contexts, TO_PATCH) + self.config.side_effect = self.test_config.get + self.relation_get.side_effect = self.test_relation.get + + def tearDown(self): + super(CeilometerContextsTest, self).tearDown() + + def test_logging_context(self): + self.test_config.set('debug', False) + self.test_config.set('verbose', False) + self.assertEquals(contexts.LoggingConfigContext()(), + {'debug': False, 'verbose': False}) + self.test_config.set('debug', True) + self.test_config.set('verbose', False) + self.assertEquals(contexts.LoggingConfigContext()(), + {'debug': True, 'verbose': False}) + self.test_config.set('debug', True) + self.test_config.set('verbose', True) + self.assertEquals(contexts.LoggingConfigContext()(), + {'debug': True, 'verbose': True}) + + def test_mongodb_context_not_related(self): + self.relation_ids.return_value = [] + self.assertEquals(contexts.MongoDBContext()(), {}) + + def test_mongodb_context_related(self): + self.relation_ids.return_value = ['shared-db:0'] + self.related_units.return_value = ['mongodb/0'] + data = { + 'hostname': 'mongodb', + 'port': 8090 + } + self.test_relation.set(data) + self.assertEquals(contexts.MongoDBContext()(), + {'db_host': 'mongodb', 'db_port': 8090, + 'db_name': 'ceilometer'}) + + @patch.object(contexts, 'get_shared_secret') + def test_ceilometer_context(self, secret): + secret.return_value = 'mysecret' + self.assertEquals(contexts.CeilometerContext()(), + {'port': 8777, 'metering_secret': 'mysecret'}) + + def test_ceilometer_service_context(self): + self.relation_ids.return_value = ['ceilometer-service:0'] + self.related_units.return_value = ['ceilometer/0'] + data = { + 'metering_secret': 'mysecret', + 'keystone_host': 'test' + } + self.test_relation.set(data) + self.assertEquals(contexts.CeilometerServiceContext()(), data) + + def test_ceilometer_service_context_not_related(self): + self.relation_ids.return_value = [] + self.assertEquals(contexts.CeilometerServiceContext()(), {}) + + @patch('os.path.exists') + def test_get_shared_secret_existing(self, exists): + exists.return_value = True + with mock_open(contexts.SHARED_SECRET, u'mysecret'): + self.assertEquals(contexts.get_shared_secret(), + 'mysecret') + + @patch('uuid.uuid4') + @patch('os.path.exists') + def test_get_shared_secret_new(self, exists, uuid4): + exists.return_value = False + uuid4.return_value = 'newsecret' + with patch('__builtin__.open'): + self.assertEquals(contexts.get_shared_secret(), + 'newsecret') diff --git a/unit_tests/test_ceilometer_hooks.py b/unit_tests/test_ceilometer_hooks.py new file mode 100644 index 0000000..e610bc4 --- /dev/null +++ b/unit_tests/test_ceilometer_hooks.py @@ -0,0 +1,99 @@ +from mock import patch, MagicMock + +import ceilometer_utils +# Patch out register_configs for import of hooks +_register_configs = ceilometer_utils.register_configs +ceilometer_utils.register_configs = MagicMock() + +import ceilometer_hooks as hooks + +# Renable old function +ceilometer_utils.register_configs = _register_configs + +from test_utils import CharmTestCase + +TO_PATCH = [ + 'relation_set', + 'configure_installation_source', + 'apt_install', + 'apt_update', + 'open_port', + 'config', + 'log', + 'relation_ids', + 'filter_installed_packages', + 'CONFIGS', + 'unit_get', + 'get_ceilometer_context' +] + + +class CeilometerHooksTest(CharmTestCase): + + def setUp(self): + super(CeilometerHooksTest, self).setUp(hooks, TO_PATCH) + self.config.side_effect = self.test_config.get + + def test_configure_source(self): + self.test_config.set('openstack-origin', 'cloud:precise-havana') + hooks.hooks.execute(['hooks/install']) + self.configure_installation_source.\ + assert_called_with('cloud:precise-havana') + + def test_install_hook(self): + self.filter_installed_packages.return_value = hooks.CEILOMETER_PACKAGES + hooks.hooks.execute(['hooks/install']) + self.assertTrue(self.configure_installation_source.called) + self.open_port.assert_called_with(hooks.CEILOMETER_PORT) + self.apt_update.assert_called_with(fatal=True) + self.apt_install.assert_called_with(hooks.CEILOMETER_PACKAGES, + fatal=True) + + def test_amqp_joined(self): + hooks.hooks.execute(['hooks/amqp-relation-joined']) + self.relation_set.assert_called_with( + username=self.test_config.get('rabbit-user'), + vhost=self.test_config.get('rabbit-vhost')) + + def test_db_joined(self): + hooks.hooks.execute(['hooks/shared-db-relation-joined']) + self.relation_set.assert_called_with( + ceilometer_database='ceilometer') + + @patch.object(hooks, 'ceilometer_joined') + def test_any_changed(self, joined): + hooks.hooks.execute(['hooks/shared-db-relation-changed']) + self.assertTrue(self.CONFIGS.write_all.called) + self.assertTrue(joined.called) + + @patch.object(hooks, 'install') + @patch.object(hooks, 'any_changed') + def test_upgrade_charm(self, changed, install): + hooks.hooks.execute(['hooks/upgrade-charm']) + self.assertTrue(changed.called) + self.assertTrue(install.called) + + @patch.object(hooks, 'install') + @patch.object(hooks, 'any_changed') + def test_config_changed(self, changed, install): + hooks.hooks.execute(['hooks/config-changed']) + self.assertTrue(changed.called) + self.assertTrue(install.called) + + def test_keystone_joined(self): + self.unit_get.return_value = 'thishost' + self.test_config.set('region', 'myregion') + hooks.hooks.execute(['hooks/identity-service-relation-joined']) + url = "http://{}:{}".format('thishost', hooks.CEILOMETER_PORT) + self.relation_set.assert_called_with( + service=hooks.CEILOMETER_SERVICE, + public_url=url, admin_url=url, internal_url=url, + requested_roles=hooks.CEILOMETER_ROLE, + region='myregion') + + def test_ceilometer_joined(self): + self.relation_ids.return_value = ['ceilometer:0'] + self.get_ceilometer_context.return_value = {'test': 'data'} + hooks.hooks.execute(['hooks/ceilometer-service-relation-joined']) + self.relation_set.assert_called_with('ceilometer:0', + {'test': 'data'}) diff --git a/unit_tests/test_ceilometer_utils.py b/unit_tests/test_ceilometer_utils.py new file mode 100644 index 0000000..eff80b1 --- /dev/null +++ b/unit_tests/test_ceilometer_utils.py @@ -0,0 +1,49 @@ +from mock import patch, call + +import ceilometer_utils as utils + +from test_utils import CharmTestCase + +TO_PATCH = [ + 'get_os_codename_package', + 'templating', + 'LoggingConfigContext', + 'MongoDBContext', + 'CeilometerContext', +] + + +class CeilometerUtilsTest(CharmTestCase): + + def setUp(self): + super(CeilometerUtilsTest, self).setUp(utils, TO_PATCH) + + def tearDown(self): + super(CeilometerUtilsTest, self).tearDown() + + def test_register_configs(self): + configs = utils.register_configs() + calls = [] + for conf in utils.CONFIG_FILES: + calls.append(call(conf, + utils.CONFIG_FILES[conf]['hook_contexts'])) + configs.register.assert_has_calls(calls, any_order=True) + + def test_restart_map(self): + restart_map = utils.restart_map() + self.assertEquals(restart_map, + {'/etc/ceilometer/ceilometer.conf': [ + 'ceilometer-agent-central', + 'ceilometer-collector', + 'ceilometer-api']}) + + def test_get_ceilometer_conf(self): + class TestContext(): + def __call__(self): + return {'data': 'test'} + with patch.dict(utils.CONFIG_FILES, + {'/etc/ceilometer/ceilometer.conf': { + 'hook_contexts': [TestContext()] + }}): + self.assertTrue(utils.get_ceilometer_context(), + {'data': 'test'}) diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py new file mode 100644 index 0000000..e90679e --- /dev/null +++ b/unit_tests/test_utils.py @@ -0,0 +1,111 @@ +import logging +import unittest +import os +import yaml +import io + +from contextlib import contextmanager +from mock import patch + + +@contextmanager +def mock_open(filename, contents=None): + ''' Slightly simpler mock of open to return contents for filename ''' + def mock_file(*args): + if args[0] == filename: + return io.StringIO(contents) + else: + return open(*args) + with patch('__builtin__.open', mock_file): + yield + + +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): + 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 is None: + return self.relation_data + elif attr in self.relation_data: + return self.relation_data[attr] + return None