Browse Source

Cleanup Arista Security Group Support

Move all security group logic to a service plugin. ACLs are now
applied to ports in response to PORT callbacks rather than via
the ML2 mechanism driver.

eAPI commands are also now built incrementally rather than by
dictionary lookup.

Change-Id: Ide2d61b2ea29d6ed34c972d006c322c66a2de3f1
changes/99/562399/3
Mitchell Jameson 4 years ago
parent
commit
fa9ae57e9e
  1. 152
      networking_arista/common/db_lib.py
  2. 8
      networking_arista/common/utils.py
  3. 634
      networking_arista/ml2/arista_sec_gp.py
  4. 3
      networking_arista/ml2/arista_sync.py
  5. 5
      networking_arista/ml2/mechanism_arista.py
  6. 115
      networking_arista/ml2/rpc/base.py
  7. 117
      networking_arista/ml2/sec_group_callback.py
  8. 0
      networking_arista/ml2/security_groups/__init__.py
  9. 150
      networking_arista/ml2/security_groups/arista_security_groups.py
  10. 112
      networking_arista/ml2/security_groups/security_group_sync.py
  11. 323
      networking_arista/ml2/security_groups/switch_helper.py
  12. 1
      networking_arista/tests/unit/ml2/ml2_test_base.py
  13. 0
      networking_arista/tests/unit/ml2/security_groups/__init__.py
  14. 68
      networking_arista/tests/unit/ml2/security_groups/sg_test_base.py
  15. 587
      networking_arista/tests/unit/ml2/security_groups/test_arista_security_groups.py
  16. 312
      networking_arista/tests/unit/ml2/security_groups/test_security_group_sync.py
  17. 16
      networking_arista/tests/unit/utils.py
  18. 5
      setup.cfg

152
networking_arista/common/db_lib.py

