--- /dev/null
+# Defines configuration options specific for Arista ML2 Mechanism driver
+
+[ml2_arista]
+# (StrOpt) EOS IP address. This is required field. If not set, all
+# communications to Arista EOS will fail
+#
+# eapi_host =
+# Example: eapi_host = 192.168.0.1
+#
+# (StrOpt) EOS command API username. This is required field.
+# if not set, all communications to Arista EOS will fail.
+#
+# eapi_username =
+# Example: arista_eapi_username = admin
+#
+# (StrOpt) EOS command API password. This is required field.
+# if not set, all communications to Arista EOS will fail.
+#
+# eapi_password =
+# Example: eapi_password = my_password
+#
+# (StrOpt) Defines if hostnames are sent to Arista EOS as FQDNs
+# ("node1.domain.com") or as short names ("node1"). This is
+# optional. If not set, a value of "True" is assumed.
+#
+# use_fqdn =
+# Example: use_fqdn = 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.
+#
+# sync_interval =
+# Example: sync_interval = 60
+#
+# (StrOpt) Defines Region Name that is assigned to this OpenStack Controller.
+# This is useful when multiple OpenStack/Neutron controllers are
+# managing the same Arista HW clusters. Note that this name must
+# match with the region name registered (or known) to keystone
+# service. Authentication with Keysotne is performed by EOS.
+# This is optional. If not set, a value of "RegionOne" is assumed.
+#
+# region_name =
+# Example: region_name = RegionOne
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 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.
+#
+
+"""DB Migration for Arista ml2 mechanism driver
+
+Revision ID: 14f24494ca31
+Revises: 2a3bae1ceb8
+Create Date: 2013-08-15 18:54:16.083640
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '14f24494ca31'
+down_revision = '2a3bae1ceb8'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+ 'neutron.plugins.ml2.plugin.Ml2Plugin'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+from neutron.db import migration
+
+
+def upgrade(active_plugins=None, options=None):
+ if not migration.should_run(active_plugins, migration_for_plugins):
+ return
+
+ op.create_table(
+ 'arista_provisioned_nets',
+ sa.Column('tenant_id', sa.String(length=255), nullable=True),
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.Column('network_id', sa.String(length=36), nullable=True),
+ sa.Column('segmentation_id', sa.Integer(),
+ autoincrement=False, nullable=True),
+ sa.PrimaryKeyConstraint('id'))
+
+ op.create_table(
+ 'arista_provisioned_vms',
+ sa.Column('tenant_id', sa.String(length=255), nullable=True),
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.Column('vm_id', sa.String(length=36), nullable=True),
+ sa.Column('host_id', sa.String(length=255), nullable=True),
+ sa.Column('port_id', sa.String(length=36), nullable=True),
+ sa.Column('network_id', sa.String(length=36), nullable=True),
+ sa.PrimaryKeyConstraint('id'))
+
+ op.create_table(
+ 'arista_provisioned_tenants',
+ sa.Column('tenant_id', sa.String(length=255), nullable=True),
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.PrimaryKeyConstraint('id'))
+
+
+def downgrade(active_plugins=None, options=None):
+ if not migration.should_run(active_plugins, migration_for_plugins):
+ return
+
+ op.drop_table('arista_provisioned_tenants')
+ op.drop_table('arista_provisioned_vms')
+ op.drop_table('arista_provisioned_nets')
--- /dev/null
+
+Arista Neutron ML2 Mechanism Driver
+
+This mechanism driver implements ML2 Driver API and is used to manage the virtual and physical networks using Arista Hardware.
+
+Note: Initial verison of this driver support VLANs only.
+
+For more details on use please refer to:
+https://wiki.openstack.org/wiki/Arista-neutron-ml2-driver
--- /dev/null
+# Copyright (c) 2013 OpenStack Foundation
+# 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.
--- /dev/null
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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 oslo.config import cfg
+
+""" Arista ML2 Mechanism driver specific configuration knobs.
+
+Following are user configurable options for Arista ML2 Mechanism
+driver. The eapi_username, eapi_password, and eapi_host are
+required options. Region Name must be the same that is used by
+Keystone service. This option is available to support multiple
+OpenStack/Neutron controllers.
+"""
+
+ARISTA_DRIVER_OPTS = [
+ cfg.StrOpt('eapi_username',
+ default='',
+ help=_('Username for Arista EOS. This is required field.'
+ 'if not set, all communications to Arista EOS'
+ 'will fail')),
+ cfg.StrOpt('eapi_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('eapi_host',
+ default='',
+ help=_('Arista EOS IP address. This is required field.'
+ 'If not set, all communications to Arista EOS'
+ 'will fail')),
+ cfg.BoolOpt('use_fqdn',
+ default=True,
+ help=_('Defines if hostnames are sent to Arista EOS as FQDNs'
+ '("node1.domain.com") or as short names ("node1").'
+ 'This is optional. If not set, a value of "True"'
+ 'is assumed.')),
+ cfg.IntOpt('sync_interval',
+ default=180,
+ help=_('Sync interval in seconds between Neutron 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.StrOpt('region_name',
+ default='RegionOne',
+ help=_('Defines Region Name that is assigned to this OpenStack'
+ 'Controller. This is useful when multiple'
+ 'OpenStack/Neutron controllers are managing the same'
+ 'Arista HW clusters. Note that this name must match with'
+ 'the region name registered (or known) to keystone'
+ 'service. Authentication with Keysotne is performed by'
+ 'EOS. This is optional. If not set, a value of'
+ '"RegionOne" is assumed'))
+]
+
+cfg.CONF.register_opts(ARISTA_DRIVER_OPTS, "ml2_arista")
--- /dev/null
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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 sqlalchemy as sa
+
+from neutron import context as nctx
+import neutron.db.api as db
+from neutron.db import db_base_plugin_v2
+from neutron.db import model_base
+from neutron.db import models_v2
+
+VLAN_SEGMENTATION = 'vlan'
+
+UUID_LEN = 36
+STR_LEN = 255
+
+
+class AristaProvisionedNets(model_base.BASEV2, models_v2.HasId,
+ models_v2.HasTenant):
+ """Stores networks provisioned on Arista EOS.
+
+ Saves the segmentation ID for each network that is provisioned
+ on EOS. This information is used during synchronization between
+ Neutron and EOS.
+ """
+ __tablename__ = 'arista_provisioned_nets'
+
+ network_id = sa.Column(sa.String(UUID_LEN))
+ segmentation_id = sa.Column(sa.Integer)
+
+ def eos_network_representation(self, segmentation_type):
+ return {u'networkId': self.network_id,
+ u'segmentationTypeId': self.segmentation_id,
+ u'segmentationType': segmentation_type}
+
+
+class AristaProvisionedVms(model_base.BASEV2, models_v2.HasId,
+ models_v2.HasTenant):
+ """Stores VMs provisioned on Arista EOS.
+
+ All VMs launched on physical hosts connected to Arista
+ Switches are remembered
+ """
+ __tablename__ = 'arista_provisioned_vms'
+
+ vm_id = sa.Column(sa.String(UUID_LEN))
+ host_id = sa.Column(sa.String(STR_LEN))
+ port_id = sa.Column(sa.String(UUID_LEN))
+ network_id = sa.Column(sa.String(UUID_LEN))
+
+ def eos_vm_representation(self):
+ return {u'vmId': self.vm_id,
+ u'host': self.host_id,
+ u'ports': {self.port_id: [{u'portId': self.port_id,
+ u'networkId': self.network_id}]}}
+
+ def eos_port_representation(self):
+ return {u'vmId': self.vm_id,
+ u'host': self.host_id,
+ u'portId': self.port_id,
+ u'networkId': self.network_id}
+
+
+class AristaProvisionedTenants(model_base.BASEV2, models_v2.HasId,
+ models_v2.HasTenant):
+ """Stores Tenants provisioned on Arista EOS.
+
+ Tenants list is maintained for sync between Neutron and EOS.
+ """
+ __tablename__ = 'arista_provisioned_tenants'
+
+ def eos_tenant_representation(self):
+ return {u'tenantId': self.tenant_id}
+
+
+def remember_tenant(tenant_id):
+ """Stores a tenant information in repository.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ tenant = (session.query(AristaProvisionedTenants).
+ filter_by(tenant_id=tenant_id).first())
+
+ if not tenant:
+ tenant = AristaProvisionedTenants(
+ tenant_id=tenant_id)
+ session.add(tenant)
+
+
+def forget_tenant(tenant_id):
+ """Removes a tenant information from repository.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ (session.query(AristaProvisionedTenants).
+ filter_by(tenant_id=tenant_id).
+ delete())
+
+
+def get_all_tenants():
+ """Returns a list of all tenants stored in repository."""
+ session = db.get_session()
+ with session.begin():
+ return session.query(AristaProvisionedTenants).all()
+
+
+def num_provisioned_tenants():
+ """Returns number of tenants stored in repository."""
+ session = db.get_session()
+ with session.begin():
+ return session.query(AristaProvisionedTenants).count()
+
+
+def remember_vm(vm_id, host_id, port_id, network_id, tenant_id):
+ """Stores all relevent information about a VM in repository.
+
+ :param vm_id: globally unique identifier for VM instance
+ :param host_id: ID of the host where the VM is placed
+ :param port_id: globally unique port ID that connects VM to network
+ :param network_id: globally unique neutron network identifier
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ vm = (session.query(AristaProvisionedVms).
+ filter_by(vm_id=vm_id, host_id=host_id,
+ port_id=port_id, tenant_id=tenant_id,
+ network_id=network_id).first())
+
+ if not vm:
+ vm = AristaProvisionedVms(
+ vm_id=vm_id,
+ host_id=host_id,
+ port_id=port_id,
+ network_id=network_id,
+ tenant_id=tenant_id)
+ session.add(vm)
+
+
+def forget_vm(vm_id, host_id, port_id, network_id, tenant_id):
+ """Removes all relevent information about a VM from repository.
+
+ :param vm_id: globally unique identifier for VM instance
+ :param host_id: ID of the host where the VM is placed
+ :param port_id: globally unique port ID that connects VM to network
+ :param network_id: globally unique neutron network identifier
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ (session.query(AristaProvisionedVms).
+ filter_by(vm_id=vm_id, host_id=host_id,
+ port_id=port_id, tenant_id=tenant_id,
+ network_id=network_id).delete())
+
+
+def remember_network(tenant_id, network_id, segmentation_id):
+ """Stores all relevent information about a Network in repository.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ :param segmentation_id: VLAN ID that is assigned to the network
+ """
+ session = db.get_session()
+ with session.begin():
+ net = (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id,
+ network_id=network_id).first())
+
+ if not net:
+ net = AristaProvisionedNets(
+ tenant_id=tenant_id,
+ network_id=network_id,
+ segmentation_id=segmentation_id)
+ session.add(net)
+
+
+def forget_network(tenant_id, network_id):
+ """Deletes all relevent information about a Network from repository.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id, network_id=network_id).
+ delete())
+
+
+def get_segmentation_id(tenant_id, network_id):
+ """Returns Segmentation ID (VLAN) associated with a network.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ net = (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id,
+ network_id=network_id).first())
+ return net and net.segmentation_id or None
+
+
+def is_vm_provisioned(vm_id, host_id, port_id,
+ network_id, tenant_id):
+ """Checks if a VM is already known to EOS
+
+ :returns: True, if yes; False otherwise.
+ :param vm_id: globally unique identifier for VM instance
+ :param host_id: ID of the host where the VM is placed
+ :param port_id: globally unique port ID that connects VM to network
+ :param network_id: globally unique neutron network identifier
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ num_vm = (session.query(AristaProvisionedVms).
+ filter_by(tenant_id=tenant_id,
+ vm_id=vm_id,
+ port_id=port_id,
+ network_id=network_id,
+ host_id=host_id).count())
+ return num_vm > 0
+
+
+def is_network_provisioned(tenant_id, network_id, seg_id=None):
+ """Checks if a networks is already known to EOS
+
+ :returns: True, if yes; False otherwise.
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ :param seg_id: Optionally matches the segmentation ID (VLAN)
+ """
+ session = db.get_session()
+ with session.begin():
+ if not seg_id:
+ num_nets = (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id,
+ network_id=network_id).count())
+ else:
+ num_nets = (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id,
+ network_id=network_id,
+ segmentation_id=seg_id).count())
+ return num_nets > 0
+
+
+def is_tenant_provisioned(tenant_id):
+ """Checks if a tenant is already known to EOS
+
+ :returns: True, if yes; False otherwise.
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ num_tenants = (session.query(AristaProvisionedTenants).
+ filter_by(tenant_id=tenant_id).count())
+ return num_tenants > 0
+
+
+def num_nets_provisioned(tenant_id):
+ """Returns number of networks for a given tennat.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ return (session.query(AristaProvisionedNets).
+ filter_by(tenant_id=tenant_id).count())
+
+
+def num_vms_provisioned(tenant_id):
+ """Returns number of VMs for a given tennat.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ return (session.query(AristaProvisionedVms).
+ filter_by(tenant_id=tenant_id).count())
+
+
+def get_networks(tenant_id):
+ """Returns all networks for a given tenant in EOS-compatible format.
+
+ See AristaRPCWrapper.get_network_list() for return value format.
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ model = AristaProvisionedNets
+ # hack for pep8 E711: comparison to None should be
+ # 'if cond is not None'
+ none = None
+ all_nets = (session.query(model).
+ filter(model.tenant_id == tenant_id,
+ model.segmentation_id != none))
+ res = dict(
+ (net.network_id, net.eos_network_representation(
+ VLAN_SEGMENTATION))
+ for net in all_nets
+ )
+ return res
+
+
+def get_vms(tenant_id):
+ """Returns all VMs for a given tenant in EOS-compatible format.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ model = AristaProvisionedVms
+ # hack for pep8 E711: comparison to None should be
+ # 'if cond is not None'
+ none = None
+ all_vms = (session.query(model).
+ filter(model.tenant_id == tenant_id,
+ model.host_id != none,
+ model.vm_id != none,
+ model.network_id != none,
+ model.port_id != none))
+ res = dict(
+ (vm.vm_id, vm.eos_vm_representation())
+ for vm in all_vms
+ )
+ return res
+
+
+def get_ports(tenant_id):
+ """Returns all ports of VMs in EOS-compatible format.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ session = db.get_session()
+ with session.begin():
+ model = AristaProvisionedVms
+ # hack for pep8 E711: comparison to None should be
+ # 'if cond is not None'
+ none = None
+ all_ports = (session.query(model).
+ filter(model.tenant_id == tenant_id,
+ model.host_id != none,
+ model.vm_id != none,
+ model.network_id != none,
+ model.port_id != none))
+ res = dict(
+ (port.port_id, port.eos_port_representation())
+ for port in all_ports
+ )
+ return res
+
+
+def get_tenants():
+ """Returns list of all tenants in EOS-compatible format."""
+ session = db.get_session()
+ with session.begin():
+ model = AristaProvisionedTenants
+ all_tenants = session.query(model)
+ res = dict(
+ (tenant.tenant_id, tenant.eos_tenant_representation())
+ for tenant in all_tenants
+ )
+ return res
+
+
+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.
+ """
+
+ def __init__(self):
+ self.admin_ctx = nctx.get_admin_context()
+
+ def get_network_name(self, tenant_id, network_id):
+ network = self._get_network(tenant_id, network_id)
+ network_name = None
+ if network:
+ network_name = network[0]['name']
+ return network_name
+
+ 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_all_ports_for_vm(self, tenant_id, vm_id):
+ filters = {'tenant_id': [tenant_id],
+ 'device_id': [vm_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 []
--- /dev/null
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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.
+
+
+"""Exceptions used by Arista ML2 Mechanism Driver."""
+
+from neutron.common import exceptions
+
+
+class AristaRpcError(exceptions.NeutronException):
+ message = _('%(msg)s')
+
+
+class AristaConfigError(exceptions.NeutronException):
+ message = _('%(msg)s')
--- /dev/null
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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 threading
+
+import jsonrpclib
+from oslo.config import cfg
+
+from neutron.openstack.common import log as logging
+from neutron.plugins.ml2.common import exceptions as ml2_exc
+from neutron.plugins.ml2 import driver_api
+from neutron.plugins.ml2.drivers.mech_arista import config # noqa
+from neutron.plugins.ml2.drivers.mech_arista import db
+from neutron.plugins.ml2.drivers.mech_arista import exceptions as arista_exc
+
+LOG = logging.getLogger(__name__)
+
+EOS_UNREACHABLE_MSG = 'Unable to reach EOS'
+
+
+class AristaRPCWrapper(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
+ """
+ required_options = ['eapi_username',
+ 'eapi_password',
+ 'eapi_host']
+
+ def __init__(self):
+ self._server = jsonrpclib.Server(self._eapi_host_url())
+ self.keystone_conf = cfg.CONF.keystone_authtoken
+ self.region = cfg.CONF.ml2_arista.region_name
+
+ def _keystone_url(self):
+ keystone_auth_url = ('%s://%s:%s/v2.0/' %
+ (self.keystone_conf.auth_protocol,
+ self.keystone_conf.auth_host,
+ self.keystone_conf.auth_port))
+ return keystone_auth_url
+
+ def get_tenants(self):
+ """Returns dict of all tanants known by EOS.
+
+ :returns: dictionary containing the networks per tenant
+ and VMs allocated per tenant
+ """
+ cmds = ['show openstack config region %s' % self.region]
+ command_output = self._run_openstack_cmds(cmds)
+ tenants = command_output[0]['tenants']
+
+ return tenants
+
+ def plug_host_into_network(self, vm_id, host, port_id,
+ network_id, tenant_id, port_name):
+ """Creates VLAN between TOR and compute host.
+
+ :param vm_id: globally unique identifier for VM instance
+ :param host: ID of the host where the VM is placed
+ :param port_id: globally unique port ID that connects VM to network
+ :param network_id: globally unique neutron network identifier
+ :param tenant_id: globally unique neutron tenant identifier
+ :param port_name: Name of the port - for display purposes
+ """
+ cmds = ['tenant %s' % tenant_id,
+ 'vm id %s hostid %s' % (vm_id, host)]
+ if port_name:
+ cmds.append('port id %s name %s network-id %s' %
+ (port_id, port_name, network_id))
+ else:
+ cmds.append('port id %s network-id %s' %
+ (port_id, network_id))
+ cmds.append('exit')
+ cmds.append('exit')
+ self._run_openstack_cmds(cmds)
+
+ def unplug_host_from_network(self, vm_id, host, port_id,
+ network_id, tenant_id):
+ """Removes previously configured VLAN between TOR and a host.
+
+ :param vm_id: globally unique identifier for VM instance
+ :param host: ID of the host where the VM is placed
+ :param port_id: globally unique port ID that connects VM to network
+ :param network_id: globally unique neutron network identifier
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ cmds = ['tenant %s' % tenant_id,
+ 'vm id %s host %s' % (vm_id, host),
+ 'no port id %s network-id %s' % (port_id, network_id),
+ 'exit',
+ 'exit']
+ self._run_openstack_cmds(cmds)
+
+ def create_network(self, tenant_id, network_id, network_name, seg_id):
+ """Creates a network on Arista Hardware
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ :param network_name: Network name - for display purposes
+ :param seg_id: Segment ID of the network
+ """
+ cmds = ['tenant %s' % tenant_id]
+ if network_name:
+ cmds.append('network id %s name %s' % (network_id, network_name))
+ else:
+ cmds.append('network id %s' % network_id)
+ cmds.append('segment 1 type vlan id %d' % seg_id)
+ cmds.append('exit')
+ cmds.append('exit')
+ cmds.append('exit')
+
+ self._run_openstack_cmds(cmds)
+
+ def create_network_segments(self, tenant_id, network_id,
+ network_name, segments):
+ """Creates a network on Arista Hardware
+
+ Note: This method is not used at the moment. create_network()
+ is used instead. This will be used once the support for
+ multiple segments is added in Neutron.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ :param network_name: Network name - for display purposes
+ :param segments: List of segments in a given network
+ """
+ if segments:
+ cmds = ['tenant %s' % tenant_id,
+ 'network id %s name %s' % (network_id, network_name)]
+ seg_num = 1
+ for seg in segments:
+ cmds.append('segment %d type %s id %d' % (seg_num,
+ seg['network_type'], seg['segmentation_id']))
+ seg_num += 1
+ cmds.append('exit') # exit for segment mode
+ cmds.append('exit') # exit for network mode
+ cmds.append('exit') # exit for tenant mode
+
+ self._run_openstack_cmds(cmds)
+
+ def delete_network(self, tenant_id, network_id):
+ """Deletes a specified network for a given tenant
+
+ :param tenant_id: globally unique neutron tenant identifier
+ :param network_id: globally unique neutron network identifier
+ """
+ cmds = ['tenant %s' % tenant_id,
+ 'no network id %s' % network_id,
+ 'exit',
+ 'exit']
+ self._run_openstack_cmds(cmds)
+
+ def delete_vm(self, tenant_id, vm_id):
+ """Deletes a VM from EOS for a given tenant
+
+ :param tenant_id : globally unique neutron tenant identifier
+ :param vm_id : id of a VM that needs to be deleted.
+ """
+ cmds = ['tenant %s' % tenant_id,
+ 'no vm id %s' % vm_id,
+ 'exit',
+ 'exit']
+ self._run_openstack_cmds(cmds)
+
+ def delete_tenant(self, tenant_id):
+ """Deletes a given tenant and all its networks and VMs from EOS.
+
+ :param tenant_id: globally unique neutron tenant identifier
+ """
+ cmds = ['no tenant %s' % tenant_id, 'exit']
+ self._run_openstack_cmds(cmds)
+
+ def delete_this_region(self):
+ """Deletes this entire region from EOS.
+
+ This is equivalent of unregistering this Neurtron stack from EOS
+ All networks for all tenants are removed.
+ """
+ cmds = []
+ self._run_openstack_cmds(cmds, deleteRegion=True)
+
+ def _register_with_eos(self):
+ """This is the registration request with EOS.
+
+ This the initial handshake between Neutron and EOS.
+ critical end-point information is registered with EOS.
+ """
+ cmds = ['auth url %s user %s password %s' %
+ (self._keystone_url(),
+ self.keystone_conf.admin_user,
+ self.keystone_conf.admin_password)]
+
+ self._run_openstack_cmds(cmds)
+
+ def _run_openstack_cmds(self, commands, deleteRegion=None):
+ """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 deleteRegion : True/False - to delte entire region from EOS
+ """
+ command_start = ['enable', 'configure', 'management openstack']
+ if deleteRegion:
+ command_start.append('no region %s' % self.region)
+ else:
+ command_start.append('region %s' % self.region)
+ command_end = ['exit', '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 = self._server.runCmds(version=1, cmds=full_command)
+
+ # Remove return values for 'configure terminal',
+ # 'management openstack' and 'exit' commands
+ ret = ret[len(command_start):-len(command_end)]
+ except Exception as error:
+ host = cfg.CONF.ml2_arista.eapi_host
+ msg = ('Error %s while trying to execute commands %s on EOS %s' %
+ (error, full_command, host))
+ LOG.exception(_("%s"), msg)
+ raise arista_exc.AristaRpcError(msg=msg)
+
+ return ret
+
+ def _eapi_host_url(self):
+ self._validate_config()
+
+ user = cfg.CONF.ml2_arista.eapi_username
+ pwd = cfg.CONF.ml2_arista.eapi_password
+ host = cfg.CONF.ml2_arista.eapi_host
+
+ eapi_server_url = ('https://%s:%s@%s/command-api' %
+ (user, pwd, host))
+ return eapi_server_url
+
+ def _validate_config(self):
+ for option in self.required_options:
+ if cfg.CONF.ml2_arista.get(option) is None:
+ msg = _('Required option %s is not set') % option
+ LOG.error(msg)
+ raise arista_exc.AristaConfigError(msg=msg)
+
+
+class SyncService(object):
+ """Synchronizatin of information between Neutron and EOS
+
+ Periodically (through configuration option), this service
+ ensures that Networks and VMs configured on EOS/Arista HW
+ are always in sync with Neutron DB.
+ """
+ def __init__(self, rpc_wrapper, neutron_db):
+ self._rpc = rpc_wrapper
+ self._ndb = neutron_db
+
+ def synchronize(self):
+ """Sends data to EOS which differs from neutron DB."""
+
+ LOG.info('Syncing Neutron <-> EOS')
+ try:
+ eos_tenants = self._rpc.get_tenants()
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available, will try sync later')
+ LOG.warning(msg)
+ return
+
+ db_tenants = db.get_tenants()
+
+ if not db_tenants and eos_tenants:
+ # No tenants configured in Neutron. Clear all EOS state
+ try:
+ self._rpc.delete_this_region()
+ msg = _('No Tenants configured in Neutron DB. But %d '
+ 'tenants disovered in EOS during synchronization.'
+ 'Enitre EOS region is cleared') % len(eos_tenants)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available, failed to delete this region')
+ LOG.warning(msg)
+ return
+
+ if len(eos_tenants) > len(db_tenants):
+ # EOS has extra tenants configured which should not be there.
+ for tenant in eos_tenants:
+ if tenant not in db_tenants:
+ try:
+ self._rpc.delete_tenant(tenant)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available,'
+ 'failed to delete tenant %s') % tenant
+ LOG.warning(msg)
+ return
+
+ # EOS and Neutron has matching set of tenants. Now check
+ # to ensure that networks and VMs match on both sides for
+ # each tenant.
+ for tenant in db_tenants:
+ db_nets = db.get_networks(tenant)
+ db_vms = db.get_vms(tenant)
+ eos_nets = self._get_eos_networks(eos_tenants, tenant)
+ eos_vms = self._get_eos_vms(eos_tenants, tenant)
+
+ # Check for the case if everything is already in sync.
+ if eos_nets == db_nets:
+ # Net list is same in both Neutron and EOS.
+ # check the vM list
+ if eos_vms == db_vms:
+ # Nothing to do. Everything is in sync for this tenant
+ break
+
+ # Neutron DB and EOS reruires synchronization.
+ # First delete anything which should not be EOS
+ # delete VMs from EOS if it is not present in neutron DB
+ for vm_id in eos_vms:
+ if vm_id not in db_vms:
+ try:
+ self._rpc.delete_vm(tenant, vm_id)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available,'
+ 'failed to delete vm %s') % vm_id
+ LOG.warning(msg)
+ return
+
+ # delete network from EOS if it is not present in neutron DB
+ for net_id in eos_nets:
+ if net_id not in db_nets:
+ try:
+ self._rpc.delete_network(tenant, net_id)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available,'
+ 'failed to delete network %s') % net_id
+ LOG.warning(msg)
+ return
+
+ # update networks in EOS if it is present in neutron DB
+ for net_id in db_nets:
+ if net_id not in eos_nets:
+ vlan_id = db_nets[net_id]['segmentationTypeId']
+ net_name = self._ndb.get_network_name(tenant, net_id)
+ try:
+ self._rpc.create_network(tenant, net_id,
+ net_name,
+ vlan_id)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available, failed to create'
+ 'network id %s') % net_id
+ LOG.warning(msg)
+ return
+
+ # Update VMs in EOS if it is present in neutron DB
+ for vm_id in db_vms:
+ if vm_id not in eos_vms:
+ vm = db_vms[vm_id]
+ ports = self._ndb.get_all_ports_for_vm(tenant, vm_id)
+ for port in ports:
+ port_id = port['id']
+ network_id = port['network_id']
+ port_name = port['name']
+ try:
+ self._rpc.plug_host_into_network(vm['vmId'],
+ vm['host'],
+ port_id,
+ network_id,
+ tenant,
+ port_name)
+ except arista_exc.AristaRpcError:
+ msg = _('EOS is not available, failed to create'
+ 'vm id %s') % vm['vmId']
+ LOG.warning(msg)
+
+ def _get_eos_networks(self, eos_tenants, tenant):
+ networks = {}
+ if eos_tenants:
+ networks = eos_tenants[tenant]['tenantNetworks']
+ return networks
+
+ def _get_eos_vms(self, eos_tenants, tenant):
+ vms = {}
+ if eos_tenants:
+ vms = eos_tenants[tenant]['tenantVmInstances']
+ return vms
+
+
+class AristaDriver(driver_api.MechanismDriver):
+ """Ml2 Mechanism driver for Arista networking hardware.
+
+ Remebers all networks and VMs that are provisioned on Arista Hardware.
+ Does not send network provisioning request if the network has already been
+ provisioned before for the given port.
+ """
+ def __init__(self, rpc=None):
+
+ self.rpc = rpc or AristaRPCWrapper()
+ self.ndb = db.NeutronNets()
+
+ confg = cfg.CONF.ml2_arista
+ self.segmentation_type = db.VLAN_SEGMENTATION
+ self.timer = None
+ self.eos = SyncService(self.rpc, self.ndb)
+ self.sync_timeout = confg['sync_interval']
+ self.eos_sync_lock = threading.Lock()
+
+ self._synchronization_thread()
+
+ def initialize(self):
+ self.rpc._register_with_eos()
+ self._cleanupDb()
+
+ def create_network_precommit(self, context):
+ """Remember the tenant, and network information."""
+
+ network = context.current
+ segments = context.network_segments
+ network_id = network['id']
+ tenant_id = network['tenant_id']
+ segmentation_id = segments[0]['segmentation_id']
+ with self.eos_sync_lock:
+ db.remember_tenant(tenant_id)
+ db.remember_network(tenant_id,
+ network_id,
+ segmentation_id)
+
+ def create_network_postcommit(self, context):
+ """Provision the network on the Arista Hardware."""
+
+ network = context.current
+ network_id = network['id']
+ network_name = network['name']
+ tenant_id = network['tenant_id']
+ segments = context.network_segments
+ vlan_id = segments[0]['segmentation_id']
+ with self.eos_sync_lock:
+ if db.is_network_provisioned(tenant_id, network_id):
+ try:
+ self.rpc.create_network(tenant_id,
+ network_id,
+ network_name,
+ vlan_id)
+ except arista_exc.AristaRpcError:
+ LOG.info(EOS_UNREACHABLE_MSG)
+ raise ml2_exc.MechanismDriverError()
+ else:
+ msg = _('Network %s is not created as it is not found in'
+ 'Arista DB') % network_id
+ LOG.info(msg)
+
+ def update_network_precommit(self, context):
+ """At the moment we only support network name change
+
+ Any other change in network is not supprted at this time.
+ We do not store the network names, therefore, no DB store
+ action is performed here.
+ """
+ new_network = context.current
+ orig_network = context.original
+ if new_network['name'] != orig_network['name']:
+ msg = _('Network name changed to %s') % new_network['name']
+ LOG.info(msg)
+
+ def update_network_postcommit(self, context):
+ """At the moment we only support network name change
+
+ If network name is changed, a new network create request is
+ sent to the Arista Hardware.
+ """
+ new_network = context.current
+ orig_network = context.original
+ if new_network['name'] != orig_network['name']:
+ network_id = new_network['id']
+ network_name = new_network['name']
+ tenant_id = new_network['tenant_id']
+ vlan_id = new_network['provider:segmentation_id']
+ with self.eos_sync_lock:
+ if db.is_network_provisioned(tenant_id, network_id):
+ try:
+ self.rpc.create_network(tenant_id,
+ network_id,
+ network_name,
+ vlan_id)
+ except arista_exc.AristaRpcError:
+ LOG.info(EOS_UNREACHABLE_MSG)
+ raise ml2_exc.MechanismDriverError()
+ else:
+ msg = _('Network %s is not updated as it is not found in'
+ 'Arista DB') % network_id
+ LOG.info(msg)
+
+ def delete_network_precommit(self, context):
+ """Delete the network infromation from the DB."""
+ network = context.current
+ network_id = network['id']
+ tenant_id = network['tenant_id']
+ with self.eos_sync_lock:
+ if db.is_network_provisioned(tenant_id, network_id):
+ db.forget_network(tenant_id, network_id)
+ # if necessary, delete tenant as well.
+ self.delete_tenant(tenant_id)
+
+ def delete_network_postcommit(self, context):
+ """Send network delete request to Arista HW."""
+ network = context.current
+ network_id = network['id']
+ tenant_id = network['tenant_id']
+ with self.eos_sync_lock:
+
+ # Succeed deleting network in case EOS is not accessible.
+ # EOS state will be updated by sync thread once EOS gets
+ # alive.
+ try:
+ self.rpc.delete_network(tenant_id, network_id)
+ except arista_exc.AristaRpcError:
+ LOG.info(EOS_UNREACHABLE_MSG)
+ raise ml2_exc.MechanismDriverError()
+
+ def create_port_precommit(self, context):
+ """Remember the infromation about a VM and its ports
+
+ A VM information, along with the physical host information
+ is saved.
+ """
+ port = context.current
+ device_id = port['device_id']
+ device_owner = port['device_owner']
+
+ # TODO(sukhdev) revisit this once port biniding support is implemented
+ host = port['binding:host_id']
+
+ # device_id and device_owner are set on VM boot
+ is_vm_boot = device_id and device_owner
+ if host and is_vm_boot:
+ port_id = port['id']
+ network_id = port['network_id']
+ tenant_id = port['tenant_id']
+ with self.eos_sync_lock:
+ db.remember_vm(device_id, host, port_id,
+ network_id, tenant_id)
+
+ def create_port_postcommit(self, context):
+ """Plug a physical host into a network.
+
+ Send provisioning request to Arista Hardware to plug a host
+ into appropriate network.
+ """
+ port = context.current
+ device_id = port['device_id']
+ device_owner = port['device_owner']
+
+ # TODO(sukhdev) revisit this once port biniding support is implemented
+ host = port['binding:host_id']
+
+ # device_id and device_owner are set on VM boot
+ is_vm_boot = device_id and device_owner
+ if host and is_vm_boot:
+ port_id = port['id']
+ port_name = port['name']
+ network_id = port['network_id']
+ tenant_id = port['tenant_id']
+ with self.eos_sync_lock:
+ hostname = self._host_name(host)
+ segmentation_id = db.get_segmentation_id(tenant_id,
+ network_id)
+ vm_provisioned = db.is_vm_provisioned(device_id,
+ host,
+ port_id,
+ network_id,
+ tenant_id)
+ net_provisioned = db.is_network_provisioned(tenant_id,
+ network_id,
+ segmentation_id)
+ if vm_provisioned and net_provisioned:
+ try:
+ self.rpc.plug_host_into_network(device_id,
+ hostname,
+ port_id,
+ network_id,
+ tenant_id,
+ port_name)
+ except arista_exc.AristaRpcError:
+ LOG.info(EOS_UNREACHABLE_MSG)
+ raise ml2_exc.MechanismDriverError()
+ else:
+ msg = _('VM %s is not created as it is not found in'
+ 'Arista DB') % device_id
+ LOG.info(msg)
+
+ def update_port_precommit(self, context):
+ # TODO(sukhdev) revisit once the port binding support is implemented
+ return
+
+ def update_port_postcommit(self, context):
+ # TODO(sukhdev) revisit once the port binding support is implemented
+ return
+
+ def delete_port_precommit(self, context):
+ """Delete information about a VM and host from the DB."""
+ port = context.current
+
+ # TODO(sukhdev) revisit this once port biniding support is implemented
+ host_id = port['binding:host_id']
+ device_id = port['device_id']
+ tenant_id = port['tenant_id']
+ network_id = port['network_id']
+ port_id = port['id']
+ with self.eos_sync_lock:
+ if db.is_vm_provisioned(device_id, host_id, port_id,
+ network_id, tenant_id):
+ db.forget_vm(device_id, host_id, port_id,
+ network_id, tenant_id)
+ # if necessary, delete tenant as well.
+ self.delete_tenant(tenant_id)
+
+ def delete_port_postcommit(self, context):
+ """unPlug a physical host from a network.
+
+ Send provisioning request to Arista Hardware to unplug a host
+ from appropriate network.
+ """
+ port = context.current
+ device_id = port['device_id']
+
+ # TODO(sukhdev) revisit this once port biniding support is implemented
+ host = port['binding:host_id']
+
+ port_id = port['id']
+ network_id = port['network_id']
+ tenant_id = port['tenant_id']
+
+ try:
+ with self.eos_sync_lock:
+ hostname = self._host_name(host)
+ self.rpc.unplug_host_from_network(device_id,
+ hostname,
+ port_id,
+ network_id,
+ tenant_id)
+ except arista_exc.AristaRpcError:
+ LOG.info(EOS_UNREACHABLE_MSG)
+ raise ml2_exc.MechanismDriverError()
+
+ def delete_tenant(self, tenant_id):
+ """delete a tenant from DB.
+
+ A tenant is deleted only if there is no network or VM configured
+ configured for this tenant.
+ """
+ objects_for_tenant = (db.num_nets_provisioned(tenant_id) +
+ db.num_vms_provisioned(tenant_id))
+ if not objects_for_tenant:
+ db.forget_tenant(tenant_id)
+
+ def _host_name(self, hostname):
+ fqdns_used = cfg.CONF.ml2_arista['use_fqdn']
+ return hostname if fqdns_used else hostname.split('.')[0]
+
+ def _synchronization_thread(self):
+ with self.eos_sync_lock:
+ self.eos.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
+
+ def _cleanupDb(self):
+ """Clean up any uncessary entries in our DB."""
+ db_tenants = db.get_tenants()
+ for tenant in db_tenants:
+ neutron_nets = self.ndb.get_all_networks_for_tenant(tenant)
+ neutron_nets_id = []
+ for net in neutron_nets:
+ neutron_nets_id.append(net['id'])
+ db_nets = db.get_networks(tenant)
+ for net_id in db_nets.keys():
+ if net_id not in neutron_nets_id:
+ db.forget_network(tenant, net_id)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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 mock
+from oslo.config import cfg
+
+import neutron.db.api as ndb
+from neutron.plugins.ml2.drivers.mech_arista import db
+from neutron.plugins.ml2.drivers.mech_arista import exceptions as arista_exc
+from neutron.plugins.ml2.drivers.mech_arista import mechanism_arista as arista
+from neutron.tests import base
+
+
+def setup_arista_wrapper_config(value=None):
+ cfg.CONF.keystone_authtoken = fake_keystone_info_class()
+ for opt in arista.AristaRPCWrapper.required_options:
+ cfg.CONF.set_override(opt, value, "ml2_arista")
+
+
+def setup_valid_config():
+ # Config is not valid if value is not set
+ setup_arista_wrapper_config('value')
+
+
+class AristaProvisionedVlansStorageTestCase(base.BaseTestCase):
+ """Test storing and retriving functionality of Arista mechanism driver.
+
+ Tests all methods of this class by invoking them seperately as well
+ as a goup.
+ """
+
+ def setUp(self):
+ super(AristaProvisionedVlansStorageTestCase, self).setUp()
+ ndb.configure_db()
+
+ def test_tenant_is_remembered(self):
+ tenant_id = 'test'
+
+ db.remember_tenant(tenant_id)
+ net_provisioned = db.is_tenant_provisioned(tenant_id)
+ self.assertTrue(net_provisioned, 'Tenant must be provisioned')
+
+ def test_tenant_is_removed(self):
+ tenant_id = 'test'
+
+ db.remember_tenant(tenant_id)
+ db.forget_tenant(tenant_id)
+ net_provisioned = db.is_tenant_provisioned(tenant_id)
+ self.assertFalse(net_provisioned, 'The Tenant should be deleted')
+
+ def test_network_is_remembered(self):
+ tenant_id = 'test'
+ network_id = '123'
+ segmentation_id = 456
+
+ db.remember_network(tenant_id, network_id, segmentation_id)
+ net_provisioned = db.is_network_provisioned(tenant_id,
+ network_id)
+ self.assertTrue(net_provisioned, 'Network must be provisioned')
+
+ def test_network_is_removed(self):
+ tenant_id = 'test'
+ network_id = '123'
+
+ db.remember_network(tenant_id, network_id, '123')
+ db.forget_network(tenant_id, network_id)
+ net_provisioned = db.is_network_provisioned(tenant_id, network_id)
+ self.assertFalse(net_provisioned, 'The network should be deleted')
+
+ def test_vm_is_remembered(self):
+ vm_id = 'VM-1'
+ tenant_id = 'test'
+ network_id = '123'
+ port_id = 456
+ host_id = 'ubuntu1'
+
+ db.remember_vm(vm_id, host_id, port_id, network_id, tenant_id)
+ vm_provisioned = db.is_vm_provisioned(vm_id, host_id, port_id,
+ network_id, tenant_id)
+ self.assertTrue(vm_provisioned, 'VM must be provisioned')
+
+ def test_vm_is_removed(self):
+ vm_id = 'VM-1'
+ tenant_id = 'test'
+ network_id = '123'
+ port_id = 456
+ host_id = 'ubuntu1'
+
+ db.remember_vm(vm_id, host_id, port_id, network_id, tenant_id)
+ db.forget_vm(vm_id, host_id, port_id, network_id, tenant_id)
+ vm_provisioned = db.is_vm_provisioned(vm_id, host_id, port_id,
+ network_id, tenant_id)
+ self.assertFalse(vm_provisioned, 'The vm should be deleted')
+
+ def test_remembers_multiple_networks(self):
+ tenant_id = 'test'
+ expected_num_nets = 100
+ nets = ['id%s' % n for n in range(expected_num_nets)]
+ for net_id in nets:
+ db.remember_network(tenant_id, net_id, 123)
+
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ self.assertEqual(expected_num_nets, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected_num_nets, num_nets_provisioned))
+
+ def test_removes_all_networks(self):
+ tenant_id = 'test'
+ num_nets = 100
+ old_nets = db.num_nets_provisioned(tenant_id)
+ nets = ['id_%s' % n for n in range(num_nets)]
+ for net_id in nets:
+ db.remember_network(tenant_id, net_id, 123)
+ for net_id in nets:
+ db.forget_network(tenant_id, net_id)
+
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ expected = old_nets
+ self.assertEqual(expected, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected, num_nets_provisioned))
+
+ def test_remembers_multiple_tenants(self):
+ expected_num_tenants = 100
+ tenants = ['id%s' % n for n in range(expected_num_tenants)]
+ for tenant_id in tenants:
+ db.remember_tenant(tenant_id)
+
+ num_tenants_provisioned = db.num_provisioned_tenants()
+ self.assertEqual(expected_num_tenants, num_tenants_provisioned,
+ 'There should be %d tenants, not %d' %
+ (expected_num_tenants, num_tenants_provisioned))
+
+ def test_removes_multiple_tenants(self):
+ num_tenants = 100
+ tenants = ['id%s' % n for n in range(num_tenants)]
+ for tenant_id in tenants:
+ db.remember_tenant(tenant_id)
+ for tenant_id in tenants:
+ db.forget_tenant(tenant_id)
+
+ num_tenants_provisioned = db.num_provisioned_tenants()
+ expected = 0
+ self.assertEqual(expected, num_tenants_provisioned,
+ 'There should be %d tenants, not %d' %
+ (expected, num_tenants_provisioned))
+
+ def test_num_vm_is_valid(self):
+ tenant_id = 'test'
+ network_id = '123'
+ port_id = 456
+ host_id = 'ubuntu1'
+
+ vm_to_remember = ['vm1', 'vm2', 'vm3']
+ vm_to_forget = ['vm2', 'vm1']
+
+ for vm in vm_to_remember:
+ db.remember_vm(vm, host_id, port_id, network_id, tenant_id)
+ for vm in vm_to_forget:
+ db.forget_vm(vm, host_id, port_id, network_id, tenant_id)
+
+ num_vms = len(db.get_vms(tenant_id))
+ expected = len(vm_to_remember) - len(vm_to_forget)
+
+ self.assertEqual(expected, num_vms,
+ 'There should be %d records, '
+ 'got %d records' % (expected, num_vms))
+ # clean up afterwards
+ db.forget_vm('vm3', host_id, port_id, network_id, tenant_id)
+
+ def test_get_network_list_returns_eos_compatible_data(self):
+ tenant = u'test-1'
+ segm_type = 'vlan'
+ network_id = u'123'
+ network2_id = u'1234'
+ vlan_id = 123
+ vlan2_id = 1234
+ expected_eos_net_list = {network_id: {u'networkId': network_id,
+ u'segmentationTypeId': vlan_id,
+ u'segmentationType': segm_type},
+ network2_id: {u'networkId': network2_id,
+ u'segmentationTypeId': vlan2_id,
+ u'segmentationType': segm_type}}
+
+ db.remember_network(tenant, network_id, vlan_id)
+ db.remember_network(tenant, network2_id, vlan2_id)
+
+ net_list = db.get_networks(tenant)
+ self.assertNotEqual(net_list != expected_eos_net_list, ('%s != %s' %
+ (net_list, expected_eos_net_list)))
+
+
+class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase):
+ """Test cases to test the RPC between Arista Driver and EOS.
+
+ Tests all methods used to send commands between Arista Driver and EOS
+ """
+
+ def setUp(self):
+ super(PositiveRPCWrapperValidConfigTestCase, self).setUp()
+ setup_valid_config()
+ self.drv = arista.AristaRPCWrapper()
+ self.region = 'RegionOne'
+ self.drv._server = mock.MagicMock()
+
+ def test_no_exception_on_correct_configuration(self):
+ self.assertNotEqual(self.drv, None)
+
+ def test_plug_host_into_network(self):
+ tenant_id = 'ten-1'
+ vm_id = 'vm-1'
+ port_id = 123
+ network_id = 'net-id'
+ host = 'host'
+ port_name = '123-port'
+
+ self.drv.plug_host_into_network(vm_id, host, port_id,
+ network_id, tenant_id, port_name)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne',
+ 'tenant ten-1', 'vm id vm-1 hostid host',
+ 'port id 123 name 123-port network-id net-id',
+ 'exit', 'exit', 'exit', 'exit']
+
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_unplug_host_from_network(self):
+ tenant_id = 'ten-1'
+ vm_id = 'vm-1'
+ port_id = 123
+ network_id = 'net-id'
+ host = 'host'
+ self.drv.unplug_host_from_network(vm_id, host, port_id,
+ network_id, tenant_id)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne',
+ 'tenant ten-1', 'vm id vm-1 host host',
+ 'no port id 123 network-id net-id',
+ 'exit', 'exit', 'exit', 'exit']
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_create_network(self):
+ tenant_id = 'ten-1'
+ network_id = 'net-id'
+ network_name = 'net-name'
+ vlan_id = 123
+ self.drv.create_network(tenant_id, network_id, network_name, vlan_id)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne',
+ 'tenant ten-1', 'network id net-id name net-name',
+ 'segment 1 type vlan id 123',
+ 'exit', 'exit', 'exit', 'exit', 'exit']
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_delete_network(self):
+ tenant_id = 'ten-1'
+ network_id = 'net-id'
+ self.drv.delete_network(tenant_id, network_id)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne',
+ 'tenant ten-1', 'no network id net-id',
+ 'exit', 'exit', 'exit', 'exit']
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_delete_vm(self):
+ tenant_id = 'ten-1'
+ vm_id = 'vm-id'
+ self.drv.delete_vm(tenant_id, vm_id)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne',
+ 'tenant ten-1', 'no vm id vm-id',
+ 'exit', 'exit', 'exit', 'exit']
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_delete_tenant(self):
+ tenant_id = 'ten-1'
+ self.drv.delete_tenant(tenant_id)
+ cmds = ['enable', 'configure', 'management openstack',
+ 'region RegionOne', 'no tenant ten-1',
+ 'exit', 'exit', 'exit']
+ self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds)
+
+ def test_get_network_info_returns_none_when_no_such_net(self):
+ expected = []
+ self.drv.get_tenants = mock.MagicMock()
+ self.drv.get_tenants.return_value = []
+
+ net_info = self.drv.get_tenants()
+
+ self.drv.get_tenants.assert_called_once_with()
+ self.assertEqual(net_info, expected, ('Network info must be "None"'
+ 'for unknown network'))
+
+ def test_get_network_info_returns_info_for_available_net(self):
+ valid_network_id = '12345'
+ valid_net_info = {'network_id': valid_network_id,
+ 'some_info': 'net info'}
+ known_nets = valid_net_info
+
+ self.drv.get_tenants = mock.MagicMock()
+ self.drv.get_tenants.return_value = known_nets
+
+ net_info = self.drv.get_tenants()
+ self.assertEqual(net_info, valid_net_info,
+ ('Must return network info for a valid net'))
+
+
+class AristaRPCWrapperInvalidConfigTestCase(base.BaseTestCase):
+ """Negative test cases to test the Arista Driver configuration."""
+
+ def setUp(self):
+ super(AristaRPCWrapperInvalidConfigTestCase, self).setUp()
+ self.setup_invalid_config() # Invalid config, required options not set
+
+ def setup_invalid_config(self):
+ setup_arista_wrapper_config(None)
+
+ def test_raises_exception_on_wrong_configuration(self):
+ self.assertRaises(arista_exc.AristaConfigError,
+ arista.AristaRPCWrapper)
+
+
+class NegativeRPCWrapperTestCase(base.BaseTestCase):
+ """Negative test cases to test the RPC between Arista Driver and EOS."""
+
+ def setUp(self):
+ super(NegativeRPCWrapperTestCase, self).setUp()
+ setup_valid_config()
+
+ def test_exception_is_raised_on_json_server_error(self):
+ drv = arista.AristaRPCWrapper()
+
+ drv._server = mock.MagicMock()
+ drv._server.runCmds.side_effect = Exception('server error')
+ self.assertRaises(arista_exc.AristaRpcError, drv.get_tenants)
+
+
+class RealNetStorageAristaDriverTestCase(base.BaseTestCase):
+ """Main test cases for Arista Mechanism driver.
+
+ Tests all mechanism driver APIs supported by Arista Driver. It invokes
+ all the APIs as they would be invoked in real world scenarios and
+ verifies the functionality.
+ """
+ def setUp(self):
+ super(RealNetStorageAristaDriverTestCase, self).setUp()
+ self.fake_rpc = mock.MagicMock()
+ ndb.configure_db()
+ self.drv = arista.AristaDriver(self.fake_rpc)
+
+ def tearDown(self):
+ super(RealNetStorageAristaDriverTestCase, self).tearDown()
+ self.drv.stop_synchronization_thread()
+
+ def test_create_and_delete_network(self):
+ tenant_id = 'ten-1'
+ network_id = 'net1-id'
+ segmentation_id = 1001
+
+ network_context = self._get_network_context(tenant_id,
+ network_id,
+ segmentation_id)
+ self.drv.create_network_precommit(network_context)
+ net_provisioned = db.is_network_provisioned(tenant_id, network_id)
+ self.assertTrue(net_provisioned, 'The network should be created')
+
+ expected_num_nets = 1
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ self.assertEqual(expected_num_nets, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected_num_nets, num_nets_provisioned))
+
+ #Now test the delete network
+ self.drv.delete_network_precommit(network_context)
+ net_provisioned = db.is_network_provisioned(tenant_id, network_id)
+ self.assertFalse(net_provisioned, 'The network should be created')
+
+ expected_num_nets = 0
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ self.assertEqual(expected_num_nets, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected_num_nets, num_nets_provisioned))
+
+ def test_create_and_delete_multiple_networks(self):
+ tenant_id = 'ten-1'
+ expected_num_nets = 100
+ segmentation_id = 1001
+ nets = ['id%s' % n for n in range(expected_num_nets)]
+ for net_id in nets:
+ network_context = self._get_network_context(tenant_id,
+ net_id,
+ segmentation_id)
+ self.drv.create_network_precommit(network_context)
+
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ self.assertEqual(expected_num_nets, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected_num_nets, num_nets_provisioned))
+
+ #now test the delete networks
+ for net_id in nets:
+ network_context = self._get_network_context(tenant_id,
+ net_id,
+ segmentation_id)
+ self.drv.delete_network_precommit(network_context)
+
+ num_nets_provisioned = db.num_nets_provisioned(tenant_id)
+ expected_num_nets = 0
+ self.assertEqual(expected_num_nets, num_nets_provisioned,
+ 'There should be %d nets, not %d' %
+ (expected_num_nets, num_nets_provisioned))
+
+ def test_create_and_delete_ports(self):
+ tenant_id = 'ten-1'
+ network_id = 'net1-id'
+ segmentation_id = 1001
+ vms = ['vm1', 'vm2', 'vm3']
+
+ network_context = self._get_network_context(tenant_id,
+ network_id,
+ segmentation_id)
+ self.drv.create_network_precommit(network_context)
+
+ for vm_id in vms:
+ port_context = self._get_port_context(tenant_id,
+ network_id,
+ vm_id,
+ network_context)
+ self.drv.create_port_precommit(port_context)
+
+ vm_list = db.get_vms(tenant_id)
+ provisioned_vms = len(vm_list)
+ expected_vms = len(vms)
+ self.assertEqual(expected_vms, provisioned_vms,
+ 'There should be %d '
+ 'hosts, not %d' % (expected_vms, provisioned_vms))
+
+ # Now test the delete ports
+ for vm_id in vms:
+ port_context = self._get_port_context(tenant_id,
+ network_id,
+ vm_id,
+ network_context)
+ self.drv.delete_port_precommit(port_context)
+
+ vm_list = db.get_vms(tenant_id)
+ provisioned_vms = len(vm_list)
+ expected_vms = 0
+ self.assertEqual(expected_vms, provisioned_vms,
+ 'There should be %d '
+ 'VMs, not %d' % (expected_vms, provisioned_vms))
+
+ def _get_network_context(self, tenant_id, net_id, seg_id):
+ network = {'id': net_id,
+ 'tenant_id': tenant_id}
+ network_segments = [{'segmentation_id': seg_id}]
+ return FakeNetworkContext(network, network_segments, network)
+
+ def _get_port_context(self, tenant_id, net_id, vm_id, network):
+ port = {'device_id': vm_id,
+ 'device_owner': 'compute',
+ 'binding:host_id': 'ubuntu1',
+ 'tenant_id': tenant_id,
+ 'id': 101,
+ 'network_id': net_id
+ }
+ return FakePortContext(port, port, network)
+
+
+class fake_keystone_info_class(object):
+ """To generate fake Keystone Authentification token information
+
+ Arista Driver expects Keystone auth info. This fake information
+ is for testing only
+ """
+ auth_protocol = 'abc'
+ auth_host = 'host'
+ auth_port = 5000
+ admin_user = 'neutron'
+ admin_password = 'fun'
+
+
+class FakeNetworkContext(object):
+ """To generate network context for testing purposes only."""
+
+ def __init__(self, network, segments=None, original_network=None):
+ self._network = network
+ self._original_network = original_network
+ self._segments = segments
+
+ @property
+ def current(self):
+ return self._network
+
+ @property
+ def original(self):
+ return self._original_network
+
+ @property
+ def network_segments(self):
+ return self._segments
+
+
+class FakePortContext(object):
+ """To generate port context for testing purposes only."""
+
+ def __init__(self, port, original_port, network):
+ self._port = port
+ self._original_port = original_port
+ self._network_context = network
+
+ @property
+ def current(self):
+ return self._port
+
+ @property
+ def original(self):
+ return self._original_port
+
+ @property
+ def network(self):
+ return self._network_context
httplib2
requests>=1.1
iso8601>=0.1.4
+jsonrpclib
kombu>=2.4.8
netaddr
python-neutronclient>=2.2.3,<3
etc/neutron/plugins/metaplugin = etc/neutron/plugins/metaplugin/metaplugin.ini
etc/neutron/plugins/midonet = etc/neutron/plugins/midonet/midonet.ini
etc/neutron/plugins/ml2 = etc/neutron/plugins/ml2/ml2_conf.ini
+ etc/neutron/plugins/ml2/ml2_conf_arista.ini
etc/neutron/plugins/mlnx = etc/neutron/plugins/mlnx/mlnx_conf.ini
etc/neutron/plugins/nec = etc/neutron/plugins/nec/nec.ini
etc/neutron/plugins/nicira = etc/neutron/plugins/nicira/nvp.ini
logger = neutron.tests.unit.ml2.drivers.mechanism_logger:LoggerMechanismDriver
test = neutron.tests.unit.ml2.drivers.mechanism_test:TestMechanismDriver
ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver
+ arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver
[build_sphinx]
all_files = 1