From 854bc0973b8966a1bb3180e4a2ea5e7cbd8289f1 Mon Sep 17 00:00:00 2001 From: Shashank Hegde Date: Thu, 13 Feb 2014 18:20:45 -0800 Subject: [PATCH] Improves Arista's ML2 driver's sync performance In large scale deployments a full sync between Neutron and EOS can take minutes. In order to cut that time, this patch batches multimle EOS CLI commands and sends them to EOS instead of sending each command separately. For example, if a tenant has 10 networks, instead of making 10 RPC calls to EOS to create those 10 networks, this patch builds a commands to create those 10 networks and makes just one RPC call to EOS which cuts down sync times significantly. All the _bulk() methods are added to batch such requests. Another optimization is to timestamp when the Region data was modified (This includes any tenant creation, their networks, VMs and ports). The sync gets the timestamp from EOS and only if the timestamps do not match, the driver performs a full sync. Closes-Bug: 1279619 Change-Id: I7d17604a7088d7dbb6e3dbb0afdb8e6759c1f67d --- neutron/plugins/ml2/drivers/mech_arista/db.py | 50 +- .../drivers/mech_arista/mechanism_arista.py | 472 ++++++++++++------ .../drivers/test_arista_mechanism_driver.py | 158 +++++- 3 files changed, 497 insertions(+), 183 deletions(-) diff --git a/neutron/plugins/ml2/drivers/mech_arista/db.py b/neutron/plugins/ml2/drivers/mech_arista/db.py index 3006b9e54de..f47bcd14093 100644 --- a/neutron/plugins/ml2/drivers/mech_arista/db.py +++ b/neutron/plugins/ml2/drivers/mech_arista/db.py @@ -92,13 +92,8 @@ def remember_tenant(tenant_id): """ 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) + tenant = AristaProvisionedTenants(tenant_id=tenant_id) + session.add(tenant) def forget_tenant(tenant_id): @@ -138,19 +133,13 @@ def remember_vm(vm_id, host_id, port_id, network_id, tenant_id): """ 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) + 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): @@ -179,16 +168,11 @@ def remember_network(tenant_id, network_id, segmentation_id): """ 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) + net = AristaProvisionedNets( + tenant_id=tenant_id, + network_id=network_id, + segmentation_id=segmentation_id) + session.add(net) def forget_network(tenant_id, network_id): @@ -411,12 +395,6 @@ class NeutronNets(db_base_plugin_v2.NeutronDbPluginV2): 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]} diff --git a/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py b/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py index 95414f6a77c..f8628bf1758 100644 --- a/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py +++ b/neutron/plugins/ml2/drivers/mech_arista/mechanism_arista.py @@ -43,6 +43,36 @@ class AristaRPCWrapper(object): self._server = jsonrpclib.Server(self._eapi_host_url()) self.keystone_conf = cfg.CONF.keystone_authtoken self.region = cfg.CONF.ml2_arista.region_name + self._region_updated_time = None + # The cli_commands dict stores the mapping between the CLI command key + # and the actual CLI command. + self.cli_commands = {} + self.initialize_cli_commands() + + def _get_exit_mode_cmds(self, modes): + """Returns a list of 'exit' commands for the modes. + + :param modes: a list of CLI modes to exit out of. + """ + return ['exit'] * len(modes) + + def initialize_cli_commands(self): + self.cli_commands['timestamp'] = [] + + def check_cli_commands(self): + """Checks whether the CLI commands are vaild. + + This method tries to execute the commands on EOS and if it succeedes + the command is stored. + """ + cmd = ['show openstack config region %s timestamp' % self.region] + try: + self._run_eos_cmds(cmd) + self.cli_commands['timestamp'] = cmd + except arista_exc.AristaRpcError: + self.cli_commands['timestamp'] = [] + msg = _("'timestamp' command '%s' is not available on EOS") % cmd + LOG.warn(msg) def _keystone_url(self): keystone_auth_url = ('%s://%s:%s/v2.0/' % @@ -58,7 +88,7 @@ class AristaRPCWrapper(object): and VMs allocated per tenant """ cmds = ['show openstack config region %s' % self.region] - command_output = self._run_openstack_cmds(cmds) + command_output = self._run_eos_cmds(cmds) tenants = command_output[0]['tenants'] return tenants @@ -168,25 +198,35 @@ class AristaRPCWrapper(object): 'exit'] self._run_openstack_cmds(cmds) - def create_network(self, tenant_id, network_id, network_name, seg_id): + def create_network(self, tenant_id, network): + """Creates a single network on Arista hardware + + :param tenant_id: globally unique neutron tenant identifier + :param network: dict containing network_id, network_name and + segmentation_id + """ + self.create_network_bulk(tenant_id, [network]) + + def create_network_bulk(self, tenant_id, network_list): """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 + :param network_list: list of dicts containing network_id, network_name + and segmentation_id """ 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') - + # Create a reference to function to avoid name lookups in the loop + append_cmd = cmds.append + for network in network_list: + try: + append_cmd('network id %s name "%s"' % + (network['network_id'], network['network_name'])) + except KeyError: + append_cmd('network id %s' % network['network_id']) + # Enter segment mode without exiting out of network mode + append_cmd('segment 1 type vlan id %d' % + network['segmentation_id']) + cmds.extend(self._get_exit_mode_cmds(['segment', 'network', 'tenant'])) self._run_openstack_cmds(cmds) def create_network_segments(self, tenant_id, network_id, @@ -222,10 +262,19 @@ class AristaRPCWrapper(object): :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.delete_network_bulk(tenant_id, [network_id]) + + def delete_network_bulk(self, tenant_id, network_id_list): + """Deletes the network ids specified for a tenant + + :param tenant_id: globally unique neutron tenant identifier + :param network_id_list: list of globally unique neutron network + identifiers + """ + cmds = ['tenant %s' % tenant_id] + for network_id in network_id_list: + cmds.append('no network id %s' % network_id) + cmds.extend(self._get_exit_mode_cmds(['network', 'tenant'])) self._run_openstack_cmds(cmds) def delete_vm(self, tenant_id, vm_id): @@ -234,10 +283,58 @@ class AristaRPCWrapper(object): :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.delete_vm_bulk(tenant_id, [vm_id]) + + def delete_vm_bulk(self, tenant_id, vm_id_list): + """Deletes VMs from EOS for a given tenant + + :param tenant_id : globally unique neutron tenant identifier + :param vm_id_list : ids of VMs that needs to be deleted. + """ + cmds = ['tenant %s' % tenant_id] + for vm_id in vm_id_list: + cmds.append('no vm id %s' % vm_id) + cmds.extend(self._get_exit_mode_cmds(['vm', 'tenant'])) + self._run_openstack_cmds(cmds) + + def create_vm_port_bulk(self, tenant_id, vm_port_list, vms): + """Sends a bulk request to create ports. + + :param tenant_id: globaly unique neutron tenant identifier + :param vm_port_list: list of ports that need to be created. + :param vms: list of vms to which the ports will be attached to. + """ + cmds = ['tenant %s' % tenant_id] + # Create a reference to function to avoid name lookups in the loop + append_cmd = cmds.append + for port in vm_port_list: + try: + vm = vms[port['device_id']] + except KeyError: + msg = _("VM id %(vmid)s not found for port %(portid)s") % { + 'vmid': port['device_id'], + 'portid': port['id']} + LOG.warn(msg) + continue + + port_name = '' if 'name' not in port else 'name "%s"' % ( + port['name'] + ) + + if port['device_owner'] == n_const.DEVICE_OWNER_DHCP: + append_cmd('network id %s' % port['network_id']) + append_cmd('dhcp id %s hostid %s port-id %s %s' % + (vm['vmId'], vm['host'], port['id'], port_name)) + elif port['device_owner'].startswith('compute'): + append_cmd('vm id %s hostid %s' % (vm['vmId'], vm['host'])) + append_cmd('port id %s %s network-id %s' % + (port['id'], port_name, port['network_id'])) + else: + msg = _("Unknown device owner: %s") % port['device_owner'] + LOG.warn(msg) + continue + + append_cmd('exit') self._run_openstack_cmds(cmds) def delete_tenant(self, tenant_id): @@ -245,68 +342,149 @@ class AristaRPCWrapper(object): :param tenant_id: globally unique neutron tenant identifier """ - cmds = ['no tenant %s' % tenant_id, 'exit'] + self.delete_tenant_bulk([tenant_id]) + + def delete_tenant_bulk(self, tenant_list): + """Sends a bulk request to delete the tenants. + + :param tenant_list: list of globaly unique neutron tenant ids which + need to be deleted. + """ + + cmds = [] + for tenant in tenant_list: + cmds.append('no tenant %s' % tenant) + cmds.append('exit') self._run_openstack_cmds(cmds) def delete_this_region(self): - """Deletes this entire region from EOS. + """Deleted the region data from EOS.""" + cmds = ['enable', + 'configure', + 'management openstack', + 'no region %s' % self.region, + 'exit', + 'exit'] + self._run_eos_cmds(cmds) - 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): + 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' % + 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) + log_cmds = ['auth url %s user %s password ******' % + (self._keystone_url(), + self.keystone_conf.admin_user)] - def _run_openstack_cmds(self, commands, deleteRegion=None): + self._run_openstack_cmds(cmds, commands_to_log=log_cmds) + + def clear_region_updated_time(self): + """Clear the region updated time which forces a resync.""" + + self._region_updated_time = None + + def region_in_sync(self): + """Check whether EOS is in sync with Neutron.""" + + eos_region_updated_times = self.get_region_updated_time() + return (self._region_updated_time and + (self._region_updated_time['regionTimestamp'] == + eos_region_updated_times['regionTimestamp'])) + + def get_region_updated_time(self): + """Return the timestamp of the last update. + + This method returns the time at which any entities in the region + were updated. + """ + timestamp_cmd = self.cli_commands['timestamp'] + if timestamp_cmd: + return self._run_eos_cmds(commands=timestamp_cmd)[0] + return None + + def _run_eos_cmds(self, commands, commands_to_log=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 + :param commands_to_log : This should be set to the command that is + logged. If it is None, then the commands + param is logged. """ - 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) + log_cmd = commands + if commands_to_log: + log_cmd = commands_to_log + + LOG.info(_('Executing command on Arista EOS: %s'), log_cmd) 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)] + ret = self._server.runCmds(version=1, cmds=commands) except Exception as error: host = cfg.CONF.ml2_arista.eapi_host msg = (_('Error %(err)s while trying to execute ' 'commands %(cmd)s on EOS %(host)s') % - {'err': error, 'cmd': full_command, 'host': host}) + {'err': error, 'cmd': commands_to_log, 'host': host}) LOG.exception(msg) raise arista_exc.AristaRpcError(msg=msg) return ret + def _build_command(self, cmds): + """Build full EOS's openstack CLI command. + + Helper method to add commands to enter and exit from openstack + CLI modes. + + :param cmds: The openstack CLI commands that need to be executed + in the openstack config mode. + """ + + full_command = [ + 'enable', + 'configure', + 'management openstack', + 'region %s' % self.region, + ] + full_command.extend(cmds) + full_command.extend(self._get_exit_mode_cmds(['region', + 'openstack'])) + full_command.extend(self.cli_commands['timestamp']) + return full_command + + def _run_openstack_cmds(self, commands, commands_to_log=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 commands_to_logs : This should be set to the command that is + logged. If it is None, then the commands + param is logged. + """ + + full_command = self._build_command(commands) + if commands_to_log: + full_log_command = self._build_command(commands_to_log) + else: + full_log_command = None + ret = self._run_eos_cmds(full_command, full_log_command) + # Remove return values for 'configure terminal', + # 'management openstack' and 'exit' commands + if self.cli_commands['timestamp']: + self._region_updated_time = ret[-1] + def _eapi_host_url(self): self._validate_config() @@ -339,18 +517,31 @@ class SyncService(object): def __init__(self, rpc_wrapper, neutron_db): self._rpc = rpc_wrapper self._ndb = neutron_db + self._force_sync = True def synchronize(self): """Sends data to EOS which differs from neutron DB.""" LOG.info(_('Syncing Neutron <-> EOS')) + try: + # Get the time at which entities in the region were updated. + # If the times match, then ML2 is in sync with EOS. Otherwise + # perform a complete sync. + if not self._force_sync and self._rpc.region_in_sync(): + LOG.info(_('OpenStack and EOS are in sync!')) + return + except arista_exc.AristaRpcError: + LOG.warning(EOS_UNREACHABLE_MSG) + self._force_sync = True + return + try: #Always register with EOS to ensure that it has correct credentials - self._rpc._register_with_eos() + self._rpc.register_with_eos() eos_tenants = self._rpc.get_tenants() except arista_exc.AristaRpcError: - msg = _('EOS is not available, will try sync later') - LOG.warning(msg) + LOG.warning(EOS_UNREACHABLE_MSG) + self._force_sync = True return db_tenants = db.get_tenants() @@ -362,24 +553,32 @@ class SyncService(object): msg = _('No Tenants configured in Neutron DB. But %d ' 'tenants disovered in EOS during synchronization.' 'Enitre EOS region is cleared') % len(eos_tenants) + LOG.info(msg) + # Re-register with EOS so that the timestamp is updated. + self._rpc.register_with_eos() + # Region has been completely cleaned. So there is nothing to + # syncronize + self._force_sync = False except arista_exc.AristaRpcError: - msg = _('EOS is not available, failed to delete this region') - LOG.warning(msg) + LOG.warning(EOS_UNREACHABLE_MSG) + self._force_sync = True 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) + # Delete tenants that are in EOS, but not in the database + tenants_to_delete = frozenset(eos_tenants.keys()).difference( + db_tenants.keys()) + + if tenants_to_delete: + try: + self._rpc.delete_tenant_bulk(tenants_to_delete) + except arista_exc.AristaRpcError: + LOG.warning(EOS_UNREACHABLE_MSG) + self._force_sync = True + return + + # None of the commands have failed till now. But if subsequent + # operations fail, then force_sync is set to true + self._force_sync = False for tenant in db_tenants: db_nets = db.get_networks(tenant) @@ -387,74 +586,55 @@ class SyncService(object): 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 - continue + db_nets_key_set = frozenset(db_nets.keys()) + db_vms_key_set = frozenset(db_vms.keys()) + eos_nets_key_set = frozenset(eos_nets.keys()) + eos_vms_key_set = frozenset(eos_vms.keys()) - # 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) + # Find the networks that are present on EOS, but not in Neutron DB + nets_to_delete = eos_nets_key_set.difference(db_nets_key_set) - # 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) + # Find the VMs that are present on EOS, but not in Neutron DB + vms_to_delete = eos_vms_key_set.difference(db_vms_key_set) - # 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) + # Find the Networks that are present in Neutron DB, but not on EOS + nets_to_update = db_nets_key_set.difference(eos_nets_key_set) - # 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'] - 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_port_into_network(vm_id, - host_id, - port_id, - net_id, - tenant, - port_name, - device_owner) - except arista_exc.AristaRpcError: - msg = _('EOS is not available, failed to create ' - 'vm id %s') % vm['vmId'] - LOG.warning(msg) + # Find the VMs that are present in Neutron DB, but not on EOS + vms_to_update = db_vms_key_set.difference(eos_vms_key_set) + + try: + if vms_to_delete: + self._rpc.delete_vm_bulk(tenant, vms_to_delete) + if nets_to_delete: + self._rpc.delete_network_bulk(tenant, nets_to_delete) + if nets_to_update: + # Create a dict of networks keyed by id. + neutron_nets = dict( + (network['id'], network) for network in + self._ndb.get_all_networks_for_tenant(tenant) + ) + + networks = [ + {'network_id': net_id, + 'segmentation_id': + db_nets[net_id]['segmentationTypeId'], + 'network_name': + neutron_nets.get(net_id, {'name': ''})['name'], } + for net_id in nets_to_update + ] + self._rpc.create_network_bulk(tenant, networks) + if vms_to_update: + # Filter the ports to only the vms that we are interested + # in. + vm_ports = [ + port for port in self._ndb.get_all_ports_for_tenant( + tenant) if port['device_id'] in vms_to_update + ] + self._rpc.create_vm_port_bulk(tenant, vm_ports, db_vms) + except arista_exc.AristaRpcError: + LOG.warning(EOS_UNREACHABLE_MSG) + self._force_sync = True def _get_eos_networks(self, eos_tenants, tenant): networks = {} @@ -492,8 +672,12 @@ class AristaDriver(driver_api.MechanismDriver): self.eos_sync_lock = threading.Lock() def initialize(self): - self.rpc._register_with_eos() - self._cleanupDb() + self.rpc.register_with_eos() + self._cleanup_db() + self.rpc.check_cli_commands() + # Registering with EOS updates self.rpc.region_updated_time. Clear it + # to force an initial sync + self.rpc.clear_region_updated_time() self._synchronization_thread() def create_network_precommit(self, context): @@ -522,10 +706,11 @@ class AristaDriver(driver_api.MechanismDriver): 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) + network_dict = { + 'network_id': network_id, + 'segmentation_id': vlan_id, + 'network_name': network_name} + self.rpc.create_network(tenant_id, network_dict) except arista_exc.AristaRpcError: LOG.info(EOS_UNREACHABLE_MSG) raise ml2_exc.MechanismDriverError() @@ -563,10 +748,11 @@ class AristaDriver(driver_api.MechanismDriver): 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) + network_dict = { + 'network_id': network_id, + 'segmentation_id': vlan_id, + 'network_name': network_name} + self.rpc.create_network(tenant_id, network_dict) except arista_exc.AristaRpcError: LOG.info(EOS_UNREACHABLE_MSG) raise ml2_exc.MechanismDriverError() @@ -810,7 +996,7 @@ class AristaDriver(driver_api.MechanismDriver): self.timer.cancel() self.timer = None - def _cleanupDb(self): + def _cleanup_db(self): """Clean up any uncessary entries in our DB.""" db_tenants = db.get_tenants() for tenant in db_tenants: 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 c0348291afb..ecd5221e69f 100644 --- a/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py +++ b/neutron/tests/unit/ml2/drivers/test_arista_mechanism_driver.py @@ -17,6 +17,7 @@ import mock from oslo.config import cfg +from neutron.common import constants as n_const 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 @@ -217,6 +218,9 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): self.region = 'RegionOne' self.drv._server = mock.MagicMock() + def _get_exit_mode_cmds(self, modes): + return ['exit'] * len(modes) + def test_no_exception_on_correct_configuration(self): self.assertIsNotNone(self.drv) @@ -290,10 +294,11 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 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) + network = { + 'network_id': 'net-id', + 'network_name': 'net-name', + 'segmentation_id': 123} + self.drv.create_network(tenant_id, network) cmds = ['enable', 'configure', 'management openstack', 'region RegionOne', 'tenant ten-1', 'network id net-id name "net-name"', @@ -301,6 +306,30 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 'exit', 'exit', 'exit', 'exit', 'exit'] self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + def test_create_network_bulk(self): + tenant_id = 'ten-2' + num_networks = 10 + networks = [{ + 'network_id': 'net-id-%d' % net_id, + 'network_name': 'net-name-%d' % net_id, + 'segmentation_id': net_id} for net_id in range(1, num_networks) + ] + + self.drv.create_network_bulk(tenant_id, networks) + cmds = ['enable', + 'configure', + 'management openstack', + 'region RegionOne', + 'tenant ten-2'] + for net_id in range(1, num_networks): + cmds.append('network id net-id-%d name "net-name-%d"' % + (net_id, net_id)) + cmds.append('segment 1 type vlan id %d' % net_id) + + cmds.extend(self._get_exit_mode_cmds(['tenant', 'region', 'openstack', + 'configure', 'enable'])) + 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' @@ -311,6 +340,29 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 'exit', 'exit', 'exit', 'exit'] self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + def test_delete_network_bulk(self): + tenant_id = 'ten-2' + num_networks = 10 + networks = [{ + 'network_id': 'net-id-%d' % net_id, + 'network_name': 'net-name-%d' % net_id, + 'segmentation_id': net_id} for net_id in range(1, num_networks) + ] + + networks = ['net-id-%d' % net_id for net_id in range(1, num_networks)] + self.drv.delete_network_bulk(tenant_id, networks) + cmds = ['enable', + 'configure', + 'management openstack', + 'region RegionOne', + 'tenant ten-2'] + for net_id in range(1, num_networks): + cmds.append('no network id net-id-%d' % net_id) + + cmds.extend(self._get_exit_mode_cmds(['tenant', 'region', 'openstack', + 'configure'])) + 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' @@ -321,6 +373,84 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 'exit', 'exit', 'exit', 'exit'] self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + def test_delete_vm_bulk(self): + tenant_id = 'ten-2' + num_vms = 10 + vm_ids = ['vm-id-%d' % vm_id for vm_id in range(1, num_vms)] + self.drv.delete_vm_bulk(tenant_id, vm_ids) + + cmds = ['enable', + 'configure', + 'management openstack', + 'region RegionOne', + 'tenant ten-2'] + + for vm_id in range(1, num_vms): + cmds.append('no vm id vm-id-%d' % vm_id) + + cmds.extend(self._get_exit_mode_cmds(['tenant', 'region', 'openstack', + 'configure'])) + self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + + def test_create_vm_port_bulk(self): + tenant_id = 'ten-3' + num_vms = 10 + num_ports_per_vm = 2 + + vms = dict( + ('vm-id-%d' % vm_id, { + 'vmId': 'vm-id-%d' % vm_id, + 'host': 'host_%d' % vm_id, + } + ) for vm_id in range(1, num_vms) + ) + + devices = [n_const.DEVICE_OWNER_DHCP, 'compute'] + vm_port_list = [] + + net_count = 1 + for vm_id in range(1, num_vms): + for port_id in range(1, num_ports_per_vm): + port = { + 'id': 'port-id-%d-%d' % (vm_id, port_id), + 'device_id': 'vm-id-%d' % vm_id, + 'device_owner': devices[(vm_id + port_id) % 2], + 'network_id': 'network-id-%d' % net_count, + 'name': 'port-%d-%d' % (vm_id, port_id) + } + vm_port_list.append(port) + net_count += 1 + + self.drv.create_vm_port_bulk(tenant_id, vm_port_list, vms) + cmds = ['enable', + 'configure', + 'management openstack', + 'region RegionOne', + 'tenant ten-3'] + + net_count = 1 + for vm_count in range(1, num_vms): + host = 'host_%s' % vm_count + for port_count in range(1, num_ports_per_vm): + vm_id = 'vm-id-%d' % vm_count + device_owner = devices[(vm_count + port_count) % 2] + port_name = '"port-%d-%d"' % (vm_count, port_count) + network_id = 'network-id-%d' % net_count + port_id = 'port-id-%d-%d' % (vm_count, port_count) + if device_owner == 'network:dhcp': + cmds.append('network id %s' % network_id) + cmds.append('dhcp id %s hostid %s port-id %s name %s' % ( + vm_id, host, port_id, port_name)) + elif device_owner == 'compute': + cmds.append('vm id %s hostid %s' % (vm_id, host)) + cmds.append('port id %s name %s network-id %s' % ( + port_id, port_name, network_id)) + net_count += 1 + + cmds.extend(self._get_exit_mode_cmds(['tenant', 'region', + 'openstack'])) + 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) @@ -329,6 +459,21 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): 'exit', 'exit', 'exit'] self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + def test_delete_tenant_bulk(self): + num_tenants = 10 + tenant_list = ['ten-%d' % t_id for t_id in range(1, num_tenants)] + self.drv.delete_tenant_bulk(tenant_list) + cmds = ['enable', + 'configure', + 'management openstack', + 'region RegionOne'] + for ten_id in range(1, num_tenants): + cmds.append('no tenant ten-%d' % ten_id) + + cmds.extend(self._get_exit_mode_cmds(['region', 'openstack', + 'configure'])) + 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() @@ -353,6 +498,11 @@ class PositiveRPCWrapperValidConfigTestCase(base.BaseTestCase): self.assertEqual(net_info, valid_net_info, ('Must return network info for a valid net')) + def test_check_cli_commands(self): + self.drv.check_cli_commands() + cmds = ['show openstack config region RegionOne timestamp'] + self.drv._server.runCmds.assert_called_once_with(version=1, cmds=cmds) + class AristaRPCWrapperInvalidConfigTestCase(base.BaseTestCase): """Negative test cases to test the Arista Driver configuration."""