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:
Bodo Petermann 2020-12-03 17:56:27 +01:00
parent e944dc144c
commit 256464aea6
45 changed files with 4746 additions and 13 deletions

View File

@ -20,8 +20,12 @@
- openstack-tox-py311: - openstack-tox-py311:
required-projects: required-projects:
- openstack/neutron - openstack/neutron
- openstack-tox-docs:
required-projects:
- openstack/neutron
- neutron-vpnaas-functional-sswan - neutron-vpnaas-functional-sswan
- neutron-tempest-plugin-vpnaas - neutron-tempest-plugin-vpnaas
- neutron-tempest-plugin-vpnaas-ovn
- neutron-tempest-plugin-vpnaas-libreswan-centos: - neutron-tempest-plugin-vpnaas-libreswan-centos:
# TODO(mlavalle) switch to voting when this job is moved to Centos # TODO(mlavalle) switch to voting when this job is moved to Centos
# 8 # 8
@ -40,8 +44,12 @@
- openstack-tox-py311: - openstack-tox-py311:
required-projects: required-projects:
- openstack/neutron - openstack/neutron
- openstack-tox-docs:
required-projects:
- openstack/neutron
- neutron-vpnaas-functional-sswan - neutron-vpnaas-functional-sswan
- neutron-tempest-plugin-vpnaas - neutron-tempest-plugin-vpnaas
- neutron-tempest-plugin-vpnaas-ovn
# TODO(mlavalle) uncomment following line when the job is moved to # TODO(mlavalle) uncomment following line when the job is moved to
# Centos 8 # Centos 8
# - neutron-tempest-plugin-vpnaas-libreswan-centos # - neutron-tempest-plugin-vpnaas-libreswan-centos
@ -62,6 +70,7 @@
- openstack/neutron - openstack/neutron
- neutron-vpnaas-openstack-tox-py310-with-sqlalchemy-main - neutron-vpnaas-openstack-tox-py310-with-sqlalchemy-main
- neutron-tempest-plugin-vpnaas - neutron-tempest-plugin-vpnaas
- neutron-tempest-plugin-vpnaas-ovn
- neutron-vpnaas-functional-sswan - neutron-vpnaas-functional-sswan
- job: - job:

View 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"

View File