@ -18,20 +18,19 @@ import collections
from oslo_config import cfg
from oslo_log import log as logging
from sqlalchemy import and_, or_
from sqlalchemy.orm import Query, aliased
from sqlalchemy import func
from sqlalchemy.orm import joinedload, Query, aliased
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants as n_const
from neutron_lib import context as nctx
from neutron_lib.plugins.ml2 import api as driver_api
import neutron.db.api as db
from neutron.db import db_base_plugin_v2
from neutron.db.models.plugins.ml2 import vlanallocation
from neutron.db.models import securitygroup as sg_models
from neutron.db.models import segment as segment_models
from neutron.db import models_v2
from neutron.db import securitygroups_db as sec_db
from neutron.db import segments_db
from neutron.plugins.ml2 import models as ml2_models
from networking_arista.common import utils
@ -265,7 +264,8 @@ def get_vm_instances(instance_id=None):
def get_baremetal_instances(instance_id=None):
"""Returns filtered list of baremetals that may be relevant on CVX"""
return get_instances(vnic_type=portbindings.VNIC_BAREMETAL)
return get_instances(vnic_type=portbindings.VNIC_BAREMETAL,
instance_id=instance_id)
def get_ports(device_owners=None, vnic_type=None, port_id=None, active=True):
@ -301,8 +301,7 @@ def get_vm_ports(port_id=None):
def get_baremetal_ports(port_id=None):
"""Returns filtered list of baremetals that may be relevant on CVX"""
return get_ports(vnic_type=portbindings.VNIC_BAREMETAL,
port_id=port_id)
return get_ports(vnic_type=portbindings.VNIC_BAREMETAL, port_id=port_id)
def get_port_bindings(binding_key=None):
@ -459,129 +458,54 @@ def get_port_binding_level(filters):
all())
class NeutronNets(db_base_plugin_v2.NeutronDbPluginV2,
sec_db.SecurityGroupDbMixin):
def get_security_groups():
session = db.get_reader_session()
with session.begin():
sg_model = sg_models.SecurityGroup
# We do a joined load to prevent the need for the sync worker
# to issue subqueries
security_groups = (session.query(sg_model)
.options(joinedload(sg_model.rules)))
return security_groups
def get_baremetal_sg_bindings():
session = db.get_reader_session()
with session.begin():
sg_binding_model = sg_models.SecurityGroupPortBinding
binding_model = ml2_models.PortBinding
sg_bindings = (session
.query(sg_binding_model,
binding_model)
.outerjoin(
binding_model,
sg_binding_model.port_id == binding_model.port_id)
.filter_unnecessary_ports(
vnic_type=portbindings.VNIC_BAREMETAL)
.group_by(sg_binding_model.port_id)
.having(func.count(sg_binding_model.port_id) == 1))
return sg_bindings
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 stroed in Arista repository
They are pulled from Neutron DB.
networks as well ports. This data is used during synchronization
between the L3 Plugin and Arista EOS.
"""
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_networks(self):
return super(NeutronNets, self).get_networks(self.admin_ctx) 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_shared_network_owner_id(self, network_id):
filters = {'id': [network_id]}
nets = self.get_networks(self.admin_ctx, filters=filters) or []
segments = segments_db.get_network_segments(self.admin_ctx,
network_id)
if not nets or not segments:
return
if (nets[0]['shared'] and
segments[0][driver_api.NETWORK_TYPE] == n_const.TYPE_VLAN):
return nets[0]['tenant_id']
def get_network_segments(self, network_id, dynamic=False, context=None):
context = context if context is not None else self.admin_ctx
segments = segments_db.get_network_segments(context, network_id,
filter_dynamic=dynamic)
if dynamic:
for segment in segments:
segment['is_dynamic'] = True
return segments
def get_all_network_segments(self, network_id, context=None):
segments = self.get_network_segments(network_id, context=context)
segments += self.get_network_segments(network_id, dynamic=True,
context=context)
return segments
def get_segment_by_id(self, context, segment_id):
return segments_db.get_segment_by_id(context,
segment_id)
def get_network_from_net_id(self, network_id, context=None):
filters = {'id': [network_id]}
ctxt = context if context else self.admin_ctx
return super(NeutronNets,
self).get_networks(ctxt, filters=filters) or []
def get_subnet_info(self, subnet_id):
return self.get_subnet(subnet_id)
def get_subnet_ip_version(self, subnet_id):
subnet = self.get_subnet(subnet_id)
return subnet['ip_version'] if 'ip_version' in subnet else None
def get_subnet_gateway_ip(self, subnet_id):
subnet = self.get_subnet(subnet_id)
return subnet['gateway_ip'] if 'gateway_ip' in subnet else None
def get_subnet_cidr(self, subnet_id):
subnet = self.get_subnet(subnet_id)
return subnet['cidr'] if 'cidr' in subnet else None
def get_network_id(self, subnet_id):
subnet = self.get_subnet(subnet_id)
return subnet['network_id'] if 'network_id' in subnet else None
def get_network_id_from_port_id(self, port_id):
port = self.get_port(port_id)
return port['network_id'] if 'network_id' in port else None
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 {}
def get_all_security_gp_to_port_bindings(self):
return super(NeutronNets, self)._get_port_security_group_bindings(
self.admin_ctx) or []
def get_security_gp_to_port_bindings(self, sec_gp_id):
filters = {'security_group_id': [sec_gp_id]}
return super(NeutronNets, self)._get_port_security_group_bindings(
self.admin_ctx, filters=filters) or []
def get_security_group(self, sec_gp_id):
return super(NeutronNets,
self).get_security_group(self.admin_ctx, sec_gp_id) or []
def get_security_groups(self):
sgs = super(NeutronNets,
self).get_security_groups(self.admin_ctx) or []
sgs_all = {}
if sgs:
for s in sgs:
sgs_all[s['id']] = s
return sgs_all
def get_security_group_rule(self, sec_gpr_id):
return super(NeutronNets,
self).get_security_group_rule(self.admin_ctx,
sec_gpr_id) or []
def validate_network_rbac_policy_change(self, resource, event, trigger,
context, object_type, policy,
**kwargs):
return super(NeutronNets, self).validate_network_rbac_policy_change(
resource, event, trigger, context, object_type, policy, kwargs)

8
networking_arista/common/utils.py

@ -41,6 +41,14 @@ UNSUPPORTED_DEVICE_OWNERS = [
UNSUPPORTED_DEVICE_IDS = [
n_const.DEVICE_ID_RESERVED_DHCP_PORT]
SUPPORTED_SG_PROTOCOLS = [
None,
n_const.PROTO_NAME_TCP,
n_const.PROTO_NAME_UDP,
n_const.PROTO_NAME_ICMP]
LOG = logging.getLogger(__name__)
def supported_device_owner(device_owner):

634
networking_arista/ml2/arista_sec_gp.py

@ -1,634 +0,0 @@
# Copyright (c) 2016 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.
import json
import re
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__)
EOS_UNREACHABLE_MSG = _('Unable to reach EOS')
# Note 'None,null' means default rule - i.e. deny everything
SUPPORTED_SG_PROTOCOLS = [None, 'tcp', 'udp', 'icmp']
acl_cmd = {
'acl': {'create': ['ip access-list {0}'],
'in_rule': ['permit {0} {1} any range {2} {3}'],
'out_rule': ['permit {0} any {1} range {2} {3}'],
'in_icmp_custom1': ['permit icmp {0} any {1}'],
'out_icmp_custom1': ['permit icmp any {0} {1}'],
'in_icmp_custom2': ['permit icmp {0} any {1} {2}'],
'out_icmp_custom2': ['permit icmp any {0} {1} {2}'],
'default': [],
'delete_acl': ['no ip access-list {0}'],
'del_in_icmp_custom1': ['ip access-list {0}',
'no permit icmp {1} any {2}',
'exit'],
'del_out_icmp_custom1': ['ip access-list {0}',
'no permit icmp any {1} {2}',
'exit'],
'del_in_icmp_custom2': ['ip access-list {0}',
'no permit icmp {1} any {2} {3}',
'exit'],
'del_out_icmp_custom2': ['ip access-list {0}',
'no permit icmp any {1} {2} {3}',
'exit'],
'del_in_acl_rule': ['ip access-list {0}',
'no permit {1} {2} any range {3} {4}',
'exit'],
'del_out_acl_rule': ['ip access-list {0}',
'no permit {1} any {2} range {3} {4}',
'exit']},
'apply': {'ingress': ['interface {0}',
'ip access-group {1} in',
'exit'],
'egress': ['interface {0}',
'ip access-group {1} out',
'exit'],
'rm_ingress': ['interface {0}',
'no ip access-group {1} in',
'exit'],
'rm_egress': ['interface {0}',
'no ip access-group {1} out',
'exit']}}
class AristaSecGroupSwitchDriver(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._ndb = db_lib.NeutronNets()
self._servers = []
self._hosts = {}
self.sg_enabled = cfg.CONF.ml2_arista.get('sec_group_support')
self._validate_config()
for s in cfg.CONF.ml2_arista.switch_info:
switch_ip, switch_user, switch_pass = s.split(":")
if switch_pass == "''":
switch_pass = ''
self._hosts[switch_ip] = (
{'user': switch_user, 'password': switch_pass})
self._servers.append(self._make_eapi_client(switch_ip))
self.aclCreateDict = acl_cmd['acl']
self.aclApplyDict = acl_cmd['apply']
def _make_eapi_client(self, host):
return api.EAPIClient(
host,
username=self._hosts[host]['user'],
password=self._hosts[host]['password'],
verify=False,
timeout=cfg.CONF.ml2_arista.conn_timeout
)
def _validate_config(self):
if not self.sg_enabled:
return
if len(cfg.CONF.ml2_arista.get('switch_info')) < 1:
msg = _('Required option - when "sec_group_support" is enabled, '
'at least one switch must be specified ')
LOG.exception(msg)
raise arista_exc.AristaConfigError(msg=msg)
def _get_port_for_acl(self, port_id, server):
"""Gets interface name for ACLs
Finds the Port-Channel name if port_id is in a Port-Channel, otherwise
ACLs are applied to Ethernet interface.
:param port_id: Name of port from ironic db
:param server: Server endpoint on the Arista switch to be configured
"""
all_intf_info = self._run_eos_cmds(
['show interfaces %s' % port_id], server)[0]
intf_info = all_intf_info.get('interfaces', {}).get(port_id, {})
member_info = intf_info.get('interfaceMembership', '')
port_group_info = re.search('Member of (?P<port_group>\S+)',
member_info)
if port_group_info:
port_id = port_group_info.group('port_group')
return port_id
def _create_acl_on_eos(self, in_cmds, out_cmds, protocol, cidr,
from_port, to_port, direction):
"""Creates an ACL on Arista HW Device.
:param name: Name for the ACL
:param server: Server endpoint on the Arista switch to be configured
"""
if protocol == 'icmp':
# ICMP rules require special processing
if ((from_port and to_port) or
(not from_port and not to_port)):
rule = 'icmp_custom2'
elif from_port and not to_port:
rule = 'icmp_custom1'
else:
msg = _('Invalid ICMP rule specified')
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
rule_type = 'in'
cmds = in_cmds
if direction == 'egress':
rule_type = 'out'
cmds = out_cmds
final_rule = rule_type + '_' + rule
acl_dict = self.aclCreateDict[final_rule]
# None port is probematic - should be replaced with 0
if not from_port:
from_port = 0
if not to_port:
to_port = 0
for c in acl_dict:
if rule == 'icmp_custom2':
cmds.append(c.format(cidr, from_port, to_port))
else:
cmds.append(c.format(cidr, from_port))
return in_cmds, out_cmds
else:
# Non ICMP rules processing here
acl_dict = self.aclCreateDict['in_rule']
cmds = in_cmds
if direction == 'egress':
acl_dict = self.aclCreateDict['out_rule']
cmds = out_cmds
if not protocol:
acl_dict = self.aclCreateDict['default']
for c in acl_dict:
cmds.append(c.format(protocol, cidr,
from_port, to_port))
return in_cmds, out_cmds
def _delete_acl_from_eos(self, name, server):
"""deletes an ACL from Arista HW Device.
:param name: Name for the ACL
:param server: Server endpoint on the Arista switch to be configured
"""
cmds = []
for c in self.aclCreateDict['delete_acl']:
cmds.append(c.format(name))
self._run_openstack_sg_cmds(cmds, server)
def _delete_acl_rule_from_eos(self, name,
protocol, cidr,
from_port, to_port,
direction, server):
"""deletes an ACL from Arista HW Device.
:param name: Name for the ACL
:param server: Server endpoint on the Arista switch to be configured
"""
cmds = []
if protocol == 'icmp':
# ICMP rules require special processing
if ((from_port and to_port) or
(not from_port and not to_port)):
rule = 'icmp_custom2'
elif from_port and not to_port:
rule = 'icmp_custom1'
else:
msg = _('Invalid ICMP rule specified')
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
rule_type = 'del_in'
if direction == 'egress':
rule_type = 'del_out'
final_rule = rule_type + '_' + rule
acl_dict = self.aclCreateDict[final_rule]
# None port is probematic - should be replaced with 0
if not from_port:
from_port = 0
if not to_port:
to_port = 0
for c in acl_dict:
if rule == 'icmp_custom2':
cmds.append(c.format(name, cidr, from_port, to_port))
else:
cmds.append(c.format(name, cidr, from_port))
else:
acl_dict = self.aclCreateDict['del_in_acl_rule']
if direction == 'egress':
acl_dict = self.aclCreateDict['del_out_acl_rule']
for c in acl_dict:
cmds.append(c.format(name, protocol, cidr,
from_port, to_port))
self._run_openstack_sg_cmds(cmds, server)
def _apply_acl_on_eos(self, port_id, name, direction, server):
"""Creates an ACL on Arista HW Device.
:param port_id: The port where the ACL needs to be applied
:param name: Name for the ACL
:param direction: must contain "ingress" or "egress"
:param server: Server endpoint on the Arista switch to be configured
"""
cmds = []
port_id = self._get_port_for_acl(port_id, server)
for c in self.aclApplyDict[direction]:
cmds.append(c.format(port_id, name))
self._run_openstack_sg_cmds(cmds, server)
def _remove_acl_from_eos(self, port_id, name, direction, server):
"""Remove an ACL from a port on Arista HW Device.
:param port_id: The port where the ACL needs to be applied
:param name: Name for the ACL
:param direction: must contain "ingress" or "egress"
:param server: Server endpoint on the Arista switch to be configured
"""
cmds = []
port_id = self._get_port_for_acl(port_id, server)
acl_cmd = self.aclApplyDict['rm_ingress']
if direction == 'egress':
acl_cmd = self.aclApplyDict['rm_egress']
for c in acl_cmd:
cmds.append(c.format(port_id, name))
self._run_openstack_sg_cmds(cmds, server)
def _create_acl_rule(self, in_cmds, out_cmds, sgr):
"""Creates an ACL on Arista Switch.
For a given Security Group (ACL), it adds additional rule
Deals with multiple configurations - such as multiple switches
"""
# Only deal with valid protocols - skip the rest
if not sgr or sgr['protocol'] not in SUPPORTED_SG_PROTOCOLS:
return in_cmds, out_cmds
remote_ip = sgr['remote_ip_prefix']
if not remote_ip:
remote_ip = 'any'
min_port = sgr['port_range_min']
if not min_port:
min_port = 0
max_port = sgr['port_range_max']
if not max_port and sgr['protocol'] != 'icmp':
max_port = 65535
in_cmds, out_cmds = self._create_acl_on_eos(in_cmds, out_cmds,
sgr['protocol'],
remote_ip,
min_port,
max_port,
sgr['direction'])
return in_cmds, out_cmds
def create_acl_rule(self, sgr):
"""Creates an ACL on Arista Switch.
For a given Security Group (ACL), it adds additional rule
Deals with multiple configurations - such as multiple switches
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
name = self._arista_acl_name(sgr['security_group_id'],
sgr['direction'])
cmds = []
for c in self.aclCreateDict['create']:
cmds.append(c.format(name))
in_cmds, out_cmds = self._create_acl_rule(cmds, cmds, sgr)
cmds = in_cmds
if sgr['direction'] == 'egress':
cmds = out_cmds
cmds.append('exit')
for s in self._servers:
try:
self._run_openstack_sg_cmds(cmds, s)
except Exception:
msg = (_('Failed to create ACL rule on EOS %s') % s)
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def delete_acl_rule(self, sgr):
"""Deletes an ACL rule on Arista Switch.
For a given Security Group (ACL), it adds removes a rule
Deals with multiple configurations - such as multiple switches
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
# Only deal with valid protocols - skip the rest
if not sgr or sgr['protocol'] not in SUPPORTED_SG_PROTOCOLS:
return
# Build seperate ACL for ingress and egress
name = self._arista_acl_name(sgr['security_group_id'],
sgr['direction'])
remote_ip = sgr['remote_ip_prefix']
if not remote_ip:
remote_ip = 'any'
min_port = sgr['port_range_min']
if not min_port:
min_port = 0
max_port = sgr['port_range_max']
if not max_port and sgr['protocol'] != 'icmp':
max_port = 65535
for s in self._servers:
try:
self._delete_acl_rule_from_eos(name,
sgr['protocol'],
remote_ip,
min_port,
max_port,
sgr['direction'],
s)
except Exception:
msg = (_('Failed to delete ACL on EOS %s') % s)
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def _create_acl_shell(self, sg_id):
"""Creates an ACL on Arista Switch.
For a given Security Group (ACL), it adds additional rule
Deals with multiple configurations - such as multiple switches
"""
# Build seperate ACL for ingress and egress
direction = ['ingress', 'egress']
cmds = []
for d in range(len(direction)):
name = self._arista_acl_name(sg_id, direction[d])
cmds.append([])
for c in self.aclCreateDict['create']:
cmds[d].append(c.format(name))
return cmds[0], cmds[1]
def create_acl(self, sg):
"""Creates an ACL on Arista Switch.
Deals with multiple configurations - such as multiple switches
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
if not sg:
msg = _('Invalid or Empty Security Group Specified')
raise arista_exc.AristaSecurityGroupError(msg=msg)
in_cmds, out_cmds = self._create_acl_shell(sg['id'])
for sgr in sg['security_group_rules']:
in_cmds, out_cmds = self._create_acl_rule(in_cmds, out_cmds, sgr)
in_cmds.append('exit')
out_cmds.append('exit')
for s in self._servers:
try:
self._run_openstack_sg_cmds(in_cmds, s)
self._run_openstack_sg_cmds(out_cmds, s)
except Exception:
msg = (_('Failed to create ACL on EOS %s') % s)
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def delete_acl(self, sg):
"""Deletes an ACL from Arista Switch.
Deals with multiple configurations - such as multiple switches
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
if not sg:
msg = _('Invalid or Empty Security Group Specified')
raise arista_exc.AristaSecurityGroupError(msg=msg)
direction = ['ingress', 'egress']
for d in range(len(direction)):
name = self._arista_acl_name(sg['id'], direction[d])
for s in self._servers:
try:
self._delete_acl_from_eos(name, s)
except Exception:
msg = (_('Failed to create ACL on EOS %s') % s)
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def apply_acl(self, sgs, switch_id, port_id, switch_info):
"""Creates an ACL on Arista Switch.
Applies ACLs to the baremetal ports only. The port/switch
details is passed through the parameters.
Deals with multiple configurations - such as multiple switches
param sgs: List of Security Groups
param switch_id: Switch ID of TOR where ACL needs to be applied
param port_id: Port ID of port where ACL needs to be applied
param switch_info: IP address of the TOR
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
# We do not support more than one security group on a port
if not sgs or len(sgs) > 1:
msg = (_('Only one Security Group Supported on a port %s') % sgs)
raise arista_exc.AristaSecurityGroupError(msg=msg)
sg = self._ndb.get_security_group(sgs[0])
# We already have ACLs on the TORs.
# Here we need to find out which ACL is applicable - i.e.
# Ingress ACL, egress ACL or both
direction = ['ingress', 'egress']
server = self._make_eapi_client(switch_info)
for d in range(len(direction)):
name = self._arista_acl_name(sg['id'], direction[d])
try:
self._apply_acl_on_eos(port_id, name, direction[d], server)
except Exception:
msg = (_('Failed to apply ACL on port %s') % port_id)
LOG.exception(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def remove_acl(self, sgs, switch_id, port_id, switch_info):
"""Removes an ACL from Arista Switch.
Removes ACLs from the baremetal ports only. The port/switch
details is passed throuhg the parameters.
param sgs: List of Security Groups
param switch_id: Switch ID of TOR where ACL needs to be removed
param port_id: Port ID of port where ACL needs to be removed
param switch_info: IP address of the TOR
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
# We do not support more than one security group on a port
if not sgs or len(sgs) > 1:
msg = (_('Only one Security Group Supported on a port %s') % sgs)
raise arista_exc.AristaSecurityGroupError(msg=msg)
sg = self._ndb.get_security_group(sgs[0])
# We already have ACLs on the TORs.
# Here we need to find out which ACL is applicable - i.e.
# Ingress ACL, egress ACL or both
direction = []
for sgr in sg['security_group_rules']:
# Only deal with valid protocols - skip the rest
if not sgr or sgr['protocol'] not in SUPPORTED_SG_PROTOCOLS:
continue
if sgr['direction'] not in direction:
direction.append(sgr['direction'])
# THIS IS TOTAL HACK NOW - just for testing
# Assumes the credential of all switches are same as specified
# in the condig file
server = self._make_eapi_client(switch_info)
for d in range(len(direction)):
name = self._arista_acl_name(sg['id'], direction[d])
try:
self._remove_acl_from_eos(port_id, name, direction[d], server)
except Exception:
msg = (_('Failed to remove ACL on port %s') % port_id)
LOG.exception(msg)
# No need to raise exception for ACL removal
# raise arista_exc.AristaSecurityGroupError(msg=msg)
def _run_openstack_sg_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
return self._run_eos_cmds(full_command, server)
def _run_eos_cmds(self, commands, server):
"""Execute/sends a CAPI (Command API) command to EOS.
This method is useful for running show commands that require no
prefix or postfix commands.
:param commands : List of commands to be executed on EOS.
:param server: Server endpoint on the Arista switch to be configured
"""
LOG.info(_LI('Executing command on Arista EOS: %s'), commands)
try:
# this returns array of return values for every command in
# commands list
ret = server.execute(commands)
LOG.info(_LI('Results of execution on Arista EOS: %s'), ret)
return ret
except Exception:
msg = (_('Error occurred while trying to execute '
'commands %(cmd)s on EOS %(host)s') %
{'cmd': commands, 'host': server})
LOG.exception(msg)
raise arista_exc.AristaServicePluginRpcError(msg=msg)
def _arista_acl_name(self, name, direction):
"""Generate an arista specific name for this ACL.
Use a unique name so that OpenStack created ACLs
can be distinguishged from the user created ACLs
on Arista HW.
"""
in_out = 'IN'
if direction == 'egress':
in_out = 'OUT'
return 'SG' + '-' + in_out + '-' + name
def perform_sync_of_sg(self):
"""Perform sync of the security groups between ML2 and EOS.
This is unconditional sync to ensure that all security
ACLs are pushed to all the switches, in case of switch
or neutron reboot
"""
# Do nothing if Security Groups are not enabled
if not self.sg_enabled:
return
arista_ports = db_lib.get_ports()
neutron_sgs = self._ndb.get_security_groups()
sg_bindings = self._ndb.get_all_security_gp_to_port_bindings()
sgs = []
sgs_dict = {}
arista_port_ids = arista_ports.keys()
# Get the list of Security Groups of interest to us
for s in sg_bindings:
if s['port_id'] in arista_port_ids:
if not s['security_group_id'] in sgs:
sgs_dict[s['port_id']] = (
{'security_group_id': s['security_group_id']})
sgs.append(s['security_group_id'])
# Create the ACLs on Arista Switches
for idx in range(len(sgs)):
self.create_acl(neutron_sgs[sgs[idx]])
# Get Baremetal port profiles, if any
bm_port_profiles = db_lib.get_all_baremetal_ports()
if bm_port_profiles:
for bm in bm_port_profiles.values():
if bm['port_id'] in sgs_dict:
sg = sgs_dict[bm['port_id']]['security_group_id']
profile = json.loads(bm['profile'])
link_info = profile['local_link_information']
for l in link_info:
if not l:
# skip all empty entries
continue
self.apply_acl([sg], l['switch_id'],
l['port_id'], l['switch_info'])

3
networking_arista/ml2/arista_sync.py

@ -237,9 +237,6 @@ class AristaSyncWorker(worker.BaseWorker):
def sync_loop(self):
while self._running:
try:
# TODO(mitchell): Move security group sync to a separate worker
# self.synchronize_security_groups()
sync_required = self.wait_for_sync_required()
if sync_required:

5
networking_arista/ml2/mechanism_arista.py

@ -29,7 +29,6 @@ from networking_arista.common import db_lib
from networking_arista.common import exceptions as arista_exc
from networking_arista.ml2 import arista_sync
from networking_arista.ml2.rpc.arista_eapi import AristaRPCWrapperEapi
from networking_arista.ml2 import sec_group_callback
LOG = logging.getLogger(__name__)
cfg.CONF.import_group('ml2_arista', 'networking_arista.common.config')
@ -61,9 +60,6 @@ class AristaDriver(driver_api.MechanismDriver):
provisioned before for the given port.
"""
def __init__(self):
self.ndb = db_lib.NeutronNets()
confg = cfg.CONF.ml2_arista
self.managed_physnets = confg['managed_physnets']
self.manage_fabric = confg['manage_fabric']
@ -74,7 +70,6 @@ class AristaDriver(driver_api.MechanismDriver):
def initialize(self):
self.mlag_pairs = db_lib.get_mlag_physnets()
self.sg_handler = sec_group_callback.AristaSecurityGroupHandler(self)
def get_workers(self):
return [arista_sync.AristaSyncWorker(self.provision_queue)]

115
networking_arista/ml2/rpc/base.py

@ -23,7 +23,6 @@ import six
from networking_arista._i18n import _, _LW
from networking_arista.common import exceptions as arista_exc
from networking_arista.ml2 import arista_sec_gp
LOG = logging.getLogger(__name__)
@ -43,7 +42,6 @@ class AristaRPCWrapperBase(object):
self.sync_interval = cfg.CONF.ml2_arista.sync_interval
self.conn_timeout = cfg.CONF.ml2_arista.conn_timeout
self.eapi_hosts = cfg.CONF.ml2_arista.eapi_host.split(',')
self.security_group_driver = arista_sec_gp.AristaSecGroupSwitchDriver()
# Indication of CVX availabililty in the driver.
self._cvx_available = True
@ -109,116 +107,3 @@ class AristaRPCWrapperBase(object):
self.set_cvx_unavailable()
return False
def _clean_acls(self, sg, failed_switch, switches_to_clean):
"""This is a helper function to clean up ACLs on switches.
This called from within an exception - when apply_acl fails.
Therefore, ensure that exception is raised after the cleanup
is done.
:param sg: Security Group to be removed
:param failed_switch: IP of the switch where ACL failed
:param switches_to_clean: List of switches containing link info
"""
if not switches_to_clean:
# This means the no switch needs cleaning - so, simply raise the
# the exception and bail out
msg = (_("Failed to apply ACL %(sg)s on switch %(switch)s") %
{'sg': sg, 'switch': failed_switch})
LOG.error(msg)
for s in switches_to_clean:
try:
# Port is being updated to remove security groups
self.security_group_driver.remove_acl(sg,
s['switch_id'],
s['port_id'],
s['switch_info'])
except Exception:
msg = (_("Failed to remove ACL %(sg)s on switch %(switch)%") %
{'sg': sg, 'switch': s['switch_info']})
LOG.warning(msg)
raise arista_exc.AristaSecurityGroupError(msg=msg)
def create_acl(self, sg):
"""Creates an ACL on Arista Switch.
Deals with multiple configurations - such as multiple switches
"""
self.security_group_driver.create_acl(sg)
def delete_acl(self, sg):
"""Deletes an ACL from Arista Switch.
Deals with multiple configurations - such as multiple switches
"""
self.security_group_driver.delete_acl(sg)
def create_acl_rule(self, sgr):
"""Creates an ACL on Arista Switch.
For a given Security Group (ACL), it adds additional rule
Deals with multiple configurations - such as multiple switches
"""
self.security_group_driver.create_acl_rule(sgr)
def delete_acl_rule(self, sgr):
"""Deletes an ACL rule on Arista Switch.
For a given Security Group (ACL), it removes a rule
Deals with multiple configurations - such as multiple switches
"""
self.security_group_driver.delete_acl_rule(sgr)
def perform_sync_of_sg(self):
"""Perform sync of the security groups between ML2 and EOS.
This is unconditional sync to ensure that all security
ACLs are pushed to all the switches, in case of switch
or neutron reboot
"""
self.security_group_driver.perform_sync_of_sg()
def apply_security_group(self, security_group, switch_bindings):
"""Applies ACLs on switch interface.
Translates neutron security group to switch ACL and applies the ACLs
on all the switch interfaces defined in the switch_bindings.
:param security_group: Neutron security group
:param switch_bindings: Switch link information
"""
switches_with_acl = []
for binding in switch_bindings:
try:
self.security_group_driver.apply_acl(security_group,
binding['switch_id'],
binding['port_id'],
binding['switch_info'])
switches_with_acl.append(binding)
except Exception:
message = _LW('Unable to apply security group on %s') % (
binding['switch_id'])
LOG.warning(message)
self._clean_acls(security_group, binding['switch_id'],
switches_with_acl)
def remove_security_group(self, security_group, switch_bindings):
"""Removes ACLs from switch interface
Translates neutron security group to switch ACL and removes the ACLs
from all the switch interfaces defined in the switch_bindings.
:param security_group: Neutron security group
:param switch_bindings: Switch link information
"""
for binding in switch_bindings:
try:
self.security_group_driver.remove_acl(security_group,
binding['switch_id'],
binding['port_id'],
binding['switch_info'])
except Exception:
message = _LW('Unable to remove security group from %s') % (
binding['switch_id'])
LOG.warning(message)

117
networking_arista/ml2/sec_group_callback.py

@ -1,117 +0,0 @@
# Copyright (c) 2016 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.
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from oslo_log import helpers as log_helpers
from oslo_log import log as logging
from oslo_utils import excutils
from networking_arista._i18n import _LE
LOG = logging.getLogger(__name__)
class AristaSecurityGroupHandler(object):
"""Security Group Handler for Arista networking hardware.
Registers for the notification of security group updates.
Once a notification is recieved, it takes appropriate actions by updating
Arista hardware appropriately.
"""
def __init__(self, client):
self.client = client
self.subscribe()
@log_helpers.log_method_call
def create_security_group(self, resource, event, trigger, **kwargs):
sg = kwargs.get('security_group')
try:
self.client.create_security_group(sg)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to create a security group %(sg_id)s "
"in Arista Driver: %(err)s"),
{"sg_id": sg["id"], "err": e})
try:
self.client.delete_security_group(sg)
except Exception:
LOG.exception(_LE("Failed to delete security group %s"),
sg['id'])
@log_helpers.log_method_call
def delete_security_group(self, resource, event, trigger, **kwargs):
sg = kwargs.get('security_group')
try:
self.client.delete_security_group(sg)
except Exception as e:
LOG.error(_LE("Failed to delete security group %(sg_id)s "
"in Arista Driver: %(err)s"),
{"sg_id": sg["id"], "err": e})
@log_helpers.log_method_call
def update_security_group(self, resource, event, trigger, **kwargs):
sg = kwargs.get('security_group')
try:
self.client.update_security_group(sg)
except Exception as e:
LOG.error(_LE("Failed to update security group %(sg_id)s "
"in Arista Driver: %(err)s"),
{"sg_id": sg["id"], "err": e})
@log_helpers.log_method_call
def create_security_group_rule(self, resource, event, trigger, **kwargs):
sgr = kwargs.get('security_group_rule')
try:
self.client.create_security_group_rule(sgr)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to create a security group %(sgr_id)s "
"rule in Arista Driver: %(err)s"),
{"sgr_id": sgr["id"], "err": e})
try:
self.client.delete_security_group_rule(sgr)
except Exception:
LOG.exception(_LE("Failed to delete security group "
"rule %s"), sgr['id'])
@log_helpers.log_method_call
def delete_security_group_rule(self, resource, event, trigger, **kwargs):
sgr_id = kwargs.get('security_group_rule_id')
try:
self.client.delete_security_group_rule(sgr_id)
except Exception as e:
LOG.error(_LE("Failed to delete security group %(sgr_id)s "
"rule in Arista Driver: %(err)s"),
{"sgr_id": sgr_id, "err": e})
def subscribe(self):
# Subscribe to the events related to security groups and rules
registry.subscribe(
self.create_security_group, resources.SECURITY_GROUP,
events.AFTER_CREATE)
registry.subscribe(
self.update_security_group, resources.SECURITY_GROUP,
events.AFTER_UPDATE)
registry.subscribe(
self.delete_security_group, resources.SECURITY_GROUP,
events.BEFORE_DELETE)
registry.subscribe(
self.create_security_group_rule, resources.SECURITY_GROUP_RULE,
events.AFTER_CREATE)
registry.subscribe(
self.delete_security_group_rule, resources.SECURITY_GROUP_RULE,
events.BEFORE_DELETE)

