From 295da5e5521eb890bd0c31a730fd4a37061484a0 Mon Sep 17 00:00:00 2001 From: "vikram.choudhary" Date: Fri, 12 Feb 2016 14:16:21 +0530 Subject: [PATCH] BGP Dynamic Routing: introduce BgpDrScheduler model This patch implements a new extension called "bgp_dragentscheduler" which does instant & auto scheuling of BgpSpeakers to an active BgpDrAgent. In addition to this the patch also implements the basic CRUD requirement for binding BgpSpeakers and BgpDrAgent. BgpSpeaker to BgpDrAgent association can be 1-to-n. An admin user can only associate/disassociate BgpSpeaker to/from a BgpDRAgent. Default scheduler class will only assign non-scheduled BgpSpeaker to an active BgpDrAgent. Partially-Implements: blueprint bgp-dynamic-routing Co-Authored-By: Ryan Tidwell Co-Authored-By: Jaume Devesa Co-Authored-By: vikram.choudhary Co-Authored-By: Numan Siddique Change-Id: Id305d9a583116e155441ac5979bf3f6aa6a8258b --- etc/policy.json | 7 +- neutron/db/bgp_dragentscheduler_db.py | 177 ++++++++++++++ .../b4caf27aae4_add_bgp_dragent_model_data.py | 46 ++++ neutron/db/migration/models/head.py | 1 + neutron/extensions/bgp_dragentscheduler.py | 171 +++++++++++++ neutron/services/bgp/bgp_plugin.py | 13 +- neutron/services/bgp/common/__init__.py | 0 neutron/services/bgp/common/constants.py | 16 ++ neutron/services/bgp/scheduler/__init__.py | 0 .../bgp/scheduler/bgp_dragent_scheduler.py | 191 +++++++++++++++ neutron/tests/common/helpers.py | 25 ++ neutron/tests/etc/policy.json | 7 +- .../tests/functional/services/bgp/__init__.py | 0 .../services/bgp/scheduler/__init__.py | 0 .../scheduler/test_bgp_dragent_scheduler.py | 208 ++++++++++++++++ .../unit/db/test_bgp_dragentscheduler_db.py | 203 ++++++++++++++++ .../extensions/test_bgp_dragentscheduler.py | 224 ++++++++++++++++++ neutron/tests/unit/services/bgp/__init__.py | 0 .../unit/services/bgp/scheduler/__init__.py | 0 .../scheduler/test_bgp_dragent_scheduler.py | 224 ++++++++++++++++++ 20 files changed, 1509 insertions(+), 4 deletions(-) create mode 100644 neutron/db/bgp_dragentscheduler_db.py create mode 100644 neutron/db/migration/alembic_migrations/versions/mitaka/expand/b4caf27aae4_add_bgp_dragent_model_data.py create mode 100644 neutron/extensions/bgp_dragentscheduler.py create mode 100644 neutron/services/bgp/common/__init__.py create mode 100644 neutron/services/bgp/common/constants.py create mode 100644 neutron/services/bgp/scheduler/__init__.py create mode 100644 neutron/services/bgp/scheduler/bgp_dragent_scheduler.py create mode 100644 neutron/tests/functional/services/bgp/__init__.py create mode 100644 neutron/tests/functional/services/bgp/scheduler/__init__.py create mode 100644 neutron/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py create mode 100644 neutron/tests/unit/db/test_bgp_dragentscheduler_db.py create mode 100644 neutron/tests/unit/extensions/test_bgp_dragentscheduler.py create mode 100644 neutron/tests/unit/services/bgp/__init__.py create mode 100644 neutron/tests/unit/services/bgp/scheduler/__init__.py create mode 100644 neutron/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py diff --git a/etc/policy.json b/etc/policy.json index 9c3e314d..3817e599 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -223,5 +223,10 @@ "add_gateway_network": "rule:admin_only", "remove_gateway_network": "rule:admin_only", - "get_advertised_routes":"rule:admin_only" + "get_advertised_routes":"rule:admin_only", + + "add_bgp_speaker_to_dragent": "rule:admin_only", + "remove_bgp_speaker_from_dragent": "rule:admin_only", + "list_bgp_speaker_on_dragent": "rule:admin_only", + "list_dragent_hosting_bgp_speaker": "rule:admin_only" } diff --git a/neutron/db/bgp_dragentscheduler_db.py b/neutron/db/bgp_dragentscheduler_db.py new file mode 100644 index 00000000..19d953c3 --- /dev/null +++ b/neutron/db/bgp_dragentscheduler_db.py @@ -0,0 +1,177 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_log import log as logging +import sqlalchemy as sa +from sqlalchemy import orm + +from neutron._i18n import _ +from neutron._i18n import _LW +from neutron.db import agents_db +from neutron.db import agentschedulers_db as as_db +from neutron.db import model_base +from neutron.extensions import bgp_dragentscheduler as bgp_dras_ext +from neutron.services.bgp.common import constants as bgp_consts + + +LOG = logging.getLogger(__name__) + + +BGP_DRAGENT_SCHEDULER_OPTS = [ + cfg.StrOpt( + 'bgp_drscheduler_driver', + default='neutron.services.bgp.scheduler' + '.bgp_dragent_scheduler.ChanceScheduler', + help=_('Driver used for scheduling BGP speakers to BGP DrAgent')), +] + +cfg.CONF.register_opts(BGP_DRAGENT_SCHEDULER_OPTS) + + +class BgpSpeakerDrAgentBinding(model_base.BASEV2): + """Represents a mapping between BGP speaker and BGP DRAgent""" + + __tablename__ = 'bgp_speaker_dragent_bindings' + + bgp_speaker_id = sa.Column(sa.String(length=36), + sa.ForeignKey("bgp_speakers.id", + ondelete='CASCADE'), + nullable=False) + dragent = orm.relation(agents_db.Agent) + agent_id = sa.Column(sa.String(length=36), + sa.ForeignKey("agents.id", + ondelete='CASCADE'), + primary_key=True) + + +class BgpDrAgentSchedulerDbMixin(bgp_dras_ext.BgpDrSchedulerPluginBase, + as_db.AgentSchedulerDbMixin): + + bgp_drscheduler = None + + def schedule_unscheduled_bgp_speakers(self, context, host): + if self.bgp_drscheduler: + return self.bgp_drscheduler.schedule_unscheduled_bgp_speakers( + context, host) + else: + LOG.warning(_LW("Cannot schedule BgpSpeaker to DrAgent. " + "Reason: No scheduler registered.")) + + def schedule_bgp_speaker(self, context, created_bgp_speaker): + if self.bgp_drscheduler: + self.bgp_drscheduler.schedule(context, created_bgp_speaker) + else: + LOG.warning(_LW("Cannot schedule BgpSpeaker to DrAgent. " + "Reason: No scheduler registered.")) + + def add_bgp_speaker_to_dragent(self, context, agent_id, speaker_id): + """Associate a BgpDrAgent with a BgpSpeaker.""" + try: + self._save_bgp_speaker_dragent_binding(context, + agent_id, + speaker_id) + except db_exc.DBDuplicateEntry: + raise bgp_dras_ext.DrAgentAssociationError( + agent_id=agent_id) + + LOG.debug('BgpSpeaker %(bgp_speaker_id)s added to ' + 'BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': speaker_id, 'agent_id': agent_id}) + + def _save_bgp_speaker_dragent_binding(self, context, + agent_id, speaker_id): + with context.session.begin(subtransactions=True): + agent_db = self._get_agent(context, agent_id) + agent_up = agent_db['admin_state_up'] + is_agent_bgp = (agent_db['agent_type'] == + bgp_consts.AGENT_TYPE_BGP_ROUTING) + if not is_agent_bgp or not agent_up: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + binding = BgpSpeakerDrAgentBinding() + binding.bgp_speaker_id = speaker_id + binding.agent_id = agent_id + context.session.add(binding) + + def remove_bgp_speaker_from_dragent(self, context, agent_id, speaker_id): + with context.session.begin(subtransactions=True): + agent_db = self._get_agent(context, agent_id) + is_agent_bgp = (agent_db['agent_type'] == + bgp_consts.AGENT_TYPE_BGP_ROUTING) + if not is_agent_bgp: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.filter_by(bgp_speaker_id=speaker_id, + agent_id=agent_id) + + num_deleted = query.delete() + if not num_deleted: + raise bgp_dras_ext.DrAgentNotHostingBgpSpeaker( + bgp_speaker_id=speaker_id, + agent_id=agent_id) + LOG.debug('BgpSpeaker %(bgp_speaker_id)s removed from ' + 'BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': speaker_id, + 'agent_id': agent_id}) + + def get_dragents_hosting_bgp_speakers(self, context, bgp_speaker_ids, + active=None, admin_state_up=None): + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.options(orm.contains_eager( + BgpSpeakerDrAgentBinding.dragent)) + query = query.join(BgpSpeakerDrAgentBinding.dragent) + + if len(bgp_speaker_ids) == 1: + query = query.filter( + BgpSpeakerDrAgentBinding.bgp_speaker_id == ( + bgp_speaker_ids[0])) + elif bgp_speaker_ids: + query = query.filter( + BgpSpeakerDrAgentBinding.bgp_speaker_id in bgp_speaker_ids) + if admin_state_up is not None: + query = query.filter(agents_db.Agent.admin_state_up == + admin_state_up) + + return [binding.dragent + for binding in query + if as_db.AgentSchedulerDbMixin.is_eligible_agent( + active, binding.dragent)] + + def get_dragent_bgp_speaker_bindings(self, context): + return context.session.query(BgpSpeakerDrAgentBinding).all() + + def list_dragent_hosting_bgp_speaker(self, context, speaker_id): + dragents = self.get_dragents_hosting_bgp_speakers(context, + [speaker_id]) + agent_ids = [dragent.id for dragent in dragents] + if not agent_ids: + return {'agents': []} + return {'agents': self.get_agents(context, filters={'id': agent_ids})} + + def list_bgp_speaker_on_dragent(self, context, agent_id): + query = context.session.query(BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = query.filter_by(agent_id=agent_id) + + bgp_speaker_ids = [item[0] for item in query] + if not bgp_speaker_ids: + # Exception will be thrown if the requested agent does not exist. + self._get_agent(context, agent_id) + return {'bgp_speakers': []} + return {'bgp_speakers': + self.get_bgp_speakers(context, + filters={'id': bgp_speaker_ids})} diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/b4caf27aae4_add_bgp_dragent_model_data.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/b4caf27aae4_add_bgp_dragent_model_data.py new file mode 100644 index 00000000..0959b521 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/b4caf27aae4_add_bgp_dragent_model_data.py @@ -0,0 +1,46 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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_bgp_dragent_model_data + +Revision ID: b4caf27aae4 +Revises: 15be7321482 +Create Date: 2015-08-20 17:05:31.038704 + +""" + +# revision identifiers, used by Alembic. +revision = 'b4caf27aae4' +down_revision = '15be73214821' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + + op.create_table( + 'bgp_speaker_dragent_bindings', + sa.Column('agent_id', + sa.String(length=36), + primary_key=True), + sa.Column('bgp_speaker_id', + sa.String(length=36), + nullable=False), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['bgp_speaker_id'], ['bgp_speakers.id'], + ondelete='CASCADE'), + ) diff --git a/neutron/db/migration/models/head.py b/neutron/db/migration/models/head.py index addeb288..577aaeb2 100644 --- a/neutron/db/migration/models/head.py +++ b/neutron/db/migration/models/head.py @@ -26,6 +26,7 @@ from neutron.db import agents_db # noqa from neutron.db import agentschedulers_db # noqa from neutron.db import allowedaddresspairs_db # noqa from neutron.db import bgp_db # noqa +from neutron.db import bgp_dragentscheduler_db # noqa from neutron.db import dns_db # noqa from neutron.db import dvr_mac_db # noqa from neutron.db import external_net_db # noqa diff --git a/neutron/extensions/bgp_dragentscheduler.py b/neutron/extensions/bgp_dragentscheduler.py new file mode 100644 index 00000000..496bd29c --- /dev/null +++ b/neutron/extensions/bgp_dragentscheduler.py @@ -0,0 +1,171 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 six +import webob + +from oslo_log import log as logging + +from neutron.api import extensions +from neutron.api.v2 import base +from neutron.api.v2 import resource +from neutron.common import exceptions +from neutron.extensions import agent +from neutron.extensions import bgp as bgp_ext +from neutron._i18n import _, _LE +from neutron import manager +from neutron import wsgi + + +LOG = logging.getLogger(__name__) + +BGP_DRAGENT_SCHEDULER_EXT_ALIAS = 'bgp_dragent_scheduler' +BGP_DRINSTANCE = 'bgp-drinstance' +BGP_DRINSTANCES = BGP_DRINSTANCE + 's' +BGP_DRAGENT = 'bgp-dragent' +BGP_DRAGENTS = BGP_DRAGENT + 's' + + +class DrAgentInvalid(agent.AgentNotFound): + message = _("BgpDrAgent %(id)s is invalid or has been disabled.") + + +class DrAgentNotHostingBgpSpeaker(exceptions.NotFound): + message = _("BGP speaker %(bgp_speaker_id)s is not hosted " + "by the BgpDrAgent %(agent_id)s.") + + +class DrAgentAssociationError(exceptions.Conflict): + message = _("BgpDrAgent %(agent_id)s is already associated " + "to a BGP speaker.") + + +class BgpDrSchedulerController(wsgi.Controller): + """Schedule BgpSpeaker for a BgpDrAgent""" + def get_plugin(self): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + if not plugin: + LOG.error(_LE('No plugin for BGP routing registered')) + msg = _('The resource could not be found.') + raise webob.exc.HTTPNotFound(msg) + return plugin + + def index(self, request, **kwargs): + plugin = self.get_plugin() + return plugin.list_bgp_speaker_on_dragent( + request.context, kwargs['agent_id']) + + def create(self, request, body, **kwargs): + plugin = self.get_plugin() + return plugin.add_bgp_speaker_to_dragent( + request.context, + kwargs['agent_id'], + body['bgp_speaker_id']) + + def delete(self, request, id, **kwargs): + plugin = self.get_plugin() + return plugin.remove_bgp_speaker_from_dragent( + request.context, kwargs['agent_id'], id) + + +class BgpDrAgentController(wsgi.Controller): + def get_plugin(self): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + if not plugin: + LOG.error(_LE('No plugin for BGP routing registered')) + msg = _LE('The resource could not be found.') + raise webob.exc.HTTPNotFound(msg) + return plugin + + def index(self, request, **kwargs): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + return plugin.list_dragent_hosting_bgp_speaker( + request.context, kwargs['bgp_speaker_id']) + + +class Bgp_dragentscheduler(extensions.ExtensionDescriptor): + """Extension class supporting Dynamic Routing scheduler. + """ + @classmethod + def get_name(cls): + return "BGP Dynamic Routing Agent Scheduler" + + @classmethod + def get_alias(cls): + return BGP_DRAGENT_SCHEDULER_EXT_ALIAS + + @classmethod + def get_description(cls): + return "Schedules BgpSpeakers on BgpDrAgent" + + @classmethod + def get_updated(cls): + return "2015-07-30T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + exts = [] + parent = dict(member_name="agent", + collection_name="agents") + + controller = resource.Resource(BgpDrSchedulerController(), + base.FAULT_MAP) + exts.append(extensions.ResourceExtension(BGP_DRINSTANCES, + controller, parent)) + + parent = dict(member_name="bgp_speaker", + collection_name="bgp-speakers") + controller = resource.Resource(BgpDrAgentController(), + base.FAULT_MAP) + exts.append(extensions.ResourceExtension(BGP_DRAGENTS, + controller, parent)) + return exts + + def get_extended_resources(self, version): + return {} + + +@six.add_metaclass(abc.ABCMeta) +class BgpDrSchedulerPluginBase(object): + """REST API to operate BGP dynamic routing agent scheduler. + + All the methods must be executed in admin context. + """ + def get_plugin_description(self): + return "Neutron BGP dynamic routing scheduler Plugin" + + def get_plugin_type(self): + return bgp_ext.BGP_EXT_ALIAS + + @abc.abstractmethod + def add_bgp_speaker_to_dragent(self, context, agent_id, speaker_id): + pass + + @abc.abstractmethod + def remove_bgp_speaker_from_dragent(self, context, agent_id, speaker_id): + pass + + @abc.abstractmethod + def list_dragent_hosting_bgp_speaker(self, context, speaker_id): + pass + + @abc.abstractmethod + def list_bgp_speaker_on_dragent(self, context, agent_id): + pass diff --git a/neutron/services/bgp/bgp_plugin.py b/neutron/services/bgp/bgp_plugin.py index 7d0ff20b..69a174fd 100644 --- a/neutron/services/bgp/bgp_plugin.py +++ b/neutron/services/bgp/bgp_plugin.py @@ -12,20 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_config import cfg +from oslo_utils import importutils + from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db from neutron.extensions import bgp as bgp_ext +from neutron.extensions import bgp_dragentscheduler as dras_ext from neutron.services import service_base PLUGIN_NAME = bgp_ext.BGP_EXT_ALIAS + '_svc_plugin' class BgpPlugin(service_base.ServicePluginBase, - bgp_db.BgpDbMixin): + bgp_db.BgpDbMixin, + bgp_dragentscheduler_db.BgpDrAgentSchedulerDbMixin): - supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS] + supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS, + dras_ext.BGP_DRAGENT_SCHEDULER_EXT_ALIAS] def __init__(self): super(BgpPlugin, self).__init__() + self.bgp_drscheduler = importutils.import_object( + cfg.CONF.bgp_drscheduler_driver) def get_plugin_name(self): return PLUGIN_NAME diff --git a/neutron/services/bgp/common/__init__.py b/neutron/services/bgp/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/services/bgp/common/constants.py b/neutron/services/bgp/common/constants.py new file mode 100644 index 00000000..b18b0ef4 --- /dev/null +++ b/neutron/services/bgp/common/constants.py @@ -0,0 +1,16 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +AGENT_TYPE_BGP_ROUTING = 'BGP dynamic routing agent' diff --git a/neutron/services/bgp/scheduler/__init__.py b/neutron/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/services/bgp/scheduler/bgp_dragent_scheduler.py b/neutron/services/bgp/scheduler/bgp_dragent_scheduler.py new file mode 100644 index 00000000..27e27585 --- /dev/null +++ b/neutron/services/bgp/scheduler/bgp_dragent_scheduler.py @@ -0,0 +1,191 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 oslo_db import exception as db_exc +from oslo_log import log as logging +from sqlalchemy.orm import exc +from sqlalchemy import sql + +from neutron.db import agents_db +from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron._i18n import _LI, _LW +from neutron.scheduler import base_resource_filter +from neutron.scheduler import base_scheduler +from neutron.services.bgp.common import constants as bgp_consts + +LOG = logging.getLogger(__name__) +BGP_SPEAKER_PER_DRAGENT = 1 + + +class BgpDrAgentFilter(base_resource_filter.BaseResourceFilter): + + def bind(self, context, agents, bgp_speaker_id): + """Bind the BgpSpeaker to a BgpDrAgent.""" + bound_agents = agents[:] + for agent in agents: + # saving agent_id to use it after rollback to avoid + # DetachedInstanceError + agent_id = agent.id + binding = bgp_dras_db.BgpSpeakerDrAgentBinding() + binding.agent_id = agent_id + binding.bgp_speaker_id = bgp_speaker_id + try: + with context.session.begin(subtransactions=True): + context.session.add(binding) + except db_exc.DBDuplicateEntry: + # it's totally ok, someone just did our job! + bound_agents.remove(agent) + LOG.info(_LI('BgpDrAgent %s already present'), agent_id) + LOG.debug('BgpSpeaker %(bgp_speaker_id)s is scheduled to be ' + 'hosted by BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': bgp_speaker_id, + 'agent_id': agent_id}) + super(BgpDrAgentFilter, self).bind(context, bound_agents, + bgp_speaker_id) + + def filter_agents(self, plugin, context, bgp_speaker): + """Return the agents that can host the BgpSpeaker.""" + agents_dict = self._get_bgp_speaker_hostable_dragents( + plugin, context, bgp_speaker) + if not agents_dict['hostable_agents'] or agents_dict['n_agents'] <= 0: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + return agents_dict + + def _get_active_dragents(self, plugin, context): + """Return a list of active BgpDrAgents.""" + with context.session.begin(subtransactions=True): + active_dragents = plugin.get_agents_db( + context, filters={ + 'agent_type': [bgp_consts.AGENT_TYPE_BGP_ROUTING], + 'admin_state_up': [True]}) + if not active_dragents: + return [] + return active_dragents + + def _get_num_dragents_hosting_bgp_speaker(self, bgp_speaker_id, + dragent_bindings): + return sum(1 if dragent_binding.bgp_speaker_id == bgp_speaker_id else 0 + for dragent_binding in dragent_bindings) + + def _get_bgp_speaker_hostable_dragents(self, plugin, context, bgp_speaker): + """Return number of additional BgpDrAgents which will actually host + the given BgpSpeaker and a list of BgpDrAgents which can host the + given BgpSpeaker + """ + # only one BgpSpeaker can be hosted by a BgpDrAgent for now. + dragents_per_bgp_speaker = BGP_SPEAKER_PER_DRAGENT + dragent_bindings = plugin.get_dragent_bgp_speaker_bindings(context) + agents_hosting = [dragent_binding.agent_id + for dragent_binding in dragent_bindings] + + num_dragents_hosting_bgp_speaker = ( + self._get_num_dragents_hosting_bgp_speaker(bgp_speaker['id'], + dragent_bindings)) + n_agents = dragents_per_bgp_speaker - num_dragents_hosting_bgp_speaker + if n_agents <= 0: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + + active_dragents = self._get_active_dragents(plugin, context) + hostable_dragents = [ + agent for agent in set(active_dragents) + if agent.id not in agents_hosting and plugin.is_eligible_agent( + active=True, agent=agent) + ] + if not hostable_dragents: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + + n_agents = min(len(hostable_dragents), n_agents) + return {'n_agents': n_agents, + 'hostable_agents': hostable_dragents, + 'hosted_agents': num_dragents_hosting_bgp_speaker} + + +class BgpDrAgentSchedulerBase(BgpDrAgentFilter): + + def schedule_unscheduled_bgp_speakers(self, context, host): + """Schedule unscheduled BgpSpeaker to a BgpDrAgent. + """ + + LOG.debug('Started auto-scheduling on host %s', host) + with context.session.begin(subtransactions=True): + query = context.session.query(agents_db.Agent) + query = query.filter_by( + agent_type=bgp_consts.AGENT_TYPE_BGP_ROUTING, + host=host, + admin_state_up=sql.true()) + try: + bgp_dragent = query.one() + except (exc.NoResultFound): + LOG.debug('No enabled BgpDrAgent on host %s', host) + return False + + if agents_db.AgentDbMixin.is_agent_down( + bgp_dragent.heartbeat_timestamp): + LOG.warn(_LW('BgpDrAgent %s is down'), bgp_dragent.id) + return False + + if self._is_bgp_speaker_hosted(context, bgp_dragent['id']): + # One BgpDrAgent can only host one BGP speaker + LOG.debug('BgpDrAgent already hosting a speaker on host %s. ' + 'Cannot schedule an another one', host) + return False + + unscheduled_speakers = self._get_unscheduled_bgp_speakers(context) + if not unscheduled_speakers: + LOG.debug('Nothing to auto-schedule on host %s', host) + return False + + self.bind(context, [bgp_dragent], unscheduled_speakers[0]) + return True + + def _is_bgp_speaker_hosted(self, context, agent_id): + speaker_binding_model = bgp_dras_db.BgpSpeakerDrAgentBinding + + query = context.session.query(speaker_binding_model) + query = query.filter(speaker_binding_model.agent_id == agent_id) + + return query.count() > 0 + + def _get_unscheduled_bgp_speakers(self, context): + """BGP speakers that needs to be scheduled. + """ + + no_agent_binding = ~sql.exists().where( + bgp_db.BgpSpeaker.id == + bgp_dras_db.BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = context.session.query(bgp_db.BgpSpeaker.id).filter( + no_agent_binding) + return [bgp_speaker_id_[0] for bgp_speaker_id_ in query] + + +class ChanceScheduler(base_scheduler.BaseChanceScheduler, + BgpDrAgentSchedulerBase): + + def __init__(self): + super(ChanceScheduler, self).__init__(self) + + +class WeightScheduler(base_scheduler.BaseWeightScheduler, + BgpDrAgentSchedulerBase): + + def __init__(self): + super(WeightScheduler, self).__init__(self) diff --git a/neutron/tests/common/helpers.py b/neutron/tests/common/helpers.py index b1444546..2a997ea3 100644 --- a/neutron/tests/common/helpers.py +++ b/neutron/tests/common/helpers.py @@ -25,6 +25,7 @@ from neutron.common import topics from neutron import context from neutron.db import agents_db from neutron.db import common_db_mixin +from neutron.services.bgp.common import constants as bgp_const HOST = 'localhost' DEFAULT_AZ = 'nova' @@ -107,6 +108,30 @@ def register_dhcp_agent(host=HOST, networks=0, admin_state_up=True, context.get_admin_context(), agent['agent_type'], agent['host']) +def _get_bgp_dragent_dict(host): + agent = { + 'binary': 'neutron-bgp-dragent', + 'host': host, + 'topic': 'q-bgp_dragent', + 'agent_type': bgp_const.AGENT_TYPE_BGP_ROUTING, + 'configurations': {'bgp_speakers': 1}} + return agent + + +def register_bgp_dragent(host=HOST, admin_state_up=True, + alive=True): + agent = _register_agent( + _get_bgp_dragent_dict(host)) + + if not admin_state_up: + set_agent_admin_state(agent['id']) + if not alive: + kill_agent(agent['id']) + + return FakePlugin()._get_agent_by_type_and_host( + context.get_admin_context(), agent['agent_type'], agent['host']) + + def kill_agent(agent_id): hour_ago = timeutils.utcnow() - datetime.timedelta(hours=1) FakePlugin().update_agent( diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 9c3e314d..3817e599 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -223,5 +223,10 @@ "add_gateway_network": "rule:admin_only", "remove_gateway_network": "rule:admin_only", - "get_advertised_routes":"rule:admin_only" + "get_advertised_routes":"rule:admin_only", + + "add_bgp_speaker_to_dragent": "rule:admin_only", + "remove_bgp_speaker_from_dragent": "rule:admin_only", + "list_bgp_speaker_on_dragent": "rule:admin_only", + "list_dragent_hosting_bgp_speaker": "rule:admin_only" } diff --git a/neutron/tests/functional/services/bgp/__init__.py b/neutron/tests/functional/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/tests/functional/services/bgp/scheduler/__init__.py b/neutron/tests/functional/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py b/neutron/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py new file mode 100644 index 00000000..5b87c7a4 --- /dev/null +++ b/neutron/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py @@ -0,0 +1,208 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 testscenarios + +from neutron import context +from neutron.db import agents_db +from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron.db import common_db_mixin +from neutron.services.bgp.scheduler import bgp_dragent_scheduler as bgp_dras +from neutron.tests.common import helpers +from neutron.tests.unit import testlib_api + +# Required to generate tests from scenarios. Not compatible with nose. +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestAutoSchedule(testlib_api.SqlTestCase, + bgp_dras_db.BgpDrAgentSchedulerDbMixin, + agents_db.AgentDbMixin, + common_db_mixin.CommonDbMixin): + """Test various scenarios for schedule_unscheduled_bgp_speakers. + + Below is the brief description of the scenario variables + -------------------------------------------------------- + host_count + number of hosts. + + agent_count + number of BGP dynamic routing agents. + + down_agent_count + number of DRAgents which are inactive. + + bgp_speaker_count + Number of bgp_speakers. + + hosted_bgp_speakers + A mapping of agent id to the ids of the bgp_speakers that they + should be initially hosting. + + expected_schedule_return_value + Expected return value of 'schedule_unscheduled_bgp_speakers'. + + expected_hosted_bgp_speakers + This stores the expected bgp_speakers that should have been + scheduled (or that could have already been scheduled) for each + agent after the 'schedule_unscheduled_bgp_speakers' function is + called. + """ + + scenarios = [ + ('No BgpDrAgent scheduled, if no DRAgent is present', + dict(host_count=1, + agent_count=0, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=False)), + + ('No BgpDrAgent scheduled, if no BGP speaker are present', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=0, + hosted_bgp_speakers={}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': []})), + + ('No BgpDrAgent scheduled, if BGP speaker already hosted', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']})), + + ('BgpDrAgent scheduled to the speaker, if the speaker is not hosted', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=True, + expected_hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']})), + + ('No BgpDrAgent scheduled, if all the agents are down', + dict(host_count=2, + agent_count=2, + down_agent_count=2, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': [], + 'agent-1': [], })), + ] + + def _strip_host_index(self, name): + """Strips the host index. + + Eg. if name = '2-agent-3', then 'agent-3' is returned. + """ + return name[name.find('-') + 1:] + + def _extract_index(self, name): + """Extracts the index number and returns. + + Eg. if name = '2-agent-3', then 3 is returned + """ + return int(name.split('-')[-1]) + + def _get_hosted_bgp_speakers_on_dragent(self, agent_id): + query = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = query.filter( + bgp_dras_db.BgpSpeakerDrAgentBinding.agent_id == + agent_id) + + return [item[0] for item in query] + + def _create_and_set_agents_down(self, hosts, agent_count=0, + down_agent_count=0, admin_state_up=True): + agents = [] + if agent_count: + for i, host in enumerate(hosts): + is_alive = i >= down_agent_count + agents.append(helpers.register_bgp_dragent( + host, + admin_state_up=admin_state_up, + alive=is_alive)) + return agents + + def _save_bgp_speakers(self, bgp_speakers): + cls = bgp_db.BgpDbMixin() + bgp_speaker_body = { + 'bgp_speaker': {'name': 'fake_bgp_speaker', + 'ip_version': '4', + 'local_as': '123', + 'advertise_floating_ip_host_routes': '0', + 'advertise_tenant_networks': '0', + 'peers': [], + 'networks': []}} + i = 1 + for bgp_speaker_id in bgp_speakers: + bgp_speaker_body['bgp_speaker']['local_as'] = i + cls._save_bgp_speaker(self.ctx, bgp_speaker_body, + uuid=bgp_speaker_id) + i = i + 1 + + def _test_auto_schedule(self, host_index): + scheduler = bgp_dras.ChanceScheduler() + self.ctx = context.get_admin_context() + msg = 'host_index = %s' % host_index + + # create hosts + hosts = ['%s-agent-%s' % (host_index, i) + for i in range(self.host_count)] + bgp_dragents = self._create_and_set_agents_down(hosts, + self.agent_count, + self.down_agent_count) + + # create bgp_speakers + self._bgp_speakers = ['%s-bgp-speaker-%s' % (host_index, i) + for i in range(self.bgp_speaker_count)] + self._save_bgp_speakers(self._bgp_speakers) + + # pre schedule the bgp_speakers to the agents defined in + # self.hosted_bgp_speakers before calling auto_schedule_bgp_speaker + for agent, bgp_speakers in self.hosted_bgp_speakers.items(): + agent_index = self._extract_index(agent) + for bgp_speaker in bgp_speakers: + bs_index = self._extract_index(bgp_speaker) + scheduler.bind(self.ctx, [bgp_dragents[agent_index]], + self._bgp_speakers[bs_index]) + + retval = scheduler.schedule_unscheduled_bgp_speakers(self.ctx, + hosts[host_index]) + self.assertEqual(self.expected_schedule_return_value, retval, + message=msg) + + if self.agent_count: + agent_id = bgp_dragents[host_index].id + hosted_bgp_speakers = self._get_hosted_bgp_speakers_on_dragent( + agent_id) + hosted_bs_ids = [self._strip_host_index(net) + for net in hosted_bgp_speakers] + expected_hosted_bgp_speakers = self.expected_hosted_bgp_speakers[ + 'agent-%s' % host_index] + self.assertItemsEqual(hosted_bs_ids, expected_hosted_bgp_speakers, + msg) + + def test_auto_schedule(self): + for i in range(self.host_count): + self._test_auto_schedule(i) diff --git a/neutron/tests/unit/db/test_bgp_dragentscheduler_db.py b/neutron/tests/unit/db/test_bgp_dragentscheduler_db.py new file mode 100644 index 00000000..18b2cf54 --- /dev/null +++ b/neutron/tests/unit/db/test_bgp_dragentscheduler_db.py @@ -0,0 +1,203 @@ +# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P. +# +# 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 oslo_config import cfg +from oslo_utils import importutils + +from neutron.api.v2 import attributes +from neutron import context +from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron.extensions import agent +from neutron.extensions import bgp +from neutron.extensions import bgp_dragentscheduler as bgp_dras_ext +from neutron import manager +from neutron.tests.unit.db import test_bgp_db +from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base_plugin +from neutron.tests.unit.extensions import test_agent + +from webob import exc + + +class BgpDrSchedulerTestExtensionManager(object): + + def get_resources(self): + attributes.RESOURCE_ATTRIBUTE_MAP.update( + agent.RESOURCE_ATTRIBUTE_MAP) + resources = agent.Agent.get_resources() + resources.extend(bgp_dras_ext.Bgp_dragentscheduler.get_resources()) + return resources + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class TestBgpDrSchedulerPlugin(bgp_db.BgpDbMixin, + bgp_dras_db.BgpDrAgentSchedulerDbMixin): + + bgp_drscheduler = importutils.import_object( + cfg.CONF.bgp_drscheduler_driver) + + supported_extension_aliases = ["bgp_dragent_scheduler"] + + def get_plugin_description(self): + return ("BGP dynamic routing service Plugin test class that test " + "BGP speaker functionality, with scheduler.") + + +class BgpDrSchedulingTestCase(test_agent.AgentDBTestMixIn, + test_bgp_db.BgpEntityCreationMixin): + + def test_schedule_bgp_speaker(self): + """Test happy path over full scheduling cycle.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + self._register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + agent_id = agent['id'] + + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request('agents', data, self.fmt, + agent_id, 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + req_show = self.new_show_request('agents', agent_id, self.fmt, + 'bgp-drinstances') + res = req_show.get_response(self.ext_api) + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(self.fmt, res) + self.assertIn('bgp_speakers', res) + self.assertTrue(bgp_speaker_id, + res['bgp_speakers'][0]['id']) + + req = self.new_delete_request('agents', + agent_id, + self.fmt, + 'bgp-drinstances', + bgp_speaker_id) + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPNoContent.code, res.status_int) + + res = req_show.get_response(self.ext_api) + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(self.fmt, res) + self.assertIn('bgp_speakers', res) + self.assertEqual([], res['bgp_speakers']) + + def test_schedule_bgp_speaker_on_invalid_agent(self): + """Test error while scheduling BGP speaker on an invalid agent.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + self._register_l3_agent(host='host1') # Register wrong agent + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + + # Raises an AgentNotFound exception if the agent is invalid + self.assertEqual(exc.HTTPNotFound.code, res.status_int) + + def test_schedule_bgp_speaker_twice_on_same_agent(self): + """Test error if a BGP speaker is scheduled twice on same agent""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + self._register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + # Try second time, should raise conflict + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPConflict.code, res.status_int) + + def test_schedule_bgp_speaker_on_two_different_agents(self): + """Test that a BGP speaker can be associated to two agents.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + self._register_bgp_dragent(host='host1') + self._register_bgp_dragent(host='host2') + data = {'bgp_speaker_id': bgp_speaker_id} + + agent1 = self._list('agents')['agents'][0] + req = self.new_create_request( + 'agents', data, self.fmt, + agent1['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + agent2 = self._list('agents')['agents'][1] + req = self.new_create_request( + 'agents', data, self.fmt, + agent2['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + def test_schedule_multi_bgp_speaker_on_one_dragent(self): + """Test only one BGP speaker can be associated to one dragent.""" + with self.bgp_speaker(4, 1) as ri1, self.bgp_speaker(4, 2) as ri2: + self._register_bgp_dragent(host='host1') + + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': ri1['id']} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + data = {'bgp_speaker_id': ri2['id']} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPConflict.code, res.status_int) + + def test_non_scheduled_bgp_speaker_binding_removal(self): + """Test exception while removing an invalid binding.""" + with self.bgp_speaker(4, 1234) as ri1: + self._register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + agent_id = agent['id'] + self.assertRaises(bgp_dras_ext.DrAgentNotHostingBgpSpeaker, + self.bgp_plugin.remove_bgp_speaker_from_dragent, + self.context, agent_id, ri1['id']) + + +class BgpDrPluginSchedulerTests(test_db_base_plugin.NeutronDbPluginV2TestCase, + BgpDrSchedulingTestCase): + + def setUp(self, plugin=None, ext_mgr=None, service_plugins=None): + if not plugin: + plugin = ('neutron.tests.unit.db.' + 'test_bgp_dragentscheduler_db.TestBgpDrSchedulerPlugin') + if not service_plugins: + service_plugins = {bgp.BGP_EXT_ALIAS: + 'neutron.services.bgp.bgp_plugin.BgpPlugin'} + + ext_mgr = ext_mgr or BgpDrSchedulerTestExtensionManager() + super(BgpDrPluginSchedulerTests, self).setUp( + plugin=plugin, ext_mgr=ext_mgr, service_plugins=service_plugins) + self.bgp_plugin = manager.NeutronManager.get_service_plugins().get( + bgp.BGP_EXT_ALIAS) + self.context = context.get_admin_context() diff --git a/neutron/tests/unit/extensions/test_bgp_dragentscheduler.py b/neutron/tests/unit/extensions/test_bgp_dragentscheduler.py new file mode 100644 index 00000000..3e3fbc7a --- /dev/null +++ b/neutron/tests/unit/extensions/test_bgp_dragentscheduler.py @@ -0,0 +1,224 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 testscenarios + +from oslo_utils import importutils + +from neutron import context +from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron.services.bgp.scheduler import bgp_dragent_scheduler as bgp_dras +from neutron.tests.common import helpers +from neutron.tests.unit import testlib_api + +# Required to generate tests from scenarios. Not compatible with nose. +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestBgpDrAgentSchedulerBaseTestCase(testlib_api.SqlTestCase): + + def setUp(self): + super(TestBgpDrAgentSchedulerBaseTestCase, self).setUp() + self.ctx = context.get_admin_context() + self.bgp_speaker = {'id': 'foo_bgp_speaker_id'} + self.bgp_speaker_id = 'foo_bgp_speaker_id' + self._save_bgp_speaker(self.bgp_speaker_id) + + def _create_and_set_agents_down(self, hosts, down_agent_count=0, + admin_state_up=True): + agents = [] + for i, host in enumerate(hosts): + is_alive = i >= down_agent_count + agents.append(helpers.register_bgp_dragent( + host, + admin_state_up=admin_state_up, + alive=is_alive)) + return agents + + def _save_bgp_speaker(self, bgp_speaker_id): + cls = bgp_db.BgpDbMixin() + bgp_speaker_body = {'bgp_speaker': {'ip_version': '4', + 'name': 'test-speaker', + 'local_as': '123', + 'advertise_floating_ip_host_routes': '0', + 'advertise_tenant_networks': '0', + 'peers': [], + 'networks': []}} + cls._save_bgp_speaker(self.ctx, bgp_speaker_body, uuid=bgp_speaker_id) + + def _test_schedule_bind_bgp_speaker(self, agents, bgp_speaker_id): + scheduler = bgp_dras.ChanceScheduler() + scheduler.resource_filter.bind(self.ctx, agents, bgp_speaker_id) + results = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).filter_by( + bgp_speaker_id=bgp_speaker_id).all() + + for result in results: + self.assertEqual(bgp_speaker_id, result.bgp_speaker_id) + + +class TestBgpDrAgentScheduler(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin): + + def test_schedule_bind_bgp_speaker_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + def test_schedule_bind_bgp_speaker_multi_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + +class TestBgpAgentFilter(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin, + bgp_dras_db.BgpDrAgentSchedulerDbMixin): + + def setUp(self): + super(TestBgpAgentFilter, self).setUp() + self.bgp_drscheduler = importutils.import_object( + 'neutron.services.bgp.scheduler' + '.bgp_dragent_scheduler.ChanceScheduler' + ) + self.plugin = self + + def _test_filter_agents_helper(self, bgp_speaker, + expected_filtered_dragent_ids=None, + expected_num_agents=1): + if not expected_filtered_dragent_ids: + expected_filtered_dragent_ids = [] + + filtered_agents = ( + self.plugin.bgp_drscheduler.resource_filter.filter_agents( + self.plugin, self.ctx, bgp_speaker)) + self.assertEqual(expected_num_agents, + filtered_agents['n_agents']) + actual_filtered_dragent_ids = [ + agent.id for agent in filtered_agents['hostable_agents']] + self.assertEqual(len(expected_filtered_dragent_ids), + len(actual_filtered_dragent_ids)) + for filtered_agent_id in actual_filtered_dragent_ids: + self.assertIn(filtered_agent_id, expected_filtered_dragent_ids) + + def test_filter_agents_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + expected_filtered_dragent_ids = [agents[0].id] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_no_agents(self): + expected_filtered_dragent_ids = [] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids, + expected_num_agents=0) + + def test_filter_agents_two_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + expected_filtered_dragent_ids = [agent.id for agent in agents] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_agent_already_scheduled(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + self._test_filter_agents_helper(self.bgp_speaker, + expected_num_agents=0) + + def test_filter_agents_multiple_agents_bgp_speakers(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + bgp_speaker = {'id': 'bar-speaker-id'} + self._save_bgp_speaker(bgp_speaker['id']) + expected_filtered_dragent_ids = [agents[1].id] + self._test_filter_agents_helper( + bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + +class TestAutoScheduleBgpSpeakers(TestBgpDrAgentSchedulerBaseTestCase): + """Unit test scenarios for schedule_unscheduled_bgp_speakers. + + bgp_speaker_present + BGP speaker is present or not + + scheduled_already + BGP speaker is already scheduled to the agent or not + + agent_down + BGP DRAgent is down or alive + + valid_host + If true, then an valid host is passed to schedule BGP speaker, + else an invalid host is passed. + """ + scenarios = [ + ('BGP speaker present', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=True)), + + ('No BGP speaker', + dict(bgp_speaker_present=False, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP speaker already scheduled', + dict(bgp_speaker_present=True, + scheduled_already=True, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP DR agent down', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=True, + valid_host=False, + expected_result=False)), + + ('Invalid host', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=False, + expected_result=False)), + ] + + def test_auto_schedule_bgp_speaker(self): + scheduler = bgp_dras.ChanceScheduler() + if self.bgp_speaker_present: + down_agent_count = 1 if self.agent_down else 0 + agents = self._create_and_set_agents_down( + ['host-a'], down_agent_count=down_agent_count) + if self.scheduled_already: + self._test_schedule_bind_bgp_speaker(agents, + self.bgp_speaker_id) + + expected_hosted_agents = (1 if self.bgp_speaker_present and + self.valid_host else 0) + host = "host-a" if self.valid_host else "host-b" + observed_ret_value = scheduler.schedule_unscheduled_bgp_speakers( + self.ctx, host) + self.assertEqual(self.expected_result, observed_ret_value) + hosted_agents = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).all() + self.assertEqual(expected_hosted_agents, len(hosted_agents)) diff --git a/neutron/tests/unit/services/bgp/__init__.py b/neutron/tests/unit/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/tests/unit/services/bgp/scheduler/__init__.py b/neutron/tests/unit/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py b/neutron/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py new file mode 100644 index 00000000..47ac21d0 --- /dev/null +++ b/neutron/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py @@ -0,0 +1,224 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 testscenarios + +from oslo_utils import importutils + +from neutron import context +from neutron.db import bgp_db +from neutron.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron.services.bgp.scheduler import bgp_dragent_scheduler as bgp_dras +from neutron.tests.common import helpers +from neutron.tests.unit import testlib_api + +# Required to generate tests from scenarios. Not compatible with nose. +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestBgpDrAgentSchedulerBaseTestCase(testlib_api.SqlTestCase): + + def setUp(self): + super(TestBgpDrAgentSchedulerBaseTestCase, self).setUp() + self.ctx = context.get_admin_context() + self.bgp_speaker = {'id': 'foo_bgp_speaker_id'} + self.bgp_speaker_id = 'foo_bgp_speaker_id' + self._save_bgp_speaker(self.bgp_speaker_id) + + def _create_and_set_agents_down(self, hosts, down_agent_count=0, + admin_state_up=True): + agents = [] + for i, host in enumerate(hosts): + is_alive = i >= down_agent_count + agents.append(helpers.register_bgp_dragent( + host, + admin_state_up=admin_state_up, + alive=is_alive)) + return agents + + def _save_bgp_speaker(self, bgp_speaker_id): + cls = bgp_db.BgpDbMixin() + bgp_speaker_body = {'bgp_speaker': { + 'name': 'fake_bgp_speaker', + 'ip_version': '4', + 'local_as': '123', + 'advertise_floating_ip_host_routes': '0', + 'advertise_tenant_networks': '0', + 'peers': [], + 'networks': []}} + cls._save_bgp_speaker(self.ctx, bgp_speaker_body, uuid=bgp_speaker_id) + + def _test_schedule_bind_bgp_speaker(self, agents, bgp_speaker_id): + scheduler = bgp_dras.ChanceScheduler() + scheduler.resource_filter.bind(self.ctx, agents, bgp_speaker_id) + results = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).filter_by( + bgp_speaker_id=bgp_speaker_id).all() + + for result in results: + self.assertEqual(bgp_speaker_id, result.bgp_speaker_id) + + +class TestBgpDrAgentScheduler(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin): + + def test_schedule_bind_bgp_speaker_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + def test_schedule_bind_bgp_speaker_multi_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + +class TestBgpAgentFilter(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin, + bgp_dras_db.BgpDrAgentSchedulerDbMixin): + + def setUp(self): + super(TestBgpAgentFilter, self).setUp() + self.bgp_drscheduler = importutils.import_object( + 'neutron.services.bgp.scheduler.' + 'bgp_dragent_scheduler.ChanceScheduler' + ) + self.plugin = self + + def _test_filter_agents_helper(self, bgp_speaker, + expected_filtered_dragent_ids=None, + expected_num_agents=1): + filtered_agents = ( + self.plugin.bgp_drscheduler.resource_filter.filter_agents( + self.plugin, self.ctx, bgp_speaker)) + self.assertEqual(expected_num_agents, + filtered_agents['n_agents']) + actual_filtered_dragent_ids = [ + agent.id for agent in filtered_agents['hostable_agents']] + if expected_filtered_dragent_ids is None: + expected_filtered_dragent_ids = [] + self.assertEqual(len(expected_filtered_dragent_ids), + len(actual_filtered_dragent_ids)) + for filtered_agent_id in actual_filtered_dragent_ids: + self.assertIn(filtered_agent_id, expected_filtered_dragent_ids) + + def test_filter_agents_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + expected_filtered_dragent_ids = [agents[0].id] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_no_agents(self): + expected_filtered_dragent_ids = [] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids, + expected_num_agents=0) + + def test_filter_agents_two_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + expected_filtered_dragent_ids = [agent.id for agent in agents] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_agent_already_scheduled(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + self._test_filter_agents_helper(self.bgp_speaker, + expected_num_agents=0) + + def test_filter_agents_multiple_agents_bgp_speakers(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + bgp_speaker = {'id': 'bar-speaker-id'} + self._save_bgp_speaker(bgp_speaker['id']) + expected_filtered_dragent_ids = [agents[1].id] + self._test_filter_agents_helper( + bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + +class TestAutoScheduleBgpSpeakers(TestBgpDrAgentSchedulerBaseTestCase): + """Unit test scenarios for schedule_unscheduled_bgp_speakers. + + bgp_speaker_present + BGP speaker is present or not + + scheduled_already + BGP speaker is already scheduled to the agent or not + + agent_down + BGP DRAgent is down or alive + + valid_host + If true, then an valid host is passed to schedule BGP speaker, + else an invalid host is passed. + """ + scenarios = [ + ('BGP speaker present', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=True)), + + ('No BGP speaker', + dict(bgp_speaker_present=False, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP speaker already scheduled', + dict(bgp_speaker_present=True, + scheduled_already=True, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP DR agent down', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=True, + valid_host=False, + expected_result=False)), + + ('Invalid host', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=False, + expected_result=False)), + ] + + def test_auto_schedule_bgp_speaker(self): + scheduler = bgp_dras.ChanceScheduler() + if self.bgp_speaker_present: + down_agent_count = 1 if self.agent_down else 0 + agents = self._create_and_set_agents_down( + ['host-a'], down_agent_count=down_agent_count) + if self.scheduled_already: + self._test_schedule_bind_bgp_speaker(agents, + self.bgp_speaker_id) + + expected_hosted_agents = (1 if self.bgp_speaker_present and + self.valid_host else 0) + host = "host-a" if self.valid_host else "host-b" + observed_ret_value = scheduler.schedule_unscheduled_bgp_speakers( + self.ctx, host) + self.assertEqual(self.expected_result, observed_ret_value) + hosted_agents = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).all() + self.assertEqual(expected_hosted_agents, len(hosted_agents))