@ -9,9 +9,14 @@ source $LIBDIR/l3_agent
NEUTRON_L3_CONF=${NEUTRON_L3_CONF:-$Q_L3_CONF_FILE} 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 { function neutron_vpnaas_install {
setup_develop $NEUTRON_VPNAAS_DIR 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 neutron_agent_vpnaas_install_agent_packages
fi fi
} }
@ -49,6 +54,43 @@ function neutron_vpnaas_configure_agent {
fi 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 { function neutron_vpnaas_configure_db {
$NEUTRON_BIN_DIR/neutron-db-manage --subproject neutron-vpnaas --config-file $NEUTRON_CONF upgrade head $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) (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 # Main plugin processing
# NOP for pre-install step # NOP for pre-install step
@ -77,6 +128,15 @@ elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
echo_summary "Configuring neutron-vpnaas agent" echo_summary "Configuring neutron-vpnaas agent"
neutron_vpnaas_configure_agent neutron_vpnaas_configure_agent
fi 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 # NOP for clean step

View File

@ -1,17 +1,30 @@
# Settings for the VPNaaS devstack plugin # Settings for the VPNaaS devstack plugin
# Plugin # Plugin
if [[ $Q_AGENT == "ovn" ]]; then
VPN_PLUGIN=${VPN_PLUGIN:-"ovn-vpnaas"}
else
VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"} VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"}
fi
# Device driver # Device driver
IPSEC_PACKAGE=${IPSEC_PACKAGE:-"strongswan"} IPSEC_PACKAGE=${IPSEC_PACKAGE:-"strongswan"}
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"} NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec:StrongSwanDriver"}
fi
function _get_service_provider { function _get_service_provider {
local ipsec_package=$1 local ipsec_package=$1
local name driver local name driver
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" driver="neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver"
fi
if [ "$ipsec_package" = "libreswan" ]; then if [ "$ipsec_package" = "libreswan" ]; then
name="openswan" name="openswan"
else else
@ -31,3 +44,5 @@ NEUTRON_VPNAAS_DIR=$DEST/neutron-vpnaas
NEUTRON_VPNAAS_CONF_FILE=neutron_vpnaas.conf NEUTRON_VPNAAS_CONF_FILE=neutron_vpnaas.conf
NEUTRON_VPNAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_VPNAAS_CONF_FILE NEUTRON_VPNAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_VPNAAS_CONF_FILE
OVN_VPNAGENT_CONF=$NEUTRON_CONF_DIR/neutron_ovn_vpn_agent.ini

View File

@ -15,6 +15,7 @@ Neutron VPNaaS uses the following configuration files for its various services.
neutron_vpnaas neutron_vpnaas
l3_agent l3_agent
neutron_ovn_vpn_agent
The following are sample configuration files for Neutron VPNaaS services and The following are sample configuration files for Neutron VPNaaS services and
utilities. These are generated from code and reflect the current state of code utilities. These are generated from code and reflect the current state of code

View 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

View File

@ -12,6 +12,8 @@ cp: RegExpFilter, cp, root, cp, -a, .*, .*/strongswan.d
ip: IpFilter, ip, root ip: IpFilter, ip, root
ip_exec: IpNetnsExecFilter, ip, root ip_exec: IpNetnsExecFilter, ip, root
ipsec: CommandFilter, ipsec, 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: RegExpFilter, rm, root, rm, -rf, (.*/strongswan.d|.*/ipsec/[0-9a-z-]+)
rm_file: RegExpFilter, rm, root, rm, -f, .*/ipsec.secrets rm_file: RegExpFilter, rm, root, rm, -f, .*/ipsec.secrets
strongswan: CommandFilter, strongswan, root strongswan: CommandFilter, strongswan, root

View File

@ -0,0 +1,5 @@
[DEFAULT]
output_file = etc/neutron_ovn_vpn_agent.ini.sample
wrap_width = 79
namespace = neutron.vpnaas.ovn_agent

View File

View File

View File

View 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']

View 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)

View 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)

View File

@ -0,0 +1,3 @@
from neutron.common import eventlet_utils
eventlet_utils.monkey_patch()

View 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()

View File

@ -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'),
)

View File

@ -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'),
)

View File

@ -1 +1 @@
5f884db48ba9 22e0145ac80b

View File

@ -23,7 +23,9 @@ Based on this comparison database can be healed with healing migration.
from neutron.db.migration.models import head 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_db # noqa
from neutron_vpnaas.db.vpn import vpn_ext_gw_db # noqa
def get_metadata(): def get_metadata():

View 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})

View File

@ -509,6 +509,19 @@ class VPNPluginDb(vpnaas.VPNPluginBase,
vpns_db.update(vpns) vpns_db.update(vpns)
return self._make_vpnservice_dict(vpns_db) 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): def update_vpnservice(self, context, vpnservice_id, vpnservice):
vpns = vpnservice['vpnservice'] vpns = vpnservice['vpnservice']
with db_api.CONTEXT_WRITER.using(context): with db_api.CONTEXT_WRITER.using(context):
@ -682,6 +695,22 @@ class VPNPluginDb(vpnaas.VPNPluginBase,
vpnservice = self._get_vpnservice(context, vpnservice_id) vpnservice = self._get_vpnservice(context, vpnservice_id)
return vpnservice['router_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): class VPNPluginRpcDbMixin(object):
def _build_local_subnet_cidr_map(self, context): def _build_local_subnet_cidr_map(self, context):

View 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()

View 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})

View File

@ -17,11 +17,35 @@ import abc
from neutron_lib.api.definitions import vpn from neutron_lib.api.definitions import vpn
from neutron_lib.api import extensions from neutron_lib.api import extensions
from neutron_lib import exceptions as nexception
from neutron_lib.plugins import constants as nconstants from neutron_lib.plugins import constants as nconstants
from neutron_lib.services import base as service_base from neutron_lib.services import base as service_base
from neutron.api.v2 import resource_helper 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): class Vpnaas(extensions.APIExtensionDescriptor):
api_definition = vpn api_definition = vpn

