]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Arista Layer 3 Sevice Plugin
authorSukhdev <sukhdev@aristanetworks.com>
Wed, 13 Aug 2014 23:37:58 +0000 (16:37 -0700)
committerSukhdev <sukhdev@aristanetworks.com>
Tue, 19 Aug 2014 22:08:09 +0000 (15:08 -0700)
This sevice plugin implements routing functions on Arista HW.

Change-Id: Ide411540254db015167111defee7d8c6c1c27347
Implements: blueprint arista-l3-service-plugin

etc/neutron/plugins/ml2/ml2_conf_arista.ini
neutron/plugins/ml2/drivers/arista/arista_l3_driver.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/arista/config.py
neutron/plugins/ml2/drivers/arista/exceptions.py
neutron/plugins/ml2/drivers/arista/mechanism_arista.py
neutron/services/l3_router/l3_arista.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/arista/test_arista_l3_driver.py [new file with mode: 0644]

index a4cfee0cd288e46962e385708e16f1bc99000757..abaf5bc7c9294619be2e12caba103ad8bde9342f 100644 (file)
 #
 # region_name =
 # Example: region_name = RegionOne
+
+
+[l3_arista]
+
+# (StrOpt) primary host IP address. This is required field. If not set, all
+#          communications to Arista EOS will fail. This is the host where
+#          primary router is created.
+#
+# primary_l3_host =
+# Example: primary_l3_host = 192.168.10.10
+#
+# (StrOpt) Primary host username. This is required field.
+#          if not set, all communications to Arista EOS will fail.
+#
+# primary_l3_host_username =
+# Example: arista_primary_l3_username = admin
+#
+# (StrOpt) Primary host password. This is required field.
+#          if not set, all communications to Arista EOS will fail.
+#
+# primary_l3_host_password =
+# Example: primary_l3_password = my_password
+#
+# (StrOpt) IP address of the second Arista switch paired as
+#          MLAG (Multi-chassis Link Aggregation) with the first.
+#          This is optional field, however, if mlag_config flag is set,
+#          then this is a required field. If not set, all
+#          communications to Arista EOS will fail. If mlag_config is set
+#          to False, then this field is ignored
+#
+# seconadary_l3_host =
+# Example: seconadary_l3_host = 192.168.10.20
+#
+# (BoolOpt) Defines if Arista switches are configured in MLAG mode
+#          If yes, all L3 configuration is pushed to both switches
+#          automatically. If this flag is set, ensure that secondary_l3_host
+#          is set to the second switch's IP.
+#          This flag is Optional. If not set, a value of "False" is assumed.
+#
+# mlag_config =
+# Example: mlag_config = True
+#
+# (BoolOpt) Defines if the router is created in default VRF or a
+#          a specific VRF. This is optional.
+#          If not set, a value of "False" is assumed.
+#
+# Example: use_vrf = True
+#
+# (IntOpt) Sync interval in seconds between Neutron plugin and EOS.
+#          This field defines how often the synchronization is performed.
+#          This is an optional field. If not set, a value of 180 seconds
+#          is assumed.
+#
+# l3_sync_interval =
+# Example: l3_sync_interval = 60
diff --git a/neutron/plugins/ml2/drivers/arista/arista_l3_driver.py b/neutron/plugins/ml2/drivers/arista/arista_l3_driver.py
new file mode 100644 (file)
index 0000000..a879f1a
--- /dev/null
@@ -0,0 +1,457 @@
+# Copyright 2014 Arista Networks, Inc.  All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+# @author: Sukhdev Kapur, Arista Networks, Inc.
+#
+
+import hashlib
+import socket
+import struct
+
+import jsonrpclib
+from oslo.config import cfg
+
+from neutron import context as nctx
+from neutron.db import db_base_plugin_v2
+from neutron.openstack.common import log as logging
+from neutron.plugins.ml2.drivers.arista import exceptions as arista_exc
+
+LOG = logging.getLogger(__name__)
+
+EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
+DEFAULT_VLAN = 1
+MLAG_SWITCHES = 2
+VIRTUAL_ROUTER_MAC = '00:11:22:33:44:55'
+IPV4_BITS = 32
+IPV6_BITS = 128
+
+router_in_vrf = {
+    'router': {'create': ['vrf definition {0}',
+                          'rd {1}',
+                          'exit'],
+               'delete': ['no vrf definition {0}']},
+
+    'interface': {'add': ['ip routing vrf {1}',
+                          'vlan {0}',
+                          'exit',
+                          'interface vlan {0}',
+                          'vrf forwarding {1}',
+                          'ip address {2}'],
+                  'remove': ['no interface vlan {0}']}}
+
+router_in_default_vrf = {
+    'router': {'create': [],   # Place holder for now.
+               'delete': []},  # Place holder for now.
+
+    'interface': {'add': ['ip routing',
+                          'vlan {0}',
+                          'exit',
+                          'interface vlan {0}',
+                          'ip address {2}'],
+                  'remove': ['no interface vlan {0}']}}
+
+router_in_default_vrf_v6 = {
+    'router': {'create': [],
+               'delete': []},
+
+    'interface': {'add': ['ipv6 unicast-routing',
+                          'vlan {0}',
+                          'exit',
+                          'interface vlan {0}',
+                          'ipv6 enable',
+                          'ipv6 address {2}'],
+                  'remove': ['no interface vlan {0}']}}
+
+additional_cmds_for_mlag = {
+    'router': {'create': ['ip virtual-router mac-address {0}'],
+               'delete': ['no ip virtual-router mac-address']},
+
+    'interface': {'add': ['ip virtual-router address {0}'],
+                  'remove': []}}
+
+additional_cmds_for_mlag_v6 = {
+    'router': {'create': [],
+               'delete': []},
+
+    'interface': {'add': ['ipv6 virtual-router address {0}'],
+                  'remove': []}}
+
+
+class AristaL3Driver(object):
+    """Wraps Arista JSON RPC.
+
+    All communications between Neutron and EOS are over JSON RPC.
+    EOS - operating system used on Arista hardware
+    Command API - JSON RPC API provided by Arista EOS
+    """
+    def __init__(self):
+        self._servers = []
+        self._hosts = []
+        self.interfaceDict = None
+        self._validate_config()
+        host = cfg.CONF.l3_arista.primary_l3_host
+        self._hosts.append(host)
+        self._servers.append(jsonrpclib.Server(self._eapi_host_url(host)))
+        self.mlag_configured = cfg.CONF.l3_arista.mlag_config
+        self.use_vrf = cfg.CONF.l3_arista.use_vrf
+        if self.mlag_configured:
+            host = cfg.CONF.l3_arista.secondary_l3_host
+            self._hosts.append(host)
+            self._servers.append(jsonrpclib.Server(self._eapi_host_url(host)))
+            self._additionalRouterCmdsDict = additional_cmds_for_mlag['router']
+            self._additionalInterfaceCmdsDict = (
+                additional_cmds_for_mlag['interface'])
+        if self.use_vrf:
+            self.routerDict = router_in_vrf['router']
+            self.interfaceDict = router_in_vrf['interface']
+        else:
+            self.routerDict = router_in_default_vrf['router']
+            self.interfaceDict = router_in_default_vrf['interface']
+
+    def _eapi_host_url(self, host):
+        user = cfg.CONF.l3_arista.primary_l3_host_username
+        pwd = cfg.CONF.l3_arista.primary_l3_host_password
+
+        eapi_server_url = ('https://%s:%s@%s/command-api' %
+                           (user, pwd, host))
+        return eapi_server_url
+
+    def _validate_config(self):
+        if cfg.CONF.l3_arista.get('primary_l3_host') == '':
+            msg = _('Required option primary_l3_host is not set')
+            LOG.error(msg)
+            raise arista_exc.AristaSevicePluginConfigError(msg=msg)
+        if cfg.CONF.l3_arista.get('mlag_config'):
+            if cfg.CONF.l3_arista.get('use_vrf'):
+                #This is invalid/unsupported configuration
+                msg = _('VRFs are not supported MLAG config mode')
+                LOG.error(msg)
+                raise arista_exc.AristaSevicePluginConfigError(msg=msg)
+            if cfg.CONF.l3_arista.get('secondary_l3_host') == '':
+                msg = _('Required option secondary_l3_host is not set')
+                LOG.error(msg)
+                raise arista_exc.AristaSevicePluginConfigError(msg=msg)
+        if cfg.CONF.l3_arista.get('primary_l3_host_username') == '':
+            msg = _('Required option primary_l3_host_username is not set')
+            LOG.error(msg)
+            raise arista_exc.AristaSevicePluginConfigError(msg=msg)
+
+    def create_router_on_eos(self, router_name, rdm, server):
+        """Creates a router on Arista HW Device.
+
+        :param router_name: globally unique identifier for router/VRF
+        :param rdm: A value generated by hashing router name
+        :param server: Server endpoint on the Arista switch to be configured
+        """
+        cmds = []
+        rd = "%s:%s" % (rdm, rdm)
+
+        for c in self.routerDict['create']:
+            cmds.append(c.format(router_name, rd))
+
+        if self.mlag_configured:
+            mac = VIRTUAL_ROUTER_MAC
+            for c in self._additionalRouterCmdsDict['create']:
+                cmds.append(c.format(mac))
+
+        self._run_openstack_l3_cmds(cmds, server)
+
+    def delete_router_from_eos(self, router_name, server):
+        """Deletes a router from Arista HW Device.
+
+        :param router_name: globally unique identifier for router/VRF
+        :param server: Server endpoint on the Arista switch to be configured
+        """
+        cmds = []
+        for c in self.routerDict['delete']:
+            cmds.append(c.format(router_name))
+        if self.mlag_configured:
+            for c in self._additionalRouterCmdsDict['delete']:
+                cmds.append(c)
+
+        self._run_openstack_l3_cmds(cmds, server)
+
+    def _select_dicts(self, ipv):
+        if self.use_vrf:
+            self.interfaceDict = router_in_vrf['interface']
+        else:
+            if ipv == 6:
+                #for IPv6 use IPv6 commmands
+                self.interfaceDict = router_in_default_vrf_v6['interface']
+                self._additionalInterfaceCmdsDict = (
+                    additional_cmds_for_mlag_v6['interface'])
+            else:
+                self.interfaceDict = router_in_default_vrf['interface']
+                self._additionalInterfaceCmdsDict = (
+                    additional_cmds_for_mlag['interface'])
+
+    def add_interface_to_router(self, segment_id,
+                                router_name, gip, router_ip, mask, server):
+        """Adds an interface to existing HW router on Arista HW device.
+
+        :param segment_id: VLAN Id associated with interface that is added
+        :param router_name: globally unique identifier for router/VRF
+        :param gip: Gateway IP associated with the subnet
+        :param router_ip: IP address of the router
+        :param mask: subnet mask to be used
+        :param server: Server endpoint on the Arista switch to be configured
+        """
+
+        if not segment_id:
+            segment_id = DEFAULT_VLAN
+        cmds = []
+        for c in self.interfaceDict['add']:
+            if self.mlag_configured:
+                ip = router_ip
+            else:
+                ip = gip + '/' + mask
+            cmds.append(c.format(segment_id, router_name, ip))
+        if self.mlag_configured:
+            for c in self._additionalInterfaceCmdsDict['add']:
+                cmds.append(c.format(gip))
+
+        self._run_openstack_l3_cmds(cmds, server)
+
+    def delete_interface_from_router(self, segment_id, router_name, server):
+        """Deletes an interface from existing HW router on Arista HW device.
+
+        :param segment_id: VLAN Id associated with interface that is added
+        :param router_name: globally unique identifier for router/VRF
+        :param server: Server endpoint on the Arista switch to be configured
+        """
+
+        if not segment_id:
+            segment_id = DEFAULT_VLAN
+        cmds = []
+        for c in self.interfaceDict['remove']:
+            cmds.append(c.format(segment_id))
+
+        self._run_openstack_l3_cmds(cmds, server)
+
+    def create_router(self, context, tenant_id, router):
+        """Creates a router on Arista Switch.
+
+        Deals with multiple configurations - such as Router per VRF,
+        a router in default VRF, Virtual Router in MLAG configurations
+        """
+        if router:
+            router_name = self._arista_router_name(tenant_id, router['name'])
+
+            rdm = str(int(hashlib.sha256(router_name).hexdigest(),
+                    16) % 6553)
+            for s in self._servers:
+                self.create_router_on_eos(router_name, rdm, s)
+
+    def delete_router(self, context, tenant_id, router_id, router):
+        """Deletes a router from Arista Switch."""
+
+        if router:
+            for s in self._servers:
+                self.delete_router_from_eos(self._arista_router_name(
+                                               tenant_id, router['name']), s)
+
+    def update_router(self, context, router_id, original_router, new_router):
+        """Updates a router which is already created on Arista Switch.
+
+        TODO: (Sukhdev) - to be implemented in next release.
+        """
+        pass
+
+    def add_router_interface(self, context, router_info):
+        """Adds an interface to a router created on Arista HW router.
+
+        This deals with both IPv6 and IPv4 configurations.
+        """
+        if router_info:
+            self._select_dicts(router_info['ip_version'])
+            cidr = router_info['cidr']
+            subnet_mask = cidr.split('/')[1]
+            router_name = self._arista_router_name(router_info['tenant_id'],
+                                                   router_info['name'])
+            if self.mlag_configured:
+                # For MLAG, we send a specific IP address as opposed to cidr
+                # For now, we are using x.x.x.253 and x.x.x.254 as virtual IP
+                for i, server in enumerate(self._servers):
+                    #get appropriate virtual IP address for this router
+                    router_ip = self._get_router_ip(cidr, i,
+                                                    router_info['ip_version'])
+                    self.add_interface_to_router(router_info['seg_id'],
+                                                 router_name,
+                                                 router_info['gip'],
+                                                 router_ip, subnet_mask,
+                                                 server)
+
+            else:
+                for s in self._servers:
+                    self.add_interface_to_router(router_info['seg_id'],
+                                                 router_name,
+                                                 router_info['gip'],
+                                                 None, subnet_mask, s)
+
+    def remove_router_interface(self, context, router_info):
+        """Removes previously configured interface from router on Arista HW.
+
+        This deals with both IPv6 and IPv4 configurations.
+        """
+        if router_info:
+            router_name = self._arista_router_name(router_info['tenant_id'],
+                                                   router_info['name'])
+            for s in self._servers:
+                self.delete_interface_from_router(router_info['seg_id'],
+                                                  router_name, s)
+
+    def _run_openstack_l3_cmds(self, commands, server):
+        """Execute/sends a CAPI (Command API) command to EOS.
+
+        In this method, list of commands is appended with prefix and
+        postfix commands - to make is understandble by EOS.
+
+        :param commands : List of command to be executed on EOS.
+        :param server: Server endpoint on the Arista switch to be configured
+        """
+        command_start = ['enable', 'configure']
+        command_end = ['exit']
+        full_command = command_start + commands + command_end
+
+        LOG.info(_('Executing command on Arista EOS: %s'), full_command)
+
+        try:
+            # this returns array of return values for every command in
+            # full_command list
+            ret = server.runCmds(version=1, cmds=full_command)
+            LOG.info(_('Results of execution on Arista EOS: %s'), ret)
+
+        except Exception:
+            msg = (_('Error occured while trying to execute '
+                     'commands %(cmd)s on EOS %(host)s') %
+                   {'cmd': full_command, 'host': server})
+            LOG.exception(msg)
+            raise arista_exc.AristaServicePluginRpcError(msg=msg)
+
+    def _arista_router_name(self, tenant_id, name):
+        # Use a unique name so that OpenStack created routers/SVIs
+        # can be distinguishged from the user created routers/SVIs
+        # on Arista HW.
+        return 'OS' + '-' + tenant_id + '-' + name
+
+    def _get_binary_from_ipv4(self, ip_addr):
+        return struct.unpack("!L", socket.inet_pton(socket.AF_INET,
+                                                    ip_addr))[0]
+
+    def _get_binary_from_ipv6(self, ip_addr):
+        hi, lo = struct.unpack("!QQ", socket.inet_pton(socket.AF_INET6,
+                                                       ip_addr))
+        return (hi << 64) | lo
+
+    def _get_ipv4_from_binary(self, bin_addr):
+        return socket.inet_ntop(socket.AF_INET, struct.pack("!L", bin_addr))
+
+    def _get_ipv6_from_binary(self, bin_addr):
+        hi = bin_addr >> 64
+        lo = bin_addr & 0xFFFFFFFF
+        return socket.inet_ntop(socket.AF_INET6, struct.pack("!QQ", hi, lo))
+
+    def _get_router_ip(self, cidr, ip_count, ip_ver):
+        """ For a given IP subnet and IP version type, generate IP for router.
+
+        This method takes the network address (cidr) and selects an
+        IP address that should be assigned to virtual router running
+        on multiple switches. It uses upper addresses in a subnet address
+        as IP for the router. Each instace of the router, on each switch,
+        requires uniqe IP address. For example in IPv4 case, on a 255
+        subnet, it will pick X.X.X.254 as first addess, X.X.X.253 for next,
+        and so on.
+        """
+        start_ip = MLAG_SWITCHES + ip_count
+        network_addr, prefix = cidr.split('/')
+        if ip_ver == 4:
+            bits = IPV4_BITS
+            ip = self._get_binary_from_ipv4(network_addr)
+        elif ip_ver == 6:
+            bits = IPV6_BITS
+            ip = self._get_binary_from_ipv6(network_addr)
+
+        mask = (pow(2, bits) - 1) << (bits - int(prefix))
+
+        network_addr = ip & mask
+
+        router_ip = pow(2, bits - int(prefix)) - start_ip
+
+        router_ip = network_addr | router_ip
+        if ip_ver == 4:
+            return self._get_ipv4_from_binary(router_ip) + '/' + prefix
+        else:
+            return self._get_ipv6_from_binary(router_ip) + '/' + prefix
+
+
+class NeutronNets(db_base_plugin_v2.NeutronDbPluginV2):
+    """Access to Neutron DB.
+
+    Provides access to the Neutron Data bases for all provisioned
+    networks as well ports. This data is used during the synchronization
+    of DB between ML2 Mechanism Driver and Arista EOS
+    Names of the networks and ports are not stored in Arista repository
+    They are pulled from Neutron DB.
+    """
+
+    def __init__(self):
+        self.admin_ctx = nctx.get_admin_context()
+
+    def get_all_networks_for_tenant(self, tenant_id):
+        filters = {'tenant_id': [tenant_id]}
+        return super(NeutronNets,
+                     self).get_networks(self.admin_ctx, filters=filters) or []
+
+    def get_all_ports_for_tenant(self, tenant_id):
+        filters = {'tenant_id': [tenant_id]}
+        return super(NeutronNets,
+                     self).get_ports(self.admin_ctx, filters=filters) or []
+
+    def _get_network(self, tenant_id, network_id):
+        filters = {'tenant_id': [tenant_id],
+                   'id': [network_id]}
+        return super(NeutronNets,
+                     self).get_networks(self.admin_ctx, filters=filters) or []
+
+    def get_subnet_info(self, subnet_id):
+        subnet = self.get_subnet(subnet_id)
+        return subnet
+
+    def get_subnet_ip_version(self, subnet_id):
+        subnet = self.get_subnet(subnet_id)
+        return subnet['ip_version']
+
+    def get_subnet_gateway_ip(self, subnet_id):
+        subnet = self.get_subnet(subnet_id)
+        return subnet['gateway_ip']
+
+    def get_subnet_cidr(self, subnet_id):
+        subnet = self.get_subnet(subnet_id)
+        return subnet['cidr']
+
+    def get_network_id(self, subnet_id):
+        subnet = self.get_subnet(subnet_id)
+        return subnet['network_id']
+
+    def get_network_id_from_port_id(self, port_id):
+        port = self.get_port(port_id)
+        return port['network_id']
+
+    def get_subnet(self, subnet_id):
+        return super(NeutronNets,
+                     self).get_subnet(self.admin_ctx, subnet_id) or []
+
+    def get_port(self, port_id):
+        return super(NeutronNets,
+                     self).get_port(self.admin_ctx, port_id) or []
index 2f968c874ddb6a840fa62abd6f6faeff83e561d0..03c695acb6abe33e17ed384ddf6525ffab60894c 100644 (file)
@@ -67,4 +67,62 @@ ARISTA_DRIVER_OPTS = [
                       '"RegionOne" is assumed.'))
 ]
 
