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
This commit is contained in:
Frode Nordahl 2019-11-20 08:49:09 +01:00
parent 3253c42659
commit 42f8f3c003
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
19 changed files with 95 additions and 922 deletions

View File

@ -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: {}

View File

@ -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

13
src/lib/__init__.py Normal file
View File

@ -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.

View File

@ -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')

View File

@ -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))

View File

@ -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.

View File

@ -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'

2
src/lxd-profile.yaml Normal file
View File

@ -0,0 +1,2 @@
config:
linux.kernel_modules: openvswitch

View File

@ -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)

View File

@ -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 }}

View File

@ -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=

View File

@ -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

View File

@ -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

View File

@ -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"

1
unit_tests/README.md Normal file
View File

@ -0,0 +1 @@
This is not the unit tests you are looking for, take a look at `layer-ovn`.

View File

@ -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

View File

@ -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'),
])

View File

@ -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","","<unknown>",["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","","<unknown>",["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': '<unknown>',
'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')

View File

@ -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')