Add initial support for local OVN cluster instead of kernel-networking

This patch adds support for configuring a local (in-node) OVN cluster
to do the actions that were being done by the kernel networking.
This way, things like ovs-dpdk or HWOL could be supported.

This also includes adaptation to the tox.ini coverage so that
test files are not considered. This was making coveraged artificially
higher that it was, so this patch is also adjusting the minimun
coverage accordingly.

Missing items to be handled in follow up patch sets:
- Documentation
- Support for IPv6
- Support for BFD
- Support for vlan provider networks

Change-Id: Ieff19d88a22d7267bd58f8d1d4ecbcdc668c6bfb
This commit is contained in:
Luis Tomas Bolivar 2023-04-26 08:41:15 +02:00
parent 629c6dff34
commit 938cdc2643
17 changed files with 1354 additions and 82 deletions

View File

@ -66,6 +66,11 @@ agent_opts = [
default='/etc/ipa/ca.crt',
help='The PEM file with CA certificate that OVN should use to'
' verify certificates presented to it by SSL peers'),
cfg.StrOpt('ovn_sb_connection',
default='',
help='The connection string for the OVN_Southbound OVSDB.\n'
'Use tcp:IP:PORT for TCP connection.\n'
'Use unix:FILE for unix domain socket connection.'),
cfg.StrOpt('ovn_nb_private_key',
default='/etc/pki/tls/private/ovn_bgp_agent.key',
help='The PEM file with private key for SSL connection to '
@ -78,6 +83,11 @@ agent_opts = [
default='/etc/ipa/ca.crt',
help='The PEM file with CA certificate that OVN should use to'
' verify certificates presented to it by SSL peers'),
cfg.StrOpt('ovn_nb_connection',
default='',
help='The connection string for the OVN_Northbound OVSDB.\n'
'Use tcp:IP:PORT for TCP connection.\n'
'Use unix:FILE for unix domain socket connection.'),
cfg.StrOpt('bgp_AS',
default='64999',
help='AS number to be used by the Agent when running in BGP '
@ -173,6 +183,25 @@ https://docs.openstack.org/oslo.rootwrap/latest/user/usage.html#daemon-mode
""")),
]
local_ovn_cluster_opts = [
cfg.StrOpt('ovn_nb_connection',
default='unix:/var/run/ovn/ovnnb_db.sock',
help='The connection string for the OVN_Northbound OVSDB.\n'
'Use tcp:IP:PORT for TCP connection.\n'
'Use unix:FILE for unix domain socket connection.'),
cfg.ListOpt('external_nics',
default=[],
help='List of NICS that the local OVN cluster needs to be '
'connected to for the external connectivity.'),
cfg.ListOpt('peer_ips',
default=[],
help='List of peer IPs used for redirecting the outgoing '
'traffic (ECMP supported).'),
cfg.ListOpt('provider_networks_pool_prefixes',
default=['192.168.0.0/16'],
help='List of prefixes for provider networks'),
]
CONF = cfg.CONF
EXTRA_LOG_LEVEL_DEFAULTS = [
'oslo.privsep.daemon=INFO'
@ -183,7 +212,8 @@ logging.register_options(CONF)
def register_opts():
CONF.register_opts(agent_opts)
CONF.register_opts(root_helper_opts, "AGENT")
CONF.register_opts(root_helper_opts, "agent")
CONF.register_opts(local_ovn_cluster_opts, "local_ovn_cluster")
def init(args, **kwargs):
@ -198,7 +228,7 @@ def setup_logging():
def get_root_helper(conf):
return conf.AGENT.root_helper
return conf.agent.root_helper
def setup_privsep():
@ -208,5 +238,6 @@ def setup_privsep():
def list_opts():
return [
("DEFAULT", agent_opts),
("AGENT", root_helper_opts)
("agent", root_helper_opts),
("ovn", local_ovn_cluster_opts),
]

View File

@ -77,3 +77,15 @@ EXPOSE_METHOD_L2VNI = 'l2vni'
EXPOSE_METHOD_VRF = 'vrf'
EXPOSE_METHOD_OVN = 'ovn'
EXPOSE_METHOD_DYNAMIC = 'dynamic'
# OVN Cluster related constants
OVN_CLUSTER_BRIDGE = 'bgp'
OVN_CLUSTER_ROUTER = 'bgp-router'
OVN_CLUSTER_ROUTER_INTERNAL_MAC = '40:44:00:00:00:06'
# FIXME(ltomasbo): This can be removed once ovsdbapp version is >=1.13.0
POLICY_ACTION_REROUTE = 'reroute'
POLICY_ACTION_TYPES = (POLICY_ACTION_REROUTE)
LR_POLICY_PRIORITY_MAX = 32767
ROUTE_DISCARD = 'discard'

View File

@ -34,7 +34,12 @@ LOG = logging.getLogger(__name__)
# LOG.setLevel(logging.DEBUG)
# logging.basicConfig(level=logging.DEBUG)
OVN_TABLES = ["Logical_Switch_Port", "NAT", "Logical_Switch"]
OVN_TABLES = ['Logical_Switch_Port', 'NAT', 'Logical_Switch']
LOCAL_CLUSTER_OVN_TABLES = ['Logical_Switch', 'Logical_Switch_Port',
'Logical_Router', 'Logical_Router_Port',
'Logical_Router_Policy',
'Logical_Router_Static_Route', 'Gateway_Chassis',
'Static_MAC_Binding']
class NBOVNBGPDriver(driver_api.AgentDriverBase):
@ -47,6 +52,7 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
self._init_vars()
self._nb_idl = None
self._local_nb_idl = None
self._post_start_event = threading.Event()
@property
@ -55,10 +61,20 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
self._post_start_event.wait()
return self._nb_idl
@property
def local_nb_idl(self):
if not self._local_nb_idl:
self._post_start_event.wait()
return self._local_nb_idl
@nb_idl.setter
def nb_idl(self, val):
self._nb_idl = val
@local_nb_idl.setter
def local_nb_idl(self, val):
self._local_nb_idl = val
def _init_vars(self):
self.ovn_bridge_mappings = {} # {'public': 'br-ex'}
self.ovs_flows = {}
@ -83,8 +99,7 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
self.chassis = self.ovs_idl.get_own_chassis_name()
# NOTE(ltomasbo): remote should point to NB DB port instead of SB DB,
# so changing 6642 by 6641
self.ovn_remote = self.ovs_idl.get_ovn_remote().replace(":6642",
":6641")
self.ovn_remote = self.ovs_idl.get_ovn_remote(nb=True)
LOG.info("Loaded chassis %s.", self.chassis)
LOG.info("Starting VRF configuration for advertising routes")
@ -110,6 +125,15 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
self.ovn_remote,
tables=OVN_TABLES,
events=events).start()
# if local OVN cluster, gets an idl for it
if CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
self.local_nb_idl = ovn.OvnNbIdl(
CONF.local_ovn_cluster.ovn_nb_connection,
tables=LOCAL_CLUSTER_OVN_TABLES,
events=[],
leader_only=True).start()
# Now IDL connections can be safely used
self._post_start_event.set()
@ -136,13 +160,11 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
self._init_vars()
LOG.debug("Configuring default wiring for each provider network")
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings()
# Apply base configuration for each bridge
self.ovn_bridge_mappings, self.ovs_flows = (
wire_utils.ensure_base_wiring_config(self.nb_idl, bridge_mappings,
self.ovn_routing_tables))
wire_utils.ensure_base_wiring_config(
self.nb_idl, self.ovs_idl, ovn_idl=self.local_nb_idl,
routing_tables=self.ovn_routing_tables))
LOG.debug("Syncing current routes.")
# add missing routes/ips for IPs on provider network
@ -164,10 +186,11 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
def _ensure_port_exposed(self, port):
port_fip = port.external_ids.get(constants.OVN_FIP_EXT_ID_KEY)
if port_fip:
external_ip, ls_name = self.get_port_external_ip_and_ls(port.name)
external_ip, external_mac, ls_name = (
self.get_port_external_ip_and_ls(port.name))
if not external_ip or not ls_name:
return
return self._expose_fip(external_ip, ls_name, port)
return self._expose_fip(external_ip, external_mac, ls_name, port)
logical_switch = port.external_ids.get(
constants.OVN_LS_NAME_EXT_ID_KEY)
@ -194,18 +217,21 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
'bridge_vlan': bridge_vlan,
'localnet': localnet}
ips = port.addresses[0].strip().split(' ')[1:]
self._expose_ip(ips, logical_switch, bridge_device, bridge_vlan,
mac = port.addresses[0].strip().split(' ')[0]
self._expose_ip(ips, mac, logical_switch, bridge_device, bridge_vlan,
port.type, port.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY))
def _expose_provider_port(self, port_ips, logical_switch, bridge_device,
bridge_vlan, localnet, proxy_cidrs=None):
def _expose_provider_port(self, port_ips, mac, logical_switch,
bridge_device, bridge_vlan, localnet,
proxy_cidrs=None):
# Connect to OVN
try:
if wire_utils.wire_provider_port(
self.ovn_routing_tables_routes, self.ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet,
self.ovn_routing_tables, proxy_cidrs):
self.ovn_routing_tables, proxy_cidrs, mac=mac,
ovn_idl=self.local_nb_idl):
# Expose the IP now that it is connected
bgp_utils.announce_ips(port_ips)
for ip in port_ips:
@ -226,7 +252,8 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
try:
wire_utils.unwire_provider_port(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables, proxy_cidrs)
bridge_vlan, self.ovn_routing_tables, proxy_cidrs,
ovn_idl=self.local_nb_idl)
except Exception as e:
LOG.exception("Unexpected exception while unwiring provider port: "
"%s", e)
@ -239,7 +266,7 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
bridge_vlan = None
network_name = localnet.options.get('network_name')
if network_name:
bridge_device = self.ovn_bridge_mappings[network_name]
bridge_device = self.ovn_bridge_mappings.get(network_name)
if localnet.tag:
bridge_vlan = localnet.tag[0]
return bridge_device, bridge_vlan
@ -274,11 +301,13 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
'bridge_device': bridge_device,
'bridge_vlan': bridge_vlan,
'localnet': localnet}
return self._expose_ip(ips, logical_switch, bridge_device, bridge_vlan,
port_type=row.type, cidr=row.external_ids.get(
mac = row.addresses[0].strip().split(' ')[0]
return self._expose_ip(ips, mac, logical_switch, bridge_device,
bridge_vlan, port_type=row.type,
cidr=row.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY))
def _expose_ip(self, ips, logical_switch, bridge_device, bridge_vlan,
def _expose_ip(self, ips, mac, logical_switch, bridge_device, bridge_vlan,
port_type, cidr):
LOG.debug("Adding BGP route for logical port with ip %s", ips)
localnet = self.ovn_provider_ls[logical_switch]['localnet']
@ -287,13 +316,13 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
# NOTE: For Amphora Load Balancer with IPv6 VIP on the provider
# network, we need a NDP Proxy so that the traffic from the
# amphora can properly be redirected back
if not self._expose_provider_port(ips, logical_switch,
if not self._expose_provider_port(ips, mac, logical_switch,
bridge_device, bridge_vlan,
localnet, [cidr]):
return []
else:
if not self._expose_provider_port(ips, logical_switch,
if not self._expose_provider_port(ips, mac, logical_switch,
bridge_device, bridge_vlan,
localnet):
return []
@ -352,15 +381,16 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
def get_port_external_ip_and_ls(self, port):
nat_entry = self.nb_idl.get_nat_by_logical_port(port)
if not nat_entry:
return
return None, None, None
net_id = nat_entry.external_ids.get(constants.OVN_FIP_NET_EXT_ID_KEY)
if not net_id:
return nat_entry.external_ip, None
return nat_entry.external_ip, nat_entry.external_mac, None
else:
return nat_entry.external_ip, "neutron-{}".format(net_id)
ls_name = "neutron-{}".format(net_id)
return nat_entry.external_ip, nat_entry.external_mac, ls_name
@lockutils.synchronized('nbbgp')
def expose_fip(self, ip, logical_switch, row):
def expose_fip(self, ip, mac, logical_switch, row):
'''Advertice BGP route by adding IP to device.
This methods ensures BGP advertises the FIP associated to a VM in a
@ -372,9 +402,9 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
VRF), and adds the IP of:
- VM FIP
'''
return self._expose_fip(ip, logical_switch, row)
return self._expose_fip(ip, mac, logical_switch, row)
def _expose_fip(self, ip, logical_switch, row):
def _expose_fip(self, ip, mac, logical_switch, row):
localnet, bridge_device, bridge_vlan = self._get_ls_localnet_info(
logical_switch)
if not bridge_device:
@ -394,7 +424,7 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
return
self.ovn_tenant_ls[tenant_logical_switch] = True
LOG.debug("Adding BGP route for FIP with ip %s", ip)
if not self._expose_provider_port([ip], tenant_logical_switch,
if not self._expose_provider_port([ip], mac, tenant_logical_switch,
bridge_device, bridge_vlan,
localnet):
return False

View File

@ -354,7 +354,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase):
if wire_utils.wire_provider_port(
self.ovn_routing_tables_routes, self.ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet,
self.ovn_routing_tables, proxy_cidrs, lladdr):
self.ovn_routing_tables, proxy_cidrs, lladdr=lladdr):
# Expose the IP now that it is connected
bgp_utils.announce_ips(port_ips)
return True
@ -428,7 +428,8 @@ class OVNBGPDriver(driver_api.AgentDriverBase):
try:
return wire_utils.unwire_provider_port(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables, proxy_cidrs, lladdr)
bridge_vlan, self.ovn_routing_tables, proxy_cidrs,
lladdr=lladdr)
except Exception as e:
LOG.exception("Unexpected exception while unwiring provider port: "
"%s", e)

View File

@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import netaddr
from oslo_config import cfg
from oslo_log import log as logging
@ -51,7 +53,8 @@ class OvnDbNotifyHandler(event.RowEventHandler):
class OvnNbIdl(OvnIdl):
SCHEMA = 'OVN_Northbound'
def __init__(self, connection_string, events=None, tables=None):
def __init__(self, connection_string, events=None, tables=None,
leader_only=False):
if connection_string.startswith("ssl"):
self._check_and_set_ssl_files(self.SCHEMA)
helper = self._get_ovsdb_helper(connection_string)
@ -61,7 +64,7 @@ class OvnNbIdl(OvnIdl):
for table in tables:
helper.register_table(table)
super(OvnNbIdl, self).__init__(
None, connection_string, helper, leader_only=False)
None, connection_string, helper, leader_only=leader_only)
def _get_ovsdb_helper(self, connection_string):
return idlutils.get_schema_helper(connection_string, self.SCHEMA)
@ -176,6 +179,200 @@ class LSGetLocalnetPortsCommand(command.ReadOnlyCommand):
raise RuntimeError(msg) from e
# FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0
class _LrpNetworksCommand(command.BaseCommand):
table = 'Logical_Router_Port'
def __init__(self, api, port, networks, exists):
super().__init__(api)
self.port = port
self.exists = exists
if isinstance(networks, (str, bytes)):
networks = [networks]
self.networks = [str(netaddr.IPNetwork(network))
for network in networks]
# FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0
class LrpAddNetworksCommand(_LrpNetworksCommand):
def run_idl(self, txn):
lrp = self.api.lookup(self.table, self.port)
for network in self.networks:
if network in lrp.networks and not self.exists:
msg = "Network '%s' already exist in networks of port %s" % (
network, lrp.uuid)
raise RuntimeError(msg)
lrp.addvalue('networks', network)
# FIXME(ltomasbo): This can be removed once ovsdbapp supports it
class LrRouteAddCommand(command.BaseCommand):
def __init__(self, api, router, prefix, nexthop, port=None,
policy='dst-ip', ecmp=False, may_exist=False):
prefix = str(netaddr.IPNetwork(prefix))
if nexthop != constants.ROUTE_DISCARD:
nexthop = str(netaddr.IPAddress(nexthop))
super().__init__(api)
self.router = router
self.prefix = prefix
self.nexthop = nexthop
self.port = port
self.policy = policy
self.ecmp = ecmp
self.may_exist = may_exist
def run_idl(self, txn):
lr = self.api.lookup('Logical_Router', self.router)
for route in lr.static_routes:
if self.prefix == route.ip_prefix:
if self.ecmp and self.nexthop != route.nexthop:
continue
if not self.may_exist:
msg = "Route %s already exists on router %s" % (
self.prefix, self.router)
raise RuntimeError(msg)
route.nexthop = self.nexthop
route.policy = self.policy
if self.port:
route.output_port = self.port
self.result = rowview.RowView(route)
return
route = txn.insert(self.api.tables['Logical_Router_Static_Route'])
route.ip_prefix = self.prefix
route.nexthop = self.nexthop
route.policy = self.policy
if self.port:
route.output_port = self.port
lr.addvalue('static_routes', route)
self.result = route.uuid
# FIXME(ltomasbo): This can be removed once ovsdbapp supports it
class LrRouteDelCommand(command.BaseCommand):
def __init__(self, api, router,
prefix=None, nexthop=None, if_exists=False):
if prefix is not None:
prefix = str(netaddr.IPNetwork(prefix))
super().__init__(api)
self.router = router
self.prefix = prefix
self.nexthop = nexthop
self.if_exists = if_exists
def run_idl(self, txn):
lr = self.api.lookup('Logical_Router', self.router)
if not self.prefix:
lr.static_routes = []
return
for route in lr.static_routes:
if self.prefix == route.ip_prefix:
if self.nexthop and route.nexthop != self.nexthop:
continue
lr.delvalue('static_routes', route)
return
if not self.if_exists:
msg = "Route for %s in router %s does not exist" % (
self.prefix, self.router)
raise RuntimeError(msg)
class StaticMACBindingFindCommand(command.DbFindCommand):
table = 'Static_MAC_Binding'
def __init__(self, api, port, ip):
super().__init__(
api,
self.table,
('logical_port', '=', port),
('ip', '=', ip),
row=True,
)
# FIXME(ltomasbo): This can be removed once ovsdbapp supports it
class StaticMACBindingAddCommand(command.AddCommand):
table_name = 'Static_MAC_Binding'
def __init__(self, api, port, ip, mac, override_dynamic_mac=False,
may_exist=False, **columns):
super().__init__(api)
self.port = port
self.ip = ip
self.mac = mac
self.override_dynamic_mac = override_dynamic_mac
self.may_exist = may_exist
self.columns = columns
def run_idl(self, txn):
cmd = StaticMACBindingFindCommand(self.api, self.port, self.ip)
cmd.run_idl(txn)
static_mac_binding_result = cmd.result
if static_mac_binding_result:
if len(static_mac_binding_result) > 1:
# With the current database schema, this cannot happen, but
# better safe than sorry.
raise RuntimeError(
"Unexpected duplicates in database for port %s "
"and ip %s" % (self.port, self.ip))
binding = static_mac_binding_result[0]
if self.may_exist:
# When no changes are made to a record, the parent
# `post_commit` method will not be called.
#
# Ensure consistent return to caller of `Command.execute()`
# even when no changes have been applied.
self.result = rowview.RowView(binding)
return
else:
raise RuntimeError(
"Static MAC Binding entry for port %s and ip %s exists" % (
self.port, self.ip))
binding = txn.insert(self.api.tables[self.table_name])
binding.logical_port = self.port
binding.ip = self.ip
binding.mac = self.mac
binding.override_dynamic_mac = self.override_dynamic_mac
self.set_columns(binding, **self.columns)
# Setting the result to something other than a :class:`rowview.RowView`
# or :class:`ovs.db.idl.Row` typed value will make the parent
# `post_commit` method retrieve the newly insterted row from IDL and
# return that to the caller.
self.result = binding.uuid
# FIXME(ltomasbo): This can be removed once ovsdbapp supports it
class StaticMACBindingDelCommand(command.BaseCommand):
table_name = 'Static_MAC_Binding'
def __init__(self, api, port, ip, if_exists=False, **columns):
super().__init__(api)
self.port = port
self.ip = ip
self.if_exists = if_exists
self.columns = columns
def run_idl(self, txn):
cmd = StaticMACBindingFindCommand(self.api, self.port, self.ip)
cmd.run_idl(txn)
static_mac_binding_result = cmd.result
if static_mac_binding_result:
if len(static_mac_binding_result) > 1:
# With the current database schema, this cannot happen, but
# better safe than sorry.
raise RuntimeError(
"Unexpected duplicates in database for port %s "
"and ip %s" % (self.port, self.ip))
binding = static_mac_binding_result[0]
binding.delete()
return
if self.if_exists:
return
else:
raise RuntimeError(
"Static MAC Binding entry for port %s and ip %s does not "
"exist" % (self.port, self.ip))
class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def __init__(self, connection):
super(OvsdbNbOvnIdl, self).__init__(connection)
@ -231,6 +428,26 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
return LSGetLocalnetPortsCommand(self, logical_switch,
if_exists=if_exists)
# FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0
def lrp_add_networks(self, port, networks, may_exist=False):
return LrpAddNetworksCommand(self, port, networks, may_exist)
def lr_route_add(self, router, prefix, nexthop, port=None,
policy='dst-ip', ecmp=False, may_exist=False):
return LrRouteAddCommand(self, router, prefix, nexthop, port,
policy, ecmp, may_exist)
def lr_route_del(self, router, prefix=None, nexthop=None, if_exists=False):
return LrRouteDelCommand(self, router, prefix, nexthop, if_exists)
def static_mac_binding_add(self, port, ip, mac, override_dynamic_mac=False,
may_exist=False):
return StaticMACBindingAddCommand(self, port, ip, mac,
override_dynamic_mac, may_exist)
def static_mac_binding_del(self, port, ip, if_exists=False):
return StaticMACBindingDelCommand(self, port, ip, if_exists)
class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
def __init__(self, connection):

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from oslo_log import log as logging
from ovs.db import idl
from ovsdbapp.backend.ovs_idl import connection
@ -24,6 +25,7 @@ from ovn_bgp_agent import exceptions as agent_exc
import ovn_bgp_agent.privileged.ovs_vsctl
from ovn_bgp_agent.utils import linux_net
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -52,12 +54,17 @@ def get_device_port_at_ovs(device):
'ovs-vsctl', ['get', 'Interface', device, 'ofport'])[0].rstrip()
def get_ovs_patch_ports_info(bridge):
in_ports = []
def get_ovs_ports_info(bridge):
ovs_ports = ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd(
'ovs-vsctl', ['list-ports', bridge])[0].rstrip()
for ovs_port in ovs_ports.split("\n"):
if ovs_port.startswith('patch-provnet-'):
return ovs_ports.split("\n")
def get_ovs_patch_ports_info(bridge, prefix='patch-provnet-'):
in_ports = []
ovs_ports = get_ovs_ports_info(bridge)
for ovs_port in ovs_ports:
if ovs_port.startswith(prefix):
ovs_ofport = get_device_port_at_ovs(ovs_port)
in_ports.append(ovs_ofport)
return in_ports
@ -133,6 +140,11 @@ def remove_extra_ovs_flows(ovs_flows, bridge, cookie):
'ovs-ofctl', ['del-flows', bridge, del_flow])
def ensure_flow(bridge, flow):
ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd(
'ovs-ofctl', ['add-flow', bridge, flow])
def ensure_evpn_ovs_flow(bridge, cookie, mac, output_port, port_dst, net,
strip_vlan=False):
ovs_port = _find_ovs_port(bridge)
@ -282,20 +294,27 @@ class OvsIdl(object):
"""
return self._get_from_ext_ids('hostname')
def get_ovn_remote(self):
def get_ovn_remote(self, nb=False):
"""Return the external_ids:ovn-remote value of the Open_vSwitch table.
"""
return self._get_from_ext_ids('ovn-remote')
if nb:
return (CONF.ovn_nb_connection if CONF.ovn_nb_connection
else self._get_from_ext_ids('ovn-nb-remote'))
return (CONF.ovn_sb_connection if CONF.ovn_sb_connection
else self._get_from_ext_ids('ovn-remote'))
def get_ovn_bridge_mappings(self):
def get_ovn_bridge_mappings(self, bridge=None):
"""Return a list of bridge mappings
Return a list of bridge mappings based on the
external_ids:ovn-bridge-mappings value of the Open_vSwitch table.
"""
key = 'ovn-bridge-mappings'
if bridge:
key = key + '-' + str(bridge)
try:
return [i.strip() for i in
self._get_from_ext_ids('ovn-bridge-mappings').split(',')]
self._get_from_ext_ids(key).split(',')]
except KeyError:
return []

View File

@ -26,15 +26,18 @@ CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def ensure_base_wiring_config(idl, bridge_mappings, routing_tables):
def ensure_base_wiring_config(idl, ovs_idl, ovn_idl=None, routing_tables={}):
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
return _ensure_base_wiring_config_underlay(idl, bridge_mappings,
return _ensure_base_wiring_config_underlay(idl, ovs_idl,
routing_tables)
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
raise NotImplementedError()
return _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl)
def _ensure_base_wiring_config_underlay(idl, bridge_mappings, routing_tables):
def _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables):
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
bridge_mappings = ovs_idl.get_ovn_bridge_mappings()
ovn_bridge_mappings = {}
flows_info = {}
for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1):
@ -65,6 +68,308 @@ def _ensure_base_wiring_config_underlay(idl, bridge_mappings, routing_tables):
return ovn_bridge_mappings, flows_info
def _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl):
"""Base configuration for extra OVN cluster instead of kernel networking
This function is in charge of the steps to ensure the in-node OVN cluster
is properly configured by:
1. Get the information about the OpenStack provider bridge(s) and the flows
info, such as the mac and the in_port
2. Add the egress ovs flows so that the destination mac is the one on the
extra ovn-cluster, in the LR
3. Create the LR in the in-node OVN cluster
4. Create the LR policy in the in-node OVN cluster to redirect the traffic
(with ECMP support) to the nexthops
5. Create the LR routes in the in-node OVN cluster to route any traffic the
peers IPs
6. Create the LS (+ Localnet port) for the connection between the router
and the OpenStack OVN networks. then it connects it to the LR
7. Create the LS (+ Localnet port) for the connection between the router
and the external network. Then it connects it to the LR
8. Create the ingress_flow at the external OVN provider bridges to
redirect the needed traffic to the in-cluster OVN networks
:param ovs_idl: The idl to communicate with local ovs DB
:param ovn_idl: The idl to communicate with local (in-node) NB DB
:return: ovn_bridge_mappings (network and bridges association) and the
flows_info per bridge
"""
# OpenStack Egress part
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
bridge_mappings = ovs_idl.get_ovn_bridge_mappings()
ovn_bridge_mappings = {}
flows_info = {}
for bridge_mapping in bridge_mappings:
network, bridge = helpers.parse_bridge_mapping(bridge_mapping)
if not network:
continue
ovn_bridge_mappings[network] = bridge
if not flows_info.get(bridge):
mac = linux_net.get_interface_address(bridge)
flows_info[bridge] = {'mac': mac, 'in_port': set([])}
flows_info[bridge]['in_port'] = ovs.get_ovs_patch_ports_info(
bridge)
_ensure_egress_flows(bridge, flows_info[bridge]['in_port'])
# Extra OVN cluster configuration
provider_cidrs = CONF.local_ovn_cluster.provider_networks_pool_prefixes
# LR
cmds = []
cmds.extend(_ensure_ovn_router(ovn_idl))
# FIXME(ltomasbo): we need to firsts create the router and then the
# policies and routes in a different transaction until ovsdbapp
# allows it to do it in one transaction. Once that happen the next
# 2 lines can be removed
_execute_commands(ovn_idl, cmds)
cmds = []
cmds.extend(_ensure_ovn_policies(ovn_idl, CONF.local_ovn_cluster.peer_ips))
cmds.extend(_ensure_ovn_routes(ovn_idl, CONF.local_ovn_cluster.peer_ips))
# Creation of all router related cmds in a single transaction
_execute_commands(ovn_idl, cmds)
# LS
bgp_bridge_mappings = ovs_idl.get_ovn_bridge_mappings(
bridge=constants.OVN_CLUSTER_BRIDGE)
for bridge_mapping in bgp_bridge_mappings:
network, bridge = helpers.parse_bridge_mapping(bridge_mapping)
if not network:
continue
# Create LS + Localnet port on it
_ensure_ovn_switch(ovn_idl, network)
# differentiate between internal LS (connecting to OpenStack)
# and external LS (connecting to the NICs)
if bridge in ovn_bridge_mappings.values():
# Internal Bridge connecting to OpenStack OVN cluster
_ensure_ovn_network_link(ovn_idl, network, 'internal',
provider_cidrs=provider_cidrs)
else:
ip, mac = linux_net.get_nic_info(bridge)
# External Bridge connecting to the external networks
_ensure_ovn_network_link(ovn_idl, network, 'external',
ip=ip, mac=mac)
_ensure_ingress_flows(bridge, mac, network, provider_cidrs)
return ovn_bridge_mappings, flows_info
def _ensure_ovn_router(ovn_idl):
return [ovn_idl.lr_add(constants.OVN_CLUSTER_ROUTER, may_exist=True)]
def _ensure_ovn_switch(ovn_idl, switch_name):
ovn_idl.ls_add(switch_name, may_exist=True).execute(check_error=True)
# Add localnet port to them
localnet_port = "{}-localnet".format(switch_name)
options = {'network_name': switch_name}
cmds = _ensure_lsp_cmds(ovn_idl, localnet_port, switch_name, 'localnet',
'unknown', **options)
_execute_commands(ovn_idl, cmds)
def _execute_commands(idl, cmds):
with idl.transaction(check_error=True) as txn:
for command in cmds:
txn.add(command)
def _ensure_ovn_network_link(ovn_idl, switch_name, direction,
provider_cidrs=None, ip=None, mac=None):
"""Base configuration for connecting LR and LSs
This function is in charge of connecting the LR to the external or internal
LS
For the internal LS it configures:
1. Creates LRP to connect to the internal switch
2. If networks (provider_cidrs) are different, adding the new networks
3. Create LSP related to the LRP with the right options, including the
arp_proxy
4. Bind the LRP to the local chassis
For the external LS it configures:
1. Creates LRP to connect to the external switch
2. If networks (ip) is different than the nic network add the nic network
and remove the extra ones
3. Create LSP related to the LRP with the right options
:param ovn_idl: The idl to communicate with local (in-node) NB DB
:param switch_name: the name of the logical switch to configure
:param direction: can be 'internal' or 'external'
:param provider_cidrs (optional): CIDRs to configure the networks of the
LRP, as well as to configure the ARP
proxy on the internal LSP
(only for the internal)
:param ip (optional): IP to configure in the LRP connected to the external
switch (only for the external)
:param mac (optional): MAC to configure in the LRP connected to the
external switch (only for the external)
"""
# It accepts 2 values for direction: internal or external
cmds = []
if direction == 'internal':
# Connect BGP router to the internal logical switch
r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
try:
ovn_idl.lrp_add(constants.OVN_CLUSTER_ROUTER, r_port_name,
constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC,
provider_cidrs, peer=[], may_exist=True).execute(
check_error=True)
except RuntimeError as rte:
# TODO(ltomasbo): Change OVSDBAPP to return a different error for
# this to avoid having to compare strings as this is error prone
networks_message = 'with different networks'
if networks_message not in str(rte):
raise
# Trying to sync the networks by adding them
cmds.append(ovn_idl.lrp_add_networks(r_port_name, provider_cidrs,
may_exist=True))
s_port_name = "openstack-{}".format(constants.OVN_CLUSTER_ROUTER)
# NOTE(ltomasbo): require v23.06.0 so that proxy-arp works as expected.
# If older version the provider_cidrs should contain all the provider
# network cidrs, pointing to the gateway IP of the network.
cidrs = ','.join(provider_cidrs) if provider_cidrs else '0.0.0.0/0'
options = {'router-port': r_port_name, 'arp_proxy': cidrs}
cmds.extend(_ensure_lsp_cmds(ovn_idl, s_port_name, switch_name,
'router', 'router', **options))
# bind to local chassis
# ovn-nbctl lrp-set-gateway-chassis bgp-router-public bgp 1
cmds.append(ovn_idl.lrp_set_gateway_chassis(
r_port_name, constants.OVN_CLUSTER_BRIDGE, 1))
else: # direction == 'external'
# Connect BGP router to the external logical switch
r_port_name = "{}-{}".format(constants.OVN_CLUSTER_ROUTER, switch_name)
# LRP
try:
ovn_idl.lrp_add(constants.OVN_CLUSTER_ROUTER, r_port_name,
mac, [ip], peer=[], may_exist=True).execute(
check_error=True)
except RuntimeError as rte:
# TODO(ltomasbo): Change OVSDBAPP to return a different error for
# this to avoid having to compare strings as this is error prone
networks_message = 'with different networks'
if networks_message not in str(rte):
raise
# Trying to sync the networks by adding them
cmds.append(ovn_idl.lrp_add_networks(r_port_name,
[ip],
may_exist=True))
lrp = ovn_idl.lrp_get(r_port_name).execute(check_error=True)
for net in lrp.networks:
if net != ip:
cmds.append(ovn_idl.lrp_del_networks(r_port_name,
[net],
if_exists=True))
# LSP
s_port_name = "{}-{}".format(switch_name, constants.OVN_CLUSTER_ROUTER)
options = {'router-port': r_port_name}
cmds.extend(_ensure_lsp_cmds(ovn_idl, s_port_name, switch_name,
'router', 'router', **options))
if cmds:
_execute_commands(ovn_idl, cmds)
def _ensure_lsp_cmds(ovn_idl, port_name, switch, port_type, addresses,
**options):
cmds = []
cmds.append(ovn_idl.lsp_add(switch, port_name, may_exist=True))
cmds.append(ovn_idl.lsp_set_type(port_name, port_type))
cmds.append(ovn_idl.lsp_set_addresses(port_name,
addresses=[addresses]))
cmds.append(ovn_idl.lsp_set_options(port_name, **options))
return cmds
def _ensure_ovn_policies(ovn_idl, next_hops):
priority = 10
match = 'inport=="{}-openstack"'.format(constants.OVN_CLUSTER_ROUTER)
action = 'reroute'
columns = {}
if len(next_hops) > 1:
columns = {'nexthops': next_hops}
elif len(next_hops) == 1:
columns = {'nexthop': next_hops[0]}
return [ovn_idl.lr_policy_add(constants.OVN_CLUSTER_ROUTER, priority,
match, action, may_exist=True, **columns)]
def _ensure_ovn_routes(ovn_idl, peer_ips):
prefix = '0.0.0.0/0'
cmds = []
for ip in peer_ips:
cmds.append(ovn_idl.lr_route_add(constants.OVN_CLUSTER_ROUTER, prefix,
ip, ecmp=True, may_exist=True))
return cmds
def _ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs):
# incomming traffic flows
# patch=`ovs-ofctl show br-ex | grep patch | cut -d "(" -f1 | xargs`
# ovs-ofctl add-flow br-ex
# "cookie=0xbadcaf2,ip,nw_dst=$PROVIDER_NET,in_port=enp2s0,priority=100,
# actions=mod_dl_dst:$ENP2S0_MAC,output=$patch"
if not provider_cidrs:
return
patch_port_prefix = 'patch-{}-'.format(switch_name)
patch_ports = ovs.get_ovs_patch_ports_info(bridge,
prefix=patch_port_prefix)
if not patch_ports:
return
bridge_ports = set(ovs.get_ovs_ports_info(bridge))
external_nic = list(bridge_ports.intersection(
set(CONF.local_ovn_cluster.external_nics)))
if not external_nic:
LOG.warning("NIC ports (%s) not found for bridge %s. Not possible to "
"create the ingress flows. It will be retried if "
"reconcile cycle is not disabled",
CONF.local_ovn_cluster.external_nics, bridge)
return
else:
# only one external_nic expected per bridge
external_nic = external_nic[0]
for provider_cidr in provider_cidrs:
ip_version = linux_net.get_ip_version(provider_cidr)
if ip_version == constants.IP_VERSION_6:
ip = 'ipv6'
else:
ip = 'ip'
flow = (
"cookie={},priority=1000,{},nw_dst={},in_port={},"
" actions=mod_dl_dst:{},output={}".format(
constants.OVS_RULE_COOKIE, ip, provider_cidr, external_nic,
mac, patch_ports[0]))
ovs.ensure_flow(bridge, flow)
def _ensure_egress_flows(bridge, patch_ports):
# outcomming traffic flows
# patch=`ovs-ofctl show br-provider | grep patch | grep provnet |
# cut -d "(" -f1 | xargs`
# ovs-ofctl add-flow br-provider "cookie=0xbadcaf3,ip,in_port=$patch,
# actions=mod_dl_dst:$ROUTER_MAC,NORMAL"
for patch_port in patch_ports:
flow = (
"cookie={},priority=1000,ip,in_port={},"
" actions=mod_dl_dst:{},NORMAL".format(
constants.OVS_RULE_COOKIE, patch_port,
constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC))
flow_v6 = (
"cookie={},priority=1000,ipv6,in_port={},"
" actions=mod_dl_dst:{},NORMAL".format(
constants.OVS_RULE_COOKIE, patch_port,
constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC))
ovs.ensure_flow(bridge, flow)
ovs.ensure_flow(bridge, flow_v6)
def cleanup_wiring(idl, bridge_mappings, ovs_flows, exposed_ips,
routing_tables, routing_tables_routes):
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
@ -72,7 +377,8 @@ def cleanup_wiring(idl, bridge_mappings, ovs_flows, exposed_ips,
exposed_ips, routing_tables,
routing_tables_routes)
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
raise NotImplementedError()
# TODO(ltomasbo): clean up old policies, routes and proxy_arps cidrs
return True
def _cleanup_wiring_underlay(idl, bridge_mappings, ovs_flows, exposed_ips,
@ -125,7 +431,7 @@ def delete_vlan_devices_leftovers(idl, bridge_mappings):
def wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet, routing_table,
proxy_cidrs, lladdr=None):
proxy_cidrs, lladdr=None, mac=None, ovn_idl=None):
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
return _wire_provider_port_underlay(routing_tables_routes, ovs_flows,
port_ips, bridge_device,
@ -133,20 +439,25 @@ def wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
routing_table, proxy_cidrs,
lladdr=lladdr)
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
# No need to wire anything
return True
# We need to add a static mac binding due to proxy-arp issue in
# core ovn that would reply on the incomming traffic from the LR,
# while it should not
return _wire_provider_port_ovn(ovn_idl, port_ips, mac)
def unwire_provider_port(routing_tables_routes, port_ips, bridge_device,
bridge_vlan, routing_table, proxy_cidrs, lladdr=None):
bridge_vlan, routing_table, proxy_cidrs, lladdr=None,
ovn_idl=None):
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
return _unwire_provider_port_underlay(routing_tables_routes, port_ips,
bridge_device, bridge_vlan,
routing_table, proxy_cidrs,
lladdr=lladdr)
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
# No need to wire anything
return True
# We need to remove thestatic mac binding added due to proxy-arp issue
# in core ovn that would reply on the incomming traffic from the LR,
# while it should not
return _unwire_provider_port_ovn(ovn_idl, port_ips)
def _ensure_updated_mac_tweak_flows(localnet, bridge_device, ovs_flows):
@ -199,6 +510,18 @@ def _wire_provider_port_underlay(routing_tables_routes, ovs_flows, port_ips,
return True
def _wire_provider_port_ovn(ovn_idl, port_ips, mac):
cmds = []
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
for port_ip in port_ips:
cmds.append(ovn_idl.static_mac_binding_add(
port, port_ip, mac, override_dynamic_mac=True, may_exist=True))
if cmds:
_execute_commands(ovn_idl, cmds)
# to keep it consisten with the underlay method
return True
def _unwire_provider_port_underlay(routing_tables_routes, port_ips,
bridge_device, bridge_vlan, routing_table,
proxy_cidrs, lladdr=None):
@ -237,6 +560,18 @@ def _unwire_provider_port_underlay(routing_tables_routes, port_ips,
return True
def _unwire_provider_port_ovn(ovn_idl, port_ips):
cmds = []
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
for port_ip in port_ips:
cmds.append(ovn_idl.static_mac_binding_del(
port, port_ip, if_exists=True))
if cmds:
_execute_commands(ovn_idl, cmds)
# to keep it consisten with the underlay method
return True
def wire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan,
routing_table, cr_lrp_ips):
if not bridge_device:

View File

@ -182,12 +182,13 @@ class LogicalSwitchPortFIPCreateEvent(base_watcher.LSPChassisEvent):
if row.type not in [constants.OVN_VM_VIF_PORT_TYPE,
constants.OVN_VIRTUAL_VIF_PORT_TYPE]:
return
external_ip, ls_name = self.agent.get_port_external_ip_and_ls(row.name)
external_ip, external_mac, ls_name = (
self.agent.get_port_external_ip_and_ls(row.name))
if not external_ip or not ls_name:
return
with _SYNC_STATE_LOCK.read_lock():
self.agent.expose_fip(external_ip, ls_name, row)
self.agent.expose_fip(external_ip, external_mac, ls_name, row)
class LogicalSwitchPortFIPDeleteEvent(base_watcher.LSPChassisEvent):

View File

@ -80,6 +80,22 @@ class GetInterfaceTestCase(base_functional.BaseFunctionalTestCase):
self.assertRaises(agent_exc.NetworkInterfaceNotFound,
linux_net.get_interface_address, 'no_interface_name')
def test_get_nic_info(self):
dev_name = uuidutils.generate_uuid()[:15]
ip = '172.24.10.100/32'
self.addCleanup(self._delete_interfaces, [dev_name])
mac_address = net_utils.get_random_mac(
'fa:16:3e:00:00:00'.split(':'))
priv_linux_net.create_interface(dev_name, 'dummy',
address=mac_address)
priv_linux_net.add_ip_address(ip, dev_name)
ret = linux_net.get_nic_info(dev_name)
self.assertEqual((ip, mac_address), ret)
def test_get_nic_info_no_interface(self):
self.assertRaises(agent_exc.NetworkInterfaceNotFound,
linux_net.get_nic_info, 'no_interface_name')
def test_get_exposed_ips(self):
ips = ['240.0.0.1', 'fd00::1']
dev_name = uuidutils.generate_uuid()[:15]

View File

@ -194,6 +194,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
mock_get_port_external_ip_and_ls = mock.patch.object(
self.nb_bgp_driver, 'get_port_external_ip_and_ls').start()
mock_get_port_external_ip_and_ls.return_value = ("192.168.0.10",
"fake-mac",
"test-ls")
mock_expose_fip = mock.patch.object(
self.nb_bgp_driver, '_expose_fip').start()
@ -203,8 +204,8 @@ class TestNBOVNBGPDriver(test_base.TestCase):
self.nb_bgp_driver._ensure_port_exposed(port0)
mock_get_port_external_ip_and_ls.assert_called_once_with(port0.name)
mock_expose_fip.assert_called_once_with("192.168.0.10", "test-ls",
port0)
mock_expose_fip.assert_called_once_with("192.168.0.10", "fake-mac",
"test-ls", port0)
mock_expose_ip.assert_not_called()
def test__ensure_port_exposed_tenant_ls(self):
@ -255,7 +256,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
mock_get_ls_localnet_info.assert_called_once_with('test-ls')
mock_expose_fip.assert_not_called()
mock_expose_ip.assert_called_once_with(
['192.168.0.10'], 'test-ls', 'br-ex', 10,
['192.168.0.10'], 'fake_mac', 'test-ls', 'br-ex', 10,
constants.OVN_VM_VIF_PORT_TYPE, None)
@mock.patch.object(wire_utils, 'wire_provider_port')
@ -269,12 +270,13 @@ class TestNBOVNBGPDriver(test_base.TestCase):
proxy_cidrs = ['192.168.0.0/24']
self.nb_bgp_driver._expose_provider_port(
port_ips, 'teset-ls', bridge_device, bridge_vlan, 'fake-localnet',
proxy_cidrs)
port_ips, 'fake-mac', 'test-ls', bridge_device, bridge_vlan,
'fake-localnet', proxy_cidrs)
mock_wire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, {}, port_ips, bridge_device,
bridge_vlan, 'fake-localnet', self.ovn_routing_tables, proxy_cidrs)
bridge_vlan, 'fake-localnet', self.ovn_routing_tables,
proxy_cidrs, mac='fake-mac', ovn_idl=mock.ANY)
mock_announce_ips.assert_called_once_with(port_ips)
@mock.patch.object(wire_utils, 'wire_provider_port')
@ -288,12 +290,13 @@ class TestNBOVNBGPDriver(test_base.TestCase):
proxy_cidrs = ['192.168.0.0/24']
self.nb_bgp_driver._expose_provider_port(
port_ips, 'test-ls', bridge_device, bridge_vlan, 'fake-localnet',
proxy_cidrs)
port_ips, 'fake-mac', 'test-ls', bridge_device, bridge_vlan,
'fake-localnet', proxy_cidrs)
mock_wire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, {}, port_ips, bridge_device,
bridge_vlan, 'fake-localnet', self.ovn_routing_tables, proxy_cidrs)
bridge_vlan, 'fake-localnet', self.ovn_routing_tables, proxy_cidrs,
mac='fake-mac', ovn_idl=mock.ANY)
mock_announce_ips.assert_not_called()
@mock.patch.object(wire_utils, 'unwire_provider_port')
@ -311,7 +314,8 @@ class TestNBOVNBGPDriver(test_base.TestCase):
mock_withdraw_ips.assert_called_once_with(port_ips)
mock_unwire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables, proxy_cidrs)
bridge_vlan, self.ovn_routing_tables, proxy_cidrs,
ovn_idl=mock.ANY)
def test__get_bridge_for_localnet_port(self):
localnet = fakes.create_object({
@ -357,14 +361,16 @@ class TestNBOVNBGPDriver(test_base.TestCase):
'localnet': 'fake-localnet'})
if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE and cidr:
mock_expose_provider_port.assert_called_once_with(
ips, 'test-ls', 'br-ex', 10, 'fake-localnet', [cidr])
ips, 'fake-mac', 'test-ls', 'br-ex', 10, 'fake-localnet',
[cidr])
else:
mock_expose_provider_port.assert_called_once_with(
ips, 'test-ls', 'br-ex', 10, 'fake-localnet')
ips, 'fake-mac', 'test-ls', 'br-ex', 10, 'fake-localnet')
def test_expose_ip(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'addresses': ['fake-mac {} {}'.format(self.ipv4, self.ipv6)],
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
@ -373,6 +379,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
def test_expose_ip_virtual(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'addresses': ['fake-mac {} {}'.format(self.ipv4, self.ipv6)],
'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls',
constants.OVN_CIDRS_EXT_ID_KEY: 'test-cidr'}})
@ -382,6 +389,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
def test_expose_ip_no_switch(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'addresses': ['fake-mac {} {}'.format(self.ipv4, self.ipv6)],
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {}})
@ -493,12 +501,14 @@ class TestNBOVNBGPDriver(test_base.TestCase):
def test_get_port_external_ip_and_ls(self):
nat_entry = fakes.create_object({
'external_ids': {constants.OVN_FIP_NET_EXT_ID_KEY: 'net1'},
'external_ip': 'fake-ip'})
'external_ip': 'fake-ip',
'external_mac': 'fake-mac'})
self.nb_idl.get_nat_by_logical_port.return_value = nat_entry
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
expected_result = (nat_entry.external_ip, "neutron-net1")
expected_result = (nat_entry.external_ip, nat_entry.external_mac,
"neutron-net1")
self.assertEqual(ret, expected_result)
def test_get_port_external_ip_and_ls_no_nat_entry(self):
@ -506,20 +516,23 @@ class TestNBOVNBGPDriver(test_base.TestCase):
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
self.assertIsNone(ret)
self.assertEqual(ret, (None, None, None))
def test_get_port_external_ip_and_ls_no_external_id(self):
nat_entry = fakes.create_object({
'external_ids': {},
'external_ip': 'fake-ip'})
'external_ip': 'fake-ip',
'external_mac': 'fake-mac'})
self.nb_idl.get_nat_by_logical_port.return_value = nat_entry
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
self.assertEqual(ret, (nat_entry.external_ip, None))
self.assertEqual(ret,
(nat_entry.external_ip, nat_entry.external_mac, None))
def test_expose_fip(self):
ip = '10.0.0.1'
mac = 'fake-mac'
logical_switch = 'lswitch1'
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
@ -531,16 +544,17 @@ class TestNBOVNBGPDriver(test_base.TestCase):
row = fakes.create_object({
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
ret = self.nb_bgp_driver.expose_fip(ip, logical_switch, row)
ret = self.nb_bgp_driver.expose_fip(ip, mac, logical_switch, row)
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
mock_expose_provider_port.assert_called_once_with([ip], 'test-ls',
mock_expose_provider_port.assert_called_once_with([ip], mac, 'test-ls',
'br-ex', 100,
'fake-localnet')
self.assertTrue(ret)
def test_expose_fip_no_device(self):
ip = '10.0.0.1'
mac = 'fake-mac'
logical_switch = 'lswitch1'
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
@ -550,7 +564,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
row = fakes.create_object({
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
ret = self.nb_bgp_driver.expose_fip(ip, logical_switch, row)
ret = self.nb_bgp_driver.expose_fip(ip, mac, logical_switch, row)
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
mock_expose_provider_port.assert_not_called()

View File

@ -19,6 +19,7 @@ from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs
from ovn_bgp_agent import constants
from ovn_bgp_agent.drivers.openstack.utils import ovs as ovs_utils
from ovn_bgp_agent import exceptions as agent_exc
from ovn_bgp_agent.tests import base as test_base
from ovn_bgp_agent.utils import linux_net
@ -72,6 +73,70 @@ class TestOVS(test_base.TestCase):
self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with(
'ovs-vsctl', ['get', 'Interface', port, 'ofport'])
def test_get_ovs_ports_info(self):
bridge = 'fake-bridge'
bridge_ports = ['br-ex']
self.mock_ovs_vsctl.ovs_cmd.return_value = bridge_ports
ret = ovs_utils.get_ovs_ports_info(bridge)
self.assertEqual(bridge_ports, ret)
self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with(
'ovs-vsctl', ['list-ports', bridge])
def test_get_ovs_patch_port_ofport(self):
patch = 'fake-patch'
ofport = ['1']
self.mock_ovs_vsctl.ovs_cmd.return_value = ofport
ret = ovs_utils.get_ovs_patch_port_ofport(patch)
self.assertEqual(ofport[0], ret)
self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with(
'ovs-vsctl',
['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport'])
def test_get_ovs_patch_port_ofport_exception(self):
patch = 'fake-patch'
self.mock_ovs_vsctl.ovs_cmd.side_effect = Exception
self.assertRaises(agent_exc.PatchPortNotFound,
ovs_utils.get_ovs_patch_port_ofport, patch)
expected_calls = [
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport'])]
self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls)
def test_get_ovs_patch_port_ofport_no_port(self):
patch = 'fake-patch'
ofport = ['[]']
self.mock_ovs_vsctl.ovs_cmd.return_value = ofport
self.assertRaises(agent_exc.PatchPortNotFound,
ovs_utils.get_ovs_patch_port_ofport, patch)
expected_calls = [
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport']),
mock.call('ovs-vsctl', ['get', 'Interface',
'patch-fake-patch-to-br-int', 'ofport'])]
self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls)
@mock.patch.object(ovs_utils, 'get_bridge_flows')
def test_remove_extra_ovs_flows(self, mock_flows):
port_iface = '1'
@ -100,6 +165,15 @@ class TestOVS(test_base.TestCase):
'ovs-ofctl', ['del-flows', self.bridge, expected_del_flow])
mock_flows.assert_called_once_with(self.bridge, self.cookie_id)
def test_ensure_flow(self):
bridge = 'fake-bridge'
flow = 'fake-flow'
ovs_utils.ensure_flow(bridge, flow)
self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with(
'ovs-ofctl', ['add-flow', bridge, flow])
@mock.patch.object(ovs_utils, 'get_device_port_at_ovs')
@mock.patch.object(linux_net, 'get_interface_address')
@mock.patch.object(linux_net, 'get_ip_version')
@ -374,12 +448,27 @@ class TestOvsIdl(test_base.TestCase):
self._test_ovs_ext_ids_getters(
self.ovs_idl.get_own_chassis_id, row, expected_return)
def test_get_own_chassis_name(self):
expected_return = 'fake-name'
row = {'hostname': expected_return}
self._test_ovs_ext_ids_getters(
self.ovs_idl.get_own_chassis_name, row, expected_return)
def test_get_ovn_remote(self):
expected_return = 'fake-ovn-remote'
row = {'ovn-remote': expected_return}
self._test_ovs_ext_ids_getters(
self.ovs_idl.get_ovn_remote, row, expected_return)
def test_get_ovn_remote_nb(self):
expected_return = 'fake-ovn-remote'
row = {'ovn-nb-remote': expected_return}
self.execute_ref.return_value = row
ret = self.ovs_idl.get_ovn_remote(nb=True)
self.assertEqual(expected_return, ret)
self.ovs_idl.idl_ovs.db_get.assert_called_once_with(
'Open_vSwitch', '.', 'external_ids')
def test_get_ovn_bridge_mappings(self):
self.execute_ref.return_value = {
'ovn-bridge-mappings': 'net0:bridge0,net1:bridge1, net2:bridge2'}
@ -394,3 +483,13 @@ class TestOvsIdl(test_base.TestCase):
self.assertEqual([], ret)
self.ovs_idl.idl_ovs.db_get.assert_called_once_with(
'Open_vSwitch', '.', 'external_ids')
def test_get_ovn_bridge_mappings_bridge(self):
bridge = 'bgp'
self.execute_ref.return_value = {
'ovn-bridge-mappings-bgp':
'net0:bridge0,net1:bridge1, net2:bridge2'}
ret = self.ovs_idl.get_ovn_bridge_mappings(bridge=bridge)
self.assertEqual(['net0:bridge0', 'net1:bridge1', 'net2:bridge2'], ret)
self.ovs_idl.idl_ovs.db_get.assert_called_once_with(
'Open_vSwitch', '.', 'external_ids')

View File

@ -0,0 +1,455 @@
# Copyright 2023 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
from unittest import mock
from oslo_config import cfg
from ovn_bgp_agent import constants
from ovn_bgp_agent.drivers.openstack.utils import ovn as ovn_utils
from ovn_bgp_agent.drivers.openstack.utils import ovs as ovs_utils
from ovn_bgp_agent.drivers.openstack.utils import wire
from ovn_bgp_agent.tests import base as test_base
from ovn_bgp_agent.utils import linux_net
CONF = cfg.CONF
class TestWire(test_base.TestCase):
def setUp(self):
super(TestWire, self).setUp()
self.nb_idl = ovn_utils.OvsdbNbOvnIdl(mock.Mock())
self.sb_idl = ovn_utils.OvsdbSbOvnIdl(mock.Mock())
self.ovs_idl = mock.Mock()
# Helper variables that are used across multiple methods
self.bridge_mappings = 'datacentre:br-ex'
# Monkey-patch parent class methods
self.nb_idl.ls_add = mock.Mock()
self.nb_idl.lr_add = mock.Mock()
self.nb_idl.lrp_add = mock.Mock()
self.nb_idl.lrp_set_gateway_chassis = mock.Mock()
self.nb_idl.lrp_add_networks = mock.Mock()
self.nb_idl.lr_route_add = mock.Mock()
self.nb_idl.lr_policy_add = mock.Mock()
@mock.patch.object(wire, '_ensure_base_wiring_config_underlay')
def test_ensure_base_wiring_config(self, mock_underlay):
wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl,
routing_tables={})
mock_underlay.assert_called_once_with(self.sb_idl, self.ovs_idl, {})
@mock.patch.object(wire, '_ensure_base_wiring_config_ovn')
def test_ensure_base_wiring_config_ovn(self, mock_ovn):
CONF.set_override('exposing_method', 'ovn')
self.addCleanup(CONF.clear_override, 'exposing_method')
wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl,
ovn_idl=self.nb_idl)
mock_ovn.assert_called_once_with(self.ovs_idl, self.nb_idl)
@mock.patch.object(wire, '_ensure_base_wiring_config_underlay')
@mock.patch.object(wire, '_ensure_base_wiring_config_ovn')
def test_ensure_base_wiring_config_not_implemeneted(self, mock_ovn,
mock_underlay):
CONF.set_override('exposing_method', 'vrf')
self.addCleanup(CONF.clear_override, 'exposing_method')
wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl,
ovn_idl=self.nb_idl)
mock_ovn.assert_not_called()
mock_underlay.assert_not_called()
def test__ensure_base_wiring_config_ovn(self):
pass
def test__ensure_ovn_router(self):
wire._ensure_ovn_router(self.nb_idl)
self.nb_idl.lr_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, may_exist=True)
@mock.patch.object(wire, '_execute_commands')
@mock.patch.object(wire, '_ensure_lsp_cmds')
def test__ensure_ovn_switch(self, m_ensure_lsp, m_cmds):
ls_name = 'test-ls'
localnet_port = "{}-localnet".format(ls_name)
options = {'network_name': ls_name}
wire._ensure_ovn_switch(self.nb_idl, ls_name)
self.nb_idl.ls_add.assert_called_once_with(ls_name, may_exist=True)
m_ensure_lsp.assert_called_once_with(
self.nb_idl, localnet_port, ls_name, 'localnet', 'unknown',
**options)
@mock.patch.object(wire, '_execute_commands')
@mock.patch.object(wire, '_ensure_lsp_cmds')
def test__ensure_ovn_network_link_internal(self, m_ensure_lsp, m_cmds):
switch_name = 'internal'
provider_cidrs = ['172.16.0.0/16']
r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
options = {'router-port': r_port_name, 'arp_proxy': '172.16.0.0/16'}
wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'internal',
provider_cidrs=provider_cidrs)
self.nb_idl.lrp_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, r_port_name,
constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC, provider_cidrs, peer=[],
may_exist=True)
m_ensure_lsp.assert_called_once_with(
self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options)
self.nb_idl.lrp_set_gateway_chassis.assert_called_once_with(
r_port_name, constants.OVN_CLUSTER_BRIDGE, 1)
@mock.patch.object(wire, '_execute_commands')
@mock.patch.object(wire, '_ensure_lsp_cmds')
def test__ensure_ovn_network_link_internal_runtime_error(
self, m_ensure_lsp, m_cmds):
switch_name = 'internal'
provider_cidrs = ['172.16.0.0/16']
r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
options = {'router-port': r_port_name, 'arp_proxy': '172.16.0.0/16'}
self.nb_idl.lrp_add.side_effect = RuntimeError(
'with different networks')
wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'internal',
provider_cidrs=provider_cidrs)
self.nb_idl.lrp_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, r_port_name,
constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC, provider_cidrs, peer=[],
may_exist=True)
self.nb_idl.lrp_add_networks.assert_called_once_with(
r_port_name, provider_cidrs, may_exist=True)
m_ensure_lsp.assert_called_once_with(
self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options)
self.nb_idl.lrp_set_gateway_chassis.assert_called_once_with(
r_port_name, constants.OVN_CLUSTER_BRIDGE, 1)
@mock.patch.object(wire, '_ensure_lsp_cmds')
def test__ensure_ovn_network_link_external(self, m_ensure_lsp):
switch_name = 'external'
ip = '1.1.1.2'
mac = 'fake-map'
r_port_name = "{}-{}".format(constants.OVN_CLUSTER_ROUTER, switch_name)
options = {'router-port': r_port_name}
wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'external',
ip=ip, mac=mac)
self.nb_idl.lrp_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, r_port_name,
mac, [ip], peer=[], may_exist=True)
m_ensure_lsp.assert_called_once_with(
self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options)
def _ensure_ovn_policies(self, next_hops):
if len(next_hops) > 1:
columns = {'nexthops': next_hops}
elif len(next_hops) == 1:
columns = {'nexthop': next_hops[0]}
wire._ensure_ovn_policies(self.nb_idl, next_hops)
self.nb_idl.lr_policy_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, 10, mock.ANY, 'reroute',
may_exist=True, **columns)
def test__ensure_ovn_policies_dual_nexthop(self):
next_hops = ['1.1.1.1', '2.2.2.2']
self._ensure_ovn_policies(next_hops)
def test__ensure_ovn_policies_single_nexthop(self):
next_hops = ['1.1.1.1']
self._ensure_ovn_policies(next_hops)
@mock.patch.object(wire, '_execute_commands')
def test_ensure_ovn_routes(self, m_cmds):
peer_ips = ['1.1.1.1']
wire._ensure_ovn_routes(self.nb_idl, peer_ips)
self.nb_idl.lr_route_add.assert_called_once_with(
constants.OVN_CLUSTER_ROUTER, '0.0.0.0/0', peer_ips[0],
ecmp=True, may_exist=True)
@mock.patch.object(ovs_utils, 'ensure_flow')
@mock.patch.object(linux_net, 'get_ip_version')
@mock.patch.object(ovs_utils, 'get_ovs_ports_info')
@mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info')
def test__ensure_ingress_flows(self, m_ovs_patch, m_ovs_get, m_ip_version,
m_ovn_flow):
CONF.set_override('external_nics', ['eth1'], group='local_ovn_cluster')
self.addCleanup(CONF.clear_override, 'external_nics',
group='local_ovn_cluster')
bridge = 'br-ex'
mac = 'fake-mac'
switch_name = 'test-ls'
provider_cidrs = ['172.16.0.0/16']
patch_port_prefix = 'patch-{}-'.format(switch_name)
m_ovs_patch.return_value = ['fake-patch']
m_ovs_get.return_value = ['eth1']
wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs)
m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix)
m_ovs_get.assert_called_once_with(bridge)
m_ip_version.assert_called_once_with(provider_cidrs[0])
m_ovn_flow.assert_called_once_with(bridge, mock.ANY)
@mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info')
def test__ensure_ingress_flows_no_network(self, m_ovs):
bridge = 'br-ex'
mac = 'fake-mac'
switch_name = 'test-ls'
provider_cidrs = []
wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs)
m_ovs.assert_not_called()
@mock.patch.object(ovs_utils, 'get_ovs_ports_info')
@mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info')
def test__ensure_ingress_flows_no_patch_port(self, m_ovs_patch, m_ovs_get):
bridge = 'br-ex'
mac = 'fake-mac'
switch_name = 'test-ls'
provider_cidrs = ['172.16.0.0/16']
patch_port_prefix = 'patch-{}-'.format(switch_name)
m_ovs_patch.return_value = []
wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs)
m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix)
m_ovs_get.assert_not_called()
@mock.patch.object(linux_net, 'get_ip_version')
@mock.patch.object(ovs_utils, 'get_ovs_ports_info')
@mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info')
def test__ensure_ingress_flows_no_external_nic(self, m_ovs_patch,
m_ovs_get, m_ip_version):
bridge = 'br-ex'
mac = 'fake-mac'
switch_name = 'test-ls'
provider_cidrs = ['172.16.0.0/16']
patch_port_prefix = 'patch-{}-'.format(switch_name)
m_ovs_patch.return_value = ['fake-patch']
m_ovs_get.return_value = ['eth1']
wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs)
m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix)
m_ovs_get.assert_called_once_with(bridge)
m_ip_version.assert_not_called()
@mock.patch.object(wire, '_cleanup_wiring_underlay')
def test_cleanup_wiring_underlay(self, mock_underlay):
ovs_flows = {}
exposed_ips = {}
routing_tables = {}
routing_tables_routes = {}
wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows,
exposed_ips, routing_tables, routing_tables_routes)
mock_underlay.assert_called_once_with(
self.sb_idl, self.bridge_mappings, ovs_flows, exposed_ips,
routing_tables, routing_tables_routes)
def test_cleanup_wiring_ovn(self):
CONF.set_override('exposing_method', 'ovn')
self.addCleanup(CONF.clear_override, 'exposing_method')
ovs_flows = {}
exposed_ips = {}
routing_tables = {}
routing_tables_routes = {}
ret = wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows,
exposed_ips, routing_tables,
routing_tables_routes)
self.assertTrue(ret)
@mock.patch.object(wire, '_cleanup_wiring_underlay')
def test_cleanup_wiring_not_implemeneted(self, mock_underlay):
CONF.set_override('exposing_method', 'vrf')
self.addCleanup(CONF.clear_override, 'exposing_method')
ovs_flows = {}
exposed_ips = {}
routing_tables = {}
routing_tables_routes = {}
wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows,
exposed_ips, routing_tables, routing_tables_routes)
mock_underlay.assert_not_called()
@mock.patch.object(wire, '_wire_provider_port_underlay')
def test_wire_provider_port_underlay(self, mock_underlay):
routing_tables_routes = {}
ovs_flows = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
localnet = 'fake-localnet'
routing_table = 5
proxy_cidrs = []
wire.wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet,
routing_table, proxy_cidrs)
mock_underlay.assert_called_once_with(
routing_tables_routes, ovs_flows, port_ips, bridge_device,
bridge_vlan, localnet, routing_table, proxy_cidrs, lladdr=None)
@mock.patch.object(wire, '_wire_provider_port_ovn')
def test_wire_provider_port_ovn(self, mock_ovn):
CONF.set_override('exposing_method', 'ovn')
self.addCleanup(CONF.clear_override, 'exposing_method')
routing_tables_routes = {}
ovs_flows = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
localnet = 'fake-localnet'
routing_table = 5
proxy_cidrs = []
mac = 'fake-mac'
wire.wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet,
routing_table, proxy_cidrs, mac=mac,
ovn_idl=self.nb_idl)
mock_ovn.assert_called_once_with(self.nb_idl, port_ips, mac)
@mock.patch.object(wire, '_wire_provider_port_underlay')
@mock.patch.object(wire, '_wire_provider_port_ovn')
def test_wire_provider_port_not_implemented(self, mock_ovn, mock_underlay):
CONF.set_override('exposing_method', 'vrf')
self.addCleanup(CONF.clear_override, 'exposing_method')
routing_tables_routes = {}
ovs_flows = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
localnet = 'fake-localnet'
routing_table = 5
proxy_cidrs = []
wire.wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
bridge_device, bridge_vlan, localnet,
routing_table, proxy_cidrs)
mock_ovn.assert_not_called()
mock_underlay.assert_not_called()
@mock.patch.object(wire, '_execute_commands')
def test__wire_provider_port_ovn(self, m_cmds):
port_ips = ['1.1.1.1', '2.2.2.2']
mac = 'fake-mac'
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
wire._wire_provider_port_ovn(self.nb_idl, port_ips, mac)
cmds = [
ovn_utils.StaticMACBindingAddCommand(
self.nb_idl, port, port_ips[0], mac, True, may_exist=True),
ovn_utils.StaticMACBindingAddCommand(
self.nb_idl, port, port_ips[1], mac, True, may_exist=True)]
# FIXME(ltomasbo): The standard assert called ones is not working
# with the object
# m_cmds.assert_called_once_with(self.nb_idl, cmds)
# so we are checking this by comparing the object dict instead
self.assertEqual(
m_cmds.call_args_list[0][0][1][0].__dict__,
cmds[0].__dict__
)
@mock.patch.object(wire, '_execute_commands')
def test__wire_provider_port_ovn_no_action(self, m_cmds):
port_ips = []
mac = 'fake-mac'
wire._wire_provider_port_ovn(self.nb_idl, port_ips, mac)
m_cmds.assert_not_called()
@mock.patch.object(wire, '_unwire_provider_port_underlay')
def test_unwire_provider_port_underlay(self, mock_underlay):
routing_tables_routes = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
routing_table = 5
proxy_cidrs = []
wire.unwire_provider_port(routing_tables_routes, port_ips,
bridge_device, bridge_vlan, routing_table,
proxy_cidrs)
mock_underlay.assert_called_once_with(
routing_tables_routes, port_ips, bridge_device, bridge_vlan,
routing_table, proxy_cidrs, lladdr=None)
@mock.patch.object(wire, '_unwire_provider_port_ovn')
def test_unwire_provider_port_ovn(self, mock_ovn):
CONF.set_override('exposing_method', 'ovn')
self.addCleanup(CONF.clear_override, 'exposing_method')
routing_tables_routes = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
routing_table = 5
proxy_cidrs = []
wire.unwire_provider_port(routing_tables_routes, port_ips,
bridge_device, bridge_vlan, routing_table,
proxy_cidrs, ovn_idl=self.nb_idl)
mock_ovn.assert_called_once_with(self.nb_idl, port_ips)
@mock.patch.object(wire, '_unwire_provider_port_underlay')
@mock.patch.object(wire, '_unwire_provider_port_ovn')
def test_unwire_provider_port_not_implemented(self, mock_ovn,
mock_underlay):
CONF.set_override('exposing_method', 'vrf')
self.addCleanup(CONF.clear_override, 'exposing_method')
routing_tables_routes = {}
port_ips = []
bridge_device = 'fake-bridge'
bridge_vlan = '101'
routing_table = 5
proxy_cidrs = []
wire.unwire_provider_port(routing_tables_routes, port_ips,
bridge_device, bridge_vlan, routing_table,
proxy_cidrs)
mock_ovn.assert_not_called()
mock_underlay.assert_not_called()
@mock.patch.object(wire, '_execute_commands')
def test__unwire_provider_port_ovn(self, m_cmds):
port_ips = ['1.1.1.1']
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
wire._unwire_provider_port_ovn(self.nb_idl, port_ips)
cmds = [ovn_utils.StaticMACBindingDelCommand(
self.nb_idl, port, port_ips[0], if_exists=True)]
# FIXME(ltomasbo): The standard assert called ones is not working
# with the object
# m_cmds.assert_called_once_with(self.nb_idl, cmds)
# so we are checking this by comparing the object dict instead
self.assertEqual(
m_cmds.call_args_list[0][0][1][0].__dict__,
cmds[0].__dict__
)
@mock.patch.object(wire, '_execute_commands')
def test__unwire_provider_port_ovn_no_action(self, m_cmds):
port_ips = []
wire._unwire_provider_port_ovn(self.nb_idl, port_ips)
m_cmds.assert_not_called()