+
+""" Arista L3 Service Plugin specific configuration knobs.
+
+Following are user configurable options for Arista L3 plugin
+driver. The eapi_username, eapi_password, and eapi_host are
+required options.
+"""
+
+ARISTA_L3_PLUGIN = [
+    cfg.StrOpt('primary_l3_host_username',
+               default='',
+               help=_('Username for Arista EOS. This is required field. '
+                      'If not set, all communications to Arista EOS '
+                      'will fail')),
+    cfg.StrOpt('primary_l3_host_password',
+               default='',
+               secret=True,  # do not expose value in the logs
+               help=_('Password for Arista EOS. This is required field. '
+                      'If not set, all communications to Arista EOS '
+                      'will fail')),
+    cfg.StrOpt('primary_l3_host',
+               default='',
+               help=_('Arista EOS IP address. This is required field. '
+                      'If not set, all communications to Arista EOS '
+                      'will fail')),
+    cfg.StrOpt('secondary_l3_host',
+               default='',
+               help=_('Arista EOS IP address for second Switch MLAGed with '
+                      'the first one. This an optional field, however, if '
+                      'mlag_config flag is set, then this is required. '
+                      'If not set, all communications to Arista EOS '
+                      'will fail')),
+    cfg.BoolOpt('mlag_config',
+                default=False,
+                help=_('This flag is used indicate if Arista Switches are '
+                       'configured in MLAG mode. If yes, all L3 config '
+                       'is pushed to both the switches automatically. '
+                       'If this flag is set to True, ensure to specify IP '
+                       'addresses of both switches. '
+                       'This is optional. If not set, a value of "False" '
+                       'is assumed.')),
+    cfg.BoolOpt('use_vrf',
+                default=False,
+                help=_('A "True" value for this flag indicates to create a '
+                       'router in VRF. If not set, all routers are created '
+                       'in default VRF.'
+                       'This is optional. If not set, a value of "False" '
+                       'is assumed.')),
+    cfg.IntOpt('l3_sync_interval',
+               default=180,
+               help=_('Sync interval in seconds between L3 Service plugin '
+                      'and EOS. This interval defines how often the '
+                      'synchronization is performed. This is an optional '
+                      'field. If not set, a value of 180 seconds is assumed'))
+]
+
+cfg.CONF.register_opts(ARISTA_L3_PLUGIN, "l3_arista")
+
 cfg.CONF.register_opts(ARISTA_DRIVER_OPTS, "ml2_arista")
