diff --git a/ovn_bgp_agent/constants.py b/ovn_bgp_agent/constants.py index ffc65412..a72274cc 100644 --- a/ovn_bgp_agent/constants.py +++ b/ovn_bgp_agent/constants.py @@ -40,7 +40,10 @@ OVN_EVPN_AS_EXT_ID_KEY = 'neutron_bgpvpn:as' OVN_EVPN_VRF_PREFIX = "vrf-" OVN_EVPN_BRIDGE_PREFIX = "br-" OVN_EVPN_VXLAN_PREFIX = "vxlan-" +OVN_EVPN_VLAN_PREFIX = "vlan-" OVN_EVPN_LO_PREFIX = "lo-" +OVN_EVPN_VETH_VRF_PREFIX = "veth-vrf-" +OVN_EVPN_VETH_OVS_PREFIX = "veth-ovs-" OVN_INTEGRATION_BRIDGE = 'br-int' OVN_LRP_PORT_NAME_PREFIX = 'lrp-' OVN_CRLRP_PORT_NAME_PREFIX = 'cr-lrp-' diff --git a/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py index 4690d878..8815eae9 100644 --- a/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py +++ b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py @@ -35,6 +35,9 @@ LOG = logging.getLogger(__name__) # logging.basicConfig(level=logging.DEBUG) OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private") +EVPN_INFO = collections.namedtuple( + 'EVPNInfo', ['vrf_name', 'lo_name', 'bridge_name', 'vxlan_name', + 'veth_vrf', 'veth_ovs', 'vlan_name']) class OVNEVPNDriver(driver_api.AgentDriverBase): @@ -130,65 +133,11 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): datapath_bridge, vlan_tag = self._get_bridge_for_datapath( gateway['provider_datapath']) - router_port_ip_version = linux_net.get_ip_version(router_port_ip) - for gateway_ip in gateway_ips: - if linux_net.get_ip_version(gateway_ip) == router_port_ip_version: - linux_net.add_ip_route( - self._ovn_routing_tables_routes, - router_ip, - gateway['vni'], - datapath_bridge, - vlan=vlan_tag, - mask=router_port_ip.split("/")[1], - via=gateway_ip) - break - - if router_port_ip_version == constants.IP_VERSION_6: - net_ip = '{}'.format(ipaddress.IPv6Network( - router_port_ip, strict=False)) - else: - net_ip = '{}'.format(ipaddress.IPv4Network( - router_port_ip, strict=False)) - - strip_vlan = False - if vlan_tag: - strip_vlan = True - ovs.ensure_evpn_ovs_flow(datapath_bridge, - constants.OVS_VRF_RULE_COOKIE, - gateway['mac'], - gateway['vrf'], - net_ip, - strip_vlan) - - network_port_datapath = self.sb_idl.get_port_datapath( + network_datapath = self.sb_idl.get_port_datapath( router_port.options['peer']) - if not network_port_datapath: - return - ports = self.sb_idl.get_ports_on_datapath( - network_port_datapath) - for port in ports: - if (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)): - continue - try: - port_ips = [port.mac[0].split(' ')[1]] - except IndexError: - continue - if len(port.mac[0].split(' ')) == 3: - port_ips.append(port.mac[0].split(' ')[2]) - 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 == router_port_ip_version: - linux_net.add_ips_to_dev( - gateway['lo'], [port_ip], - clear_local_route_at_table=gateway['vni']) - self._ovn_exposed_evpn_ips.setdefault( - gateway['lo'], []).extend([port_ip]) + self._expose_subnet(router_port_ip, gateway_ips, gateway, + datapath_bridge, vlan_tag, network_datapath) def _get_bridge_for_datapath(self, datapath): network_name, network_tag = self.sb_idl.get_network_name_and_tag( @@ -246,10 +195,15 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): "Not exposing it.", ips) return + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + LOG.info("Adding BGP route for CR-LRP Port %s on AS %s and " "VNI %s", ips, evpn_info['bgp_as'], evpn_info['vni']) - vrf, lo, bridge, vxlan = self._ensure_evpn_devices(evpn_info['vni']) - if not vrf or not lo: + evpn_devices = self._ensure_evpn_devices(datapath_bridge, + evpn_info['vni'], + vlan_tag) + if not evpn_devices.vrf_name or not evpn_devices.lo_name: return self.ovn_local_cr_lrps[cr_lrp_port_name] = { @@ -259,32 +213,27 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): 'mac': cr_lrp_port.mac[0].split(' ')[0], 'vni': int(evpn_info['vni']), 'bgp_as': evpn_info['bgp_as'], - 'lo': lo, - 'bridge': bridge, - 'vxlan': vxlan, - 'vrf': vrf + 'lo': evpn_devices.lo_name, + 'bridge': evpn_devices.bridge_name, + 'vxlan': evpn_devices.vxlan_name, + 'vrf': evpn_devices.vrf_name, + 'veth_vrf': evpn_devices.veth_vrf, + 'veth_ovs': evpn_devices.veth_ovs, + 'vlan': evpn_devices.vlan_name } frr.vrf_reconfigure(evpn_info, action="add-vrf") - datapath_bridge, vlan_tag = self._get_bridge_for_datapath( - cr_lrp_datapath) - - ips_without_mask = [ip.split("/")[0] for ip in ips] - linux_net.add_ips_to_dev(lo, ips_without_mask) - self._ovn_exposed_evpn_ips.setdefault( - lo, []).extend(ips_without_mask) - - self._connect_evpn_to_ovn(vrf, ips, datapath_bridge, evpn_info['vni'], + self._connect_evpn_to_ovn(evpn_devices.vrf_name, evpn_devices.veth_vrf, + evpn_devices.veth_ovs, ips, datapath_bridge, + evpn_info['vni'], evpn_devices.vlan_name, vlan_tag) - nei_bridge = datapath_bridge - if vlan_tag: - nei_bridge = '{}.{}'.format(datapath_bridge, vlan_tag) + ips_without_mask = [ip.split("/")[0] for ip in ips] + nei_dev = evpn_devices.vlan_name if vlan_tag else evpn_devices.veth_vrf for ip in ips_without_mask: linux_net.add_ip_nei( - ip, self.ovn_local_cr_lrps[cr_lrp_port_name]['mac'], - nei_bridge) + ip, self.ovn_local_cr_lrps[cr_lrp_port_name]['mac'], nei_dev) # Check if there are networks attached to the router, # and if so, add the needed routes/rules @@ -332,8 +281,8 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): cr_lrp_datapath) if vlan_tag: - self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips, - vlan_tag=vlan_tag) + self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips, + vlan_tag=vlan_tag) else: cr_lrps_on_same_provider = [ p for p in self.ovn_local_cr_lrps.values() @@ -342,18 +291,14 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): # NOTE: no need to remove the NDP proxy if there are other # cr-lrp ports on the same chassis connected to the same # provider flat network - self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips, - cleanup_ndp_proxy=False) + self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips, + cleanup_ndp_proxy=False) else: - self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips) + self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips) - nei_bridge = datapath_bridge - if vlan_tag: - nei_bridge = '{}.{}'.format(datapath_bridge, vlan_tag) + nei_dev = cr_lrp_info['vlan'] if vlan_tag else cr_lrp_info['veth_vrf'] for ip in ips: - linux_net.del_ip_nei( - ip, self.ovn_local_cr_lrps[cr_lrp_port_name]['mac'], - nei_bridge) + linux_net.del_ip_nei(ip, cr_lrp_info['mac'], nei_dev) self._remove_evpn_devices(evpn_vni) ovs.remove_evpn_router_ovs_flows(datapath_bridge, @@ -447,46 +392,66 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) - ip_version = linux_net.get_ip_version(ip) + self._expose_subnet(ip, cr_lrp_ips, cr_lrp_info, datapath_bridge, + vlan_tag, row.datapath) + + def _expose_subnet(self, router_interface, cr_lrp_ips, cr_lrp_info, + datapath_bridge, vlan_tag, network_datapath): + router_interface_ip_version = linux_net.get_ip_version( + router_interface) + if vlan_tag: + dev = cr_lrp_info['vlan'] + dev_ovs = dev + strip_vlan = True + else: + dev = cr_lrp_info['veth_vrf'] + dev_ovs = cr_lrp_info['veth_ovs'] + strip_vlan = False + for cr_lrp_ip in cr_lrp_ips: - if linux_net.get_ip_version(cr_lrp_ip) == ip_version: + if (linux_net.get_ip_version(cr_lrp_ip) == + router_interface_ip_version): linux_net.add_ip_route( self._ovn_routing_tables_routes, - ip.split("/")[0], - evpn_info['vni'], - datapath_bridge, - vlan=vlan_tag, - mask=ip.split("/")[1], + router_interface.split("/")[0], + cr_lrp_info['vni'], + dev, + mask=router_interface.split("/")[1], via=cr_lrp_ip) break - if ip_version == constants.IP_VERSION_6: + if router_interface_ip_version == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( - ip, strict=False)) + router_interface, strict=False)) else: net_ip = '{}'.format(ipaddress.IPv4Network( - ip, strict=False)) + router_interface, strict=False)) - strip_vlan = False - if vlan_tag: - strip_vlan = True + # NOTE(ltomasbo): strip_vlan is used for subnets/routers associated to + # provider vlan networks assuming the EVPN VXLAN header is replacing + # the vlan id in the fabric. If that is not the case, we could simply + # set this to False in all the cases and have the traffic sent with + # both vxlan header (for the EVPN) plus the vlan header (related to + # the provider vlan id being used) ovs.ensure_evpn_ovs_flow(datapath_bridge, constants.OVS_VRF_RULE_COOKIE, cr_lrp_info['mac'], - cr_lrp_info['vrf'], + dev_ovs, + dev, net_ip, - strip_vlan) + strip_vlan=strip_vlan) # Check if there are VMs on the network # and if so expose the route - network_port_datapath = row.datapath - if not network_port_datapath: + if not network_datapath: return ports = self.sb_idl.get_ports_on_datapath( - network_port_datapath) + network_datapath) for port in ports: if (port.type not in (constants.OVN_VM_VIF_PORT_TYPE, - constants.OVN_VIRTUAL_VIF_PORT_TYPE)): + constants.OVN_VIRTUAL_VIF_PORT_TYPE) or + (port.type == constants.OVN_VM_VIF_PORT_TYPE and + not port.chassis)): continue try: port_ips = [port.mac[0].split(' ')[1]] @@ -499,10 +464,10 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): # 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: + if port_ip_version == router_interface_ip_version: linux_net.add_ips_to_dev( cr_lrp_info['lo'], [port_ip], - clear_local_route_at_table=evpn_info['vni']) + clear_local_route_at_table=cr_lrp_info['vni']) self._ovn_exposed_evpn_ips.setdefault( cr_lrp_info['lo'], []).extend([port_ip]) @@ -534,6 +499,11 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) + if vlan_tag: + dev = cr_lrp_info['vlan'] + else: + dev = cr_lrp_info['veth_vrf'] + 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: @@ -541,8 +511,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): self._ovn_routing_tables_routes, ip.split("/")[0], cr_lrp_info['vni'], - datapath_bridge, - vlan=vlan_tag, + dev, mask=ip.split("/")[1], via=cr_lrp_ip) if (linux_net.get_ip_version(cr_lrp_ip) == @@ -570,7 +539,25 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): LOG.debug("Router Interface port already cleanup from the agent " "%s", lrp_logical_port) - def _ensure_evpn_devices(self, vni): + def _ensure_evpn_devices(self, datapath_bridge, vni, vlan_tag): + '''Create the needed devices for EVPN connectivity + + This method creates and associate the needed devices for EVPN + connectivity. It creates: + - VRF device + - Linux Bridge device, associated to the VRF + - VXLAN device, using loopback IP, associate to the bridge + - Dummy device to expose the IPs, associated to the VRF + - If vlan_tag, create vlan device on OVS bridge, associated to the VRF + - If no vlan_tag, create veth pair, one end associated to the VRF + + param datapath_bridge: OVS bridge to connect the vlan device + param vni: VNI number to use for vxlan tunnel ids and vrf routing table + param vlan_tag: vlan id to use for connectivity + + return: a namedtuple with the name of the devices created: vrf_name, + lo_name, bridge_name, vxlan_name, veth_vrf, veth_ovs, and vlan_name. + ''' # ensure vrf device. # NOTE: It uses vni id as table number vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) @@ -601,61 +588,100 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): # connect dummy to vrf linux_net.set_master_for_device(lo_name, vrf_name) - return vrf_name, lo_name, bridge_name, vxlan_name + if vlan_tag: + vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(vni) + # add vlan port to OVS bridge + ovs.add_vlan_port_to_ovs_bridge(datapath_bridge, vlan_name, + vlan_tag) + linux_net.set_device_status(vlan_name, constants.LINK_UP) + # connect vlan to vrf + linux_net.set_master_for_device(vlan_name, vrf_name) + # ensure proxy NDP is enabled for ipv6 traffic + linux_net.enable_proxy_ndp(vlan_name) + + return EVPN_INFO(vrf_name, lo_name, bridge_name, vxlan_name, None, + None, vlan_name) + else: + # ensure veth-pair interfaces + veth_vrf = constants.OVN_EVPN_VETH_VRF_PREFIX + str(vni) + veth_ovs = constants.OVN_EVPN_VETH_OVS_PREFIX + str(vni) + linux_net.ensure_veth(veth_vrf, veth_ovs) + # connect veth to vrf + linux_net.set_master_for_device(veth_vrf, vrf_name) + + return EVPN_INFO(vrf_name, lo_name, bridge_name, vxlan_name, + veth_vrf, veth_ovs, None) def _remove_evpn_devices(self, vni): vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(vni) vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(vni) lo_name = constants.OVN_EVPN_LO_PREFIX + str(vni) + veth_name = constants.OVN_EVPN_VETH_VRF_PREFIX + str(vni) + vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(vni) - for device in [lo_name, vrf_name, bridge_name, vxlan_name]: + for device in [lo_name, vrf_name, bridge_name, vxlan_name, veth_name, + vlan_name]: linux_net.delete_device(device) - def _connect_evpn_to_ovn(self, vrf, ips, datapath_bridge, vni, vlan_tag): - # add vrf to ovs bridge - ovs.add_device_to_ovs_bridge(vrf, datapath_bridge, vlan_tag) + def _connect_evpn_to_ovn(self, vrf, veth_vrf, veth_ovs, ips, + datapath_bridge, vni, vlan, vlan_tag): + # NOTE(ltomasbo): vlan device is already attached to ovs bridge + # when created + if not vlan_tag: + # add veth to ovs bridge + ovs.add_device_to_ovs_bridge(veth_ovs, datapath_bridge) - if vlan_tag: - linux_net.ensure_vlan_device_for_network(datapath_bridge, vlan_tag) # add route for ip to ovs provider bridge (at the vrf routing table) for ip in ips: ip_without_mask = ip.split("/")[0] - linux_net.add_ip_route( - self._ovn_routing_tables_routes, ip_without_mask, - vni, datapath_bridge, vlan=vlan_tag) - - # add proxy ndp config for ipv6 - if (linux_net.get_ip_version(ip_without_mask) == - constants.IP_VERSION_6): - linux_net.add_ndp_proxy(ip, datapath_bridge, vlan=vlan_tag) + if vlan_tag: + # ip route add GW_PORT_IP dev VLAN_DEVICE table VRF_TABLE_ID + linux_net.add_ip_route( + self._ovn_routing_tables_routes, ip_without_mask, + vni, vlan) + # add proxy ndp config for ipv6 + if (linux_net.get_ip_version(ip_without_mask) == + constants.IP_VERSION_6): + linux_net.add_ndp_proxy(ip, vlan) + else: + linux_net.add_ip_route( + self._ovn_routing_tables_routes, ip_without_mask, + vni, veth_vrf) + # add proxy ndp config for ipv6 + if (linux_net.get_ip_version(ip_without_mask) == + constants.IP_VERSION_6): + linux_net.add_ndp_proxy(ip, datapath_bridge) # add unreachable route to vrf linux_net.add_unreachable_route(vrf) - def _disconnect_evpn_to_ovn(self, vni, datapath_bridge, ips, - vlan_tag=None, cleanup_ndp_proxy=True): - vrf = constants.OVN_EVPN_VRF_PREFIX + str(vni) - # remove vrf from ovs bridge - ovs.del_device_from_ovs_bridge(vrf, datapath_bridge) + def _disconnect_evpn_from_ovn(self, vni, datapath_bridge, ips, + vlan_tag=None, cleanup_ndp_proxy=True): + if vlan_tag: + # remove vlan from ovs bridge + device = constants.OVN_EVPN_VLAN_PREFIX + str(vni) + else: + # remove veth from ovs bridge + device = constants.OVN_EVPN_VETH_OVS_PREFIX + str(vni) + ovs.del_device_from_ovs_bridge(device, datapath_bridge) linux_net.delete_routes_from_table(vni) - if vlan_tag: - linux_net.delete_vlan_device_for_network(datapath_bridge, - vlan_tag) - elif cleanup_ndp_proxy: + if cleanup_ndp_proxy: for ip in ips: if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: linux_net.del_ndp_proxy(ip, datapath_bridge) def _remove_extra_vrfs(self): - vrfs, los, bridges, vxlans = ([], [], [], []) + vrfs, los, bridges, vxlans, veths, vlans = ([], [], [], [], [], []) for cr_lrp_info in self.ovn_local_cr_lrps.values(): vrfs.append(cr_lrp_info['vrf']) los.append(cr_lrp_info['lo']) bridges.append(cr_lrp_info['bridge']) vxlans.append(cr_lrp_info['vxlan']) + veths.append(cr_lrp_info['veth_vrf']) + vlans.append(cr_lrp_info['vlan']) filter_out = ["{}.{}".format(key, value[0]['vlan']) for key, value in self._ovn_routing_tables_routes.items() @@ -666,7 +692,6 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): if (interface.startswith(constants.OVN_EVPN_VRF_PREFIX) and interface not in vrfs): linux_net.delete_device(interface) - ovs.del_device_from_ovs_bridge(interface) elif (interface.startswith(constants.OVN_EVPN_LO_PREFIX) and interface not in los): linux_net.delete_device(interface) @@ -678,6 +703,13 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): elif (interface.startswith(constants.OVN_EVPN_VXLAN_PREFIX) and interface not in vxlans): linux_net.delete_device(interface) + elif (interface.startswith(constants.OVN_EVPN_VETH_VRF_PREFIX) and + interface not in veths): + linux_net.delete_device(interface) + ovs.del_device_from_ovs_bridge(interface) + elif (interface.startswith(constants.OVN_EVPN_VLAN_PREFIX) and + interface not in vlans): + ovs.del_device_from_ovs_bridge(interface) def _remove_extra_routes(self): table_ids = self._get_table_ids() @@ -685,13 +717,9 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): if not vrf_routes: return # remove from vrf_routes the routes that should be kept - for bridge, routes_info in self._ovn_routing_tables_routes.items(): + for device, routes_info in self._ovn_routing_tables_routes.items(): for route_info in routes_info: - oif = linux_net.get_interface_index(bridge) - if route_info['vlan']: - vlan_device_name = '{}.{}'.format(bridge, - route_info['vlan']) - oif = linux_net.get_interface_index(vlan_device_name) + oif = linux_net.get_interface_index(device) if 'gateway' in route_info['route'].keys(): # subnet route possible_matchings = [ r for r in vrf_routes @@ -712,7 +740,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): linux_net.delete_ip_routes(vrf_routes) def _remove_extra_ovs_flows(self): - cr_lrp_mac_vrf_mappings = self._get_cr_lrp_mac_vrf_mapping() + cr_lrp_mac_mappings = self._get_cr_lrp_mac_mapping() cookie_id = "cookie={}/-1".format(constants.OVS_VRF_RULE_COOKIE) for bridge in set(self.ovn_bridge_mappings.values()): current_flows = ovs.get_bridge_flows(bridge, filter_=cookie_id) @@ -720,7 +748,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): flow_info = ovs.get_flow_info(flow) if not flow_info.get('mac'): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) - elif flow_info['mac'] not in cr_lrp_mac_vrf_mappings.keys(): + elif flow_info['mac'] not in cr_lrp_mac_mappings.keys(): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) elif flow_info['port']: if (not flow_info.get('nw_src') and not @@ -728,9 +756,17 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) else: - device = cr_lrp_mac_vrf_mappings[flow_info['mac']] - vrf_port = ovs.get_device_port_at_ovs(device) - if vrf_port != flow_info['port']: + dev_info = cr_lrp_mac_mappings[flow_info['mac']] + if dev_info.get('vlan'): + dev = dev_info['vlan'] + dev_ovs = dev + else: + dev = dev_info['veth_vrf'] + dev_ovs = dev_info['veth_ovs'] + dev_ovs_port = ovs.get_device_port_at_ovs( + dev_ovs) + + if dev_ovs_port != flow_info['port']: ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) nw_src_ip = nw_src_mask = None @@ -745,7 +781,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): flow_info['ipv6_src'].split('/')[1]) for route_info in self._ovn_routing_tables_routes[ - bridge]: + dev]: if (route_info['route']['dst'] == nw_src_ip and route_info['route'][ 'dst_len'] == nw_src_mask): @@ -767,8 +803,11 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): table_ids.append(cr_lrp_info['vni']) return table_ids - def _get_cr_lrp_mac_vrf_mapping(self): - mac_vrf_mappings = {} + def _get_cr_lrp_mac_mapping(self): + mac_mappings = {} for cr_lrp_info in self.ovn_local_cr_lrps.values(): - mac_vrf_mappings[cr_lrp_info['mac']] = cr_lrp_info['vrf'] - return mac_vrf_mappings + mac_mappings[cr_lrp_info['mac']] = { + 'veth_vrf': cr_lrp_info['veth_vrf'], + 'veth_ovs': cr_lrp_info['veth_ovs'], + 'vlan': cr_lrp_info['vlan']} + return mac_mappings diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovs.py b/ovn_bgp_agent/drivers/openstack/utils/ovs.py index a355dc7c..0d2870f7 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/ovs.py +++ b/ovn_bgp_agent/drivers/openstack/utils/ovs.py @@ -29,6 +29,8 @@ LOG = logging.getLogger(__name__) def _find_ovs_port(bridge): + # TODO(ltomasbo): What happens if there are several patch ports on the + # same bridge? ovs_port = None ovs_ports = ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['list-ports', bridge])[0].rstrip() @@ -95,12 +97,13 @@ def remove_extra_ovs_flows(flows_info, cookie): 'ovs-ofctl', ['del-flows', bridge, del_flow]) -def ensure_evpn_ovs_flow(bridge, cookie, mac, port, net, strip_vlan=False): +def ensure_evpn_ovs_flow(bridge, cookie, mac, output_port, port_dst, net, + strip_vlan=False): ovs_port = _find_ovs_port(bridge) if not ovs_port: return ovs_ofport = get_device_port_at_ovs(ovs_port) - vrf_ofport = get_device_port_at_ovs(port) + vrf_ofport = get_device_port_at_ovs(output_port) strip_vlan_opt = 'strip_vlan,' if strip_vlan else '' ip_version = linux_net.get_ip_version(net) @@ -110,14 +113,14 @@ def ensure_evpn_ovs_flow(bridge, cookie, mac, port, net, strip_vlan=False): "cookie={},priority=1000,ipv6,in_port={},dl_src:{}," "ipv6_src={} actions=mod_dl_dst:{},{}output={}".format( cookie, ovs_ofport, mac, net, - ndb.interfaces[bridge]['address'], strip_vlan_opt, + ndb.interfaces[port_dst]['address'], strip_vlan_opt, vrf_ofport)) else: flow = ( "cookie={},priority=1000,ip,in_port={},dl_src:{},nw_src={}" "actions=mod_dl_dst:{},{}output={}".format( cookie, ovs_ofport, mac, net, - ndb.interfaces[bridge]['address'], strip_vlan_opt, + ndb.interfaces[port_dst]['address'], strip_vlan_opt, vrf_ofport)) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['add-flow', bridge, flow]) @@ -216,6 +219,15 @@ def del_device_from_ovs_bridge(device, bridge=None): ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd('ovs-vsctl', args) +def add_vlan_port_to_ovs_bridge(bridge, vlan, vlan_tag): + # ovs-vsctl add-port BRIDGE VLAN tag=VALN_ID + # -- set interface VLAN type=internal + args = [ + '--may-exist', 'add-port', bridge, vlan, 'tag={}'.format(vlan_tag), + '--', 'set', 'interface', vlan, 'type=internal'] + ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd('ovs-vsctl', args) + + def del_flow(flow, bridge, cookie): cookie_id = "cookie={}/-1".format(cookie) f = '{},priority{}'.format( diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py index fc038dec..d631501d 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py @@ -142,10 +142,11 @@ class TestOVS(test_base.TestCase): @mock.patch.object(linux_net, 'get_ip_version') def _test_ensure_evpn_ovs_flow(self, mock_ip_version, mock_ofport, ip_version, strip_vlan=False): - address = '172.24.200.7' - self.fake_ndb.interfaces[self.bridge] = {'address': address} + address = '00:00:00:00:00:00' mock_ip_version.return_value = ip_version port = 'fake-port' + port_dst = 'fake-port-dst' + self.fake_ndb.interfaces[port_dst] = {'address': address} ovs_port = constants.OVS_PATCH_PROVNET_PORT_PREFIX + 'fake-port' port_iface = '1' ovs_port_iface = '2' @@ -156,7 +157,7 @@ class TestOVS(test_base.TestCase): # Invoke the method ovs_utils.ensure_evpn_ovs_flow( - self.bridge, self.cookie, self.mac, port, net, + self.bridge, self.cookie, self.mac, port, port_dst, net, strip_vlan=strip_vlan) mock_ip_version.assert_called_once_with(net) @@ -202,7 +203,8 @@ class TestOVS(test_base.TestCase): self.mock_ovs_vsctl.ovs_cmd.return_value = [port] ret = ovs_utils.ensure_evpn_ovs_flow( - self.bridge, self.cookie, self.mac, port, 'fake-net') + self.bridge, self.cookie, self.mac, port, 'fake-port-dst', + 'fake-net') self.assertIsNone(ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( diff --git a/ovn_bgp_agent/utils/linux_net.py b/ovn_bgp_agent/utils/linux_net.py index 66478af6..3c958104 100644 --- a/ovn_bgp_agent/utils/linux_net.py +++ b/ovn_bgp_agent/utils/linux_net.py @@ -84,6 +84,17 @@ def ensure_vxlan(vxlan_name, vni, lo_ip): 'state', constants.LINK_UP).commit() +def ensure_veth(veth_name, veth_peer): + try: + set_device_status(veth_name, constants.LINK_UP) + except KeyError: + with pyroute2.NDB() as ndb: + ndb.interfaces.create( + kind="veth", ifname=veth_name, peer=veth_peer).set( + 'state', constants.LINK_UP).commit() + set_device_status(veth_peer, constants.LINK_UP) + + def set_master_for_device(device, master): with pyroute2.NDB() as ndb: # Check if already associated to the master, and associate it if not @@ -93,6 +104,13 @@ def set_master_for_device(device, master): iface.set('master', ndb.interfaces[master]['index']) +def set_device_status(device, status): + with pyroute2.NDB() as ndb: + with ndb.interfaces[device] as dev: + if dev['state'] != status: + dev['state'] = status + + def ensure_dummy_device(device): with pyroute2.NDB() as ndb: try: @@ -237,10 +255,9 @@ def ensure_vlan_device_for_network(bridge, vlan_tag): link=ndb.interfaces[bridge]['index']).set( 'state', constants.LINK_UP).commit() - ipv4_flag = "net.ipv4.conf.{}/{}.proxy_arp".format(bridge, vlan_tag) - ovn_bgp_agent.privileged.linux_net.set_kernel_flag(ipv4_flag, 1) - ipv6_flag = "net.ipv6.conf.{}/{}.proxy_ndp".format(bridge, vlan_tag) - ovn_bgp_agent.privileged.linux_net.set_kernel_flag(ipv6_flag, 1) + device = "{}/{}".format(bridge, vlan_tag) + enable_proxy_arp(device) + enable_proxy_ndp(device) def delete_vlan_device_for_network(bridge, vlan_tag): @@ -248,6 +265,16 @@ def delete_vlan_device_for_network(bridge, vlan_tag): delete_device(vlan_device_name) +def enable_proxy_ndp(device): + flag = "net.ipv6.conf.{}.proxy_ndp".format(device) + ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) + + +def enable_proxy_arp(device): + flag = "net.ipv4.conf.{}.proxy_arp".format(device) + ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) + + def get_exposed_ips(nic): exposed_ips = [] with pyroute2.NDB() as ndb: @@ -272,10 +299,15 @@ def get_nic_ip(nic, ip_version): def get_exposed_ips_on_network(nic, network): exposed_ips = [] with pyroute2.NDB() as ndb: - exposed_ips = [ip.address - for ip in ndb.interfaces[nic].ipaddr.summary() - if ((ip.prefixlen == 32 or ip.prefixlen == 128) and - ipaddress.ip_address(ip.address) in network)] + try: + exposed_ips = [ip.address + for ip in ndb.interfaces[nic].ipaddr.summary() + if ((ip.prefixlen == 32 or ip.prefixlen == 128) and + ipaddress.ip_address(ip.address) in network)] + except KeyError: + # Nic does not exists + LOG.debug("Nic %s does not yet exists, so it does not have " + "exposed IPs", nic) return exposed_ips @@ -411,10 +443,11 @@ def del_ndp_proxy(ip, dev, vlan=None): def add_ips_to_dev(nic, ips, clear_local_route_at_table=False): - with pyroute2.NDB() as ndb: + already_added_ips = [] + for ip in ips: try: - with ndb.interfaces[nic] as iface: - for ip in ips: + with pyroute2.NDB() as ndb: + with ndb.interfaces[nic] as iface: address = '{}/32'.format(ip) if get_ip_version(ip) == constants.IP_VERSION_6: address = '{}/128'.format(ip) @@ -422,16 +455,21 @@ def add_ips_to_dev(nic, ips, clear_local_route_at_table=False): except KeyError: # NDB raises KeyError: 'object exists' # if the ip is already added - pass + already_added_ips.append(ip) if clear_local_route_at_table: - with pyroute2.NDB() as ndb: - for ip in ips: + for ip in ips: + with pyroute2.NDB() as ndb: + oif = ndb.interfaces[nic]['index'] + if ip in already_added_ips: + continue route = {'table': clear_local_route_at_table, 'proto': 2, 'scope': 254, - 'dst': ip} + 'dst': ip, + 'oif': oif} try: + LOG.debug("Deleting local route: %s", route) with ndb.routes[route] as r: r.remove() except (KeyError, ValueError): @@ -546,8 +584,14 @@ def del_ip_nei(ip, lladdr, dev): # This is doing something like: # sudo ip nei del 172.24.4.69 # lladdr fa:16:3e:d3:5d:7b dev br-ex nud permanent - network_bridge_if = iproute.link_lookup( - ifname=dev)[0] + try: + network_bridge_if = iproute.link_lookup( + ifname=dev)[0] + except IndexError: + # Neigbhbor device does not exists, continuing + LOG.debug("No need to remove nei for dev %s as it does not " + "exists", dev) + return if ip_version == constants.IP_VERSION_6: iproute.neigh('del', dst=ip.split("/")[0], @@ -607,6 +651,7 @@ def add_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, with ndb.routes[route] as r: LOG.debug("Route already existing: %s", r) except KeyError: + LOG.debug("Creating route at table %s: %s", route_table, route) ndb.routes.create(route).commit() LOG.debug("Route created at table %s: %s", route_table, route) route_info = {'vlan': vlan, 'route': route} @@ -631,11 +676,18 @@ def del_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, ip, strict=False).network_address) with pyroute2.NDB() as ndb: - if vlan: - oif_name = '{}.{}'.format(dev, vlan) - oif = ndb.interfaces[oif_name]['index'] - else: - oif = ndb.interfaces[dev]['index'] + try: + if vlan: + oif_name = '{}.{}'.format(dev, vlan) + oif = ndb.interfaces[oif_name]['index'] + else: + oif = ndb.interfaces[dev]['index'] + except KeyError: + LOG.debug("Device %s does not exists, so the associated " + "routes should have been automatically deleted.", dev) + if ovn_routing_tables_routes.get(dev): + del ovn_routing_tables_routes[dev] + return route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, 'table': int(route_table), 'proto': 3} @@ -649,6 +701,7 @@ def del_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, with pyroute2.NDB() as ndb: try: + LOG.debug("Deleting route at table %s: %s", route_table, route) with ndb.routes[route] as r: r.remove() LOG.debug("Route deleted at table %s: %s", route_table, route)