ovn-bgp-agent/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py

1086 lines
48 KiB
Python

# Copyright 2021 Red Hat, Inc.
#
# 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 collections
import ipaddress
import pyroute2
import threading
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from ovn_bgp_agent import constants
from ovn_bgp_agent.drivers import driver_api
from ovn_bgp_agent.drivers.openstack.utils import driver_utils
from ovn_bgp_agent.drivers.openstack.utils import frr
from ovn_bgp_agent.drivers.openstack.utils import ovn
from ovn_bgp_agent.drivers.openstack.utils import ovs
from ovn_bgp_agent.drivers.openstack.watchers import bgp_watcher as watcher
from ovn_bgp_agent import exceptions as agent_exc
from ovn_bgp_agent.utils import linux_net
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
# LOG.setLevel(logging.DEBUG)
# logging.basicConfig(level=logging.DEBUG)
OVN_TABLES = ["Port_Binding", "Chassis", "Datapath_Binding"]
class OVNBGPDriver(driver_api.AgentDriverBase):
def __init__(self):
self._expose_tenant_networks = (CONF.expose_tenant_networks or
CONF.expose_ipv6_gua_tenant_networks)
self.allowed_address_scopes = set(CONF.address_scopes or [])
self.ovn_routing_tables = {} # {'br-ex': 200}
self.ovn_bridge_mappings = {} # {'public': 'br-ex'}
self.ovn_local_cr_lrps = {}
self.ovn_local_lrps = {}
# {'br-ex': [route1, route2]}
self.ovn_routing_tables_routes = collections.defaultdict()
# {ovn_lb: VIP1, VIP2}
self.ovn_lb_vips = collections.defaultdict()
self._sb_idl = None
self._post_fork_event = threading.Event()
@property
def sb_idl(self):
if not self._sb_idl:
self._post_fork_event.wait()
return self._sb_idl
@sb_idl.setter
def sb_idl(self, val):
self._sb_idl = val
def start(self):
self.ovs_idl = ovs.OvsIdl()
self.ovs_idl.start(CONF.ovsdb_connection)
self.chassis = self.ovs_idl.get_own_chassis_name()
self.ovn_remote = self.ovs_idl.get_ovn_remote()
LOG.info("Loaded chassis %s.", self.chassis)
LOG.info("Starting VRF configuration for advertising routes")
# Create VRF
linux_net.ensure_vrf(CONF.bgp_vrf, CONF.bgp_vrf_table_id)
# Ensure FRR is configure to leak the routes
# NOTE: If we want to recheck this every X time, we should move it
# inside the sync function instead
frr.vrf_leak(CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id)
# Create OVN dummy device
linux_net.ensure_ovn_device(CONF.bgp_nic, CONF.bgp_vrf)
# Clear vrf routing table
if CONF.clear_vrf_routes_on_startup:
linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id)
LOG.info("VRF configuration for advertising routes completed")
if self._expose_tenant_networks and self.allowed_address_scopes:
LOG.info("Configured allowed address scopes: %s",
", ".join(self.allowed_address_scopes))
events = ()
for event in self._get_events():
event_class = getattr(watcher, event)
events += (event_class(self),)
self._post_fork_event.clear()
# TODO(lucasagomes): The OVN package in the ubuntu LTS is old
# and does not support Chassis_Private. Once the package is updated
# we can remove this fallback mode.
try:
try:
self.sb_idl = ovn.OvnSbIdl(
self.ovn_remote,
chassis=self.chassis,
tables=OVN_TABLES + ["Chassis_Private",
"Logical_DP_Group"],
events=events).start()
except AssertionError:
self.sb_idl = ovn.OvnSbIdl(
self.ovn_remote,
chassis=self.chassis,
tables=OVN_TABLES + ["Chassis_Private"],
events=events).start()
except AssertionError:
self.sb_idl = ovn.OvnSbIdl(
self.ovn_remote,
chassis=self.chassis,
tables=OVN_TABLES,
events=events).start()
# Now IDL connections can be safely used
self._post_fork_event.set()
def _get_events(self):
events = set(["PortBindingChassisCreatedEvent",
"PortBindingChassisDeletedEvent",
"FIPSetEvent",
"FIPUnsetEvent",
"OVNLBVIPPortEvent",
"ChassisCreateEvent"])
if self._expose_tenant_networks:
events.update(["SubnetRouterAttachedEvent",
"SubnetRouterDetachedEvent",
"TenantPortCreatedEvent",
"TenantPortDeletedEvent"])
return events
@lockutils.synchronized('bgp')
def sync(self):
self._expose_tenant_networks = (CONF.expose_tenant_networks or
CONF.expose_ipv6_gua_tenant_networks)
self.ovn_local_cr_lrps = {}
self.ovn_local_lrps = {}
self.ovn_routing_tables_routes = collections.defaultdict()
self.ovn_lb_vips = collections.defaultdict()
LOG.debug("Ensuring VRF configuration for advertising routes")
# Create VRF
linux_net.ensure_vrf(CONF.bgp_vrf,
CONF.bgp_vrf_table_id)
# Create OVN dummy device
linux_net.ensure_ovn_device(CONF.bgp_nic,
CONF.bgp_vrf)
LOG.debug("Configuring br-ex default rule and routing tables for "
"each provider network")
flows_info = {}
# 1) Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings()
# 2) Get macs for bridge mappings
extra_routes = {}
with pyroute2.NDB() as ndb:
for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1):
network = bridge_mapping.split(":")[0]
bridge = bridge_mapping.split(":")[1]
self.ovn_bridge_mappings[network] = bridge
if not extra_routes.get(bridge):
extra_routes[bridge] = (
linux_net.ensure_routing_table_for_bridge(
self.ovn_routing_tables, bridge))
vlan_tag = self.sb_idl.get_network_vlan_tag_by_network_name(
network)
if vlan_tag:
vlan_tag = vlan_tag[0]
linux_net.ensure_vlan_device_for_network(bridge,
vlan_tag)
linux_net.ensure_arp_ndp_enabled_for_bridge(bridge,
bridge_index,
vlan_tag)
if flows_info.get(bridge):
continue
flows_info[bridge] = {
'mac': ndb.interfaces[bridge]['address'],
'in_port': set([])}
# 3) Get in_port for bridge mappings (br-ex, br-ex2)
ovs.get_ovs_flows_info(bridge, flows_info,
constants.OVS_RULE_COOKIE)
# 4) Add/Remove flows for each bridge mappings
ovs.remove_extra_ovs_flows(flows_info, constants.OVS_RULE_COOKIE)
LOG.debug("Syncing current routes.")
exposed_ips = linux_net.get_exposed_ips(CONF.bgp_nic)
# get the rules pointing to ovn bridges
ovn_ip_rules = linux_net.get_ovn_ip_rules(
self.ovn_routing_tables.values())
# add missing routes/ips for IPs on provider network
ports = self.sb_idl.get_ports_on_chassis(self.chassis)
for port in ports:
self._ensure_port_exposed(port, exposed_ips, ovn_ip_rules)
# this information is only available when there are cr-lrps add
# missing routes/ips for FIPs associated to VMs/LBs on the chassis
cr_lrp_ports = self.sb_idl.get_cr_lrp_ports_on_chassis(
self.chassis)
for cr_lrp_port in cr_lrp_ports:
self._ensure_cr_lrp_associated_ports_exposed(
cr_lrp_port, exposed_ips, ovn_ip_rules)
for cr_lrp_port, cr_lrp_info in self.ovn_local_cr_lrps.items():
lrp_ports = self.sb_idl.get_lrp_ports_for_router(
cr_lrp_info['router_datapath'])
for lrp in lrp_ports:
self._process_lrp_port(lrp, cr_lrp_port, exposed_ips,
ovn_ip_rules)
# add missing routes/ips related to ovn-octavia loadbalancers
# on the provider networks
ovn_lb_vips = self.sb_idl.get_ovn_lb_vips_on_provider_datapath(
cr_lrp_info['provider_datapath'])
for ovn_lb_port, ovn_lb_ip in ovn_lb_vips.items():
self._expose_ovn_lb_on_provider(ovn_lb_ip,
ovn_lb_port,
cr_lrp_port,
exposed_ips,
ovn_ip_rules)
# remove extra routes/ips
# remove all the leftovers on the list of current ips on dev OVN
linux_net.delete_exposed_ips(exposed_ips, CONF.bgp_nic)
# remove all the leftovers on the list of current ip rules for ovn
# bridges
linux_net.delete_ip_rules(ovn_ip_rules)
# remove all the extra rules not needed
linux_net.delete_bridge_ip_routes(self.ovn_routing_tables,
self.ovn_routing_tables_routes,
extra_routes)
def _ensure_cr_lrp_associated_ports_exposed(self, cr_lrp_port,
exposed_ips, ovn_ip_rules):
ips, patch_port_row = self.sb_idl.get_cr_lrp_nat_addresses_info(
cr_lrp_port, self.chassis, self.sb_idl)
if not ips:
return
self._expose_ip(ips, patch_port_row, associated_port=cr_lrp_port)
for ip in ips:
if exposed_ips and ip in exposed_ips:
exposed_ips.remove(ip)
if ovn_ip_rules:
ip_version = linux_net.get_ip_version(ip)
if ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(ip)
else:
ip_dst = "{}/32".format(ip)
ovn_ip_rules.pop(ip_dst, None)
def _ensure_port_exposed(self, port, exposed_ips, ovn_ip_rules):
if port.type not in constants.OVN_VIF_PORT_TYPES or not port.mac:
return
port_ips = []
if port.mac == ['unknown']:
# For FIPs associated to VM ports we don't need the port IP, so
# we can check if it is a VM on the provider and trigger the
# expose_ip without passing any port_ips
try:
if ((port.type != constants.OVN_VM_VIF_PORT_TYPE and
port.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE) or
self.sb_idl.is_provider_network(port.datapath)):
return
except agent_exc.DatapathNotFound:
# There is no need to expose anything related to a removed
# datapath
LOG.debug("Port %s not being exposed as its datapath %s was "
"removed", port.logical_port, port.datapath)
return
else:
if len(port.mac[0].split(' ')) < 2:
return
port_ips = port.mac[0].split(' ')[1:]
ips_adv = self._expose_ip(port_ips, port)
for port_ip in ips_adv:
ip_address = port_ip.split("/")[0]
if exposed_ips and ip_address in exposed_ips:
# remove each ip to add from the list of current ips on dev OVN
exposed_ips.remove(ip_address)
if ovn_ip_rules:
ip_version = linux_net.get_ip_version(port_ip)
if ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(ip_address)
else:
ip_dst = "{}/32".format(ip_address)
ovn_ip_rules.pop(ip_dst, None)
def _expose_provider_port(self, port_ips, provider_datapath,
bridge_device=None, bridge_vlan=None,
lladdr=None):
linux_net.add_ips_to_dev(CONF.bgp_nic, port_ips)
if not bridge_device and not bridge_vlan:
bridge_device, bridge_vlan = self._get_bridge_for_datapath(
provider_datapath)
for ip in port_ips:
try:
if lladdr:
linux_net.add_ip_rule(
ip, self.ovn_routing_tables[bridge_device],
bridge_device, lladdr=lladdr)
else:
linux_net.add_ip_rule(
ip, self.ovn_routing_tables[bridge_device],
bridge_device)
except agent_exc.InvalidPortIP:
LOG.exception("Invalid IP to create a rule for port"
" on the provider network: %s", ip)
return []
linux_net.add_ip_route(
self.ovn_routing_tables_routes, ip,
self.ovn_routing_tables[bridge_device], bridge_device,
vlan=bridge_vlan)
def _expose_tenant_port(self, port, ip_version, exposed_ips=None,
ovn_ip_rules=None):
# specific case for ovn-lb vips on tenant networks
if not port.mac and not port.chassis and not port.up[0]:
ext_n_cidr = port.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY)
if ext_n_cidr:
ovn_lb_ip = ext_n_cidr.split(" ")[0].split("/")[0]
linux_net.add_ips_to_dev(
CONF.bgp_nic, [ovn_lb_ip])
if exposed_ips and ovn_lb_ip in exposed_ips:
exposed_ips.remove(ovn_lb_ip)
if ovn_ip_rules:
ovn_ip_rules.pop(ext_n_cidr.split(" ")[0], None)
return
elif (not port.mac or
port.type not in (
constants.OVN_VM_VIF_PORT_TYPE,
constants.OVN_VIRTUAL_VIF_PORT_TYPE) or
(port.type == constants.OVN_VM_VIF_PORT_TYPE and
not port.chassis)):
return
try:
if port.mac == ['unknown']:
# Handling the case for unknown MACs when configdrive is used
# instead of dhcp
n_cidrs = port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY)
port_ips = [ip.split("/")[0] for ip in n_cidrs.split(" ")]
else:
port_ips = port.mac[0].split(' ')[1:]
except IndexError:
return
for port_ip in port_ips:
# Only adding the port ips that match the lrp
# IP version
port_ip_version = linux_net.get_ip_version(port_ip)
if port_ip_version == ip_version:
linux_net.add_ips_to_dev(
CONF.bgp_nic, [port_ip])
if exposed_ips and port_ip in exposed_ips:
exposed_ips.remove(port_ip)
if ovn_ip_rules:
if port_ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(port_ip)
else:
ip_dst = "{}/32".format(port_ip)
ovn_ip_rules.pop(ip_dst, None)
def _withdraw_provider_port(self, port_ips, provider_datapath,
bridge_device=None, bridge_vlan=None,
lladdr=None):
linux_net.del_ips_from_dev(CONF.bgp_nic, port_ips)
# assuming either you pass both or none
if not bridge_device and not bridge_vlan:
bridge_device, bridge_vlan = self._get_bridge_for_datapath(
provider_datapath)
for ip in port_ips:
if lladdr:
if linux_net.get_ip_version(ip) == constants.IP_VERSION_6:
cr_lrp_ip = '{}/128'.format(ip)
else:
cr_lrp_ip = '{}/32'.format(ip)
linux_net.del_ip_rule(
cr_lrp_ip, self.ovn_routing_tables[bridge_device],
bridge_device, lladdr=lladdr)
else:
linux_net.del_ip_rule(
ip, self.ovn_routing_tables[bridge_device], bridge_device)
linux_net.del_ip_route(
self.ovn_routing_tables_routes, ip,
self.ovn_routing_tables[bridge_device], bridge_device,
vlan=bridge_vlan)
def _get_bridge_for_datapath(self, datapath):
network_name, network_tag = self.sb_idl.get_network_name_and_tag(
datapath, self.ovn_bridge_mappings.keys())
if network_name:
if network_tag:
return self.ovn_bridge_mappings[network_name], network_tag[0]
return self.ovn_bridge_mappings[network_name], None
return None, None
@lockutils.synchronized('bgp')
def expose_ovn_lb(self, ip, row):
self._process_ovn_lb(ip, row, constants.EXPOSE)
@lockutils.synchronized('bgp')
def withdraw_ovn_lb(self, ip, row):
self._process_ovn_lb(ip, row, constants.WITHDRAW)
def _process_ovn_lb(self, ip, row, action):
try:
provider_network = self.sb_idl.is_provider_network(row.datapath)
except agent_exc.DatapathNotFound:
# There is no need to expose anything related to a removed
# datapath
LOG.debug("LoadBalancer with VIP %s not being exposed as its "
"associated datapath %s was removed", ip, row.datapath)
return
if not provider_network:
if not self._expose_tenant_networks:
return
if action == constants.EXPOSE:
return self._expose_remote_ip([ip], row)
if action == constants.WITHDRAW:
return self._withdraw_remote_ip([ip], row)
# if unknown action return
return
local_lb_cr_lrp = None
for cr_lrp_port, cr_lrp_info in self.ovn_local_cr_lrps.items():
if cr_lrp_info['provider_datapath'] == row.datapath:
local_lb_cr_lrp = cr_lrp_port
break
if not local_lb_cr_lrp:
return
vip_port = row.logical_port
if action == constants.EXPOSE:
self._expose_ovn_lb_on_provider(ip, vip_port, local_lb_cr_lrp)
if action == constants.WITHDRAW:
self._withdraw_ovn_lb_on_provider(vip_port, local_lb_cr_lrp)
def _expose_ovn_lb_on_provider(self, ip, ovn_lb_vip_port, cr_lrp,
exposed_ips=None, ovn_ip_rules=None):
self.ovn_local_cr_lrps[cr_lrp]['ovn_lb_vips'].append(ovn_lb_vip_port)
self.ovn_lb_vips.setdefault(ovn_lb_vip_port, []).append(ip)
bridge_device = self.ovn_local_cr_lrps[cr_lrp]['bridge_device']
bridge_vlan = self.ovn_local_cr_lrps[cr_lrp]['bridge_vlan']
LOG.debug("Adding BGP route for loadbalancer VIP %s", ip)
self._expose_provider_port([ip], None, bridge_device=bridge_device,
bridge_vlan=bridge_vlan)
LOG.debug("Added BGP route for loadbalancer VIP %s", ip)
if exposed_ips and ip in exposed_ips:
exposed_ips.remove(ip)
if ovn_ip_rules:
ip_version = linux_net.get_ip_version(ip)
if ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(ip)
else:
ip_dst = "{}/32".format(ip)
ovn_ip_rules.pop(ip_dst, None)
def _withdraw_ovn_lb_on_provider(self, ovn_lb_vip_port, cr_lrp):
bridge_device = self.ovn_local_cr_lrps[cr_lrp]['bridge_device']
bridge_vlan = self.ovn_local_cr_lrps[cr_lrp]['bridge_vlan']
for ip in self.ovn_lb_vips[ovn_lb_vip_port].copy():
LOG.debug("Deleting BGP route for loadbalancer VIP %s", ip)
self._withdraw_provider_port([ip], None,
bridge_device=bridge_device,
bridge_vlan=bridge_vlan)
if ip in self.ovn_lb_vips[ovn_lb_vip_port]:
self.ovn_lb_vips[ovn_lb_vip_port].remove(ip)
LOG.debug("Deleted BGP route for loadbalancer VIP %s", ip)
if ovn_lb_vip_port in self.ovn_local_cr_lrps[cr_lrp]['ovn_lb_vips']:
self.ovn_local_cr_lrps[cr_lrp]['ovn_lb_vips'].remove(
ovn_lb_vip_port)
@lockutils.synchronized('bgp')
def expose_ip(self, ips, row, associated_port=None):
'''Advertice BGP route by adding IP to device.
This methods ensures BGP advertises the IP of the VM in the provider
network, or the FIP associated to a VM in a tenant networks.
It relies on Zebra, which creates and advertises a route when an IP
is added to a local interface.
This method assumes a device named self.ovn_decice exists (inside a
VRF), and adds the IP of either:
- VM IP on the provider network,
- VM FIP, or
- CR-LRP OVN port
'''
self._expose_ip(ips, row, associated_port)
def _expose_ip(self, ips, row, associated_port=None):
if (row.type == constants.OVN_VM_VIF_PORT_TYPE or
row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE):
try:
provider_network = self.sb_idl.is_provider_network(
row.datapath)
except agent_exc.DatapathNotFound:
# There is no need to expose anything related to a removed
# datapath
LOG.debug("Port %s not being exposed as its associated "
"datapath %s was removed", row.logical_port,
row.datapath)
return
# VM on provider Network
if provider_network:
LOG.debug("Adding BGP route for logical port with ip %s", ips)
self._expose_provider_port(ips, row.datapath)
# 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 row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE:
bridge_device, bridge_vlan = self._get_bridge_for_datapath(
row.datapath)
# NOTE: This is neutron specific as we need the provider
# prefix to add the ndp proxy
n_cidr = row.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY)
if n_cidr and (linux_net.get_ip_version(n_cidr) ==
constants.IP_VERSION_6):
linux_net.add_ndp_proxy(n_cidr, bridge_device,
bridge_vlan)
LOG.debug("Added BGP route for logical port with ip %s", ips)
return ips
# VM with FIP
else:
# FIPs are only supported with IPv4
fip_address, fip_datapath = self.sb_idl.get_fip_associated(
row.logical_port)
if fip_address:
LOG.debug("Adding BGP route for FIP with ip %s",
fip_address)
self._expose_provider_port([fip_address], fip_datapath)
LOG.debug("Added BGP route for FIP with ip %s",
fip_address)
return [fip_address]
# FIP association to VM
elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE:
if (associated_port and self.sb_idl.is_port_on_chassis(
associated_port, self.chassis)):
LOG.debug("Adding BGP route for FIP with ip %s", ips)
self._expose_provider_port(ips, row.datapath)
LOG.debug("Added BGP route for FIP with ip %s", ips)
return ips
# CR-LRP Port
elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and
row.logical_port.startswith('cr-')):
cr_lrp_datapath = self.sb_idl.get_provider_datapath_from_cr_lrp(
row.logical_port)
if not cr_lrp_datapath:
return []
bridge_device, bridge_vlan = self._get_bridge_for_datapath(
cr_lrp_datapath)
mac = row.mac[0].split(' ')[0]
# Keeping information about the associated network for
# tenant network advertisement
self.ovn_local_cr_lrps[row.logical_port] = {
'router_datapath': row.datapath,
'provider_datapath': cr_lrp_datapath,
'ips': ips,
'mac': mac,
'subnets_datapath': {},
'subnets_cidr': [],
'ovn_lb_vips': [],
'bridge_vlan': bridge_vlan,
'bridge_device': bridge_device
}
self._expose_cr_lrp_port(ips, mac, bridge_device, bridge_vlan,
router_datapath=row.datapath,
provider_datapath=cr_lrp_datapath,
cr_lrp_port=row.logical_port)
return ips
return []
@lockutils.synchronized('bgp')
def withdraw_ip(self, ips, row, associated_port=None):
'''Withdraw BGP route by removing IP from device.
This methods ensures BGP withdraw an advertised IP of a VM, either
in the provider network, or the FIP associated to a VM in a tenant
networks.
It relies on Zebra, which withdraws the advertisement as soon as the
IP is deleted from the local interface.
This method assumes a device named self.ovn_decice exists (inside a
VRF), and removes the IP of either:
- VM IP on the provider network,
- VM FIP, or
- CR-LRP OVN port
'''
if (row.type == constants.OVN_VM_VIF_PORT_TYPE or
row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE):
try:
provider_network = self.sb_idl.is_provider_network(
row.datapath)
except agent_exc.DatapathNotFound:
# NOTE(ltomasbo): Datapath has been deleted. This means that:
# - If it was a provider network we need to withdraw it
# - It it was a VM with a FIP, the removal would be handled
# by the FIP dissassociation even (FIP removal) that must
# happen before removing the subnet from the router, and
# before being able to remove the subnet
# This means we only need to process the "provider_network"
# case
provider_network = True
LOG.debug("Port %s belongs to a removed datapath %s. "
"Assuming it was a provider network to avoid "
"leaks.", row.logical_port, row.datapath)
# VM on provider Network
if provider_network:
LOG.debug("Deleting BGP route for logical port with ip %s",
ips)
self._withdraw_provider_port(ips, row.datapath)
if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE:
virtual_provider_ports = (
self.sb_idl.get_virtual_ports_on_datapath_by_chassis(
row.datapath, self.chassis))
if not virtual_provider_ports:
cr_lrps_on_same_provider = [
p for p in self.ovn_local_cr_lrps.values()
if p['provider_datapath'] == row.datapath]
if not cr_lrps_on_same_provider:
bridge_device, bridge_vlan = (
self._get_bridge_for_datapath(row.datapath))
# NOTE: This is neutron specific as we need the
# provider prefix to add the ndp proxy
n_cidr = row.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY)
if n_cidr and (linux_net.get_ip_version(n_cidr) ==
constants.IP_VERSION_6):
linux_net.del_ndp_proxy(n_cidr, bridge_device,
bridge_vlan)
LOG.debug("Deleted BGP route for logical port with ip %s", ips)
# VM with FIP
else:
# FIPs are only supported with IPv4
fip_address, fip_datapath = self.sb_idl.get_fip_associated(
row.logical_port)
if not fip_address:
return
LOG.debug("Deleting BGP route for FIP with ip %s", fip_address)
self._withdraw_provider_port([fip_address], fip_datapath)
LOG.debug("Deleted BGP route for FIP with ip %s", fip_address)
# FIP disassociation to VM
elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE:
if (associated_port and (
self.sb_idl.is_port_on_chassis(
associated_port, self.chassis) or
self.sb_idl.is_port_deleted(associated_port))):
LOG.debug("Deleting BGP route for FIP with ip %s", ips)
self._withdraw_provider_port(ips, row.datapath)
LOG.debug("Deleted BGP route for FIP with ip %s", ips)
# CR-LRP Port
elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and
row.logical_port.startswith('cr-')):
cr_lrp_datapath = self.ovn_local_cr_lrps.get(
row.logical_port, {}).get('provider_datapath')
if not cr_lrp_datapath:
return
bridge_vlan = self.ovn_local_cr_lrps[row.logical_port].get(
'bridge_vlan')
bridge_device = self.ovn_local_cr_lrps[row.logical_port].get(
'bridge_device')
mac = row.mac[0].split(' ')[0]
self._withdraw_cr_lrp_port(ips, mac, bridge_device, bridge_vlan,
provider_datapath=cr_lrp_datapath,
cr_lrp_port=row.logical_port)
@lockutils.synchronized('bgp')
def expose_remote_ip(self, ips, row):
self._expose_remote_ip(ips, row)
def _expose_remote_ip(self, ips, row):
try:
if (self.sb_idl.is_provider_network(row.datapath) or
not self._expose_tenant_networks):
return
except agent_exc.DatapathNotFound:
# There is no need to expose anything related to a removed
# datapath
LOG.debug("Port %s not being exposed as its datapath %s was "
"removed", row.logical_port, row.datapath)
return
if not CONF.expose_tenant_networks:
# This means CONF.expose_ipv6_gua_tenant_networks is enabled
gua_ips = []
for ip in ips:
if driver_utils.is_ipv6_gua(ip):
gua_ips.append(ip)
if not gua_ips:
return
ips = gua_ips
ips_to_expose = []
for ip in ips:
if self._address_scope_allowed(ip, None, row):
ips_to_expose.append(ip)
if not ips_to_expose:
return
port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath)
if port_lrp in self.ovn_local_lrps.keys():
LOG.debug("Adding BGP route for tenant IP %s on chassis %s",
ips_to_expose, self.chassis)
linux_net.add_ips_to_dev(CONF.bgp_nic, ips_to_expose)
LOG.debug("Added BGP route for tenant IP %s on chassis %s",
ips_to_expose, self.chassis)
@lockutils.synchronized('bgp')
def withdraw_remote_ip(self, ips, row, chassis=None):
self._withdraw_remote_ip(ips, row, chassis)
def _withdraw_remote_ip(self, ips, row, chassis=None):
try:
if (self.sb_idl.is_provider_network(row.datapath) or
not self._expose_tenant_networks):
return
except agent_exc.DatapathNotFound:
# There is no need to continue as the subnet removal (patch port
# removal) will trigger a withdraw_subnet event that will remove
# the associated IPs
LOG.debug("Port %s not being withdrawn as its datapath %s was "
"removed. The subnet withdraw action will take care of "
"the withdrawal.", row.logical_port, row.datapath)
return
if not CONF.expose_tenant_networks:
# This means CONF.expose_ipv6_gua_tenant_networks is enabled
gua_ips = []
for ip in ips:
if driver_utils.is_ipv6_gua(ip):
gua_ips.append(ip)
if not gua_ips:
return
ips = gua_ips
ips_to_withdraw = []
for ip in ips:
if self._address_scope_allowed(ip, None, row):
ips_to_withdraw.append(ip)
if not ips_to_withdraw:
return
port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath)
if port_lrp in self.ovn_local_lrps.keys():
LOG.debug("Deleting BGP route for tenant IP %s on chassis %s",
ips_to_withdraw, self.chassis)
linux_net.del_ips_from_dev(CONF.bgp_nic, ips_to_withdraw)
LOG.debug("Deleted BGP route for tenant IP %s on chassis %s",
ips_to_withdraw, self.chassis)
def _process_lrp_port(self, lrp, associated_cr_lrp, exposed_ips=None,
ovn_ip_rules=None):
if (lrp.chassis or
not lrp.logical_port.startswith('lrp-') or
"chassis-redirect-port" in lrp.options.keys() or
associated_cr_lrp.strip('cr-') == lrp.logical_port):
return
# add missing route/ips for tenant network VMs
if self._expose_tenant_networks:
try:
lrp_ip = lrp.mac[0].split(' ')[1]
except IndexError:
# This should not happen: subnet without CIDR
return
if not lrp.options.get('peer'):
# if there is no peer associated to the port we need to
# 1) creation: wait for another re-sync to expose it
# 2) deletion: no need to add it as it being removed
return
if not self._address_scope_allowed(lrp_ip, lrp.options['peer']):
return
subnet_datapath = self.sb_idl.get_port_datapath(
lrp.options['peer'])
self._expose_lrp_port(lrp_ip, lrp.logical_port,
associated_cr_lrp, subnet_datapath,
exposed_ips=exposed_ips,
ovn_ip_rules=ovn_ip_rules)
def _expose_cr_lrp_port(self, ips, mac, bridge_device, bridge_vlan,
router_datapath, provider_datapath, cr_lrp_port):
LOG.debug("Adding BGP route for CR-LRP Port %s", ips)
ips_without_mask = [ip.split("/")[0] for ip in ips]
self._expose_provider_port(ips_without_mask, provider_datapath,
bridge_device, bridge_vlan,
lladdr=mac)
# add proxy ndp config for ipv6
for ip in ips:
if linux_net.get_ip_version(ip) == constants.IP_VERSION_6:
linux_net.add_ndp_proxy(ip, bridge_device, bridge_vlan)
LOG.debug("Added BGP route for CR-LRP Port %s", ips)
# Check if there are networks attached to the router,
# and if so, add the needed routes/rules
lrp_ports = self.sb_idl.get_lrp_ports_for_router(router_datapath)
for lrp in lrp_ports:
self._process_lrp_port(lrp, cr_lrp_port)
ovn_lb_vips = self.sb_idl.get_ovn_lb_vips_on_provider_datapath(
provider_datapath)
for ovn_lb_port, ovn_lb_ip in ovn_lb_vips.items():
self._expose_ovn_lb_on_provider(ovn_lb_ip,
ovn_lb_port,
cr_lrp_port)
def _withdraw_cr_lrp_port(self, ips, mac, bridge_device, bridge_vlan,
provider_datapath, cr_lrp_port):
LOG.debug("Deleting BGP route for CR-LRP Port %s", ips)
# Removing information about the associated network for
# tenant network advertisement
ips_without_mask = [ip.split("/")[0] for ip in ips]
self._withdraw_provider_port(ips_without_mask, provider_datapath,
bridge_device=bridge_device,
bridge_vlan=bridge_vlan,
lladdr=mac)
# del proxy ndp config for ipv6
for ip in ips_without_mask:
if linux_net.get_ip_version(ip) == constants.IP_VERSION_6:
cr_lrps_on_same_provider = [
p for p in self.ovn_local_cr_lrps.values()
if p['provider_datapath'] == provider_datapath]
# if no other cr-lrp port on the same provider
# delete the ndp proxy
if (len(cr_lrps_on_same_provider) <= 1):
linux_net.del_ndp_proxy(ip, bridge_device, bridge_vlan)
LOG.debug("Deleted BGP route for CR-LRP Port %s", ips)
# Check if there are networks attached to the router,
# and if so delete the needed routes/rules
local_cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp_port)
for subnet_cidr in local_cr_lrp_info['subnets_cidr']:
self._withdraw_lrp_port(subnet_cidr, None, cr_lrp_port)
# check if there are loadbalancers associated to the router,
# and if so delete the needed routes/rules
ovn_lb_vips = self.ovn_local_cr_lrps[cr_lrp_port][
'ovn_lb_vips'].copy()
for ovn_lb_vip in ovn_lb_vips:
self._withdraw_ovn_lb_on_provider(ovn_lb_vip, cr_lrp_port)
self.ovn_local_cr_lrps[cr_lrp_port]['ovn_lb_vips'].remove(
ovn_lb_vip)
try:
del self.ovn_local_cr_lrps[cr_lrp_port]
except KeyError:
LOG.debug("Gateway port %s already cleanup from the agent.",
cr_lrp_port)
def _expose_lrp_port(self, ip, lrp, associated_cr_lrp, subnet_datapath,
exposed_ips=None, ovn_ip_rules=None):
if not self._expose_tenant_networks:
return
if not CONF.expose_tenant_networks:
# This means CONF.expose_ipv6_gua_tenant_networks is enabled
if not driver_utils.is_ipv6_gua(ip):
return
cr_lrp_info = self.ovn_local_cr_lrps.get(associated_cr_lrp, {})
cr_lrp_ips = [ip_address.split('/')[0]
for ip_address in cr_lrp_info.get('ips', [])]
# this is the router gateway port
if ip.split('/')[0] in cr_lrp_ips:
return
cr_lrp_datapath = cr_lrp_info.get('provider_datapath')
if not cr_lrp_datapath:
return
bridge_device = cr_lrp_info.get('bridge_device')
bridge_vlan = cr_lrp_info.get('bridge_vlan')
# update information needed for the loadbalancers
cr_lrp_info['subnets_datapath'].update({lrp: subnet_datapath})
cr_lrp_info['subnets_cidr'].append(ip)
self.ovn_local_lrps.update({lrp: associated_cr_lrp})
LOG.debug("Adding IP Rules for network %s on chassis %s", ip,
self.chassis)
try:
linux_net.add_ip_rule(
ip, self.ovn_routing_tables[bridge_device], bridge_device)
except agent_exc.InvalidPortIP:
LOG.exception("Invalid IP to create a rule for the "
"lrp (network router interface) port: %s", ip)
return
LOG.debug("Added IP Rules for network %s on chassis %s", ip,
self.chassis)
if ovn_ip_rules:
ovn_ip_rules.pop(ip, None)
LOG.debug("Adding IP Routes for network %s on chassis %s", ip,
self.chassis)
# NOTE(ltomasbo): This assumes the provider network can only have
# (at most) 2 subnets, one for IPv4, one for IPv6
ip_version = linux_net.get_ip_version(ip)
for cr_lrp_ip in cr_lrp_ips:
if linux_net.get_ip_version(cr_lrp_ip) == ip_version:
linux_net.add_ip_route(
self.ovn_routing_tables_routes,
ip.split("/")[0],
self.ovn_routing_tables[bridge_device],
bridge_device,
vlan=bridge_vlan,
mask=ip.split("/")[1],
via=cr_lrp_ip)
break
LOG.debug("Added IP Routes for network %s on chassis %s", ip,
self.chassis)
# Check if there are VMs on the network
# and if so expose the route
ports = self.sb_idl.get_ports_on_datapath(subnet_datapath)
for port in ports:
self._expose_tenant_port(port, ip_version=ip_version,
exposed_ips=exposed_ips,
ovn_ip_rules=ovn_ip_rules)
def _withdraw_lrp_port(self, ip, lrp, associated_cr_lrp):
if not self._expose_tenant_networks:
return
if not CONF.expose_tenant_networks:
# This means CONF.expose_ipv6_gua_tenant_networks is enabled
if not driver_utils.is_ipv6_gua(ip):
return
cr_lrp_info = self.ovn_local_cr_lrps.get(associated_cr_lrp, {})
LOG.debug("Deleting IP Rules for network %s on chassis %s", ip,
self.chassis)
exposed_lrp = False
if lrp:
if lrp in self.ovn_local_lrps.keys():
exposed_lrp = True
self.ovn_local_lrps.pop(lrp)
else:
for subnet_lp in cr_lrp_info['subnets_datapath'].keys():
if subnet_lp in self.ovn_local_lrps.keys():
exposed_lrp = True
self.ovn_local_lrps.pop(subnet_lp)
break
cr_lrp_info['subnets_datapath'].pop(lrp, None)
if not exposed_lrp:
return
cr_lrp_ips = [ip_address.split('/')[0]
for ip_address in cr_lrp_info.get('ips', [])]
bridge_device = cr_lrp_info.get('bridge_device')
bridge_vlan = cr_lrp_info.get('bridge_vlan')
linux_net.del_ip_rule(ip, self.ovn_routing_tables[bridge_device],
bridge_device)
LOG.debug("Deleted IP Rules for network %s on chassis %s", ip,
self.chassis)
LOG.debug("Deleting IP Routes for network %s on chassis %s", ip,
self.chassis)
ip_version = linux_net.get_ip_version(ip)
for cr_lrp_ip in cr_lrp_ips:
if linux_net.get_ip_version(cr_lrp_ip) == ip_version:
linux_net.del_ip_route(
self.ovn_routing_tables_routes,
ip.split("/")[0],
self.ovn_routing_tables[bridge_device],
bridge_device,
vlan=bridge_vlan,
mask=ip.split("/")[1],
via=cr_lrp_ip)
if (linux_net.get_ip_version(cr_lrp_ip) ==
constants.IP_VERSION_6):
net = ipaddress.IPv6Network(ip, strict=False)
else:
net = ipaddress.IPv4Network(ip, strict=False)
break
LOG.debug("Deleted IP Routes for network %s on chassis %s", ip,
self.chassis)
# Check if there are VMs on the network
# and if so withdraw the routes
if net:
vms_on_net = linux_net.get_exposed_ips_on_network(
CONF.bgp_nic, net)
linux_net.delete_exposed_ips(vms_on_net, CONF.bgp_nic)
@lockutils.synchronized('bgp')
def expose_subnet(self, ip, row):
cr_lrp = self.sb_idl.is_router_gateway_on_chassis(
row.datapath, self.chassis)
subnet_datapath = self.sb_idl.get_port_datapath(
row.options['peer'])
if not cr_lrp:
return
if not self._address_scope_allowed(ip, row.options['peer']):
return
self._expose_lrp_port(ip, row.logical_port, cr_lrp, subnet_datapath)
@lockutils.synchronized('bgp')
def withdraw_subnet(self, ip, row):
try:
cr_lrp = self.sb_idl.is_router_gateway_on_chassis(
row.datapath, self.chassis)
except agent_exc.DatapathNotFound:
# NOTE(ltomasbo): This happens when the router (datapath) gets
# deleted at the same time as subnets are detached from it.
# Usually this will be hit when router is deleted without
# removing its gateway. In that case we don't need to withdraw
# the subnet as it is not exposed, just the cr-lrp which is
# handle in a different event/method (withdraw_ip)
LOG.debug("Router is being deleted, so it's datapath does "
"not exists any more. Checking if port %s belongs "
"to chassis redirect and skip in that case.",
row.logical_port)
cr_lrp = [cr_lrp_name
for cr_lrp_name in self.ovn_local_cr_lrps.keys()
if row.logical_port in cr_lrp_name]
# if cr_lrp exists, this means the lrp port is for the router
# gateway, so there is no need to proceed
if cr_lrp:
LOG.debug("Port %s is related to chassis redirect, so "
"there is no need to do further actions for "
"subnet withdrawal, as this port was not "
"triggering a subnet exposure.",
row.logical_port)
return
if not cr_lrp or cr_lrp not in self.ovn_local_cr_lrps.keys():
# NOTE(ltomasbo) there is a chance the cr-lrp just got moved
# to this node but was not yet processed. In that case there
# is no need to withdraw the network as it was not exposed here
return
self._withdraw_lrp_port(ip, row.logical_port, cr_lrp)
def _address_scope_allowed(self, ip, port_name, sb_port=None):
if not self.allowed_address_scopes:
# No address scopes to filter on => announce everything
return True
if not sb_port:
sb_port = self.sb_idl.get_port_by_name(port_name)
if not sb_port:
LOG.error("Port %s missing, skipping.", port_name)
return False
address_scopes = driver_utils.get_addr_scopes(sb_port)
# if we should filter on address scopes and this port has no
# address scopes set we do not need to expose it
if not any(address_scopes.values()):
return False
# if address scope does not match, no need to expose it
ip_version = linux_net.get_ip_version(ip)
return address_scopes[ip_version] in self.allowed_address_scopes