From 41235cd678981d495d8ef2e11350c8c7bfa17dba Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Thu, 30 Jan 2020 12:27:52 +0100 Subject: [PATCH] Add ovsdb-subordinate interface Interface for use between a subordinate providing payload with OVSDB and a consumnig princple charm. Also fixup interface.yamls in the individual interface subdirectories; repo locations, add subdir key for clarity and remove ignore key as it is not needed. Change-Id: I9eff6c6e71ca314b2c723b4aa7f482f9d297c962 --- src/ovsdb/interface.yaml | 10 +- src/ovsdb_cluster/interface.yaml | 10 +- src/ovsdb_cms/interface.yaml | 10 +- src/ovsdb_subordinate/interface.yaml | 5 + .../ovsdb_subordinate_common.py | 30 ++++ src/ovsdb_subordinate/provides.py | 149 ++++++++++++++++++ src/ovsdb_subordinate/requires.py | 115 ++++++++++++++ unit_tests/test_ovsdb_subordinate_common.py | 42 +++++ unit_tests/test_ovsdb_subordinate_provides.py | 145 +++++++++++++++++ unit_tests/test_ovsdb_subordinate_requires.py | 144 +++++++++++++++++ 10 files changed, 636 insertions(+), 24 deletions(-) create mode 100644 src/ovsdb_subordinate/interface.yaml create mode 100644 src/ovsdb_subordinate/ovsdb_subordinate_common.py create mode 100644 src/ovsdb_subordinate/provides.py create mode 100644 src/ovsdb_subordinate/requires.py create mode 100644 unit_tests/test_ovsdb_subordinate_common.py create mode 100644 unit_tests/test_ovsdb_subordinate_provides.py create mode 100644 unit_tests/test_ovsdb_subordinate_requires.py diff --git a/src/ovsdb/interface.yaml b/src/ovsdb/interface.yaml index 74ac479..4ce0539 100644 --- a/src/ovsdb/interface.yaml +++ b/src/ovsdb/interface.yaml @@ -1,11 +1,5 @@ name: ovsdb summary: Interface for OVSDB maintainer: OpenStack Charmers -repo: https://github.com/openstack-charmers/charm-interface-ovsdb.git -ignore: - - 'unit_tests' - - '.stestr.conf' - - 'test-requirements.txt' - - 'tox.ini' - - '.gitignore' - - '.travis.yml' +repo: https://opendev.org/x/charm-interface-ovsdb.git +subdir: src/ovsdb diff --git a/src/ovsdb_cluster/interface.yaml b/src/ovsdb_cluster/interface.yaml index a35b904..f29702c 100644 --- a/src/ovsdb_cluster/interface.yaml +++ b/src/ovsdb_cluster/interface.yaml @@ -1,11 +1,5 @@ name: ovsdb-cluster summary: Interface for OVSDB maintainer: OpenStack Charmers -repo: https://github.com/openstack-charmers/charm-interface-ovsdb.git -ignore: - - 'unit_tests' - - '.stestr.conf' - - 'test-requirements.txt' - - 'tox.ini' - - '.gitignore' - - '.travis.yml' +repo: https://opendev.org/x/charm-interface-ovsdb.git +subdir: src/ovsdb_cluster diff --git a/src/ovsdb_cms/interface.yaml b/src/ovsdb_cms/interface.yaml index 0502913..4e3d18b 100644 --- a/src/ovsdb_cms/interface.yaml +++ b/src/ovsdb_cms/interface.yaml @@ -1,11 +1,5 @@ name: ovsdb-cms summary: Interface for OVSDB maintainer: OpenStack Charmers -repo: https://github.com/openstack-charmers/charm-interface-ovsdb.git -ignore: - - 'unit_tests' - - '.stestr.conf' - - 'test-requirements.txt' - - 'tox.ini' - - '.gitignore' - - '.travis.yml' +repo: https://opendev.org/x/charm-interface-ovsdb.git +subdir: src/ovsdb_cms diff --git a/src/ovsdb_subordinate/interface.yaml b/src/ovsdb_subordinate/interface.yaml new file mode 100644 index 0000000..3be5455 --- /dev/null +++ b/src/ovsdb_subordinate/interface.yaml @@ -0,0 +1,5 @@ +name: ovsdb-subordinate +summary: Interface for subordinate relation between ovn-chassis and principle +maintainer: OpenStack Charmers +repo: https://opendev.org/x/charm-interface-ovsdb.git +subdir: src/ovsdb_subordinate diff --git a/src/ovsdb_subordinate/ovsdb_subordinate_common.py b/src/ovsdb_subordinate/ovsdb_subordinate_common.py new file mode 100644 index 0000000..63a7908 --- /dev/null +++ b/src/ovsdb_subordinate/ovsdb_subordinate_common.py @@ -0,0 +1,30 @@ +# Copyright 2020 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. +"""Common functions for the ``ovsdb-subordinate`` interface classes""" + +import hashlib + + +def hash_hexdigest(s): + """Hash string using SHA-2 256/224 function and return a hexdigest + + :param s: String data + :type s: str + :returns: hexdigest of hashed data + :rtype: str + :raises: TypeError + """ + if not isinstance(s, str): + raise TypeError + return hashlib.sha224(s.encode('utf8')).hexdigest() diff --git a/src/ovsdb_subordinate/provides.py b/src/ovsdb_subordinate/provides.py new file mode 100644 index 0000000..affd9cf --- /dev/null +++ b/src/ovsdb_subordinate/provides.py @@ -0,0 +1,149 @@ +# Copyright 2020 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 charms.reactive as reactive + +# the reactive framework unfortunately does not grok `import as` in conjunction +# with decorators on class instance methods, so we have to revert to `from ...` +# imports +from charms.reactive import ( + Endpoint, + when, + when_not, +) + +from .ovsdb_subordinate_common import hash_hexdigest + + +class OVSDBSubordinateProvides(Endpoint): + """This interface is used on a principle charm to connect to subordinate + """ + + @property + def chassis_name(self): + """Retrieve chassis-name from relation data + + :returns: Chassis name as provided on subordinate relation + :rtype: str + """ + return self.all_joined_units.received.get('chassis-name', '') + + @property + def ovn_configured(self): + """Retrieve whether OVN is configured from relation data + + :returns: True or False + :rtype: bool + """ + return self.all_joined_units.received.get('ovn-configured', False) + + def _add_interface_request(self, bridge, ifname, ifdata): + """Retrieve interface requests from relation and add/update requests + + :param bridge: Name of bridge + :type bridge: str + :param ifname: Name of interface + :type ifname: str + :param ifdata: Data to be attached to interface in Open vSwitch + :type ifdata: Dict[str,Union[str,Dict[str,str]]] + """ + for relation in self.relations: + relation_ifs = relation.to_publish.get('create-interfaces', {}) + relation_ifs.update({bridge: {ifname: ifdata}}) + relation.to_publish['create-interfaces'] = relation_ifs + + def _interface_requests(self): + """Retrieve interface requests from relation + + :returns: Current interface requests + :rtype: Optional[Dict[str,Union[str,Dict[str,str]]]] + """ + for relation in self.relations: + return relation.to_publish_raw.get('create-interfaces') + + def create_interface(self, bridge, ifname, ethaddr, ifid, + iftype=None, ifstatus=None): + """Request system interface created and attach it to CMS + + Calls to this function are additive so a principle charm can request to + have multiple interfaces created and maintained. + + The flag {endpoint_name}.{interface_name}.created will be set when + ready. + + :param bridge: Bridge the new interface should be created on + :type bridge: str + :param ifname: Interface name we want the new netdev to get + :type ifname: str + :param ethaddr: Ethernet address we want to attach to the netdev + :type ethaddr: str + :param ifid: Unique identifier for port from CMS + :type ifid: str + :param iftype: Interface type, defaults to 'internal' + :type iftype: Optional[str] + :param ifstatus: Interface status, defaults to 'active' + :type ifstatus: Optional[str] + """ + # The keys in the ifdata dictionary map directly to column names in the + # OpenvSwitch Interface table as defined in DB-SCHEMA [0] referenced in + # RFC 7047 [1] + # + # There are some established conventions for keys in the external-ids + # column of various tables, consult the OVS Integration Guide [2] for + # more details. + # + # NOTE(fnordahl): Technically the ``external-ids`` column is called + # ``external_ids`` (with an underscore) and we rely on ``ovs-vsctl``'s + # behaviour of transforming dashes to underscores for us [3] so we can + # have a more pleasant data structure. + # + # 0: http://www.openvswitch.org/ovs-vswitchd.conf.db.5.pdf + # 1: https://tools.ietf.org/html/rfc7047 + # 2: http://docs.openvswitch.org/en/latest/topics/integration/ + # 3: https://github.com/openvswitch/ovs/blob/ + # 20dac08fdcce4b7fda1d07add3b346aa9751cfbc/ + # lib/db-ctl-base.c#L189-L215 + ifdata = { + 'type': iftype or 'internal', + 'external-ids': { + 'iface-id': ifid, + 'iface-status': ifstatus or 'active', + 'attached-mac': ethaddr, + }, + } + self._add_interface_request(bridge, ifname, ifdata) + reactive.clear_flag( + self.expand_name('{endpoint_name}.interfaces.created')) + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + reactive.set_flag(self.expand_name('{endpoint_name}.connected')) + reactive.set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + reactive.clear_flag(self.expand_name('{endpoint_name}.available')) + reactive.clear_flag(self.expand_name('{endpoint_name}.connected')) + + @when('endpoint.{endpoint_name}.changed.interfaces-created') + def new_requests(self): + ifreq = self._interface_requests() + + if ifreq is not None and self.all_joined_units.received[ + 'interfaces-created'] == hash_hexdigest(ifreq): + reactive.set_flag( + self.expand_name('{endpoint_name}.interfaces.created')) + reactive.clear_flag( + self.expand_name( + 'endpoint.{endpoint_name}.changed.interfaces-created')) diff --git a/src/ovsdb_subordinate/requires.py b/src/ovsdb_subordinate/requires.py new file mode 100644 index 0000000..0df3d5e --- /dev/null +++ b/src/ovsdb_subordinate/requires.py @@ -0,0 +1,115 @@ +# Copyright 2020 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 subprocess + +import charms.reactive as reactive + +# the reactive framework unfortunately does not grok `import as` in conjunction +# with decorators on class instance methods, so we have to revert to `from ...` +# imports +from charms.reactive import ( + Endpoint, + when, + when_not, +) + +from .ovsdb_subordinate_common import hash_hexdigest + + +class OVSDBSubordinateRequires(Endpoint): + """This interface is used on the subordinate side of the relation""" + + def _get_ovs_value(self, tbl, col, rec=None): + """Get value of column in record in table + + :param tbl: Name of table + :type tbl: str + :param col: Name of column + :type col: str + :param rec: Record ID + :type rec: Optional[str] + :raises: subprocess.CalledProcessError + """ + cp = subprocess.run(('ovs-vsctl', 'get', tbl, rec or '.', col), + stdout=subprocess.PIPE, + check=True, universal_newlines=True) + return cp.stdout.rstrip().replace('"', '').replace("'", '') + + def publish_chassis_name(self): + """Publish chassis name""" + ovs_hostname = self._get_ovs_value('Open_vSwitch', + 'external_ids:hostname') + for relation in self.relations: + relation.to_publish['chassis-name'] = ovs_hostname + + def publish_ovn_configured(self): + """Publish whether OVN is configured in the local OVSDB""" + ovn_configured = False + try: + self._get_ovs_value('Open_vSwitch', 'external_ids:ovn-remote') + ovn_configured = True + except subprocess.CalledProcessError: + # No OVN + pass + + for relation in self.relations: + relation.to_publish['ovn-configured'] = ovn_configured + + @property + def interface_requests(self): + """Retrieve current interface requests + + :returns: Current interface requests + :rtype: Dict[str,Union[str,Dict[str,str]]] + """ + return self.all_joined_units.received.get('create-interfaces', {}) + + def interface_requests_handled(self): + """Notify peer that interface requests has been dealt with + + Sets a hash of request data back on relation to signal to the other end + it has been dealt with so it can proceed. + + Note that we do not use the reactive request response pattern library + as we do not have use for per-unit granularity and we do not have + actual useful data to return. + """ + # The raw data is a json dump using sorted keys + ifreq_hexdigest = hash_hexdigest( + self.all_joined_units.received_raw['create-interfaces']) + for relation in self.relations: + relation.to_publish['interfaces-created'] = ifreq_hexdigest + reactive.clear_flag( + self.expand_name('{endpoint_name}.interfaces.new_requests')) + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.publish_chassis_name() + self.publish_ovn_configured() + reactive.set_flag(self.expand_name('{endpoint_name}.connected')) + reactive.set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + reactive.clear_flag(self.expand_name('{endpoint_name}.available')) + reactive.clear_flag(self.expand_name('{endpoint_name}.connected')) + + @when('endpoint.{endpoint_name}.changed.create-interfaces') + def new_requests(self): + reactive.set_flag( + self.expand_name('{endpoint_name}.interfaces.new_requests')) + reactive.clear_flag( + self.expand_name( + 'endpoint.{endpoint_name}.changed.create-interfaces')) diff --git a/unit_tests/test_ovsdb_subordinate_common.py b/unit_tests/test_ovsdb_subordinate_common.py new file mode 100644 index 0000000..95bf936 --- /dev/null +++ b/unit_tests/test_ovsdb_subordinate_common.py @@ -0,0 +1,42 @@ +# Copyright 2020 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 hashlib + +from ovsdb_subordinate import ovsdb_subordinate_common as common + +import charms_openstack.test_utils as test_utils + + +class TestOVSDBSubordinateCommon(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self._patches = {} + self._patches_start = {} + + def tearDown(self): + for k, v in self._patches.items(): + v.stop() + setattr(self, k, None) + self._patches = None + self._patches_start = None + + def test_hash_hexdigest(self): + s = 's' + self.assertEquals( + common.hash_hexdigest(s), + hashlib.sha224(s.encode('utf8')).hexdigest()) + with self.assertRaises(TypeError): + common.hash_hexdigest({}) diff --git a/unit_tests/test_ovsdb_subordinate_provides.py b/unit_tests/test_ovsdb_subordinate_provides.py new file mode 100644 index 0000000..c0d7f12 --- /dev/null +++ b/unit_tests/test_ovsdb_subordinate_provides.py @@ -0,0 +1,145 @@ +# Copyright 2020 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_subordinate import provides + +import charms_openstack.test_utils as test_utils + + +_hook_args = {} + + +class TestOVSDBSubordinateProvides(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.target = provides.OVSDBSubordinateProvides('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_chassis_name(self): + self.patch_target('_all_joined_units') + self._all_joined_units.received.get.return_value = 'fakename' + self.assertEquals(self.target.chassis_name, 'fakename') + self._all_joined_units.received.get.assert_called_once_with( + 'chassis-name', '') + + def test_ovn_configured(self): + self.patch_target('_all_joined_units') + self._all_joined_units.received.get.return_value = True + self.assertEquals(self.target.ovn_configured, True) + self._all_joined_units.received.get.assert_called_once_with( + 'ovn-configured', False) + + def test__add_interface_request(self): + to_publish = self.patch_topublish() + to_publish.get.return_value = {} + self.target._add_interface_request('br-ex', 'eth0', {'data': ''}) + to_publish.__setitem__.assert_called_once_with( + 'create-interfaces', {'br-ex': {'eth0': {'data': ''}}}) + + def test__interfrace_requests(self): + self.patch_target('_relations') + relation = mock.MagicMock() + self._relations.__iter__.return_value = [relation] + relation.to_publish_raw.get.return_value = 'aValue' + self.assertEquals(self.target._interface_requests(), 'aValue') + relation.to_publish_raw.get.assert_called_once_with( + 'create-interfaces') + + def test_create_interface(self): + self.patch_target('_add_interface_request') + self.patch_object(provides.reactive, 'clear_flag') + ifdata = { + 'type': 'internal', + 'external-ids': { + 'iface-id': 'fakeuuid', + 'iface-status': 'active', + 'attached-mac': 'fakemac', + }, + } + self.target.create_interface('br-ex', 'eth0', 'fakemac', 'fakeuuid') + self._add_interface_request.assert_called_once_with( + 'br-ex', 'eth0', ifdata) + self.clear_flag.assert_called_once_with( + 'some-relation.interfaces.created') + self._add_interface_request.reset_mock() + ifdata['type'] = 'someothervalue' + ifdata['external-ids']['iface-status'] = 'inactive' + self.target.create_interface('br-ex', 'eth0', 'fakemac', 'fakeuuid', + iftype='someothervalue', + ifstatus='inactive') + self._add_interface_request.assert_called_once_with( + 'br-ex', 'eth0', ifdata) + + def test_joined(self): + self.patch_object(provides.reactive, 'set_flag') + self.target.joined() + self.set_flag.assert_has_calls([ + mock.call('some-relation.connected'), + mock.call('some-relation.available'), + ]) + + def test_broken(self): + self.patch_object(provides.reactive, 'clear_flag') + self.target.broken() + self.clear_flag.assert_has_calls([ + mock.call('some-relation.available'), + mock.call('some-relation.connected'), + ]) + + def test_new_requests(self): + self.patch_target('_interface_requests') + self.patch_target('_all_joined_units') + self.patch_object(provides, 'hash_hexdigest') + self.hash_hexdigest.return_value = 'fakehash' + self._interface_requests.return_value = 'fakerequests' + self.patch_object(provides.reactive, 'set_flag') + self.patch_object(provides.reactive, 'clear_flag') + self.target.new_requests() + self.hash_hexdigest.assert_called_once_with('fakerequests') + self.assertFalse(self.set_flag.called) + self.assertFalse(self.clear_flag.called) + self._all_joined_units.received.__getitem__.return_value = 'fakehash' + self.target.new_requests() + self.set_flag.assert_called_once_with( + 'some-relation.interfaces.created') + self.clear_flag.assert_called_once_with( + 'endpoint.some-relation.changed.interfaces-created') diff --git a/unit_tests/test_ovsdb_subordinate_requires.py b/unit_tests/test_ovsdb_subordinate_requires.py new file mode 100644 index 0000000..0d56c56 --- /dev/null +++ b/unit_tests/test_ovsdb_subordinate_requires.py @@ -0,0 +1,144 @@ +# Copyright 2020 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_subordinate import requires + +import charms_openstack.test_utils as test_utils + + +_hook_args = {} + + +class TestOVSDBSubordinateProvides(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.target = requires.OVSDBSubordinateRequires('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__get_ovs_value(self): + self.patch_object(requires.subprocess, 'run') + cp = mock.MagicMock() + cp.stdout = '"hostname-42"\n' + self.run.return_value = cp + self.assertEquals( + self.target._get_ovs_value('tbl', 'col'), + 'hostname-42') + self.run.assert_called_once_with( + ('ovs-vsctl', 'get', 'tbl', '.', 'col'), + stdout=mock.ANY, check=True, universal_newlines=True) + self.run.reset_mock() + self.target._get_ovs_value('tbl', 'col', rec='rec') + self.run.assert_called_once_with( + ('ovs-vsctl', 'get', 'tbl', 'rec', 'col'), + stdout=mock.ANY, check=True, universal_newlines=True) + + def test_publish_chassis_name(self): + self.patch_target('_get_ovs_value') + to_publish = self.patch_topublish() + self._get_ovs_value.return_value = 'aHostname' + self.target.publish_chassis_name() + to_publish.__setitem__.assert_called_once_with( + 'chassis-name', 'aHostname') + + def test_publish_ovn_configured(self): + self.patch_object(requires, 'subprocess') + self.subprocess.CalledProcessError = Exception + self.patch_target('_get_ovs_value') + to_publish = self.patch_topublish() + self._get_ovs_value.side_effect = Exception + self.target.publish_ovn_configured() + to_publish.__setitem__.assert_called_once_with('ovn-configured', False) + self._get_ovs_value.assert_called_once_with( + 'Open_vSwitch', 'external_ids:ovn-remote') + to_publish.__setitem__.reset_mock() + self._get_ovs_value.side_effect = None + self.target.publish_ovn_configured() + to_publish.__setitem__.assert_called_once_with('ovn-configured', True) + + def test_interface_requests(self): + self.patch_target('_all_joined_units') + self._all_joined_units.received.get.return_value = 'fakereq' + self.assertEquals( + self.target.interface_requests, 'fakereq') + + def test_interface_requests_handled(self): + self.patch_object(requires, 'hash_hexdigest') + self.hash_hexdigest.return_value = 'fakehash' + self.patch_target('_all_joined_units') + self._all_joined_units.received_raw.__getitem__.return_value = 'ifreq' + to_publish = self.patch_topublish() + self.patch_object(requires.reactive, 'clear_flag') + self.target.interface_requests_handled() + self.hash_hexdigest.assert_called_once_with('ifreq') + to_publish.__setitem__.assert_called_once_with( + 'interfaces-created', 'fakehash') + self.clear_flag.assert_called_once_with( + 'some-relation.interfaces.new_requests') + + def test_joined(self): + self.patch_target('publish_chassis_name') + self.patch_target('publish_ovn_configured') + self.patch_object(requires.reactive, 'set_flag') + self.target.joined() + self.publish_chassis_name.assert_called_once_with() + self.publish_ovn_configured.assert_called_once_with() + self.set_flag.assert_has_calls([ + mock.call('some-relation.connected'), + mock.call('some-relation.available'), + ]) + + def test_broken(self): + self.patch_object(requires.reactive, 'clear_flag') + self.target.broken() + self.clear_flag.assert_has_calls([ + mock.call('some-relation.available'), + mock.call('some-relation.connected'), + ]) + + def test_new_requests(self): + self.patch_object(requires.reactive, 'set_flag') + self.patch_object(requires.reactive, 'clear_flag') + self.target.new_requests() + self.set_flag.assert_called_once_with( + 'some-relation.interfaces.new_requests') + self.clear_flag.assert_called_once_with( + 'endpoint.some-relation.changed.create-interfaces')