View File

@ -288,19 +288,23 @@ class TestLogicalSwitchPortFIPCreateEvent(test_base.TestCase):
external_ip = '10.0.0.10'
ls_name = 'neutron-net-id'
self.agent.get_port_external_ip_and_ls.return_value = (external_ip,
'mac',
ls_name)
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
name='net-id')
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_fip.assert_called_once_with(external_ip, ls_name,
row)
self.agent.expose_fip.assert_called_once_with(external_ip, 'mac',
ls_name, row)
def test_run_no_external_ip(self):
external_ip = None
ls_name = 'logical_switch'
self.agent.get_port_external_ip_and_ls.return_value = (external_ip,
'mac',
ls_name)
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
name='net-id')
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_fip.assert_not_called()

View File

@ -98,6 +98,24 @@ class TestLinuxNet(test_base.TestCase):
self.assertRaises(agent_exc.NetworkInterfaceNotFound,
linux_net.get_interface_address, 'fake-nic')
def test_get_nic_info(self):
device_idx = 7
nic_addr = IPRouteDict({'prefixlen': 32,
'attrs': [('IFA_ADDRESS', self.ip)]})
self.fake_ipr.link_lookup.return_value = [device_idx]
self.fake_ipr.get_addr.return_value = [nic_addr]
fake_link = mock.MagicMock()
fake_link.get_attr.return_value = self.mac
self.fake_ipr.get_links.return_value = [fake_link]
ret = linux_net.get_nic_info('fake-nic')
self.assertEqual(('{}/32'.format(self.ip), self.mac), ret)
def test_get_nic_info_index_error(self):
self.fake_ipr.link_lookup.return_value = ''
self.assertRaises(agent_exc.NetworkInterfaceNotFound,
linux_net.get_nic_info, 'fake-nic')
@mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_vrf')
def test_ensure_vrf(self, mock_ensure_vrf):
linux_net.ensure_vrf('fake-vrf', 10)

