]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add L3 VRRP HA base classes
authorSylvain Afchain <sylvain.afchain@enovance.com>
Mon, 26 May 2014 12:28:06 +0000 (14:28 +0200)
committerSylvain Afchain <sylvain.afchain@enovance.com>
Wed, 10 Sep 2014 12:06:13 +0000 (12:06 +0000)
Add L3 HA base classes on the plugin side. A new admin-only ha
attribute is added to the API router resource. Conversion from
or to HA router is possible. Each tenant gets a single network
used for HA traffic. The tenant_id for that network is set to
'' so that it isn't visible via the CLI or GUI. A new table
is added to map a tenant to its HA network. Specific HA
attributes are added to the extra router attributes table.
Finally, each HA router gets a port on the HA network, per
l3 agent it is scheduled on. A new table is added to track
these bindings. A new table is added in order to track
VRID allocations.

DVR integration is not expected to work. Any issues will
be reported as bugs and handled after the feature merges.
Migrating a router to HA or from HA works server side
but is not expected to work (Yet) agent side. This will be
dealt with as a bug in the future.

DocImpact
Partially-implements: blueprint l3-high-availability
Change-Id: I9d935cf5e0c231e8cb7af5f61b9a9fc552c3521e
Co-Authored-By: Assaf Muller <amuller@redhat.com>
15 files changed:
etc/neutron.conf
etc/policy.json
neutron/api/rpc/handlers/l3_rpc.py
neutron/common/constants.py
neutron/db/l3_agentschedulers_db.py
neutron/db/l3_attrs_db.py
neutron/db/l3_dvr_db.py
neutron/db/l3_hamode_db.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/16a27a58e093_ext_l3_ha_mode.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/migration/models/head.py
neutron/extensions/l3_ext_ha_mode.py [new file with mode: 0644]
neutron/services/l3_router/l3_router_plugin.py
neutron/tests/unit/db/test_l3_dvr_db.py
neutron/tests/unit/db/test_l3_ha_db.py [new file with mode: 0644]

index 2ba7ada424f0ef677f8bb3749c54801f375407ef..0836626424af65b9dd7dc65ec5b683fffe6c9535 100644 (file)
@@ -172,6 +172,22 @@ lock_path = $state_path/lock
 
 # ===========  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
index c5aec3b3e52674947260f31d379ac562a24fb8fa..e7db4357547b34fb63f1bb7da8bda9792cb6d487 100644 (file)
     "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",
index f31209dc913cea2751af580e2bd0b1c18095c8e2..f5c7389d505b83712dd653b81d0219b93283d19e 100644 (file)
@@ -38,7 +38,8 @@ class L3RpcCallback(n_rpc.RpcCallback):
     # 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):
@@ -104,6 +105,10 @@ class L3RpcCallback(n_rpc.RpcCallback):
             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
@@ -224,3 +229,11 @@ class L3RpcCallback(n_rpc.RpcCallback):
                   '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)
index f1c15c535b618c26a18b5b31f50df51bc4da792c..b053b1ae3cb5c70298bffa3e594b022039653e6e 100644 (file)
@@ -29,6 +29,7 @@ FLOATINGIP_STATUS_ACTIVE = 'ACTIVE'
 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"
@@ -42,10 +43,17 @@ DEVICE_ID_RESERVED_DHCP_PORT = "reserved_dhcp_port"
 
 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'
 
@@ -101,6 +109,7 @@ L3_AGENT_SCHEDULER_EXT_ALIAS = 'l3_agent_scheduler'
 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'
index e9dc8e3089dd1a19cc1e3f940b18d301c27e7071..2d52921fd1a13e8b25675aa684e67932937b8fa4 100644 (file)
@@ -284,8 +284,14 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase,
                 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 []
 
index d43cdc7b421c3c01db1870714caf7e2634fed8b4..7c82f84af1e8f25531c18e44ed96d0f061d6db79 100644 (file)
@@ -40,6 +40,11 @@ class RouterExtraAttributes(model_base.BASEV2):
     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',
index 6a91c0fe068cb7e27df5c27816b6fb0c4dcbb8f0..69561719263ea8c72818d91d31a4236df3262483 100644 (file)
@@ -61,7 +61,7 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
 
     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(
@@ -128,7 +128,7 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
         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)
@@ -534,7 +534,7 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
                                                   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
diff --git a/neutron/db/l3_hamode_db.py b/neutron/db/l3_hamode_db.py
new file mode 100644 (file)
index 0000000..19ecf3c
--- /dev/null
@@ -0,0 +1,459 @@
+# 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)
diff --git a/neutron/db/migration/alembic_migrations/versions/16a27a58e093_ext_l3_ha_mode.py b/neutron/db/migration/alembic_migrations/versions/16a27a58e093_ext_l3_ha_mode.py
new file mode 100644 (file)
index 0000000..cb1cb04
--- /dev/null
@@ -0,0 +1,86 @@
+# 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')
index afbbf75d328e772531a3b1eaac423c31d31bb29c..487d741ba15b3a649f314b846264af4cfb20eb55 100644 (file)
@@ -1 +1 @@
-86d6d9776e2b
+16a27a58e093
index d15d3df7967f924708465463c717d1fc939bebc1..47cb2630bcb5eaa5075c9d2ccda6c299896b2504 100644 (file)
@@ -34,6 +34,7 @@ from neutron.db import l3_attrs_db  # noqa
 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
diff --git a/neutron/extensions/l3_ext_ha_mode.py b/neutron/extensions/l3_ext_ha_mode.py
new file mode 100644 (file)
index 0000000..f8487bb
--- /dev/null
@@ -0,0 +1,91 @@
+# 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 {}
index 15e0cbf4ac16626517902a01fb3d1282c21ebdcd..671db44bce41d13366d516504f9430b849b23022 100644 (file)
@@ -24,18 +24,18 @@ from neutron.common import rpc as n_rpc
 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.
 
@@ -43,17 +43,19 @@ class L3RouterPlugin(common_db_mixin.CommonDbMixin,
     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
index 9612aa7b2a9348d2b6c7f57199271fe9bd0a68e0..be27ce9ca409478d5d401bd39e654afeeaf32436 100644 (file)
@@ -118,7 +118,7 @@ class L3DvrTestCase(testlib_api.SqlTestCase):
             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):
diff --git a/neutron/tests/unit/db/test_l3_ha_db.py b/neutron/tests/unit/db/test_l3_ha_db.py
new file mode 100644 (file)
index 0000000..4616612
--- /dev/null
@@ -0,0 +1,390 @@
+# 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)