0
networking_arista/ml2/security_groups/__init__.py

150
networking_arista/ml2/security_groups/arista_security_groups.py

@ -0,0 +1,150 @@
# Copyright (c) 2016 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.
from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.plugins import directory
from neutron_lib.services import base as service_base
from oslo_log import helpers as log_helpers
from oslo_log import log as logging
from networking_arista.ml2.security_groups import security_group_sync
from networking_arista.ml2.security_groups import switch_helper
LOG = logging.getLogger(__name__)
class AristaSecurityGroupHandler(
service_base.ServicePluginBase,
switch_helper.AristaSecurityGroupSwitchHelper):
"""Security Group Handler for Arista networking hardware.
Registers for the notification of security group updates.
Once a notification is recieved, it takes appropriate actions by updating
Arista hardware appropriately.
"""
def __init__(self):
super(AristaSecurityGroupHandler, self).__init__()
self.initialize_switch_endpoints()
self.subscribe()
def get_plugin_description(self):
return "Arista baremetal security group service plugin"
@classmethod
def get_plugin_type(cls):
return "arista_security_group"
def get_workers(self):
return security_group_sync.AristaSecurityGroupSyncWorker()
@log_helpers.log_method_call
def create_security_group(self, resource, event, trigger, **kwargs):
sg = kwargs.get('security_group')
rules = sg['security_group_rules']
sg_id = sg['id']
cmds = self.get_create_security_group_commands(sg_id, rules)
self.run_cmds_on_all_switches(cmds)
@log_helpers.log_method_call
def delete_security_group(self, resource, event, trigger, **kwargs):
sg_id = kwargs.get('security_group_id')
cmds = self.get_delete_security_group_commands(sg_id)
self.run_cmds_on_all_switches(cmds)
@log_helpers.log_method_call
def create_security_group_rule(self, resource, event, trigger, **kwargs):
sgr = kwargs.get('security_group_rule')
sg_id = sgr['security_group_id']
cmds = self.get_create_security_group_rule_commands(sg_id, sgr)
self.run_cmds_on_all_switches(cmds)
@log_helpers.log_method_call
def delete_security_group_rule(self, resource, event, trigger, **kwargs):
sgr_id = kwargs.get('security_group_rule_id')
context = kwargs.get('context')
plugin = directory.get_plugin()
sgr = plugin.get_security_group_rule(context, sgr_id)
sg_id = sgr['security_group_id']
cmds = self.get_delete_security_group_rule_commands(sg_id, sgr)
self.run_cmds_on_all_switches(cmds)
@staticmethod
def _valid_baremetal_port(port):
"""Check if port is a baremetal port with exactly one security group"""
if port.get(portbindings.VNIC_TYPE) != portbindings.VNIC_BAREMETAL:
return False
sgs = port.get('security_groups', [])
if len(sgs) == 0:
# Nothing to do
return False
if len(port.get('security_groups', [])) > 1:
LOG.warning('SG provisioning failed for %(port)s. Only one '
'SG may be applied per port.',
{'port': port['id']})
return False
return True
@log_helpers.log_method_call
def apply_security_group(self, resource, event, trigger, **kwargs):
port = kwargs.get('port')
if not self._valid_baremetal_port(port):
return
# _valid_baremetal_port guarantees we have exactly one SG
sg_id = port.get('security_groups')[0]
profile = port.get(portbindings.PROFILE, {})
self._update_port_group_info(switches=self._get_switches(profile))
switch_cmds = self.get_apply_security_group_commands(sg_id, profile)
self.run_per_switch_cmds(switch_cmds)
@log_helpers.log_method_call
def remove_security_group(self, resource, event, trigger, **kwargs):
port = kwargs.get('port')
if not self._valid_baremetal_port(port):
return
# _valid_baremetal_port guarantees we have exactly one SG
sg_id = port.get('security_groups')[0]
profile = port.get(portbindings.PROFILE, {})
self._update_port_group_info(switches=self._get_switches(profile))
switch_cmds = self.get_remove_security_group_commands(sg_id, profile)
self.run_per_switch_cmds(switch_cmds)
def subscribe(self):
# Subscribe to the events related to security groups and rules
registry.subscribe(
self.create_security_group, resources.SECURITY_GROUP,
events.AFTER_CREATE)
registry.subscribe(
self.delete_security_group, resources.SECURITY_GROUP,
events.AFTER_DELETE)
registry.subscribe(
self.create_security_group_rule, resources.SECURITY_GROUP_RULE,
events.AFTER_CREATE)
# We need to handle SG rules in before delete to be able to query
# the db for the rule details
registry.subscribe(
self.delete_security_group_rule, resources.SECURITY_GROUP_RULE,
events.BEFORE_DELETE)
# Apply SG rules to intfs on AFTER_UPDATE, remove them on AFTER_DELETE
registry.subscribe(
self.apply_security_group, resources.PORT, events.AFTER_UPDATE)
registry.subscribe(
self.remove_security_group, resources.PORT, events.AFTER_DELETE)

