From 42f8f3c00338aba6b6c91c26eb80e13655e09e09 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Wed, 20 Nov 2019 08:49:09 +0100 Subject: [PATCH] Move chassis code to layer-ovn The two charms ``ovn-chassis`` and ``ovn-dedicated-chassis`` are apart from metadata identical. Move the shared code to layer. Add missing LXD profile. Reasoning behind updates to functional test bundles: The `ovn-central` charm has been changed to disable openvswitch components, and consequently it is no longer suitable as being a principle charm for testing the ``ovn-chassis`` subordinate charm. I chose to use the ``magpie`` charm as a principle charm for the test as we regularily use it in our teams testing already and it is most likely to stay on the bleeding edge wrt. series support. Change-Id: If58c870481876eeac26127ca2459d34b6617cb6e --- src/config.yaml | 68 +----- src/layer.yaml | 3 +- src/lib/__init__.py | 13 ++ src/lib/charm/openstack/ovn_chassis.py | 211 ------------------ src/lib/charm/ovsdb.py | 154 ------------- src/lib/charms/__init__.py | 13 ++ src/lib/charms/ovn_chassis.py | 21 ++ src/lxd-profile.yaml | 2 + src/reactive/ovn_chassis_handlers.py | 77 ++----- .../networking_ovn_metadata_agent.ini | 16 -- src/templates/ovn-host | 12 - src/tests/bundles/bionic.yaml | 5 +- src/tests/bundles/eoan.yaml | 5 +- src/tests/tests.yaml | 2 + unit_tests/README.md | 1 + unit_tests/__init__.py | 6 + .../test_lib_charm_openstack_ovn_chassis.py | 203 ----------------- unit_tests/test_lib_charm_ovsdb.py | 141 ------------ .../test_reactive_ovn_chassis_handlers.py | 64 +----- 19 files changed, 95 insertions(+), 922 deletions(-) create mode 100644 src/lib/__init__.py delete mode 100644 src/lib/charm/openstack/ovn_chassis.py delete mode 100644 src/lib/charm/ovsdb.py create mode 100644 src/lib/charms/__init__.py create mode 100644 src/lib/charms/ovn_chassis.py create mode 100644 src/lxd-profile.yaml delete mode 100644 src/templates/networking_ovn_metadata_agent.ini delete mode 100644 src/templates/ovn-host create mode 100644 unit_tests/README.md delete mode 100644 unit_tests/test_lib_charm_openstack_ovn_chassis.py delete mode 100644 unit_tests/test_lib_charm_ovsdb.py diff --git a/src/config.yaml b/src/config.yaml index 1b13285..a985d46 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -1,67 +1 @@ -options: - interface-bridge-mappings: - type: string - default: - description: > - A space-delimited list of key-value pairs that map a network interface - MAC address or name to a local ovs bridge to which it should be - connected. - - Note: MAC addresses of physical interfaces that belong to a bond will be - resolved to the bond name and the bond will be added to the ovs bridge. - - Bridges referenced here must be mentioned in the `ovn-bridge-mappings` - configuration option. - - If a match is found the bridge will be created if it does not already - exist, the matched interface will be added to it and the mapping found in - `ovn-bridge-mappings` will be added to the local OVSDB under the - `external_ids:ovn-bridge-mappings` key in the Open_vSwitch table. - - An example value mapping two network interface mac address to two ovs - bridges would be: - - 00:00:5e:00:00:42:br-internet enp3s0f0:br-provider - - - Note: OVN gives you distributed East/West and highly available - North/South routing by default. You do not need to add provider networks - for use with external Layer3 connectivity to all chassis. - - Doing so will create a scaling problem at the physical network layer - that needs to be resolved with globally shared Layer2 (does not scale) or - tunneling at the top-of-rack switch layer (adds complexity) and is - generally not a recommended configuration. - - Add provider networks for use with external Layer3 connectivity to - individual chassis located near the datacenter border gateways by adding - the MAC address of the physical interfaces of those units. - ovn-bridge-mappings: - type: string - default: - description: > - A space-delimited list of key-value pairs that map a physical network - name to a local ovs bridge that provides connectivity to that network. - - The physical network name can be referenced when the administrator - programs the OVN logical flows either by talking directly to the - Northbound database or by interfacing with a Cloud Management System - (CMS). - - Each charm unit will evaluate each key-value pair and determine if the - configuration is relevant for the host it is running on based on matches - found in the `interface-bridge-mappings` configuration option. - - If a match is found the bridge will be created if it does not already - exist, the matched interface will be added to it and the mapping will be - added to the local OVSDB under the `external_ids:ovn-bridge-mappings` key - in the Open_vSwitch table. - - An example value mapping two physical network names to two ovs bridges - would be: - - physnet1:br-internet physnet2:br-provider - - NOTE: Values in this configuration option will only have effect for units - that have a interface referenced in the `interface-bridge-mappings` - configuration option. +options: {} diff --git a/src/layer.yaml b/src/layer.yaml index 65e30bf..7f818d6 100644 --- a/src/layer.yaml +++ b/src/layer.yaml @@ -1,5 +1,5 @@ includes: - - layer:openstack + - layer:ovn - interface:ovsdb - interface:neutron-plugin options: @@ -10,6 +10,7 @@ repo: https://github.com/openstack/charm-ovn-controller config: deletes: - debug + - source - ssl_ca - ssl_cert - ssl_key diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/src/lib/charm/openstack/ovn_chassis.py b/src/lib/charm/openstack/ovn_chassis.py deleted file mode 100644 index 82d55b8..0000000 --- a/src/lib/charm/openstack/ovn_chassis.py +++ /dev/null @@ -1,211 +0,0 @@ -import collections -import os -import socket -import subprocess - -import charms.reactive as reactive - -import charmhelpers.core as ch_core -import charmhelpers.contrib.openstack.context as os_context - -import charms_openstack.adapters -import charms_openstack.charm - -import charm.ovsdb as ovsdb - - -OVS_ETCDIR = '/etc/openvswitch' - - -@charms_openstack.adapters.config_property -def ovn_key(cls): - return os.path.join(OVS_ETCDIR, 'key_host') - - -@charms_openstack.adapters.config_property -def ovn_cert(cls): - return os.path.join(OVS_ETCDIR, 'cert_host') - - -@charms_openstack.adapters.config_property -def ovn_ca_cert(cls): - return os.path.join(OVS_ETCDIR, - '{}.crt'.format(cls.charm_instance.name)) - - -class NeutronPluginRelationAdapter( - charms_openstack.adapters.OpenStackRelationAdapter): - - @property - def metadata_shared_secret(self): - return self.relation.get_or_create_shared_secret() - - -class OVNChassisCharmRelationAdapters( - charms_openstack.adapters.OpenStackRelationAdapters): - relation_adapters = { - 'nova_compute': NeutronPluginRelationAdapter, - } - - -class OVNChassisCharm(charms_openstack.charm.OpenStackCharm): - # OpenvSwitch and OVN is distributed as part of the Ubuntu Cloud Archive - # Pockets get their name from OpenStack releases - release = 'train' - package_codenames = { - 'ovn-host': collections.OrderedDict([ - ('2.12', 'train'), - ]), - } - name = 'ovn-chassis' - packages = ['ovn-host'] - services = ['ovn-host'] - adapters_class = OVNChassisCharmRelationAdapters - required_relations = ['certificates', 'ovsdb'] - restart_map = { - '/etc/default/ovn-host': ['ovn-host'], - } - python_version = 3 - - def __init__(self, **kwargs): - if reactive.is_flag_set('charm.ovn-chassis.enable-openstack-metadata'): - metadata_agent = 'networking-ovn-metadata-agent' - self.packages.extend(['networking-ovn-metadata-agent', 'haproxy']) - self.services.append(metadata_agent) - self.restart_map.update({ - '/etc/neutron/' - 'networking_ovn_metadata_agent.ini': [metadata_agent], - }) - super().__init__(**kwargs) - - def run(self, *args): - cp = subprocess.run( - args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True, - universal_newlines=True) - ch_core.hookenv.log(cp, level=ch_core.hookenv.INFO) - - def configure_tls(self, certificates_interface=None): - """Override default handler prepare certs per OVNs taste.""" - # The default handler in ``OpenStackCharm`` class does the CA only - tls_objects = self.get_certs_and_keys( - certificates_interface=certificates_interface) - - for tls_object in tls_objects: - with open(ovn_ca_cert(self.adapters_instance), 'w') as crt: - chain = tls_object.get('chain') - if chain: - crt.write(tls_object['ca'] + os.linesep + chain) - else: - crt.write(tls_object['ca']) - - self.configure_cert(OVS_ETCDIR, - tls_object['cert'], - tls_object['key'], - cn='host') - break - - def configure_ovs(self, ovsdb_interface): - self.run('ovs-vsctl', - 'set-ssl', - ovn_key(self.adapters_instance), - ovn_cert(self.adapters_instance), - ovn_ca_cert(self.adapters_instance)) - self.run('ovs-vsctl', - 'set', 'open', '.', - 'external-ids:ovn-encap-type=geneve', '--', - 'set', 'open', '.', - 'external-ids:ovn-encap-ip={}' - .format(ovsdb_interface.cluster_local_addr), '--', - 'set', 'open', '.', - 'external-ids:system-id={}' - .format( - socket.getfqdn(ovsdb_interface.cluster_local_addr))) - self.run('ovs-vsctl', - 'set', - 'open', - '.', - 'external-ids:ovn-remote={}' - .format(','.join(ovsdb_interface.db_sb_connection_strs))) - self.restart_all() - - def configure_bridges(self): - # we use the resolve_port method of NeutronPortContext to translate - # MAC addresses into interface names - npc = os_context.NeutronPortContext() - - # build map of bridge config with existing interfaces on host - ifbridges = collections.defaultdict(list) - config_ifbm = self.config['interface-bridge-mappings'] or '' - for pair in config_ifbm.split(): - ifname_or_mac, bridge = pair.rsplit(':', 1) - ifbridges[bridge].append(ifname_or_mac) - for br in ifbridges.keys(): - # resolve mac addresses to interface names - ifbridges[br] = npc.resolve_ports(ifbridges[br]) - # remove empty bridges - ifbridges = {k: v for k, v in ifbridges.items() if len(v) > 0} - - # build map of bridges to ovn networks with existing if-mapping on host - # and at the same time build ovn-bridge-mappings string - ovn_br_map_str = '' - ovnbridges = collections.defaultdict(list) - config_obm = self.config['ovn-bridge-mappings'] or '' - for pair in sorted(config_obm.split()): - network, bridge = pair.split(':', 1) - if bridge in ifbridges: - ovnbridges[bridge].append(network) - if ovn_br_map_str: - ovn_br_map_str += ',' - ovn_br_map_str += '{}:{}'.format(network, bridge) - - bridges = ovsdb.SimpleOVSDB('ovs-vsctl', 'bridge') - ports = ovsdb.SimpleOVSDB('ovs-vsctl', 'port') - for bridge in bridges.find('external_ids:charm-ovn-chassis=managed'): - # remove bridges and ports that are managed by us and no longer in - # config - if bridge['name'] not in ifbridges: - ch_core.hookenv.log('removing bridge "{}" as it is no longer' - 'present in configuration for this unit.' - .format(bridge['name']), - level=ch_core.hookenv.DEBUG) - ovsdb.del_br(bridge['name']) - else: - for port in ports.find('external_ids:charm-ovn-chassis={}' - .format(bridge['name'])): - if port['name'] not in ifbridges[bridge['name']]: - ch_core.hookenv.log('removing port "{}" from bridge ' - '"{}" as it is no longer present ' - 'in configuration for this unit.' - .format(port['name'], - bridge['name']), - level=ch_core.hookenv.DEBUG) - ovsdb.del_port(bridge['name'], port['name']) - for br in ifbridges.keys(): - if br not in ovnbridges: - continue - try: - next(bridges.find('name={}'.format(br))) - except StopIteration: - ovsdb.add_br(br, ('charm-ovn-chassis', 'managed')) - else: - ch_core.hookenv.log('skip adding already existing bridge "{}"' - .format(br), level=ch_core.hookenv.DEBUG) - for port in ifbridges[br]: - if port not in ovsdb.list_ports(br): - ovsdb.add_port(br, port, ('charm-ovn-chassis', br)) - else: - ch_core.hookenv.log('skip adding already existing port ' - '"{}" to bridge "{}"' - .format(port, br), - level=ch_core.hookenv.DEBUG) - - opvs = ovsdb.SimpleOVSDB('ovs-vsctl', 'Open_vSwitch') - if ovn_br_map_str: - opvs.set('.', 'external_ids:ovn-bridge-mappings', ovn_br_map_str) - # NOTE(fnordahl): Workaround for LP: #1848757 - opvs.set('.', 'external_ids:ovn-cms-options', - 'enable-chassis-as-gw') - else: - opvs.remove('.', 'external_ids', 'ovn-bridge-mappings') - # NOTE(fnordahl): Workaround for LP: #1848757 - opvs.remove('.', 'external_ids', 'ovn-cms-options') diff --git a/src/lib/charm/ovsdb.py b/src/lib/charm/ovsdb.py deleted file mode 100644 index a49819a..0000000 --- a/src/lib/charm/ovsdb.py +++ /dev/null @@ -1,154 +0,0 @@ -# TODO: much of this code is shared with the ``ovn-dedicated-chassis`` and -# ``ovn-central`` charms and we should move this to a layer or library. -import json -import subprocess - - -def _run(*args): - """Run a process, check result, capture decoded output from STDERR/STDOUT. - - :param args: Command and arguments to run - :type args: Union - :returns: Information about the completed process - :rtype: subprocess.CompletedProcess - :raises subprocess.CalledProcessError - """ - return subprocess.run( - args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True, - universal_newlines=True) - - -def add_br(bridge, external_id=None): - """Add bridge and optionally attach a external_id to bridge. - - :param bridge: Name of bridge to create - :type bridge: str - :param external_id: Key-value pair - :type external_id: Option[None,Union[str,str]] - :raises: subprocess.CalledProcessError - """ - cmd = ['ovs-vsctl', 'add-br', bridge, '--', 'set', 'bridge', bridge, - 'protocols=OpenFlow13'] - if external_id: - cmd.extend(('--', 'br-set-external-id', bridge)) - cmd.extend(external_id) - _run(*cmd) - - -def del_br(bridge): - """Remove bridge. - - :param bridge: Name of bridge to remove - :type bridge: str - :raises: subprocess.CalledProcessError - """ - _run('ovs-vsctl', 'del-br', bridge) - - -def add_port(bridge, port, external_id=None): - """Add port to bridge and optionally attach a external_id to port. - - :param bridge: Name of bridge to attach port to - :type bridge: str - :param port: Name of port as represented in netdev - :type port: str - :param external_id: Key-value pair - :type external_id: Option[None,Union[str,str]] - :raises: subprocess.CalledProcessError - """ - _run('ip', 'link', 'set', port, 'up') - _run('ovs-vsctl', 'add-port', bridge, port) - if external_id: - ports = SimpleOVSDB('ovs-vsctl', 'port') - for port in ports.find('name={}'.format(port)): - ports.set(port['_uuid'], - 'external_ids:{}'.format(external_id[0]), - external_id[1]) - - -def del_port(bridge, port): - """Remove port from bridge. - - :param bridge: Name of bridge to remove port from - :type bridge: str - :param port: Name of port to remove - :type port: str - :raises: subprocess.CalledProcessError - """ - _run('ovs-vsctl', 'del-port', bridge, port) - - -def list_ports(bridge): - """List ports on a bridge. - - :param bridge: Name of bridge to list ports on - :type bridge: str - :returns: List of ports - :rtype: List - """ - cp = _run('ovs-vsctl', 'list-ports', bridge) - return cp.stdout.splitlines() - - -class SimpleOVSDB(object): - """Simple interface to OVSDB through the use of command line tools. - - OVS and OVN is managed through a set of databases. These databases have - similar command line tools to manage them. We make use of the similarity - to provide a generic class that can be used to manage them. - - The OpenvSwitch project does provide a Python API, but on the surface it - appears to be a bit too involved for our simple use case. - - Examples: - chassis = SimpleOVSDB('ovn-sbctl', 'chassis') - for chs in chassis: - print(chs) - - bridges = SimpleOVSDB('ovs-vsctl', 'bridge') - for br in bridges: - if br['name'] == 'br-test': - bridges.set(br['uuid'], 'external_ids:charm', 'managed') - """ - - def __init__(self, tool, table): - """SimpleOVSDB constructor - - :param tool: Which tool with database commands to operate on. - Usually one of `ovs-vsctl`, `ovn-nbctl`, `ovn-sbctl` - :type tool: str - :param table: Which table to operate on - :type table: str - """ - self.tool = tool - self.tbl = table - - def _find_tbl(self, condition=None): - cmd = [self.tool, '-f', 'json', 'find', self.tbl] - if condition: - cmd.append(condition) - cp = _run(*cmd) - data = json.loads(cp.stdout) - for row in data['data']: - values = [] - for col in row: - if isinstance(col, list): - values.append(col[1]) - else: - values.append(col) - yield dict(zip(data['headings'], values)) - - def __iter__(self): - return self._find_tbl() - - def clear(self, rec, col): - _run(self.tool, 'clear', self.tbl, rec, col) - - def find(self, condition): - return self._find_tbl(condition=condition) - - def remove(self, rec, col, value): - _run(self.tool, 'remove', self.tbl, rec, col, value) - - def set(self, rec, col, value): - _run(self.tool, 'set', self.tbl, rec, '{}={}'.format(col, value)) diff --git a/src/lib/charms/__init__.py b/src/lib/charms/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/charms/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/src/lib/charms/ovn_chassis.py b/src/lib/charms/ovn_chassis.py new file mode 100644 index 0000000..5b6e416 --- /dev/null +++ b/src/lib/charms/ovn_chassis.py @@ -0,0 +1,21 @@ +# 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 charms.ovn_charm + + +class OVNChassisCharm(charms.ovn_charm.BaseOVNChassisCharm): + # OpenvSwitch and OVN is distributed as part of the Ubuntu Cloud Archive + # Pockets get their name from OpenStack releases + release = 'train' + name = 'ovn-chassis' diff --git a/src/lxd-profile.yaml b/src/lxd-profile.yaml new file mode 100644 index 0000000..044e653 --- /dev/null +++ b/src/lxd-profile.yaml @@ -0,0 +1,2 @@ +config: + linux.kernel_modules: openvswitch diff --git a/src/reactive/ovn_chassis_handlers.py b/src/reactive/ovn_chassis_handlers.py index 45e170c..ed302ab 100644 --- a/src/reactive/ovn_chassis_handlers.py +++ b/src/reactive/ovn_chassis_handlers.py @@ -1,63 +1,22 @@ -import charmhelpers.core as ch_core - +# 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 charms.reactive as reactive -import charms_openstack.bus -import charms_openstack.charm as charm +from . import ovn_chassis_charm_handlers -charms_openstack.bus.discover() - -# Use the charms.openstack defaults for common states and hooks -charm.use_defaults( - 'charm.installed', - 'config.changed', - 'update-status', - 'upgrade-charm', - 'certificates.available', -) - - -@reactive.when_not('nova-compute.connected') -def disable_metadata(): - reactive.clear_flag('charm.ovn-chassis.enable-openstack-metadata') - - -@reactive.when('nova-compute.connected') -def enable_metadata(): - reactive.set_flag('charm.ovn-chassis.enable-openstack-metadata') - nova_compute = reactive.endpoint_from_flag('nova-compute.connected') - nova_compute.publish_shared_secret() - with charm.provide_charm_instance() as charm_instance: - charm_instance.install() - charm_instance.assess_status() - - -@reactive.when('charm.installed') -@reactive.when_any('config.changed.ovn-bridge-mappings', - 'config.changed.interface-bridge-mappings', - 'run-default-upgrade-charm') -def configure_bridges(): - with charm.provide_charm_instance() as charm_instance: - charm_instance.configure_bridges() - reactive.clear_flag('config.changed.ovn-bridge-mappings') - reactive.clear_flag('config.changed.interface-bridge-mappings') - charm_instance.assess_status() - - -@reactive.when('ovsdb.available') -def configure_ovs(): - ovsdb = reactive.endpoint_from_flag('ovsdb.available') - with charm.provide_charm_instance() as charm_instance: - ch_core.hookenv.log( - 'DEBUG: {} {} {} {}' - .format(charm_instance, - charm_instance.packages, - charm_instance.services, - charm_instance.restart_map), - level=ch_core.hookenv.INFO) - charm_instance.render_with_interfaces( - charm.optional_interfaces((ovsdb,), - 'nova-compute.connected')) - charm_instance.configure_ovs(ovsdb) - charm_instance.assess_status() +@reactive.when_not(ovn_chassis_charm_handlers.OVN_CHASSIS_ENABLE_HANDLERS_FLAG) +def enable_ovn_chassis_handlers(): + reactive.set_flag( + ovn_chassis_charm_handlers.OVN_CHASSIS_ENABLE_HANDLERS_FLAG) diff --git a/src/templates/networking_ovn_metadata_agent.ini b/src/templates/networking_ovn_metadata_agent.ini deleted file mode 100644 index e13d420..0000000 --- a/src/templates/networking_ovn_metadata_agent.ini +++ /dev/null @@ -1,16 +0,0 @@ -############################################################################### -# [ WARNING ] -# Configuration file maintained by Juju. Local changes may be overwritten. -# Configuration managed by neutron-openvswitch charm -############################################################################### -[DEFAULT] -metadata_proxy_shared_secret={{ nova_compute.metadata_shared_secret }} - -[ovs] -ovsdb_connection=unix:/var/run/openvswitch/db.sock - -[ovn] -ovn_sb_connection={{ ','.join(ovsdb.db_sb_connection_strs) }} -ovn_sb_private_key={{ options.ovn_key }} -ovn_sb_certificate={{ options.ovn_cert }} -ovn_sb_ca_cert={{ options.ovn_ca_cert }} diff --git a/src/templates/ovn-host b/src/templates/ovn-host deleted file mode 100644 index 4d91fc5..0000000 --- a/src/templates/ovn-host +++ /dev/null @@ -1,12 +0,0 @@ -# This is a POSIX shell fragment -*- sh -*- - -############################################################################### -# [ WARNING ] -# Configuration file maintained by Juju. Local changes may be overwritten. -# Configuration managed by neutron-openvswitch charm -############################################################################### - -# OVN_CTL_OPTS: Extra options to pass to ovs-ctl. This is, for example, -# a suitable place to specify --ovn-controller-wrapper=valgrind. -# OVN_CTL_OPTS= - diff --git a/src/tests/bundles/bionic.yaml b/src/tests/bundles/bionic.yaml index bf925d0..5755b46 100644 --- a/src/tests/bundles/bionic.yaml +++ b/src/tests/bundles/bionic.yaml @@ -4,7 +4,7 @@ relations: - mysql:shared-db - - ovn-central:certificates - vault:certificates -- - ovn-central:juju-info +- - magpie:juju-info - ovn-chassis:juju-info - - ovn-chassis:ovsdb - ovn-central:ovsdb @@ -22,6 +22,9 @@ applications: num_units: 3 options: source: cloud:bionic-train + magpie: + charm: cs:~admcleod/magpie + num_units: 2 ovn-chassis: series: bionic charm: cs:~openstack-charmers-next/ovn-chassis diff --git a/src/tests/bundles/eoan.yaml b/src/tests/bundles/eoan.yaml index 40f2614..760ba9a 100644 --- a/src/tests/bundles/eoan.yaml +++ b/src/tests/bundles/eoan.yaml @@ -4,7 +4,7 @@ relations: - mysql:shared-db - - ovn-central:certificates - vault:certificates -- - ovn-central:juju-info +- - magpie:juju-info - ovn-chassis:juju-info - - ovn-chassis:ovsdb - ovn-central:ovsdb @@ -20,6 +20,9 @@ applications: ovn-central: charm: cs:~openstack-charmers-next/ovn-central num_units: 3 + magpie: + charm: cs:~admcleod/magpie + num_units: 2 ovn-chassis: series: eoan charm: cs:~openstack-charmers-next/ovn-chassis diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml index b08e6cc..917e3a7 100644 --- a/src/tests/tests.yaml +++ b/src/tests/tests.yaml @@ -5,6 +5,8 @@ gate_bundles: smoke_bundles: - bionic target_deploy_status: + magpie: + workload-status-message: icmp ok ovn-central: workload-status: blocked workload-status-message: "'certificates' missing" diff --git a/unit_tests/README.md b/unit_tests/README.md new file mode 100644 index 0000000..96d169d --- /dev/null +++ b/unit_tests/README.md @@ -0,0 +1 @@ +This is not the unit tests you are looking for, take a look at `layer-ovn`. diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index cc7e9a5..5408a9d 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -37,3 +37,9 @@ sys.modules['neutronclient'] = neutronclient sys.modules['neutronclient.v2_0'] = neutronclient.v2_0 sys.modules['neutron_lib'] = neutron_lib sys.modules['neutron_lib.constants'] = neutron_lib.constants +import reactive +reactive.ovn_chassis_charm_handlers = mock.MagicMock() +reactive.ovn_chassis_charm_handlers.OVN_CHASSIS_ENABLE_HANDLERS_FLAG = \ + 'MOCKED_FLAG' +sys.modules['reactive.ovn_chassis_charm_handlers'] = \ + reactive.ovn_chassis_charm_handlers diff --git a/unit_tests/test_lib_charm_openstack_ovn_chassis.py b/unit_tests/test_lib_charm_openstack_ovn_chassis.py deleted file mode 100644 index 944f66a..0000000 --- a/unit_tests/test_lib_charm_openstack_ovn_chassis.py +++ /dev/null @@ -1,203 +0,0 @@ -# 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 io -import mock -import os - -import charms_openstack.test_utils as test_utils - -import charm.openstack.ovn_chassis as ovn_chassis - - -class TestOVNConfigProperties(test_utils.PatchHelper): - - def test_ovn_key(self): - self.assertEquals(ovn_chassis.ovn_key(None), - os.path.join(ovn_chassis.OVS_ETCDIR, 'key_host')) - - def test_ovn_cert(self): - self.assertEquals(ovn_chassis.ovn_cert(None), - os.path.join(ovn_chassis.OVS_ETCDIR, 'cert_host')) - - def test_ovn_ca_cert(self): - cls = mock.MagicMock() - cls.charm_instance.name = mock.PropertyMock().return_value = 'name' - self.assertEquals(ovn_chassis.ovn_ca_cert(cls), - os.path.join(ovn_chassis.OVS_ETCDIR, 'name.crt')) - - -class Helper(test_utils.PatchHelper): - - def setUp(self): - super().setUp() - self.patch_release(ovn_chassis.OVNChassisCharm.release) - self.patch_object(ovn_chassis.reactive, 'is_flag_set', - return_value=False) - self.target = ovn_chassis.OVNChassisCharm() - # remove the 'is_flag_set' patch so the tests can use it - self._patches['is_flag_set'].stop() - setattr(self, 'is_flag_set', None) - del(self._patches['is_flag_set']) - del(self._patches_start['is_flag_set']) - - 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) - - -class TestOVNChassisCharm(Helper): - - def test_optional_openstack_metadata(self): - self.assertEquals(self.target.packages, ['ovn-host']) - self.assertEquals(self.target.services, ['ovn-host']) - self.patch_object(ovn_chassis.reactive, 'is_flag_set', - return_value=True) - c = ovn_chassis.OVNChassisCharm() - self.assertEquals(c.packages, [ - 'ovn-host', 'networking-ovn-metadata-agent', 'haproxy' - ]) - self.assertEquals(c.services, [ - 'ovn-host', 'networking-ovn-metadata-agent']) - - def test_run(self): - self.patch_object(ovn_chassis.subprocess, 'run') - self.patch_object(ovn_chassis.ch_core.hookenv, 'log') - self.target.run('some', 'args') - self.run.assert_called_once_with( - ('some', 'args'), - stdout=ovn_chassis.subprocess.PIPE, - stderr=ovn_chassis.subprocess.STDOUT, - check=True, - universal_newlines=True) - - def test_configure_tls(self): - self.patch_target('get_certs_and_keys') - self.get_certs_and_keys.return_value = [{ - 'cert': 'fakecert', - 'key': 'fakekey', - 'cn': 'fakecn', - 'ca': 'fakeca', - 'chain': 'fakechain', - }] - with mock.patch('builtins.open', create=True) as mocked_open: - mocked_file = mock.MagicMock(spec=io.FileIO) - mocked_open.return_value = mocked_file - self.target.configure_cert = mock.MagicMock() - self.target.run = mock.MagicMock() - self.target.configure_tls() - mocked_open.assert_called_once_with( - '/etc/openvswitch/ovn-chassis.crt', 'w') - mocked_file.__enter__().write.assert_called_once_with( - 'fakeca\nfakechain') - self.target.configure_cert.assert_called_once_with( - ovn_chassis.OVS_ETCDIR, - 'fakecert', - 'fakekey', - cn='host') - - def test_configure_ovs(self): - self.patch_target('run') - self.patch_target('restart_all') - self.patch_object(ovn_chassis, 'ovn_key') - self.patch_object(ovn_chassis, 'ovn_cert') - self.patch_object(ovn_chassis, 'ovn_ca_cert') - ovsdb_interface = mock.MagicMock() - db_sb_connection_strs = mock.PropertyMock().return_value = ['dbsbconn'] - ovsdb_interface.db_sb_connection_strs = db_sb_connection_strs - cluster_local_addr = mock.PropertyMock().return_value = ( - 'cluster_local_addr') - ovsdb_interface.cluster_local_addr = cluster_local_addr - self.target.configure_ovs(ovsdb_interface) - self.run.assert_has_calls([ - mock.call('ovs-vsctl', 'set-ssl', mock.ANY, mock.ANY, mock.ANY), - mock.call('ovs-vsctl', 'set', 'open', '.', - 'external-ids:ovn-encap-type=geneve', '--', - 'set', 'open', '.', - 'external-ids:ovn-encap-ip=cluster_local_addr', '--', - 'set', 'open', '.', - 'external-ids:system-id=cluster_local_addr'), - mock.call('ovs-vsctl', 'set', 'open', '.', - 'external-ids:ovn-remote=dbsbconn'), - ]) - - def test_configure_bridges(self): - self.patch_object(ovn_chassis.os_context, 'NeutronPortContext') - npc = mock.MagicMock() - - def _fake_resolve_ports(mac_or_if): - result = [] - for entry in mac_or_if: - if ':' in entry: - result.append('eth0') - continue - result.append(entry) - return result - - npc.resolve_ports.side_effect = _fake_resolve_ports - self.NeutronPortContext.return_value = npc - self.patch_target('config') - self.config.__getitem__.side_effect = [ - '00:01:02:03:04:05:br-provider eth5:br-other', - 'provider:br-provider other:br-other'] - self.patch_object(ovn_chassis.ovsdb, 'SimpleOVSDB') - bridges = mock.MagicMock() - bridges.find.side_effect = [ - [ - {'name': 'delete-bridge'}, - {'name': 'br-other'} - ], - StopIteration, - ] - ports = mock.MagicMock() - ports.find.side_effect = [[{'name': 'delete-port'}]] - opvs = mock.MagicMock() - self.SimpleOVSDB.side_effect = [bridges, ports, opvs] - self.patch_object(ovn_chassis.ovsdb, 'del_br') - self.patch_object(ovn_chassis.ovsdb, 'del_port') - self.patch_object(ovn_chassis.ovsdb, 'add_br') - self.patch_object(ovn_chassis.ovsdb, 'list_ports') - self.list_ports().__iter__.return_value = [] - self.patch_object(ovn_chassis.ovsdb, 'add_port') - self.target.configure_bridges() - npc.resolve_ports.assert_has_calls([ - mock.call(['00:01:02:03:04:05']), - mock.call(['eth5']), - ], any_order=True) - bridges.find.assert_has_calls([ - mock.call('name=br-provider'), - mock.call('name=br-other'), - ], any_order=True) - self.del_br.assert_called_once_with('delete-bridge') - self.del_port.assert_called_once_with('br-other', 'delete-port') - self.add_br.assert_has_calls([ - mock.call('br-provider', ('charm-ovn-chassis', 'managed')), - mock.call('br-other', ('charm-ovn-chassis', 'managed')), - ], any_order=True) - self.add_port.assert_has_calls([ - mock.call( - 'br-provider', 'eth0', ('charm-ovn-chassis', 'br-provider')), - mock.call( - 'br-other', 'eth5', ('charm-ovn-chassis', 'br-other')), - ], any_order=True) - opvs.set.assert_has_calls([ - mock.call('.', 'external_ids:ovn-bridge-mappings', - 'other:br-other,provider:br-provider'), - mock.call('.', 'external_ids:ovn-cms-options', - 'enable-chassis-as-gw'), - ]) diff --git a/unit_tests/test_lib_charm_ovsdb.py b/unit_tests/test_lib_charm_ovsdb.py deleted file mode 100644 index a95f25d..0000000 --- a/unit_tests/test_lib_charm_ovsdb.py +++ /dev/null @@ -1,141 +0,0 @@ -import mock -import subprocess - -import charms_openstack.test_utils as test_utils - -import charm.ovsdb as ovsdb - -VSCTL_BRIDGE_TBL = ''' -{"data":[[["uuid","1e21ba48-61ff-4b32-b35e-cb80411da351"],["set",[]],["set",[]],"0000a0369fdd3890","","",["map",[["charm-ovn-chassis","managed"],["other","value"]]],["set",[]],["set",[]],["map",[]],["set",[]],false,["set",[]],"br-test",["set",[]],["map",[]],["set",[["uuid","617f9359-77e2-41be-8af6-4c44e7a6bcc3"],["uuid","da840476-8809-4107-8733-591f4696f056"]]],["set",[]],false,["map",[]],["set",[]],["map",[]],false],[["uuid","bb685b0f-a383-40a1-b7a5-b5c2066bfa42"],["set",[]],["set",[]],"00000e5b68bba140","","",["map",[]],"secure",["set",[]],["map",[]],["set",[]],false,["set",[]],"br-int",["set",[]],["map",[["disable-in-band","true"]]],["set",[["uuid","07f4c231-9fd2-49b0-a558-5b69d657fdb0"],["uuid","8bbd2441-866f-4317-a284-09491702776c"],["uuid","d9e9c081-6482-4006-b7d6-239182b56c2e"]]],["set",[]],false,["map",[]],["set",[]],["map",[]],false]],"headings":["_uuid","auto_attach","controller","datapath_id","datapath_type","datapath_version","external_ids","fail_mode","flood_vlans","flow_tables","ipfix","mcast_snooping_enable","mirrors","name","netflow","other_config","ports","protocols","rstp_enable","rstp_status","sflow","status","stp_enable"]} -''' - - -class TestOVSDB(test_utils.PatchHelper): - - def test__run(self): - self.patch_object(ovsdb.subprocess, 'run') - self.run.return_value = 'aReturn' - self.assertEquals(ovsdb._run('aArg'), 'aReturn') - self.run.assert_called_once_with( - ('aArg',), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - check=True, universal_newlines=True) - - def test_add_br(self): - self.patch_object(ovsdb, '_run') - ovsdb.add_br('br-x') - self._run.assert_called_once_with( - 'ovs-vsctl', 'add-br', 'br-x', '--', 'set', 'bridge', 'br-x', - 'protocols=OpenFlow13') - self._run.reset_mock() - ovsdb.add_br('br-x', ('charm', 'managed')) - self._run.assert_called_once_with( - 'ovs-vsctl', 'add-br', 'br-x', '--', 'set', 'bridge', 'br-x', - 'protocols=OpenFlow13', '--', - 'br-set-external-id', 'br-x', 'charm', 'managed') - - def test_del_br(self): - self.patch_object(ovsdb, '_run') - ovsdb.del_br('br-x') - self._run.assert_called_once_with( - 'ovs-vsctl', 'del-br', 'br-x') - - def test_add_port(self): - self.patch_object(ovsdb, '_run') - ovsdb.add_port('br-x', 'enp3s0f0') - self._run.assert_has_calls([ - mock.call('ip', 'link', 'set', 'enp3s0f0', 'up'), - mock.call('ovs-vsctl', 'add-port', 'br-x', 'enp3s0f0'), - ]) - - def test_list_ports(self): - self.patch_object(ovsdb, '_run') - ovsdb.list_ports('someBridge') - self._run.assert_called_once_with('ovs-vsctl', 'list-ports', - 'someBridge') - - -class Helper(test_utils.PatchHelper): - - 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) - - -class TestSimpleOVSDB(Helper): - - def setUp(self): - super().setUp() - self.target = ovsdb.SimpleOVSDB('atool', 'atable') - - def test__find_tbl(self): - self.patch_object(ovsdb, '_run') - cp = mock.MagicMock() - cp.stdout = mock.PropertyMock().return_value = VSCTL_BRIDGE_TBL - self._run.return_value = cp - self.maxDiff = None - expect = { - '_uuid': '1e21ba48-61ff-4b32-b35e-cb80411da351', - 'auto_attach': [], - 'controller': [], - 'datapath_id': '0000a0369fdd3890', - 'datapath_type': '', - 'datapath_version': '', - 'external_ids': [['charm-ovn-chassis', 'managed'], - ['other', 'value']], - 'fail_mode': [], - 'flood_vlans': [], - 'flow_tables': [], - 'ipfix': [], - 'mcast_snooping_enable': False, - 'mirrors': [], - 'name': 'br-test', - 'netflow': [], - 'other_config': [], - 'ports': [['uuid', '617f9359-77e2-41be-8af6-4c44e7a6bcc3'], - ['uuid', 'da840476-8809-4107-8733-591f4696f056']], - 'protocols': [], - 'rstp_enable': False, - 'rstp_status': [], - 'sflow': [], - 'status': [], - 'stp_enable': False} - # this in effect also tests the __iter__ front end method - for el in self.target: - self.assertDictEqual(el, expect) - break - self._run.assert_called_once_with( - 'atool', '-f', 'json', 'find', 'atable') - self._run.reset_mock() - # this in effect also tests the find front end method - for el in self.target.find(condition='name=br-test'): - break - self._run.assert_called_once_with( - 'atool', '-f', 'json', 'find', 'atable', 'name=br-test') - - def test_clear(self): - self.patch_object(ovsdb, '_run') - self.target.clear('1e21ba48-61ff-4b32-b35e-cb80411da351', - 'external_ids') - self._run.assert_called_once_with( - 'atool', 'clear', 'atable', - '1e21ba48-61ff-4b32-b35e-cb80411da351', 'external_ids') - - def test_remove(self): - self.patch_object(ovsdb, '_run') - self.target.remove('1e21ba48-61ff-4b32-b35e-cb80411da351', - 'external_ids', 'other') - self._run.assert_called_once_with( - 'atool', 'remove', 'atable', - '1e21ba48-61ff-4b32-b35e-cb80411da351', 'external_ids', 'other') - - def test_set(self): - self.patch_object(ovsdb, '_run') - self.target.set('1e21ba48-61ff-4b32-b35e-cb80411da351', - 'external_ids:other', 'value') - self._run.assert_called_once_with( - 'atool', 'set', 'atable', - '1e21ba48-61ff-4b32-b35e-cb80411da351', 'external_ids:other=value') diff --git a/unit_tests/test_reactive_ovn_chassis_handlers.py b/unit_tests/test_reactive_ovn_chassis_handlers.py index 09eb16a..0a4726d 100644 --- a/unit_tests/test_reactive_ovn_chassis_handlers.py +++ b/unit_tests/test_reactive_ovn_chassis_handlers.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock - import reactive.ovn_chassis_handlers as handlers import charms_openstack.test_utils as test_utils @@ -21,69 +19,23 @@ import charms_openstack.test_utils as test_utils class TestRegisteredHooks(test_utils.TestRegisteredHooks): + def setUp(self): + super().setUp() + def test_hooks(self): - defaults = [ - 'charm.installed', - 'config.changed', - 'update-status', - 'upgrade-charm', - 'certificates.available', - ] hook_set = { - 'when': { - 'configure_ovs': ('ovsdb.available',), - 'enable_metadata': ('nova-compute.connected',), - 'configure_bridges': ('charm.installed',), - }, 'when_not': { - 'disable_metadata': ('nova-compute.connected',), - }, - 'when_any': { - 'configure_bridges': ( - 'config.changed.ovn-bridge-mappings', - 'config.changed.interface-bridge-mappings', - 'run-default-upgrade-charm',), + 'enable_ovn_chassis_handlers': ('MOCKED_FLAG',), }, } # test that the hooks were registered via the # reactive.ovn_handlers - self.registered_hooks_test_helper(handlers, hook_set, defaults) + self.registered_hooks_test_helper(handlers, hook_set, {}) class TestOvnHandlers(test_utils.PatchHelper): - def setUp(self): - super().setUp() - self.charm = mock.MagicMock() - self.patch_object(handlers.charm, 'provide_charm_instance', - new=mock.MagicMock()) - self.provide_charm_instance().__enter__.return_value = \ - self.charm - self.provide_charm_instance().__exit__.return_value = None - - def test_disable_metadata(self): - self.patch_object(handlers.reactive, 'clear_flag') - handlers.disable_metadata() - self.clear_flag.assert_called_once_with( - 'charm.ovn-chassis.enable-openstack-metadata') - - def test_enable_metadata(self): - self.patch_object(handlers.reactive, 'endpoint_from_flag') + def test_enable_ovn_chassis_handlers(self): self.patch_object(handlers.reactive, 'set_flag') - nova_compute = mock.MagicMock() - self.endpoint_from_flag.return_value = nova_compute - handlers.enable_metadata() - self.set_flag.assert_called_once_with( - 'charm.ovn-chassis.enable-openstack-metadata') - nova_compute.publish_shared_secret.assert_called_once_with() - self.charm.install.assert_called_once_with() - self.charm.assess_status.assert_called_once_with() - - def configure_ovs(self): - self.patch_object(handlers.reactive, 'endpoint_from_flag') - ovsdb = mock.MagicMock() - self.endpoint_from_flag.return_value = ovsdb - self.charm.render_with_interfaces.assert_called_once_with( - self.charm.optional_interfaces((ovsdb,), 'nova-compute.connected')) - self.charm.configure_ovs.assert_called_once_with(ovsdb) - self.charm.assess_status.assert_called_once_with() + handlers.enable_ovn_chassis_handlers() + self.set_flag.assert_called_once_with('MOCKED_FLAG')