Browse Source

Update L3 plugin to handle switches that support vrf instance

L3 plugin will be able to handle both swtiches that support only the
newer syntax with vrf instance and the old vrf definition syntax.

Change-Id: I2e9b88c4e6d18eb3880f44532c93cc8cb9b2f2f4
tags/2018.1.11
Alin Iorga Mitchell Jameson 6 months ago
parent
commit
51a03396b7
6 changed files with 165 additions and 38 deletions
  1. +11
    -1
      networking_arista/common/api.py
  2. +4
    -0
      networking_arista/common/exceptions.py
  3. +67
    -10
      networking_arista/l3Plugin/arista_l3_driver.py
  4. +5
    -1
      networking_arista/l3Plugin/l3_arista.py
  5. +75
    -23
      networking_arista/tests/unit/l3Plugin/test_arista_l3_driver.py
  6. +3
    -3
      networking_arista/tests/unit/utils.py

+ 11
- 1
networking_arista/common/api.py View File

@@ -28,6 +28,7 @@ LOG = logging.getLogger(__name__)

# EAPI error message
ERR_CVX_NOT_LEADER = 'only available on cluster leader'
ERR_INVALID_COMMAND = 'invalid command'


class EAPIClient(object):
@@ -48,7 +49,7 @@ class EAPIClient(object):
(scheme, host, '/command-api', '', '')
)

def execute(self, commands, commands_to_log=None):
def execute(self, commands, commands_to_log=None, keep_alive=True):
params = {
'timestamps': False,
'format': 'json',
@@ -75,6 +76,12 @@ class EAPIClient(object):
{'ip': self.host, 'data': json.dumps(log_data)}
)

# We can disable keep_alive if we call this from plugin init so we
# don't break the SSL session. Normally keep_alive=False should be used
# only for calls from init, all the rest should use keep_alive=True
self.session.headers['Connection'] = ('keep-alive' if keep_alive
else 'close')

# request handling
try:
error = None
@@ -134,6 +141,9 @@ class EAPIClient(object):
LOG.info(_LI('%(ip)s is not the CVX leader'),
{'ip': self.host})
return
msg = resp_data['error'].get('message', '')
if ERR_INVALID_COMMAND in msg:
raise arista_exc.AristaServicePluginInvalidCommand(msg=msg)
msg = ('Unexpected EAPI error: %s' %
resp_data.get('error', {}).get('message', 'Unknown Error'))
LOG.info(msg)


+ 4
- 0
networking_arista/common/exceptions.py View File

@@ -37,6 +37,10 @@ class AristaServicePluginConfigError(exceptions.NeutronException):
message = _('%(msg)s')


class AristaServicePluginInvalidCommand(exceptions.NeutronException):
message = _('%(msg)s')


class VlanUnavailable(exceptions.NeutronException):
"""An exception indicating VLAN creation failed because it's not available.



+ 67
- 10
networking_arista/l3Plugin/arista_l3_driver.py View File

@@ -36,11 +36,17 @@ IPV6_BITS = 128

# This string-format-at-a-distance confuses pylint :(
# pylint: disable=too-many-format-args
vrf_router_v1 = {'create': ['vrf definition {0}',
'rd {1}',
'exit'],
'delete': ['no vrf definition {0}']}
vrf_router_v2 = {'create': ['vrf instance {0}',
'rd {1}',
'exit'],
'delete': ['no vrf instance {0}']}

router_in_vrf = {
'router': {'create': ['vrf definition {0}',
'rd {1}',
'exit'],
'delete': ['no vrf definition {0}']},
'router': {}, # update on init

'interface': {'add': ['ip routing vrf {1}',
'vlan {0}',
@@ -104,6 +110,7 @@ class AristaL3Driver(object):
self._hosts.append(host)
self._servers.append(self._make_eapi_client(host))
self._mlag_configured = cfg.CONF.l3_arista.mlag_config
self._vrf_instance_supported = None
self._use_vrf = cfg.CONF.l3_arista.use_vrf
if self._mlag_configured:
host = cfg.CONF.l3_arista.secondary_l3_host
@@ -113,7 +120,8 @@ class AristaL3Driver(object):
self._additionalInterfaceCmdsDict = (
additional_cmds_for_mlag['interface'])
if self._use_vrf:
self.routerDict = router_in_vrf['router']
self.routerDict = vrf_router_v2
self._update_vrf_commands(keep_alive=False)
self._interfaceDict = router_in_vrf['interface']
else:
self.routerDict = router_in_default_vrf['router']
@@ -163,6 +171,41 @@ class AristaL3Driver(object):
timeout=cfg.CONF.l3_arista.conn_timeout
)

def _check_vrf_instance_support(self, host, keep_alive=True):
cmds = ['vrf instance _tmp_openstack_vrf',
'no vrf instance _tmp_openstack_vrf']
try:
self._run_config_cmds(cmds, host, log_exception=False,
keep_alive=keep_alive)
except arista_exc.AristaServicePluginInvalidCommand:
msg = _('Switch does not support vrf instance command')
LOG.info(msg)
return False
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 True

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_instance_support(
self._servers[0], keep_alive=keep_alive)

if new_vrf_support == self._vrf_instance_supported:
return

LOG.info(_LI('Updating VRF command supported: %s'),
'vrf instance' if new_vrf_support else 'vrf definition')
self._vrf_instance_supported = new_vrf_support
if self._vrf_instance_supported:
self.routerDict = vrf_router_v2
else:
self.routerDict = vrf_router_v1

def _validate_config(self):
if cfg.CONF.l3_arista.get('primary_l3_host') == '':
msg = _('Required option primary_l3_host is not set')
@@ -397,7 +440,8 @@ class AristaL3Driver(object):
LOG.exception(msg)
raise arista_exc.AristaServicePluginRpcError(msg=msg)

def _run_config_cmds(self, commands, server):
def _run_config_cmds(self, commands, server, log_exception=True,
keep_alive=True):
"""Execute/sends a CAPI (Command API) command to EOS.

