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

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}
function is_ovn_enabled {
[[ $Q_AGENT == "ovn" ]] && return 0
return 1
}
function neutron_vpnaas_install {
setup_develop $NEUTRON_VPNAAS_DIR
if is_service_enabled q-l3 neutron-l3; then
if is_service_enabled q-l3 neutron-l3 q-ovn-vpn-agent; then
neutron_agent_vpnaas_install_agent_packages
fi
}
@ -49,6 +54,43 @@ function neutron_vpnaas_configure_agent {
fi
}
function neutron_vpnaas_configure_ovn_agent {
cp $NEUTRON_VPNAAS_DIR/etc/neutron_ovn_vpn_agent.ini.sample $OVN_VPNAGENT_CONF
iniset $OVN_VPNAGENT_CONF DEFAULT interface_driver openvswitch
iniset $OVN_VPNAGENT_CONF DEFAULT state_path $DATA_DIR/neutron
iniset_rpc_backend neutron-vpnaas $OVN_VPNAGENT_CONF
iniset $OVN_VPNAGENT_CONF agent root_helper "$Q_RR_COMMAND"
if [[ "$Q_USE_ROOTWRAP_DAEMON" == "True" ]]; then
iniset $OVN_VPNAGENT_CONF agent root_helper_daemon "$Q_RR_DAEMON_COMMAND"
fi
if [[ "$IPSEC_PACKAGE" == "strongswan" ]]; then
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnStrongSwanDriver
elif [[ "$IPSEC_PACKAGE" == "libreswan" ]]; then
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnLibreSwanDriver
else
iniset_multiline $OVN_VPNAGENT_CONF vpnagent vpn_device_driver $NEUTRON_VPNAAS_DEVICE_DRIVER
fi
OVSDB_SERVER_LOCAL_HOST=$SERVICE_LOCAL_HOST
if [[ "$SERVICE_IP_VERSION" == 6 ]]; then
OVSDB_SERVER_LOCAL_HOST=[$OVSDB_SERVER_LOCAL_HOST]
fi
OVN_SB_REMOTE=${OVN_SB_REMOTE:-$OVN_PROTO:$SERVICE_HOST:6642}
iniset $OVN_VPNAGENT_CONF ovs ovsdb_connection tcp:$OVSDB_SERVER_LOCAL_HOST:6640
iniset $OVN_VPNAGENT_CONF ovn ovn_sb_connection $OVN_SB_REMOTE
if is_service_enabled tls-proxy; then
iniset $OVN_VPNAGENT_CONF ovn \
ovn_sb_ca_cert $INT_CA_DIR/ca-chain.pem
iniset $OVN_VPNAGENT_CONF ovn \
ovn_sb_certificate $INT_CA_DIR/$DEVSTACK_CERT_NAME.crt
iniset $OVN_VPNAGENT_CONF ovn \
ovn_sb_private_key $INT_CA_DIR/private/$DEVSTACK_CERT_NAME.key
fi
}
function neutron_vpnaas_configure_db {
$NEUTRON_BIN_DIR/neutron-db-manage --subproject neutron-vpnaas --config-file $NEUTRON_CONF upgrade head
}
@ -58,6 +100,15 @@ function neutron_vpnaas_generate_config_files {
(cd $NEUTRON_VPNAAS_DIR && exec ./tools/generate_config_file_samples.sh)
}
function neutron_vpnaas_start_vpnagent {
NEUTRON_OVN_BIN_DIR=$(get_python_exec_prefix)
NEUTRON_OVN_VPNAGENT_BINARY="neutron-ovn-vpn-agent"
run_process q-ovn-vpn-agent "$NEUTRON_OVN_BIN_DIR/$NEUTRON_OVN_VPNAGENT_BINARY --config-file $OVN_VPNAGENT_CONF"
# Format logging
setup_logging $OVN_VPNAGENT_CONF
}
# Main plugin processing
# NOP for pre-install step
@ -77,6 +128,15 @@ elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
echo_summary "Configuring neutron-vpnaas agent"
neutron_vpnaas_configure_agent
fi
if is_service_enabled q-ovn-vpn-agent && is_ovn_enabled; then
echo_summary "Configuring neutron-ovn-vpn-agent"
neutron_vpnaas_configure_ovn_agent
fi
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
if is_service_enabled q-ovn-vpn-agent && is_ovn_enabled; then
neutron_vpnaas_start_vpnagent
fi
# NOP for clean step

