This sevice plugin implements routing functions on Arista HW.
Change-Id: Ide411540254db015167111defee7d8c6c1c27347
Implements: blueprint arista-l3-service-plugin
#
# 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
--- /dev/null
+# 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 []
'"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")
class AristaConfigError(exceptions.NeutronException):
message = _('%(msg)s')
+
+
+class AristaServicePluginRpcError(exceptions.NeutronException):
+ message = _('%(msg)s')
+
+
+class AristaSevicePluginConfigError(exceptions.NeutronException):
+ message = _('%(msg)s')
LOG = logging.getLogger(__name__)
EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
+DEFAULT_VLAN = 1
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']))
--- /dev/null
+# 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
--- /dev/null
+# 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))