index b3dae3dae297607ca6eed6e6a3df7ce408cfa957..73802c2f7cbb1ea2bf617bd6cae1b9b5f69edb43 100644 (file)
@@ -25,3 +25,11 @@ class AristaRpcError(exceptions.NeutronException):
 
 class AristaConfigError(exceptions.NeutronException):
     message = _('%(msg)s')
+
+
+class AristaServicePluginRpcError(exceptions.NeutronException):
+    message = _('%(msg)s')
+
+
+class AristaSevicePluginConfigError(exceptions.NeutronException):
+    message = _('%(msg)s')
index 0e9c5b52b520d6df3760e189c6d63066c2d3e8ca..b52c98bce5653f3fd0ebedd65820393962052c9e 100644 (file)
@@ -29,6 +29,7 @@ from neutron.plugins.ml2.drivers.arista import exceptions as arista_exc
 LOG = logging.getLogger(__name__)
 
 EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
+DEFAULT_VLAN = 1
 
 
 class AristaRPCWrapper(object):
@@ -223,6 +224,8 @@ class AristaRPCWrapper(object):
             except KeyError:
                 append_cmd('network id %s' % network['network_id'])
             # Enter segment mode without exiting out of network mode
+            if not network['segmentation_id']:
+                network['segmentation_id'] = DEFAULT_VLAN
             append_cmd('segment 1 type vlan id %d' %
                        network['segmentation_id'])
         cmds.extend(self._get_exit_mode_cmds(['segment', 'network', 'tenant']))
