256464aea6
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
186 lines
7.1 KiB
Python
186 lines
7.1 KiB
Python
# (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
|