112
networking_arista/ml2/security_groups/security_group_sync.py

@ -0,0 +1,112 @@
# Copyright (c) 2018 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.
import json
from neutron_lib import worker
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import loopingcall
from networking_arista.common import db_lib
from networking_arista.ml2.security_groups import switch_helper
LOG = logging.getLogger(__name__)
class AristaSecurityGroupSyncWorker(
worker.BaseWorker,
switch_helper.AristaSecurityGroupSwitchHelper):
"""Worker that handles synchronizing Security Group ACLs on Arista switches
The worker periodically queries the neutron db and sends all security
groups, security group rules and security group port bindings to to
registered switches.
"""
def __init__(self):
super(AristaSecurityGroupSyncWorker, self).__init__()
self.initialize_switch_endpoints()
self._loop = None
def start(self):
super(AristaSecurityGroupSyncWorker, self).start()
if self._loop is None:
self._loop = loopingcall.FixedIntervalLoopingCall(
self.synchronize
)
self._loop.start(interval=cfg.CONF.ml2_arista.sync_interval)
def stop(self):
if self._loop is not None:
self._loop.stop()
def wait(self):
if self._loop is not None:
self._loop.wait()
self._loop = None
def reset(self):
self.stop()
self.wait()
self.start()
def update_switch_commands(self, full_switch_cmds, sg_id, profile):
"""Add port's SG bindings to existing per switch cmds
This is an optimization to configure all interfaces on a switch
with a single eAPI call, rather than one call per security group
binding.
"""
new_cmds = self.get_apply_security_group_commands(sg_id, profile)
for switch_ip, cmds in new_cmds.items():
if switch_ip not in full_switch_cmds:
full_switch_cmds[switch_ip] = []
full_switch_cmds[switch_ip].extend(cmds)
return full_switch_cmds
def synchronize(self):
"""Perform sync of the security groups between ML2 and EOS.
This is unconditional sync to ensure that all security
ACLs are pushed to all the switches, in case of switch
or neutron reboot.
There is a known limitation in that stale groups, rules
and bindings are never cleaned up.
"""
security_groups = db_lib.get_security_groups()
sg_bindings = db_lib.get_baremetal_sg_bindings()
# Ensure that all SGs have default deny for ingress and egress
cmds = []
for sg in security_groups:
cmds.extend(self.get_create_security_group_commands(sg['id'],
sg['rules']))
self.run_cmds_on_all_switches(cmds)
self._update_port_group_info()
# Apply appropriate ACLs to baremetal connected ports
switch_cmds = {}
for sg_binding, port_binding in sg_bindings:
sg_id = sg_binding['security_group_id']
try:
binding_profile = json.loads(port_binding.profile)
except ValueError:
binding_profile = {}
switch_cmds = self.update_switch_commands(switch_cmds, sg_id,
binding_profile)
self.run_per_switch_cmds(switch_cmds)

