# =========== end of items for agent scheduler extension =====
+# =========== items for l3 extension ==============
+# Enable high availability for virtual routers.
+# l3_ha = False
+#
+# Maximum number of l3 agents which a HA router will be scheduled on. If it
+# is set to 0 the router will be scheduled on every agent.
+# max_l3_agents_per_router = 3
+#
+# Minimum number of l3 agents which a HA router will be scheduled on. The
+# default value is 2.
+# min_l3_agents_per_router = 2
+#
+# CIDR of the administrative network if HA mode is enabled
+# l3_ha_net_cidr = 169.254.192.0/18
+# =========== end of items for l3 extension =======
+
# =========== WSGI parameters related to the API server ==============
# Number of separate worker processes to spawn. The default, 0, runs the
# worker thread in the current process. Greater than 0 launches that number of
"update_port:mac_learning_enabled": "rule:admin_or_network_owner",
"delete_port": "rule:admin_or_owner",
+ "get_router:ha": "rule:admin_only",
"create_router": "rule:regular_user",
"create_router:external_gateway_info:enable_snat": "rule:admin_only",
"create_router:distributed": "rule:admin_only",
+ "create_router:ha": "rule:admin_only",
"get_router": "rule:admin_or_owner",
"get_router:distributed": "rule:admin_only",
"update_router:external_gateway_info:enable_snat": "rule:admin_only",
"update_router:distributed": "rule:admin_only",
+ "update_router:ha": "rule:admin_only",
"delete_router": "rule:admin_or_owner",
"add_router_interface": "rule:admin_or_owner",
# 1.1 Support update_floatingip_statuses
# 1.2 Added methods for DVR support
# 1.3 Added a method that returns the list of activated services
- RPC_API_VERSION = '1.3'
+ # 1.4 Added L3 HA update_router_state
+ RPC_API_VERSION = '1.4'
@property
def plugin(self):
for interface in router.get(constants.INTERFACE_KEY, []):
self._ensure_host_set_on_port(context, host,
interface, router['id'])
+ interface = router.get(constants.HA_INTERFACE_KEY)
+ if interface:
+ self._ensure_host_set_on_port(context, host, interface,
+ router['id'])
def _ensure_host_set_on_port(self, context, host, port, router_id=None):
if (port and
'and on host %(host)s', {'snat_port_list': snat_port_list,
'host': host})
return snat_port_list
+
+ def update_router_state(self, context, **kwargs):
+ router_id = kwargs.get('router_id')
+ state = kwargs.get('state')
+ host = kwargs.get('host')
+
+ return self.l3plugin.update_router_state(context, router_id, state,
+ host=host)
FLOATINGIP_STATUS_DOWN = 'DOWN'
FLOATINGIP_STATUS_ERROR = 'ERROR'
+DEVICE_OWNER_ROUTER_HA_INTF = "network:router_ha_interface"
DEVICE_OWNER_ROUTER_INTF = "network:router_interface"
DEVICE_OWNER_ROUTER_GW = "network:router_gateway"
DEVICE_OWNER_FLOATINGIP = "network:floatingip"
FLOATINGIP_KEY = '_floatingips'
INTERFACE_KEY = '_interfaces'
+HA_INTERFACE_KEY = '_ha_interface'
+HA_ROUTER_STATE_KEY = '_ha_state'
METERING_LABEL_KEY = '_metering_labels'
FLOATINGIP_AGENT_INTF_KEY = '_floatingip_agent_interfaces'
SNAT_ROUTER_INTF_KEY = '_snat_router_interfaces'
+HA_NETWORK_NAME = 'HA network tenant %s'
+HA_SUBNET_NAME = 'HA subnet tenant %s'
+HA_PORT_NAME = 'HA port tenant %s'
+MINIMUM_AGENTS_FOR_HA = 2
+
IPv4 = 'IPv4'
IPv6 = 'IPv6'
DHCP_AGENT_SCHEDULER_EXT_ALIAS = 'dhcp_agent_scheduler'
LBAAS_AGENT_SCHEDULER_EXT_ALIAS = 'lbaas_agent_scheduler'
L3_DISTRIBUTED_EXT_ALIAS = 'dvr'
+L3_HA_MODE_EXT_ALIAS = 'l3-ha'
# Protocol names and numbers for Security Groups/Firewalls
PROTO_NAME_TCP = 'tcp'
RouterL3AgentBinding.router_id.in_(router_ids))
router_ids = [item[0] for item in query]
if router_ids:
- return self.get_sync_data(context, router_ids=router_ids,
- active=True)
+ if n_utils.is_extension_supported(self,
+ constants.L3_HA_MODE_EXT_ALIAS):
+ return self.get_ha_sync_data_for_host(context, host,
+ router_ids=router_ids,
+ active=True)
+ else:
+ return self.get_sync_data(context, router_ids=router_ids,
+ active=True)
else:
return []
service_router = sa.Column(sa.Boolean, default=False,
server_default=sa.sql.false(),
nullable=False)
+ ha = sa.Column(sa.Boolean, default=False,
+ server_default=sa.sql.false(),
+ nullable=False)
+ ha_vr_id = sa.Column(sa.Integer())
+
router = orm.relationship(
l3_db.Router,
backref=orm.backref("extra_attributes", lazy='joined',
def _create_router_db(self, context, router, tenant_id):
"""Create a router db object with dvr additions."""
- router['distributed'] = _is_distributed_router(router)
+ router['distributed'] = is_distributed_router(router)
with context.session.begin(subtransactions=True):
router_db = super(
L3_NAT_with_dvr_db_mixin, self)._create_router_db(
router_is_uuid = isinstance(router, basestring)
if router_is_uuid:
router = self._get_router(context, router)
- if _is_distributed_router(router):
+ if is_distributed_router(router):
return DEVICE_OWNER_DVR_INTERFACE
return super(L3_NAT_with_dvr_db_mixin,
self)._get_device_owner(context, router)
l3_port_check=False)
-def _is_distributed_router(router):
+def is_distributed_router(router):
"""Return True if router to be handled is distributed."""
try:
# See if router is a DB object first
--- /dev/null
+# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import netaddr
+from oslo.config import cfg
+from oslo.db import exception as db_exc
+import sqlalchemy as sa
+from sqlalchemy import orm
+
+from neutron.api.v2 import attributes
+from neutron.common import constants
+from neutron.db import agents_db
+from neutron.db import l3_dvr_db
+from neutron.db import model_base
+from neutron.db import models_v2
+from neutron.extensions import l3_ext_ha_mode as l3_ha
+from neutron.openstack.common import excutils
+from neutron.openstack.common.gettextutils import _LI
+from neutron.openstack.common.gettextutils import _LW
+from neutron.openstack.common import log as logging
+
+VR_ID_RANGE = set(range(1, 255))
+MAX_ALLOCATION_TRIES = 10
+
+LOG = logging.getLogger(__name__)
+
+L3_HA_OPTS = [
+ cfg.BoolOpt('l3_ha',
+ default=False,
+ help=_('Enable HA mode for virtual routers.')),
+ cfg.IntOpt('max_l3_agents_per_router',
+ default=3,
+ help=_('Maximum number of agents on which a router will be '
+ 'scheduled.')),
+ cfg.IntOpt('min_l3_agents_per_router',
+ default=constants.MINIMUM_AGENTS_FOR_HA,
+ help=_('Minimum number of agents on which a router will be '
+ 'scheduled.')),
+ cfg.StrOpt('l3_ha_net_cidr',
+ default='169.254.192.0/18',
+ help=_('Subnet used for the l3 HA admin network.')),
+]
+cfg.CONF.register_opts(L3_HA_OPTS)
+
+
+class L3HARouterAgentPortBinding(model_base.BASEV2):
+ """Represent agent binding state of a HA router port.
+
+ A HA Router has one HA port per agent on which it is spawned.
+ This binding table stores which port is used for a HA router by a
+ L3 agent.
+ """
+
+ __tablename__ = 'ha_router_agent_port_bindings'
+
+ port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id',
+ ondelete='CASCADE'),
+ nullable=False, primary_key=True)
+ port = orm.relationship(models_v2.Port)
+
+ router_id = sa.Column(sa.String(36), sa.ForeignKey('routers.id',
+ ondelete='CASCADE'),
+ nullable=False)
+
+ l3_agent_id = sa.Column(sa.String(36),
+ sa.ForeignKey("agents.id",
+ ondelete='CASCADE'))
+ agent = orm.relationship(agents_db.Agent)
+
+ state = sa.Column(sa.Enum('active', 'standby', name='l3_ha_states'),
+ default='standby',
+ server_default='standby')
+
+
+class L3HARouterNetwork(model_base.BASEV2):
+ """Host HA network for a tenant.
+
+ One HA Network is used per tenant, all HA router ports are created
+ on this network.
+ """
+
+ __tablename__ = 'ha_router_networks'
+
+ tenant_id = sa.Column(sa.String(255), primary_key=True,
+ nullable=False)
+ network_id = sa.Column(sa.String(36),
+ sa.ForeignKey('networks.id', ondelete="CASCADE"),
+ nullable=False, primary_key=True)
+ network = orm.relationship(models_v2.Network)
+
+
+class L3HARouterVRIdAllocation(model_base.BASEV2):
+ """VRID allocation per HA network.
+
+ Keep a track of the VRID allocations per HA network.
+ """
+
+ __tablename__ = 'ha_router_vrid_allocations'
+
+ network_id = sa.Column(sa.String(36),
+ sa.ForeignKey('networks.id', ondelete="CASCADE"),
+ nullable=False, primary_key=True)
+ vr_id = sa.Column(sa.Integer(), nullable=False, primary_key=True)
+
+
+class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin):
+ """Mixin class to add high availability capability to routers."""
+
+ extra_attributes = (
+ l3_dvr_db.L3_NAT_with_dvr_db_mixin.extra_attributes + [
+ {'name': 'ha', 'default': cfg.CONF.l3_ha},
+ {'name': 'ha_vr_id', 'default': 0}])
+
+ def _verify_configuration(self):
+ self.ha_cidr = cfg.CONF.l3_ha_net_cidr
+ try:
+ net = netaddr.IPNetwork(self.ha_cidr)
+ except netaddr.AddrFormatError:
+ raise l3_ha.HANetworkCIDRNotValid(cidr=self.ha_cidr)
+ if ('/' not in self.ha_cidr or net.network != net.ip):
+ raise l3_ha.HANetworkCIDRNotValid(cidr=self.ha_cidr)
+
+ if cfg.CONF.min_l3_agents_per_router < constants.MINIMUM_AGENTS_FOR_HA:
+ raise l3_ha.HAMinimumAgentsNumberNotValid()
+
+ def __init__(self):
+ self._verify_configuration()
+ super(L3_HA_NAT_db_mixin, self).__init__()
+
+ def get_ha_network(self, context, tenant_id):
+ return (context.session.query(L3HARouterNetwork).
+ filter(L3HARouterNetwork.tenant_id == tenant_id).
+ first())
+
+ def _get_allocated_vr_id(self, context, network_id):
+ with context.session.begin(subtransactions=True):
+ query = (context.session.query(L3HARouterVRIdAllocation).
+ filter(L3HARouterVRIdAllocation.network_id == network_id))
+
+ allocated_vr_ids = set(a.vr_id for a in query) - set([0])
+
+ return allocated_vr_ids
+
+ def _allocate_vr_id(self, context, network_id, router_id):
+ for count in range(MAX_ALLOCATION_TRIES):
+ try:
+ with context.session.begin(subtransactions=True):
+ allocated_vr_ids = self._get_allocated_vr_id(context,
+ network_id)
+ available_vr_ids = VR_ID_RANGE - allocated_vr_ids
+
+ if not available_vr_ids:
+ raise l3_ha.NoVRIDAvailable(router_id=router_id)
+
+ allocation = L3HARouterVRIdAllocation()
+ allocation.network_id = network_id
+ allocation.vr_id = available_vr_ids.pop()
+
+ context.session.add(allocation)
+
+ return allocation.vr_id
+
+ except db_exc.DBDuplicateEntry:
+ LOG.info(_LI("Attempt %(count)s to allocate a VRID in the "
+ "network %(network)s for the router %(router)s"),
+ {'count': count, 'network': network_id,
+ 'router': router_id})
+
+ raise l3_ha.MaxVRIDAllocationTriesReached(
+ network_id=network_id, router_id=router_id,
+ max_tries=MAX_ALLOCATION_TRIES)
+
+ def _delete_vr_id_allocation(self, context, ha_network, vr_id):
+ with context.session.begin(subtransactions=True):
+ context.session.query(L3HARouterVRIdAllocation).filter_by(
+ network_id=ha_network.network_id,
+ vr_id=vr_id).delete()
+
+ def _set_vr_id(self, context, router, ha_network):
+ with context.session.begin(subtransactions=True):
+ router.extra_attributes.ha_vr_id = self._allocate_vr_id(
+ context, ha_network.network_id, router.id)
+
+ def _create_ha_subnet(self, context, network_id, tenant_id):
+ args = {'subnet':
+ {'network_id': network_id,
+ 'tenant_id': '',
+ 'name': constants.HA_SUBNET_NAME % tenant_id,
+ 'ip_version': 4,
+ 'cidr': cfg.CONF.l3_ha_net_cidr,
+ 'enable_dhcp': False,
+ 'host_routes': attributes.ATTR_NOT_SPECIFIED,
+ 'dns_nameservers': attributes.ATTR_NOT_SPECIFIED,
+ 'allocation_pools': attributes.ATTR_NOT_SPECIFIED,
+ 'gateway_ip': None}}
+ return self._core_plugin.create_subnet(context, args)
+
+ def _create_ha_network_tenant_binding(self, context, tenant_id,
+ network_id):
+ with context.session.begin(subtransactions=True):
+ ha_network = L3HARouterNetwork(tenant_id=tenant_id,
+ network_id=network_id)
+ context.session.add(ha_network)
+ return ha_network
+
+ def _create_ha_network(self, context, tenant_id):
+ admin_ctx = context.elevated()
+
+ args = {'network':
+ {'name': constants.HA_NETWORK_NAME % tenant_id,
+ 'tenant_id': '',
+ 'shared': False,
+ 'admin_state_up': True,
+ 'status': constants.NET_STATUS_ACTIVE}}
+ network = self._core_plugin.create_network(context, args)
+ try:
+ ha_network = self._create_ha_network_tenant_binding(admin_ctx,
+ tenant_id,
+ network['id'])
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self._core_plugin.delete_network(admin_ctx, network['id'])
+
+ try:
+ self._create_ha_subnet(admin_ctx, network['id'], tenant_id)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self._core_plugin.delete_network(admin_ctx, network['id'])
+
+ return ha_network
+
+ def get_number_of_agents_for_scheduling(self, context):
+ """Return the number of agents on which the router will be scheduled.
+
+ Raises an exception if there are not enough agents available to honor
+ the min_agents config parameter. If the max_agents parameter is set to
+ 0 all the agents will be used.
+ """
+
+ min_agents = cfg.CONF.min_l3_agents_per_router
+ num_agents = len(self.get_l3_agents(context))
+ max_agents = cfg.CONF.max_l3_agents_per_router
+ if max_agents:
+ if max_agents > num_agents:
+ LOG.info(_LI("Number of available agents lower than "
+ "max_l3_agents_per_router. L3 agents "
+ "available: %s"), num_agents)
+ else:
+ num_agents = max_agents
+
+ if num_agents < min_agents:
+ raise l3_ha.HANotEnoughAvailableAgents(min_agents=min_agents,
+ num_agents=num_agents)
+
+ return num_agents
+
+ def _create_ha_port_binding(self, context, port_id, router_id):
+ with context.session.begin(subtransactions=True):
+ portbinding = L3HARouterAgentPortBinding(port_id=port_id,
+ router_id=router_id)
+ context.session.add(portbinding)
+
+ return portbinding
+
+ def add_ha_port(self, context, router_id, network_id, tenant_id):
+ port = self._core_plugin.create_port(context, {
+ 'port':
+ {'tenant_id': '',
+ 'network_id': network_id,
+ 'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
+ 'mac_address': attributes.ATTR_NOT_SPECIFIED,
+ 'admin_state_up': True,
+ 'device_id': router_id,
+ 'device_owner': constants.DEVICE_OWNER_ROUTER_HA_INTF,
+ 'name': constants.HA_PORT_NAME % tenant_id}})
+
+ try:
+ return self._create_ha_port_binding(context, port['id'], router_id)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self._core_plugin.delete_port(context, port['id'],
+ l3_port_check=False)
+
+ def _create_ha_interfaces(self, context, router, ha_network):
+ admin_ctx = context.elevated()
+
+ num_agents = self.get_number_of_agents_for_scheduling(context)
+
+ port_ids = []
+ try:
+ for index in range(num_agents):
+ binding = self.add_ha_port(admin_ctx, router.id,
+ ha_network.network['id'],
+ router.tenant_id)
+ port_ids.append(binding.port_id)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ for port_id in port_ids:
+ self._core_plugin.delete_port(admin_ctx, port_id,
+ l3_port_check=False)
+
+ def _delete_ha_interfaces(self, context, router_id):
+ admin_ctx = context.elevated()
+ device_filter = {'device_id': [router_id],
+ 'device_owner':
+ [constants.DEVICE_OWNER_ROUTER_HA_INTF]}
+ ports = self._core_plugin.get_ports(admin_ctx, filters=device_filter)
+
+ for port in ports:
+ self._core_plugin.delete_port(admin_ctx, port['id'],
+ l3_port_check=False)
+
+ def _notify_ha_interfaces_updated(self, context, router_id):
+ self.l3_rpc_notifier.routers_updated(context, [router_id])
+
+ @classmethod
+ def _is_ha(cls, router):
+ ha = router.get('ha')
+ if not attributes.is_attr_set(ha):
+ ha = cfg.CONF.l3_ha
+ return ha
+
+ def _create_router_db(self, context, router, tenant_id):
+ router['ha'] = self._is_ha(router)
+
+ if router['ha'] and l3_dvr_db.is_distributed_router(router):
+ raise l3_ha.DistributedHARouterNotSupported()
+
+ with context.session.begin(subtransactions=True):
+ router_db = super(L3_HA_NAT_db_mixin, self)._create_router_db(
+ context, router, tenant_id)
+
+ if router['ha']:
+ try:
+ ha_network = self.get_ha_network(context,
+ router_db.tenant_id)
+ if not ha_network:
+ ha_network = self._create_ha_network(context,
+ router_db.tenant_id)
+
+ self._set_vr_id(context, router_db, ha_network)
+ self._create_ha_interfaces(context, router_db, ha_network)
+ self._notify_ha_interfaces_updated(context, router_db.id)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ self.delete_router(context, router_db.id)
+
+ return router_db
+
+ def _update_router_db(self, context, router_id, data, gw_info):
+ ha = data.pop('ha', None)
+
+ if ha and data.get('distributed'):
+ raise l3_ha.DistributedHARouterNotSupported()
+
+ with context.session.begin(subtransactions=True):
+ router_db = super(L3_HA_NAT_db_mixin, self)._update_router_db(
+ context, router_id, data, gw_info)
+
+ ha_not_changed = ha is None or ha == router_db.extra_attributes.ha
+ if ha_not_changed:
+ return router_db
+
+ ha_network = self.get_ha_network(context,
+ router_db.tenant_id)
+ router_db.extra_attributes.ha = ha
+ if not ha:
+ self._delete_vr_id_allocation(
+ context, ha_network, router_db.extra_attributes.ha_vr_id)
+ router_db.extra_attributes.ha_vr_id = None
+
+ if ha:
+ if not ha_network:
+ ha_network = self._create_ha_network(context,
+ router_db.tenant_id)
+
+ self._set_vr_id(context, router_db, ha_network)
+ self._create_ha_interfaces(context, router_db, ha_network)
+ self._notify_ha_interfaces_updated(context, router_db.id)
+ else:
+ self._delete_ha_interfaces(context, router_db.id)
+ self._notify_ha_interfaces_updated(context, router_db.id)
+
+ return router_db
+
+ def update_router_state(self, context, router_id, state, host):
+ with context.session.begin(subtransactions=True):
+ bindings = self.get_ha_router_port_bindings(context, [router_id],
+ host)
+ if bindings:
+ if len(bindings) > 1:
+ LOG.warn(_LW("The router %(router_id)s is bound multiple "
+ "times on the agent %(host)s"),
+ {'router_id': router_id, 'host': host})
+
+ bindings[0].update({'state': state})
+
+ def delete_router(self, context, id):
+ router_db = self._get_router(context, id)
+ if router_db.extra_attributes.ha:
+ ha_network = self.get_ha_network(context,
+ router_db.tenant_id)
+ if ha_network:
+ self._delete_vr_id_allocation(
+ context, ha_network, router_db.extra_attributes.ha_vr_id)
+ self._delete_ha_interfaces(context, router_db.id)
+
+ return super(L3_HA_NAT_db_mixin, self).delete_router(context, id)
+
+ def get_ha_router_port_bindings(self, context, router_ids, host=None):
+ query = context.session.query(L3HARouterAgentPortBinding)
+
+ if host:
+ query = query.join(agents_db.Agent).filter(
+ agents_db.Agent.host == host)
+
+ query = query.filter(
+ L3HARouterAgentPortBinding.router_id.in_(router_ids))
+
+ return query.all()
+
+ def _process_sync_ha_data(self, context, routers, host):
+ routers_dict = dict((router['id'], router) for router in routers)
+
+ bindings = self.get_ha_router_port_bindings(context,
+ routers_dict.keys(),
+ host)
+ for binding in bindings:
+ port_dict = self._core_plugin._make_port_dict(binding.port)
+
+ router = routers_dict.get(binding.router_id)
+ router[constants.HA_INTERFACE_KEY] = port_dict
+ router[constants.HA_ROUTER_STATE_KEY] = binding.state
+
+ for router in routers_dict.values():
+ interface = router.get(constants.HA_INTERFACE_KEY)
+ if interface:
+ self._populate_subnet_for_ports(context, [interface])
+
+ return routers_dict.values()
+
+ def get_ha_sync_data_for_host(self, context, host=None, router_ids=None,
+ active=None):
+ sync_data = super(L3_HA_NAT_db_mixin, self).get_sync_data(context,
+ router_ids,
+ active)
+ return self._process_sync_ha_data(context, sync_data, host)
--- /dev/null
+# Copyright 2014 OpenStack Foundation
+#
+# 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.
+#
+
+"""ext_l3_ha_mode
+
+Revision ID: 16a27a58e093
+Revises: 86d6d9776e2b
+Create Date: 2014-02-01 10:24:12.412733
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '16a27a58e093'
+down_revision = '86d6d9776e2b'
+
+
+from alembic import op
+import sqlalchemy as sa
+
+l3_ha_states = sa.Enum('active', 'standby', name='l3_ha_states')
+
+
+def upgrade(active_plugins=None, options=None):
+ op.add_column('router_extra_attributes',
+ sa.Column('ha', sa.Boolean(),
+ nullable=False,
+ server_default=sa.sql.false()))
+ op.add_column('router_extra_attributes',
+ sa.Column('ha_vr_id', sa.Integer()))
+
+ op.create_table('ha_router_agent_port_bindings',
+ sa.Column('port_id', sa.String(length=36),
+ nullable=False),
+ sa.Column('router_id', sa.String(length=36),
+ nullable=False),
+ sa.Column('l3_agent_id', sa.String(length=36),
+ nullable=True),
+ sa.Column('state', l3_ha_states,
+ server_default='standby'),
+ sa.PrimaryKeyConstraint('port_id'),
+ sa.ForeignKeyConstraint(['port_id'], ['ports.id'],
+ ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['router_id'], ['routers.id'],
+ ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['l3_agent_id'], ['agents.id'],
+ ondelete='CASCADE'))
+
+ op.create_table('ha_router_networks',
+ sa.Column('tenant_id', sa.String(length=255),
+ nullable=False, primary_key=True),
+ sa.Column('network_id', sa.String(length=36),
+ nullable=False,
+ primary_key=True),
+ sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
+ ondelete='CASCADE'))
+
+ op.create_table('ha_router_vrid_allocations',
+ sa.Column('network_id', sa.String(length=36),
+ nullable=False,
+ primary_key=True),
+ sa.Column('vr_id', sa.Integer(),
+ nullable=False,
+ primary_key=True),
+ sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
+ ondelete='CASCADE'))
+
+
+def downgrade(active_plugins=None, options=None):
+ op.drop_table('ha_router_vrid_allocations')
+ op.drop_table('ha_router_networks')
+ op.drop_table('ha_router_agent_port_bindings')
+ l3_ha_states.drop(op.get_bind(), checkfirst=False)
+ op.drop_column('router_extra_attributes', 'ha_vr_id')
+ op.drop_column('router_extra_attributes', 'ha')
-86d6d9776e2b
+16a27a58e093
from neutron.db import l3_db # noqa
from neutron.db import l3_dvrscheduler_db # noqa
from neutron.db import l3_gwmode_db # noqa
+from neutron.db import l3_hamode_db # noqa
from neutron.db.loadbalancer import loadbalancer_db # noqa
from neutron.db.metering import metering_db # noqa
from neutron.db import model_base
--- /dev/null
+# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from neutron.api import extensions
+from neutron.api.v2 import attributes
+from neutron.common import constants
+from neutron.common import exceptions
+
+HA_INFO = 'ha'
+EXTENDED_ATTRIBUTES_2_0 = {
+ 'routers': {
+ HA_INFO: {'allow_post': True, 'allow_put': True,
+ 'default': attributes.ATTR_NOT_SPECIFIED, 'is_visible': True,
+ 'enforce_policy': True,
+ 'convert_to': attributes.convert_to_boolean_if_not_none}
+ }
+}
+
+
+class DistributedHARouterNotSupported(NotImplementedError):
+ message = _("Currenly distributed HA routers are "
+ "not supported.")
+
+
+class MaxVRIDAllocationTriesReached(exceptions.NeutronException):
+ message = _("Failed to allocate a VRID in the network %(network_id)s "
+ "for the router %(router_id)s after %(max_tries)s tries.")
+
+
+class NoVRIDAvailable(exceptions.Conflict):
+ message = _("No more Virtual Router Identifier (VRID) available when "
+ "creating router %(router_id)s. The limit of number "
+ "of HA Routers per tenant is 254.")
+
+
+class HANetworkCIDRNotValid(exceptions.NeutronException):
+ message = _("The HA Network CIDR specified in the configuration file "
+ "isn't valid; %(cidr)s.")
+
+
+class HANotEnoughAvailableAgents(exceptions.NeutronException):
+ message = _("Not enough l3 agents available to ensure HA. Minimum "
+ "required %(min_agents)s, available %(num_agents)s.")
+
+
+class HAMinimumAgentsNumberNotValid(exceptions.NeutronException):
+ message = (_("min_l3_agents_per_router config parameter is not valid. "
+ "It has to be equal to or more than %s for HA.") %
+ constants.MINIMUM_AGENTS_FOR_HA)
+
+
+class L3_ext_ha_mode(extensions.ExtensionDescriptor):
+ """Extension class supporting virtual router in HA mode."""
+
+ @classmethod
+ def get_name(cls):
+ return "HA Router extension"
+
+ @classmethod
+ def get_alias(cls):
+ return constants.L3_HA_MODE_EXT_ALIAS
+
+ @classmethod
+ def get_description(cls):
+ return "Add HA capability to routers."
+
+ @classmethod
+ def get_namespace(cls):
+ return ""
+
+ @classmethod
+ def get_updated(cls):
+ return "2014-04-26T00:00:00-00:00"
+
+ def get_extended_resources(self, version):
+ if version == "2.0":
+ return EXTENDED_ATTRIBUTES_2_0
+ else:
+ return {}
from neutron.common import topics
from neutron.db import common_db_mixin
from neutron.db import extraroute_db
-from neutron.db import l3_dvr_db
from neutron.db import l3_dvrscheduler_db
from neutron.db import l3_gwmode_db
+from neutron.db import l3_hamode_db
from neutron.openstack.common import importutils
from neutron.plugins.common import constants
class L3RouterPlugin(common_db_mixin.CommonDbMixin,
extraroute_db.ExtraRoute_db_mixin,
- l3_dvr_db.L3_NAT_with_dvr_db_mixin,
l3_gwmode_db.L3_NAT_db_mixin,
- l3_dvrscheduler_db.L3_DVRsch_db_mixin):
+ l3_dvrscheduler_db.L3_DVRsch_db_mixin,
+ l3_hamode_db.L3_HA_NAT_db_mixin):
"""Implementation of the Neutron L3 Router Service Plugin.
router and floatingip resources and manages associated
request/response.
All DB related work is implemented in classes
- l3_db.L3_NAT_db_mixin, l3_dvr_db.L3_NAT_with_dvr_db_mixin, and
- extraroute_db.ExtraRoute_db_mixin.
+ l3_db.L3_NAT_db_mixin, l3_hamode_db.L3_HA_NAT_db_mixin,
+ l3_dvr_db.L3_NAT_with_dvr_db_mixin, and extraroute_db.ExtraRoute_db_mixin.
"""
supported_extension_aliases = ["dvr", "router", "ext-gw-mode",
- "extraroute", "l3_agent_scheduler"]
+ "extraroute", "l3_agent_scheduler",
+ "l3-ha"]
def __init__(self):
self.setup_rpc()
self.router_scheduler = importutils.import_object(
cfg.CONF.router_scheduler_driver)
self.start_periodic_agent_status_check()
+ super(L3RouterPlugin, self).__init__()
def setup_rpc(self):
# RPC support
pass_router_id=False)
def _test__is_distributed_router(self, router, expected):
- result = l3_dvr_db._is_distributed_router(router)
+ result = l3_dvr_db.is_distributed_router(router)
self.assertEqual(expected, result)
def test__is_distributed_router_by_db_object(self):
--- /dev/null
+# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
+#
+# 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 mock
+from oslo.config import cfg
+
+from neutron.common import constants
+from neutron import context
+from neutron.db import agents_db
+from neutron.db import common_db_mixin
+from neutron.db import l3_hamode_db
+from neutron.extensions import l3_ext_ha_mode
+from neutron import manager
+from neutron.openstack.common import uuidutils
+from neutron.tests.unit import testlib_api
+from neutron.tests.unit import testlib_plugin
+
+_uuid = uuidutils.generate_uuid
+
+
+class FakeL3Plugin(common_db_mixin.CommonDbMixin,
+ l3_hamode_db.L3_HA_NAT_db_mixin):
+ pass
+
+
+class FakeL3PluginWithAgents(FakeL3Plugin,
+ agents_db.AgentDbMixin):
+ pass
+
+
+class L3HATestFramework(testlib_api.SqlTestCase,
+ testlib_plugin.PluginSetupHelper):
+ def setUp(self):
+ super(L3HATestFramework, self).setUp()
+
+ self.admin_ctx = context.get_admin_context()
+ self.setup_coreplugin('neutron.plugins.ml2.plugin.Ml2Plugin')
+ self.core_plugin = manager.NeutronManager.get_plugin()
+ mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin, 'get_l3_agents',
+ create=True, return_value=[1, 2]).start()
+ notif_p = mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin,
+ '_notify_ha_interfaces_updated')
+ self.notif_m = notif_p.start()
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+
+ def _create_router(self, ha=True, tenant_id='tenant1', distributed=None):
+ router = {'name': 'router1', 'admin_state_up': True}
+ if ha is not None:
+ router['ha'] = ha
+ if distributed is not None:
+ router['distributed'] = distributed
+ return self.plugin._create_router_db(self.admin_ctx, router, tenant_id)
+
+ def _update_router(self, router_id, ha=True, distributed=None):
+ data = {'ha': ha} if ha is not None else {}
+ if distributed is not None:
+ data['distributed'] = distributed
+ return self.plugin._update_router_db(self.admin_ctx, router_id,
+ data, None)
+
+
+class L3HAGetSyncDataTestCase(L3HATestFramework):
+
+ def setUp(self):
+ super(L3HAGetSyncDataTestCase, self).setUp()
+ self.plugin = FakeL3PluginWithAgents()
+ self._register_agents()
+
+ def _register_agents(self):
+ agent_status = {
+ 'agent_type': constants.AGENT_TYPE_L3,
+ 'binary': 'neutron-l3-agent',
+ 'host': 'l3host',
+ 'topic': 'N/A'
+ }
+ self.plugin.create_or_update_agent(self.admin_ctx, agent_status)
+ agent_status['host'] = 'l3host_2'
+ self.plugin.create_or_update_agent(self.admin_ctx, agent_status)
+ self.agent1, self.agent2 = self.plugin.get_agents(self.admin_ctx)
+
+ def _bind_router(self, router_id):
+ with self.admin_ctx.session.begin(subtransactions=True):
+ bindings = self.plugin.get_ha_router_port_bindings(self.admin_ctx,
+ [router_id])
+
+ for agent_id, binding in zip(
+ [self.agent1['id'], self.agent2['id']], bindings):
+ binding.l3_agent_id = agent_id
+
+ def test_l3_agent_routers_query_interface(self):
+ router = self._create_router()
+ self._bind_router(router.id)
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx,
+ self.agent1['host'])
+ self.assertEqual(1, len(routers))
+ router = routers[0]
+
+ self.assertIsNotNone(router.get('ha'))
+
+ interface = router.get(constants.HA_INTERFACE_KEY)
+ self.assertIsNotNone(interface)
+
+ self.assertEqual(constants.DEVICE_OWNER_ROUTER_HA_INTF,
+ interface['device_owner'])
+ self.assertEqual(cfg.CONF.l3_ha_net_cidr, interface['subnet']['cidr'])
+
+ def test_update_state(self):
+ router = self._create_router()
+ self._bind_router(router.id)
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx,
+ self.agent1['host'])
+ state = routers[0].get(constants.HA_ROUTER_STATE_KEY)
+ self.assertEqual('standby', state)
+
+ self.plugin.update_router_state(self.admin_ctx, router.id, 'active',
+ self.agent1['host'])
+
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx,
+ self.agent1['host'])
+
+ state = routers[0].get(constants.HA_ROUTER_STATE_KEY)
+ self.assertEqual('active', state)
+
+
+class L3HATestCase(L3HATestFramework):
+
+ def setUp(self):
+ super(L3HATestCase, self).setUp()
+ self.plugin = FakeL3Plugin()
+
+ def test_verify_configuration_succeed(self):
+ # Default configuration should pass
+ self.plugin._verify_configuration()
+
+ def test_verify_configuration_l3_ha_net_cidr_is_not_a_cidr(self):
+ cfg.CONF.set_override('l3_ha_net_cidr', 'not a cidr')
+ self.assertRaises(
+ l3_ext_ha_mode.HANetworkCIDRNotValid,
+ self.plugin._verify_configuration)
+
+ def test_verify_configuration_l3_ha_net_cidr_is_not_a_subnet(self):
+ cfg.CONF.set_override('l3_ha_net_cidr', '10.0.0.1/8')
+ self.assertRaises(
+ l3_ext_ha_mode.HANetworkCIDRNotValid,
+ self.plugin._verify_configuration)
+
+ def test_verify_conifguration_min_l3_agents_per_router_below_minimum(self):
+ cfg.CONF.set_override('min_l3_agents_per_router', 0)
+ self.assertRaises(
+ l3_ext_ha_mode.HAMinimumAgentsNumberNotValid,
+ self.plugin._verify_configuration)
+
+ def test_ha_router_create(self):
+ router = self._create_router()
+ self.assertTrue(router.extra_attributes['ha'])
+
+ def test_ha_router_create_with_distributed(self):
+ self.assertRaises(l3_ext_ha_mode.DistributedHARouterNotSupported,
+ self._create_router,
+ distributed=True)
+
+ def test_no_ha_router_create(self):
+ router = self._create_router(ha=False)
+ self.assertFalse(router.extra_attributes['ha'])
+
+ def test_router_create_with_ha_conf_enabled(self):
+ cfg.CONF.set_override('l3_ha', True)
+
+ router = self._create_router(ha=None)
+ self.assertTrue(router.extra_attributes['ha'])
+
+ def test_migration_from_ha(self):
+ router = self._create_router()
+ self.assertTrue(router.extra_attributes['ha'])
+
+ router = self._update_router(router.id, ha=False)
+ self.assertFalse(router.extra_attributes['ha'])
+ self.assertIsNone(router.extra_attributes['ha_vr_id'])
+
+ def test_migration_to_ha(self):
+ router = self._create_router(ha=False)
+ self.assertFalse(router.extra_attributes['ha'])
+
+ router = self._update_router(router.id, ha=True)
+ self.assertTrue(router.extra_attributes['ha'])
+ self.assertIsNotNone(router.extra_attributes['ha_vr_id'])
+
+ def test_migrate_ha_router_to_distributed(self):
+ router = self._create_router()
+ self.assertTrue(router.extra_attributes['ha'])
+
+ self.assertRaises(l3_ext_ha_mode.DistributedHARouterNotSupported,
+ self._update_router,
+ router.id,
+ distributed=True)
+
+ def test_unique_ha_network_per_tenant(self):
+ tenant1 = _uuid()
+ tenant2 = _uuid()
+ self._create_router(tenant_id=tenant1)
+ self._create_router(tenant_id=tenant2)
+ ha_network1 = self.plugin.get_ha_network(self.admin_ctx, tenant1)
+ ha_network2 = self.plugin.get_ha_network(self.admin_ctx, tenant2)
+ self.assertNotEqual(
+ ha_network1['network_id'], ha_network2['network_id'])
+
+ def _deployed_router_change_ha_flag(self, to_ha):
+ self._create_router(ha=not to_ha)
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx)
+ router = routers[0]
+ interface = router.get(constants.HA_INTERFACE_KEY)
+ if to_ha:
+ self.assertIsNone(interface)
+ else:
+ self.assertIsNotNone(interface)
+
+ self._update_router(router['id'], to_ha)
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx)
+ router = routers[0]
+ interface = router.get(constants.HA_INTERFACE_KEY)
+ if to_ha:
+ self.assertIsNotNone(interface)
+ else:
+ self.assertIsNone(interface)
+
+ def test_deployed_router_can_have_ha_enabled(self):
+ self._deployed_router_change_ha_flag(to_ha=True)
+
+ def test_deployed_router_can_have_ha_disabled(self):
+ self._deployed_router_change_ha_flag(to_ha=False)
+
+ def test_create_ha_router_notifies_agent(self):
+ self._create_router()
+ self.assertTrue(self.notif_m.called)
+
+ def test_update_router_to_ha_notifies_agent(self):
+ router = self._create_router(ha=False)
+ self.notif_m.reset_mock()
+ self._update_router(router.id, ha=True)
+ self.assertTrue(self.notif_m.called)
+
+ def test_unique_vr_id_between_routers(self):
+ self._create_router()
+ self._create_router()
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx)
+ self.assertEqual(2, len(routers))
+ self.assertNotEqual(routers[0]['ha_vr_id'], routers[1]['ha_vr_id'])
+
+ @mock.patch('neutron.db.l3_hamode_db.VR_ID_RANGE', new=set(range(1, 1)))
+ def test_vr_id_depleted(self):
+ self.assertRaises(l3_ext_ha_mode.NoVRIDAvailable, self._create_router)
+
+ @mock.patch('neutron.db.l3_hamode_db.VR_ID_RANGE', new=set(range(1, 2)))
+ def test_vr_id_unique_range_per_tenant(self):
+ self._create_router()
+ self._create_router(tenant_id=_uuid())
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx)
+ self.assertEqual(2, len(routers))
+ self.assertEqual(routers[0]['ha_vr_id'], routers[1]['ha_vr_id'])
+
+ @mock.patch('neutron.db.l3_hamode_db.MAX_ALLOCATION_TRIES', new=2)
+ def test_vr_id_allocation_contraint_conflict(self):
+ router = self._create_router()
+ network = self.plugin.get_ha_network(self.admin_ctx, router.tenant_id)
+
+ with mock.patch.object(self.plugin, '_get_allocated_vr_id',
+ return_value=set()) as alloc:
+ self.assertRaises(l3_ext_ha_mode.MaxVRIDAllocationTriesReached,
+ self.plugin._allocate_vr_id, self.admin_ctx,
+ network.network_id, router.id)
+ self.assertEqual(2, len(alloc.mock_calls))
+
+ def test_vr_id_allocation_delete_router(self):
+ router = self._create_router()
+ network = self.plugin.get_ha_network(self.admin_ctx, router.tenant_id)
+
+ allocs_before = self.plugin._get_allocated_vr_id(self.admin_ctx,
+ network.network_id)
+ router = self._create_router()
+ allocs_current = self.plugin._get_allocated_vr_id(self.admin_ctx,
+ network.network_id)
+ self.assertNotEqual(allocs_before, allocs_current)
+
+ self.plugin.delete_router(self.admin_ctx, router.id)
+ allocs_after = self.plugin._get_allocated_vr_id(self.admin_ctx,
+ network.network_id)
+ self.assertEqual(allocs_before, allocs_after)
+
+ def test_vr_id_allocation_router_migration(self):
+ router = self._create_router()
+ network = self.plugin.get_ha_network(self.admin_ctx, router.tenant_id)
+
+ allocs_before = self.plugin._get_allocated_vr_id(self.admin_ctx,
+ network.network_id)
+ router = self._create_router()
+ self._update_router(router.id, ha=False)
+ allocs_after = self.plugin._get_allocated_vr_id(self.admin_ctx,
+ network.network_id)
+ self.assertEqual(allocs_before, allocs_after)
+
+ def test_one_ha_router_one_not(self):
+ self._create_router(ha=False)
+ self._create_router()
+ routers = self.plugin.get_ha_sync_data_for_host(self.admin_ctx)
+
+ ha0 = routers[0]['ha']
+ ha1 = routers[1]['ha']
+
+ self.assertNotEqual(ha0, ha1)
+
+ def test_add_ha_port_binding_failure_rolls_back_port(self):
+ router = self._create_router()
+ device_filter = {'device_id': [router.id]}
+ ports_before = self.core_plugin.get_ports(
+ self.admin_ctx, filters=device_filter)
+ network = self.plugin.get_ha_network(self.admin_ctx, router.tenant_id)
+
+ with mock.patch.object(self.plugin, '_create_ha_port_binding',
+ side_effect=ValueError):
+ self.assertRaises(ValueError, self.plugin.add_ha_port,
+ self.admin_ctx, router.id, network.network_id,
+ router.tenant_id)
+
+ ports_after = self.core_plugin.get_ports(
+ self.admin_ctx, filters=device_filter)
+
+ self.assertEqual(ports_before, ports_after)
+
+ def test_create_ha_network_binding_failure_rolls_back_network(self):
+ networks_before = self.core_plugin.get_networks(self.admin_ctx)
+
+ with mock.patch.object(self.plugin,
+ '_create_ha_network_tenant_binding',
+ side_effect=ValueError):
+ self.assertRaises(ValueError, self.plugin._create_ha_network,
+ self.admin_ctx, _uuid())
+
+ networks_after = self.core_plugin.get_networks(self.admin_ctx)
+ self.assertEqual(networks_before, networks_after)
+
+ def test_create_ha_network_subnet_failure_rolls_back_network(self):
+ networks_before = self.core_plugin.get_networks(self.admin_ctx)
+
+ with mock.patch.object(self.plugin, '_create_ha_subnet',
+ side_effect=ValueError):
+ self.assertRaises(ValueError, self.plugin._create_ha_network,
+ self.admin_ctx, _uuid())
+
+ networks_after = self.core_plugin.get_networks(self.admin_ctx)
+ self.assertEqual(networks_before, networks_after)
+
+ def test_create_ha_interfaces_binding_failure_rolls_back_ports(self):
+ router = self._create_router()
+ network = self.plugin.get_ha_network(self.admin_ctx, router.tenant_id)
+ device_filter = {'device_id': [router.id]}
+ ports_before = self.core_plugin.get_ports(
+ self.admin_ctx, filters=device_filter)
+
+ with mock.patch.object(self.plugin, '_create_ha_port_binding',
+ side_effect=ValueError):
+ self.assertRaises(ValueError, self.plugin._create_ha_interfaces,
+ self.admin_ctx, router, network)
+
+ ports_after = self.core_plugin.get_ports(
+ self.admin_ctx, filters=device_filter)
+ self.assertEqual(ports_before, ports_after)
+
+ def test_create_router_db_ha_attribute_failure_rolls_back_router(self):
+ routers_before = self.plugin.get_routers(self.admin_ctx)
+
+ for method in ('_set_vr_id',
+ '_create_ha_interfaces',
+ '_notify_ha_interfaces_updated'):
+ with mock.patch.object(self.plugin, method,
+ side_effect=ValueError):
+ self.assertRaises(ValueError, self._create_router)
+
+ routers_after = self.plugin.get_routers(self.admin_ctx)
+ self.assertEqual(routers_before, routers_after)