VPNaaS support for OVN
Adds VPNaaS support for OVN. Add a new stand-alone VPN agent to support OVN+VPN. Add OVN-specific service and device drivers that support this new VPN agent. This will have no impact on the existing VPN solution for ML2/OVS, the existing L3 agent and its VPN extension will still work. Add a new VPN agent scheduler that will schedule VPN services to VPN agents on a per-router basis. Add two new database tables: vpn_ext_gws (to store extra port IDs) and routervpnagentbindings (to store VPN agent ID per router). More details see spec (neutron-specs/specs/xena/vpnaas-ovn.rst). This work is based on work of MingShuan Xian (xianms@cn.ibm.com), see https://bugs.launchpad.net/networking-ovn/+bug/1586253 Depends-On: https://review.opendev.org/c/openstack/neutron/+/847005 Depends-On: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/847007 Closes-Bug: #1905391 Change-Id: I632f86762d63edbfe225727db11ea21bbb1ffc25
This commit is contained in:
parent
e944dc144c
commit
256464aea6
@ -20,8 +20,12 @@
|
||||
- openstack-tox-py311:
|
||||
required-projects:
|
||||
- openstack/neutron
|
||||
- openstack-tox-docs:
|
||||
required-projects:
|
||||
- openstack/neutron
|
||||
- neutron-vpnaas-functional-sswan
|
||||
- neutron-tempest-plugin-vpnaas
|
||||
- neutron-tempest-plugin-vpnaas-ovn
|
||||
- neutron-tempest-plugin-vpnaas-libreswan-centos:
|
||||
# TODO(mlavalle) switch to voting when this job is moved to Centos
|
||||
# 8
|
||||
@ -40,8 +44,12 @@
|
||||
- openstack-tox-py311:
|
||||
required-projects:
|
||||
- openstack/neutron
|
||||
- openstack-tox-docs:
|
||||
required-projects:
|
||||
- openstack/neutron
|
||||
- neutron-vpnaas-functional-sswan
|
||||
- neutron-tempest-plugin-vpnaas
|
||||
- neutron-tempest-plugin-vpnaas-ovn
|
||||
# TODO(mlavalle) uncomment following line when the job is moved to
|
||||
# Centos 8
|
||||
# - neutron-tempest-plugin-vpnaas-libreswan-centos
|
||||
@ -62,6 +70,7 @@
|
||||
- openstack/neutron
|
||||
- neutron-vpnaas-openstack-tox-py310-with-sqlalchemy-main
|
||||
- neutron-tempest-plugin-vpnaas
|
||||
- neutron-tempest-plugin-vpnaas-ovn
|
||||
- neutron-vpnaas-functional-sswan
|
||||
|
||||
- job:
|
||||
|
44
devstack/ovn-local.conf.sample
Normal file
44
devstack/ovn-local.conf.sample
Normal file
@ -0,0 +1,44 @@
|
||||
[[local|localrc]]
|
||||
DATABASE_PASSWORD=password
|
||||
RABBIT_PASSWORD=password
|
||||
SERVICE_PASSWORD=password
|
||||
SERVICE_TOKEN=password
|
||||
ADMIN_PASSWORD=password
|
||||
|
||||
Q_AGENT=ovn
|
||||
Q_ML2_PLUGIN_MECHANISM_DRIVERS=ovn,logger
|
||||
Q_ML2_PLUGIN_TYPE_DRIVERS=local,flat,vlan,geneve
|
||||
Q_ML2_TENANT_NETWORK_TYPE=geneve
|
||||
|
||||
LOGFILE="/opt/stack/logs/devstacklog.txt"
|
||||
|
||||
enable_service ovn-northd
|
||||
enable_service ovn-controller
|
||||
enable_service q-ovn-metadata-agent
|
||||
enable_service q-ovn-vpn-agent
|
||||
enable_service q-svc
|
||||
enable_service q-log
|
||||
|
||||
# Disable Neutron agents not used with OVN.
|
||||
disable_service q-agt
|
||||
disable_service q-l3
|
||||
disable_service q-dhcp
|
||||
disable_service q-meta
|
||||
|
||||
enable_plugin neutron https://opendev.org/openstack/neutron
|
||||
enable_plugin neutron-tempest-plugin https://opendev.org/openstack/neutron-tempest-plugin.git
|
||||
enable_plugin neutron-vpnaas https://opendev.org/openstack/neutron-vpnaas.git
|
||||
|
||||
# Horizon (the web UI) is enabled by default. You may want to disable
|
||||
# it here to speed up DevStack a bit.
|
||||
enable_service horizon
|
||||
|
||||
# disable_service cinder c-sch c-api c-vol c-bak
|
||||
|
||||
#new
|
||||
# OVN_BUILD_MODULES=True
|
||||
#new
|
||||
# ENABLE_CHASSIS_AS_GW=True
|
||||
|
||||
# IPsec driver to use. Optional, defaults to strongswan.
|
||||
IPSEC_PACKAGE="strongswan"
|
@ -9,9 +9,14 @@ source $LIBDIR/l3_agent
|
||||
|
||||
NEUTRON_L3_CONF=${NEUTRON_L3_CONF:-$Q_L3_CONF_FILE}
|
||||
|
||||
function is_ovn_enabled {
|
||||
[[ $Q_AGENT == "ovn" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
function neutron_vpnaas_install {
|
||||
setup_develop $NEUTRON_VPNAAS_DIR
|
||||
if is_service_enabled q-l3 neutron-l3; then
|
||||
if is_service_enabled q-l3 neutron-l3 q-ovn-vpn-agent; then
|
||||
neutron_agent_vpnaas_install_agent_packages
|
||||
fi
|
||||
}
|
||||
@ -49,6 +54,43 @@ function neutron_vpnaas_configure_agent {
|
||||
fi
|
||||
}
|
||||
|
||||
function neutron_vpnaas_configure_ovn_agent {
|
||||
cp $NEUTRON_VPNAAS_DIR/etc/neutron_ovn_vpn_agent.ini.sample $OVN_VPNAGENT_CONF
|
||||
|
||||
iniset $OVN_VPNAGENT_CONF DEFAULT interface_driver openvswitch
|
||||
iniset $OVN_VPNAGENT_CONF DEFAULT state_path $DATA_DIR/neutron
|
||||
iniset_rpc_backend neutron-vpnaas $OVN_VPNAGENT_CONF
|
||||
iniset $OVN_VPNAGENT_CONF agent root_helper "$Q_RR_COMMAND"
|
||||
if [[ "$Q_USE_ROOTWRAP_DAEMON" == "True" ]]; then
|
||||
iniset $OVN_VPNAGENT_CONF agent root_helper_daemon "$Q_RR_DAEMON_COMMAND"
|
||||
fi
|
||||
|
||||
if [[ "$IPSEC_PACKAGE" == "strongswan" ]]; then
|
||||
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnStrongSwanDriver
|
||||
elif [[ "$IPSEC_PACKAGE" == "libreswan" ]]; then
|
||||
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnLibreSwanDriver
|
||||
else
|
||||
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver $NEUTRON_VPNAAS_DEVICE_DRIVER
|
||||
fi
|
||||
|
||||
OVSDB_SERVER_LOCAL_HOST=$SERVICE_LOCAL_HOST
|
||||
if [[ "$SERVICE_IP_VERSION" == 6 ]]; then
|
||||
OVSDB_SERVER_LOCAL_HOST=[$OVSDB_SERVER_LOCAL_HOST]
|
||||
fi
|
||||
OVN_SB_REMOTE=${OVN_SB_REMOTE:-$OVN_PROTO:$SERVICE_HOST:6642}
|
||||
|
||||
iniset $OVN_VPNAGENT_CONF ovs ovsdb_connection tcp:$OVSDB_SERVER_LOCAL_HOST:6640
|
||||
iniset $OVN_VPNAGENT_CONF ovn ovn_sb_connection $OVN_SB_REMOTE
|
||||
if is_service_enabled tls-proxy; then
|
||||
iniset $OVN_VPNAGENT_CONF ovn \
|
||||
ovn_sb_ca_cert $INT_CA_DIR/ca-chain.pem
|
||||
iniset $OVN_VPNAGENT_CONF ovn \
|
||||
ovn_sb_certificate $INT_CA_DIR/$DEVSTACK_CERT_NAME.crt
|
||||
iniset $OVN_VPNAGENT_CONF ovn \
|
||||
ovn_sb_private_key $INT_CA_DIR/private/$DEVSTACK_CERT_NAME.key
|
||||
fi
|
||||
}
|
||||
|
||||
function neutron_vpnaas_configure_db {
|
||||
$NEUTRON_BIN_DIR/neutron-db-manage --subproject neutron-vpnaas --config-file $NEUTRON_CONF upgrade head
|
||||
}
|
||||
@ -58,6 +100,15 @@ function neutron_vpnaas_generate_config_files {
|
||||
(cd $NEUTRON_VPNAAS_DIR && exec ./tools/generate_config_file_samples.sh)
|
||||
}
|
||||
|
||||
function neutron_vpnaas_start_vpnagent {
|
||||
NEUTRON_OVN_BIN_DIR=$(get_python_exec_prefix)
|
||||
NEUTRON_OVN_VPNAGENT_BINARY="neutron-ovn-vpn-agent"
|
||||
|
||||
run_process q-ovn-vpn-agent "$NEUTRON_OVN_BIN_DIR/$NEUTRON_OVN_VPNAGENT_BINARY --config-file $OVN_VPNAGENT_CONF"
|
||||
# Format logging
|
||||
setup_logging $OVN_VPNAGENT_CONF
|
||||
}
|
||||
|
||||
# Main plugin processing
|
||||
|
||||
# NOP for pre-install step
|
||||
@ -77,6 +128,15 @@ elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
|
||||
echo_summary "Configuring neutron-vpnaas agent"
|
||||
neutron_vpnaas_configure_agent
|
||||
fi
|
||||
if is_service_enabled q-ovn-vpn-agent && is_ovn_enabled; then
|
||||
echo_summary "Configuring neutron-ovn-vpn-agent"
|
||||
neutron_vpnaas_configure_ovn_agent
|
||||
fi
|
||||
|
||||
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||
if is_service_enabled q-ovn-vpn-agent && is_ovn_enabled; then
|
||||
neutron_vpnaas_start_vpnagent
|
||||
fi
|
||||
|
||||
# NOP for clean step
|
||||
|
||||
|
@ -1,23 +1,36 @@
|
||||
# Settings for the VPNaaS devstack plugin
|
||||
|
||||
# Plugin
|
||||
VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"}
|
||||
if [[ $Q_AGENT == "ovn" ]]; then
|
||||
VPN_PLUGIN=${VPN_PLUGIN:-"ovn-vpnaas"}
|
||||
else
|
||||
VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"}
|
||||
fi
|
||||
|
||||
# Device driver
|
||||
IPSEC_PACKAGE=${IPSEC_PACKAGE:-"strongswan"}
|
||||
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec:StrongSwanDriver"}
|
||||
if [[ $Q_AGENT == "ovn" ]]; then
|
||||
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnStrongSwanDriver"}
|
||||
else
|
||||
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec:StrongSwanDriver"}
|
||||
fi
|
||||
|
||||
function _get_service_provider {
|
||||
local ipsec_package=$1
|
||||
local name driver
|
||||
local ipsec_package=$1
|
||||
local name driver
|
||||
|
||||
driver="neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver"
|
||||
if [ "$ipsec_package" = "libreswan" ]; then
|
||||
name="openswan"
|
||||
else
|
||||
name="strongswan"
|
||||
fi
|
||||
echo "VPN:${name}:${driver}:default"
|
||||
if [[ $Q_AGENT == "ovn" ]]; then
|
||||
driver="neutron_vpnaas.services.vpn.service_drivers.ovn_ipsec.IPsecOvnVPNDriver"
|
||||
else
|
||||
driver="neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver"
|
||||
fi
|
||||
|
||||
if [ "$ipsec_package" = "libreswan" ]; then
|
||||
name="openswan"
|
||||
else
|
||||
name="strongswan"
|
||||
fi
|
||||
echo "VPN:${name}:${driver}:default"
|
||||
}
|
||||
|
||||
# Service Driver, default value depends on IPSEC_PACKAGE.
|
||||
@ -31,3 +44,5 @@ NEUTRON_VPNAAS_DIR=$DEST/neutron-vpnaas
|
||||
|
||||
NEUTRON_VPNAAS_CONF_FILE=neutron_vpnaas.conf
|
||||
NEUTRON_VPNAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_VPNAAS_CONF_FILE
|
||||
|
||||
OVN_VPNAGENT_CONF=$NEUTRON_CONF_DIR/neutron_ovn_vpn_agent.ini
|
||||
|
@ -15,6 +15,7 @@ Neutron VPNaaS uses the following configuration files for its various services.
|
||||
|
||||
neutron_vpnaas
|
||||
l3_agent
|
||||
neutron_ovn_vpn_agent
|
||||
|
||||
The following are sample configuration files for Neutron VPNaaS services and
|
||||
utilities. These are generated from code and reflect the current state of code
|
||||
|
8
doc/source/configuration/neutron_ovn_vpn_agent.rst
Normal file
8
doc/source/configuration/neutron_ovn_vpn_agent.rst
Normal file
@ -0,0 +1,8 @@
|
||||
=========================
|
||||
neutron_ovn_vpn_agent.ini
|
||||
=========================
|
||||
|
||||
This is a configuration file for the OVN VPN agent.
|
||||
|
||||
.. show-options::
|
||||
:config-file: etc/oslo-config-generator/neutron_ovn_vpn_agent.ini
|
@ -12,6 +12,8 @@ cp: RegExpFilter, cp, root, cp, -a, .*, .*/strongswan.d
|
||||
ip: IpFilter, ip, root
|
||||
ip_exec: IpNetnsExecFilter, ip, root
|
||||
ipsec: CommandFilter, ipsec, root
|
||||
sysctl_ip4_forward: RegExpFilter, sysctl, root, sysctl, -w, net.ipv4.ip_forward=1
|
||||
sysctl_ip6_forward: RegExpFilter, sysctl, root, sysctl, -w, net.ipv6.conf.all.forwarding=1
|
||||
rm: RegExpFilter, rm, root, rm, -rf, (.*/strongswan.d|.*/ipsec/[0-9a-z-]+)
|
||||
rm_file: RegExpFilter, rm, root, rm, -f, .*/ipsec.secrets
|
||||
strongswan: CommandFilter, strongswan, root
|
||||
|
5
etc/oslo-config-generator/neutron_ovn_vpn_agent.ini
Normal file
5
etc/oslo-config-generator/neutron_ovn_vpn_agent.ini
Normal file
@ -0,0 +1,5 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/neutron_ovn_vpn_agent.ini.sample
|
||||
wrap_width = 79
|
||||
|
||||
namespace = neutron.vpnaas.ovn_agent
|
0
neutron_vpnaas/agent/__init__.py
Normal file
0
neutron_vpnaas/agent/__init__.py
Normal file
0
neutron_vpnaas/agent/ovn/__init__.py
Normal file
0
neutron_vpnaas/agent/ovn/__init__.py
Normal file
0
neutron_vpnaas/agent/ovn/vpn/__init__.py
Normal file
0
neutron_vpnaas/agent/ovn/vpn/__init__.py
Normal file
167
neutron_vpnaas/agent/ovn/vpn/agent.py
Normal file
167
neutron_vpnaas/agent/ovn/vpn/agent.py
Normal file
@ -0,0 +1,167 @@
|
||||
# Copyright 2017 Red Hat, Inc.
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 uuid
|
||||
|
||||
from neutron.agent.linux import external_process
|
||||
from neutron.common.ovn import utils as ovn_utils
|
||||
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
from ovsdbapp.backend.ovs_idl import event as row_event
|
||||
from ovsdbapp.backend.ovs_idl import vlog
|
||||
|
||||
from neutron_vpnaas.agent.ovn.vpn import ovsdb
|
||||
from neutron_vpnaas.services.vpn.common import constants
|
||||
from neutron_vpnaas.services.vpn import vpn_service
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OVN_VPNAGENT_UUID_NAMESPACE = uuid.UUID('e1ce3b12-b1e0-4c81-ba27-07c0fec9c12b')
|
||||
|
||||
|
||||
class ChassisCreateEventBase(row_event.RowEvent):
|
||||
"""Row create event - Chassis name == our_chassis.
|
||||
|
||||
On connection, we get a dump of all chassis so if we catch a creation
|
||||
of our own chassis it has to be a reconnection. In this case, we need
|
||||
to do a full sync to make sure that we capture all changes while the
|
||||
connection to OVSDB was down.
|
||||
"""
|
||||
table = None
|
||||
|
||||
def __init__(self, vpn_agent):
|
||||
self.agent = vpn_agent
|
||||
self.first_time = True
|
||||
events = (self.ROW_CREATE,)
|
||||
super().__init__(
|
||||
events, self.table, (('name', '=', self.agent.chassis),))
|
||||
self.event_name = self.__class__.__name__
|
||||
|
||||
def run(self, event, row, old):
|
||||
if self.first_time:
|
||||
self.first_time = False
|
||||
else:
|
||||
# NOTE(lucasagomes): Re-register the ovn vpn agent
|
||||
# with the local chassis in case its entry was re-created
|
||||
# (happens when restarting the ovn-controller)
|
||||
self.agent.register_vpn_agent()
|
||||
LOG.info("Connection to OVSDB established, doing a full sync")
|
||||
self.agent.sync()
|
||||
|
||||
|
||||
class ChassisCreateEvent(ChassisCreateEventBase):
|
||||
table = 'Chassis'
|
||||
|
||||
|
||||
class ChassisPrivateCreateEvent(ChassisCreateEventBase):
|
||||
table = 'Chassis_Private'
|
||||
|
||||
|
||||
class SbGlobalUpdateEvent(row_event.RowEvent):
|
||||
"""Row update event on SB_Global table."""
|
||||
|
||||
def __init__(self, vpn_agent):
|
||||
self.agent = vpn_agent
|
||||
table = 'SB_Global'
|
||||
events = (self.ROW_UPDATE,)
|
||||
super().__init__(events, table, None)
|
||||
self.event_name = self.__class__.__name__
|
||||
|
||||
def run(self, event, row, old):
|
||||
table = ('Chassis_Private' if self.agent.has_chassis_private
|
||||
else 'Chassis')
|
||||
external_ids = {constants.OVN_AGENT_VPN_SB_CFG_KEY: str(row.nb_cfg)}
|
||||
self.agent.sb_idl.db_set(
|
||||
table, self.agent.chassis,
|
||||
('external_ids', external_ids)).execute()
|
||||
|
||||
|
||||
class OvnVpnAgent(service.Service):
|
||||
def __init__(self, conf):
|
||||
super().__init__()
|
||||
self.conf = conf
|
||||
vlog.use_python_logger(max_level=config.get_ovn_ovsdb_log_level())
|
||||
self._process_monitor = external_process.ProcessMonitor(
|
||||
config=self.conf,
|
||||
resource_type='ipsec')
|
||||
|
||||
self.service = vpn_service.VPNService(self)
|
||||
self.device_drivers = self.service.load_device_drivers(self.conf.host)
|
||||
|
||||
def _load_config(self):
|
||||
self.chassis = self._get_own_chassis_name()
|
||||
try:
|
||||
self.chassis_id = uuid.UUID(self.chassis)
|
||||
except ValueError:
|
||||
# OVS system-id could be a non UUID formatted string.
|
||||
self.chassis_id = uuid.uuid5(OVN_VPNAGENT_UUID_NAMESPACE,
|
||||
self.chassis)
|
||||
LOG.debug("Loaded chassis name %s (UUID: %s).",
|
||||
self.chassis, self.chassis_id)
|
||||
|
||||
def start(self):
|
||||
super().start()
|
||||
|
||||
self.ovs_idl = ovsdb.VPNAgentOvsIdl().start()
|
||||
self._load_config()
|
||||
|
||||
tables = ('SB_Global', 'Chassis')
|
||||
events = (SbGlobalUpdateEvent(self), )
|
||||
# TODO(lucasagomes): Remove this in the future. Try to register
|
||||
# the Chassis_Private table, if not present, fallback to the normal
|
||||
# Chassis table.
|
||||
# Open the connection to OVN SB database.
|
||||
self.has_chassis_private = False
|
||||
try:
|
||||
self.sb_idl = ovsdb.VPNAgentOvnSbIdl(
|
||||
chassis=self.chassis, tables=tables + ('Chassis_Private', ),
|
||||
events=events + (ChassisPrivateCreateEvent(self), )).start()
|
||||
self.has_chassis_private = True
|
||||
except AssertionError:
|
||||
self.sb_idl = ovsdb.VPNAgentOvnSbIdl(
|
||||
chassis=self.chassis, tables=tables,
|
||||
events=events + (ChassisCreateEvent(self), )).start()
|
||||
|
||||
# Register the agent with its corresponding Chassis
|
||||
self.register_vpn_agent()
|
||||
|
||||
# Do the initial sync.
|
||||
self.sync()
|
||||
|
||||
def sync(self):
|
||||
for driver in self.device_drivers:
|
||||
driver.sync(driver.context, [])
|
||||
|
||||
@ovn_utils.retry()
|
||||
def register_vpn_agent(self):
|
||||
# NOTE(lucasagomes): db_add() will not overwrite the UUID if
|
||||
# it's already set.
|
||||
table = ('Chassis_Private' if self.has_chassis_private else 'Chassis')
|
||||
# Generate unique, but consistent vpn agent id for chassis name
|
||||
agent_id = uuid.uuid5(self.chassis_id, 'vpn_agent')
|
||||
ext_ids = {constants.OVN_AGENT_VPN_ID_KEY: str(agent_id)}
|
||||
self.sb_idl.db_add(table, self.chassis, 'external_ids',
|
||||
ext_ids).execute(check_error=True)
|
||||
|
||||
def _get_own_chassis_name(self):
|
||||
"""Return the external_ids:system-id value of the Open_vSwitch table.
|
||||
|
||||
As long as ovn-controller is running on this node, the key is
|
||||
guaranteed to exist and will include the chassis name.
|
||||
"""
|
||||
ext_ids = self.ovs_idl.db_get(
|
||||
'Open_vSwitch', '.', 'external_ids').execute()
|
||||
return ext_ids['system-id']
|
80
neutron_vpnaas/agent/ovn/vpn/ovsdb.py
Normal file
80
neutron_vpnaas/agent/ovn/vpn/ovsdb.py
Normal file
@ -0,0 +1,80 @@
|
||||
# Copyright 2017 Red Hat, Inc.
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config
|
||||
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn
|
||||
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor
|
||||
from oslo_log import log as logging
|
||||
from ovs.db import idl
|
||||
from ovsdbapp.backend.ovs_idl import connection
|
||||
from ovsdbapp.backend.ovs_idl import idlutils
|
||||
from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs
|
||||
import tenacity
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VPNAgentOvnSbIdl(ovsdb_monitor.OvnIdl):
|
||||
|
||||
SCHEMA = 'OVN_Southbound'
|
||||
|
||||
def __init__(self, chassis=None, events=None, tables=None):
|
||||
connection_string = config.get_ovn_sb_connection()
|
||||
ovsdb_monitor._check_and_set_ssl_files(self.SCHEMA)
|
||||
helper = self._get_ovsdb_helper(connection_string)
|
||||
if tables is None:
|
||||
tables = ('Chassis', 'SB_Global')
|
||||
for table in tables:
|
||||
helper.register_table(table)
|
||||
try:
|
||||
super().__init__(
|
||||
None, connection_string, helper, leader_only=False)
|
||||
except TypeError:
|
||||
# TODO(bpetermann) We can remove this when we require ovs>=2.12.0
|
||||
super().__init__(None, connection_string, helper)
|
||||
if chassis:
|
||||
table = ('Chassis_Private' if 'Chassis_Private' in tables
|
||||
else 'Chassis')
|
||||
self.set_table_condition(table, [['name', '==', chassis]])
|
||||
if events:
|
||||
self.notify_handler.watch_events(events)
|
||||
|
||||
@tenacity.retry(
|
||||
wait=tenacity.wait_exponential(max=180),
|
||||
reraise=True)
|
||||
def _get_ovsdb_helper(self, connection_string):
|
||||
return idlutils.get_schema_helper(connection_string, self.SCHEMA)
|
||||
|
||||
def start(self):
|
||||
conn = connection.Connection(
|
||||
self, timeout=config.get_ovn_ovsdb_timeout())
|
||||
return impl_idl_ovn.OvsdbSbOvnIdl(conn)
|
||||
|
||||
|
||||
class VPNAgentOvsIdl(object):
|
||||
|
||||
def start(self):
|
||||
connection_string = config.cfg.CONF.ovs.ovsdb_connection
|
||||
helper = idlutils.get_schema_helper(connection_string,
|
||||
'Open_vSwitch')
|
||||
tables = ('Open_vSwitch', 'Bridge', 'Port', 'Interface')
|
||||
for table in tables:
|
||||
helper.register_table(table)
|
||||
ovs_idl = idl.Idl(
|
||||
connection_string, helper,
|
||||
probe_interval=config.get_ovn_ovsdb_probe_interval())
|
||||
conn = connection.Connection(
|
||||
ovs_idl, timeout=config.cfg.CONF.ovs.ovsdb_connection_timeout)
|
||||
return idl_ovs.OvsdbIdl(conn)
|
52
neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py
Normal file
52
neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Copyright 2020, SysEleven GbmH
|
||||
# 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 neutron.api.rpc.agentnotifiers import utils as ag_utils
|
||||
from neutron_lib import rpc as n_rpc
|
||||
import oslo_messaging
|
||||
|
||||
from neutron_vpnaas.services.vpn.common import topics
|
||||
|
||||
# default messaging timeout is 60 sec, so 2 here is chosen to not block API
|
||||
# call for more than 2 minutes
|
||||
AGENT_NOTIFY_MAX_ATTEMPTS = 2
|
||||
|
||||
|
||||
class VPNAgentNotifyAPI(object):
|
||||
"""API for plugin to notify VPN agent."""
|
||||
|
||||
def __init__(self, topic=topics.IPSEC_AGENT_TOPIC):
|
||||
target = oslo_messaging.Target(topic=topic, version='1.0')
|
||||
self.client = n_rpc.get_client(target)
|
||||
|
||||
def agent_updated(self, context, admin_state_up, host):
|
||||
cctxt = self.client.prepare(server=host)
|
||||
cctxt.cast(context, 'agent_updated',
|
||||
payload={'admin_state_up': admin_state_up})
|
||||
|
||||
def vpnservice_removed_from_agent(self, context, router_id, host):
|
||||
"""Notify agent about removed VPN service(s) of a router."""
|
||||
cctxt = self.client.prepare(server=host)
|
||||
cctxt.cast(context, 'vpnservice_removed_from_agent',
|
||||
router_id=router_id)
|
||||
|
||||
def vpnservice_added_to_agent(self, context, router_ids, host):
|
||||
"""Notify agent about added VPN service(s) of router(s)."""
|
||||
# need to use call here as we want to be sure agent received
|
||||
# notification and router will not be "lost". However using call()
|
||||
# itself is not a guarantee, calling code should handle exceptions and
|
||||
# retry
|
||||
cctxt = self.client.prepare(server=host)
|
||||
call = ag_utils.retry(cctxt.call, AGENT_NOTIFY_MAX_ATTEMPTS)
|
||||
call(context, 'vpnservice_added_to_agent', router_ids=router_ids)
|
3
neutron_vpnaas/cmd/eventlet/__init__.py
Normal file
3
neutron_vpnaas/cmd/eventlet/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from neutron.common import eventlet_utils
|
||||
|
||||
eventlet_utils.monkey_patch()
|
19
neutron_vpnaas/cmd/eventlet/ovn_agent.py
Normal file
19
neutron_vpnaas/cmd/eventlet/ovn_agent.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron_vpnaas.services.vpn import ovn_agent
|
||||
|
||||
|
||||
def main():
|
||||
ovn_agent.main()
|
@ -0,0 +1,57 @@
|
||||
# Copyright 2016 MingShuang Xian/IBM
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Add table for vpn gateway (gateway port and transit network)
|
||||
|
||||
Revision ID: 22e0145ac80b
|
||||
Revises: 3b739d6906cf
|
||||
Create Date: 2016-09-18 09:01:18.660362
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '22e0145ac80b'
|
||||
down_revision = '3b739d6906cf'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'vpn_ext_gws',
|
||||
sa.Column('id', sa.String(length=36), nullable=False,
|
||||
primary_key=True),
|
||||
sa.Column('project_id', sa.String(length=255),
|
||||
index=True),
|
||||
sa.Column('router_id', sa.String(length=36), nullable=False,
|
||||
unique=True),
|
||||
sa.Column('status', sa.String(length=16), nullable=False),
|
||||
sa.Column('gw_port_id', sa.String(length=36)),
|
||||
sa.Column('transit_port_id', sa.String(length=36)),
|
||||
sa.Column('transit_network_id', sa.String(length=36)),
|
||||
sa.Column('transit_subnet_id', sa.String(length=36)),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['router_id'], ['routers.id']),
|
||||
sa.ForeignKeyConstraint(['gw_port_id'], ['ports.id'],
|
||||
ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['transit_port_id'], ['ports.id'],
|
||||
ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['transit_network_id'], ['networks.id'],
|
||||
ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['transit_subnet_id'], ['subnets.id'],
|
||||
ondelete='SET NULL'),
|
||||
)
|
@ -0,0 +1,41 @@
|
||||
# Copyright 2016 MingShuang Xian/IBM
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""vpn scheduler
|
||||
|
||||
Revision ID: 3b739d6906cf
|
||||
Revises: 5f884db48ba9
|
||||
Create Date: 2016-08-15 03:32:46.124718
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3b739d6906cf'
|
||||
down_revision = '5f884db48ba9'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'routervpnagentbindings',
|
||||
sa.Column('router_id', sa.String(length=36),
|
||||
unique=True, nullable=False),
|
||||
sa.Column('vpn_agent_id', sa.String(length=36), nullable=False),
|
||||
sa.ForeignKeyConstraint(['router_id'], ['routers.id'],
|
||||
ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('router_id', 'vpn_agent_id'),
|
||||
)
|
@ -1 +1 @@
|
||||
5f884db48ba9
|
||||
22e0145ac80b
|
||||
|
@ -23,7 +23,9 @@ Based on this comparison database can be healed with healing migration.
|
||||
|
||||
from neutron.db.migration.models import head
|
||||
|
||||
from neutron_vpnaas.db.vpn import vpn_agentschedulers_db # noqa
|
||||
from neutron_vpnaas.db.vpn import vpn_db # noqa
|
||||
from neutron_vpnaas.db.vpn import vpn_ext_gw_db # noqa
|
||||
|
||||
|
||||
def get_metadata():
|
||||
|
415
neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py
Normal file
415
neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py
Normal file
@ -0,0 +1,415 @@
|
||||
# Copyright (c) 2013 OpenStack Foundation.
|
||||
# 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 random
|
||||
|
||||
from neutron.extensions import router_availability_zone as router_az
|
||||
from neutron import worker as neutron_worker
|
||||
from neutron_lib import context as ncontext
|
||||
from neutron_lib.db import api as db_api
|
||||
from neutron_lib.db import model_base
|
||||
from neutron_lib.plugins import constants as plugin_const
|
||||
from neutron_lib.plugins import directory
|
||||
from oslo_config import cfg
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_log import log as logging
|
||||
import oslo_messaging
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import func
|
||||
|
||||
from neutron_vpnaas._i18n import _
|
||||
from neutron_vpnaas.db.vpn import vpn_models
|
||||
from neutron_vpnaas.extensions import vpn_agentschedulers
|
||||
from neutron_vpnaas.services.vpn.common.constants import AGENT_TYPE_VPN
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
VPN_AGENTS_SCHEDULER_OPTS = [
|
||||
cfg.StrOpt('vpn_scheduler_driver',
|
||||
default='neutron_vpnaas.scheduler.vpn_agent_scheduler'
|
||||
'.LeastRoutersScheduler',
|
||||
help=_('Driver to use for scheduling '
|
||||
'router to a VPN agent')),
|
||||
cfg.BoolOpt('vpn_auto_schedule', default=True,
|
||||
help=_('Allow auto scheduling of routers to VPN agent.')),
|
||||
cfg.BoolOpt('allow_automatic_vpnagent_failover', default=False,
|
||||
help=_('Automatically reschedule routers from offline VPN '
|
||||
'agents to online VPN agents.')),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(VPN_AGENTS_SCHEDULER_OPTS)
|
||||
|
||||
|
||||
class RouterVPNAgentBinding(model_base.BASEV2):
|
||||
"""Represents binding between neutron routers and VPN agents."""
|
||||
|
||||
router_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey("routers.id", ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
nullable=False)
|
||||
vpn_agent_id = sa.Column(sa.String(36), primary_key=True, nullable=False)
|
||||
|
||||
|
||||
class VPNAgentSchedulerDbMixin(
|
||||
vpn_agentschedulers.VPNAgentSchedulerPluginBase):
|
||||
"""Mixin class to add VPN agent scheduler extension to plugins
|
||||
using the VPN agent.
|
||||
"""
|
||||
|
||||
vpn_scheduler = None
|
||||
agent_notifiers = {}
|
||||
|
||||
@property
|
||||
def l3_plugin(self):
|
||||
return directory.get_plugin(plugin_const.L3)
|
||||
|
||||
@property
|
||||
def core_plugin(self):
|
||||
return directory.get_plugin()
|
||||
|
||||
def add_periodic_vpn_agent_status_check(self):
|
||||
if not cfg.CONF.allow_automatic_vpnagent_failover:
|
||||
LOG.info("Skipping periodic VPN agent status check because "
|
||||
"automatic rescheduling is disabled.")
|
||||
return
|
||||
|
||||
interval = max(cfg.CONF.agent_down_time // 2, 1)
|
||||
# add random initial delay to allow agents to check in after the
|
||||
# neutron server first starts. random to offset multiple servers
|
||||
initial_delay = random.randint(interval, interval * 2)
|
||||
|
||||
check_worker = neutron_worker.PeriodicWorker(
|
||||
self.reschedule_vpnservices_from_down_agents,
|
||||
interval, initial_delay)
|
||||
self.add_worker(check_worker)
|
||||
|
||||
def reschedule_vpnservices_from_down_agents(self):
|
||||
"""Reschedule VPN services from down VPN agents.
|
||||
|
||||
VPN services are scheduled per router.
|
||||
"""
|
||||
context = ncontext.get_admin_context()
|
||||
try:
|
||||
down_bindings = self.get_down_router_bindings(context)
|
||||
|
||||
agents_back_online = set()
|
||||
for binding in down_bindings:
|
||||
if binding.vpn_agent_id in agents_back_online:
|
||||
continue
|
||||
agent = self.core_plugin.get_agent(context,
|
||||
binding.vpn_agent_id)
|
||||
if agent['alive']:
|
||||
agents_back_online.add(binding.vpn_agent_id)
|
||||
continue
|
||||
|
||||
LOG.warning(
|
||||
"Rescheduling vpn services for router %(router)s from "
|
||||
"agent %(agent)s because the agent is not alive.",
|
||||
{'router': binding.router_id,
|
||||
'agent': binding.vpn_agent_id})
|
||||
try:
|
||||
self.reschedule_router(context, binding.router_id, agent)
|
||||
except (vpn_agentschedulers.RouterReschedulingFailed,
|
||||
oslo_messaging.RemoteError):
|
||||
# Catch individual rescheduling errors here
|
||||
# so one broken one doesn't stop the iteration.
|
||||
LOG.exception("Failed to reschedule vpn services for "
|
||||
"router %s", binding.router_id)
|
||||
except Exception:
|
||||
# we want to be thorough and catch whatever is raised
|
||||
# to avoid loop abortion
|
||||
LOG.exception("Exception encountered during vpn service "
|
||||
"rescheduling.")
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_down_router_bindings(self, context):
|
||||
vpn_agents = self.get_vpn_agents(context, active=False)
|
||||
if not vpn_agents:
|
||||
return []
|
||||
vpn_agent_ids = [vpn_agent['id'] for vpn_agent in vpn_agents]
|
||||
|
||||
query = context.session.query(RouterVPNAgentBinding)
|
||||
query = query.filter(
|
||||
RouterVPNAgentBinding.vpn_agent_id.in_(vpn_agent_ids))
|
||||
return query.all()
|
||||
|
||||
def validate_agent_router_combination(self, context, agent, router):
|
||||
"""Validate if the router can be correctly assigned to the agent.
|
||||
|
||||
:raises: InvalidVPNAgent if attempting to assign router to an
|
||||
unsuitable agent (disabled, type != VPN, incompatible configuration)
|
||||
"""
|
||||
if agent['agent_type'] != AGENT_TYPE_VPN:
|
||||
raise vpn_agentschedulers.InvalidVPNAgent(id=agent['id'])
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def check_agent_router_scheduling_needed(self, context, agent, router):
|
||||
"""Check if the scheduling of router's VPN services is needed.
|
||||
|
||||
:raises: RouterHostedByVPNAgent if router is already assigned
|
||||
to a different agent.
|
||||
:returns: True if scheduling is needed, otherwise False
|
||||
"""
|
||||
router_id = router['id']
|
||||
agent_id = agent['id']
|
||||
query = context.session.query(RouterVPNAgentBinding)
|
||||
bindings = query.filter_by(router_id=router_id).all()
|
||||
if not bindings:
|
||||
return True
|
||||
for binding in bindings:
|
||||
if binding.vpn_agent_id == agent_id:
|
||||
# router already bound to the agent we need
|
||||
return False
|
||||
# Router is already bound to some agent
|
||||
raise vpn_agentschedulers.RouterHostedByVPNAgent(
|
||||
router_id=router_id,
|
||||
agent_id=bindings[0].vpn_agent_id)
|
||||
|
||||
def create_router_to_agent_binding(self, context, router_id, agent_id):
|
||||
"""Create router to VPN agent binding."""
|
||||
try:
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
binding = RouterVPNAgentBinding()
|
||||
binding.vpn_agent_id = agent_id
|
||||
binding.router_id = router_id
|
||||
context.session.add(binding)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
LOG.debug('VPN service of router %(router_id)s has already been '
|
||||
'scheduled to a VPN agent.',
|
||||
{'router_id': router_id})
|
||||
return False
|
||||
except db_exc.DBReferenceError:
|
||||
LOG.debug('Router %s has already been removed '
|
||||
'by concurrent operation', router_id)
|
||||
return False
|
||||
|
||||
LOG.debug('VPN service of router %(router_id)s is scheduled to '
|
||||
'VPN agent %(agent_id)s',
|
||||
{'router_id': router_id, 'agent_id': agent_id})
|
||||
return True
|
||||
|
||||
def add_router_to_vpn_agent(self, context, agent_id, router_id):
|
||||
"""Add a VPN agent to host VPN services of a router."""
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router = self.l3_plugin.get_router(context, router_id)
|
||||
agent = self.core_plugin.get_agent(context, agent_id)
|
||||
self.validate_agent_router_combination(context, agent, router)
|
||||
if not self.check_agent_router_scheduling_needed(
|
||||
context, agent, router):
|
||||
return
|
||||
try:
|
||||
success = self.create_router_to_agent_binding(
|
||||
context, router['id'], agent['id'])
|
||||
except db_exc.DBError:
|
||||
success = False
|
||||
|
||||
if not success:
|
||||
raise vpn_agentschedulers.RouterSchedulingFailed(
|
||||
router_id=router_id, agent_id=agent_id)
|
||||
|
||||
# notify agent
|
||||
vpn_notifier = self.agent_notifiers.get(AGENT_TYPE_VPN)
|
||||
if vpn_notifier:
|
||||
vpn_notifier.vpnservice_added_to_agent(
|
||||
context, [router_id], agent['host'])
|
||||
|
||||
# update port binding
|
||||
self.vpn_router_agent_binding_changed(
|
||||
context, router_id, agent['host'])
|
||||
|
||||
def remove_router_from_vpn_agent(self, context, agent_id, router_id):
|
||||
"""Remove the router from VPN agent.
|
||||
|
||||
After removal, the VPN service(s) of the router will be non-hosted
|
||||
until there is an update which leads to re-schedule or the router is
|
||||
added to another agent manually.
|
||||
"""
|
||||
agent = self.core_plugin.get_agent(context, agent_id)
|
||||
|
||||
self._unbind_router(context, router_id, agent_id)
|
||||
|
||||
vpn_notifier = self.agent_notifiers.get(AGENT_TYPE_VPN)
|
||||
if vpn_notifier:
|
||||
vpn_notifier.vpnservice_removed_from_agent(
|
||||
context, router_id, agent['host'])
|
||||
|
||||
def _unbind_router(self, context, router_id, agent_id):
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
query = context.session.query(RouterVPNAgentBinding)
|
||||
query = query.filter(
|
||||
RouterVPNAgentBinding.router_id == router_id,
|
||||
RouterVPNAgentBinding.vpn_agent_id == agent_id)
|
||||
return query.delete()
|
||||
|
||||
def reschedule_router(self, context, router_id, cur_agent):
|
||||
"""Reschedule router to a new VPN agent
|
||||
|
||||
Remove the router from the agent currently hosting it and
|
||||
schedule it again
|
||||
"""
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
deleted = self._unbind_router(context, router_id, cur_agent['id'])
|
||||
if not deleted:
|
||||
# If nothing was deleted, the binding didn't exist anymore
|
||||
# because some other server deleted the binding concurrently.
|
||||
# Stop here.
|
||||
return
|
||||
|
||||
new_agent = self.schedule_router(context, router_id)
|
||||
if not new_agent:
|
||||
# No new_agent means that another server scheduled the
|
||||
# router concurrently. Don't raise RouterReschedulingFailed.
|
||||
return
|
||||
|
||||
self._notify_agents_router_rescheduled(context, router_id,
|
||||
cur_agent, new_agent)
|
||||
# update port binding
|
||||
self.vpn_router_agent_binding_changed(
|
||||
context, router_id, new_agent['host'])
|
||||
|
||||
def _notify_agents_router_rescheduled(self, context, router_id,
|
||||
old_agent, new_agent):
|
||||
vpn_notifier = self.agent_notifiers.get(AGENT_TYPE_VPN)
|
||||
if not vpn_notifier:
|
||||
return
|
||||
|
||||
old_host = old_agent['host']
|
||||
new_host = new_agent['host']
|
||||
if old_host != new_host:
|
||||
vpn_notifier.vpnservice_removed_from_agent(
|
||||
context, router_id, old_host)
|
||||
|
||||
try:
|
||||
vpn_notifier.vpnservice_added_to_agent(
|
||||
context, [router_id], new_host)
|
||||
except oslo_messaging.MessagingException:
|
||||
self._unbind_router(context, router_id, new_agent['id'])
|
||||
raise vpn_agentschedulers.RouterReschedulingFailed(
|
||||
router_id=router_id)
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def list_routers_on_vpn_agent(self, context, agent_id):
|
||||
query = context.session.query(RouterVPNAgentBinding.router_id)
|
||||
query = query.filter(RouterVPNAgentBinding.vpn_agent_id == agent_id)
|
||||
|
||||
router_ids = [item[0] for item in query]
|
||||
if router_ids:
|
||||
return {'routers':
|
||||
self.l3_plugin.get_routers(context,
|
||||
filters={'id': router_ids})}
|
||||
else:
|
||||
# Exception will be thrown if the requested agent does not exist.
|
||||
self.core_plugin.get_agent(context, agent_id)
|
||||
return {'routers': []}
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_vpn_agents_hosting_routers(self, context, router_ids, active=None):
|
||||
if not router_ids:
|
||||
return []
|
||||
query = context.session.query(RouterVPNAgentBinding)
|
||||
query = query.filter(RouterVPNAgentBinding.router_id.in_(router_ids))
|
||||
|
||||
filters = {'id': [binding.vpn_agent_id for binding in query]}
|
||||
vpn_agents = self.core_plugin.get_agents(context, filters=filters)
|
||||
if active is not None:
|
||||
vpn_agents = [agent
|
||||
for agent in vpn_agents
|
||||
if agent['alive'] == active]
|
||||
return vpn_agents
|
||||
|
||||
def list_vpn_agents_hosting_router(self, context, router_id):
|
||||
vpn_agents = self.get_vpn_agents_hosting_routers(context, [router_id])
|
||||
return {'agents': vpn_agents}
|
||||
|
||||
def get_vpn_agents(self, context, active=None, host=None):
|
||||
filters = {'agent_type': [AGENT_TYPE_VPN]}
|
||||
if host is not None:
|
||||
filters['host'] = [host]
|
||||
vpn_agents = self.core_plugin.get_agents(context, filters=filters)
|
||||
if active is None:
|
||||
return vpn_agents
|
||||
else:
|
||||
return [vpn_agent
|
||||
for vpn_agent in vpn_agents
|
||||
if vpn_agent['alive'] == active]
|
||||
|
||||
def get_vpn_agent_on_host(self, context, host, active=None):
|
||||
agents = self.get_vpn_agents(context, active=active, host=host)
|
||||
if agents:
|
||||
return agents[0]
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_unscheduled_vpn_routers(self, context, router_ids=None):
|
||||
"""Get IDs of routers which have unscheduled VPN services."""
|
||||
query = context.session.query(vpn_models.VPNService.router_id)
|
||||
query = query.outerjoin(
|
||||
RouterVPNAgentBinding,
|
||||
vpn_models.VPNService.router_id == RouterVPNAgentBinding.router_id)
|
||||
query = query.filter(RouterVPNAgentBinding.vpn_agent_id.is_(None))
|
||||
if router_ids:
|
||||
query = query.filter(
|
||||
vpn_models.VPNService.router_id.in_(router_ids))
|
||||
return [router_id for router_id, in query.all()]
|
||||
|
||||
def auto_schedule_routers(self, context, vpn_agent):
|
||||
if self.vpn_scheduler:
|
||||
return self.vpn_scheduler.auto_schedule_routers(
|
||||
self, context, vpn_agent)
|
||||
|
||||
def schedule_router(self, context, router, candidates=None):
|
||||
"""Schedule VPN services of a router to a VPN agent.
|
||||
|
||||
Returns the chosen agent; None if another server scheduled the
|
||||
router concurrently.
|
||||
Raises RouterReschedulingFailed if no suitable agent is found.
|
||||
"""
|
||||
if self.vpn_scheduler:
|
||||
return self.vpn_scheduler.schedule(
|
||||
self, context, router, candidates=candidates)
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_vpn_agent_with_min_routers(self, context, agent_ids):
|
||||
"""Return VPN agent with the least number of routers."""
|
||||
if not agent_ids:
|
||||
return None
|
||||
query = context.session.query(
|
||||
RouterVPNAgentBinding.vpn_agent_id,
|
||||
func.count(RouterVPNAgentBinding.router_id).label('count'))
|
||||
query = query.group_by(RouterVPNAgentBinding.vpn_agent_id)
|
||||
query = query.order_by('count')
|
||||
query = query.filter(RouterVPNAgentBinding.vpn_agent_id.in_(agent_ids))
|
||||
used_agent_ids = [agent_id for agent_id, _ in query.all()]
|
||||
unused_agent_ids = set(agent_ids) - set(used_agent_ids)
|
||||
if unused_agent_ids:
|
||||
return unused_agent_ids.pop()
|
||||
else:
|
||||
return used_agent_ids[0]
|
||||
|
||||
def get_hosts_to_notify(self, context, router_id):
|
||||
"""Returns all hosts to send notification about router update"""
|
||||
agents = self.get_vpn_agents_hosting_routers(context, [router_id],
|
||||
active=True)
|
||||
return [a['host'] for a in agents]
|
||||
|
||||
|
||||
class AZVPNAgentSchedulerDbMixin(VPNAgentSchedulerDbMixin,
|
||||
router_az.RouterAvailabilityZonePluginBase):
|
||||
"""Mixin class to add availability_zone supported VPN agent scheduler."""
|
||||
|
||||
def get_router_availability_zones(self, router):
|
||||
return list({agent.availability_zone for agent in router.vpn_agents})
|
@ -509,6 +509,19 @@ class VPNPluginDb(vpnaas.VPNPluginBase,
|
||||
vpns_db.update(vpns)
|
||||
return self._make_vpnservice_dict(vpns_db)
|
||||
|
||||
def set_vpnservice_status(self, context, vpnservice_id, status,
|
||||
updated_pending_status=False):
|
||||
vpns = {'status': status}
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
vpns_db = self._get_resource(context, vpn_models.VPNService,
|
||||
vpnservice_id)
|
||||
if (utils.in_pending_status(vpns_db.status) and
|
||||
not updated_pending_status):
|
||||
raise vpnaas.VPNStateInvalidToUpdate(
|
||||
id=vpnservice_id, state=vpns_db.status)
|
||||
vpns_db.update(vpns)
|
||||
return self._make_vpnservice_dict(vpns_db)
|
||||
|
||||
def update_vpnservice(self, context, vpnservice_id, vpnservice):
|
||||
vpns = vpnservice['vpnservice']
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
@ -682,6 +695,22 @@ class VPNPluginDb(vpnaas.VPNPluginBase,
|
||||
vpnservice = self._get_vpnservice(context, vpnservice_id)
|
||||
return vpnservice['router_id']
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_peer_cidrs_for_router(self, context, router_id):
|
||||
filters = {'router_id': [router_id]}
|
||||
vpnservices = model_query.get_collection_query(
|
||||
context, vpn_models.VPNService, filters=filters).all()
|
||||
cidrs = []
|
||||
for vpnservice in vpnservices:
|
||||
for ipsec_site_connection in vpnservice.ipsec_site_connections:
|
||||
if ipsec_site_connection.peer_cidrs:
|
||||
for peer_cidr in ipsec_site_connection.peer_cidrs:
|
||||
cidrs.append(peer_cidr.cidr)
|
||||
if ipsec_site_connection.peer_ep_group is not None:
|
||||
for ep in ipsec_site_connection.peer_ep_group.endpoints:
|
||||
cidrs.append(ep.endpoint)
|
||||
return cidrs
|
||||
|
||||
|
||||
class VPNPluginRpcDbMixin(object):
|
||||
def _build_local_subnet_cidr_map(self, context):
|
||||
|
236
neutron_vpnaas/db/vpn/vpn_ext_gw_db.py
Normal file
236
neutron_vpnaas/db/vpn/vpn_ext_gw_db.py
Normal file
@ -0,0 +1,236 @@
|
||||
# (c) Copyright 2016 IBM Corporation, All Rights Reserved.
|
||||
# (c) Copyright 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.
|
||||
|
||||
from neutron.db.models import l3 as l3_models
|
||||
from neutron.db import models_v2
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_lib.db import api as db_api
|
||||
from neutron_lib.db import model_base
|
||||
from neutron_lib.db import model_query
|
||||
from neutron_lib import exceptions as n_exc
|
||||
from neutron_lib.plugins import constants as plugin_const
|
||||
from neutron_lib.plugins import directory
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import uuidutils
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from neutron_vpnaas._i18n import _
|
||||
from neutron_vpnaas.services.vpn.common import constants as v_constants
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RouterIsNotVPNExternal(n_exc.BadRequest):
|
||||
message = _("Router %(router_id)s has no VPN external network gateway set")
|
||||
|
||||
|
||||
class RouterHasVPNExternal(n_exc.BadRequest):
|
||||
message = _(
|
||||
"Router %(router_id)s already has VPN external network gateway")
|
||||
|
||||
|
||||
class VPNNetworkInUse(n_exc.NetworkInUse):
|
||||
message = _("Network %(network_id)s is used by VPN service")
|
||||
|
||||
|
||||
class VPNExtGW(model_base.BASEV2, model_base.HasId, model_base.HasProject):
|
||||
__tablename__ = 'vpn_ext_gws'
|
||||
router_id = sa.Column(sa.String(36), sa.ForeignKey('routers.id'),
|
||||
nullable=False, unique=True)
|
||||
status = sa.Column(sa.String(16), nullable=False)
|
||||
gw_port_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey('ports.id', ondelete='SET NULL'))
|
||||
transit_port_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey('ports.id', ondelete='SET NULL'))
|
||||
transit_network_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey('networks.id', ondelete='SET NULL'))
|
||||
transit_subnet_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey('subnets.id', ondelete='SET NULL'))
|
||||
|
||||
gw_port = orm.relationship(models_v2.Port, lazy='joined',
|
||||
foreign_keys=[gw_port_id])
|
||||
transit_port = orm.relationship(models_v2.Port, lazy='joined',
|
||||
foreign_keys=[transit_port_id])
|
||||
transit_network = orm.relationship(models_v2.Network)
|
||||
transit_subnet = orm.relationship(models_v2.Subnet)
|
||||
router = orm.relationship(l3_models.Router)
|
||||
|
||||
|
||||
@registry.has_registry_receivers
|
||||
class VPNExtGWPlugin_db(object):
|
||||
"""DB class to support vpn external ports configuration."""
|
||||
|
||||
@property
|
||||
def _core_plugin(self):
|
||||
return directory.get_plugin()
|
||||
|
||||
@property
|
||||
def _vpn_plugin(self):
|
||||
return directory.get_plugin(plugin_const.VPN)
|
||||
|
||||
@staticmethod
|
||||
@registry.receives(resources.PORT, [events.BEFORE_DELETE])
|
||||
def _prevent_vpn_port_delete_callback(resource, event,
|
||||
trigger, payload=None):
|
||||
vpn_plugin = directory.get_plugin(plugin_const.VPN)
|
||||
if vpn_plugin:
|
||||
vpn_plugin.prevent_vpn_port_deletion(payload.context,
|
||||
payload.resource_id)
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def _id_used(self, context, id_column, resource_id):
|
||||
return context.session.query(VPNExtGW).filter(
|
||||
sa.and_(
|
||||
id_column == resource_id,
|
||||
VPNExtGW.status != lib_constants.PENDING_DELETE
|
||||
)
|
||||
).count() > 0
|
||||
|
||||
def prevent_vpn_port_deletion(self, context, port_id):
|
||||
"""Checks to make sure a port is allowed to be deleted.
|
||||
|
||||
Raises an exception if this is not the case. This should be called by
|
||||
any plugin when the API requests the deletion of a port, since some
|
||||
ports for L3 are not intended to be deleted directly via a DELETE
|
||||
to /ports, but rather via other API calls that perform the proper
|
||||
deletion checks.
|
||||
"""
|
||||
try:
|
||||
port = self._core_plugin.get_port(context, port_id)
|
||||
except n_exc.PortNotFound:
|
||||
# non-existent ports don't need to be protected from deletion
|
||||
return
|
||||
|
||||
port_id_column = {
|
||||
v_constants.DEVICE_OWNER_VPN_ROUTER_GW: VPNExtGW.gw_port_id,
|
||||
v_constants.DEVICE_OWNER_TRANSIT_NETWORK:
|
||||
VPNExtGW.transit_port_id,
|
||||
}.get(port['device_owner'])
|
||||
|
||||
if not port_id_column:
|
||||
# This is not a VPN port
|
||||
return
|
||||
|
||||
if self._id_used(context, port_id_column, port_id):
|
||||
reason = _('has device owner %s') % port['device_owner']
|
||||
raise n_exc.ServicePortInUse(port_id=port['id'], reason=reason)
|
||||
|
||||
@staticmethod
|
||||
@registry.receives(resources.SUBNET, [events.BEFORE_DELETE])
|
||||
def _prevent_vpn_subnet_delete_callback(resource, event,
|
||||
trigger, payload=None):
|
||||
vpn_plugin = directory.get_plugin(plugin_const.VPN)
|
||||
if vpn_plugin:
|
||||
vpn_plugin.prevent_vpn_subnet_deletion(payload.context,
|
||||
payload.resource_id)
|
||||
|
||||
def prevent_vpn_subnet_deletion(self, context, subnet_id):
|
||||
if self._id_used(context, VPNExtGW.transit_subnet_id, subnet_id):
|
||||
reason = _('Subnet is used by VPN service')
|
||||
raise n_exc.SubnetInUse(subnet_id=subnet_id, reason=reason)
|
||||
|
||||
@staticmethod
|
||||
@registry.receives(resources.NETWORK, [events.BEFORE_DELETE])
|
||||
def _prevent_vpn_network_delete_callback(resource, event,
|
||||
trigger, payload=None):
|
||||
vpn_plugin = directory.get_plugin(plugin_const.VPN)
|
||||
if vpn_plugin:
|
||||
vpn_plugin.prevent_vpn_network_deletion(payload.context,
|
||||
payload.resource_id)
|
||||
|
||||
def prevent_vpn_network_deletion(self, context, network_id):
|
||||
if self._id_used(context, VPNExtGW.transit_network_id, network_id):
|
||||
raise VPNNetworkInUse(network_id=network_id)
|
||||
|
||||
def _make_vpn_ext_gw_dict(self, gateway_db):
|
||||
if not gateway_db:
|
||||
return None
|
||||
gateway = {
|
||||
'id': gateway_db['id'],
|
||||
'tenant_id': gateway_db['tenant_id'],
|
||||
'router_id': gateway_db['router_id'],
|
||||
'status': gateway_db['status'],
|
||||
}
|
||||
if gateway_db.gw_port:
|
||||
gateway['network_id'] = gateway_db.gw_port['network_id']
|
||||
gateway['external_fixed_ips'] = [
|
||||
{'subnet_id': ip["subnet_id"], 'ip_address': ip["ip_address"]}
|
||||
for ip in gateway_db.gw_port['fixed_ips']
|
||||
]
|
||||
for key in ('gw_port_id', 'transit_port_id', 'transit_network_id',
|
||||
'transit_subnet_id'):
|
||||
value = gateway_db.get(key)
|
||||
if value:
|
||||
gateway[key] = value
|
||||
return gateway
|
||||
|
||||
def _get_vpn_gw_by_router_id(self, context, router_id):
|
||||
try:
|
||||
gateway_db = context.session.query(VPNExtGW).filter(
|
||||
VPNExtGW.router_id == router_id).one()
|
||||
except exc.NoResultFound:
|
||||
return None
|
||||
return gateway_db
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_vpn_gw_by_router_id(self, context, router_id):
|
||||
return self._get_vpn_gw_by_router_id(context, router_id)
|
||||
|
||||
@db_api.CONTEXT_READER
|
||||
def get_vpn_gw_dict_by_router_id(self, context, router_id, refresh=False):
|
||||
gateway_db = self._get_vpn_gw_by_router_id(context, router_id)
|
||||
if gateway_db and refresh:
|
||||
context.session.refresh(gateway_db)
|
||||
return self._make_vpn_ext_gw_dict(gateway_db)
|
||||
|
||||
def create_gateway(self, context, gateway):
|
||||
info = gateway['gateway']
|
||||
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
gateway_db = VPNExtGW(
|
||||
id=uuidutils.generate_uuid(),
|
||||
tenant_id=info['tenant_id'],
|
||||
router_id=info['router_id'],
|
||||
status=lib_constants.PENDING_CREATE,
|
||||
gw_port_id=info.get('gw_port_id'),
|
||||
transit_port_id=info.get('transit_port_id'),
|
||||
transit_network_id=info.get('transit_network_id'),
|
||||
transit_subnet_id=info.get('transit_subnet_id'))
|
||||
context.session.add(gateway_db)
|
||||
|
||||
return self._make_vpn_ext_gw_dict(gateway_db)
|
||||
|
||||
def update_gateway(self, context, gateway_id, gateway):
|
||||
info = gateway['gateway']
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
gateway_db = model_query.get_by_id(context, VPNExtGW, gateway_id)
|
||||
gateway_db.update(info)
|
||||
return self._make_vpn_ext_gw_dict(gateway_db)
|
||||
|
||||
def delete_gateway(self, context, gateway_id):
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
query = context.session.query(VPNExtGW)
|
||||
return query.filter(VPNExtGW.id == gateway_id).delete()
|
190
neutron_vpnaas/extensions/vpn_agentschedulers.py
Normal file
190
neutron_vpnaas/extensions/vpn_agentschedulers.py
Normal file
@ -0,0 +1,190 @@
|
||||
# (c) Copyright 2016 IBM Corporation, 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 abc
|
||||
|
||||
from neutron.api import extensions
|
||||
from neutron.api.v2 import resource
|
||||
from neutron import policy
|
||||
from neutron import wsgi
|
||||
from neutron_lib.api import extensions as lib_extensions
|
||||
from neutron_lib.api import faults as base
|
||||
from neutron_lib import exceptions
|
||||
from neutron_lib.plugins import constants as plugin_const
|
||||
from neutron_lib.plugins import directory
|
||||
from neutron_lib import rpc as n_rpc
|
||||
from oslo_log import log as logging
|
||||
import webob.exc
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VPN_ROUTER = 'vpn-router'
|
||||
VPN_ROUTERS = VPN_ROUTER + 's'
|
||||
VPN_AGENT = 'vpn-agent'
|
||||
VPN_AGENTS = VPN_AGENT + 's'
|
||||
|
||||
|
||||
class VPNRouterSchedulerController(wsgi.Controller):
|
||||
def get_plugin(self):
|
||||
plugin = directory.get_plugin(plugin_const.VPN)
|
||||
if not plugin:
|
||||
LOG.error('No plugin for VPN registered to handle VPN '
|
||||
'router scheduling')
|
||||
msg = 'The resource could not be found.'
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
return plugin
|
||||
|
||||
def index(self, request, **kwargs):
|
||||
plugin = self.get_plugin()
|
||||
policy.enforce(request.context,
|
||||
"get_%s" % VPN_ROUTERS,
|
||||
{})
|
||||
return plugin.list_routers_on_vpn_agent(
|
||||
request.context, kwargs['agent_id'])
|
||||
|
||||
def create(self, request, body, **kwargs):
|
||||
plugin = self.get_plugin()
|
||||
policy.enforce(request.context,
|
||||
"create_%s" % VPN_ROUTER,
|
||||
{})
|
||||
agent_id = kwargs['agent_id']
|
||||
router_id = body['router_id']
|
||||
result = plugin.add_router_to_vpn_agent(request.context, agent_id,
|
||||
router_id)
|
||||
notify(request.context, 'vpn_agent.router.add', router_id, agent_id)
|
||||
return result
|
||||
|
||||
def delete(self, request, id, **kwargs):
|
||||
plugin = self.get_plugin()
|
||||
policy.enforce(request.context,
|
||||
"delete_%s" % VPN_ROUTER,
|
||||
{})
|
||||
agent_id = kwargs['agent_id']
|
||||
result = plugin.remove_router_from_vpn_agent(request.context, agent_id,
|
||||
id)
|
||||
notify(request.context, 'vpn_agent.router.remove', id, agent_id)
|
||||
return result
|
||||
|
||||
|
||||
class VPNAgentsHostingRouterController(wsgi.Controller):
|
||||
def get_plugin(self):
|
||||
plugin = directory.get_plugin(plugin_const.VPN)
|
||||
if not plugin:
|
||||
LOG.error('VPN plugin not registered to handle agent scheduling')
|
||||
msg = 'The resource could not be found.'
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
return plugin
|
||||
|
||||
def index(self, request, **kwargs):
|
||||
plugin = self.get_plugin()
|
||||
policy.enforce(request.context,
|
||||
"get_%s" % VPN_AGENTS,
|
||||
{})
|
||||
return plugin.list_vpn_agents_hosting_router(
|
||||
request.context, kwargs['router_id'])
|
||||
|
||||
|
||||
class Vpn_agentschedulers(lib_extensions.ExtensionDescriptor):
|
||||
"""Extension class supporting VPN agent scheduler.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "VPN Agent Scheduler"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return "vpn-agent-scheduler"
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Schedule VPN services of routers among VPN agents"
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2016-08-15T10:00:00-00:00"
|
||||
|
||||
@classmethod
|
||||
def get_resources(cls):
|
||||
"""Returns Ext Resources."""
|
||||
exts = []
|
||||
parent = dict(member_name="agent",
|
||||
collection_name="agents")
|
||||
|
||||
controller = resource.Resource(VPNRouterSchedulerController(),
|
||||
base.FAULT_MAP)
|
||||
exts.append(extensions.ResourceExtension(
|
||||
VPN_ROUTERS, controller, parent))
|
||||
|
||||
parent = dict(member_name="router",
|
||||
collection_name="routers")
|
||||
|
||||
controller = resource.Resource(VPNAgentsHostingRouterController(),
|
||||
base.FAULT_MAP)
|
||||
exts.append(extensions.ResourceExtension(
|
||||
VPN_AGENTS, controller, parent))
|
||||
return exts
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
return {}
|
||||
|
||||
|
||||
class InvalidVPNAgent(exceptions.agent.AgentNotFound):
|
||||
message = "Agent %(id)s is not a VPN Agent or has been disabled"
|
||||
|
||||
|
||||
class RouterHostedByVPNAgent(exceptions.Conflict):
|
||||
message = ("The VPN service of router %(router_id)s has been already "
|
||||
"hosted by the VPN Agent %(agent_id)s.")
|
||||
|
||||
|
||||
class RouterSchedulingFailed(exceptions.Conflict):
|
||||
message = ("Failed scheduling router %(router_id)s to the VPN Agent "
|
||||
"%(agent_id)s.")
|
||||
|
||||
|
||||
class RouterReschedulingFailed(exceptions.Conflict):
|
||||
message = ("Failed rescheduling router %(router_id)s: "
|
||||
"No eligible VPN agent found.")
|
||||
|
||||
|
||||
class VPNAgentSchedulerPluginBase(object, metaclass=abc.ABCMeta):
|
||||
"""REST API to operate the VPN agent scheduler.
|
||||
|
||||
All methods must be in an admin context.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_router_to_vpn_agent(self, context, id, router_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_router_from_vpn_agent(self, context, id, router_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_routers_on_vpn_agent(self, context, id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_vpn_agents_hosting_router(self, context, router_id):
|
||||
pass
|
||||
|
||||
|
||||
def notify(context, action, router_id, agent_id):
|
||||
info = {'id': agent_id, 'router_id': router_id}
|
||||
notifier = n_rpc.get_notifier('router')
|
||||
notifier.info(context, action, {'agent': info})
|
@ -17,11 +17,35 @@ import abc
|
||||
|
||||
from neutron_lib.api.definitions import vpn
|
||||
from neutron_lib.api import extensions
|
||||
from neutron_lib import exceptions as nexception
|
||||
from neutron_lib.plugins import constants as nconstants
|
||||
from neutron_lib.services import base as service_base
|
||||
|
||||
from neutron.api.v2 import resource_helper
|
||||
|
||||
from neutron_vpnaas._i18n import _
|
||||
|
||||
|
||||
class RouteInUseByVPN(nexception.InUse):
|
||||
"""Operational error indicating a route is used for VPN.
|
||||
|
||||
:param destinations: Destination CIDRs that are peers for VPN
|
||||
"""
|
||||
message = _("Route(s) to %(destinations)s are used for VPN")
|
||||
|
||||
|
||||
class VPNGatewayNotReady(nexception.BadRequest):
|
||||
message = _("VPN gateway not ready")
|
||||
|
||||
|
||||
class VPNGatewayInError(nexception.Conflict):
|
||||
message = _("VPN gateway is in ERROR state. "
|
||||
"Please remove all errored VPN services and try again.")
|
||||
|
||||
|
||||
class NoVPNAgentAvailable(nexception.ServiceUnavailable):
|
||||
message = _("No VPN agent available")
|
||||
|
||||
|
||||
class Vpnaas(extensions.APIExtensionDescriptor):
|
||||
api_definition = vpn
|
||||
|
@ -10,11 +10,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import neutron.conf.plugins.ml2.drivers.ovn.ovn_conf
|
||||
import neutron.services.provider_configuration
|
||||
|
||||
import neutron_vpnaas.services.vpn.agent
|
||||
import neutron_vpnaas.services.vpn.device_drivers.ipsec
|
||||
import neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec
|
||||
import neutron_vpnaas.services.vpn.ovn_agent
|
||||
|
||||
|
||||
def list_agent_opts():
|
||||
@ -31,6 +33,24 @@ def list_agent_opts():
|
||||
]
|
||||
|
||||
|
||||
def list_ovn_agent_opts():
|
||||
return [
|
||||
('vpnagent',
|
||||
neutron_vpnaas.services.vpn.ovn_agent.VPN_AGENT_OPTS),
|
||||
('ovs',
|
||||
neutron_vpnaas.services.vpn.ovn_agent.OVS_OPTS),
|
||||
('ovn',
|
||||
neutron.conf.plugins.ml2.drivers.ovn.ovn_conf.ovn_opts),
|
||||
('ipsec',
|
||||
neutron_vpnaas.services.vpn.device_drivers.ipsec.ipsec_opts),
|
||||
('strongswan',
|
||||
neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec.
|
||||
strongswan_opts),
|
||||
('pluto',
|
||||
neutron_vpnaas.services.vpn.device_drivers.ipsec.pluto_opts)
|
||||
]
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [
|
||||
('service_providers',
|
||||
|
185
neutron_vpnaas/scheduler/vpn_agent_scheduler.py
Normal file
185
neutron_vpnaas/scheduler/vpn_agent_scheduler.py
Normal file
@ -0,0 +1,185 @@
|
||||
# (c) Copyright 2016 IBM Corporation, 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 abc
|
||||
import random
|
||||
|
||||
from neutron.extensions import availability_zone as az_ext
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron_vpnaas.extensions import vpn_agentschedulers
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VPNScheduler(object, metaclass=abc.ABCMeta):
|
||||
@property
|
||||
def l3_plugin(self):
|
||||
return directory.get_plugin(plugin_constants.L3)
|
||||
|
||||
@abc.abstractmethod
|
||||
def schedule(self, plugin, context, router_id,
|
||||
candidates=None, hints=None):
|
||||
"""Schedule the router to an active VPN agent.
|
||||
|
||||
Schedule the router only if it is not already scheduled.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_unscheduled_routers(self, context, plugin, router_ids=None):
|
||||
"""Get the list of routers with VPN services to be scheduled.
|
||||
|
||||
If router IDs are omitted, look for all unscheduled routers.
|
||||
|
||||
:param context: the context
|
||||
:param plugin: the core plugin
|
||||
:param router_ids: the list of routers to be checked for scheduling
|
||||
:returns: the list of routers to be scheduled
|
||||
"""
|
||||
unscheduled_router_ids = plugin.get_unscheduled_vpn_routers(
|
||||
context, router_ids=router_ids)
|
||||
if unscheduled_router_ids:
|
||||
return self.l3_plugin.get_routers(
|
||||
context, filters={'id': unscheduled_router_ids})
|
||||
return []
|
||||
|
||||
def _get_routers_can_schedule(self, context, plugin, routers, vpn_agent):
|
||||
"""Get the subset of routers whose VPN services can be scheduled on
|
||||
the VPN agent.
|
||||
"""
|
||||
# Assuming that only an active, enabled VPN agent is passed in,
|
||||
# all routers can be scheduled to it
|
||||
return routers
|
||||
|
||||
def auto_schedule_routers(self, plugin, context, vpn_agent):
|
||||
"""Schedule non-hosted routers to a VPN agent.
|
||||
|
||||
:returns: True if routers have been successfully assigned to the agent
|
||||
"""
|
||||
unscheduled_routers = self._get_unscheduled_routers(context, plugin)
|
||||
|
||||
target_routers = self._get_routers_can_schedule(
|
||||
context, plugin, unscheduled_routers, vpn_agent)
|
||||
if not target_routers:
|
||||
if unscheduled_routers:
|
||||
LOG.warning('No unscheduled routers compatible with VPN agent '
|
||||
'configuration on host %s', vpn_agent['host'])
|
||||
return []
|
||||
|
||||
self._bind_routers(context, plugin, target_routers, vpn_agent)
|
||||
return [router['id'] for router in target_routers]
|
||||
|
||||
def _get_candidates(self, plugin, context, sync_router):
|
||||
"""Return VPN agents where a router could be scheduled."""
|
||||
active_vpn_agents = plugin.get_vpn_agents(context, active=True)
|
||||
if not active_vpn_agents:
|
||||
LOG.warning('No active VPN agents')
|
||||
return active_vpn_agents
|
||||
|
||||
def _bind_routers(self, context, plugin, routers, vpn_agent):
|
||||
for router in routers:
|
||||
plugin.create_router_to_agent_binding(
|
||||
context, router['id'], vpn_agent['id'])
|
||||
|
||||
def _schedule_router(self, plugin, context, router_id,
|
||||
candidates=None):
|
||||
current_vpn_agents = plugin.get_vpn_agents_hosting_routers(
|
||||
context, [router_id])
|
||||
if current_vpn_agents:
|
||||
chosen_agent = current_vpn_agents[0]
|
||||
LOG.debug('VPN service of router %(router_id)s has already '
|
||||
'been hosted by VPN agent %(agent_id)s',
|
||||
{'router_id': router_id,
|
||||
'agent_id': chosen_agent})
|
||||
return chosen_agent
|
||||
|
||||
sync_router = self.l3_plugin.get_router(context, router_id)
|
||||
candidates = candidates or self._get_candidates(
|
||||
plugin, context, sync_router)
|
||||
if not candidates:
|
||||
raise vpn_agentschedulers.RouterReschedulingFailed(
|
||||
router_id=router_id)
|
||||
|
||||
chosen_agent = self._choose_vpn_agent(plugin, context, candidates)
|
||||
if plugin.create_router_to_agent_binding(context, router_id,
|
||||
chosen_agent['id']):
|
||||
return chosen_agent
|
||||
|
||||
@abc.abstractmethod
|
||||
def _choose_vpn_agent(self, plugin, context, candidates):
|
||||
"""Choose an agent from candidates based on a specific policy."""
|
||||
pass
|
||||
|
||||
|
||||
class ChanceScheduler(VPNScheduler):
|
||||
"""Randomly allocate an VPN agent for a router."""
|
||||
|
||||
def schedule(self, plugin, context, router_id,
|
||||
candidates=None):
|
||||
return self._schedule_router(
|
||||
plugin, context, router_id, candidates=candidates)
|
||||
|
||||
def _choose_vpn_agent(self, plugin, context, candidates):
|
||||
return random.choice(candidates)
|
||||
|
||||
|
||||
class LeastRoutersScheduler(VPNScheduler):
|
||||
"""Allocate to an VPN agent with the least number of routers bound."""
|
||||
|
||||
def schedule(self, plugin, context, router_id,
|
||||
candidates=None):
|
||||
return self._schedule_router(
|
||||
plugin, context, router_id, candidates=candidates)
|
||||
|
||||
def _choose_vpn_agent(self, plugin, context, candidates):
|
||||
candidates_dict = {c['id']: c for c in candidates}
|
||||
chosen_agent_id = plugin.get_vpn_agent_with_min_routers(
|
||||
context, candidates_dict.keys())
|
||||
return candidates_dict[chosen_agent_id]
|
||||
|
||||
|
||||
class AZLeastRoutersScheduler(LeastRoutersScheduler):
|
||||
"""Availability zone aware scheduler."""
|
||||
def _get_az_hints(self, router):
|
||||
return (router.get(az_ext.AZ_HINTS) or
|
||||
cfg.CONF.default_availability_zones)
|
||||
|
||||
def _get_routers_can_schedule(self, context, plugin, routers, vpn_agent):
|
||||
"""Overwrite VPNScheduler's method to filter by availability zone."""
|
||||
target_routers = []
|
||||
for r in routers:
|
||||
az_hints = self._get_az_hints(r)
|
||||
if not az_hints or vpn_agent['availability_zone'] in az_hints:
|
||||
target_routers.append(r)
|
||||
|
||||
if not target_routers:
|
||||
return
|
||||
|
||||
return super()._get_routers_can_schedule(
|
||||
context, plugin, target_routers, vpn_agent)
|
||||
|
||||
def _get_candidates(self, plugin, context, sync_router):
|
||||
"""Overwrite VPNScheduler's method to filter by availability zone."""
|
||||
all_candidates = super()._get_candidates(plugin, context, sync_router)
|
||||
|
||||
candidates = []
|
||||
az_hints = self._get_az_hints(sync_router)
|
||||
for agent in all_candidates:
|
||||
if not az_hints or agent['availability_zone'] in az_hints:
|
||||
candidates.append(agent)
|
||||
|
||||
return candidates
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from neutron_lib import constants
|
||||
|
||||
# Endpoint group types
|
||||
SUBNET_ENDPOINT = 'subnet'
|
||||
CIDR_ENDPOINT = 'cidr'
|
||||
@ -30,3 +32,15 @@ VPN_SUPPORTED_ENDPOINT_TYPES = [
|
||||
SUBNET_ENDPOINT, CIDR_ENDPOINT, VLAN_ENDPOINT,
|
||||
NETWORK_ENDPOINT, ROUTER_ENDPOINT,
|
||||
]
|
||||
|
||||
AGENT_TYPE_VPN = "VPN Agent"
|
||||
|
||||
DEVICE_OWNER_VPN_ROUTER_GW = constants.DEVICE_OWNER_NETWORK_PREFIX + \
|
||||
"vpn_router_gateway"
|
||||
|
||||
DEVICE_OWNER_TRANSIT_NETWORK = constants.DEVICE_OWNER_NETWORK_PREFIX + \
|
||||
"vpn_namespace"
|
||||
|
||||
OVN_AGENT_VPN_SB_CFG_KEY = 'neutron:ovn-vpnagent-sb-cfg'
|
||||
OVN_AGENT_VPN_DESC_KEY = 'neutron:description-vpnagent'
|
||||
OVN_AGENT_VPN_ID_KEY = 'neutron:ovn-vpnagent-id'
|
||||
|
376
neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py
Normal file
376
neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py
Normal file
@ -0,0 +1,376 @@
|
||||
# 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 netaddr
|
||||
|
||||
from neutron.agent.common import utils as agent_common_utils
|
||||
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 = {
|
||||
'internal': PORT_PREFIX_INTERNAL,
|
||||
'external': PORT_PREFIX_EXTERNAL,
|
||||
}
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceManager(object):
|
||||
"""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 = agent_common_utils.load_interface_driver(conf)
|
||||
|
||||
def get_interface_name(self, port, ptype):
|
||||
suffix = port['id']
|
||||
return (PORT_PREFIXES[ptype] + suffix)[:self.driver.DEV_NAME_LEN]
|
||||
|
||||
def get_namespace_name(self, process_id):
|
||||
return self.OVN_NS_PREFIX + process_id
|
||||
|
||||
def get_existing_process_ids(self):
|
||||
"""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):
|
||||
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, ptype):
|
||||
namespace = self.get_namespace_name(process_id)
|
||||
prefix = PORT_PREFIXES[ptype]
|
||||
device = ip_lib.IPDevice(None, namespace=namespace)
|
||||
ports = 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):
|
||||
self._del_port(process_id, 'internal')
|
||||
|
||||
def del_external_port(self, process_id):
|
||||
self._del_port(process_id, 'external')
|
||||
|
||||
def setup_external(self, process_id, network_details):
|
||||
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 = '%s/%s' % (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):
|
||||
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 = '%s/%s' % (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(object):
|
||||
def __init__(self, use_ipv6=False):
|
||||
self.ip_wrapper_root = ip_lib.IPWrapper()
|
||||
self.use_ipv6 = use_ipv6
|
||||
|
||||
def exists(self, name):
|
||||
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):
|
||||
|
||||
def __init__(self, vpn_service, host):
|
||||
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):
|
||||
"""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):
|
||||
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, network_details):
|
||||
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
|
||||
|
||||
def destroy_process(self, process_id):
|
||||
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):
|
||||
# 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):
|
||||
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, router_id):
|
||||
# must run under the same lock as sync()
|
||||
self.destroy_process(router_id)
|
||||
|
||||
def vpnservice_added_to_agent(self, context, router_ids):
|
||||
routers = [{'id': router_id} for router_id in router_ids]
|
||||
self.sync(context, routers)
|
||||
|
||||
|
||||
class OvnStrongSwanDriver(OvnIPsecDriver):
|
||||
def create_process(self, process_id, vpnservice, namespace):
|
||||
return OvnStrongSwanProcess(
|
||||
self.conf,
|
||||
process_id,
|
||||
vpnservice,
|
||||
namespace)
|
||||
|
||||
|
||||
class OvnOpenSwanDriver(OvnIPsecDriver):
|
||||
def create_process(self, process_id, vpnservice, namespace):
|
||||
return OvnOpenSwanProcess(
|
||||
self.conf,
|
||||
process_id,
|
||||
vpnservice,
|
||||
namespace)
|
||||
|
||||
|
||||
class OvnLibreSwanDriver(OvnIPsecDriver):
|
||||
def create_process(self, process_id, vpnservice, namespace):
|
||||
return OvnLibreSwanProcess(
|
||||
self.conf,
|
||||
process_id,
|
||||
vpnservice,
|
||||
namespace)
|
0
neutron_vpnaas/services/vpn/ovn/__init__.py
Normal file
0
neutron_vpnaas/services/vpn/ovn/__init__.py
Normal file
84
neutron_vpnaas/services/vpn/ovn/agent_monitor.py
Normal file
84
neutron_vpnaas/services/vpn/ovn/agent_monitor.py
Normal file
@ -0,0 +1,84 @@
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron.plugins.ml2.drivers.ovn.agent import neutron_agent
|
||||
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
|
||||
from neutron_vpnaas.services.vpn.common import constants
|
||||
|
||||
|
||||
class OVNVPNAgent(neutron_agent.NeutronAgent):
|
||||
agent_type = constants.AGENT_TYPE_VPN
|
||||
binary = "neutron-ovn-vpn-agent"
|
||||
|
||||
@property
|
||||
def nb_cfg(self):
|
||||
return int(self.chassis_private.external_ids.get(
|
||||
constants.OVN_AGENT_VPN_SB_CFG_KEY, 0))
|
||||
|
||||
@staticmethod
|
||||
def id_from_chassis_private(chassis_private):
|
||||
return chassis_private.external_ids.get(
|
||||
constants.OVN_AGENT_VPN_ID_KEY)
|
||||
|
||||
@property
|
||||
def agent_id(self):
|
||||
return self.id_from_chassis_private(self.chassis_private)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.chassis_private.external_ids.get(
|
||||
constants.OVN_AGENT_VPN_DESC_KEY, '')
|
||||
|
||||
|
||||
class ChassisVPNAgentWriteEvent(ovsdb_monitor.ChassisAgentEvent):
|
||||
events = (ovsdb_monitor.BaseEvent.ROW_CREATE,
|
||||
ovsdb_monitor.BaseEvent.ROW_UPDATE)
|
||||
|
||||
@staticmethod
|
||||
def _vpnagent_nb_cfg(row):
|
||||
return int(
|
||||
row.external_ids.get(constants.OVN_AGENT_VPN_SB_CFG_KEY, -1))
|
||||
|
||||
@staticmethod
|
||||
def agent_id(row):
|
||||
return row.external_ids.get(constants.OVN_AGENT_VPN_ID_KEY)
|
||||
|
||||
def match_fn(self, event, row, old=None):
|
||||
if not self.agent_id(row):
|
||||
# Don't create a cached object with an agent_id of 'None'
|
||||
return False
|
||||
if event == self.ROW_CREATE:
|
||||
return True
|
||||
try:
|
||||
return self._vpnagent_nb_cfg(row) != self._vpnagent_nb_cfg(old)
|
||||
except (AttributeError, KeyError):
|
||||
return False
|
||||
|
||||
def run(self, event, row, old):
|
||||
neutron_agent.AgentCache().update(constants.AGENT_TYPE_VPN, row,
|
||||
clear_down=True)
|
||||
|
||||
|
||||
class OVNVPNAgentMonitor(object):
|
||||
def watch_agent_events(self):
|
||||
l3_plugin = directory.get_plugin(plugin_constants.L3)
|
||||
sb_ovn = l3_plugin._sb_ovn
|
||||
if sb_ovn:
|
||||
idl = sb_ovn.ovsdb_connection.idl
|
||||
if isinstance(idl, ovsdb_monitor.OvnSbIdl):
|
||||
idl.notify_handler.watch_event(
|
||||
ChassisVPNAgentWriteEvent(idl.driver))
|
71
neutron_vpnaas/services/vpn/ovn_agent.py
Normal file
71
neutron_vpnaas/services/vpn/ovn_agent.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright 2013, Nachi Ueno, NTT I3, Inc.
|
||||
# Copyright 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 sys
|
||||
|
||||
from neutron.common import config as common_config
|
||||
from neutron.conf.agent import common as agent_config
|
||||
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
|
||||
from neutron_vpnaas._i18n import _
|
||||
from neutron_vpnaas.agent.ovn.vpn import agent
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VPN_AGENT_OPTS = [
|
||||
cfg.MultiStrOpt(
|
||||
'vpn_device_driver',
|
||||
default=['neutron_vpnaas.services.vpn.device_drivers.'
|
||||
'ovn_ipsec.OvnStrongSwanDriver'],
|
||||
sample_default=['neutron_vpnaas.services.vpn.device_drivers.'
|
||||
'ovn_ipsec.OvnStrongSwanDriver'],
|
||||
help=_("The OVN VPN device drivers Neutron will use")),
|
||||
]
|
||||
|
||||
OVS_OPTS = [
|
||||
cfg.StrOpt('ovsdb_connection',
|
||||
default='unix:/usr/local/var/run/openvswitch/db.sock',
|
||||
help=_('The connection string for the native OVSDB backend.\n'
|
||||
'Use tcp:IP:PORT for TCP connection.\n'
|
||||
'Use unix:FILE for unix domain socket connection.')),
|
||||
cfg.IntOpt('ovsdb_connection_timeout',
|
||||
default=180,
|
||||
help=_('Timeout in seconds for the OVSDB '
|
||||
'connection transaction'))
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
common_config.register_common_config_options()
|
||||
agent_config.register_interface_driver_opts_helper(conf)
|
||||
agent_config.register_interface_opts(conf)
|
||||
agent_config.register_availability_zone_opts_helper(conf)
|
||||
ovn_conf.register_opts()
|
||||
conf.register_opts(VPN_AGENT_OPTS, 'vpnagent')
|
||||
conf.register_opts(OVS_OPTS, 'ovs')
|
||||
|
||||
|
||||
def main():
|
||||
register_opts(cfg.CONF)
|
||||
common_config.init(sys.argv[1:])
|
||||
agent_config.setup_logging()
|
||||
agent_config.setup_privsep()
|
||||
|
||||
agt = agent.OvnVpnAgent(cfg.CONF)
|
||||
service.launch(cfg.CONF, agt, restart_method='mutate').wait()
|
76
neutron_vpnaas/services/vpn/ovn_plugin.py
Normal file
76
neutron_vpnaas/services/vpn/ovn_plugin.py
Normal file
@ -0,0 +1,76 @@
|
||||
# (c) Copyright 2016 IBM Corporation
|
||||
# (c) Copyright 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.
|
||||
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
|
||||
from neutron_vpnaas.api.rpc.agentnotifiers import vpn_rpc_agent_api as nfy_api
|
||||
from neutron_vpnaas.db.vpn import vpn_agentschedulers_db as agent_db
|
||||
from neutron_vpnaas.db.vpn.vpn_db import VPNPluginDb
|
||||
from neutron_vpnaas.db.vpn import vpn_ext_gw_db
|
||||
from neutron_vpnaas.services.vpn.common import constants
|
||||
from neutron_vpnaas.services.vpn.ovn import agent_monitor
|
||||
from neutron_vpnaas.services.vpn.plugin import VPNDriverPlugin
|
||||
|
||||
|
||||
class VPNOVNPlugin(VPNPluginDb,
|
||||
vpn_ext_gw_db.VPNExtGWPlugin_db,
|
||||
agent_db.AZVPNAgentSchedulerDbMixin,
|
||||
agent_monitor.OVNVPNAgentMonitor):
|
||||
"""Implementation of the VPN Service Plugin.
|
||||
|
||||
This class manages the workflow of VPNaaS request/response.
|
||||
Most DB related works are implemented in class
|
||||
vpn_db.VPNPluginDb.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.vpn_scheduler = importutils.import_object(
|
||||
cfg.CONF.vpn_scheduler_driver)
|
||||
self.add_periodic_vpn_agent_status_check()
|
||||
self.agent_notifiers[constants.AGENT_TYPE_VPN] = \
|
||||
nfy_api.VPNAgentNotifyAPI()
|
||||
super().__init__()
|
||||
registry.subscribe(self.post_fork_initialize,
|
||||
resources.PROCESS,
|
||||
events.AFTER_INIT)
|
||||
|
||||
def check_router_in_use(self, context, router_id):
|
||||
pass
|
||||
|
||||
def post_fork_initialize(self, resource, event, trigger, payload=None):
|
||||
self.watch_agent_events()
|
||||
|
||||
def vpn_router_agent_binding_changed(self, context, router_id, host):
|
||||
pass
|
||||
|
||||
supported_extension_aliases = ["vpnaas",
|
||||
"vpn-endpoint-groups",
|
||||
"service-type",
|
||||
"vpn-agent-scheduler"]
|
||||
path_prefix = "/vpn"
|
||||
|
||||
|
||||
class VPNOVNDriverPlugin(VPNOVNPlugin, VPNDriverPlugin):
|
||||
def vpn_router_agent_binding_changed(self, context, router_id, host):
|
||||
super().vpn_router_agent_binding_changed(context, router_id, host)
|
||||
filters = {'router_id': [router_id]}
|
||||
vpnservices = self.get_vpnservices(context, filters=filters)
|
||||
for vpnservice in vpnservices:
|
||||
driver = self._get_driver_for_vpnservice(context, vpnservice)
|
||||
driver.update_port_bindings(context, router_id, host)
|
@ -75,6 +75,13 @@ class VPNDriverPlugin(VPNPlugin, vpn_db.VPNPluginRpcDbMixin):
|
||||
def _flavors_plugin(self):
|
||||
return directory.get_plugin(constants.FLAVORS)
|
||||
|
||||
def start_rpc_listeners(self):
|
||||
servers = []
|
||||
for driver_name, driver in self.drivers.items():
|
||||
if hasattr(driver, 'start_rpc_listeners'):
|
||||
servers.extend(driver.start_rpc_listeners())
|
||||
return servers
|
||||
|
||||
def _check_orphan_vpnservice_associations(self):
|
||||
context = ncontext.get_admin_context()
|
||||
vpnservices = self.get_vpnservices(context)
|
||||
|
516
neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py
Normal file
516
neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py
Normal file
@ -0,0 +1,516 @@
|
||||
# Copyright 2016, Yi Jing Zhu, IBM.
|
||||
# Copyright 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 netaddr
|
||||
|
||||
from neutron_lib.api.definitions import portbindings
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_lib import context as nctx
|
||||
from neutron_lib.db import api as db_api
|
||||
from neutron_lib import exceptions as n_exc
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
from neutron_lib.plugins import utils as p_utils
|
||||
from neutron_lib import rpc as n_rpc
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_db import exception as o_exc
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron_vpnaas.db.vpn import vpn_agentschedulers_db as agent_db
|
||||
from neutron_vpnaas.db.vpn.vpn_ext_gw_db import RouterIsNotVPNExternal
|
||||
from neutron_vpnaas.db.vpn import vpn_models
|
||||
from neutron_vpnaas.extensions import vpnaas
|
||||
from neutron_vpnaas.services.vpn.common import constants as v_constants
|
||||
from neutron_vpnaas.services.vpn.common import topics
|
||||
from neutron_vpnaas.services.vpn.service_drivers import base_ipsec
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
IPSEC = 'ipsec'
|
||||
BASE_IPSEC_VERSION = '1.0'
|
||||
|
||||
TRANSIT_NETWORK_PREFIX = 'vpn-transit-network-'
|
||||
TRANSIT_SUBNET_PREFIX = 'vpn-transit-subnet-'
|
||||
TRANSIT_PORT_PREFIX = 'vpn-ns-'
|
||||
VPN_GW_PORT_PREFIX = 'vpn-gw-'
|
||||
VPN_TRANSIT_LIP = '169.254.0.1'
|
||||
VPN_TRANSIT_RIP = '169.254.0.2'
|
||||
VPN_TRANSIT_CIDR = '169.254.0.0/28'
|
||||
HIDDEN_PROJECT_ID = ''
|
||||
|
||||
|
||||
class IPsecVpnOvnDriverCallBack(base_ipsec.IPsecVpnDriverCallBack):
|
||||
def __init__(self, driver):
|
||||
super().__init__(driver)
|
||||
self.admin_ctx = nctx.get_admin_context()
|
||||
|
||||
@property
|
||||
def core_plugin(self):
|
||||
return self.driver.core_plugin
|
||||
|
||||
@property
|
||||
def service_plugin(self):
|
||||
return self.driver.service_plugin
|
||||
|
||||
def _get_vpn_gateway(self, context, router_id):
|
||||
return self.service_plugin.get_vpn_gw_by_router_id(context, router_id)
|
||||
|
||||
def get_vpn_transit_network_details(self, context, router_id):
|
||||
vpn_gw = self._get_vpn_gateway(context, router_id)
|
||||
network_id = vpn_gw.gw_port['network_id']
|
||||
external_network = self.core_plugin.get_network(context, network_id)
|
||||
|
||||
details = {
|
||||
'gw_port': vpn_gw.gw_port,
|
||||
'transit_port': vpn_gw.transit_port,
|
||||
'transit_gateway_ip': VPN_TRANSIT_LIP,
|
||||
'external_network': external_network,
|
||||
}
|
||||
return details
|
||||
|
||||
def get_subnet_info(self, context, subnet_id=None):
|
||||
try:
|
||||
return self.core_plugin.get_subnet(context, subnet_id)
|
||||
except n_exc.SubnetNotFound:
|
||||
return None
|
||||
|
||||
def _get_agent_hosting_vpn_services(self, context, host):
|
||||
agent = self.service_plugin.get_vpn_agent_on_host(context, host)
|
||||
if not agent:
|
||||
return []
|
||||
|
||||
# We're here because a VPN agent asked for the VPN services it's
|
||||
# hosting. This means, the agent is alive. This is a chance to
|
||||
# schedule VPN services of routers that are still unscheduled.
|
||||
if cfg.CONF.vpn_auto_schedule:
|
||||
self.service_plugin.auto_schedule_routers(context, agent)
|
||||
|
||||
query = context.session.query(vpn_models.VPNService)
|
||||
query = query.join(vpn_models.IPsecSiteConnection)
|
||||
query = query.join(agent_db.RouterVPNAgentBinding,
|
||||
agent_db.RouterVPNAgentBinding.router_id ==
|
||||
vpn_models.VPNService.router_id)
|
||||
query = query.filter(
|
||||
agent_db.RouterVPNAgentBinding.vpn_agent_id == agent['id'])
|
||||
return query
|
||||
|
||||
|
||||
@registry.has_registry_receivers
|
||||
class BaseOvnIPsecVPNDriver(base_ipsec.BaseIPsecVPNDriver):
|
||||
def __init__(self, service_plugin):
|
||||
self._l3_plugin = None
|
||||
self._core_plugin = None
|
||||
super().__init__(service_plugin)
|
||||
|
||||
@property
|
||||
def l3_plugin(self):
|
||||
if self._l3_plugin is None:
|
||||
self._l3_plugin = directory.get_plugin(plugin_constants.L3)
|
||||
return self._l3_plugin
|
||||
|
||||
@property
|
||||
def core_plugin(self):
|
||||
if self._core_plugin is None:
|
||||
self._core_plugin = directory.get_plugin()
|
||||
return self._core_plugin
|
||||
|
||||
@registry.receives(resources.ROUTER, [events.PRECOMMIT_UPDATE])
|
||||
def _handle_router_precommit_update(self, resource, event, trigger,
|
||||
payload):
|
||||
"""Check that a router update won't remove routes we need for VPN."""
|
||||
LOG.debug("Router %s PRECOMMIT_UPDATE event: %s",
|
||||
payload.resource_id, payload.request_body)
|
||||
router_id = payload.resource_id
|
||||
context = payload.context
|
||||
router_data = payload.request_body
|
||||
routes_removed = router_data.get('routes_removed')
|
||||
|
||||
if not routes_removed:
|
||||
return
|
||||
|
||||
removed_cidrs = {r['destination'] for r in routes_removed}
|
||||
vpn_cidrs = set(
|
||||
self.service_plugin.get_peer_cidrs_for_router(context, router_id))
|
||||
conflict_cidrs = removed_cidrs.intersection(vpn_cidrs)
|
||||
|
||||
if conflict_cidrs:
|
||||
raise vpnaas.RouteInUseByVPN(
|
||||
destinations=", ".join(conflict_cidrs))
|
||||
|
||||
def get_vpn_gw_port_name(self, router_id):
|
||||
return VPN_GW_PORT_PREFIX + router_id
|
||||
|
||||
def get_vpn_namespace_port_name(self, router_id):
|
||||
return TRANSIT_PORT_PREFIX + router_id
|
||||
|
||||
def get_transit_network_name(self, router_id):
|
||||
return TRANSIT_NETWORK_PREFIX + router_id
|
||||
|
||||
def get_transit_subnet_name(self, router_id):
|
||||
return TRANSIT_SUBNET_PREFIX + router_id
|
||||
|
||||
def make_transit_network(self, router_id, tenant_id, agent_host,
|
||||
gateway_update):
|
||||
context = nctx.get_admin_context()
|
||||
network_data = {
|
||||
'tenant_id': HIDDEN_PROJECT_ID,
|
||||
'name': self.get_transit_network_name(router_id),
|
||||
'admin_state_up': True,
|
||||
'shared': False,
|
||||
}
|
||||
network = p_utils.create_network(self.core_plugin, context,
|
||||
{'network': network_data})
|
||||
gateway_update['transit_network_id'] = network['id']
|
||||
|
||||
# The subnet tenant_id must be of the user, otherwise updating the
|
||||
# router by the user may fail (it needs access to all subnets)
|
||||
subnet_data = {
|
||||
'tenant_id': tenant_id,
|
||||
'name': self.get_transit_subnet_name(router_id),
|
||||
'gateway_ip': VPN_TRANSIT_LIP,
|
||||
'cidr': VPN_TRANSIT_CIDR,
|
||||
'network_id': network['id'],
|
||||
'ip_version': 4,
|
||||
'enable_dhcp': False,
|
||||
}
|
||||
subnet = p_utils.create_subnet(self.core_plugin, context,
|
||||
{'subnet': subnet_data})
|
||||
gateway_update['transit_subnet_id'] = subnet['id']
|
||||
|
||||
self.l3_plugin.add_router_interface(context, router_id,
|
||||
{'subnet_id': subnet['id']})
|
||||
|
||||
fixed_ip = {'subnet_id': subnet['id'], 'ip_address': VPN_TRANSIT_RIP}
|
||||
port_data = {
|
||||
'tenant_id': HIDDEN_PROJECT_ID,
|
||||
'network_id': network['id'],
|
||||
'fixed_ips': [fixed_ip],
|
||||
'device_id': subnet['id'],
|
||||
'device_owner': v_constants.DEVICE_OWNER_TRANSIT_NETWORK,
|
||||
'admin_state_up': True,
|
||||
portbindings.HOST_ID: agent_host,
|
||||
'name': self.get_vpn_namespace_port_name(router_id)
|
||||
}
|
||||
port = p_utils.create_port(self.core_plugin, context,
|
||||
{"port": port_data})
|
||||
gateway_update['transit_port_id'] = port['id']
|
||||
|
||||
def _del_port(self, context, port_id):
|
||||
try:
|
||||
self.core_plugin.delete_port(context, port_id, l3_port_check=False)
|
||||
except n_exc.PortNotFound:
|
||||
pass
|
||||
|
||||
def _remove_router_interface(self, context, router_id, subnet_id):
|
||||
try:
|
||||
self.l3_plugin.remove_router_interface(
|
||||
context, router_id, {'subnet_id': subnet_id})
|
||||
except (n_exc.l3.RouterInterfaceNotFoundForSubnet,
|
||||
n_exc.SubnetNotFound):
|
||||
pass
|
||||
|
||||
def _del_subnet(self, context, subnet_id):
|
||||
try:
|
||||
self.core_plugin.delete_subnet(context, subnet_id)
|
||||
except n_exc.SubnetNotFound:
|
||||
pass
|
||||
|
||||
def _del_network(self, context, network_id):
|
||||
try:
|
||||
self.core_plugin.delete_network(context, network_id)
|
||||
except n_exc.NetworkNotFound:
|
||||
pass
|
||||
|
||||
def del_transit_network(self, gw):
|
||||
context = nctx.get_admin_context()
|
||||
router_id = gw['router_id']
|
||||
|
||||
port_id = gw.get('transit_port_id')
|
||||
if port_id:
|
||||
self._del_port(context, port_id)
|
||||
|
||||
subnet_id = gw.get('transit_subnet_id')
|
||||
if subnet_id:
|
||||
self._remove_router_interface(context, router_id, subnet_id)
|
||||
self._del_subnet(context, subnet_id)
|
||||
|
||||
network_id = gw.get('transit_network_id')
|
||||
if network_id:
|
||||
self._del_network(context, network_id)
|
||||
|
||||
def make_gw_port(self, router_id, network_id, agent_host, gateway_update):
|
||||
context = nctx.get_admin_context()
|
||||
port_data = {'tenant_id': HIDDEN_PROJECT_ID,
|
||||
'network_id': network_id,
|
||||
'fixed_ips': lib_constants.ATTR_NOT_SPECIFIED,
|
||||
'device_id': router_id,
|
||||
'device_owner': v_constants.DEVICE_OWNER_VPN_ROUTER_GW,
|
||||
'admin_state_up': True,
|
||||
portbindings.HOST_ID: agent_host,
|
||||
'name': self.get_vpn_gw_port_name(router_id)}
|
||||
gw_port = p_utils.create_port(self.core_plugin, context.elevated(),
|
||||
{'port': port_data})
|
||||
|
||||
if not gw_port['fixed_ips']:
|
||||
LOG.debug('No IPs available for external network %s', network_id)
|
||||
gateway_update['gw_port_id'] = gw_port['id']
|
||||
|
||||
def del_gw_port(self, gateway):
|
||||
context = nctx.get_admin_context()
|
||||
port_id = gateway.get('gw_port_id')
|
||||
if port_id:
|
||||
self._del_port(context, port_id)
|
||||
|
||||
def _get_peer_cidrs(self, vpnservice):
|
||||
cidrs = []
|
||||
for ipsec_site_connection in vpnservice.ipsec_site_connections:
|
||||
if ipsec_site_connection.peer_cidrs:
|
||||
for peer_cidr in ipsec_site_connection.peer_cidrs:
|
||||
cidrs.append(peer_cidr.cidr)
|
||||
if ipsec_site_connection.peer_ep_group is not None:
|
||||
for ep in ipsec_site_connection.peer_ep_group.endpoints:
|
||||
cidrs.append(ep.endpoint)
|
||||
return cidrs
|
||||
|
||||
def _routes_update(self, cidrs, nexthop):
|
||||
routes = [{'destination': cidr, 'nexthop': nexthop}
|
||||
for cidr in cidrs]
|
||||
return {'router': {'routes': routes}}
|
||||
|
||||
def _update_static_routes(self, context, ipsec_site_connection):
|
||||
vpnservice = self.service_plugin.get_vpnservice(
|
||||
context, ipsec_site_connection['vpnservice_id'])
|
||||
router_id = vpnservice['router_id']
|
||||
gw = self.service_plugin.get_vpn_gw_by_router_id(context, router_id)
|
||||
|
||||
nexthop = gw.transit_port['fixed_ips'][0]['ip_address']
|
||||
|
||||
router = self.l3_plugin.get_router(context, router_id)
|
||||
old_routes = router.get('routes', [])
|
||||
|
||||
old_cidrs = set([r['destination'] for r in old_routes
|
||||
if r['nexthop'] == nexthop])
|
||||
new_cidrs = set(
|
||||
self.service_plugin.get_peer_cidrs_for_router(context, router_id))
|
||||
|
||||
to_remove = old_cidrs - new_cidrs
|
||||
if to_remove:
|
||||
self.l3_plugin.remove_extraroutes(context, router_id,
|
||||
self._routes_update(to_remove, nexthop))
|
||||
|
||||
to_add = new_cidrs - old_cidrs
|
||||
if to_add:
|
||||
self.l3_plugin.add_extraroutes(context, router_id,
|
||||
self._routes_update(to_add, nexthop))
|
||||
|
||||
def _get_gateway_ips(self, router):
|
||||
"""Obtain the IPv4 and/or IPv6 GW IP for the router.
|
||||
|
||||
If there are multiples, (arbitrarily) use the first one.
|
||||
"""
|
||||
gateway = self.service_plugin.get_vpn_gw_dict_by_router_id(
|
||||
nctx.get_admin_context(),
|
||||
router['id'])
|
||||
if gateway is None or gateway['external_fixed_ips'] is None:
|
||||
raise RouterIsNotVPNExternal(router_id=router['id'])
|
||||
|
||||
v4_ip = v6_ip = None
|
||||
for fixed_ip in gateway['external_fixed_ips']:
|
||||
addr = fixed_ip['ip_address']
|
||||
vers = netaddr.IPAddress(addr).version
|
||||
if vers == lib_constants.IP_VERSION_4:
|
||||
if v4_ip is None:
|
||||
v4_ip = addr
|
||||
elif v6_ip is None:
|
||||
v6_ip = addr
|
||||
return v4_ip, v6_ip
|
||||
|
||||
def _update_gateway(self, context, gateway_id, **kwargs):
|
||||
gateway = {'gateway': kwargs}
|
||||
return self.service_plugin.update_gateway(context, gateway_id, gateway)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def _ensure_gateway(self, context, vpnservice):
|
||||
gw = self.service_plugin.get_vpn_gw_dict_by_router_id(
|
||||
context, vpnservice['router_id'], refresh=True)
|
||||
if not gw:
|
||||
gateway = {'gateway': {
|
||||
'router_id': vpnservice['router_id'],
|
||||
'tenant_id': vpnservice['tenant_id'],
|
||||
}}
|
||||
# create_gateway may raise oslo_db.exception.DBDuplicateEntry
|
||||
# if someone else created one in the meantime
|
||||
return self.service_plugin.create_gateway(context, gateway)
|
||||
|
||||
if gw['status'] == lib_constants.ERROR:
|
||||
raise vpnaas.VPNGatewayInError()
|
||||
|
||||
# Raise an exception if an existing gateway is in status
|
||||
# PENDING_CREATE or PENDING_DELETE.
|
||||
# One of the next retries should succeed.
|
||||
if gw['status'] != lib_constants.ACTIVE:
|
||||
raise o_exc.RetryRequest(vpnaas.VPNGatewayNotReady())
|
||||
return gw
|
||||
|
||||
@db_api.CONTEXT_WRITER
|
||||
def _setup(self, context, vpnservice_dict):
|
||||
router_id = vpnservice_dict['router_id']
|
||||
agent = self.service_plugin.schedule_router(context, router_id)
|
||||
if not agent:
|
||||
raise vpnaas.NoVPNAgentAvailable
|
||||
agent_host = agent['host']
|
||||
|
||||
gateway = self._ensure_gateway(context, vpnservice_dict)
|
||||
|
||||
# If the gateway status is ACTIVE the ports have been created already
|
||||
if gateway['status'] == lib_constants.ACTIVE:
|
||||
return
|
||||
|
||||
vpnservice = self.service_plugin._get_vpnservice(context,
|
||||
vpnservice_dict['id'])
|
||||
network_id = vpnservice.router.gw_port.network_id
|
||||
gateway_update = {} # keeps track of already-created IDs
|
||||
try:
|
||||
self.make_gw_port(router_id, network_id, agent_host,
|
||||
gateway_update)
|
||||
self.make_transit_network(router_id,
|
||||
vpnservice_dict['tenant_id'],
|
||||
agent_host,
|
||||
gateway_update)
|
||||
except Exception:
|
||||
self._update_gateway(context, gateway['id'],
|
||||
status=lib_constants.ERROR,
|
||||
**gateway_update)
|
||||
raise
|
||||
|
||||
self._update_gateway(context, gateway['id'],
|
||||
status=lib_constants.ACTIVE,
|
||||
**gateway_update)
|
||||
|
||||
def _cleanup(self, context, router_id):
|
||||
gw = self.service_plugin.get_vpn_gw_dict_by_router_id(context,
|
||||
router_id)
|
||||
if not gw:
|
||||
return
|
||||
self._update_gateway(context, gw['id'],
|
||||
status=lib_constants.PENDING_DELETE)
|
||||
try:
|
||||
self.del_gw_port(gw)
|
||||
self.del_transit_network(gw)
|
||||
self.service_plugin.delete_gateway(context, gw['id'])
|
||||
except Exception:
|
||||
LOG.exception("Cleanup of VPN gateway for router %s failed.",
|
||||
router_id)
|
||||
self._update_gateway(context, gw['id'],
|
||||
status=lib_constants.ERROR)
|
||||
raise
|
||||
|
||||
def create_vpnservice(self, context, vpnservice_dict):
|
||||
try:
|
||||
self._setup(context, vpnservice_dict)
|
||||
except Exception:
|
||||
LOG.exception("Setting up the VPN gateway for router %s failed.",
|
||||
vpnservice_dict['router_id'])
|
||||
self.service_plugin.set_vpnservice_status(
|
||||
context, vpnservice_dict['id'], lib_constants.ERROR,
|
||||
updated_pending_status=True)
|
||||
raise
|
||||
super().create_vpnservice(context, vpnservice_dict)
|
||||
|
||||
def delete_vpnservice(self, context, vpnservice):
|
||||
router_id = vpnservice['router_id']
|
||||
super().delete_vpnservice(context, vpnservice)
|
||||
services = self.service_plugin.get_vpnservices(context)
|
||||
router_ids = [s['router_id'] for s in services]
|
||||
if router_id not in router_ids:
|
||||
self._cleanup(context, router_id)
|
||||
|
||||
def create_ipsec_site_connection(self, context, ipsec_site_connection):
|
||||
self._update_static_routes(context, ipsec_site_connection)
|
||||
super().create_ipsec_site_connection(context, ipsec_site_connection)
|
||||
|
||||
def delete_ipsec_site_connection(self, context, ipsec_site_connection):
|
||||
self._update_static_routes(context, ipsec_site_connection)
|
||||
super().delete_ipsec_site_connection(context, ipsec_site_connection)
|
||||
|
||||
def update_ipsec_site_connection(
|
||||
self, context, old_ipsec_site_connection, ipsec_site_connection):
|
||||
self._update_static_routes(context, ipsec_site_connection)
|
||||
super().update_ipsec_site_connection(
|
||||
context, old_ipsec_site_connection, ipsec_site_connection)
|
||||
|
||||
def _update_port_binding(self, context, port_id, host):
|
||||
port_data = {'binding:host_id': host}
|
||||
self.core_plugin.update_port(context, port_id, {'port': port_data})
|
||||
|
||||
def update_port_bindings(self, context, router_id, host):
|
||||
gw = self.service_plugin.get_vpn_gw_dict_by_router_id(context,
|
||||
router_id)
|
||||
if not gw:
|
||||
return
|
||||
port_id = gw.get('gw_port_id')
|
||||
if port_id:
|
||||
self._update_port_binding(context, port_id, host)
|
||||
port_id = gw.get('transit_port_id')
|
||||
if port_id:
|
||||
self._update_port_binding(context, port_id, host)
|
||||
|
||||
|
||||
class IPsecOvnVpnAgentApi(base_ipsec.IPsecVpnAgentApi):
|
||||
def _agent_notification(self, context, method, router_id,
|
||||
version=None, **kwargs):
|
||||
"""Notify update for the agent.
|
||||
|
||||
This method will find where is the router, and
|
||||
dispatch notification for the agent.
|
||||
"""
|
||||
admin_context = context if context.is_admin else context.elevated()
|
||||
if not version:
|
||||
version = self.target.version
|
||||
|
||||
vpn_agents = self.driver.service_plugin.get_vpn_agents_hosting_routers(
|
||||
admin_context, [router_id], active=True)
|
||||
|
||||
for vpn_agent in vpn_agents:
|
||||
LOG.debug('Notify agent at %(topic)s.%(host)s the message '
|
||||
'%(method)s %(args)s',
|
||||
{'topic': self.topic,
|
||||
'host': vpn_agent['host'],
|
||||
'method': method,
|
||||
'args': kwargs})
|
||||
cctxt = self.client.prepare(server=vpn_agent['host'],
|
||||
version=version)
|
||||
cctxt.cast(context, method, **kwargs)
|
||||
|
||||
|
||||
class IPsecOvnVPNDriver(BaseOvnIPsecVPNDriver):
|
||||
"""VPN Service Driver class for IPsec."""
|
||||
|
||||
def create_rpc_conn(self):
|
||||
self.agent_rpc = IPsecOvnVpnAgentApi(
|
||||
topics.IPSEC_AGENT_TOPIC, BASE_IPSEC_VERSION, self)
|
||||
|
||||
def start_rpc_listeners(self):
|
||||
self.endpoints = [IPsecVpnOvnDriverCallBack(self)]
|
||||
self.conn = n_rpc.Connection()
|
||||
self.conn.create_consumer(
|
||||
topics.IPSEC_DRIVER_TOPIC, self.endpoints, fanout=False)
|
||||
return self.conn.consume_in_threads()
|
491
neutron_vpnaas/tests/functional/common/ovn_base.py
Normal file
491
neutron_vpnaas/tests/functional/common/ovn_base.py
Normal file
@ -0,0 +1,491 @@
|
||||
# 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
|
||||
|
||||
import netaddr
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.common import config as common_config
|
||||
from neutron.common.ovn import constants as ovn_const
|
||||
from neutron.conf.agent import common as agent_conf
|
||||
from neutron.conf import common as common_conf
|
||||
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
|
||||
from neutron.conf.plugins.ml2.drivers import ovs_conf
|
||||
from neutron.tests.common import net_helpers
|
||||
from neutron.tests.functional import base
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
from neutron_lib.utils import helpers
|
||||
from oslo_config import cfg
|
||||
from ovsdbapp.backend.ovs_idl import event
|
||||
|
||||
from neutron_vpnaas.agent.ovn.vpn import agent
|
||||
from neutron_vpnaas.agent.ovn.vpn import ovsdb
|
||||
from neutron_vpnaas.services.vpn.common import constants as vpn_const
|
||||
from neutron_vpnaas.services.vpn.device_drivers import ipsec
|
||||
from neutron_vpnaas.services.vpn import ovn_agent
|
||||
from neutron_vpnaas.services.vpn.service_drivers import ovn_ipsec
|
||||
|
||||
|
||||
OVS_INTERFACE_DRIVER = 'neutron.agent.linux.interface.OVSInterfaceDriver'
|
||||
IPSEC_SERVICE_PROVIDER = ('VPN:ovn:neutron_vpnaas.services.vpn.'
|
||||
'service_drivers.ovn_ipsec.IPsecOvnVPNDriver:'
|
||||
'default')
|
||||
VPN_PLUGIN = 'neutron_vpnaas.services.vpn.ovn_plugin.VPNOVNDriverPlugin'
|
||||
|
||||
PUBLIC_NET = netaddr.IPNetwork('19.4.4.0/24')
|
||||
LOCAL_NETS = list(netaddr.IPNetwork('10.0.0.0/16').subnet(24))
|
||||
PEER_NET = netaddr.IPNetwork('10.1.0.0/16')
|
||||
PEER_ADDR = '19.4.5.6'
|
||||
|
||||
|
||||
class VPNAgentHealthEvent(event.WaitEvent):
|
||||
event_name = 'VPNAgentHealthEvent'
|
||||
|
||||
def __init__(self, chassis, sb_cfg, table, timeout=5):
|
||||
self.chassis = chassis
|
||||
self.sb_cfg = sb_cfg
|
||||
super().__init__(
|
||||
(self.ROW_UPDATE,), table, (('name', '=', self.chassis),),
|
||||
timeout=timeout)
|
||||
|
||||
def matches(self, event, row, old=None):
|
||||
if not super().matches(event, row, old):
|
||||
return False
|
||||
return int(row.external_ids.get(
|
||||
vpn_const.OVN_AGENT_VPN_SB_CFG_KEY, 0)) >= self.sb_cfg
|
||||
|
||||
|
||||
class OvnSiteInfo:
|
||||
def __init__(self, parent, index, ext_net, ext_sub):
|
||||
self.ext_net = ext_net
|
||||
self.ext_sub = ext_sub
|
||||
self.parent = parent
|
||||
self.context = parent.context
|
||||
self.fmt = parent.fmt
|
||||
self.index = index
|
||||
|
||||
def create_base(self):
|
||||
router_data = {
|
||||
'name': 'r%d' % self.index,
|
||||
'admin_state_up': True,
|
||||
'tenant_id': self.parent._tenant_id,
|
||||
'external_gateway_info': {
|
||||
'enable_snat': True,
|
||||
'network_id': self.ext_net['id'],
|
||||
'external_fixed_ips': [
|
||||
{'ip_address': str(PUBLIC_NET[4 + 2 * self.index]),
|
||||
'subnet_id': self.ext_sub['id']}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.router = self.parent.l3_plugin.create_router(
|
||||
self.context, {'router': router_data})
|
||||
|
||||
# local subnet
|
||||
private_net = LOCAL_NETS[self.index]
|
||||
self.local_cidr = str(private_net)
|
||||
|
||||
net = self.parent._make_network(self.fmt, 'local%d' % self.index, True)
|
||||
self.local_net = net['network']
|
||||
sub = self.parent._make_subnet(self.fmt, net, private_net[1],
|
||||
self.local_cidr, enable_dhcp=False)
|
||||
self.local_sub = sub['subnet']
|
||||
interface_info = {'subnet_id': self.local_sub['id']}
|
||||
self.parent.l3_plugin.add_router_interface(
|
||||
self.context, self.router['id'], interface_info)
|
||||
|
||||
def create_vpnservice(self):
|
||||
plugin = self.parent.vpn_plugin
|
||||
data = {
|
||||
'tenant_id': self.parent._tenant_id,
|
||||
'name': 'my-service',
|
||||
'description': 'new service',
|
||||
'subnet_id': self.local_sub['id'],
|
||||
'router_id': self.router['id'],
|
||||
'flavor_id': None,
|
||||
'admin_state_up': True,
|
||||
}
|
||||
self.vpnservice = plugin.create_vpnservice(self.context,
|
||||
{'vpnservice': data})
|
||||
self.local_addr = self.vpnservice['external_v4_ip']
|
||||
|
||||
data = {
|
||||
'tenant_id': self.parent._tenant_id,
|
||||
'name': 'ikepolicy%d' % self.index,
|
||||
'description': '',
|
||||
'auth_algorithm': 'sha1',
|
||||
'encryption_algorithm': 'aes-128',
|
||||
'phase1_negotiation_mode': 'main',
|
||||
'ike_version': 'v1',
|
||||
'pfs': 'group5',
|
||||
'lifetime': {'units': 'seconds', 'value': 3600},
|
||||
}
|
||||
self.ikepolicy = plugin.create_ikepolicy(self.context,
|
||||
{'ikepolicy': data})
|
||||
|
||||
data = {
|
||||
'tenant_id': self.parent._tenant_id,
|
||||
'name': 'ipsecpolicy%d' % self.index,
|
||||
'description': '',
|
||||
'transform_protocol': 'esp',
|
||||
'auth_algorithm': 'sha1',
|
||||
'encryption_algorithm': 'aes-128',
|
||||
'encapsulation_mode': 'tunnel',
|
||||
'pfs': 'group5',
|
||||
'lifetime': {'units': 'seconds', 'value': 3600},
|
||||
}
|
||||
self.ipsecpolicy = plugin.create_ipsecpolicy(self.context,
|
||||
{'ipsecpolicy': data})
|
||||
|
||||
def create_site_connection(self, peer_addr, peer_cidr):
|
||||
data = {
|
||||
'tenant_id': self.parent._tenant_id,
|
||||
'name': 'conn%d' % self.index,
|
||||
'description': '',
|
||||
'local_id': self.local_addr,
|
||||
'peer_address': peer_addr,
|
||||
'peer_id': peer_addr,
|
||||
'peer_cidrs': [peer_cidr],
|
||||
'mtu': 1500,
|
||||
'initiator': 'bi-directional',
|
||||
'auth_mode': 'psk',
|
||||
'psk': 'secret',
|
||||
'dpd': {
|
||||
'action': 'hold',
|
||||
'interval': 30,
|
||||
'timeout': 120,
|
||||
},
|
||||
'admin_state_up': True,
|
||||
'vpnservice_id': self.vpnservice['id'],
|
||||
'ikepolicy_id': self.ikepolicy['id'],
|
||||
'ipsecpolicy_id': self.ipsecpolicy['id'],
|
||||
'local_ep_group_id': None,
|
||||
'peer_ep_group_id': None,
|
||||
}
|
||||
self.siteconn = self.parent.vpn_plugin.create_ipsec_site_connection(
|
||||
self.context, {'ipsec_site_connection': data})
|
||||
|
||||
|
||||
class TestOvnVPNAgentBase(base.TestOVNFunctionalBase):
|
||||
FAKE_CHASSIS_HOST = 'ovn-host-fake'
|
||||
|
||||
def setUp(self):
|
||||
cfg.CONF.set_override('service_provider', [IPSEC_SERVICE_PROVIDER],
|
||||
group='service_providers')
|
||||
service_plugins = {'vpnaas_plugin': VPN_PLUGIN}
|
||||
super().setUp(service_plugins=service_plugins)
|
||||
common_config.register_common_config_options()
|
||||
|
||||
self.mock_ovsdb_idl = mock.Mock()
|
||||
mock_instance = mock.Mock()
|
||||
mock_instance.start.return_value = self.mock_ovsdb_idl
|
||||
mock_ovs_idl = mock.patch.object(ovsdb, 'VPNAgentOvsIdl').start()
|
||||
mock_ovs_idl.return_value = mock_instance
|
||||
|
||||
self.vpn_plugin = directory.get_plugin(plugin_constants.VPN)
|
||||
# normally called in post_for_initialize
|
||||
self.vpn_plugin.watch_agent_events()
|
||||
self.vpn_service_driver = self.vpn_plugin.drivers['ovn']
|
||||
|
||||
self.handler = self.sb_api.idl.notify_handler
|
||||
self.agent = self._start_vpn_agent()
|
||||
self.agent_driver = self.agent.device_drivers[0]
|
||||
|
||||
def _start_vpn_agent(self):
|
||||
# Set up a ConfigOpts separate to cfg.CONF in order to avoid conflicts
|
||||
# with other tests.
|
||||
# The OVN VPN agent registers a different variant of
|
||||
# vpnagent.vpn_device_drivers than the L3 agent extension.
|
||||
conf = agent_conf.setup_conf()
|
||||
conf.register_opts(ovn_conf.ovn_opts, group='ovn')
|
||||
conf.register_opts(ipsec.ipsec_opts, 'ipsec')
|
||||
common_conf.register_core_common_config_opts(conf)
|
||||
ovs_conf.register_ovs_opts(conf)
|
||||
ovn_agent.register_opts(conf)
|
||||
agent_conf.register_process_monitor_opts(conf)
|
||||
agent_conf.setup_privsep()
|
||||
|
||||
conf.set_override('state_path', self.get_default_temp_dir().path)
|
||||
conf.set_override('interface_driver', OVS_INTERFACE_DRIVER)
|
||||
conf.set_override('vpn_device_driver', [self.VPN_DEVICE_DRIVER],
|
||||
group='vpnagent')
|
||||
|
||||
ovn_sb_db = self.ovsdb_server_mgr.get_ovsdb_connection_path('sb')
|
||||
conf.set_override('ovn_sb_connection', ovn_sb_db, group='ovn')
|
||||
|
||||
self.chassis_name = self.add_fake_chassis(self.FAKE_CHASSIS_HOST)
|
||||
mock.patch.object(agent.OvnVpnAgent,
|
||||
'_get_own_chassis_name',
|
||||
return_value=self.chassis_name).start()
|
||||
conf.set_override('host', self.FAKE_CHASSIS_HOST)
|
||||
|
||||
self.br_int = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
|
||||
conf.set_override('integration_bridge', self.br_int.br_name, 'OVS')
|
||||
|
||||
# name prefix for namespaces managed by vpn agent
|
||||
# will be patched into device driver to make sure concurrent
|
||||
# tests don't interfere with each other
|
||||
# (a vpn agent will normally remove all unknown qvpn- namespaces)
|
||||
self.ns_prefix = 'qvpn-test-%s-' % helpers.get_random_string(8)
|
||||
|
||||
agt = agent.OvnVpnAgent(conf)
|
||||
driver = agt.device_drivers[0]
|
||||
driver.agent_rpc = mock.Mock()
|
||||
# let initial sync get an empty list of vpnservices
|
||||
driver.agent_rpc.get_vpn_services_on_host.return_value = []
|
||||
driver.devmgr.plugin = driver.agent_rpc
|
||||
driver.devmgr.OVN_NS_PREFIX = self.ns_prefix
|
||||
|
||||
agt.start()
|
||||
self.addCleanup(agt.ovs_idl.ovsdb_connection.stop)
|
||||
self.addCleanup(agt.sb_idl.ovsdb_connection.stop)
|
||||
# let agent remove remaining vpn namespaces in cleanup
|
||||
self.addCleanup(driver._cleanup_stale_vpn_processes, [])
|
||||
|
||||
return agt
|
||||
|
||||
@property
|
||||
def agent_chassis_table(self):
|
||||
if self.agent.has_chassis_private:
|
||||
return 'Chassis_Private'
|
||||
return 'Chassis'
|
||||
|
||||
def _make_ext_network(self):
|
||||
network = self._make_network(
|
||||
self.fmt, 'external-net', True, as_admin=True,
|
||||
arg_list=('router:external',
|
||||
'provider:network_type',
|
||||
'provider:physical_network'),
|
||||
**{'router:external': True,
|
||||
'provider:network_type': 'flat',
|
||||
'provider:physical_network': 'public'})
|
||||
|
||||
pools = [{'start': PUBLIC_NET[2], 'end': PUBLIC_NET[253]}]
|
||||
gateway = PUBLIC_NET[1]
|
||||
cidr = str(PUBLIC_NET)
|
||||
subnet = self._make_subnet(self.fmt, network, gateway, cidr,
|
||||
allocation_pools=pools,
|
||||
enable_dhcp=False)
|
||||
return network['network'], subnet['subnet']
|
||||
|
||||
def _find_lswitch_by_neutron_name(self, name):
|
||||
for row in self.nb_api._tables['Logical_Switch'].rows.values():
|
||||
if (row.external_ids.get(
|
||||
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY) == name):
|
||||
return row
|
||||
|
||||
def _find_transit_lswitch(self, router_id):
|
||||
name = ovn_ipsec.TRANSIT_NETWORK_PREFIX + router_id
|
||||
return self._find_lswitch_by_neutron_name(name)
|
||||
|
||||
def _match_extids(self, row, expected):
|
||||
for key, value in expected.items():
|
||||
if row.external_ids.get(key) != value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _find_transit_ns_port(self, router_id, ports):
|
||||
name = ovn_ipsec.TRANSIT_PORT_PREFIX + router_id
|
||||
extids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: name}
|
||||
|
||||
for row in ports:
|
||||
if self._match_extids(row, extids):
|
||||
return row
|
||||
|
||||
def _find_transit_router_port(self, router_id, network_name, ports):
|
||||
extids = {
|
||||
ovn_const.OVN_DEVID_EXT_ID_KEY: router_id,
|
||||
ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface',
|
||||
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network_name,
|
||||
}
|
||||
for row in ports:
|
||||
if self._match_extids(row, extids):
|
||||
return row
|
||||
|
||||
def _find_vpn_gw_port(self, router_id, ports):
|
||||
name = ovn_ipsec.VPN_GW_PORT_PREFIX + router_id
|
||||
extids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: name}
|
||||
|
||||
for row in ports:
|
||||
if self._match_extids(row, extids):
|
||||
return row
|
||||
|
||||
def _find_lrouter_by_neutron_id(self, router_id):
|
||||
for row in self.nb_api._tables['Logical_Router'].rows.values():
|
||||
if row.name == "neutron-" + router_id:
|
||||
return row
|
||||
|
||||
def test_agent(self):
|
||||
chassis_row = self.sb_api.db_find(
|
||||
self.agent_chassis_table,
|
||||
('name', '=', self.chassis_name)).execute(
|
||||
check_error=True)[0]
|
||||
|
||||
# Assert that, prior to creating a resource the VPN agent
|
||||
# didn't populate the external_ids from the Chassis
|
||||
self.assertNotIn(vpn_const.OVN_AGENT_VPN_SB_CFG_KEY,
|
||||
chassis_row['external_ids'])
|
||||
|
||||
# Let's list the agents to force the nb_cfg to be bumped on NB
|
||||
# db, which will automatically increment the nb_cfg counter on
|
||||
# NB_Global and make ovn-controller copy it over to SB_Global. Upon
|
||||
# this event, VPN agent will update the external_ids on its
|
||||
# Chassis row to signal that it's healthy.
|
||||
|
||||
row_event = VPNAgentHealthEvent(self.chassis_name, 1,
|
||||
self.agent_chassis_table)
|
||||
self.handler.watch_event(row_event)
|
||||
self.new_list_request('agents').get_response(self.api)
|
||||
|
||||
# If we do not time out waiting for the event, then we are assured
|
||||
# that the VPN agent has populated the external_ids from the
|
||||
# chassis with the nb_cfg, 1 revisions when listing the agents.
|
||||
self.assertTrue(row_event.wait())
|
||||
|
||||
def test_service(self):
|
||||
r = self.new_list_request('agents').get_response(self.api)
|
||||
ext_net, ext_sub = self._make_ext_network()
|
||||
|
||||
server = ovn_ipsec.IPsecVpnOvnDriverCallBack(self.vpn_service_driver)
|
||||
|
||||
# Mock the controller side RPC client (prepare and cast)
|
||||
# to be able to check that "vpnservice_updated" will be called
|
||||
prepare_mock = mock.Mock()
|
||||
prepared_mock = mock.Mock()
|
||||
self.vpn_service_driver.agent_rpc.client.prepare = prepare_mock
|
||||
prepare_mock.return_value = prepared_mock
|
||||
|
||||
# Create a site (router, network, subnet, vpnservice, site conn)
|
||||
site = OvnSiteInfo(self, 1, ext_net, ext_sub)
|
||||
site.create_base()
|
||||
site.create_vpnservice()
|
||||
site.create_site_connection(PEER_ADDR, str(PEER_NET))
|
||||
|
||||
# Check that the vpnservice_updated RPC was triggered towards
|
||||
# the agent
|
||||
prepare_mock.assert_called_once_with(
|
||||
server=self.FAKE_CHASSIS_HOST,
|
||||
version=self.vpn_service_driver.agent_rpc.target.version)
|
||||
prepared_mock.cast.assert_called_once_with(
|
||||
self.context, 'vpnservice_updated',
|
||||
router={'id': site.router['id']})
|
||||
|
||||
# Mock the agent->controller RPCs. Let them return data from the
|
||||
# actual VPN plugin
|
||||
def get_vpn_services_on_host(ctx, host):
|
||||
r = server.get_vpn_services_on_host(self.context, host)
|
||||
return r
|
||||
|
||||
def get_vpn_transit_network_details(router_id):
|
||||
return server.get_vpn_transit_network_details(
|
||||
self.context, router_id)
|
||||
|
||||
def get_subnet_info(subnet_id):
|
||||
return server.get_subnet_info(self.context, subnet_id)
|
||||
|
||||
r = self.agent_driver.agent_rpc
|
||||
r.get_vpn_services_on_host.side_effect = get_vpn_services_on_host
|
||||
r.get_vpn_transit_network_details.side_effect = \
|
||||
get_vpn_transit_network_details
|
||||
r.get_subnet_info.side_effect = get_subnet_info
|
||||
|
||||
# Call the agent's vpnservice_updated as if it was coming from
|
||||
# the controller.
|
||||
for driver in self.agent.device_drivers:
|
||||
driver.vpnservice_updated(driver.context,
|
||||
router={'id': site.router['id']})
|
||||
|
||||
# Check that transit network and VPN gateway port are set up correctly
|
||||
# - transit network exists
|
||||
# - router port in transit network exists
|
||||
# - transit network port to be bound to chassis exists and
|
||||
# host is assigned
|
||||
# - VPN gateway port exists and host is assigned
|
||||
# - static route exists towards peer CIDR
|
||||
|
||||
# expect transit network in NB
|
||||
transit_row = self._find_transit_lswitch(site.router['id'])
|
||||
self.assertIsNotNone(transit_row)
|
||||
|
||||
# check the transit network router port exists
|
||||
transit_router_port = self._find_transit_router_port(
|
||||
site.router['id'], transit_row.name, transit_row.ports)
|
||||
self.assertIsNotNone(transit_router_port)
|
||||
|
||||
# check that the namespace port in the transit network exists
|
||||
transit_ns_port = self._find_transit_ns_port(site.router['id'],
|
||||
transit_row.ports)
|
||||
self.assertIsNotNone(transit_ns_port)
|
||||
|
||||
# check that the port has the requested-host option
|
||||
requested_host = transit_ns_port.options.get(
|
||||
ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY)
|
||||
self.assertEqual(requested_host, self.FAKE_CHASSIS_HOST)
|
||||
|
||||
# get vpn gateway port via external network lswitch
|
||||
ext_row = self._find_lswitch_by_neutron_name("external-net")
|
||||
self.assertIsNotNone(ext_row)
|
||||
|
||||
vpn_gw_port = self._find_vpn_gw_port(site.router['id'], ext_row.ports)
|
||||
self.assertIsNotNone(vpn_gw_port)
|
||||
# check that vpn gateway port has the requested-host option
|
||||
requested_host = vpn_gw_port.options.get(
|
||||
ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY)
|
||||
self.assertEqual(requested_host, self.FAKE_CHASSIS_HOST)
|
||||
|
||||
# check that static route towards peer cidr is set
|
||||
router_row = self._find_lrouter_by_neutron_id(site.router['id'])
|
||||
self.assertIsNotNone(router_row)
|
||||
for r in router_row.static_routes:
|
||||
if r.ip_prefix == str(PEER_NET):
|
||||
route = r
|
||||
break
|
||||
else:
|
||||
route = None
|
||||
|
||||
self.assertIsNotNone(route)
|
||||
self.assertEqual(route.nexthop, ovn_ipsec.VPN_TRANSIT_RIP)
|
||||
|
||||
# Check agent side
|
||||
# - network namespace
|
||||
# - routes towards transit network's gateway IP
|
||||
# - devices and their IP addresses in the namespace
|
||||
ns_name = self.ns_prefix + site.router['id']
|
||||
devlen = lib_constants.LINUX_DEV_LEN
|
||||
transit_dev = ('vr' + transit_ns_port.name)[:devlen]
|
||||
gw_dev = ('vg' + vpn_gw_port.name)[:devlen]
|
||||
self.assertTrue(ip_lib.network_namespace_exists(ns_name))
|
||||
device = ip_lib.IPDevice(None, namespace=ns_name)
|
||||
routes = device.route.list_routes(lib_constants.IP_VERSION_4,
|
||||
proto='static',
|
||||
via=ovn_ipsec.VPN_TRANSIT_LIP)
|
||||
self.assertEqual(len(routes), 1)
|
||||
self.assertEqual(routes[0]['via'], ovn_ipsec.VPN_TRANSIT_LIP)
|
||||
self.assertEqual(routes[0]['cidr'], site.local_cidr)
|
||||
self.assertEqual(routes[0]['device'], transit_dev)
|
||||
|
||||
# check addresses in namespace
|
||||
addrs = device.addr.list(ip_version=lib_constants.IP_VERSION_4)
|
||||
addrs_dict = {a['name']: a for a in addrs}
|
||||
self.assertIn(transit_dev, addrs_dict)
|
||||
self.assertEqual(
|
||||
addrs_dict[transit_dev]['cidr'],
|
||||
transit_ns_port.external_ids[ovn_const.OVN_CIDRS_EXT_ID_KEY])
|
||||
|
||||
self.assertIn(gw_dev, addrs_dict)
|
||||
self.assertEqual(
|
||||
addrs_dict[gw_dev]['cidr'],
|
||||
vpn_gw_port.external_ids[ovn_const.OVN_CIDRS_EXT_ID_KEY])
|
@ -0,0 +1,20 @@
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron_vpnaas.tests.functional.common import test_ovn
|
||||
|
||||
|
||||
class TestOvnOpenSwan(test_ovn.TestOvnVPNAgentBase):
|
||||
VPN_DEVICE_DRIVER = ('neutron_vpnaas.services.vpn.device_drivers.'
|
||||
'ovn_ipsec.OvnOpenSwanDriver')
|
@ -0,0 +1,20 @@
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron_vpnaas.tests.functional.common import ovn_base
|
||||
|
||||
|
||||
class TestOvnStrongSwan(ovn_base.TestOvnVPNAgentBase):
|
||||
VPN_DEVICE_DRIVER = ('neutron_vpnaas.services.vpn.device_drivers.'
|
||||
'ovn_ipsec.OvnStrongSwanDriver')
|
525
neutron_vpnaas/tests/unit/db/vpn/test_vpn_agentschedulers_db.py
Normal file
525
neutron_vpnaas/tests/unit/db/vpn/test_vpn_agentschedulers_db.py
Normal file
@ -0,0 +1,525 @@
|
||||
# Copyright 2023 SysEleven GmbH.
|
||||
#
|
||||
# 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 neutron.api import extensions
|
||||
from neutron.common.ovn import constants as ovn_constants
|
||||
from neutron import policy
|
||||
from neutron.tests.common import helpers
|
||||
from neutron.tests.unit.api import test_extensions
|
||||
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin
|
||||
from neutron.tests.unit.extensions import test_l3
|
||||
from neutron.tests.unit import testlib_api
|
||||
from neutron import wsgi
|
||||
from neutron_lib import context
|
||||
from neutron_lib import exceptions as n_exc
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
from neutron_lib import rpc as n_rpc
|
||||
from oslo_db import exception as db_exc
|
||||
import oslo_messaging
|
||||
from oslo_utils import uuidutils
|
||||
from sqlalchemy import orm
|
||||
from webob import exc
|
||||
|
||||
from neutron_vpnaas.api.rpc.agentnotifiers import vpn_rpc_agent_api
|
||||
from neutron_vpnaas.extensions import vpn_agentschedulers
|
||||
from neutron_vpnaas.services.vpn.common import constants
|
||||
from neutron_vpnaas.tests.unit.db.vpn import test_vpn_db
|
||||
|
||||
|
||||
VPN_HOSTA = "host-1"
|
||||
VPN_HOSTB = "host-2"
|
||||
|
||||
|
||||
class VPNAgentSchedulerTestMixIn(object):
|
||||
def _request_list(self, path, admin_context=True,
|
||||
expected_code=exc.HTTPOk.code):
|
||||
req = self._path_req(path, admin_context=admin_context)
|
||||
res = req.get_response(self.ext_api)
|
||||
self.assertEqual(expected_code, res.status_int)
|
||||
return self.deserialize(self.fmt, res)
|
||||
|
||||
def _path_req(self, path, method='GET', data=None,
|
||||
query_string=None,
|
||||
admin_context=True):
|
||||
content_type = 'application/%s' % self.fmt
|
||||
body = None
|
||||
if data is not None: # empty dict is valid
|
||||
body = wsgi.Serializer().serialize(data, content_type)
|
||||
if admin_context:
|
||||
return testlib_api.create_request(
|
||||
path, body, content_type, method, query_string=query_string)
|
||||
else:
|
||||
return testlib_api.create_request(
|
||||
path, body, content_type, method, query_string=query_string,
|
||||
context=context.Context('', 'tenant_id'))
|
||||
|
||||
def _path_create_request(self, path, data, admin_context=True):
|
||||
return self._path_req(path, method='POST', data=data,
|
||||
admin_context=admin_context)
|
||||
|
||||
def _path_show_request(self, path, admin_context=True):
|
||||
return self._path_req(path, admin_context=admin_context)
|
||||
|
||||
def _path_delete_request(self, path, admin_context=True):
|
||||
return self._path_req(path, method='DELETE',
|
||||
admin_context=admin_context)
|
||||
|
||||
def _path_update_request(self, path, data, admin_context=True):
|
||||
return self._path_req(path, method='PUT', data=data,
|
||||
admin_context=admin_context)
|
||||
|
||||
def _list_routers_hosted_by_vpn_agent(self, agent_id,
|
||||
expected_code=exc.HTTPOk.code,
|
||||
admin_context=True):
|
||||
path = "/agents/%s/%s.%s" % (agent_id,
|
||||
vpn_agentschedulers.VPN_ROUTERS,
|
||||
self.fmt)
|
||||
return self._request_list(path, expected_code=expected_code,
|
||||
admin_context=admin_context)
|
||||
|
||||
def _add_router_to_vpn_agent(self, id, router_id,
|
||||
expected_code=exc.HTTPCreated.code,
|
||||
admin_context=True):
|
||||
path = "/agents/%s/%s.%s" % (id,
|
||||
vpn_agentschedulers.VPN_ROUTERS,
|
||||
self.fmt)
|
||||
req = self._path_create_request(path,
|
||||
{'router_id': router_id},
|
||||
admin_context=admin_context)
|
||||
res = req.get_response(self.ext_api)
|
||||
self.assertEqual(expected_code, res.status_int)
|
||||
|
||||
def _list_vpn_agents_hosting_router(self, router_id,
|
||||
expected_code=exc.HTTPOk.code,
|
||||
admin_context=True):
|
||||
path = "/routers/%s/%s.%s" % (router_id,
|
||||
vpn_agentschedulers.VPN_AGENTS,
|
||||
self.fmt)
|
||||
return self._request_list(path, expected_code=expected_code,
|
||||
admin_context=admin_context)
|
||||
|
||||
def _remove_router_from_vpn_agent(self, id, router_id,
|
||||
expected_code=exc.HTTPNoContent.code,
|
||||
admin_context=True):
|
||||
path = "/agents/%s/%s/%s.%s" % (id,
|
||||
vpn_agentschedulers.VPN_ROUTERS,
|
||||
router_id,
|
||||
self.fmt)
|
||||
req = self._path_delete_request(path, admin_context=admin_context)
|
||||
res = req.get_response(self.ext_api)
|
||||
self.assertEqual(expected_code, res.status_int)
|
||||
|
||||
|
||||
class VPNAgentSchedulerTestCaseBase(test_vpn_db.VPNTestMixin,
|
||||
test_l3.L3NatTestCaseMixin,
|
||||
VPNAgentSchedulerTestMixIn,
|
||||
test_plugin.NeutronDbPluginV2TestCase):
|
||||
fmt = 'json'
|
||||
|
||||
def setUp(self):
|
||||
# NOTE(ivasilevskaya) mocking this way allows some control over mocked
|
||||
# client like further method mocking with asserting calls
|
||||
self.client_mock = mock.MagicMock(name="mocked client")
|
||||
mock.patch.object(
|
||||
n_rpc, 'get_client').start().return_value = self.client_mock
|
||||
|
||||
service_plugins = {
|
||||
'vpnaas_plugin': 'neutron_vpnaas.services.vpn.ovn_plugin.'
|
||||
'VPNOVNPlugin'}
|
||||
plugin_str = 'neutron.tests.unit.extensions.test_l3.TestL3NatIntPlugin'
|
||||
super().setUp(plugin_str, service_plugins=service_plugins)
|
||||
|
||||
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
|
||||
self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr)
|
||||
self.adminContext = context.get_admin_context()
|
||||
|
||||
self.core_plugin = directory.get_plugin()
|
||||
self.core_plugin.get_agents = \
|
||||
mock.MagicMock(side_effect=self._get_agents)
|
||||
self.core_plugin.get_agent = \
|
||||
mock.MagicMock(side_effect=self._get_agent)
|
||||
self._agents = {}
|
||||
self._vpn_agents_by_host = {}
|
||||
|
||||
self.service_plugin = directory.get_plugin(plugin_constants.VPN)
|
||||
policy.init()
|
||||
|
||||
def _get_agents(self, context, filters=None):
|
||||
if not filters:
|
||||
return self._agents.values()
|
||||
|
||||
agents = []
|
||||
for agent in self._agents.values():
|
||||
for key, values in filters.items():
|
||||
if agent[key] not in values:
|
||||
break
|
||||
else:
|
||||
agents.append(agent)
|
||||
return agents
|
||||
|
||||
def _get_agent(self, context, agent_id):
|
||||
try:
|
||||
return self._agents[agent_id]
|
||||
except KeyError:
|
||||
raise n_exc.agent.AgentNotFound(id=agent_id)
|
||||
|
||||
def _get_any_metadata_agent_id(self):
|
||||
for agent in self._agents.values():
|
||||
if agent['agent_type'] == ovn_constants.OVN_METADATA_AGENT:
|
||||
return agent['id']
|
||||
|
||||
def _take_down_vpn_agent(self, host):
|
||||
self._vpn_agents_by_host[host]['alive'] = False
|
||||
|
||||
def _get_another_agent_host(self, host):
|
||||
for agent in self._vpn_agents_by_host.values():
|
||||
if agent['host'] != host:
|
||||
return agent['host']
|
||||
|
||||
def _register_agent_states(self):
|
||||
self._register_vpn_agent(host=VPN_HOSTA)
|
||||
self._register_vpn_agent(host=VPN_HOSTB)
|
||||
self._register_metadata_agent(host=VPN_HOSTA)
|
||||
self._register_metadata_agent(host=VPN_HOSTB)
|
||||
|
||||
def _register_vpn_agent(self, host=None):
|
||||
agent = {
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'binary': "neutron-ovn-vpn-agent",
|
||||
'host': host,
|
||||
'availability_zone': helpers.DEFAULT_AZ,
|
||||
'topic': 'n/a',
|
||||
'configurations': {},
|
||||
'start_flag': True,
|
||||
'agent_type': constants.AGENT_TYPE_VPN,
|
||||
'alive': True,
|
||||
'admin_state_up': True}
|
||||
self._agents[agent['id']] = agent
|
||||
self._vpn_agents_by_host[host] = agent
|
||||
|
||||
def _register_metadata_agent(self, host=None):
|
||||
agent = {
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'binary': "neutron-ovn-metadata-agent",
|
||||
'host': host,
|
||||
'availability_zone': helpers.DEFAULT_AZ,
|
||||
'topic': 'n/a',
|
||||
'configurations': {},
|
||||
'start_flag': True,
|
||||
'agent_type': ovn_constants.OVN_METADATA_AGENT,
|
||||
'alive': True,
|
||||
'admin_state_up': True}
|
||||
self._agents[agent['id']] = agent
|
||||
|
||||
|
||||
class VPNAgentSchedulerTestCase(VPNAgentSchedulerTestCaseBase):
|
||||
def _take_down_agent_and_run_reschedule(self, host):
|
||||
self._take_down_vpn_agent(host)
|
||||
plugin = directory.get_plugin(plugin_constants.VPN)
|
||||
plugin.reschedule_vpnservices_from_down_agents()
|
||||
|
||||
def _get_agent_host_by_router(self, router_id):
|
||||
agents = self._list_vpn_agents_hosting_router(router_id)
|
||||
return agents['agents'][0]['host']
|
||||
|
||||
def test_schedule_router(self):
|
||||
self._register_agent_states()
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
self.service_plugin.schedule_router(self.adminContext, router_id)
|
||||
host = self._get_agent_host_by_router(router_id)
|
||||
|
||||
self.assertIn(host, (VPN_HOSTA, VPN_HOSTB))
|
||||
|
||||
def test_router_rescheduler_catches_rpc_db_and_reschedule_exceptions(self):
|
||||
self._register_agent_states()
|
||||
agent_a_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
mock.patch.object(
|
||||
self.service_plugin, 'reschedule_router',
|
||||
side_effect=[
|
||||
db_exc.DBError(), oslo_messaging.RemoteError(),
|
||||
vpn_agentschedulers.RouterReschedulingFailed(
|
||||
router_id='f'),
|
||||
ValueError('this raises'),
|
||||
Exception()
|
||||
]).start()
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA) # DBError
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA) # RemoteError
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA) # schedule err
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA) # Value error
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA) # Exception
|
||||
|
||||
def test_router_rescheduler_catches_exceptions_on_fetching_bindings(self):
|
||||
with mock.patch('neutron_lib.context.get_admin_context') as get_ctx:
|
||||
mock_ctx = mock.Mock()
|
||||
get_ctx.return_value = mock_ctx
|
||||
mock_ctx.session.query.side_effect = db_exc.DBError()
|
||||
|
||||
# check that no exception is raised
|
||||
self.service_plugin.reschedule_vpnservices_from_down_agents()
|
||||
|
||||
def test_router_rescheduler_iterates_after_reschedule_failure(self):
|
||||
self._register_agent_states()
|
||||
agent_a = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTA)
|
||||
|
||||
with self.vpnservice() as s1, self.vpnservice() as s2:
|
||||
# schedule the services to agent A
|
||||
self.service_plugin.auto_schedule_routers(
|
||||
self.adminContext, agent_a)
|
||||
|
||||
rs_mock = mock.patch.object(
|
||||
self.service_plugin, 'reschedule_router',
|
||||
side_effect=vpn_agentschedulers.RouterReschedulingFailed(
|
||||
router_id='f'),
|
||||
).start()
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA)
|
||||
# make sure both had a reschedule attempt even though first failed
|
||||
router_id_1 = s1['vpnservice']['router_id']
|
||||
router_id_2 = s2['vpnservice']['router_id']
|
||||
rs_mock.assert_has_calls(
|
||||
[mock.call(mock.ANY, router_id_1, agent_a),
|
||||
mock.call(mock.ANY, router_id_2, agent_a)],
|
||||
any_order=True)
|
||||
|
||||
def test_router_is_not_rescheduled_from_alive_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_a_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
|
||||
patch_func_str = ('neutron_vpnaas.db.vpn.vpn_agentschedulers_db.'
|
||||
'VPNAgentSchedulerDbMixin.reschedule_router')
|
||||
with mock.patch(patch_func_str) as rr:
|
||||
# take down the unrelated agent and run reschedule check
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTB)
|
||||
self.assertFalse(rr.called)
|
||||
|
||||
def test_router_reschedule_from_dead_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_a_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
host_before = self._get_agent_host_by_router(router_id)
|
||||
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA)
|
||||
host_after = self._get_agent_host_by_router(router_id)
|
||||
|
||||
self.assertEqual(VPN_HOSTA, host_before)
|
||||
self.assertEqual(VPN_HOSTB, host_after)
|
||||
|
||||
def test_router_reschedule_succeeded_after_failed_notification(self):
|
||||
self._register_agent_states()
|
||||
agent_a = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTA)
|
||||
|
||||
with self.vpnservice() as service:
|
||||
# schedule the vpn routers to agent A
|
||||
self.service_plugin.auto_schedule_routers(
|
||||
self.adminContext, agent_a)
|
||||
ctxt_mock = mock.MagicMock()
|
||||
call_mock = mock.MagicMock(
|
||||
side_effect=[oslo_messaging.MessagingTimeout, None])
|
||||
ctxt_mock.call = call_mock
|
||||
self.client_mock.prepare = mock.MagicMock(return_value=ctxt_mock)
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA)
|
||||
self.assertEqual(2, call_mock.call_count)
|
||||
# make sure vpn service was rescheduled even when first attempt
|
||||
# failed to notify VPN agent
|
||||
router_id = service['vpnservice']['router_id']
|
||||
host = self._get_agent_host_by_router(router_id)
|
||||
|
||||
vpn_agents = self._list_vpn_agents_hosting_router(router_id)
|
||||
self.assertEqual(1, len(vpn_agents['agents']))
|
||||
self.assertEqual(VPN_HOSTB, host)
|
||||
|
||||
def test_router_reschedule_failed_notification_all_attempts(self):
|
||||
self._register_agent_states()
|
||||
agent_a = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTA)
|
||||
|
||||
with self.vpnservice() as vpnservice:
|
||||
# schedule the vpn routers to agent A
|
||||
self.service_plugin.auto_schedule_routers(
|
||||
self.adminContext, agent_a)
|
||||
# mock client.prepare and context.call
|
||||
ctxt_mock = mock.MagicMock()
|
||||
call_mock = mock.MagicMock(
|
||||
side_effect=oslo_messaging.MessagingTimeout)
|
||||
ctxt_mock.call = call_mock
|
||||
self.client_mock.prepare = mock.MagicMock(return_value=ctxt_mock)
|
||||
# perform operations
|
||||
self._take_down_agent_and_run_reschedule(VPN_HOSTA)
|
||||
self.assertEqual(
|
||||
vpn_rpc_agent_api.AGENT_NOTIFY_MAX_ATTEMPTS,
|
||||
call_mock.call_count)
|
||||
router_id = vpnservice['vpnservice']['router_id']
|
||||
vpn_agents = self._list_vpn_agents_hosting_router(router_id)
|
||||
self.assertEqual(0, len(vpn_agents['agents']))
|
||||
|
||||
def test_router_auto_schedule_with_hosted(self):
|
||||
self._register_agent_states()
|
||||
agent_a = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTA)
|
||||
agent_b = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTB)
|
||||
|
||||
with self.vpnservice() as vpnservice:
|
||||
self._register_agent_states()
|
||||
ret_a = self.service_plugin.auto_schedule_routers(
|
||||
self.adminContext, agent_a)
|
||||
ret_b = self.service_plugin.auto_schedule_routers(
|
||||
self.adminContext, agent_b)
|
||||
router_id = vpnservice['vpnservice']['router_id']
|
||||
vpn_agents = self._list_vpn_agents_hosting_router(router_id)
|
||||
host = self._get_agent_host_by_router(router_id)
|
||||
self.assertTrue(len(ret_a))
|
||||
self.assertIn(router_id, ret_a)
|
||||
self.assertFalse(len(ret_b))
|
||||
self.assertEqual(1, len(vpn_agents['agents']))
|
||||
self.assertEqual(VPN_HOSTA, host)
|
||||
|
||||
def test_add_router_to_vpn_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_a = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTA)
|
||||
agent_a_id = agent_a['id']
|
||||
agent_b = self.service_plugin.get_vpn_agent_on_host(
|
||||
self.adminContext, VPN_HOSTB)
|
||||
agent_b_id = agent_b['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
num_before_add = len(
|
||||
self._list_routers_hosted_by_vpn_agent(
|
||||
agent_a_id)['routers'])
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
# add router again to same agent is fine
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
# add router to a second agent is a conflict
|
||||
self._add_router_to_vpn_agent(agent_b_id, router_id,
|
||||
expected_code=exc.HTTPConflict.code)
|
||||
num_after_add = len(
|
||||
self._list_routers_hosted_by_vpn_agent(
|
||||
agent_a_id)['routers'])
|
||||
self.assertEqual(0, num_before_add)
|
||||
self.assertEqual(1, num_after_add)
|
||||
|
||||
def test_add_router_to_vpn_agent_wrong_type(self):
|
||||
self._register_agent_states()
|
||||
agent_id = self._get_any_metadata_agent_id()
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
# add_router_to_vpn_agent with a metadata agent id shall fail
|
||||
self._add_router_to_vpn_agent(
|
||||
agent_id, router_id,
|
||||
expected_code=exc.HTTPNotFound.code)
|
||||
|
||||
def _test_add_router_to_vpn_agent_db_error(self, exception):
|
||||
self._register_agent_states()
|
||||
agent_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router, \
|
||||
mock.patch.object(orm.Session, 'add', side_effect=exception):
|
||||
router_id = router['router']['id']
|
||||
|
||||
self._add_router_to_vpn_agent(
|
||||
agent_id, router_id,
|
||||
expected_code=exc.HTTPConflict.code)
|
||||
|
||||
def test_add_router_to_vpn_agent_duplicate(self):
|
||||
self._test_add_router_to_vpn_agent_db_error(db_exc.DBDuplicateEntry)
|
||||
|
||||
def test_add_router_to_vpn_agent_reference_error(self):
|
||||
self._test_add_router_to_vpn_agent_db_error(
|
||||
db_exc.DBReferenceError('', '', '', ''))
|
||||
|
||||
def test_add_router_to_vpn_agent_db_error(self):
|
||||
self._test_add_router_to_vpn_agent_db_error(db_exc.DBError)
|
||||
|
||||
def test_list_routers_hosted_by_vpn_agent_with_invalid_agent(self):
|
||||
invalid_agentid = 'non_existing_agent'
|
||||
self._list_routers_hosted_by_vpn_agent(invalid_agentid,
|
||||
exc.HTTPNotFound.code)
|
||||
|
||||
def test_remove_router_from_vpn_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
|
||||
self._add_router_to_vpn_agent(agent_id, router_id)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_id)
|
||||
num_before = len(routers['routers'])
|
||||
|
||||
self._remove_router_from_vpn_agent(agent_id, router_id)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_id)
|
||||
num_after = len(routers['routers'])
|
||||
|
||||
self.assertEqual(1, num_before)
|
||||
self.assertEqual(0, num_after)
|
||||
|
||||
def test_remove_router_from_vpn_agent_wrong_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_a_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
agent_b_id = self._vpn_agents_by_host[VPN_HOSTB]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_a_id)
|
||||
num_before = len(routers['routers'])
|
||||
|
||||
# try to remove router from wrong agent is not an error
|
||||
self._remove_router_from_vpn_agent(agent_b_id, router_id)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_a_id)
|
||||
num_after = len(routers['routers'])
|
||||
|
||||
self.assertEqual(1, num_before)
|
||||
self.assertEqual(1, num_after)
|
||||
|
||||
def test_remove_router_from_vpn_agent_unknown_agent(self):
|
||||
self._register_agent_states()
|
||||
agent_a_id = self._vpn_agents_by_host[VPN_HOSTA]['id']
|
||||
|
||||
with self.router() as router:
|
||||
router_id = router['router']['id']
|
||||
|
||||
self._add_router_to_vpn_agent(agent_a_id, router_id)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_a_id)
|
||||
num_before = len(routers['routers'])
|
||||
|
||||
# try to remove router from unknown agent is an error
|
||||
self._remove_router_from_vpn_agent(
|
||||
'unknown-agent', router_id,
|
||||
expected_code=exc.HTTPNotFound.code)
|
||||
routers = self._list_routers_hosted_by_vpn_agent(agent_a_id)
|
||||
num_after = len(routers['routers'])
|
||||
|
||||
self.assertEqual(1, num_before)
|
||||
self.assertEqual(1, num_after)
|
@ -2290,3 +2290,77 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
|
||||
self.context,
|
||||
private_subnet['id'],
|
||||
router['id'])
|
||||
|
||||
def _setup_ipsec_site_connections_with_ep_groups(self, peer_cidr_lists):
|
||||
private_subnet, router = self.create_basic_topology()
|
||||
vpn_service_info = self.prepare_service_info(private_subnet=None,
|
||||
router=router)
|
||||
vpn_service = self.plugin.create_vpnservice(self.context,
|
||||
vpn_service_info)
|
||||
|
||||
ike_policy = self.create_ike_policy()
|
||||
ipsec_policy = self.create_ipsec_policy()
|
||||
ipsec_site_connection = self.prepare_connection_info(
|
||||
vpn_service['id'],
|
||||
ike_policy['id'],
|
||||
ipsec_policy['id'])
|
||||
|
||||
local_ep_group = self.create_endpoint_group(
|
||||
group_type='subnet', endpoints=[private_subnet['id']])
|
||||
for peer_cidrs in peer_cidr_lists:
|
||||
peer_ep_group = self.create_endpoint_group(
|
||||
group_type='cidr', endpoints=peer_cidrs)
|
||||
ipsec_site_connection['ipsec_site_connection'].update(
|
||||
{'local_ep_group_id': local_ep_group['id'],
|
||||
'peer_ep_group_id': peer_ep_group['id']})
|
||||
self.plugin.create_ipsec_site_connection(self.context,
|
||||
ipsec_site_connection)
|
||||
return private_subnet, router
|
||||
|
||||
def _setup_ipsec_site_connections_without_ep_groups(self, peer_cidr_lists):
|
||||
private_subnet, router = self.create_basic_topology()
|
||||
vpn_service_info = \
|
||||
self.prepare_service_info(private_subnet=private_subnet,
|
||||
router=router)
|
||||
vpn_service = self.plugin.create_vpnservice(self.context,
|
||||
vpn_service_info)
|
||||
|
||||
ike_policy = self.create_ike_policy()
|
||||
ipsec_policy = self.create_ipsec_policy()
|
||||
ipsec_site_connection = self.prepare_connection_info(
|
||||
vpn_service['id'],
|
||||
ike_policy['id'],
|
||||
ipsec_policy['id'])
|
||||
|
||||
for peer_cidrs in peer_cidr_lists:
|
||||
ipsec_site_connection['ipsec_site_connection'].update(
|
||||
{'peer_cidrs': peer_cidrs})
|
||||
self.plugin.create_ipsec_site_connection(self.context,
|
||||
ipsec_site_connection)
|
||||
return private_subnet, router
|
||||
|
||||
def _test_get_peer_cidrs_for_router(self, setup_func):
|
||||
mock.patch.object(self.plugin, '_get_validator').start()
|
||||
|
||||
# create 1st setup with two connections
|
||||
peer_cidrs = [
|
||||
['20.1.0.0/24', '20.2.0.0/24'],
|
||||
['20.3.0.0/24']
|
||||
]
|
||||
private_subnet, router = setup_func(peer_cidrs)
|
||||
|
||||
# create a 2nd setup for a different router
|
||||
setup_func([['10.1.0.0/24', '10.2.0.0/24']])
|
||||
|
||||
returned_cidrs = self.plugin.get_peer_cidrs_for_router(self.context,
|
||||
router['id'])
|
||||
expected = ['20.1.0.0/24', '20.2.0.0/24', '20.3.0.0/24']
|
||||
self.assertEqual(sorted(expected), sorted(returned_cidrs))
|
||||
|
||||
def test_get_peer_cidrs_for_router_with_ep_groups(self):
|
||||
self._test_get_peer_cidrs_for_router(
|
||||
self._setup_ipsec_site_connections_with_ep_groups)
|
||||
|
||||
def test_get_peer_cidrs_for_router_without_ep_groups(self):
|
||||
self._test_get_peer_cidrs_for_router(
|
||||
self._setup_ipsec_site_connections_without_ep_groups)
|
||||
|
218
neutron_vpnaas/tests/unit/db/vpn/test_vpn_ext_gw_db.py
Normal file
218
neutron_vpnaas/tests/unit/db/vpn/test_vpn_ext_gw_db.py
Normal file
@ -0,0 +1,218 @@
|
||||
# Copyright 2023 SysEleven GmbH
|
||||
#
|
||||
# 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 neutron.api import extensions
|
||||
from neutron.tests.unit.api import test_extensions
|
||||
from neutron.tests.unit.extensions import test_l3 as test_l3_plugin
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib.callbacks import exceptions as cb_exc
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_lib import context
|
||||
from neutron_lib.plugins import constants as nconstants
|
||||
from neutron_lib.plugins import directory
|
||||
from neutron_vpnaas.db.vpn.vpn_ext_gw_db import VPNExtGWPlugin_db
|
||||
from neutron_vpnaas.services.vpn.common import constants as v_constants
|
||||
from neutron_vpnaas.tests import base
|
||||
from neutron_vpnaas.tests.unit.db.vpn import test_vpn_db
|
||||
|
||||
|
||||
OVN_VPN_PLUGIN_KLASS = "neutron_vpnaas.services.vpn.ovn_plugin.VPNOVNPlugin"
|
||||
|
||||
|
||||
class VPNOVNPluginDbTestCase(test_l3_plugin.L3NatTestCaseMixin,
|
||||
base.NeutronDbPluginV2TestCase):
|
||||
def setUp(self, core_plugin=None, vpnaas_plugin=OVN_VPN_PLUGIN_KLASS,
|
||||
vpnaas_provider=None):
|
||||
|
||||
service_plugins = {'vpnaas_plugin': vpnaas_plugin}
|
||||
plugin_str = 'neutron.tests.unit.extensions.test_l3.TestL3NatIntPlugin'
|
||||
super().setUp(plugin_str, service_plugins=service_plugins)
|
||||
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
|
||||
self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr)
|
||||
self.core_plugin = directory.get_plugin()
|
||||
self.tenant_id = 'tenant1'
|
||||
|
||||
|
||||
class TestVPNExtGw(VPNOVNPluginDbTestCase):
|
||||
def _pre_port_delete(self, admin_context, port_id):
|
||||
registry.publish(
|
||||
resources.PORT, events.BEFORE_DELETE, self,
|
||||
payload=events.DBEventPayload(
|
||||
admin_context,
|
||||
metadata={'port_check': True},
|
||||
resource_id=port_id))
|
||||
|
||||
def _pre_subnet_delete(self, admin_context, subnet_id):
|
||||
registry.publish(resources.SUBNET, events.BEFORE_DELETE, self,
|
||||
payload=events.DBEventPayload(admin_context,
|
||||
resource_id=subnet_id))
|
||||
|
||||
def _pre_network_delete(self, admin_context, network_id):
|
||||
registry.publish(resources.NETWORK, events.BEFORE_DELETE, self,
|
||||
payload=events.DBEventPayload(admin_context,
|
||||
resource_id=network_id))
|
||||
|
||||
def _test_prevent_vpn_port_deletion(self, device_owner, gw_key):
|
||||
plugin = directory.get_plugin(nconstants.VPN)
|
||||
with self.router() as router, \
|
||||
self.port(device_owner=device_owner) as port:
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['router']['id'],
|
||||
gw_key: port['port']['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
admin_context = context.get_admin_context()
|
||||
plugin.create_gateway(admin_context, gateway)
|
||||
self.assertRaises(
|
||||
cb_exc.CallbackFailure,
|
||||
self._pre_port_delete, admin_context, port['port']['id'])
|
||||
|
||||
def test_prevent_vpn_port_deletion_gw_port(self):
|
||||
self._test_prevent_vpn_port_deletion(
|
||||
v_constants.DEVICE_OWNER_VPN_ROUTER_GW, 'gw_port_id')
|
||||
|
||||
def test_prevent_vpn_port_deletion_transit_port(self):
|
||||
self._test_prevent_vpn_port_deletion(
|
||||
v_constants.DEVICE_OWNER_TRANSIT_NETWORK, 'transit_port_id')
|
||||
|
||||
def test_prevent_vpn_port_deletion_other_device_owner(self):
|
||||
plugin = directory.get_plugin(nconstants.VPN)
|
||||
device_owner = v_constants.DEVICE_OWNER_TRANSIT_NETWORK
|
||||
with self.router() as router, \
|
||||
self.port(device_owner=device_owner) as transit_port, \
|
||||
self.port(device_owner='other-device-owner') as other_port:
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['router']['id'],
|
||||
'transit_port_id': transit_port['port']['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
admin_context = context.get_admin_context()
|
||||
plugin.create_gateway(admin_context, gateway)
|
||||
# BEFORE_DELETE event for other_port should not raise an exception
|
||||
self._pre_port_delete(admin_context, other_port['port']['id'])
|
||||
|
||||
def test_prevent_vpn_subnet_deletion(self):
|
||||
plugin = directory.get_plugin(nconstants.VPN)
|
||||
with self.router() as router, self.subnet() as subnet:
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['router']['id'],
|
||||
'transit_subnet_id': subnet['subnet']['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
admin_context = context.get_admin_context()
|
||||
plugin.create_gateway(admin_context, gateway)
|
||||
self.assertRaises(
|
||||
cb_exc.CallbackFailure,
|
||||
self._pre_subnet_delete, admin_context, subnet['subnet']['id'])
|
||||
# should not raise an exception for other subnet id
|
||||
self._pre_subnet_delete(admin_context, "other-id")
|
||||
|
||||
def test_prevent_vpn_network_deletion(self):
|
||||
plugin = directory.get_plugin(nconstants.VPN)
|
||||
with self.router() as router, self.network() as network:
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['router']['id'],
|
||||
'transit_network_id': network['network']['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
admin_context = context.get_admin_context()
|
||||
plugin.create_gateway(admin_context, gateway)
|
||||
self.assertRaises(
|
||||
cb_exc.CallbackFailure,
|
||||
self._pre_network_delete, admin_context,
|
||||
network['network']['id'])
|
||||
# should not raise an exception for other network id
|
||||
self._pre_network_delete(admin_context, "other-id")
|
||||
|
||||
|
||||
class TestVPNExtGwDB(base.NeutronDbPluginV2TestCase,
|
||||
test_vpn_db.NeutronResourcesMixin):
|
||||
def setUp(self):
|
||||
plugin_str = 'neutron.tests.unit.extensions.test_l3.TestL3NatIntPlugin'
|
||||
super().setUp(plugin_str)
|
||||
|
||||
self.core_plugin = directory.get_plugin()
|
||||
self.l3_plugin = directory.get_plugin(nconstants.L3)
|
||||
self.tenant_id = 'tenant1'
|
||||
self.context = context.get_admin_context()
|
||||
|
||||
def _create_gw_port(self, router):
|
||||
port = {'port': {
|
||||
'tenant_id': self.tenant_id,
|
||||
'network_id': router['external_gateway_info']['network_id'],
|
||||
'fixed_ips': lib_constants.ATTR_NOT_SPECIFIED,
|
||||
'mac_address': lib_constants.ATTR_NOT_SPECIFIED,
|
||||
'admin_state_up': True,
|
||||
'device_id': router['id'],
|
||||
'device_owner': v_constants.DEVICE_OWNER_VPN_ROUTER_GW,
|
||||
'name': ''
|
||||
}}
|
||||
return self.core_plugin.create_port(self.context, port)
|
||||
|
||||
def test_create_gateway(self):
|
||||
private_subnet, router = self.create_basic_topology()
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
gwdb = VPNExtGWPlugin_db()
|
||||
new_gateway = gwdb.create_gateway(self.context, gateway)
|
||||
expected = {**gateway['gateway'],
|
||||
'status': lib_constants.PENDING_CREATE}
|
||||
self.assertDictSupersetOf(expected, new_gateway)
|
||||
|
||||
def test_update_gateway_with_external_port(self):
|
||||
private_subnet, router = self.create_basic_topology()
|
||||
gwdb = VPNExtGWPlugin_db()
|
||||
# create gateway
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
new_gateway = gwdb.create_gateway(self.context, gateway)
|
||||
|
||||
# create external port and update gateway with the port id
|
||||
gw_port = self._create_gw_port(router)
|
||||
gateway_update = {'gateway': {
|
||||
'gw_port_id': gw_port['id']
|
||||
}}
|
||||
gwdb.update_gateway(self.context, new_gateway['id'], gateway_update)
|
||||
|
||||
# check that get_vpn_gw_dict_by_router_id includes external_fixed_ips
|
||||
found_gateway = gwdb.get_vpn_gw_dict_by_router_id(self.context,
|
||||
router['id'])
|
||||
self.assertIn('external_fixed_ips', found_gateway)
|
||||
expected = sorted(gw_port['fixed_ips'])
|
||||
returned = sorted(found_gateway['external_fixed_ips'])
|
||||
self.assertEqual(returned, expected)
|
||||
|
||||
def test_delete_gateway(self):
|
||||
private_subnet, router = self.create_basic_topology()
|
||||
gwdb = VPNExtGWPlugin_db()
|
||||
# create gateway
|
||||
gateway = {'gateway': {
|
||||
'router_id': router['id'],
|
||||
'tenant_id': self.tenant_id
|
||||
}}
|
||||
new_gateway = gwdb.create_gateway(self.context, gateway)
|
||||
self.assertIsNotNone(new_gateway)
|
||||
deleted = gwdb.delete_gateway(self.context, new_gateway['id'])
|
||||
self.assertEqual(deleted, 1)
|
||||
deleted = gwdb.delete_gateway(self.context, new_gateway['id'])
|
||||
self.assertEqual(deleted, 0)
|
||||
found_gateway = gwdb.get_vpn_gw_dict_by_router_id(self.context,
|
||||
router['id'])
|
||||
self.assertIsNone(found_gateway)
|
@ -0,0 +1,260 @@
|
||||
# Copyright 2023 SysEleven GmbH.
|
||||
#
|
||||
# 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 neutron.agent.linux import ip_lib
|
||||
from neutron.conf.agent import common as agent_config
|
||||
from neutron.conf import common as common_config
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from neutron_vpnaas.services.vpn.device_drivers import ovn_ipsec
|
||||
from neutron_vpnaas.tests import base
|
||||
from neutron_vpnaas.tests.unit.services.vpn.device_drivers import test_ipsec
|
||||
|
||||
_uuid = uuidutils.generate_uuid
|
||||
|
||||
|
||||
FAKE_PROCESS_ID = "c5b52e50-e678-491e-98dd-34e2676a6f81"
|
||||
FAKE_NAMESPACE_NAME = "qvpn-c5b52e50-e678-491e-98dd-34e2676a6f81"
|
||||
|
||||
FAKE_GW_PORT_ID = "e95d89fb-1723-4865-876f-a1c8efed4b55"
|
||||
FAKE_GW_PORT_INTERFACE_NAME = "vge95d89fb-172"
|
||||
FAKE_GW_PORT_IP_ADDRESS = "20.20.20.20"
|
||||
FAKE_GW_PORT_MAC_ADDRESS = "11:22:33:44:55:66"
|
||||
FAKE_GW_PORT_SUBNET_ID = _uuid()
|
||||
FAKE_GW_PORT_SUBNET_INFO = {
|
||||
'id': FAKE_GW_PORT_SUBNET_ID,
|
||||
'cidr': '20.20.20.0/24',
|
||||
'ip_version': 4
|
||||
}
|
||||
FAKE_GW_PORT = {
|
||||
'id': FAKE_GW_PORT_ID,
|
||||
'mac_address': FAKE_GW_PORT_MAC_ADDRESS,
|
||||
'fixed_ips': [{
|
||||
'ip_address': FAKE_GW_PORT_IP_ADDRESS,
|
||||
'subnet_id': FAKE_GW_PORT_SUBNET_ID
|
||||
}]
|
||||
}
|
||||
FAKE_TRANSIT_PORT_ID = "0eb4bdb3-fe2e-4724-bb04-f84b6a5974f8"
|
||||
FAKE_TRANSIT_PORT_INTERFACE_NAME = "vr0eb4bdb3-fe2"
|
||||
FAKE_TRANSIT_PORT_MAC_ADDRESS = "22:33:44:55:66:77"
|
||||
FAKE_TRANSIT_PORT_IP_ADDRESS = "169.254.0.2"
|
||||
FAKE_TRANSIT_PORT_SUBNET_ID = _uuid()
|
||||
FAKE_TRANSIT_PORT = {
|
||||
'id': FAKE_TRANSIT_PORT_ID,
|
||||
'mac_address': FAKE_TRANSIT_PORT_MAC_ADDRESS,
|
||||
'fixed_ips': [{
|
||||
'ip_address': FAKE_TRANSIT_PORT_IP_ADDRESS,
|
||||
'subnet_id': FAKE_TRANSIT_PORT_SUBNET_ID
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
def fake_interface_driver(*args, **kwargs):
|
||||
driver = mock.Mock()
|
||||
driver.DEV_NAME_LEN = 14
|
||||
return driver
|
||||
|
||||
|
||||
class TestDeviceManager(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.conf = cfg.CONF
|
||||
self.conf.register_opts(common_config.core_opts)
|
||||
self.conf.register_opts(agent_config.INTERFACE_DRIVER_OPTS)
|
||||
self.conf.set_override('interface_driver',
|
||||
'neutron_vpnaas.tests.unit.services.vpn.device_drivers'
|
||||
'.test_ovn_ipsec.fake_interface_driver')
|
||||
self.host = "some-hostname"
|
||||
self.plugin = mock.Mock()
|
||||
self.plugin.get_subnet_info.return_value = FAKE_GW_PORT_SUBNET_INFO
|
||||
self.context = mock.Mock()
|
||||
|
||||
def test_names(self):
|
||||
mgr = ovn_ipsec.DeviceManager(self.conf, self.host,
|
||||
self.plugin, self.context)
|
||||
port = {'id': "0df5beb8-4794-4217-acde-e6ce4875a59f"}
|
||||
name = mgr.get_interface_name(port, "internal")
|
||||
self.assertEqual(name, "vr0df5beb8-479")
|
||||
|
||||
name = mgr.get_interface_name(port, "external")
|
||||
self.assertEqual(name, "vg0df5beb8-479")
|
||||
|
||||
name = mgr.get_namespace_name("0df5beb8-4794-4217-acde-e6ce4875a59f")
|
||||
self.assertEqual(name, "qvpn-0df5beb8-4794-4217-acde-e6ce4875a59f")
|
||||
|
||||
def test_setup_external(self):
|
||||
ext_net_id = _uuid()
|
||||
network_details = {
|
||||
'gw_port': FAKE_GW_PORT,
|
||||
'external_network': {
|
||||
'id': ext_net_id
|
||||
}
|
||||
}
|
||||
|
||||
mgr = ovn_ipsec.DeviceManager(self.conf, self.host,
|
||||
self.plugin, self.context)
|
||||
|
||||
with mock.patch.object(ip_lib, 'ensure_device_is_ready') as dev_ready:
|
||||
with mock.patch.object(mgr, 'set_default_route') as set_def_route:
|
||||
dev_ready.return_value = False
|
||||
|
||||
mgr.setup_external(FAKE_PROCESS_ID, network_details)
|
||||
|
||||
dev_ready.assert_called_once()
|
||||
self.plugin.get_subnet_info.assert_called_once_with(
|
||||
FAKE_GW_PORT_SUBNET_ID
|
||||
)
|
||||
set_def_route.assert_called_once_with(
|
||||
FAKE_NAMESPACE_NAME,
|
||||
FAKE_GW_PORT_SUBNET_INFO,
|
||||
FAKE_GW_PORT_INTERFACE_NAME
|
||||
)
|
||||
mgr.driver.init_l3.assert_called_once()
|
||||
mgr.driver.plug.assert_called_once()
|
||||
|
||||
def test_setup_internal(self):
|
||||
network_details = {'transit_port': FAKE_TRANSIT_PORT}
|
||||
|
||||
mgr = ovn_ipsec.DeviceManager(self.conf, self.host,
|
||||
self.plugin, self.context)
|
||||
|
||||
with mock.patch.object(ip_lib, 'ensure_device_is_ready') as dev_ready:
|
||||
dev_ready.return_value = False
|
||||
|
||||
mgr.setup_internal(FAKE_PROCESS_ID, network_details)
|
||||
|
||||
dev_ready.assert_called_once()
|
||||
mgr.driver.init_l3.assert_called_once()
|
||||
mgr.driver.plug.assert_called_once()
|
||||
|
||||
def test_list_routes(self):
|
||||
mgr = ovn_ipsec.DeviceManager(self.conf, self.host,
|
||||
self.plugin, self.context)
|
||||
mock_ipdev = mock.Mock()
|
||||
routes = [
|
||||
{'cidr': '192.168.111.0/24', 'via': FAKE_TRANSIT_PORT_IP_ADDRESS}
|
||||
]
|
||||
with mock.patch.object(ip_lib, 'IPDevice') as ipdev:
|
||||
ipdev.return_value = mock_ipdev
|
||||
mock_ipdev.route.list_routes.return_value = routes
|
||||
returned = mgr.list_routes(FAKE_NAMESPACE_NAME)
|
||||
self.assertEqual(returned, routes)
|
||||
|
||||
def test_del_static_routes(self):
|
||||
mgr = ovn_ipsec.DeviceManager(self.conf, self.host, self.plugin,
|
||||
self.context)
|
||||
mock_ipdev = mock.Mock()
|
||||
routes = [
|
||||
{'cidr': '192.168.111.0/24', 'via': FAKE_TRANSIT_PORT_IP_ADDRESS},
|
||||
{'cidr': '192.168.112.0/24', 'via': FAKE_TRANSIT_PORT_IP_ADDRESS}
|
||||
]
|
||||
with mock.patch.object(ip_lib, 'IPDevice') as ipdev:
|
||||
ipdev.return_value = mock_ipdev
|
||||
mock_ipdev.route.list_routes.return_value = routes
|
||||
|
||||
mgr.del_static_routes(FAKE_NAMESPACE_NAME)
|
||||
|
||||
mock_ipdev.route.delete_route.assert_has_calls([
|
||||
mock.call(routes[0]['cidr'], via=FAKE_TRANSIT_PORT_IP_ADDRESS),
|
||||
mock.call(routes[1]['cidr'], via=FAKE_TRANSIT_PORT_IP_ADDRESS),
|
||||
], any_order=True)
|
||||
|
||||
|
||||
class TestOvnStrongSwanDriver(test_ipsec.IPSecDeviceLegacy):
|
||||
|
||||
def setUp(self, driver=ovn_ipsec.OvnStrongSwanDriver,
|
||||
ipsec_process=ovn_ipsec.OvnStrongSwanProcess):
|
||||
conf = cfg.CONF
|
||||
conf.register_opts(common_config.core_opts)
|
||||
conf.register_opts(agent_config.INTERFACE_DRIVER_OPTS)
|
||||
conf.set_override('interface_driver',
|
||||
'neutron_vpnaas.tests.unit.services.vpn.device_drivers'
|
||||
'.test_ovn_ipsec.fake_interface_driver')
|
||||
|
||||
super().setUp(driver, ipsec_process)
|
||||
self.driver.nsmgr = mock.Mock()
|
||||
self.driver.nsmgr.exists.return_value = False
|
||||
self.driver.devmgr = mock.Mock()
|
||||
self.driver.devmgr.get_namespace_name.return_value = \
|
||||
FAKE_NAMESPACE_NAME
|
||||
self.driver.devmgr.list_routes.return_value = []
|
||||
self.driver.devmgr.get_existing_process_ids.return_value = []
|
||||
self.driver.agent_rpc.get_vpn_transit_network_details.return_value = {
|
||||
'transit_gateway_ip': '192.168.1.1',
|
||||
}
|
||||
|
||||
def test_iptables_apply(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_get_namespace_for_router(self):
|
||||
"""Different for OvnIPsecDriver"""
|
||||
namespace = self.driver.get_namespace(FAKE_PROCESS_ID)
|
||||
self.assertEqual(FAKE_NAMESPACE_NAME, namespace)
|
||||
|
||||
def test_fail_getting_namespace_for_unknown_router(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_create_router(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_destroy_router(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_remove_rule(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_add_nat_rules_with_multiple_local_subnets(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def _test_add_nat_rule(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_add_nat_rule(self):
|
||||
"""Not applicable for OvnIPsecDriver"""
|
||||
pass
|
||||
|
||||
def test_stale_cleanup(self):
|
||||
process = self.fake_ensure_process(FAKE_PROCESS_ID)
|
||||
|
||||
self.driver.devmgr.get_existing_process_ids.return_value = [
|
||||
FAKE_PROCESS_ID]
|
||||
|
||||
self.driver.agent_rpc.get_vpn_services_on_host.return_value = []
|
||||
context = mock.Mock()
|
||||
with mock.patch.object(self.driver, 'ensure_process') as ensure:
|
||||
ensure.return_value = process
|
||||
self.driver.sync(context, [])
|
||||
process.disable.assert_called()
|
||||
|
||||
|
||||
class TestOvnOpenSwanDriver(TestOvnStrongSwanDriver):
|
||||
def setUp(self):
|
||||
super().setUp(driver=ovn_ipsec.OvnOpenSwanDriver,
|
||||
ipsec_process=ovn_ipsec.OvnOpenSwanProcess)
|
||||
|
||||
|
||||
class TestOvnLibreSwanDriver(TestOvnStrongSwanDriver):
|
||||
def setUp(self):
|
||||
super().setUp(driver=ovn_ipsec.OvnLibreSwanDriver,
|
||||
ipsec_process=ovn_ipsec.OvnLibreSwanProcess)
|
@ -0,0 +1,306 @@
|
||||
# Copyright 2020, SysEleven GbmH
|
||||
# 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 neutron_lib import context as n_ctx
|
||||
from neutron_lib.plugins import constants
|
||||
from neutron_lib.plugins import directory
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from neutron_vpnaas.services.vpn.service_drivers import ipsec_validator
|
||||
from neutron_vpnaas.services.vpn.service_drivers \
|
||||
import ovn_ipsec as ipsec_driver
|
||||
from neutron_vpnaas.tests import base
|
||||
|
||||
|
||||
_uuid = uuidutils.generate_uuid
|
||||
|
||||
FAKE_HOST = 'fake_host'
|
||||
FAKE_TENANT_ID = 'tenant1'
|
||||
FAKE_ROUTER_ID = _uuid()
|
||||
FAKE_TRANSIT_IP_ADDRESS = '169.254.0.2'
|
||||
|
||||
FAKE_VPNSERVICE_1 = {
|
||||
'id': _uuid(),
|
||||
'router_id': FAKE_ROUTER_ID,
|
||||
'tenant_id': FAKE_TENANT_ID
|
||||
}
|
||||
|
||||
FAKE_VPNSERVICE_2 = {
|
||||
'id': _uuid(),
|
||||
'router_id': FAKE_ROUTER_ID,
|
||||
'tenant_id': FAKE_TENANT_ID
|
||||
}
|
||||
|
||||
FAKE_VPN_CONNECTION_1 = {
|
||||
'vpnservice_id': FAKE_VPNSERVICE_1['id']
|
||||
}
|
||||
|
||||
|
||||
class FakeSqlQueryObject(dict):
|
||||
"""To fake SqlAlchemy query object and access keys as attributes."""
|
||||
|
||||
def __init__(self, **entries):
|
||||
self.__dict__.update(entries)
|
||||
super(FakeSqlQueryObject, self).__init__(**entries)
|
||||
|
||||
|
||||
class FakeGatewayDB(object):
|
||||
def __init__(self):
|
||||
self.gateways_by_router = {}
|
||||
self.gateways_by_id = {}
|
||||
|
||||
def create_gateway(self, context, gateway):
|
||||
info = gateway['gateway']
|
||||
fake_gw = {
|
||||
'id': _uuid(),
|
||||
'status': 'PENDING_CREATE',
|
||||
'external_fixed_ips': [{'subnet_id': '1',
|
||||
'ip_address': '10.2.3.4'}],
|
||||
**info
|
||||
}
|
||||
self.gateways_by_router[info['router_id']] = fake_gw
|
||||
self.gateways_by_id[fake_gw['id']] = fake_gw
|
||||
return fake_gw
|
||||
|
||||
def update_gateway(self, context, gateway_id, gateway):
|
||||
self.gateways_by_id[gateway_id].update(**gateway['gateway'])
|
||||
|
||||
def delete_gateway(self, context, gateway_id):
|
||||
fake_gw = self.gateways_by_id.pop(gateway_id, None)
|
||||
if fake_gw:
|
||||
self.gateways_by_router.pop(fake_gw['router_id'])
|
||||
return 1 if fake_gw else 0
|
||||
|
||||
def get_vpn_gw_dict_by_router_id(self, context, router_id, refresh=False):
|
||||
return self.gateways_by_router.get(router_id)
|
||||
|
||||
|
||||
class TestOvnIPsecDriver(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
mock.patch('neutron_lib.rpc.Connection').start()
|
||||
self.create_port = \
|
||||
mock.patch('neutron_lib.plugins.utils.create_port').start()
|
||||
self.create_network = \
|
||||
mock.patch('neutron_lib.plugins.utils.create_network').start()
|
||||
self.create_subnet = \
|
||||
mock.patch('neutron_lib.plugins.utils.create_subnet').start()
|
||||
|
||||
self.create_port.side_effect = lambda pl, c, p: {
|
||||
'id': _uuid(),
|
||||
'fixed_ips': [{'subnet_id': '1', 'ip_address': '10.1.1.2'}]}
|
||||
self.create_network.side_effect = lambda pl, c, n: {'id': _uuid()}
|
||||
self.create_subnet.side_effect = lambda pl, c, s: {'id': _uuid()}
|
||||
|
||||
vpn_agent = {'host': FAKE_HOST}
|
||||
|
||||
self.core_plugin = mock.Mock()
|
||||
self.core_plugin.get_vpn_agents_hosting_routers.return_value = \
|
||||
[vpn_agent]
|
||||
|
||||
directory.add_plugin(constants.CORE, self.core_plugin)
|
||||
|
||||
self._fake_router = FakeSqlQueryObject(
|
||||
id=FAKE_ROUTER_ID,
|
||||
gw_port=FakeSqlQueryObject(network_id=_uuid())
|
||||
)
|
||||
|
||||
self.l3_plugin = mock.Mock()
|
||||
self.l3_plugin.get_router.return_value = self._fake_router
|
||||
directory.add_plugin(constants.L3, self.l3_plugin)
|
||||
|
||||
self.svc_plugin = mock.Mock()
|
||||
self.svc_plugin.get_vpn_agents_hosting_routers.return_value = \
|
||||
[vpn_agent]
|
||||
self.svc_plugin.schedule_router.return_value = vpn_agent
|
||||
self.svc_plugin._get_vpnservice.return_value = FakeSqlQueryObject(
|
||||
router_id=FAKE_ROUTER_ID,
|
||||
router=self._fake_router
|
||||
)
|
||||
self.svc_plugin.get_vpnservice.return_value = FAKE_VPNSERVICE_1
|
||||
self.svc_plugin.get_vpnservice_router_id.return_value = FAKE_ROUTER_ID
|
||||
self.driver = ipsec_driver.IPsecOvnVPNDriver(self.svc_plugin)
|
||||
self.validator = ipsec_validator.IpsecVpnValidator(self.driver)
|
||||
self.context = n_ctx.get_admin_context()
|
||||
|
||||
def test_create_vpnservice(self):
|
||||
mock.patch.object(self.driver.agent_rpc.client, 'cast')
|
||||
mock.patch.object(self.driver.agent_rpc.client, 'prepare')
|
||||
fake_gw_db = FakeGatewayDB()
|
||||
|
||||
self.svc_plugin.get_vpn_gw_dict_by_router_id.side_effect = \
|
||||
fake_gw_db.get_vpn_gw_dict_by_router_id
|
||||
self.svc_plugin.create_gateway.side_effect = fake_gw_db.create_gateway
|
||||
self.svc_plugin.update_gateway.side_effect = fake_gw_db.update_gateway
|
||||
|
||||
self.driver.create_vpnservice(self.context, FAKE_VPNSERVICE_1)
|
||||
self.svc_plugin.create_gateway.assert_called_once()
|
||||
|
||||
# check that the plugin utils create functions were called
|
||||
self.create_port.assert_called()
|
||||
self.create_network.assert_called_once()
|
||||
self.create_subnet.assert_called_once()
|
||||
|
||||
# check that the core plugin create functions were not called directly
|
||||
self.core_plugin.create_port.assert_not_called()
|
||||
self.core_plugin.create_network.assert_not_called()
|
||||
self.core_plugin.create_subnet.assert_not_called()
|
||||
|
||||
self.svc_plugin.reset_mock()
|
||||
|
||||
self.driver.create_vpnservice(self.context, FAKE_VPNSERVICE_2)
|
||||
self.svc_plugin.create_gateway.assert_not_called()
|
||||
|
||||
def test_delete_vpnservice(self):
|
||||
mock.patch.object(self.driver.agent_rpc.client, 'cast')
|
||||
mock.patch.object(self.driver.agent_rpc.client, 'prepare')
|
||||
fake_gw_db = FakeGatewayDB()
|
||||
self.svc_plugin.get_vpn_gw_dict_by_router_id.side_effect = \
|
||||
fake_gw_db.get_vpn_gw_dict_by_router_id
|
||||
self.svc_plugin.create_gateway.side_effect = fake_gw_db.create_gateway
|
||||
self.svc_plugin.update_gateway.side_effect = fake_gw_db.update_gateway
|
||||
self.svc_plugin.delete_gateway.side_effect = fake_gw_db.delete_gateway
|
||||
|
||||
# create 2 VPN services on same router
|
||||
self.driver.create_vpnservice(self.context, FAKE_VPNSERVICE_1)
|
||||
self.driver.create_vpnservice(self.context, FAKE_VPNSERVICE_2)
|
||||
self.svc_plugin.reset_mock()
|
||||
|
||||
# deleting one VPN service must not delete the VPN gateway
|
||||
self.svc_plugin.get_vpnservices.return_value = [FAKE_VPNSERVICE_2]
|
||||
self.driver.delete_vpnservice(self.context, FAKE_VPNSERVICE_1)
|
||||
self.core_plugin.delete_port.assert_not_called()
|
||||
self.core_plugin.delete_network.assert_not_called()
|
||||
self.core_plugin.delete_subnet.assert_not_called()
|
||||
self.svc_plugin.create_gateway.assert_not_called()
|
||||
self.svc_plugin.delete_gateway.assert_not_called()
|
||||
|
||||
# deleting last VPN service shall delete the VPN gateway
|
||||
self.svc_plugin.get_vpnservices.return_value = []
|
||||
self.driver.delete_vpnservice(self.context, FAKE_VPNSERVICE_1)
|
||||
self.core_plugin.delete_port.assert_called()
|
||||
self.core_plugin.delete_network.assert_called_once()
|
||||
self.core_plugin.delete_subnet.assert_called_once()
|
||||
self.svc_plugin.create_gateway.assert_not_called()
|
||||
self.svc_plugin.delete_gateway.assert_called_once()
|
||||
|
||||
def _test_ipsec_site_connection(self, old_peers, new_peers,
|
||||
func, args,
|
||||
expected_add, expected_remove):
|
||||
self._fake_router['routes'] = [
|
||||
{'destination': peer, 'nexthop': FAKE_TRANSIT_IP_ADDRESS}
|
||||
for peer in old_peers
|
||||
]
|
||||
transit_port = FakeSqlQueryObject(
|
||||
id=_uuid(),
|
||||
fixed_ips=[
|
||||
{'subnet_id': _uuid(), 'ip_address': FAKE_TRANSIT_IP_ADDRESS}
|
||||
]
|
||||
)
|
||||
self.svc_plugin.get_vpn_gw_by_router_id.return_value = \
|
||||
FakeSqlQueryObject(id=_uuid(),
|
||||
router_id=FAKE_ROUTER_ID,
|
||||
transit_port_id=transit_port.id,
|
||||
transit_port=transit_port)
|
||||
|
||||
self.svc_plugin.get_peer_cidrs_for_router.return_value = new_peers
|
||||
|
||||
# create/update/delete_ipsec_site_connection
|
||||
with mock.patch.object(self.driver.agent_rpc.client, 'cast'
|
||||
) as rpc_mock, \
|
||||
mock.patch.object(self.driver.agent_rpc.client, 'prepare'
|
||||
) as prepare_mock:
|
||||
prepare_mock.return_value = self.driver.agent_rpc.client
|
||||
func(self.context, *args)
|
||||
|
||||
prepare_args = {'server': 'fake_host', 'version': '1.0'}
|
||||
prepare_mock.assert_called_once_with(**prepare_args)
|
||||
|
||||
# check that agent RPC vpnservice_updated is called
|
||||
rpc_mock.assert_called_once_with(self.context, 'vpnservice_updated',
|
||||
router={'id': FAKE_ROUTER_ID})
|
||||
|
||||
# check that routes were updated
|
||||
if expected_add:
|
||||
expected_router = {'router': {'routes': [
|
||||
{'destination': peer,
|
||||
'nexthop': FAKE_TRANSIT_IP_ADDRESS}
|
||||
for peer in expected_add
|
||||
]}}
|
||||
self.l3_plugin.add_extraroutes.assert_called_once_with(
|
||||
self.context, FAKE_ROUTER_ID, expected_router)
|
||||
else:
|
||||
self.l3_plugin.add_extraroutes.assert_not_called()
|
||||
|
||||
if expected_remove:
|
||||
expected_router = {'router': {'routes': [
|
||||
{'destination': peer,
|
||||
'nexthop': FAKE_TRANSIT_IP_ADDRESS}
|
||||
for peer in expected_remove
|
||||
]}}
|
||||
self.l3_plugin.remove_extraroutes.assert_called_once_with(
|
||||
self.context, FAKE_ROUTER_ID, expected_router)
|
||||
else:
|
||||
self.l3_plugin.remove_extraroutes.assert_not_called()
|
||||
|
||||
def test_create_ipsec_site_connection_1(self):
|
||||
old_peers = []
|
||||
new_peers = ['192.168.1.0/24']
|
||||
expected_add = new_peers
|
||||
expected_remove = []
|
||||
self._test_ipsec_site_connection(
|
||||
old_peers, new_peers,
|
||||
self.driver.create_ipsec_site_connection,
|
||||
[FAKE_VPN_CONNECTION_1],
|
||||
expected_add, expected_remove
|
||||
)
|
||||
|
||||
def test_create_ipsec_site_connection_2(self):
|
||||
"""Test creating a 2nd site connection."""
|
||||
old_peers = ['192.168.1.0/24']
|
||||
new_peers = ['192.168.1.0/24', '192.168.2.0/24']
|
||||
expected_add = ['192.168.2.0/24']
|
||||
expected_remove = []
|
||||
self._test_ipsec_site_connection(
|
||||
old_peers, new_peers,
|
||||
self.driver.create_ipsec_site_connection,
|
||||
[FAKE_VPN_CONNECTION_1],
|
||||
expected_add, expected_remove
|
||||
)
|
||||
|
||||
def test_update_ipsec_site_connection(self):
|
||||
old_peers = ['192.168.1.0/24']
|
||||
new_peers = ['192.168.2.0/24']
|
||||
expected_add = new_peers
|
||||
expected_remove = old_peers
|
||||
self._test_ipsec_site_connection(
|
||||
old_peers, new_peers,
|
||||
self.driver.update_ipsec_site_connection,
|
||||
[FAKE_VPN_CONNECTION_1, FAKE_VPN_CONNECTION_1],
|
||||
expected_add, expected_remove
|
||||
)
|
||||
|
||||
def test_delete_ipsec_site_connection(self):
|
||||
old_peers = ['192.168.1.0/24', '192.168.2.0/24']
|
||||
new_peers = ['192.168.2.0/24']
|
||||
expected_add = []
|
||||
expected_remove = ['192.168.1.0/24']
|
||||
self._test_ipsec_site_connection(
|
||||
old_peers, new_peers,
|
||||
self.driver.delete_ipsec_site_connection,
|
||||
[FAKE_VPN_CONNECTION_1],
|
||||
expected_add, expected_remove
|
||||
)
|
8
releasenotes/notes/vpnaas-for-ovn-a487c62b877e3201.yaml
Normal file
8
releasenotes/notes/vpnaas-for-ovn-a487c62b877e3201.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
prelude: >
|
||||
VPNaaS support for ML2/OVN
|
||||
features:
|
||||
- |
|
||||
Neutron VPNaaS now supports OVN networking. There is a new stand-alone
|
||||
VPN agent to support ML2/OVN+VPN. OVN-specific service and device drivers
|
||||
have been added.
|
@ -30,6 +30,7 @@ data_files =
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
neutron-vpn-netns-wrapper = neutron_vpnaas.services.vpn.common.netns_wrapper:main
|
||||
neutron-ovn-vpn-agent = neutron_vpnaas.cmd.eventlet.ovn_agent:main
|
||||
neutron.agent.l3.extensions =
|
||||
vpnaas = neutron_vpnaas.services.vpn.agent:L3WithVPNaaS
|
||||
device_drivers =
|
||||
@ -38,10 +39,12 @@ neutron.db.alembic_migrations =
|
||||
neutron-vpnaas = neutron_vpnaas.db.migration:alembic_migrations
|
||||
neutron.service_plugins =
|
||||
vpnaas = neutron_vpnaas.services.vpn.plugin:VPNDriverPlugin
|
||||
ovn-vpnaas = neutron_vpnaas.services.vpn.ovn_plugin:VPNOVNDriverPlugin
|
||||
neutron.services.vpn.plugin.VPNDriverPlugin = neutron_vpnaas.services.vpn.plugin:VPNDriverPlugin
|
||||
oslo.config.opts =
|
||||
neutron.vpnaas = neutron_vpnaas.opts:list_opts
|
||||
neutron.vpnaas.agent = neutron_vpnaas.opts:list_agent_opts
|
||||
neutron.vpnaas.ovn_agent = neutron_vpnaas.opts:list_ovn_agent_opts
|
||||
oslo.policy.policies =
|
||||
neutron-vpnaas = neutron_vpnaas.policies:list_rules
|
||||
neutron.policies =
|
||||
|
Loading…
Reference in New Issue
Block a user