View File

@ -1,23 +1,36 @@
# Settings for the VPNaaS devstack plugin
# Plugin
VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"}
if [[ $Q_AGENT == "ovn" ]]; then
VPN_PLUGIN=${VPN_PLUGIN:-"ovn-vpnaas"}
else
VPN_PLUGIN=${VPN_PLUGIN:-"vpnaas"}
fi
# Device driver
IPSEC_PACKAGE=${IPSEC_PACKAGE:-"strongswan"}
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec:StrongSwanDriver"}
if [[ $Q_AGENT == "ovn" ]]; then
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.OvnStrongSwanDriver"}
else
NEUTRON_VPNAAS_DEVICE_DRIVER=${NEUTRON_VPNAAS_DEVICE_DRIVER:-"neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec:StrongSwanDriver"}
fi
function _get_service_provider {
local ipsec_package=$1
local name driver
local ipsec_package=$1
local name driver
driver="neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver"
if [ "$ipsec_package" = "libreswan" ]; then
name="openswan"
else
name="strongswan"
fi
echo "VPN:${name}:${driver}:default"
if [[ $Q_AGENT == "ovn" ]]; then
driver="neutron_vpnaas.services.vpn.service_drivers.ovn_ipsec.IPsecOvnVPNDriver"
else
driver="neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver"
fi
if [ "$ipsec_package" = "libreswan" ]; then
name="openswan"
else
name="strongswan"
fi
echo "VPN:${name}:${driver}:default"
}
# Service Driver, default value depends on IPSEC_PACKAGE.
@ -31,3 +44,5 @@ NEUTRON_VPNAAS_DIR=$DEST/neutron-vpnaas
NEUTRON_VPNAAS_CONF_FILE=neutron_vpnaas.conf
NEUTRON_VPNAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_VPNAAS_CONF_FILE
OVN_VPNAGENT_CONF=$NEUTRON_CONF_DIR/neutron_ovn_vpn_agent.ini

