diff --git a/hooks/hooks.py b/hooks/ceph_hooks.py similarity index 100% rename from hooks/hooks.py rename to hooks/ceph_hooks.py diff --git a/hooks/client-relation-changed b/hooks/client-relation-changed index 9416ca6..52d9663 120000 --- a/hooks/client-relation-changed +++ b/hooks/client-relation-changed @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/client-relation-joined b/hooks/client-relation-joined index 9416ca6..52d9663 120000 --- a/hooks/client-relation-joined +++ b/hooks/client-relation-joined @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/config-changed b/hooks/config-changed index 9416ca6..52d9663 120000 --- a/hooks/config-changed +++ b/hooks/config-changed @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/install.real b/hooks/install.real index 9416ca6..52d9663 120000 --- a/hooks/install.real +++ b/hooks/install.real @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/mon-relation-changed b/hooks/mon-relation-changed index 9416ca6..52d9663 120000 --- a/hooks/mon-relation-changed +++ b/hooks/mon-relation-changed @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/mon-relation-departed b/hooks/mon-relation-departed index 9416ca6..52d9663 120000 --- a/hooks/mon-relation-departed +++ b/hooks/mon-relation-departed @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/mon-relation-joined b/hooks/mon-relation-joined index 9416ca6..52d9663 120000 --- a/hooks/mon-relation-joined +++ b/hooks/mon-relation-joined @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/nrpe-external-master-relation-changed b/hooks/nrpe-external-master-relation-changed index 9416ca6..52d9663 120000 --- a/hooks/nrpe-external-master-relation-changed +++ b/hooks/nrpe-external-master-relation-changed @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/nrpe-external-master-relation-joined b/hooks/nrpe-external-master-relation-joined index 9416ca6..52d9663 120000 --- a/hooks/nrpe-external-master-relation-joined +++ b/hooks/nrpe-external-master-relation-joined @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/osd-relation-joined b/hooks/osd-relation-joined index 9416ca6..52d9663 120000 --- a/hooks/osd-relation-joined +++ b/hooks/osd-relation-joined @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/radosgw-relation-joined b/hooks/radosgw-relation-joined index 9416ca6..52d9663 120000 --- a/hooks/radosgw-relation-joined +++ b/hooks/radosgw-relation-joined @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/start b/hooks/start index 9416ca6..52d9663 120000 --- a/hooks/start +++ b/hooks/start @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/stop b/hooks/stop index 9416ca6..52d9663 120000 --- a/hooks/stop +++ b/hooks/stop @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/hooks/update-status b/hooks/update-status new file mode 120000 index 0000000..52d9663 --- /dev/null +++ b/hooks/update-status @@ -0,0 +1 @@ +ceph_hooks.py \ No newline at end of file diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm index 9416ca6..52d9663 120000 --- a/hooks/upgrade-charm +++ b/hooks/upgrade-charm @@ -1 +1 @@ -hooks.py \ No newline at end of file +ceph_hooks.py \ No newline at end of file diff --git a/unit_tests/test_status.py b/unit_tests/test_status.py new file mode 100644 index 0000000..bd8b024 --- /dev/null +++ b/unit_tests/test_status.py @@ -0,0 +1,95 @@ +import mock +import test_utils + +with mock.patch('utils.get_unit_hostname'): + import ceph_hooks as hooks + +TO_PATCH = [ + 'status_set', + 'config', + 'ceph', + 'relation_ids', + 'relation_get', + 'related_units', + 'local_unit', +] + +NO_PEERS = { + 'ceph-mon1': True +} + +ENOUGH_PEERS_INCOMPLETE = { + 'ceph-mon1': True, + 'ceph-mon2': False, + 'ceph-mon3': False, +} + +ENOUGH_PEERS_COMPLETE = { + 'ceph-mon1': True, + 'ceph-mon2': True, + 'ceph-mon3': True, +} + + +class ServiceStatusTestCase(test_utils.CharmTestCase): + + def setUp(self): + super(ServiceStatusTestCase, self).setUp(hooks, TO_PATCH) + self.config.side_effect = self.test_config.get + self.test_config.set('monitor-count', 3) + self.local_unit.return_value = 'ceph-mon1' + + @mock.patch.object(hooks, 'get_peer_units') + def test_assess_status_no_peers(self, _peer_units): + _peer_units.return_value = NO_PEERS + hooks.assess_status() + self.status_set.assert_called_with('blocked', mock.ANY) + + @mock.patch.object(hooks, 'get_peer_units') + def test_assess_status_peers_incomplete(self, _peer_units): + _peer_units.return_value = ENOUGH_PEERS_INCOMPLETE + hooks.assess_status() + self.status_set.assert_called_with('waiting', mock.ANY) + + @mock.patch.object(hooks, 'get_peer_units') + def test_assess_status_peers_complete_active(self, _peer_units): + _peer_units.return_value = ENOUGH_PEERS_COMPLETE + self.ceph.is_bootstrapped.return_value = True + self.ceph.is_quorum.return_value = True + hooks.assess_status() + self.status_set.assert_called_with('active', mock.ANY) + + @mock.patch.object(hooks, 'get_peer_units') + def test_assess_status_peers_complete_down(self, _peer_units): + _peer_units.return_value = ENOUGH_PEERS_COMPLETE + self.ceph.is_bootstrapped.return_value = False + self.ceph.is_quorum.return_value = False + hooks.assess_status() + self.status_set.assert_called_with('blocked', mock.ANY) + + def test_get_peer_units_no_peers(self): + self.relation_ids.return_value = ['mon:1'] + self.related_units.return_value = [] + self.assertEquals({'ceph-mon1': True}, + hooks.get_peer_units()) + + def test_get_peer_units_peers_incomplete(self): + self.relation_ids.return_value = ['mon:1'] + self.related_units.return_value = ['ceph-mon2', + 'ceph-mon3'] + self.relation_get.return_value = None + self.assertEquals({'ceph-mon1': True, + 'ceph-mon2': False, + 'ceph-mon3': False}, + hooks.get_peer_units()) + + def test_get_peer_units_peers_complete(self): + self.relation_ids.return_value = ['mon:1'] + self.related_units.return_value = ['ceph-mon2', + 'ceph-mon3'] + self.relation_get.side_effect = ['ceph-mon2', + 'ceph-mon3'] + self.assertEquals({'ceph-mon1': True, + 'ceph-mon2': True, + 'ceph-mon3': True}, + hooks.get_peer_units()) \ No newline at end of file diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py new file mode 100644 index 0000000..663a048 --- /dev/null +++ b/unit_tests/test_utils.py @@ -0,0 +1,121 @@ +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. ' % f) + 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 is 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