diff --git a/neutron/services/l3_router/l3_arista.py b/neutron/services/l3_router/l3_arista.py
new file mode 100644 (file)
index 0000000..82200b2
--- /dev/null
@@ -0,0 +1,294 @@
+# Copyright 2014 Arista Networks, Inc.  All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+# @author: Sukhdev Kapur, Arista Networks, Inc.
+#
+
+import copy
+import threading
+
+from oslo.config import cfg
+
+from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api
+from neutron.common import constants as q_const
+from neutron.common import log
+from neutron.common import rpc as q_rpc
+from neutron.common import topics
+from neutron import context as nctx
+from neutron.db import db_base_plugin_v2
+from neutron.db import extraroute_db
+from neutron.db import l3_agentschedulers_db
+from neutron.db import l3_gwmode_db
+from neutron.db import l3_rpc_base
+from neutron.openstack.common import excutils
+from neutron.openstack.common import log as logging
+from neutron.plugins.common import constants
+from neutron.plugins.ml2.driver_context import NetworkContext  # noqa
+from neutron.plugins.ml2.drivers.arista.arista_l3_driver import AristaL3Driver  # noqa
+from neutron.plugins.ml2.drivers.arista.arista_l3_driver import NeutronNets  # noqa
+
+LOG = logging.getLogger(__name__)
+
+
+class AristaL3ServicePluginRpcCallbacks(q_rpc.RpcCallback,
+                               l3_rpc_base.L3RpcCallbackMixin):
+
+    RPC_API_VERSION = '1.2'
+
+
+class AristaL3ServicePlugin(db_base_plugin_v2.NeutronDbPluginV2,
+                   extraroute_db.ExtraRoute_db_mixin,
+                   l3_gwmode_db.L3_NAT_db_mixin,
+                   l3_agentschedulers_db.L3AgentSchedulerDbMixin):
+
+    """Implements L3 Router service plugin for Arista hardware.
+
+    Creates routers in Arista hardware, manages them, adds/deletes interfaces
+    to the routes.
+    """
+
+    supported_extension_aliases = ["router", "ext-gw-mode",
+                                   "extraroute"]
+
+    def __init__(self, driver=None):
+
+        self.driver = driver or AristaL3Driver()
+        self.ndb = NeutronNets()
+        self.setup_rpc()
+        self.sync_timeout = cfg.CONF.l3_arista.l3_sync_interval
+        self.sync_lock = threading.Lock()
+        self._synchronization_thread()
+
+    def setup_rpc(self):
+        # RPC support
+        self.topic = topics.L3PLUGIN
+        self.conn = q_rpc.create_connection(new=True)
+        self.agent_notifiers.update(
+            {q_const.AGENT_TYPE_L3: l3_rpc_agent_api.L3AgentNotifyAPI()})
+        self.endpoints = [AristaL3ServicePluginRpcCallbacks()]
+        self.conn.create_consumer(self.topic, self.endpoints,
+                                  fanout=False)
+        self.conn.consume_in_threads()
+
+    def get_plugin_type(self):
+        return constants.L3_ROUTER_NAT
+
+    def get_plugin_description(self):
+        """Returns string description of the plugin."""
+        return ("Arista L3 Router Service Plugin for Arista Hardware "
+                "based routing")
+
+    def _synchronization_thread(self):
+        with self.sync_lock:
+            self.synchronize()
+
+        self.timer = threading.Timer(self.sync_timeout,
+                                     self._synchronization_thread)
+        self.timer.start()
+
+    def stop_synchronization_thread(self):
+        if self.timer:
+            self.timer.cancel()
+            self.timer = None
+
+    @log.log
+    def create_router(self, context, router):
+        """Create a new router entry in DB, and create it Arista HW."""
+
+        tenant_id = self._get_tenant_id_for_create(context, router['router'])
+
+        # Add router to the DB
+        with context.session.begin(subtransactions=True):
+            new_router = super(AristaL3ServicePlugin, self).create_router(
+                                                                 context,
+                                                                 router)
+        # create router on the Arista Hw
+        try:
+            self.driver.create_router(context, tenant_id, new_router)
+            return new_router
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_("Error creating router on Arista HW "
+                            "router=%s ") % new_router)
+                super(AristaL3ServicePlugin, self).delete_router(context,
+                                                    new_router['id'])
+
+    @log.log
+    def update_router(self, context, router_id, router):
+        """Update an existing router in DB, and update it in Arista HW."""
+
+        with context.session.begin(subtransactions=True):
+            # Read existing router record from DB
+            original_router = super(AristaL3ServicePlugin, self).get_router(
+                                    context, router_id)
+            # Update router DB
+            new_router = super(AristaL3ServicePlugin, self).update_router(
+                context, router_id, router)
+
+        # Modify router on the Arista Hw
+        try:
+            self.driver.update_router(context, router_id,
+                                      original_router, new_router)
+            return new_router
+        except Exception:
+            LOG.error(_("Error updating router on Arista HW "
+                        "router=%s ") % new_router)
+
+    @log.log
+    def delete_router(self, context, router_id):
+        """Delete an existing router from Arista HW as well as from the DB."""
+
+        router = super(AristaL3ServicePlugin, self).get_router(context,
+                                                               router_id)
+        tenant_id = router['tenant_id']
+
+        # Delete router on the Arista Hw
+        try:
+            self.driver.delete_router(context, tenant_id, router_id, router)
+        except Exception as e:
+            LOG.error(_("Error deleting router on Arista HW "
+                        "router %(r)s exception=%(e)s") %
+                      {'r': router, 'e': e})
+
+        with context.session.begin(subtransactions=True):
+            super(AristaL3ServicePlugin, self).delete_router(context,
+                                                             router_id)
+
+    @log.log
+    def add_router_interface(self, context, router_id, interface_info):
+        """Add a subnet of a network to an existing router."""
+
+        new_router = super(AristaL3ServicePlugin, self).add_router_interface(
+            context, router_id, interface_info)
+
+        # Get network info for the subnet that is being added to the router.
+        # Check if the interface information is by port-id or subnet-id
+        add_by_port, add_by_sub = self._validate_interface_info(interface_info)
+        if add_by_sub:
+            subnet = self.get_subnet(context, interface_info['subnet_id'])
+        elif add_by_port:
+            port = self.get_port(context, interface_info['port_id'])
+            subnet_id = port['fixed_ips'][0]['subnet_id']
+            subnet = self.get_subnet(context, subnet_id)
+        network_id = subnet['network_id']
+
+        # To create SVI's in Arista HW, the segmentation Id is required
+        # for this network.
+        ml2_db = NetworkContext(self, context, {'id': network_id})
+        seg_id = ml2_db.network_segments[0]['segmentation_id']
+
+        # Package all the info needed for Hw programming
+        router = super(AristaL3ServicePlugin, self).get_router(context,
+                                                               router_id)
+        router_info = copy.deepcopy(new_router)
+        router_info['seg_id'] = seg_id
+        router_info['name'] = router['name']
+        router_info['cidr'] = subnet['cidr']
+        router_info['gip'] = subnet['gateway_ip']
+        router_info['ip_version'] = subnet['ip_version']
+
+        try:
+            self.driver.add_router_interface(context, router_info)
+            return new_router
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_("Error Adding subnet %(subnet)s to "
+                            "router %(router_id)s on Arista HW") %
+                          {'subnet': subnet, 'router_id': router_id})
+                super(AristaL3ServicePlugin, self).remove_router_interface(
+                                                    context,
+                                                    router_id,
+                                                    interface_info)
+
+    @log.log
+    def remove_router_interface(self, context, router_id, interface_info):
+        """Remove a subnet of a network from an existing router."""
+
+        new_router = (
+                   super(AristaL3ServicePlugin, self).remove_router_interface(
+                         context, router_id, interface_info))
+
+        # Get network information of the subnet that is being removed
+        subnet = self.get_subnet(context, new_router['subnet_id'])
+        network_id = subnet['network_id']
+
+        # For SVI removal from Arista HW, segmentation ID is needed
+        ml2_db = NetworkContext(self, context, {'id': network_id})
+        seg_id = ml2_db.network_segments[0]['segmentation_id']
+
+        router = super(AristaL3ServicePlugin, self).get_router(context,
+                                                               router_id)
+        router_info = copy.deepcopy(new_router)
+        router_info['seg_id'] = seg_id
+        router_info['name'] = router['name']
+
+        try:
+            self.driver.remove_router_interface(context, router_info)
+            return new_router
+        except Exception as exc:
+            LOG.error(_("Error removing interface %(interface)s from "
+                        "router %(router_id)s on Arista HW"
+                        "Exception =(exc)s") % {'interface': interface_info,
+                                                'router_id': router_id,
+                                                'exc': exc})
+
+    def synchronize(self):
+        """Synchronizes Router DB from Neturon DB with EOS.
+
+        Walks through the Neturon Db and ensures that all the routers
+        created in Netuton DB match with EOS. After creating appropriate
+        routers, it ensures to add interfaces as well.
+        Uses idempotent properties of EOS configuration, which means
+        same commands can be repeated.
+        """
+        LOG.info(_('Syncing Neutron Router DB <-> EOS'))
+        ctx = nctx.get_admin_context()
+
+        routers = super(AristaL3ServicePlugin, self).get_routers(ctx)
+        for r in routers:
+            tenant_id = r['tenant_id']
+            ports = self.ndb.get_all_ports_for_tenant(tenant_id)
+
+            try:
+                self.driver.create_router(self, tenant_id, r)
+
+            except Exception:
+                continue
+
+            # Figure out which interfaces are added to this router
+            for p in ports:
+                if p['device_id'] == r['id']:
+                    net_id = p['network_id']
+                    subnet_id = p['fixed_ips'][0]['subnet_id']
+                    subnet = self.ndb.get_subnet_info(subnet_id)
+                    ml2_db = NetworkContext(self, ctx, {'id': net_id})
+                    seg_id = ml2_db.network_segments[0]['segmentation_id']
+
+                    r['seg_id'] = seg_id
+                    r['cidr'] = subnet['cidr']
+                    r['gip'] = subnet['gateway_ip']
+                    r['ip_version'] = subnet['ip_version']
+
+                    try:
+                        self.driver.add_router_interface(self, r)
+                    except Exception:
+                        LOG.error(_("Error Adding interface %(subnet_id)s to "
+                                    "router %(router_id)s on Arista HW") %
+                                  {'subnet_id': subnet_id,
+                                   'router_id': r})
+
+    def _validate_interface_info(self, interface_info):
+        port_id_specified = interface_info and 'port_id' in interface_info
+        subnet_id_specified = interface_info and 'subnet_id' in interface_info
+        return port_id_specified, subnet_id_specified
diff --git a/neutron/tests/unit/ml2/drivers/arista/test_arista_l3_driver.py b/neutron/tests/unit/ml2/drivers/arista/test_arista_l3_driver.py
new file mode 100644 (file)
index 0000000..7b0649e
--- /dev/null
@@ -0,0 +1,396 @@
+# Copyright (c) 2013 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.
+#
+# @author: Sukhdev Kapur, Arista Networks, Inc.
+#
+
+
+import mock
+from oslo.config import cfg
+
+from neutron.plugins.ml2.drivers.arista import arista_l3_driver as arista
+from neutron.tests import base
+
+
+def setup_arista_config(value='', vrf=False, mlag=False):
+    cfg.CONF.set_override('primary_l3_host', value, "l3_arista")
+    cfg.CONF.set_override('primary_l3_host_username', value, "l3_arista")
+    if vrf:
+        cfg.CONF.set_override('use_vrf', value, "l3_arista")
+    if mlag:
+        cfg.CONF.set_override('secondary_l3_host', value, "l3_arista")
+        cfg.CONF.set_override('mlag_config', value, "l3_arista")
+
+
+class AristaL3DriverTestCasesDefaultVrf(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions in Default VRF.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCasesDefaultVrf, self).setUp()
+        setup_arista_config('value')
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_create_router_on_eos(self):
+        router_name = 'test-router-1'
+        route_domain = '123:123'
+
+        self.drv.create_router_on_eos(router_name, route_domain,
+                                      self.drv._servers[0])
+        cmds = ['enable', 'configure', 'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+    def test_delete_router_from_eos(self):
+        router_name = 'test-router-1'
+
+        self.drv.delete_router_from_eos(router_name, self.drv._servers[0])
+        cmds = ['enable', 'configure', 'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+    def test_add_interface_to_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+        router_ip = '10.10.10.10'
+        gw_ip = '10.10.10.1'
+        mask = '255.255.255.0'
+
+        self.drv.add_interface_to_router(segment_id, router_name, gw_ip,
+                                         router_ip, mask, self.drv._servers[0])
+        cmds = ['enable', 'configure', 'ip routing',
+                'vlan %s' % segment_id, 'exit',
+                'interface vlan %s' % segment_id,
+                'ip address %s/%s' % (gw_ip, mask), 'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+    def test_delete_interface_from_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+
+        self.drv.delete_interface_from_router(segment_id, router_name,
+                                              self.drv._servers[0])
+        cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
+                'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+
+class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions using multiple VRFs.
+    Note that the configuration commands are different when VRFs are used.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCasesUsingVRFs, self).setUp()
+        setup_arista_config('value', vrf=True)
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_create_router_on_eos(self):
+        max_vrfs = 5
+        routers = ['testRouter-%s' % n for n in range(max_vrfs)]
+        domains = ['10%s' % n for n in range(max_vrfs)]
+
+        for (r, d) in zip(routers, domains):
+            self.drv.create_router_on_eos(r, d, self.drv._servers[0])
+
+            cmds = ['enable', 'configure',
+                    'vrf definition %s' % r,
+                    'rd %(rd)s:%(rd)s' % {'rd': d}, 'exit', 'exit']
+
+            self.drv._servers[0].runCmds.assert_called_with(version=1,
+                                                            cmds=cmds)
+
+    def test_delete_router_from_eos(self):
+        max_vrfs = 5
+        routers = ['testRouter-%s' % n for n in range(max_vrfs)]
+
+        for r in routers:
+            self.drv.delete_router_from_eos(r, self.drv._servers[0])
+            cmds = ['enable', 'configure', 'no vrf definition %s' % r,
+                    'exit']
+
+            self.drv._servers[0].runCmds.assert_called_with(version=1,
+                                                            cmds=cmds)
+
+    def test_add_interface_to_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+        router_ip = '10.10.10.10'
+        gw_ip = '10.10.10.1'
+        mask = '255.255.255.0'
+
+        self.drv.add_interface_to_router(segment_id, router_name, gw_ip,
+                                         router_ip, mask, self.drv._servers[0])
+        cmds = ['enable', 'configure',
+                'ip routing vrf %s' % router_name,
+                'vlan %s' % segment_id, 'exit',
+                'interface vlan %s' % segment_id,
+                'vrf forwarding %s' % router_name,
+                'ip address %s/%s' % (gw_ip, mask), 'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+    def test_delete_interface_from_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+
+        self.drv.delete_interface_from_router(segment_id, router_name,
+                                              self.drv._servers[0])
+        cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
+                'exit']
+
+        self.drv._servers[0].runCmds.assert_called_once_with(version=1,
+                                                             cmds=cmds)
+
+
+class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions in Default VRF using MLAG configuration.
+    MLAG configuration means that the commands will be sent to both
+    primary and secondary Arista Switches.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCasesMlagConfig, self).setUp()
+        setup_arista_config('value', mlag=True)
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_create_router_on_eos(self):
+        router_name = 'test-router-1'
+        route_domain = '123:123'
+        router_mac = '00:11:22:33:44:55'
+
+        for s in self.drv._servers:
+            self.drv.create_router_on_eos(router_name, route_domain, s)
+            cmds = ['enable', 'configure',
+                    'ip virtual-router mac-address %s' % router_mac, 'exit']
+
+            s.runCmds.assert_called_with(version=1, cmds=cmds)
+
+    def test_delete_router_from_eos(self):
+        router_name = 'test-router-1'
+
+        for s in self.drv._servers:
+            self.drv.delete_router_from_eos(router_name, s)
+            cmds = ['enable', 'configure',
+                    'no ip virtual-router mac-address', 'exit']
+
+            s.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+    def test_add_interface_to_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+        router_ip = '10.10.10.10'
+        gw_ip = '10.10.10.1'
+        mask = '255.255.255.0'
+
+        for s in self.drv._servers:
+            self.drv.add_interface_to_router(segment_id, router_name, gw_ip,
+                                             router_ip, mask, s)
+            cmds = ['enable', 'configure', 'ip routing',
+                    'vlan %s' % segment_id, 'exit',
+                    'interface vlan %s' % segment_id,
+                    'ip address %s' % router_ip,
+                    'ip virtual-router address %s' % gw_ip, 'exit']
+
+            s.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+    def test_delete_interface_from_router_on_eos(self):
+        router_name = 'test-router-1'
+        segment_id = '123'
+
+        for s in self.drv._servers:
+            self.drv.delete_interface_from_router(segment_id, router_name, s)
+
+            cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
+                    'exit']
+
+            s.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+
+class AristaL3DriverTestCases_v4(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions in Default VRF using IPv4.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCases_v4, self).setUp()
+        setup_arista_config('value')
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_add_v4_interface_to_router(self):
+        gateway_ip = '10.10.10.1'
+        cidrs = ['10.10.10.0/24', '10.11.11.0/24']
+
+        # Add couple of IPv4 subnets to router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 4}
+
+            self.assertFalse(self.drv.add_router_interface(None, router))
+
+    def test_delete_v4_interface_from_router(self):
+        gateway_ip = '10.10.10.1'
+        cidrs = ['10.10.10.0/24', '10.11.11.0/24']
+
+        # remove couple of IPv4 subnets from router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 4}
+
+            self.assertFalse(self.drv.remove_router_interface(None, router))
+
+
+class AristaL3DriverTestCases_v6(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions in Default VRF using IPv6.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCases_v6, self).setUp()
+        setup_arista_config('value')
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_add_v6_interface_to_router(self):
+        gateway_ip = '3FFE::1'
+        cidrs = ['3FFE::/16', '2001::/16']
+
+        # Add couple of IPv6 subnets to router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 6}
+
+            self.assertFalse(self.drv.add_router_interface(None, router))
+
+    def test_delete_v6_interface_from_router(self):
+        gateway_ip = '3FFE::1'
+        cidrs = ['3FFE::/16', '2001::/16']
+
+        # remove couple of IPv6 subnets from router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 6}
+
+            self.assertFalse(self.drv.remove_router_interface(None, router))
+
+
+class AristaL3DriverTestCases_MLAG_v6(base.BaseTestCase):
+    """Test cases to test the RPC between Arista Driver and EOS.
+
+    Tests all methods used to send commands between Arista L3 Driver and EOS
+    to program routing functions in Default VRF on MLAG'ed switches using IPv6.
+    """
+
+    def setUp(self):
+        super(AristaL3DriverTestCases_MLAG_v6, self).setUp()
+        setup_arista_config('value', mlag=True)
+        self.drv = arista.AristaL3Driver()
+        self.drv._servers = []
+        self.drv._servers.append(mock.MagicMock())
+        self.drv._servers.append(mock.MagicMock())
+
+    def test_no_exception_on_correct_configuration(self):
+        self.assertIsNotNone(self.drv)
+
+    def test_add_v6_interface_to_router(self):
+        gateway_ip = '3FFE::1'
+        cidrs = ['3FFE::/16', '2001::/16']
+
+        # Add couple of IPv6 subnets to router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 6}
+
+            self.assertFalse(self.drv.add_router_interface(None, router))
+
+    def test_delete_v6_interface_from_router(self):
+        gateway_ip = '3FFE::1'
+        cidrs = ['3FFE::/16', '2001::/16']
+
+        # remove couple of IPv6 subnets from router
+        for cidr in cidrs:
+            router = {'name': 'test-router-1',
+                      'tenant_id': 'ten-a',
+                      'seg_id': '123',
+                      'cidr': "%s" % cidr,
+                      'gip': "%s" % gateway_ip,
+                      'ip_version': 6}
+
+            self.assertFalse(self.drv.remove_router_interface(None, router))