neutron-vpnaas/neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py
Niklas Schwarz f63f99d822 Add type annotations
Add type annotations to the different parameters
and return values to modernize the python used.
This will also introduce mypy as another tool
for static code analysis which will currently not
run in CI

Change-Id: Ic09e47673f916328568c413d0e8485d36c283c24
2024-04-18 10:42:46 +02:00

397 lines
15 KiB
Python

# Copyright (c) 2016 Yi Jing Zhu, IBM.
# Copyright (c) 2023 SysEleven GmbH
# 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.
import typing as ty
import abc
import netaddr
from neutron.agent.common import utils as agent_common_utils
from neutron.agent.linux import interface
from neutron.agent.linux import ip_lib
from neutron_lib import constants as lib_constants
from neutron_lib import context as nctx
from oslo_concurrency import lockutils
from oslo_log import log as logging
from neutron_vpnaas.services.vpn.common import topics
from neutron_vpnaas.services.vpn.device_drivers import ipsec
from neutron_vpnaas.services.vpn.device_drivers import libreswan_ipsec
from neutron_vpnaas.services.vpn.device_drivers import strongswan_ipsec
PORT_PREFIX_INTERNAL = 'vr'
PORT_PREFIX_EXTERNAL = 'vg'
PORT_PREFIXES: ty.Dict[str, str] = {
'internal': PORT_PREFIX_INTERNAL,
'external': PORT_PREFIX_EXTERNAL,
}
LOG = logging.getLogger(__name__)
class DeviceManager:
"""Device Manager for ports in qvpn-xx namespace.
It is a veth pair, one side in qvpn and the other
side is attached to ovs.
"""
OVN_NS_PREFIX = "qvpn-"
def __init__(self, conf, host, plugin, context):
self.conf = conf
self.host = host
self.plugin = plugin
self.context = context
self.driver: interface.LinuxInterfaceDriver = \
agent_common_utils.load_interface_driver(conf)
def get_interface_name(self, port: ty.Dict[str, str], ptype: str) -> str:
suffix = port['id']
return (PORT_PREFIXES[ptype] + suffix)[:self.driver.DEV_NAME_LEN]
def get_namespace_name(self, process_id: str):
return self.OVN_NS_PREFIX + process_id
def get_existing_process_ids(self) -> ty.List:
"""Return the process IDs derived from the existing VPN namespaces."""
return [ns[len(self.OVN_NS_PREFIX):]
for ns in ip_lib.list_network_namespaces()
if ns.startswith(self.OVN_NS_PREFIX)]
def set_default_route(self, namespace, subnet, device_name):
device = ip_lib.IPDevice(device_name, namespace=namespace)
gateway = device.route.get_gateway(ip_version=subnet['ip_version'])
if gateway:
gateway = gateway.get('gateway')
new_gateway = subnet['gateway_ip']
if gateway == new_gateway:
return
device.route.add_gateway(subnet['gateway_ip'])
def add_routes(self, namespace, cidrs, via):
device = ip_lib.IPDevice(None, namespace=namespace)
for cidr in cidrs:
device.route.add_route(cidr, via=via, metric=100, proto='static')
def delete_routes(self, namespace, cidrs, via):
device = ip_lib.IPDevice(None, namespace=namespace)
for cidr in cidrs:
device.route.delete_route(cidr, via=via, metric=100,
proto='static')
def list_routes(self, namespace,
via=None) -> ty.List[ty.Dict[str, ty.Any]]:
device = ip_lib.IPDevice(None, namespace=namespace)
return device.route.list_routes(
lib_constants.IP_VERSION_4, proto='static', via=via)
def del_static_routes(self, namespace):
device = ip_lib.IPDevice(None, namespace=namespace)
routes = device.route.list_routes(
lib_constants.IP_VERSION_4, proto='static')
for r in routes:
device.route.delete_route(r['cidr'], via=r['via'])
def _del_port(self, process_id: str, ptype: str):
namespace = self.get_namespace_name(process_id)
prefix = PORT_PREFIXES[ptype]
device = ip_lib.IPDevice(None, namespace=namespace)
ports: ty.List[ty.Dict[str, ty.Union[str, ty.Any]]] = \
device.addr.list()
for p in ports:
if not p['name'].startswith(prefix):
continue
interface_name = p['name']
self.driver.unplug(interface_name, namespace=namespace)
def del_internal_port(self, process_id: str):
self._del_port(process_id, 'internal')
def del_external_port(self, process_id: str):
self._del_port(process_id, 'external')
def setup_external(self, process_id: str,
network_details) -> ty.Optional[str]:
network = network_details["external_network"]
vpn_port = network_details['gw_port']
ns_name = self.get_namespace_name(process_id)
interface_name = self.get_interface_name(vpn_port, 'external')
if not ip_lib.ensure_device_is_ready(interface_name,
namespace=ns_name):
try:
self.driver.plug(network['id'],
vpn_port['id'],
interface_name,
vpn_port['mac_address'],
namespace=ns_name,
mtu=network.get('mtu'),
prefix=PORT_PREFIX_EXTERNAL)
except Exception:
LOG.exception('plug external port %s failed', vpn_port)
return None
ip_cidrs = []
subnets = []
for fixed_ip in vpn_port['fixed_ips']:
subnet_id = fixed_ip['subnet_id']
subnet = self.plugin.get_subnet_info(subnet_id)
net = netaddr.IPNetwork(subnet['cidr'])
ip_cidr = f'{fixed_ip["ip_address"]}/{net.prefixlen}'
ip_cidrs.append(ip_cidr)
subnets.append(subnet)
self.driver.init_l3(interface_name, ip_cidrs,
namespace=ns_name)
for subnet in subnets:
self.set_default_route(ns_name, subnet, interface_name)
return interface_name
def setup_internal(self, process_id, network_details) -> ty.Optional[str]:
vpn_port = network_details["transit_port"]
ns_name = self.get_namespace_name(process_id)
interface_name = self.get_interface_name(vpn_port, 'internal')
if not ip_lib.ensure_device_is_ready(interface_name,
namespace=ns_name):
try:
self.driver.plug('',
vpn_port['id'],
interface_name,
vpn_port['mac_address'],
namespace=ns_name,
prefix=PORT_PREFIX_INTERNAL)
except Exception:
LOG.exception('plug internal port %s failed', vpn_port['id'])
return None
ip_cidrs = []
for fixed_ip in vpn_port['fixed_ips']:
ip_cidr = f'{fixed_ip["ip_address"]}/28'
ip_cidrs.append(ip_cidr)
self.driver.init_l3(interface_name, ip_cidrs,
namespace=ns_name)
return interface_name
class NamespaceManager:
def __init__(self, use_ipv6=False):
self.ip_wrapper_root = ip_lib.IPWrapper()
self.use_ipv6 = use_ipv6
def exists(self, name) -> bool:
return ip_lib.network_namespace_exists(name)
def create(self, name):
ip_wrapper = self.ip_wrapper_root.ensure_namespace(name)
cmd = ['sysctl', '-w', 'net.ipv4.ip_forward=1']
ip_wrapper.netns.execute(cmd)
if self.use_ipv6:
cmd = ['sysctl', '-w', 'net.ipv6.conf.all.forwarding=1']
ip_wrapper.netns.execute(cmd)
def delete(self, name):
try:
self.ip_wrapper_root.netns.delete(name)
except RuntimeError:
msg = 'Failed trying to delete namespace: %s'
LOG.exception(msg, name)
class OvnOpenSwanProcess(ipsec.OpenSwanProcess):
pass
class OvnStrongSwanProcess(strongswan_ipsec.StrongSwanProcess):
pass
class OvnLibreSwanProcess(libreswan_ipsec.LibreSwanProcess):
pass
class IPsecOvnDriverApi(ipsec.IPsecVpnDriverApi):
def __init__(self, topic):
super().__init__(topic)
self.admin_ctx = nctx.get_admin_context_without_session()
def get_vpn_transit_network_details(self, router_id):
cctxt = self.client.prepare()
return cctxt.call(self.admin_ctx, 'get_vpn_transit_network_details',
router_id=router_id)
def get_subnet_info(self, subnet_id):
cctxt = self.client.prepare()
return cctxt.call(self.admin_ctx, 'get_subnet_info',
subnet_id=subnet_id)
class OvnIPsecDriver(ipsec.IPsecDriver, metaclass=abc.ABCMeta):
def __init__(self, vpn_service, host: str):
self.nsmgr = NamespaceManager()
super().__init__(vpn_service, host)
self.agent_rpc = IPsecOvnDriverApi(topics.IPSEC_DRIVER_TOPIC)
self.devmgr = DeviceManager(self.conf, self.host,
self.agent_rpc, self.context)
get_router_based_iptables_manager = None
def get_namespace(self, router_id) -> str:
"""Get namespace for VPN services of router.
:router_id: router_id
:returns: namespace string.
"""
return self.devmgr.get_namespace_name(router_id)
def _cleanup_namespace(self, router_id: str):
ns_name = self.devmgr.get_namespace_name(router_id)
if not self.nsmgr.exists(ns_name):
return
self.devmgr.del_internal_port(router_id)
self.devmgr.del_external_port(router_id)
self.nsmgr.delete(ns_name)
def _ensure_namespace(self, router_id: str, network_details) -> str:
ns_name = self.get_namespace(router_id)
if not self.nsmgr.exists(ns_name):
self.nsmgr.create(ns_name)
# set up vpn external port on provider net
self.devmgr.setup_external(router_id, network_details)
# set up vpn internal port on transit net
self.devmgr.setup_internal(router_id, network_details)
return ns_name
@abc.abstractmethod
def create_process(self, process_id: str,
vpnservice, namespace) -> ipsec.BaseSwanProcess:
pass
def destroy_process(self, process_id: str):
LOG.info('process %s is destroyed', process_id)
namespace = self.devmgr.get_namespace_name(process_id)
# If the namespace exists but the process_id is not in the table
# there may be an active swan process from a previous run of the agent
# which does not have a process object in memory.
# To be able to clean it up we need to create a dummy process object
# here (without a vpnservice), so that destroy_process will stop
# the swan.
if self.nsmgr.exists(namespace) and process_id not in self.processes:
self.ensure_process(process_id)
super().destroy_process(process_id)
self._cleanup_namespace(process_id)
def create_router(self, router):
pass
def destroy_router(self, process_id):
pass
def _update_nat(self, vpnservice, func):
pass
def _update_route(self, vpnservice, network_details):
router_id = vpnservice['router_id']
gateway_ip = network_details['transit_gateway_ip']
namespace = self.devmgr.get_namespace_name(router_id)
old_local_cidrs = set()
for route in self.devmgr.list_routes(namespace, via=gateway_ip):
old_local_cidrs.add(route['cidr'])
new_local_cidrs = set()
for ipsec_site_conn in vpnservice['ipsec_site_connections']:
new_local_cidrs.update(ipsec_site_conn['local_cidrs'])
self.devmgr.delete_routes(namespace,
old_local_cidrs - new_local_cidrs,
gateway_ip)
self.devmgr.add_routes(namespace,
new_local_cidrs - old_local_cidrs,
gateway_ip)
def _sync_vpn_processes(self, vpnservices, sync_router_ids: ty.List[str]):
# Ensure the ipsec process is enabled only for
# - the vpn services which are not yet in self.processes
# - vpn services whose router id is in 'sync_router_ids'
for vpnservice in vpnservices:
router_id = vpnservice['router_id']
if router_id not in self.processes or router_id in sync_router_ids:
net_details = self.agent_rpc.get_vpn_transit_network_details(
router_id)
self._ensure_namespace(router_id, net_details)
self._update_route(vpnservice, net_details)
process = self.ensure_process(router_id, vpnservice=vpnservice)
process.update()
def _cleanup_stale_vpn_processes(self, vpn_router_ids: ty.List[str]):
super()._cleanup_stale_vpn_processes(vpn_router_ids)
# Look for additional namespaces on this node that we don't know
# and that should be deleted
for router_id in self.devmgr.get_existing_process_ids():
if router_id not in vpn_router_ids:
self.destroy_process(router_id)
@lockutils.synchronized('vpn-agent', 'neutron-')
def vpnservice_removed_from_agent(self, context: nctx.Context,
router_id: str):
# must run under the same lock as sync()
self.destroy_process(router_id)
def vpnservice_added_to_agent(self, context: nctx.Context,
router_ids: ty.List[str]):
routers = [{'id': router_id} for router_id in router_ids]
self.sync(context, routers)
class OvnStrongSwanDriver(OvnIPsecDriver):
def create_process(self, process_id: str, vpnservice,
namespace: str) -> ipsec.BaseSwanProcess:
return OvnStrongSwanProcess(
self.conf,
process_id,
vpnservice,
namespace)
class OvnOpenSwanDriver(OvnIPsecDriver):
def create_process(self, process_id: str, vpnservice,
namespace: str) -> ipsec.BaseSwanProcess:
return OvnOpenSwanProcess(
self.conf,
process_id,
vpnservice,
namespace)
class OvnLibreSwanDriver(OvnIPsecDriver):
def create_process(self, process_id: str, vpnservice,
namespace: str) -> ipsec.BaseSwanProcess:
return OvnLibreSwanProcess(
self.conf,
process_id,
vpnservice,
namespace)