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 <openstack-charmers@lists.ubuntu.com>
-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 <openstack-charmers@lists.ubuntu.com>
-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 <openstack-charmers@lists.ubuntu.com>
-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 <openstack-charmers@lists.ubuntu.com>
+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')