From: sukhdev Date: Mon, 30 Sep 2013 23:05:01 +0000 (-0700) Subject: Arista ML2 mechanism driver clean up and integration with port binding X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=91fa02e4682470c13226583a51ae639279d048b9;p=openstack-build%2Fneutron-build.git Arista ML2 mechanism driver clean up and integration with port binding fixes bug: 1215097 This patch addresses minor cleanups and integration with port binding and dhcp support into Arista ML2 mechanism driver. Change-Id: Icebc4aa1c57278171cc6b5209107b8eab833249c --- diff --git a/neutron/db/migration/alembic_migrations/versions/14f24494ca31_arista_ml2.py b/neutron/db/migration/alembic_migrations/versions/14f24494ca31_arista_ml2.py index a02ea1909..4ce63f9c0 100644 --- a/neutron/db/migration/alembic_migrations/versions/14f24494ca31_arista_ml2.py +++ b/neutron/db/migration/alembic_migrations/versions/14f24494ca31_arista_ml2.py @@ -56,7 +56,7 @@ def upgrade(active_plugins=None, options=None): '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('vm_id', sa.String(length=255), 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), diff --git a/neutron/plugins/ml2/drivers/mech_arista/db.py b/neutron/plugins/ml2/drivers/mech_arista/db.py index b13e1aa83..885d76832 100644 --- a/neutron/plugins/ml2/drivers/mech_arista/db.py +++ b/neutron/plugins/ml2/drivers/mech_arista/db.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ class AristaProvisionedVms(model_base.BASEV2, models_v2.HasId, """ __tablename__ = 'arista_provisioned_vms' - vm_id = sa.Column(sa.String(UUID_LEN)) + vm_id = sa.Column(sa.String(STR_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)) diff --git a/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py b/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py index f8f2cbbab..292629a9a 100644 --- a/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py +++ b/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,10 +37,6 @@ class AristaRPCWrapper(object): 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 @@ -54,7 +50,7 @@ class AristaRPCWrapper(object): return keystone_auth_url def get_tenants(self): - """Returns dict of all tanants known by EOS. + """Returns dict of all tenants known by EOS. :returns: dictionary containing the networks per tenant and VMs allocated per tenant @@ -65,6 +61,33 @@ class AristaRPCWrapper(object): return tenants + def plug_port_into_network(self, vm_id, host_id, port_id, + net_id, tenant_id, port_name, device_owner): + """Genric routine plug a port of a VM instace into network. + + :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 + :param device_owner: Device owner - e.g. compute or network:dhcp + """ + if device_owner == 'network:dhcp': + self.plug_dhcp_port_into_network(vm_id, + host_id, + port_id, + net_id, + tenant_id, + port_name) + elif device_owner.startswith('compute'): + self.plug_host_into_network(vm_id, + host_id, + port_id, + net_id, + tenant_id, + port_name) + def plug_host_into_network(self, vm_id, host, port_id, network_id, tenant_id, port_name): """Creates VLAN between TOR and compute host. @@ -79,7 +102,7 @@ class AristaRPCWrapper(object): 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' % + 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' % @@ -88,6 +111,28 @@ class AristaRPCWrapper(object): cmds.append('exit') self._run_openstack_cmds(cmds) + def plug_dhcp_port_into_network(self, dhcp_id, host, port_id, + network_id, tenant_id, port_name): + """Creates VLAN between TOR and dhcp host. + + :param dhcp_id: globally unique identifier for dhcp + :param host: ID of the host where the dhcp is hosted + :param port_id: globally unique port ID that connects dhcp 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, + 'network id %s' % network_id] + if port_name: + cmds.append('dhcp id %s hostid %s port-id %s name "%s"' % + (dhcp_id, host, port_id, port_name)) + else: + cmds.append('dhcp id %s hostid %s port-id %s' % + (dhcp_id, host, port_id)) + 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. @@ -99,12 +144,28 @@ class AristaRPCWrapper(object): :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), + 'vm id %s hostid %s' % (vm_id, host), + 'no port id %s' % port_id, 'exit', 'exit'] self._run_openstack_cmds(cmds) + def unplug_dhcp_port_from_network(self, dhcp_id, host, port_id, + network_id, tenant_id): + """Removes previously configured VLAN between TOR and a dhcp host. + + :param dhcp_id: globally unique identifier for dhcp + :param host: ID of the host where the dhcp is hosted + :param port_id: globally unique port ID that connects dhcp to network + :param network_id: globally unique neutron network identifier + :param tenant_id: globally unique neutron tenant identifier + """ + cmds = ['tenant %s' % tenant_id, + 'network id %s' % network_id, + 'no dhcp id %s port-id %s' % (dhcp_id, port_id), + 'exit'] + self._run_openstack_cmds(cmds) + def create_network(self, tenant_id, network_id, network_name, seg_id): """Creates a network on Arista Hardware @@ -115,7 +176,8 @@ class AristaRPCWrapper(object): """ cmds = ['tenant %s' % tenant_id] if network_name: - cmds.append('network id %s name %s' % (network_id, 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) @@ -140,7 +202,7 @@ class AristaRPCWrapper(object): """ if segments: cmds = ['tenant %s' % tenant_id, - 'network id %s name %s' % (network_id, network_name)] + '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, @@ -255,11 +317,14 @@ class AristaRPCWrapper(object): 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) + if cfg.CONF.ml2_arista.get('eapi_host') == '': + msg = _('Required option eapi_host is not set') + LOG.error(msg) + raise arista_exc.AristaConfigError(msg=msg) + if cfg.CONF.ml2_arista.get('eapi_username') == '': + msg = _('Required option eapi_username is not set') + LOG.error(msg) + raise arista_exc.AristaConfigError(msg=msg) class SyncService(object): @@ -278,6 +343,8 @@ class SyncService(object): LOG.info(_('Syncing Neutron <-> EOS')) try: + #Always register with EOS to ensure that it has correct credentials + self._rpc._register_with_eos() eos_tenants = self._rpc.get_tenants() except arista_exc.AristaRpcError: msg = _('EOS is not available, will try sync later') @@ -298,21 +365,20 @@ class SyncService(object): 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 eos_tenants.keys(): + if tenant not in db_tenants: + #send delete tenant to EOS + try: + self._rpc.delete_tenant(tenant) + del eos_tenants[tenant] + except arista_exc.AristaRpcError: + msg = _('EOS is not available, ' + 'failed to delete tenant %s') % tenant + LOG.warning(msg) + for tenant in db_tenants: db_nets = db.get_networks(tenant) db_vms = db.get_vms(tenant) @@ -325,7 +391,7 @@ class SyncService(object): # check the vM list if eos_vms == db_vms: # Nothing to do. Everything is in sync for this tenant - break + continue # Neutron DB and EOS reruires synchronization. # First delete anything which should not be EOS @@ -338,7 +404,6 @@ class SyncService(object): 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: @@ -349,7 +414,6 @@ class SyncService(object): 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: @@ -364,7 +428,6 @@ class SyncService(object): 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: @@ -373,29 +436,33 @@ class SyncService(object): ports = self._ndb.get_all_ports_for_vm(tenant, vm_id) for port in ports: port_id = port['id'] - network_id = port['network_id'] + net_id = port['network_id'] port_name = port['name'] + device_owner = port['device_owner'] + vm_id = vm['vmId'] + host_id = vm['host'] try: - self._rpc.plug_host_into_network(vm['vmId'], - vm['host'], + self._rpc.plug_port_into_network(vm_id, + host_id, port_id, - network_id, + net_id, tenant, - port_name) + port_name, + device_owner) except arista_exc.AristaRpcError: - msg = _('EOS is not available, failed to create' + 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: + if eos_tenants and tenant in eos_tenants: networks = eos_tenants[tenant]['tenantNetworks'] return networks def _get_eos_vms(self, eos_tenants, tenant): vms = {} - if eos_tenants: + if eos_tenants and tenant in eos_tenants: vms = eos_tenants[tenant]['tenantVmInstances'] return vms @@ -410,6 +477,9 @@ class AristaDriver(driver_api.MechanismDriver): def __init__(self, rpc=None): self.rpc = rpc or AristaRPCWrapper() + self.db_nets = db.AristaProvisionedNets() + self.db_vms = db.AristaProvisionedVms() + self.db_tenants = db.AristaProvisionedTenants() self.ndb = db.NeutronNets() confg = cfg.CONF.ml2_arista @@ -419,11 +489,10 @@ class AristaDriver(driver_api.MechanismDriver): 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() + self._synchronization_thread() def create_network_precommit(self, context): """Remember the tenant, and network information.""" @@ -466,7 +535,7 @@ class AristaDriver(driver_api.MechanismDriver): 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. + Any other change in network is not supported at this time. We do not store the network names, therefore, no DB store action is performed here. """ @@ -540,8 +609,6 @@ class AristaDriver(driver_api.MechanismDriver): 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 @@ -563,8 +630,6 @@ class AristaDriver(driver_api.MechanismDriver): 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 @@ -576,45 +641,97 @@ class AristaDriver(driver_api.MechanismDriver): 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) + network_id) if vm_provisioned and net_provisioned: try: - self.rpc.plug_host_into_network(device_id, + self.rpc.plug_port_into_network(device_id, hostname, port_id, network_id, tenant_id, - port_name) + port_name, + device_owner) 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' + 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 + """At the moment we only support port name change. + + Any other change to port is not supported at this time. + We do not store the port names, therefore, no DB store + action is performed here. + """ + new_port = context.current + orig_port = context.original + if new_port['name'] != orig_port['name']: + msg = _('Port name changed to %s') % new_port['name'] + LOG.info(msg) def update_port_postcommit(self, context): - # TODO(sukhdev) revisit once the port binding support is implemented - return + """At the moment we only support port name change + + Any other change to port is not supported at this time. + """ + port = context.current + orig_port = context.original + if port['name'] == orig_port['name']: + # nothing to do + return + + device_id = port['device_id'] + device_owner = port['device_owner'] + host = port['binding:host_id'] + 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_port_into_network(device_id, + hostname, + port_id, + network_id, + tenant_id, + port_name, + device_owner) + except arista_exc.AristaRpcError: + LOG.info(EOS_UNREACHABLE_MSG) + raise ml2_exc.MechanismDriverError() + else: + msg = _('VM %s is not updated as it is not found in ' + 'Arista DB') % device_id + LOG.info(msg) 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'] @@ -636,22 +753,27 @@ class AristaDriver(driver_api.MechanismDriver): """ 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'] + device_owner = port['device_owner'] 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) + if device_owner == 'network:dhcp': + self.rpc.unplug_dhcp_port_from_network(device_id, + hostname, + port_id, + network_id, + tenant_id) + else: + 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() diff --git a/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py b/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py index 5397f77b8..e1f551462 100644 --- a/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py +++ b/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py @@ -1,5 +1,5 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2013 OpenStack, LLC. +# Copyright (c) 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ from neutron.plugins.ml2.drivers.mech_arista import mechanism_arista as arista from neutron.tests import base -def setup_arista_wrapper_config(value=None): +def setup_arista_wrapper_config(value=''): cfg.CONF.keystone_authtoken = fake_keystone_info_class() - for opt in arista.AristaRPCWrapper.required_options: - cfg.CONF.set_override(opt, value, "ml2_arista") + cfg.CONF.set_override('eapi_host', value, "ml2_arista") + cfg.CONF.set_override('eapi_username', value, "ml2_arista") def setup_valid_config(): @@ -233,11 +233,29 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 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', + '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_plug_dhcp_port_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_dhcp_port_into_network(vm_id, host, port_id, + network_id, tenant_id, port_name) + cmds = ['enable', 'configure', 'management openstack', + 'region RegionOne', + 'tenant ten-1', 'network id net-id', + 'dhcp id vm-1 hostid host port-id 123 name "123-port"', + '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' @@ -248,11 +266,28 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 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', + 'tenant ten-1', 'vm id vm-1 hostid host', + 'no port id 123', 'exit', 'exit', 'exit', 'exit'] self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + def test_unplug_dhcp_port_from_network(self): + tenant_id = 'ten-1' + vm_id = 'vm-1' + port_id = 123 + network_id = 'net-id' + host = 'host' + + self.drv.unplug_dhcp_port_from_network(vm_id, host, port_id, + network_id, tenant_id) + cmds = ['enable', 'configure', 'management openstack', + 'region RegionOne', + 'tenant ten-1', 'network id net-id', + 'no dhcp id vm-1 port-id 123', + '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' @@ -261,7 +296,7 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 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', + '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) @@ -327,7 +362,7 @@ class AristaRPCWrapperInvalidConfigTestCase(base.BaseTestCase): self.setup_invalid_config() # Invalid config, required options not set def setup_invalid_config(self): - setup_arista_wrapper_config(None) + setup_arista_wrapper_config('') def test_raises_exception_on_wrong_configuration(self): self.assertRaises(arista_exc.AristaConfigError,