View File

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

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_exec: IpNetnsExecFilter, ip, root
ipsec: CommandFilter, ipsec, root
sysctl_ip4_forward: RegExpFilter, sysctl, root, sysctl, -w, net.ipv4.ip_forward=1
sysctl_ip6_forward: RegExpFilter, sysctl, root, sysctl, -w, net.ipv6.conf.all.forwarding=1
rm: RegExpFilter, rm, root, rm, -rf, (.*/strongswan.d|.*/ipsec/[0-9a-z-]+)
rm_file: RegExpFilter, rm, root, rm, -f, .*/ipsec.secrets
strongswan: CommandFilter, strongswan, root

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_vpnaas.db.vpn import vpn_agentschedulers_db # noqa
from neutron_vpnaas.db.vpn import vpn_db # noqa
from neutron_vpnaas.db.vpn import vpn_ext_gw_db # noqa
def get_metadata():

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)
return self._make_vpnservice_dict(vpns_db)
def set_vpnservice_status(self, context, vpnservice_id, status,
updated_pending_status=False):
vpns = {'status': status}
with db_api.CONTEXT_WRITER.using(context):
vpns_db = self._get_resource(context, vpn_models.VPNService,
vpnservice_id)
if (utils.in_pending_status(vpns_db.status) and
not updated_pending_status):
raise vpnaas.VPNStateInvalidToUpdate(
id=vpnservice_id, state=vpns_db.status)
vpns_db.update(vpns)
return self._make_vpnservice_dict(vpns_db)
def update_vpnservice(self, context, vpnservice_id, vpnservice):
vpns = vpnservice['vpnservice']
with db_api.CONTEXT_WRITER.using(context):
@ -682,6 +695,22 @@ class VPNPluginDb(vpnaas.VPNPluginBase,
vpnservice = self._get_vpnservice(context, vpnservice_id)
return vpnservice['router_id']
@db_api.CONTEXT_READER
def get_peer_cidrs_for_router(self, context, router_id):
filters = {'router_id': [router_id]}
vpnservices = model_query.get_collection_query(
context, vpn_models.VPNService, filters=filters).all()
cidrs = []
for vpnservice in vpnservices:
for ipsec_site_connection in vpnservice.ipsec_site_connections:
if ipsec_site_connection.peer_cidrs:
for peer_cidr in ipsec_site_connection.peer_cidrs:
cidrs.append(peer_cidr.cidr)
if ipsec_site_connection.peer_ep_group is not None:
for ep in ipsec_site_connection.peer_ep_group.endpoints:
cidrs.append(ep.endpoint)
return cidrs
class VPNPluginRpcDbMixin(object):
def _build_local_subnet_cidr_map(self, context):

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 import extensions
from neutron_lib import exceptions as nexception
from neutron_lib.plugins import constants as nconstants
from neutron_lib.services import base as service_base
from neutron.api.v2 import resource_helper
from neutron_vpnaas._i18n import _
class RouteInUseByVPN(nexception.InUse):
"""Operational error indicating a route is used for VPN.
:param destinations: Destination CIDRs that are peers for VPN
"""
message = _("Route(s) to %(destinations)s are used for VPN")
class VPNGatewayNotReady(nexception.BadRequest):
message = _("VPN gateway not ready")
class VPNGatewayInError(nexception.Conflict):
message = _("VPN gateway is in ERROR state. "
"Please remove all errored VPN services and try again.")
class NoVPNAgentAvailable(nexception.ServiceUnavailable):
message = _("No VPN agent available")
class Vpnaas(extensions.APIExtensionDescriptor):
api_definition = vpn

View File

@ -10,11 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import neutron.conf.plugins.ml2.drivers.ovn.ovn_conf
import neutron.services.provider_configuration
import neutron_vpnaas.services.vpn.agent
import neutron_vpnaas.services.vpn.device_drivers.ipsec
import neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec
import neutron_vpnaas.services.vpn.ovn_agent
def list_agent_opts():
@ -31,6 +33,24 @@ def list_agent_opts():
]
def list_ovn_agent_opts():
return [
('vpnagent',
neutron_vpnaas.services.vpn.ovn_agent.VPN_AGENT_OPTS),
('ovs',
neutron_vpnaas.services.vpn.ovn_agent.OVS_OPTS),
('ovn',
neutron.conf.plugins.ml2.drivers.ovn.ovn_conf.ovn_opts),
('ipsec',
neutron_vpnaas.services.vpn.device_drivers.ipsec.ipsec_opts),
('strongswan',
neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec.
strongswan_opts),
('pluto',
neutron_vpnaas.services.vpn.device_drivers.ipsec.pluto_opts)
]
def list_opts():
return [
('service_providers',

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
# under the License.
from neutron_lib import constants
# Endpoint group types
SUBNET_ENDPOINT = 'subnet'
CIDR_ENDPOINT = 'cidr'
@ -30,3 +32,15 @@ VPN_SUPPORTED_ENDPOINT_TYPES = [
SUBNET_ENDPOINT, CIDR_ENDPOINT, VLAN_ENDPOINT,
NETWORK_ENDPOINT, ROUTER_ENDPOINT,
]
AGENT_TYPE_VPN = "VPN Agent"
DEVICE_OWNER_VPN_ROUTER_GW = constants.DEVICE_OWNER_NETWORK_PREFIX + \
"vpn_router_gateway"
DEVICE_OWNER_TRANSIT_NETWORK = constants.DEVICE_OWNER_NETWORK_PREFIX + \
"vpn_namespace"
OVN_AGENT_VPN_SB_CFG_KEY = 'neutron:ovn-vpnagent-sb-cfg'
OVN_AGENT_VPN_DESC_KEY = 'neutron:description-vpnagent'
OVN_AGENT_VPN_ID_KEY = 'neutron:ovn-vpnagent-id'

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):
return directory.get_plugin(constants.FLAVORS)
def start_rpc_listeners(self):
servers = []
for driver_name, driver in self.drivers.items():
if hasattr(driver, 'start_rpc_listeners'):
servers.extend(driver.start_rpc_listeners())
return servers
def _check_orphan_vpnservice_associations(self):
context = ncontext.get_admin_context()
vpnservices = self.get_vpnservices(context)

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,
private_subnet['id'],
router['id'])
def _setup_ipsec_site_connections_with_ep_groups(self, peer_cidr_lists):
private_subnet, router = self.create_basic_topology()
vpn_service_info = self.prepare_service_info(private_subnet=None,
router=router)
vpn_service = self.plugin.create_vpnservice(self.context,
vpn_service_info)
ike_policy = self.create_ike_policy()
ipsec_policy = self.create_ipsec_policy()
ipsec_site_connection = self.prepare_connection_info(
vpn_service['id'],
ike_policy['id'],
ipsec_policy['id'])
local_ep_group = self.create_endpoint_group(
group_type='subnet', endpoints=[private_subnet['id']])
for peer_cidrs in peer_cidr_lists:
peer_ep_group = self.create_endpoint_group(
group_type='cidr', endpoints=peer_cidrs)
ipsec_site_connection['ipsec_site_connection'].update(
{'local_ep_group_id': local_ep_group['id'],
'peer_ep_group_id': peer_ep_group['id']})
self.plugin.create_ipsec_site_connection(self.context,
ipsec_site_connection)
return private_subnet, router
def _setup_ipsec_site_connections_without_ep_groups(self, peer_cidr_lists):
private_subnet, router = self.create_basic_topology()
vpn_service_info = \
self.prepare_service_info(private_subnet=private_subnet,
router=router)
vpn_service = self.plugin.create_vpnservice(self.context,
vpn_service_info)
ike_policy = self.create_ike_policy()
ipsec_policy = self.create_ipsec_policy()
ipsec_site_connection = self.prepare_connection_info(
vpn_service['id'],
ike_policy['id'],
ipsec_policy['id'])
for peer_cidrs in peer_cidr_lists:
ipsec_site_connection['ipsec_site_connection'].update(
{'peer_cidrs': peer_cidrs})
self.plugin.create_ipsec_site_connection(self.context,
ipsec_site_connection)
return private_subnet, router
def _test_get_peer_cidrs_for_router(self, setup_func):
mock.patch.object(self.plugin, '_get_validator').start()
# create 1st setup with two connections
peer_cidrs = [
['20.1.0.0/24', '20.2.0.0/24'],
['20.3.0.0/24']
]
private_subnet, router = setup_func(peer_cidrs)
# create a 2nd setup for a different router
setup_func([['10.1.0.0/24', '10.2.0.0/24']])
returned_cidrs = self.plugin.get_peer_cidrs_for_router(self.context,
router['id'])
expected = ['20.1.0.0/24', '20.2.0.0/24', '20.3.0.0/24']
self.assertEqual(sorted(expected), sorted(returned_cidrs))
def test_get_peer_cidrs_for_router_with_ep_groups(self):
self._test_get_peer_cidrs_for_router(
self._setup_ipsec_site_connections_with_ep_groups)
def test_get_peer_cidrs_for_router_without_ep_groups(self):
self._test_get_peer_cidrs_for_router(
self._setup_ipsec_site_connections_without_ep_groups)

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