In this method, list of commands is appended with prefix and
@@ -409,22 +453,35 @@ class AristaL3Driver(object):
command_start = ['enable', 'configure']
command_end = ['exit']
full_command = command_start + commands + command_end
self._run_eos_cmds(full_command, server)
self._run_eos_cmds(full_command, server, log_exception, keep_alive)

def _run_eos_cmds(self, commands, server):
def _run_eos_cmds(self, commands, server, log_exception=True,
keep_alive=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)
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:
# 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})
LOG.exception(msg)
if log_exception:
LOG.exception(msg)
raise arista_exc.AristaServicePluginRpcError(msg=msg)

def _arista_router_name(self, router_id, name):


+ 5
- 1
networking_arista/l3Plugin/l3_arista.py View File

@@ -112,6 +112,9 @@ class AristaL3SyncWorker(worker.BaseWorker):
Uses idempotent properties of EOS configuration, which means
same commands can be repeated.
"""
# Update vrf creation command support if needed
self.driver._update_vrf_commands()

LOG.info(_LI('Syncing Neutron Router DB <-> EOS'))
routers, router_interfaces = self.get_routers_and_interfaces()
expected_vrfs = set()
@@ -165,7 +168,8 @@ class AristaL3SyncWorker(worker.BaseWorker):
if self._use_vrf:
eos_vrfs = self.get_vrfs(server)
vrfs_to_delete = eos_vrfs - expected_vrfs
delete_cmds.extend(['no vrf definition %s' % vrf
delete_cmds.extend([c.format(vrf)
for c in self.driver.routerDict['delete']
for vrf in vrfs_to_delete])
if delete_cmds:
self.driver._run_config_cmds(delete_cmds, server)


+ 75
- 23
networking_arista/tests/unit/l3Plugin/test_arista_l3_driver.py View File

@@ -65,7 +65,8 @@ class AristaL3DriverTestCasesDefaultVrf(base.BaseTestCase):
self.drv._servers[0])
cmds = ['enable', 'configure', 'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)

def test_delete_router_from_eos(self):
router_name = 'test-router-1'
@@ -73,7 +74,8 @@ class AristaL3DriverTestCasesDefaultVrf(base.BaseTestCase):
self.drv.delete_router_from_eos(router_name, self.drv._servers[0])
cmds = ['enable', 'configure', 'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)

def test_add_interface_to_router_on_eos(self):
router_name = 'test-router-1'
@@ -89,7 +91,8 @@ class AristaL3DriverTestCasesDefaultVrf(base.BaseTestCase):
'interface vlan %s' % segment_id,
'ip address %s/%s' % (gw_ip, mask), 'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)

def test_delete_interface_from_router_on_eos(self):
router_name = 'test-router-1'
@@ -100,7 +103,8 @@ class AristaL3DriverTestCasesDefaultVrf(base.BaseTestCase):
cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)


class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
@@ -121,10 +125,34 @@ class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
def test_no_exception_on_correct_configuration(self):
self.assertIsNotNone(self.drv)

def test_create_router_on_eos(self):
def test_create_router_on_eos_v2_syntax(self):
max_vrfs = 5
routers = ['testRouter-%s' % n for n in range(max_vrfs)]
routers = ['testRouterV2-%s' % n for n in range(max_vrfs)]
domains = ['20%s' % n for n in range(max_vrfs)]

with mock.patch.object(self.drv, '_check_vrf_instance_support') as chk:
chk.return_value = True
self.drv._update_vrf_commands()
chk.assert_called_once_with(self.drv._servers[0], keep_alive=True)

for (r, d) in zip(routers, domains):
self.drv.create_router_on_eos(r, d, self.drv._servers[0])

cmds = ['enable', 'configure',
'vrf instance %s' % r,
'rd %(rd)s:%(rd)s' % {'rd': d}, 'exit', 'exit']

self.drv._servers[0].execute.assert_called_with(cmds,
keep_alive=True)

def test_create_router_on_eos_v1_syntax(self):
max_vrfs = 5
routers = ['testRouterV1-%s' % n for n in range(max_vrfs)]
domains = ['10%s' % n for n in range(max_vrfs)]
with mock.patch.object(self.drv, '_check_vrf_instance_support') as chk:
chk.return_value = False
self.drv._update_vrf_commands()
chk.assert_called_once_with(self.drv._servers[0], keep_alive=True)

for (r, d) in zip(routers, domains):
self.drv.create_router_on_eos(r, d, self.drv._servers[0])
@@ -133,18 +161,40 @@ class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
'vrf definition %s' % r,
'rd %(rd)s:%(rd)s' % {'rd': d}, 'exit', 'exit']

self.drv._servers[0].execute.assert_called_with(cmds)
self.drv._servers[0].execute.assert_called_with(cmds,
keep_alive=True)

def test_delete_router_from_eos(self):
def test_delete_router_from_eos_v1_syntax(self):
max_vrfs = 5
routers = ['testRouter-%s' % n for n in range(max_vrfs)]

with mock.patch.object(self.drv, '_check_vrf_instance_support') as chk:
chk.return_value = False
self.drv._update_vrf_commands()
chk.assert_called_once_with(self.drv._servers[0], keep_alive=True)

for r in routers:
self.drv.delete_router_from_eos(r, self.drv._servers[0])
cmds = ['enable', 'configure', 'no vrf definition %s' % r,
'exit']
cmds = ['enable', 'configure', 'no vrf definition %s' % r, 'exit']

self.drv._servers[0].execute.assert_called_with(cmds,
keep_alive=True)

def test_delete_router_from_eos_v2_syntax(self):
max_vrfs = 5
routers = ['testRouter-%s' % n for n in range(max_vrfs)]

with mock.patch.object(self.drv, '_check_vrf_instance_support') as chk:
chk.return_value = True
self.drv._update_vrf_commands()
chk.assert_called_once_with(self.drv._servers[0], keep_alive=True)

for r in routers:
self.drv.delete_router_from_eos(r, self.drv._servers[0])
cmds = ['enable', 'configure', 'no vrf instance %s' % r, 'exit']

self.drv._servers[0].execute.assert_called_with(cmds)
self.drv._servers[0].execute.assert_called_with(cmds,
keep_alive=True)

def test_add_interface_to_router_on_eos(self):
router_name = 'test-router-1'
@@ -162,7 +212,8 @@ class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
'vrf forwarding %s' % router_name,
'ip address %s/%s' % (gw_ip, mask), 'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)

def test_delete_interface_from_router_on_eos(self):
router_name = 'test-router-1'
@@ -173,7 +224,8 @@ class AristaL3DriverTestCasesUsingVRFs(base.BaseTestCase):
cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
'exit']

self.drv._servers[0].execute.assert_called_once_with(cmds)
self.drv._servers[0].execute.assert_called_once_with(cmds,
keep_alive=True)


class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
@@ -206,7 +258,7 @@ class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
cmds = ['enable', 'configure',
'ip virtual-router mac-address %s' % router_mac, 'exit']

s.execute.assert_called_with(cmds)
s.execute.assert_called_with(cmds, keep_alive=True)

def test_delete_router_from_eos(self):
router_name = 'test-router-1'
@@ -215,7 +267,7 @@ class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
self.drv.delete_router_from_eos(router_name, s)
cmds = ['enable', 'configure', 'exit']

s.execute.assert_called_once_with(cmds)
s.execute.assert_called_once_with(cmds, keep_alive=True)

def test_add_interface_to_router_on_eos(self):
router_name = 'test-router-1'
@@ -233,7 +285,7 @@ class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
'ip address %s' % router_ip,
'ip virtual-router address %s' % gw_ip, 'exit']

s.execute.assert_called_once_with(cmds)
s.execute.assert_called_once_with(cmds, keep_alive=True)

def test_delete_interface_from_router_on_eos(self):
router_name = 'test-router-1'
@@ -245,7 +297,7 @@ class AristaL3DriverTestCasesMlagConfig(base.BaseTestCase):
cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
'exit']

s.execute.assert_called_once_with(cmds)
s.execute.assert_called_once_with(cmds, keep_alive=True)


class AristaL3DriverTestCasesMlagVRFConfig(base.BaseTestCase):
@@ -278,12 +330,12 @@ class AristaL3DriverTestCasesMlagVRFConfig(base.BaseTestCase):
self.drv.create_router_on_eos(r, d, s)

cmds = ['enable', 'configure',
'vrf definition %s' % r,
'vrf instance %s' % r,
'rd %(rd)s:%(rd)s' % {'rd': d},
'exit',
'ip virtual-router mac-address %s' % router_mac,
'exit']
s.execute.assert_called_with(cmds)
s.execute.assert_called_with(cmds, keep_alive=True)

def test_delete_router_from_eos(self):
max_vrfs = 5
@@ -292,10 +344,10 @@ class AristaL3DriverTestCasesMlagVRFConfig(base.BaseTestCase):
for s in self.drv._servers:
for r in routers:
self.drv.delete_router_from_eos(r, s)
cmds = ['enable', 'configure', 'no vrf definition %s' % r,
cmds = ['enable', 'configure', 'no vrf instance %s' % r,
'exit']

s.execute.assert_called_with(cmds)
s.execute.assert_called_with(cmds, keep_alive=True)

def test_add_interface_to_router_on_eos(self):
router_name = 'test-router-1'
@@ -316,7 +368,7 @@ class AristaL3DriverTestCasesMlagVRFConfig(base.BaseTestCase):
'ip virtual-router address %s' % gw_ip,
'exit']

s.execute.assert_called_once_with(cmds)
s.execute.assert_called_once_with(cmds, keep_alive=True)

def test_delete_interface_from_router_on_eos(self):
router_name = 'test-router-1'
@@ -328,7 +380,7 @@ class AristaL3DriverTestCasesMlagVRFConfig(base.BaseTestCase):
cmds = ['enable', 'configure', 'no interface vlan %s' % segment_id,
'exit']

s.execute.assert_called_once_with(cmds)
s.execute.assert_called_once_with(cmds, keep_alive=True)


class AristaL3DriverTestCases_v4(base.BaseTestCase):


+ 3
- 3
networking_arista/tests/unit/utils.py View File

@@ -108,7 +108,7 @@ class MockSwitch(object):
self._access_group_re = re.compile(
'^(?P<delete>no )?ip access-group (?P<acl>\S+) (?P<dir>\S+)$')
self._vrf_mode_re = re.compile(
'^(?P<delete>no )?vrf definition (?P<name>\S+)$')
'^(?P<delete>no )?vrf instance (?P<name>\S+)$')
self._vlan_re = re.compile('^(?P<delete>no )?vlan (?P<vlan>\d+)$')
self._ip_address_re = re.compile(
'^ip address (?P<ip>[\d.]+)/(?P<mask>\d+)$')
@@ -119,7 +119,7 @@ class MockSwitch(object):
'^ip virtual-router mac-address (?P<varp_mac>\S+)$')
self._mode = None

def execute(self, commands, commands_to_log=None):
def execute(self, commands, commands_to_log=None, keep_alive=True):
ret = []
for command in commands:
if command == 'show vlan':
@@ -156,7 +156,7 @@ class MockSwitch(object):
'mask': '',
'vip': ''}
self._mode = ('interface', intf)
elif 'vrf definition' in command:
elif 'vrf instance' in command:
vrf_match = self._vrf_mode_re.match(command)
delete = vrf_match.group('delete')
vrf_name = vrf_match.group('name')


Loading…
Cancel
Save