neutron-vpnaas/neutron_vpnaas/scheduler/vpn_agent_scheduler.py
Bodo Petermann 256464aea6 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
2023-11-16 21:08:50 +01:00

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