View File

@ -10,11 +10,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import neutron.conf.plugins.ml2.drivers.ovn.ovn_conf
import neutron.services.provider_configuration import neutron.services.provider_configuration
import neutron_vpnaas.services.vpn.agent import neutron_vpnaas.services.vpn.agent
import neutron_vpnaas.services.vpn.device_drivers.ipsec import neutron_vpnaas.services.vpn.device_drivers.ipsec
import neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec import neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec
import neutron_vpnaas.services.vpn.ovn_agent
def list_agent_opts(): 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(): def list_opts():
return [ return [
('service_providers', ('service_providers',

View 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

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from neutron_lib import constants
# Endpoint group types # Endpoint group types
SUBNET_ENDPOINT = 'subnet' SUBNET_ENDPOINT = 'subnet'
CIDR_ENDPOINT = 'cidr' CIDR_ENDPOINT = 'cidr'
@ -30,3 +32,15 @@ VPN_SUPPORTED_ENDPOINT_TYPES = [
SUBNET_ENDPOINT, CIDR_ENDPOINT, VLAN_ENDPOINT, SUBNET_ENDPOINT, CIDR_ENDPOINT, VLAN_ENDPOINT,
NETWORK_ENDPOINT, ROUTER_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'

View 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)

View 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))

View 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()

View 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)

View File

@ -75,6 +75,13 @@ class VPNDriverPlugin(VPNPlugin, vpn_db.VPNPluginRpcDbMixin):
def _flavors_plugin(self): def _flavors_plugin(self):
return directory.get_plugin(constants.FLAVORS) 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): def _check_orphan_vpnservice_associations(self):
context = ncontext.get_admin_context() context = ncontext.get_admin_context()
vpnservices = self.get_vpnservices(context) vpnservices = self.get_vpnservices(context)

View 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()

View 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])

View File

@ -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')

View File

@ -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')

View 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)

View File

@ -2290,3 +2290,77 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
self.context, self.context,
private_subnet['id'], private_subnet['id'],
router['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)

View 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)

View File

@ -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)

View File

@ -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
)

View 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.

View File

@ -30,6 +30,7 @@ data_files =
[entry_points] [entry_points]
console_scripts = console_scripts =
neutron-vpn-netns-wrapper = neutron_vpnaas.services.vpn.common.netns_wrapper:main 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 = neutron.agent.l3.extensions =
vpnaas = neutron_vpnaas.services.vpn.agent:L3WithVPNaaS vpnaas = neutron_vpnaas.services.vpn.agent:L3WithVPNaaS
device_drivers = device_drivers =
@ -38,10 +39,12 @@ neutron.db.alembic_migrations =
neutron-vpnaas = neutron_vpnaas.db.migration:alembic_migrations neutron-vpnaas = neutron_vpnaas.db.migration:alembic_migrations
neutron.service_plugins = neutron.service_plugins =
vpnaas = neutron_vpnaas.services.vpn.plugin:VPNDriverPlugin 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 neutron.services.vpn.plugin.VPNDriverPlugin = neutron_vpnaas.services.vpn.plugin:VPNDriverPlugin
oslo.config.opts = oslo.config.opts =
neutron.vpnaas = neutron_vpnaas.opts:list_opts neutron.vpnaas = neutron_vpnaas.opts:list_opts
neutron.vpnaas.agent = neutron_vpnaas.opts:list_agent_opts neutron.vpnaas.agent = neutron_vpnaas.opts:list_agent_opts
neutron.vpnaas.ovn_agent = neutron_vpnaas.opts:list_ovn_agent_opts
oslo.policy.policies = oslo.policy.policies =
neutron-vpnaas = neutron_vpnaas.policies:list_rules neutron-vpnaas = neutron_vpnaas.policies:list_rules
neutron.policies = neutron.policies =