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:
parent
39ca279703
commit
762e909cbd
@ -43,3 +43,58 @@
|
|||||||
#
|
#
|
||||||
# region_name =
|
# region_name =
|
||||||
# Example: region_name = RegionOne
|
# 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
|
||||||
|
457
neutron/plugins/ml2/drivers/arista/arista_l3_driver.py
Normal file
457
neutron/plugins/ml2/drivers/arista/arista_l3_driver.py
Normal 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 []
|
@ -67,4 +67,62 @@ ARISTA_DRIVER_OPTS = [
|
|||||||
'"RegionOne" is assumed.'))
|
'"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")
|
cfg.CONF.register_opts(ARISTA_DRIVER_OPTS, "ml2_arista")
|
||||||
|
@ -25,3 +25,11 @@ class AristaRpcError(exceptions.NeutronException):
|
|||||||
|
|
||||||
class AristaConfigError(exceptions.NeutronException):
|
class AristaConfigError(exceptions.NeutronException):
|
||||||
message = _('%(msg)s')
|
message = _('%(msg)s')
|
||||||
|
|
||||||
|
|
||||||
|
class AristaServicePluginRpcError(exceptions.NeutronException):
|
||||||
|
message = _('%(msg)s')
|
||||||
|
|
||||||
|
|
||||||
|
class AristaSevicePluginConfigError(exceptions.NeutronException):
|
||||||
|
message = _('%(msg)s')
|
||||||
|
@ -29,6 +29,7 @@ from neutron.plugins.ml2.drivers.arista import exceptions as arista_exc
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
|
EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
|
||||||
|
DEFAULT_VLAN = 1
|
||||||
|
|
||||||
|
|
||||||
class AristaRPCWrapper(object):
|
class AristaRPCWrapper(object):
|
||||||
@ -223,6 +224,8 @@ class AristaRPCWrapper(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
append_cmd('network id %s' % network['network_id'])
|
append_cmd('network id %s' % network['network_id'])
|
||||||
# Enter segment mode without exiting out of network mode
|
# 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' %
|
append_cmd('segment 1 type vlan id %d' %
|
||||||
network['segmentation_id'])
|
network['segmentation_id'])
|
||||||
cmds.extend(self._get_exit_mode_cmds(['segment', 'network', 'tenant']))
|
cmds.extend(self._get_exit_mode_cmds(['segment', 'network', 'tenant']))
|
||||||
|
294
neutron/services/l3_router/l3_arista.py
Normal file
294
neutron/services/l3_router/l3_arista.py
Normal 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
|
396
neutron/tests/unit/ml2/drivers/arista/test_arista_l3_driver.py
Normal file
396
neutron/tests/unit/ml2/drivers/arista/test_arista_l3_driver.py
Normal 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))
|
Loading…
Reference in New Issue
Block a user