diff --git a/.gitignore b/.gitignore index 9dd3eb8..a9b7fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .tox .testrepository +.stestr +.unit-state.db +**/__pycache__/ + diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..801646b --- /dev/null +++ b/.testr.conf @@ -0,0 +1,8 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION + +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/common.py b/common.py index 85a9adb..b2da391 100644 --- a/common.py +++ b/common.py @@ -139,8 +139,8 @@ class CRM(dict): if first: results = results + ' ' first = False - results = results + ('%s %s' % (prefix, d)) - + results = results + ('%s %s ' % (prefix, d)) + results = results.rstrip() return results def clone(self, name, resource, description=None, **kwargs): @@ -275,7 +275,7 @@ class CRM(dict): """ specs = ' '.join(resources) if 'description' in kwargs: - specs = specs + (' description="' % kwargs['description']) + specs = specs + (' description=%s"' % kwargs['description']) for key in 'meta', 'params': if key not in kwargs: diff --git a/test-requirements.txt b/test-requirements.txt index 095ec9c..4d72d9a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,7 @@ -flake8>=2.2.4,<=2.4.1 +# Lint and unit test requirements +flake8 os-testr>=0.4.1 +charms.reactive +mock>=1.2 +coverage>=3.6 +netifaces diff --git a/tox.ini b/tox.ini index a9a84a8..a8c1c8e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] -envlist = pep8,py27 +envlist = pep8,py35 skipsdist = True +skip_missing_interpreters = True [testenv] setenv = VIRTUAL_ENV={envdir} @@ -9,29 +10,15 @@ install_command = pip install {opts} {packages} commands = ostestr {posargs} -[testenv:py27] -basepython = python2.7 -deps = -r{toxinidir}/test-requirements.txt -# TODO: Need to write unit tests then remove the following command. -commands = /bin/true - -[testenv:py34] -basepython = python3.4 -deps = -r{toxinidir}/test-requirements.txt -# TODO: Need to write unit tests then remove the following command. -commands = /bin/true - [testenv:py35] basepython = python3.5 deps = -r{toxinidir}/test-requirements.txt -# TODO: Need to write unit tests then remove the following command. -commands = /bin/true +commands = ostestr {posargs} [testenv:py36] basepython = python3.6 deps = -r{toxinidir}/test-requirements.txt -# TODO: Need to write unit tests then remove the following command. -commands = /bin/true +commands = ostestr {posargs} [testenv:pep8] basepython = python3 @@ -43,4 +30,4 @@ basepython = python3 commands = {posargs} [flake8] -ignore = E402,E226 +ignore = E402,E226,W504 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unit_tests/test_common.py b/unit_tests/test_common.py new file mode 100644 index 0000000..1d481c5 --- /dev/null +++ b/unit_tests/test_common.py @@ -0,0 +1,357 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import unittest + +import common + + +class TestHAClusterCommonCRM(unittest.TestCase): + + def test_init(self): + crm = common.CRM() + expect = { + 'resources': {}, + 'delete_resources': [], + 'resource_params': {}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + self.assertEqual( + crm, + expect) + + expect['resources'] = {'res1': 'res1'} + self.assertEqual( + common.CRM(resources={'res1': 'res1'}), + expect) + self.assertEqual( + common.CRM({'resources': {'res1': 'res1'}}), + expect) + + def test_primitive(self): + crm = common.CRM() + crm.primitive('www8', 'apache', + params='configfile=/etc/apache/www8.conf', + operations='$id-ref=apache_ops') + self.assertEqual( + crm['resources']['www8'], + 'apache') + self.assertEqual( + crm['resource_params']['www8'], + (' params configfile=/etc/apache/www8.conf ' + 'operations $id-ref=apache_ops')) + + def test_primitive_description(self): + crm = common.CRM() + crm.primitive('www8', 'apache', + description='super awesome', + params='configfile=/etc/apache/www8.conf', + operations='$id-ref=apache_ops') + self.assertEqual( + crm['resources']['www8'], + 'apache') + self.assertEqual( + crm['resource_params']['www8'], + ('description="super awesome"' + ' params configfile=/etc/apache/www8.conf ' + 'operations $id-ref=apache_ops')) + + def test_primitive_multiops(self): + crm = common.CRM() + ops = ['monitor role=Master interval=60s', + 'monitor role=Slave interval=300s'] + + crm.primitive('r0', 'ocf:linbit:drbd', + params='drbd_resource=r0', + op=ops) + self.assertEqual( + crm['resources']['r0'], + 'ocf:linbit:drbd') + self.assertEqual( + crm['resource_params']['r0'], + (' params drbd_resource=r0 op monitor role=Master ' + 'interval=60s op monitor role=Slave interval=300s')) + + def test__parse(self): + crm = common.CRM() + self.assertEqual( + crm._parse('prefix', 'var1'), + ' prefix var1') + self.assertEqual( + crm._parse('prefix', ['var1']), + ' prefix var1') + self.assertEqual( + crm._parse('prefix', ['var1', 'var2']), + ' prefix var1 prefix var2') + + def test_clone(self): + crm = common.CRM() + crm.clone( + 'cl_nova_haproxy', + 'res_neutron_haproxy', + description='FE Haproxy') + self.assertEqual( + crm['clones']['cl_nova_haproxy'], + 'res_neutron_haproxy description="FE Haproxy"') + + def test_clone_meta(self): + crm = common.CRM() + crm.clone( + 'cl_nova_haproxy', + 'res_neutron_haproxy', + description='FE Haproxy', + meta='clone-node-max=1') + self.assertEqual( + crm['clones']['cl_nova_haproxy'], + ('res_neutron_haproxy description="FE Haproxy" ' + 'meta clone-node-max=1')) + + def test_colocation(self): + crm = common.CRM() + crm.colocation('console_with_vip', 'ALWAYS', 'nova-console', 'vip') + self.assertEqual( + crm['colocations']['console_with_vip'], + 'ALWAYS: nova-console vip') + + def test_colocation_node_attr(self): + crm = common.CRM() + crm.colocation( + 'console_with_vip', + 'ALWAYS', + 'nova-console', + 'vip', + node_attribute='attr1') + self.assertEqual( + crm['colocations']['console_with_vip'], + 'ALWAYS: nova-console vip node-attribute=attr1') + + def test_group(self): + crm = common.CRM() + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld') + self.assertEqual( + crm['groups']['grp_mysql'], + 'res_mysql_rbd res_mysql_fs res_mysql_vip res_mysqld') + + def test_group_meta(self): + crm = common.CRM() + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld', meta='container="vm"') + self.assertEqual( + crm['groups']['grp_mysql'], + ('res_mysql_rbd res_mysql_fs res_mysql_vip res_mysqld ' + 'meta container="vm"')) + + def test_group_meta_and_params(self): + crm = common.CRM() + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld', meta='container="vm"', + params='config=/etc/mysql/db0.conf') + self.assertEqual( + crm['groups']['grp_mysql'], + ('res_mysql_rbd res_mysql_fs res_mysql_vip res_mysqld ' + 'meta container="vm" ' + 'params config=/etc/mysql/db0.conf')) + + def test_group_desc(self): + crm = common.CRM() + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld', description='useful desc') + self.assertEqual( + crm['groups']['grp_mysql'], + ('res_mysql_rbd res_mysql_fs res_mysql_vip res_mysqld ' + 'description=useful desc"')) + + def test_delete_resource(self): + crm = common.CRM() + crm.delete_resource('res_mysql_vip') + self.assertEqual( + crm['delete_resources'], + ('res_mysql_vip',)) + + def test_delete_resource_multi(self): + crm = common.CRM() + crm.delete_resource('res_mysql_vip', 'grp_mysql') + self.assertEqual( + crm['delete_resources'], + ('res_mysql_vip', 'grp_mysql')) + + def test_init_services(self): + crm = common.CRM() + crm.init_services('haproxy') + self.assertEqual( + crm['init_services'], + ('haproxy',)) + + def test_init_services_multi(self): + crm = common.CRM() + crm.init_services('haproxy', 'apache2') + self.assertEqual( + crm['init_services'], + ('haproxy', 'apache2')) + + def test_ms_meta(self): + crm = common.CRM() + crm.ms('disk1', 'drbd1', meta='notify=true globally-unique=false') + self.assertEqual( + crm['ms']['disk1'], + 'drbd1 meta notify=true globally-unique=false') + + def test_ms_meta_and_params(self): + crm = common.CRM() + crm.ms('disk1', 'drbd1', + meta='notify=true globally-unique=false', + params='config=/etc/mysql/db0.conf') + self.assertEqual( + crm['ms']['disk1'], + 'drbd1 meta notify=true globally-unique=false ' + 'params config=/etc/mysql/db0.conf') + + def test_ms_desc(self): + crm = common.CRM() + crm.ms('disk1', 'drbd1', description='useful desc') + self.assertEqual( + crm['ms']['disk1'], + 'drbd1 description="useful desc"') + + # The method signature of 'order' seems broken. Leaving out unit tests for + # it as they would just confirm broken behaviour. + + def test_add(self): + crm = common.CRM() + mock1 = mock.MagicMock() + mock2 = mock.MagicMock() + mock1.configure_resource = mock2 + crm.add(mock1) + mock2.assert_called_once_with(crm) + + +class TestHAClusterCommonInitService(unittest.TestCase): + + def test_init(self): + init_svc = common.InitService('apache', 'apache2') + self.assertEqual( + init_svc.service_name, + 'apache') + self.assertEqual( + init_svc.init_service_name, + 'apache2') + self.assertTrue(init_svc.clone) + + def test_init_no_clone(self): + init_svc = common.InitService('apache', 'apache2', clone=False) + self.assertFalse(init_svc.clone) + + def test_configure_resource(self): + crm = common.CRM() + init_svc = common.InitService('apache', 'apache2') + init_svc.configure_resource(crm) + self.assertEqual( + crm['resources']['res_apache_apache2'], + 'lsb:apache2') + self.assertEqual( + crm['resource_params']['res_apache_apache2'], + (' op monitor interval="5s"')) + self.assertEqual(crm['init_services'], ('apache2',)) + self.assertEqual( + crm['clones']['cl_res_apache_apache2'], + 'res_apache_apache2') + + def test_configure_resource_no_clone(self): + crm = common.CRM() + init_svc = common.InitService('apache', 'apache2', clone=False) + init_svc.configure_resource(crm) + self.assertEqual( + crm['resources']['res_apache_apache2'], + 'lsb:apache2') + self.assertEqual( + crm['resource_params']['res_apache_apache2'], + (' op monitor interval="5s"')) + self.assertEqual(crm['init_services'], ('apache2',)) + self.assertFalse(crm['clones'].get('cl_res_apache_apache2')) + + +class TestHAClusterCommonVirtualIP(unittest.TestCase): + + def test_init(self): + vip_svc = common.VirtualIP('apache', '10.110.1.1') + self.assertEqual(vip_svc.service_name, 'apache') + self.assertEqual(vip_svc.vip, '10.110.1.1') + self.assertIsNone(vip_svc.nic) + self.assertIsNone(vip_svc.cidr) + + def test_init_no_default(self): + vip_svc = common.VirtualIP('apache', '10.110.1.1', 'eth1', '24') + self.assertEqual(vip_svc.service_name, 'apache') + self.assertEqual(vip_svc.vip, '10.110.1.1') + self.assertEqual(vip_svc.nic, 'eth1') + self.assertEqual(vip_svc.cidr, '24') + + def test_configure_resource(self): + crm = common.CRM() + vip_svc = common.VirtualIP('apache', '10.110.1.1', 'eth1', '24') + vip_svc.configure_resource(crm) + self.assertEqual( + crm['resources']['res_apache_eth1_vip'], + 'ocf:heartbeat:IPaddr2') + self.assertEqual( + crm['resource_params']['res_apache_eth1_vip'], + (' params ip="10.110.1.1" nic="eth1" cidr_netmask="24" ' + 'op monitor depth="0" timeout="20s" interval="10s"')) + + def test_configure_resource_no_nic(self): + crm = common.CRM() + vip_svc = common.VirtualIP('apache', '10.110.1.1') + vip_svc.configure_resource(crm) + self.assertEqual( + crm['resources']['res_apache_a7815c8_vip'], + 'ocf:heartbeat:IPaddr2') + self.assertEqual( + crm['resource_params']['res_apache_a7815c8_vip'], + (' params ip="10.110.1.1" ' + 'op monitor depth="0" timeout="20s" interval="10s"')) + + +class TestHAClusterCommonDNSEntry(unittest.TestCase): + + def test_init(self): + dns_svc = common.DNSEntry( + 'keystone', + '10.110.1.1', + 'keystone.admin', + 'admin') + self.assertEqual(dns_svc.service_name, 'keystone') + self.assertEqual(dns_svc.ip, '10.110.1.1') + self.assertEqual(dns_svc.fqdn, 'keystone.admin') + self.assertEqual(dns_svc.endpoint_type, 'admin') + + def test_configure_resource(self): + crm = common.CRM() + dns_svc = common.DNSEntry( + 'keystone', + '10.110.1.1', + 'keystone.admin', + 'admin') + dns_svc.configure_resource(crm) + self.assertEqual( + crm['resources']['res_keystone_admin_hostname'], + 'ocf:maas:dns') + self.assertEqual( + crm['resource_params']['res_keystone_admin_hostname'], + ' params fqdn="keystone.admin" ip_address="10.110.1.1"') diff --git a/unit_tests/test_requires.py b/unit_tests/test_requires.py new file mode 100644 index 0000000..ff5a8b3 --- /dev/null +++ b/unit_tests/test_requires.py @@ -0,0 +1,414 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import mock +import unittest + +import common + +# Deal with the 'relations.hacluster.common' import in requires.py which +# is invalid in the unit tests as there is no 'relations'. +relations_mock = mock.MagicMock() +relations_mock.hacluster.common = common +modules = { + 'relations': relations_mock, + 'relations.hacluster': mock.MagicMock(), + 'relations.hacluster.common': common, +} +module_patcher = mock.patch.dict('sys.modules', modules) +module_patcher.start() + +with mock.patch('charmhelpers.core.hookenv.metadata') as _meta: + _meta.return_Value = 'ss' + import requires + +_hook_args = {} + +TO_PATCH = [ + 'data_changed', +] + + +def mock_hook(*args, **kwargs): + + def inner(f): + # remember what we were passed. Note that we can't actually determine + # the class we're attached to, as the decorator only gets the function. + _hook_args[f.__name__] = dict(args=args, kwargs=kwargs) + return f + return inner + + +class TestHAClusterRequires(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls._patched_hook = mock.patch('charms.reactive.when', mock_hook) + cls._patched_hook_started = cls._patched_hook.start() + # force requires to rerun the mock_hook decorator: + # try except is Python2/Python3 compatibility as Python3 has moved + # reload to importlib. + try: + reload(requires) + except NameError: + import importlib + importlib.reload(requires) + + @classmethod + def tearDownClass(cls): + cls._patched_hook.stop() + cls._patched_hook_started = None + cls._patched_hook = None + # and fix any breakage we did to the module + try: + reload(requires) + except NameError: + import importlib + importlib.reload(requires) + + def patch(self, method): + _m = mock.patch.object(self.obj, method) + _mock = _m.start() + self.addCleanup(_m.stop) + return _mock + + def setUp(self): + self.cr = requires.HAClusterRequires('some-relation', []) + self._patches = {} + self._patches_start = {} + self.obj = requires + for method in TO_PATCH: + setattr(self, method, self.patch(method)) + + def tearDown(self): + self.cr = None + for k, v in self._patches.items(): + v.stop() + setattr(self, k, None) + self._patches = None + self._patches_start = None + + def patch_kr(self, attr, return_value=None): + mocked = mock.patch.object(self.cr, attr) + self._patches[attr] = mocked + started = mocked.start() + started.return_value = return_value + self._patches_start[attr] = started + setattr(self, attr, started) + + def test_joined(self): + self.patch_kr('set_state') + self.cr.joined() + self.set_state.assert_called_once_with('{relation_name}.connected') + + def test_changed(self): + self.patch_kr('is_clustered', True) + self.patch_kr('set_state') + self.cr.changed() + self.set_state.assert_called_once_with('{relation_name}.available') + + def test_changed_not_clustered(self): + self.patch_kr('is_clustered', False) + self.patch_kr('remove_state') + self.cr.changed() + self.remove_state.assert_called_once_with('{relation_name}.available') + + def test_departed(self): + self.patch_kr('remove_state') + self.cr.departed() + self.remove_state.assert_has_calls([ + mock.call('{relation_name}.available'), + mock.call('{relation_name}.connected')]) + + def test_is_clustered(self): + self.patch_kr('get_remote_all') + + self.get_remote_all.return_value = [True] + self.assertTrue(self.cr.is_clustered()) + + self.get_remote_all.return_value = ['true'] + self.assertTrue(self.cr.is_clustered()) + + self.get_remote_all.return_value = ['yes'] + self.assertTrue(self.cr.is_clustered()) + + self.get_remote_all.return_value = None + self.assertFalse(self.cr.is_clustered()) + + self.get_remote_all.return_value = [False] + self.assertFalse(self.cr.is_clustered()) + + self.get_remote_all.return_value = ['false'] + self.assertFalse(self.cr.is_clustered()) + + self.get_remote_all.return_value = ['flump'] + self.assertFalse(self.cr.is_clustered()) + + def jsonify(self, options): + json_encode_options = dict( + sort_keys=True, + ) + for k, v in options.items(): + if v: + options[k] = json.dumps(v, **json_encode_options) + + def test_manage_resources(self): + res = common.CRM() + res.primitive('res_neutron_haproxy', 'lsb:haproxy', + op='monitor interval="5s"') + res.init_services('haproxy') + res.clone('cl_nova_haproxy', 'res_neutron_haproxy') + expected = { + 'json_clones': {"cl_nova_haproxy": "res_neutron_haproxy"}, + 'json_init_services': ["haproxy"], + 'json_resource_params': { + "res_neutron_haproxy": ' op monitor interval="5s"'}, + 'json_resources': {"res_neutron_haproxy": "lsb:haproxy"}} + self.jsonify(expected) + self.data_changed.return_value = True + self.patch_kr('set_local') + self.patch_kr('set_remote') + self.cr.manage_resources(res) + self.set_local.assert_called_once_with(**expected) + self.set_remote.assert_called_once_with(**expected) + + def test_manage_resources_no_change(self): + res = common.CRM() + res.primitive('res_neutron_haproxy', 'lsb:haproxy', + op='monitor interval="5s"') + res.init_services('haproxy') + res.clone('cl_nova_haproxy', 'res_neutron_haproxy') + self.data_changed.return_value = False + self.patch_kr('set_local') + self.patch_kr('set_remote') + self.cr.manage_resources(res) + self.assertFalse(self.set_local.called) + self.assertFalse(self.set_remote.called) + + def test_bind_resources(self): + self.patch_kr('get_local', 'resources') + self.patch_kr('bind_on') + self.patch_kr('manage_resources') + self.cr.bind_resources() + self.bind_on.assert_called_once_with(iface=None, mcastport=4440) + self.manage_resources.assert_called_once_with('resources') + + def test_bind_resources_no_defaults(self): + self.patch_kr('get_local', 'resources') + self.patch_kr('bind_on') + self.patch_kr('manage_resources') + self.cr.bind_resources(iface='tr34', mcastport=111) + self.bind_on.assert_called_once_with(iface='tr34', mcastport=111) + self.manage_resources.assert_called_once_with('resources') + + def test_add_vip(self): + expected = { + 'resources': { + 'res_mysql_4b8ce37_vip': 'ocf:heartbeat:IPaddr2'}, + 'delete_resources': [], + 'resource_params': { + 'res_mysql_4b8ce37_vip': ( + ' params ip="10.110.5.43" op monitor depth="0" ' + 'timeout="20s" interval="10s"')}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + self.patch_kr('get_local', None) + self.patch_kr('set_local') + self.cr.add_vip('mysql', '10.110.5.43') + self.set_local.assert_called_once_with(resources=expected) + + def test_add_additional_vip(self): + existing_resource = { + 'resources': { + 'res_mysql_4b8ce37_vip': 'ocf:heartbeat:IPaddr2'}, + 'delete_resources': [], + 'resource_params': { + 'res_mysql_4b8ce37_vip': ( + ' params ip="10.110.5.43" op monitor depth="0" ' + 'timeout="20s" interval="10s"')}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + expected = { + 'resources': { + 'res_mysql_4b8ce37_vip': 'ocf:heartbeat:IPaddr2', + 'res_mysql_1993276_vip': 'ocf:heartbeat:IPaddr2'}, + 'delete_resources': [], + 'resource_params': { + 'res_mysql_4b8ce37_vip': ( + ' params ip="10.110.5.43" op monitor depth="0" ' + 'timeout="20s" interval="10s"'), + 'res_mysql_1993276_vip': ( + ' params ip="10.120.5.43" op monitor depth="0" ' + 'timeout="20s" interval="10s"')}, + 'groups': { + 'grp_mysql_vips': ('res_mysql_1993276_vip ' + 'res_mysql_4b8ce37_vip')}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + self.patch_kr('get_local', existing_resource) + self.patch_kr('set_local') + self.cr.add_vip('mysql', '10.120.5.43') + self.set_local.assert_called_once_with(resources=expected) + + def test_add_init_service(self): + expected = { + 'resources': { + 'res_mysql_telnetd': 'lsb:telnetd'}, + 'delete_resources': [], + 'resource_params': { + 'res_mysql_telnetd': ' op monitor interval="5s"'}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {'cl_res_mysql_telnetd': 'res_mysql_telnetd'}, + 'locations': {}, + 'init_services': ('telnetd',)} + self.patch_kr('get_local', None) + self.patch_kr('set_local') + self.cr.add_init_service('mysql', 'telnetd') + self.set_local.assert_called_once_with(resources=expected) + + def test_add_dnsha(self): + expected = { + 'resources': { + 'res_keystone_public_hostname': 'ocf:maas:dns'}, + 'delete_resources': [], + 'resource_params': { + 'res_keystone_public_hostname': ( + ' params fqdn="keystone.public" ' + 'ip_address="10.110.5.43"')}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + self.patch_kr('get_local', None) + self.patch_kr('set_local') + self.cr.add_dnsha( + 'keystone', + '10.110.5.43', + 'keystone.public', + 'public') + self.set_local.assert_called_once_with(resources=expected) + + def test_add_additional_dnsha(self): + existing_resource = { + 'resources': { + 'res_keystone_public_hostname': 'ocf:maas:dns'}, + 'delete_resources': [], + 'resource_params': { + 'res_keystone_public_hostname': ( + ' params fqdn="keystone.public" ' + 'ip_address="10.110.5.43"')}, + 'groups': {}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + expected = { + 'resources': { + 'res_keystone_public_hostname': 'ocf:maas:dns', + 'res_keystone_admin_hostname': 'ocf:maas:dns'}, + 'delete_resources': [], + 'resource_params': { + 'res_keystone_public_hostname': ( + ' params fqdn="keystone.public" ' + 'ip_address="10.110.5.43"'), + 'res_keystone_admin_hostname': ( + ' params fqdn="keystone.admin" ' + 'ip_address="10.120.5.43"')}, + 'groups': { + 'grp_keystone_hostnames': ('res_keystone_admin_hostname ' + 'res_keystone_public_hostname')}, + 'ms': {}, + 'orders': {}, + 'colocations': {}, + 'clones': {}, + 'locations': {}, + 'init_services': []} + + self.patch_kr('get_local', existing_resource) + self.patch_kr('set_local') + self.cr.add_dnsha( + 'keystone', + '10.120.5.43', + 'keystone.admin', + 'admin') + self.set_local.assert_called_once_with(resources=expected) + + @mock.patch.object(requires.hookenv, 'related_units') + @mock.patch.object(requires.hookenv, 'relation_get') + def test_get_remote_all(self, relation_get, related_units): + unit_data = { + 'rid:1': { + 'app1/0': { + 'key1': 'value1', + 'key2': 'value2'}, + 'app1/1': { + 'key1': 'value1', + 'key2': 'value3'}}, + 'rid:2': { + 'app2/0': { + 'key1': 'value1', + 'key2': 'value3'}}, + 'rid:3': {}} + + def get_unit_data(key, unit, relation_id): + return unit_data[relation_id].get(unit, {}).get(key, {}) + conv1 = mock.MagicMock() + conv1.relation_ids = ['rid:1', 'rid:2'] + conv2 = mock.MagicMock() + conv2.relation_ids = ['rid:3'] + self.patch_kr('conversations', [conv1, conv2]) + related_units.side_effect = lambda x: unit_data[x].keys() + relation_get.side_effect = get_unit_data + # Check de-duplication: + self.assertEqual( + self.cr.get_remote_all('key1'), + ['value1']) + # Check multiple values: + self.assertEqual( + self.cr.get_remote_all('key2'), + ['value2', 'value3']) + # Check missing key + self.assertEqual( + self.cr.get_remote_all('key100'), + []) + # Check missing key with default + self.assertEqual( + self.cr.get_remote_all('key100', default='defaultvalue'), + ['defaultvalue'])