From f5580e571df4654cce569b50a4ce0e09ade45254 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Mon, 11 Nov 2019 12:03:52 +0100 Subject: [PATCH] ovsdb-cms: Provide client bound address on relation Change-Id: I10b34f8a7517c956c4bbdab9252f323072bbc19d --- src/lib/ovsdb.py | 30 ++++++++-- src/ovsdb_cms/provides.py | 5 ++ src/ovsdb_cms/requires.py | 10 ++++ unit_tests/test_lib_ovsdb.py | 19 +++++-- unit_tests/test_ovsdb_cms_provides.py | 52 ++++++++++++++++++ unit_tests/test_ovsdb_cms_requires.py | 79 +++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 unit_tests/test_ovsdb_cms_provides.py create mode 100644 unit_tests/test_ovsdb_cms_requires.py diff --git a/src/lib/ovsdb.py b/src/lib/ovsdb.py index 625c752..901a9ed 100644 --- a/src/lib/ovsdb.py +++ b/src/lib/ovsdb.py @@ -46,8 +46,7 @@ class OVSDB(reactive.Endpoint): fmt = '{}' return fmt.format(ipaddr) - @property - def cluster_local_addr(self): + def _endpoint_local_bound_addr(self): """Retrieve local address bound to endpoint. :returns: IPv4 or IPv6 address bound to endpoint @@ -62,21 +61,40 @@ class OVSDB(reactive.Endpoint): return self._format_addr(addr['address']) @property - def cluster_remote_addrs(self): - """Retrieve remote addresses bound to remote endpoint. + def cluster_local_addr(self): + """Retrieve local address bound to endpoint. - :returns: IPv4 or IPv6 addresses bound to remote endpoints. + :returns: IPv4 or IPv6 address bound to endpoint + :rtype: str + """ + return self._endpoint_local_bound_addr() + + def _remote_addrs(self, key): + """Retrieve addresses published by remote units. + + :param key: Relation data key to retrieve value from. + :type key: str + :returns: IPv4 or IPv6 addresses published by remote units. :rtype: Iterator[str] """ for relation in self.relations: for unit in relation.units: try: addr = self._format_addr( - unit.received.get('bound-address', '')) + unit.received.get(key, '')) yield addr except ValueError: continue + @property + def cluster_remote_addrs(self): + """Retrieve remote addresses bound to remote endpoint. + + :returns: IPv4 or IPv6 addresses bound to remote endpoints. + :rtype: Iterator[str] + """ + return self._remote_addrs('bound-address') + @property def db_nb_port(self): """Provide port number for OVN Northbound OVSDB. diff --git a/src/ovsdb_cms/provides.py b/src/ovsdb_cms/provides.py index ab1742c..f13b450 100644 --- a/src/ovsdb_cms/provides.py +++ b/src/ovsdb_cms/provides.py @@ -29,6 +29,11 @@ from .lib import ovsdb as ovsdb class OVSDBCMSProvides(ovsdb.OVSDB): + @property + def client_remote_addrs(self): + """Retrieve remote bound addresses as provided by our clients.""" + return self._remote_addrs('cms-client-bound-address') + @when('endpoint.{endpoint_name}.joined') def joined(self): ch_core.hookenv.log('{}: {} -> {}' diff --git a/src/ovsdb_cms/requires.py b/src/ovsdb_cms/requires.py index 141af4f..67812d3 100644 --- a/src/ovsdb_cms/requires.py +++ b/src/ovsdb_cms/requires.py @@ -31,6 +31,15 @@ from .lib import ovsdb as ovsdb class OVSDBCMSRequires(ovsdb.OVSDB): + def publish_cms_client_bound_addr(self): + """Announce address on relation. + + This will be used by our peers to grant access. + """ + for relation in self.relations: + relation.to_publish['cms-client-bound-address'] = ( + self._endpoint_local_bound_addr()) + @when('endpoint.{endpoint_name}.joined') def joined(self): ch_core.hookenv.log('{}: {} -> {}' @@ -39,6 +48,7 @@ class OVSDBCMSRequires(ovsdb.OVSDB): inspect.currentframe().f_code.co_name), level=ch_core.hookenv.INFO) super().joined() + self.publish_cms_client_bound_addr() if self.expected_units_available(): reactive.set_flag(self.expand_name('{endpoint_name}.available')) diff --git a/unit_tests/test_lib_ovsdb.py b/unit_tests/test_lib_ovsdb.py index 5d07d94..3be59db 100644 --- a/unit_tests/test_lib_ovsdb.py +++ b/unit_tests/test_lib_ovsdb.py @@ -67,7 +67,7 @@ class TestOVSDBLib(test_utils.PatchHelper): with self.assertRaises(ValueError): self.target._format_addr('2001:db8::g') - def test_cluster_local_addr(self): + def test__endpoint_local_bound_addr(self): relation = mock.MagicMock() relation.relation_id = 'some-endpoint:42' self.patch_target('_relations') @@ -92,11 +92,17 @@ class TestOVSDBLib(test_utils.PatchHelper): 'egress-subnets': ['42.42.42.42/32'], 'ingress-addresses': ['42.42.42.42'], } - self.assertEquals(self.target.cluster_local_addr, '42.42.42.42') + self.assertEquals(self.target._endpoint_local_bound_addr(), + '42.42.42.42') self.network_get.assert_called_once_with( 'some-relation', relation_id='some-endpoint:42') - def test_cluster_remote_addr(self): + def test_cluster_local_addr(self): + self.patch_target('_endpoint_local_bound_addr') + self.target.cluster_local_addr + self._endpoint_local_bound_addr.assert_called_once_with() + + def test__remote_addrs(self): relation = mock.MagicMock() relation.relation_id = 'some-endpoint:42' unit4 = mock.MagicMock() @@ -107,9 +113,14 @@ class TestOVSDBLib(test_utils.PatchHelper): self.patch_target('_relations') self._relations.__iter__.return_value = [relation] self.assertEquals( - sorted(self.target.cluster_remote_addrs), + sorted(self.target._remote_addrs('bound-address')), ['1.2.3.4', '[2001:db8::42]']) + def test_cluster_remote_addrs(self): + self.patch_target('_remote_addrs') + self.target.cluster_remote_addrs + self._remote_addrs.assert_called_once_with('bound-address') + def test_db_nb_port(self): self.assertEquals(self.target.db_nb_port, self.target.DB_NB_PORT) diff --git a/unit_tests/test_ovsdb_cms_provides.py b/unit_tests/test_ovsdb_cms_provides.py new file mode 100644 index 0000000..f171669 --- /dev/null +++ b/unit_tests/test_ovsdb_cms_provides.py @@ -0,0 +1,52 @@ +# Copyright 2019 Canonical Ltd +# +# 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 + +from ovsdb_cms import provides + +import charms_openstack.test_utils as test_utils + + +_hook_args = {} + + +class TestOVSDBCMSProvides(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.target = provides.OVSDBCMSProvides('some-relation', []) + self._patches = {} + self._patches_start = {} + + def tearDown(self): + self.target = None + for k, v in self._patches.items(): + v.stop() + setattr(self, k, None) + self._patches = None + self._patches_start = None + + def patch_target(self, attr, return_value=None): + mocked = mock.patch.object(self.target, attr) + self._patches[attr] = mocked + started = mocked.start() + started.return_value = return_value + self._patches_start[attr] = started + setattr(self, attr, started) + + def test_client_remote_addrs(self): + self.patch_target('_remote_addrs') + self.target.client_remote_addrs + self._remote_addrs.assert_called_once_with('cms-client-bound-address') diff --git a/unit_tests/test_ovsdb_cms_requires.py b/unit_tests/test_ovsdb_cms_requires.py new file mode 100644 index 0000000..45bc5dd --- /dev/null +++ b/unit_tests/test_ovsdb_cms_requires.py @@ -0,0 +1,79 @@ +# Copyright 2019 Canonical Ltd +# +# 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 + +from ovsdb_cms import requires + +import charms_openstack.test_utils as test_utils + + +_hook_args = {} + + +class TestOVSDBCMSProvides(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.target = requires.OVSDBCMSRequires('some-relation', []) + self._patches = {} + self._patches_start = {} + + def tearDown(self): + self.target = None + for k, v in self._patches.items(): + v.stop() + setattr(self, k, None) + self._patches = None + self._patches_start = None + + def patch_target(self, attr, return_value=None): + mocked = mock.patch.object(self.target, attr) + self._patches[attr] = mocked + started = mocked.start() + started.return_value = return_value + self._patches_start[attr] = started + setattr(self, attr, started) + + def patch_topublish(self): + self.patch_target('_relations') + relation = mock.MagicMock() + to_publish = mock.PropertyMock() + type(relation).to_publish = to_publish + self._relations.__iter__.return_value = [relation] + return relation.to_publish + + def test_publish_cms_client_bound_addr(self): + self.patch_target('_endpoint_local_bound_addr') + self._endpoint_local_bound_addr.return_value = 'a.b.c.d' + to_publish = self.patch_topublish() + self.target.publish_cms_client_bound_addr() + to_publish.__setitem__.assert_called_once_with( + 'cms-client-bound-address', 'a.b.c.d') + + def test_joined(self): + self.patch_target('publish_cms_client_bound_addr') + self.patch_target('expected_units_available') + self.patch_object(requires.reactive, 'set_flag') + self.expected_units_available.return_value = False + self.target.joined() + self.publish_cms_client_bound_addr.assert_called_once_with() + self.set_flag.assert_called_once_with('some-relation.connected') + self.set_flag.reset_mock() + self.expected_units_available.return_value = True + self.target.joined() + self.set_flag.assert_has_calls([ + mock.call('some-relation.connected'), + mock.call('some-relation.available'), + ])