Arista Layer 3 Sevice Plugin

This sevice plugin implements routing functions on Arista HW.

Change-Id: Ide411540254db015167111defee7d8c6c1c27347
Implements: blueprint arista-l3-service-plugin
This commit is contained in:
Sukhdev 2014-08-13 16:37:58 -07:00
parent 39ca279703
commit 762e909cbd
7 changed files with 1271 additions and 0 deletions

View File

@ -43,3 +43,58 @@
#
# 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

View File

@ -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 []

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

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

View 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']))

View File

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

View File

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