323
networking_arista/ml2/security_groups/switch_helper.py

@ -0,0 +1,323 @@
# Copyright (c) 2018 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.
import re
from neutron_lib import constants as n_const
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 exceptions as arista_exc
from networking_arista.common import utils
LOG = logging.getLogger(__name__)
class AristaSecurityGroupSwitchHelper(object):
"""Helper class for applying baremetal security groups on Arista switches
This helper class contains methods for adding and removing security
groups, security group rules and security group port bindings to and from
Arista switches.
"""
def initialize_switch_endpoints(self):
"""Initialize endpoints for switch communication"""
self._switches = {}
self._port_group_info = {}
self._validate_config()
for s in cfg.CONF.ml2_arista.switch_info:
switch_ip, switch_user, switch_pass = s.split(":")
if switch_pass == "''":
switch_pass = ''
self._switches[switch_ip] = api.EAPIClient(
switch_ip,
switch_user,
switch_pass,
verify=False,
timeout=cfg.CONF.ml2_arista.conn_timeout)
def _validate_config(self):
"""Ensure at least one switch is configured"""
if len(cfg.CONF.ml2_arista.get('switch_info')) < 1:
msg = _('Required option - when "sec_group_support" is enabled, '
'at least one switch must be specified ')
LOG.exception(msg)
raise arista_exc.AristaConfigError(msg=msg)
def _run_openstack_sg_cmds(self, commands, switch):
"""Execute/sends a CAPI (Command API) command to EOS.
In this method, list of commands is appended with prefix and