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