View File

@ -77,6 +77,26 @@ def get_interface_address(nic):
raise agent_exc.NetworkInterfaceNotFound(device=nic)
@tenacity.retry(
retry=tenacity.retry_if_exception_type(
netlink_exceptions.NetlinkDumpInterrupted),
wait=tenacity.wait_exponential(multiplier=0.02, max=1),
stop=tenacity.stop_after_delay(8),
reraise=True)
def get_nic_info(nic):
try:
with pyroute2.IPRoute() as ipr:
idx = ipr.link_lookup(ifname=nic)[0]
nic_addr = ipr.get_addr(index=idx)[0]
ip = '{}/{}'.format(
nic_addr.get_attr('IFA_ADDRESS'),
nic_addr.get('prefixlen'))
mac = ipr.get_links(idx)[0].get_attr('IFLA_ADDRESS')
return ip, mac
except IndexError:
raise agent_exc.NetworkInterfaceNotFound(device=nic)
def ensure_vrf(vrf_name, vrf_table):
ovn_bgp_agent.privileged.linux_net.ensure_vrf(vrf_name, vrf_table)

View File

@ -13,7 +13,7 @@ oslo.privsep>=2.3.0 # Apache-2.0
oslo.rootwrap>=5.15.0 # Apache-2.0
oslo.service>=1.40.2 # Apache-2.0
ovs>=2.8.0 # Apache-2.0
ovsdbapp>=1.4.0 # Apache-2.0
ovsdbapp>=1.16.0 # Apache-2.0
pyroute2>=0.6.6;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2)
stevedore>=1.20.0 # Apache-2.0
tenacity>=6.0.0 # Apache-2.0

View File

@ -42,9 +42,9 @@ setenv =
commands =
stestr run --exclude-regex ".tests.functional" {posargs}
coverage combine
coverage html -d cover --omit='*tests*'
coverage xml -o cover/coverage.xml --omit='*tests*'
coverage report --fail-under=85 --skip-covered --omit='*tests*'
coverage html -d cover --omit='ovn_bgp_agent/tests/*'
coverage xml -o cover/coverage.xml --omit='ovn_bgp_agent/tests/*'
coverage report --fail-under=82 --skip-covered --omit='ovn_bgp_agent/tests/*,ovn_bgp_agent/privileged/linux_net.py,ovn_bgp_agent/utils/linux_net.py'
[testenv:docs]
deps = -r{toxinidir}/doc/requirements.txt