753 lines
31 KiB
Python
753 lines
31 KiB
Python
# 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.
|
|
|
|
import hashlib
|
|
import socket
|
|
import struct
|
|
|
|
from neutron.plugins.ml2.driver_context import NetworkContext # noqa
|
|
from neutron_lib.callbacks import events
|
|
from neutron_lib.callbacks import registry
|
|
from neutron_lib.callbacks import resources
|
|
from neutron_lib import constants as const
|
|
from neutron_lib import context as nctx
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
|
|
from networking_arista._i18n import _, _LI
|
|
from networking_arista.common import api
|
|
from networking_arista.common import db_lib
|
|
from networking_arista.common import exceptions as arista_exc
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
cfg.CONF.import_group('l3_arista', 'networking_arista.common.config')
|
|
|
|
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
|
|
|
|
# This string-format-at-a-distance confuses pylint :(
|
|
# pylint: disable=too-many-format-args
|
|
router_in_vrf_v1 = {
|
|
'router': {'create': ['vrf definition {0}',
|
|
'rd {1}',
|
|
'exit',
|
|
'ip routing vrf {0}'],
|
|
'delete': ['no vrf definition {0}']},
|
|
'interface': {'add': ['vlan {0}',
|
|
'exit',
|
|
'interface vlan {0}',
|
|
'vrf forwarding {1}',
|
|
'ip address {2}'],
|
|
'remove': ['no interface vlan {0}']}}
|
|
|
|
router_in_vrf_v2 = {
|
|
'router': {'create': ['vrf instance {0}',
|
|
'rd {1}',
|
|
'exit',
|
|
'ip routing vrf {0}'],
|
|
'delete': ['no vrf instance {0}']},
|
|
'interface': {'add': ['vlan {0}',
|
|
'exit',
|
|
'interface vlan {0}',
|
|
'vrf {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': []},
|
|
|
|
'interface': {'add': ['ip virtual-router address {0}'],
|
|
'remove': []}}
|
|
|
|
additional_cmds_for_mlag_v6 = {
|
|
'router': {'create': [],
|
|
'delete': []},
|
|
|
|
'interface': {'add': ['ipv6 virtual-router address {0}'],
|
|
'remove': []}}
|
|
|
|
additional_cmds_for_default_route = {
|
|
'add': ['ip route vrf {0} 0.0.0.0/0 {1}'],
|
|
# Remove is used when updating a network to remove default gateway
|
|
# when deleting an interface we don't need to delete the route
|
|
'remove': ['no ip route vrf {0} 0.0.0.0/0']}
|
|
|
|
|
|
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(self._make_eapi_client(host))
|
|
self._mlag_configured = cfg.CONF.l3_arista.mlag_config
|
|
self._vrf_syntax_v2_supported = None
|
|
self._router_in_vrf = router_in_vrf_v2
|
|
self._use_vrf = cfg.CONF.l3_arista.use_vrf
|
|
self._vrf_default_route = False
|
|
if self._use_vrf:
|
|
self._vrf_default_route = cfg.CONF.l3_arista.vrf_default_route
|
|
if self._vrf_default_route:
|
|
# only subscribe for events if vrf default route is enabled
|
|
self.subscribe()
|
|
if self._mlag_configured:
|
|
host = cfg.CONF.l3_arista.secondary_l3_host
|
|
self._hosts.append(host)
|
|
self._servers.append(self._make_eapi_client(host))
|
|
self._additionalRouterCmdsDict = additional_cmds_for_mlag['router']
|
|
self._additionalInterfaceCmdsDict = (
|
|
additional_cmds_for_mlag['interface'])
|
|
if self._use_vrf:
|
|
self.routerDict = self._router_in_vrf['router']
|
|
self._update_vrf_commands(keep_alive=False)
|
|
self._interfaceDict = self._router_in_vrf['interface']
|
|
else:
|
|
self.routerDict = router_in_default_vrf['router']
|
|
self._interfaceDict = router_in_default_vrf['interface']
|
|
self._enable_cleanup = cfg.CONF.l3_arista.enable_cleanup
|
|
self._protected_vlans = self._parse_protected_vlans(
|
|
cfg.CONF.l3_arista.protected_vlans)
|
|
|
|
def subscribe(self):
|
|
# Subscribe to the events related to networks and subnets
|
|
registry.subscribe(self.update_subnet, resources.SUBNET,
|
|
events.AFTER_UPDATE)
|
|
registry.subscribe(self.update_network, resources.NETWORK,
|
|
events.AFTER_UPDATE)
|
|
|
|
def update_subnet(self, resource, event, trigger, payload):
|
|
subnet_info = payload.latest_state
|
|
|
|
if subnet_info['ip_version'] == 6:
|
|
LOG.info('IPv6 networks not supported with L3 plugin')
|
|
return
|
|
ctx = nctx.get_admin_context()
|
|
ml2_db = NetworkContext(self, ctx, {'id': subnet_info['network_id']})
|
|
seg_id = ml2_db.network_segments[0]['segmentation_id']
|
|
router_info = db_lib.get_subnet_gateway_ipv4(subnet_info['id'])
|
|
if router_info:
|
|
router_info['seg_id'] = seg_id
|
|
router_name = self._arista_router_name(router_info['id'],
|
|
router_info['name'])
|
|
self._delete_default_gateway(router_name)
|
|
self.add_router_interface(ctx, router_info)
|
|
self._setup_default_gateway(router_info)
|
|
|
|
def _reset_network_default_route(self, network_id):
|
|
router_info = self._prepare_network_default_gateway(network_id)
|
|
if router_info:
|
|
router_name = self._arista_router_name(router_info['id'],
|
|
router_info['name'])
|
|
self._delete_default_gateway(router_name)
|
|
self._add_network_default_gateway(network_id)
|
|
|
|
def update_network(self, resource, event, trigger, payload):
|
|
self._reset_network_default_route(payload.resource_id)
|
|
|
|
def _prepare_network_default_gateway(self, network_id):
|
|
router_info = db_lib.get_network_gateway_ipv4(network_id)
|
|
if not router_info:
|
|
return
|
|
|
|
ip_version = router_info['ip_version']
|
|
if ip_version == 6:
|
|
LOG.info('IPv6 networks not supported with L3 plugin')
|
|
return
|
|
|
|
ctx = nctx.get_admin_context()
|
|
ml2_db = NetworkContext(self, ctx, {'id': network_id})
|
|
seg_id = ml2_db.network_segments[0]['segmentation_id']
|
|
router_info['seg_id'] = seg_id
|
|
return router_info
|
|
|
|
def _add_network_default_gateway(self, network_id):
|
|
router_info = self._prepare_network_default_gateway(network_id)
|
|
if router_info:
|
|
ctx = nctx.get_admin_context()
|
|
self.add_router_interface(ctx, router_info)
|
|
self._setup_default_gateway(router_info)
|
|
|
|
@staticmethod
|
|
def _raise_invalid_protected_vlans(vlan_string):
|
|
msg = '%s is not a valid vlan or vlan range' % vlan_string
|
|
LOG.error(msg)
|
|
raise arista_exc.AristaServicePluginConfigError(msg=msg)
|
|
|
|
def _parse_protected_vlans(self, vlan_strings):
|
|
# VLAN 1 is always protected as it exists by default on EOS
|
|
vlans = set([1])
|
|
for vlan_string in vlan_strings:
|
|
vlan_parsed = vlan_string.split(':', 2)
|
|
|
|
if len(vlan_parsed) > 2:
|
|
self._raise_invalid_protected_vlans(vlan_string)
|
|
|
|
try:
|
|
min_vlan = int(vlan_parsed[0])
|
|
except ValueError:
|
|
self._raise_invalid_protected_vlans(vlan_string)
|
|
|
|
try:
|
|
max_vlan = int(vlan_parsed[-1])
|
|
except ValueError:
|
|
self._raise_invalid_protected_vlans(vlan_string)
|
|
|
|
if not (const.MIN_VLAN_TAG <= min_vlan <=
|
|
max_vlan <= const.MAX_VLAN_TAG):
|
|
self._raise_invalid_protected_vlans(vlan_string)
|
|
vlans.update(range(min_vlan, max_vlan + 1))
|
|
return vlans
|
|
|
|
@staticmethod
|
|
def _make_eapi_client(host):
|
|
return api.EAPIClient(
|
|
host,
|
|
username=cfg.CONF.l3_arista.primary_l3_host_username,
|
|
password=cfg.CONF.l3_arista.primary_l3_host_password,
|
|
verify=False,
|
|
timeout=cfg.CONF.l3_arista.conn_timeout
|
|
)
|
|
|
|
def _supports_vrf_instance(self, version):
|
|
ver_tokens = version.split('.')
|
|
if len(ver_tokens) < 2 or int(ver_tokens[0]) < 4:
|
|
return False
|
|
if int(ver_tokens[0]) == 4 and int(ver_tokens[1]) < 22:
|
|
return False
|
|
return True
|
|
|
|
def _check_vrf_syntax_v2_support(self, host, keep_alive=True):
|
|
cmds = ['show version']
|
|
result = None
|
|
try:
|
|
result = self._run_eos_cmds(cmds, host, log_exception=False,
|
|
keep_alive=keep_alive,
|
|
update_vrf_commands=False)
|
|
LOG.info(_LI('show version result %s'), result)
|
|
except Exception:
|
|
# We don't know what exception we got return None
|
|
# At this moment we don't know what command we support for vrf
|
|
# creation
|
|
return None
|
|
|
|
return result and self._supports_vrf_instance(result[0].get('version',
|
|
''))
|
|
|
|
def _update_vrf_commands(self, keep_alive=True):
|
|
# This assumes all switches run the same version. This needs to be
|
|
# updated if we'll support distributed routing
|
|
new_vrf_support = self._check_vrf_syntax_v2_support(
|
|
self._servers[0], keep_alive=keep_alive)
|
|
|
|
if new_vrf_support == self._vrf_syntax_v2_supported:
|
|
return
|
|
|
|
LOG.info(_LI('Updating VRF command supported: %s'),
|
|
'vrf instance' if new_vrf_support else 'vrf definition')
|
|
self._vrf_syntax_v2_supported = new_vrf_support
|
|
if self._vrf_syntax_v2_supported is False:
|
|
self._router_in_vrf = router_in_vrf_v1
|
|
else:
|
|
self._router_in_vrf = router_in_vrf_v2
|
|
# we don't need to update self.interfaceDict as it is updated by
|
|
# _select_dicts function before it is used
|
|
self.routerDict = self._router_in_vrf['router']
|
|
|
|
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.AristaServicePluginConfigError(msg=msg)
|
|
if cfg.CONF.l3_arista.get('mlag_config'):
|
|
if cfg.CONF.l3_arista.get('secondary_l3_host') == '':
|
|
msg = _('Required option secondary_l3_host is not set')
|
|
LOG.error(msg)
|
|
raise arista_exc.AristaServicePluginConfigError(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.AristaServicePluginConfigError(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_config_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_config_cmds(cmds, server)
|
|
|
|
def _select_dicts(self, ipv):
|
|
if self._use_vrf:
|
|
if ipv == 6:
|
|
msg = (_('IPv6 subnets are not supported with VRFs'))
|
|
LOG.info(msg)
|
|
|
|
self._interfaceDict = self._router_in_vrf['interface']
|
|
else:
|
|
if ipv == 6:
|
|
# for IPv6 use IPv6 commands
|
|
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, fixed_ip, 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 fixed_ip: Fixed IP associated with the port
|
|
: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:
|
|
# In VARP config, use router ID else, use fixed IP.
|
|
# If fixed Ip was not set this will be gateway IP address.
|
|
ip = router_ip
|
|
else:
|
|
ip = fixed_ip + '/' + mask
|
|
cmds.append(c.format(segment_id, router_name, ip))
|
|
if self._mlag_configured:
|
|
for c in self._additionalInterfaceCmdsDict['add']:
|
|
cmds.append(c.format(fixed_ip))
|
|
|
|
self._run_config_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_config_cmds(cmds, server)
|
|
|
|
def create_router(self, context, 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(router['id'],
|
|
router['name'])
|
|
|
|
hashed = hashlib.sha256(router_name.encode('utf-8'))
|
|
rdm = str(int(hashed.hexdigest(), 16) % 65536)
|
|
|
|
mlag_peer_failed = False
|
|
for s in self._servers:
|
|
try:
|
|
self.create_router_on_eos(router_name, rdm, s)
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if self._mlag_configured and not mlag_peer_failed:
|
|
# In paired switch, it is OK to fail on one switch
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to create router %s on EOS') %
|
|
router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
if self._vrf_default_route:
|
|
ext_gateway = router.get('external_gateway_info')
|
|
if ext_gateway:
|
|
network_id = ext_gateway.get('network_id')
|
|
if network_id:
|
|
self._add_network_default_gateway(network_id)
|
|
|
|
def delete_router(self, context, router_id, router):
|
|
"""Deletes a router from Arista Switch."""
|
|
|
|
if router:
|
|
router_name = self._arista_router_name(router_id, router['name'])
|
|
mlag_peer_failed = False
|
|
for s in self._servers:
|
|
try:
|
|
self.delete_router_from_eos(router_name, s)
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if self._mlag_configured and not mlag_peer_failed:
|
|
# In paired switch, it is OK to fail on one switch
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to create router %s on EOS') %
|
|
router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
def update_router(self, context, router_id, original_router, new_router):
|
|
"""Updates a router which is already created on Arista Switch.
|
|
|
|
"""
|
|
if not self._vrf_default_route or not new_router:
|
|
return
|
|
|
|
ext_gateway = new_router.get('external_gateway_info')
|
|
if ext_gateway is None:
|
|
# Remove default gateway if it exists
|
|
orig_ext_gateway = original_router.get('external_gateway_info')
|
|
if orig_ext_gateway is None:
|
|
# External gateway did not change
|
|
return
|
|
network_id = orig_ext_gateway['network_id']
|
|
ml2_db = NetworkContext(self, context, {'id': network_id})
|
|
seg_id = ml2_db.network_segments[0]['segmentation_id']
|
|
new_router['seg_id'] = seg_id
|
|
new_router['ip_version'] = 4
|
|
self.remove_router_interface(context, new_router,
|
|
delete_gateway=True)
|
|
return
|
|
|
|
network_id = ext_gateway.get('network_id')
|
|
if network_id:
|
|
self._reset_network_default_route(network_id)
|
|
|
|
def _setup_default_gateway(self, router_info):
|
|
mlag_peer_failed = False
|
|
gip = router_info['gip']
|
|
router_name = self._arista_router_name(router_info['id'],
|
|
router_info['name'])
|
|
|
|
for s in self._servers:
|
|
try:
|
|
self._setup_switch_default_gateway(router_name, gip, s)
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if self._mlag_configured and not mlag_peer_failed:
|
|
# In paired switch, it is OK to fail on one switch
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to setup router gateway %s on EOS') %
|
|
router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
def _setup_switch_default_gateway(self, router_name, gip, server):
|
|
cmds = [
|
|
c.format(router_name, gip)
|
|
for c in additional_cmds_for_default_route['add']
|
|
]
|
|
|
|
self._run_config_cmds(cmds, server)
|
|
|
|
def _delete_default_gateway(self, router_name):
|
|
mlag_peer_failed = False
|
|
|
|
for s in self._servers:
|
|
try:
|
|
self._delete_switch_default_gateway(router_name, s)
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if self._mlag_configured and not mlag_peer_failed:
|
|
# In paired switch, it is OK to fail on one switch
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to delete router gateway %s on EOS') %
|
|
router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
def _delete_switch_default_gateway(self, router_name, server):
|
|
cmds = [
|
|
c.format(router_name)
|
|
for c in additional_cmds_for_default_route['remove']
|
|
]
|
|
|
|
self._run_config_cmds(cmds, server)
|
|
|
|
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:
|
|
if router_info['ip_version'] == 6 and self._use_vrf:
|
|
# For the moment we ignore the interfaces to be added
|
|
# on IPv6 subnets.
|
|
LOG.info('Using VRFs. Ignoring IPv6 interface')
|
|
return
|
|
self._select_dicts(router_info['ip_version'])
|
|
cidr = router_info['cidr']
|
|
subnet_mask = cidr.split('/')[1]
|
|
router_name = self._arista_router_name(router_info['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
|
|
# unless either collides with the router interface fixed_ip,
|
|
# in which case we use x.x.x.252
|
|
mlag_peer_failed = False
|
|
router_ips = self._get_router_ips(cidr, len(self._servers),
|
|
router_info['ip_version'],
|
|
router_info['fixed_ip'])
|
|
for i, server in enumerate(self._servers):
|
|
# Get appropriate virtual IP address for this router
|
|
router_ip = router_ips[i]
|
|
try:
|
|
self.add_interface_to_router(router_info['seg_id'],
|
|
router_name,
|
|
router_info['fixed_ip'],
|
|
router_ip, subnet_mask,
|
|
server)
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if not mlag_peer_failed:
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to add interface to router '
|
|
'%s on EOS') % router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(
|
|
msg=msg)
|
|
|
|
else:
|
|
for s in self._servers:
|
|
self.add_interface_to_router(router_info['seg_id'],
|
|
router_name,
|
|
router_info['fixed_ip'],
|
|
None, subnet_mask, s)
|
|
|
|
def remove_router_interface(self, context, router_info,
|
|
delete_gateway=False):
|
|
"""Removes previously configured interface from router on Arista HW.
|
|
|
|
This deals with both IPv6 and IPv4 configurations.
|
|
"""
|
|
if router_info:
|
|
if router_info['ip_version'] == 6 and self._use_vrf:
|
|
# For the moment we ignore the interfaces to be added
|
|
# on IPv6 subnets.
|
|
LOG.info('Using VRFs. Ignoring IPv6 interface')
|
|
return
|
|
router_name = self._arista_router_name(router_info['id'],
|
|
router_info['name'])
|
|
mlag_peer_failed = False
|
|
for s in self._servers:
|
|
try:
|
|
if delete_gateway:
|
|
self._delete_switch_default_gateway(router_name, s)
|
|
self.delete_interface_from_router(router_info['seg_id'],
|
|
router_name, s)
|
|
if self._mlag_configured:
|
|
mlag_peer_failed = False
|
|
except Exception:
|
|
if self._mlag_configured and not mlag_peer_failed:
|
|
mlag_peer_failed = True
|
|
else:
|
|
msg = (_('Failed to remove interface to router '
|
|
'%s on EOS') % router_name)
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
def _run_config_cmds(self, commands, server, log_exception=True,
|
|
keep_alive=True, update_vrf_commands=True):
|
|
"""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
|
|
self._run_eos_cmds(full_command, server, log_exception, keep_alive,
|
|
update_vrf_commands)
|
|
|
|
def _run_eos_cmds(self, commands, server, log_exception=True,
|
|
keep_alive=True, update_vrf_commands=True):
|
|
LOG.info(_LI('Executing command on Arista EOS: %s'), commands)
|
|
|
|
try:
|
|
# this returns array of return values for every command in
|
|
# full_command list
|
|
ret = server.execute(commands, keep_alive=keep_alive)
|
|
LOG.info(_LI('Results of execution on Arista EOS: %s'), ret)
|
|
return ret
|
|
except arista_exc.AristaServicePluginInvalidCommand:
|
|
msg = (_('VRF creation command unsupported. This request should '
|
|
'work on next retry.'))
|
|
if log_exception:
|
|
LOG.exception(msg)
|
|
if self._use_vrf and update_vrf_commands:
|
|
# For now we assume that the only command that raises this
|
|
# exception is vrf instance/definition and we need to update
|
|
# the current support
|
|
self._update_vrf_commands()
|
|
raise
|
|
except Exception:
|
|
msg = (_('Error occurred while trying to execute '
|
|
'commands %(cmd)s on EOS %(host)s') %
|
|
{'cmd': commands, 'host': server})
|
|
if log_exception:
|
|
LOG.exception(msg)
|
|
raise arista_exc.AristaServicePluginRpcError(msg=msg)
|
|
|
|
def _arista_router_name(self, router_id, name):
|
|
"""Generate an arista specific name for this router.
|
|
|
|
Use a unique name so that OpenStack created routers/SVIs
|
|
can be distinguishged from the user created routers/SVIs
|
|
on Arista HW. Replace spaces with underscores for CLI compatibility
|
|
"""
|
|
return '__OpenStack__' + router_id + '-' + name.replace(' ', '_')
|
|
|
|
def _get_binary_from_ipv4(self, ip_addr):
|
|
"""Converts IPv4 address to binary form."""
|
|
|
|
return struct.unpack("!L", socket.inet_pton(socket.AF_INET,
|
|
ip_addr))[0]
|
|
|
|
def _get_binary_from_ipv6(self, ip_addr):
|
|
"""Converts IPv6 address to binary form."""
|
|
|
|
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):
|
|
"""Converts binary address to Ipv4 format."""
|
|
|
|
return socket.inet_ntop(socket.AF_INET, struct.pack("!L", bin_addr))
|
|
|
|
def _get_ipv6_from_binary(self, bin_addr):
|
|
"""Converts binary address to Ipv6 format."""
|
|
|
|
hi = bin_addr >> 64
|
|
lo = bin_addr & 0xFFFFFFFF
|
|
return socket.inet_ntop(socket.AF_INET6, struct.pack("!QQ", hi, lo))
|
|
|
|
def _get_router_ips(self, cidr, ip_count, ip_ver, fixed_ip):
|
|
"""For a given IP subnet and IP version type, generate IP for router.
|
|
|
|
This method takes the network address (cidr) and selects a set of
|
|
IP addresses 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, skipping an address specified as the reserved_ip
|
|
"""
|
|
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_ips = list()
|
|
ip_idx = 0
|
|
while len(router_ips) < ip_count:
|
|
start_ip = MLAG_SWITCHES + ip_idx
|
|
router_ip = pow(2, bits - int(prefix)) - start_ip
|
|
|
|
router_ip = network_addr | router_ip
|
|
if ip_ver == 4:
|
|
router_ip = self._get_ipv4_from_binary(router_ip)
|
|
else:
|
|
router_ip = self._get_ipv6_from_binary(router_ip)
|
|
ip_idx += 1
|
|
if router_ip != fixed_ip.lower():
|
|
router_ips.append(router_ip + '/' + prefix)
|
|
return router_ips
|