f63f99d822
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
397 lines
15 KiB
Python
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)
|