read_me = '''This is a Python 3 script to migrate configuration from Catalyst 3750-X to Meraki MS-series switches. Usage syntax: python migrate_cat3k.py -k <API key> -o <org name> -i <init file> [-u <default user> -p <default pass> -x <proxy>] Mandatory parameters: -k <API key> : Your Meraki Dashboard API key -o <org name> : Name of the Meraki organization you want to interact with -i <init file> : OS path to the init configuration file Optional parameters: -u <default user> : Catalyst switch SSH username, if none is defined in init config -p <default pass> : Catalyst switch SSH password, if none is defined in init config -x <proxy> : Whether to use the new Dashboard API mega proxy or not. Valid forms: -x use_mega_proxy Sends API requests to "api-mp.meraki.com" (default) -x do_not_use_mega_proxy Sends API requests to "api.meraki.com" Usage example: python migrate_cat3k.py -k 1234 -o "My Meraki Account" -i init_config.txt Init configuration file example: https://github.com/meraki/automation-scripts/blob/master/migrate_cat3k/migrate_cat3k_init_example.txt Usage notes: SSH sources require a username and password, either by setting the defaults, or providing one in the init file. Required Python modules: Requests : http://docs.python-requests.org Paramiko : http://www.paramiko.org/installing.html After installing Python, you can install these additional modules using pip with the following commands: pip install requests pip install paramiko General notes: * Depending on your operating system, the commands can be "pip3" and "python3" instead of "pip" and "python" * In Windows, to pass argument values containing spaces, you will need to use double quotes "" * The usernames provided for SSH access to source switches must have a privilege level of 15. In it's current form, the script will not attempt to enter the enable command * The Meraki Dashboard API key used must have full organization permissions * The only IOS platform tested is the Catalyst 3750-X * The CLI parser currently supports the following IOS commands: hostname interface GigabitEthernet description switchport mode switchport access vlan switchport voice vlan switchport trunk native vlan vlan name ''' import sys, getopt, requests, json, paramiko, re, time, datetime #SECTION: GLOBAL VARIABLES: MODIFY TO CHANGE SCRIPT BEHAVIOUR API_EXEC_DELAY = 0.21 #Used in merakiRequestThrottler() to avoid hitting dashboard API max request rate #connect and read timeouts for the Requests module in seconds REQUESTS_CONNECT_TIMEOUT = 90 REQUESTS_READ_TIMEOUT = 90 #SECTION: GLOBAL VARIABLES AND CLASSES: DO NOT MODIFY LAST_MERAKI_REQUEST = datetime.datetime.now() #used by merakiRequestThrottler() API_BASE_URL = 'https://api-mp.meraki.com/api/v0' API_BASE_URL_MEGA_PROXY = 'https://api-mp.meraki.com/api/v0' API_BASE_URL_NO_MEGA = 'https://api.meraki.com/api/v0' ACTION_BATCH_QUEUE = [] #Max number of loops to try when waiting for action batches to complete and retry interval in seconds ABWAIT_MAX_LOOPS = 20 ABWAIT_RETRY_INTERVAL = 2 #SECTION: Classes class c_conversion: def __init__(self): self.hostname = None self.rawConfig = None self.sourceType = None self.sourceValue = None self.sourceUser = None self.sourcePass = None self.targetNetwork = None self.targetDevices = [] self.portConfig = None #end class ### SECTION: General functions def printHelpAndExit(): print(read_me) sys.exit(0) def portCountsForSwitchModel (p_model): copper = None sfp = None if p_model.startswith('MS'): splitNumbers = re.findall(r'\d+', p_model) #check if switch is a supported device model if int(splitNumbers[0]) in [120, 125, 210, 220, 225, 250, 350, 410, 425]: copper = int(splitNumbers[1]) if copper == 8: sfp = 2 elif copper in [24, 48]: sfp = 4 else: sfp = 0 return copper, sfp ### SECTION: Functions for interacting with SSH and files def loadinitcfg(p_filename, p_defaultuser, p_defaultpass): #loads initial configuration from a file with network and device definitions configtable = [] netList = [] serialList = [] failValue = [None, None, None] networkdefined = False currentnet = '' dcount = 0 linenum = 0 try: f = open(p_filename, 'r') except: return(configtable) #iterate through file and parse lines for line in f: linenum += 1 stripped = line.strip() #drop blank lines if len(stripped) > 0: #drop comments if stripped[0] != '#': #process network definition lines if stripped [:4] == 'net=': if len(stripped[4:]) > 0: currentnet = stripped[4:].strip() networkdefined = True netList.append( {'name': currentnet} ) else: print('ERROR 01: Init config (line %d): Network name cannot be blank' % linenum) return failValue else: #else process as a device record if networkdefined: splitline = stripped.split() if len(splitline) > 1: configtable.append(c_conversion()) lastItem = len(configtable) - 1 configtable[lastItem].targetNetwork = currentnet #look for file keyword and load source accordingly if splitline[0] == 'file': configtable[lastItem].sourceType = 'file' if len(splitline) > 2: configtable[lastItem].sourceValue = splitline[1].strip() remainingFields = splitline [2:] else: print('ERROR 02: Init config (line %d): Invalid definition: %s' % (linenum, stripped)) return failValue else: #not a source file definition. assume FQDN/IP configtable[lastItem].sourceType = 'fqdn' configtable[lastItem].sourceValue = splitline[0].strip() remainingFields = splitline [1:] #map to correct Meraki serial(s) if len(remainingFields) > 0: serials = remainingFields[0].split(',') for serial in serials: configtable[lastItem].targetDevices.append(serial) #configtable[lastItem].targetDevices[len(configtable[lastItem].targetDevices)-1].serial = serial serialList.append( {'serial': serial, 'networkName': currentnet} ) if len(remainingFields) > 2: #device-specific username and password defined configtable[lastItem].sourceUser = remainingFields[1] configtable[lastItem].sourcePass = remainingFields[2] elif len(remainingFields) > 1: #got either username or password, but not both print('ERROR 03: Init config (line %d): Invalid definition: %s' % (linenum, stripped)) return failValue else: #no device-specific username/password configuration. use defaults #abort if default user/password are invalid if (p_defaultuser == None or p_defaultpass == None) and configtable[lastItem].sourceType == 'fqdn': print('ERROR 04: Default SSH credentials needed, but not defined') return failValue configtable[lastItem].sourceUser = p_defaultuser configtable[lastItem].sourcePass = p_defaultpass else: print('ERROR 05: Init config (line %d): Invalid definition: %s' % (linenum, stripped)) return failValue else: print('ERROR 06: Init config (line %d): Device with no network defined' % linenum) return failValue f.close() return (configtable, netList, serialList) def waitForOutput(p_session, p_timeout): increment = 0.1 i = 0 while i < p_timeout: i += increment time.sleep(increment) if p_session.recv_ready(): return p_session.recv(65535).decode('ascii') return None def loadCatalystConfigSsh (p_hostip, p_user, p_pass): #logs into a IOS-based device using SSH and pulls its current configuration #returns None on error linetable = [] configStr = '' ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: ssh.connect(p_hostip, username=p_user, password=p_pass) session = ssh.invoke_shell() output = waitForOutput(session, 10) session.send("show running\n") output = waitForOutput(session, 10) outputHasMorePages = True while outputHasMorePages: outputHasMorePages = False page = waitForOutput(session, 10) if not page is None: length = len(page) if length > 9: if page[length-9:].strip() == "--More--": configStr += page[:length-9] session.send(" ") outputHasMorePages = True else: configStr += page except: print('ERROR 07: Could not connect to source device: "%s"' % p_hostip) return None if configStr == '': print('ERROR 08: No config on device: "%s"' % p_hostip) return None for line in configStr.splitlines(): strippedline = line.strip() if len(strippedline) > 0: #ignore comments if strippedline[0] != '!': stringWithoutBackspaces = '' for char in strippedline: if char == '\x08': #check if character is a backspace stringWithoutBackspaces += ' ' else: stringWithoutBackspaces += char linetable.append(stringWithoutBackspaces.strip()) return (linetable) def loadCatalystConfigFile(p_filename): #loads source device configuration from file linetable = [] try: f = open(p_filename, 'r') except: print('ERROR 09: Could not read source config file: %s' % p_filename) return None strippedline = '' for line in f: strippedline = line.strip() if len(strippedline) > 0: #ignore comments if strippedline[0] != '!': linetable.append(strippedline) f.close() return (linetable) def parseHostname(p_rawcfg): #extract hostname form device config #command parser loop for cfgline in p_rawcfg: pieces = cfgline.split() if pieces[0] == 'hostname': return (pieces[1].strip()) return None def parsePortConfig(p_rawcfg): #parses port (interface) configuration from a Catalyst configuration table stackMembers = [] currentStackMember = 0 currentModule = 0 currentPort = 0 currentVlan = 0 vlanNames = {} intcount = 0 avlan = '' #string for building allowed VLAN value supportedinterface = False #command parser loop for cfgline in p_rawcfg: pieces = cfgline.split() firstPiece = pieces[0].strip() if firstPiece == 'interface': #if interface is of a supported type, create new entry. otherwise ignore it #and lock int command parsing functions until a supported one comes up if pieces[1].startswith('GigabitEthernet'): intNumber = pieces[1][15:] splitNumber = intNumber.split('/') currentStackMember = int(splitNumber[0]) - 1 currentModule = splitNumber[1] currentPort = splitNumber[2] if len(stackMembers) <= currentStackMember: stackMembers.append({'0':{}, '1':{}}) supportedinterface = True else: supportedinterface = False elif firstPiece == 'description' and supportedinterface: if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} #set int desc as port name. strip everything except alphanumerics and "_" stackMembers[currentStackMember][currentModule][currentPort]['name'] = re.sub(r'\W+','_', cfgline[12:])[:20] elif firstPiece == 'switchport' and supportedinterface: if pieces[1] == 'mode': if pieces[2] == 'access': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['mode'] = 'access' elif pieces[2] == 'trunk': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['mode'] = 'trunk' elif pieces[1] == 'access': if pieces[2] == 'vlan': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['access'] = pieces[3] if pieces[3] in vlanNames: if not 'tags' in stackMembers[currentStackMember][currentModule][currentPort]: stackMembers[currentStackMember][currentModule][currentPort]['tags'] = '' stackMembers[currentStackMember][currentModule][currentPort]['tags'] += vlanNames[pieces[3]] stackMembers[currentStackMember][currentModule][currentPort]['tags'] += ' ' elif pieces[1] == 'voice': if pieces[2] == 'vlan': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['voice'] = pieces[3] if not 'tags' in stackMembers[currentStackMember][currentModule][currentPort]: stackMembers[currentStackMember][currentModule][currentPort]['tags'] = '' stackMembers[currentStackMember][currentModule][currentPort]['tags'] += vlanNames[pieces[3]] stackMembers[currentStackMember][currentModule][currentPort]['tags'] += ' ' elif pieces[1] == 'trunk': if pieces[2] == 'native': if pieces[3] == 'vlan': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['native'] = pieces[4] elif pieces[2] == 'allowed': if pieces[3] == 'vlan': if not str(currentPort) in stackMembers[currentStackMember][currentModule]: stackMembers[currentStackMember][currentModule][currentPort] = {} stackMembers[currentStackMember][currentModule][currentPort]['allowed'] = [] if not 'tags' in stackMembers[currentStackMember][currentModule][currentPort]: stackMembers[currentStackMember][currentModule][currentPort]['tags'] = '' splitStr = pieces[4].split(',') for line in splitStr: stackMembers[currentStackMember][currentModule][currentPort]['allowed'].append(line) if line in vlanNames: stackMembers[currentStackMember][currentModule][currentPort]['tags'] += vlanNames[line] stackMembers[currentStackMember][currentModule][currentPort]['tags'] += ' ' elif firstPiece == 'vlan': try: currentVlan = int(pieces[1]) except: currentVlan = 0 elif firstPiece == 'name': vlanNames[str(currentVlan)] = pieces[1] return(stackMembers) ### SECTION: Functions for interacting with Dashboard def merakiRequestThrottler(): #prevents hitting max request rate shaper of the Meraki Dashboard API global LAST_MERAKI_REQUEST if (datetime.datetime.now()-LAST_MERAKI_REQUEST).total_seconds() < (API_EXEC_DELAY): time.sleep(API_EXEC_DELAY) LAST_MERAKI_REQUEST = datetime.datetime.now() return def getOrgId(p_apiKey, p_orgName): #returns the organizations' list for a specified admin, with filters applied merakiRequestThrottler() try: r = requests.get( API_BASE_URL + '/organizations', headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) except: return None if r.status_code != requests.codes.ok: return None rjson = r.json() for org in rjson: if org['name'] == p_orgName: return org['id'] return None def getNetworks(p_apiKey, p_orgId): #returns a list of all networks in an organization merakiRequestThrottler() try: r = requests.get( API_BASE_URL + '/organizations/%s/networks' % (p_orgId), headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) except: print('ERROR 10: Unable to get networks') return(None) if r.status_code != requests.codes.ok: return(None) return(r.json()) def getInventory(p_apiKey, p_orgId): #returns a list of all networks in an organization merakiRequestThrottler() try: r = requests.get( API_BASE_URL + '/organizations/%s/inventory' % p_orgId, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) except: print('ERROR 11: Unable to get inventory') return(None) if r.status_code != requests.codes.ok: return(None) return(r.json()) def createActionBatch (p_apiKey, p_orgId, p_actions): merakiRequestThrottler() payload = json.dumps( { 'confirmed':True, 'synchronous':False, 'actions': p_actions } ) try: r = requests.post( API_BASE_URL + '/organizations/%s/actionBatches' % p_orgId, data=payload, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)) except: return None if 200 <= r.status_code < 300: rjson = r.json() return rjson['id'] return None def queueActionBatch (p_apiKey, p_orgId, p_action, p_forceCommit=False): #return success, batchId global ACTION_BATCH_QUEUE if not p_action is None: ACTION_BATCH_QUEUE.append(p_action) queueLength = len(ACTION_BATCH_QUEUE) if queueLength == 100 or (queueLength > 0 and p_forceCommit): print('Committing action batch:') print(ACTION_BATCH_QUEUE) batchId = createActionBatch (p_apiKey, p_orgId, ACTION_BATCH_QUEUE) ACTION_BATCH_QUEUE = [] if not batchId is None: return (True, batchId) else: return (False, None) return (True, None) def getActionBatches(p_apiKey, p_orgId): merakiRequestThrottler() try: r = requests.get( API_BASE_URL + '/organizations/%s/actionBatches' % p_orgId, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) except: print('ERROR 12: Unable to get action batches') return(None) if r.status_code != requests.codes.ok: return(None) return(r.json()) def waitForActionBatchesToComplete(p_apiKey, p_orgId, p_batchIds): flag_waitSomeMore = True flag_batchHasFailed = False i = -1 while flag_waitSomeMore: i += 1 if i >= ABWAIT_MAX_LOOPS: break flag_waitSomeMore = False actionBatches = getActionBatches(p_apiKey, p_orgId) #print(actionBatches) for id in p_batchIds: for record in actionBatches: if record['id'] == id: if not record['status']['completed'] and not record['status']['failed']: flag_waitSomeMore = True break if record['status']['failed']: print('ERROR 29: Action batch with batchId %s has failed' % id) if 'errors' in record['status']: print(record['status']['errors']) return False if flag_waitSomeMore: time.sleep(ABWAIT_RETRY_INTERVAL) return True def sendHostnameToQueue(p_apiKey, p_orgId, p_networkId, p_hostname, p_increment, p_serial): batchIdList = [] if not p_hostname is None: name = p_hostname if p_increment > 0: name += '_%s' % p_increment body = { 'name' : name } action = { 'resource' : '/networks/' + p_networkId + '/devices/' + p_serial, 'operation' : 'update', 'body' : body } success, batchId = queueActionBatch (p_apiKey, p_orgId, action) if not success: print('ERROR 13: Failed to queue action batch') if not batchId is None: batchIdList.append(batchId) if len(batchIdList) > 0: return batchIdList return None def sendPortConfigToQueue(p_apiKey, p_orgId, p_portConfig, p_serial, p_copperCount, p_sfpCount): #p_portConfig module 0: copper ports, module 1: sfp ports portList = [] batchIdList = [] for port in p_portConfig['0']: portNum = int(port) config = p_portConfig['0'][port] if portNum <= int(p_copperCount): portList.append({'number': portNum, 'config': config}) for port in p_portConfig['1']: portNum = int(port) config = p_portConfig['1'][port] if portNum <= int(p_sfpCount): portList.append({'number': portNum + int(p_copperCount), 'config': config}) for port in portList: body = {} switchportMode = 'access' if 'mode' in port['config']: body['type'] = port['config']['mode'] if port['config']['mode'] == 'trunk': switchportMode = 'trunk' if 'access' in port['config']: if switchportMode == 'access': body['vlan'] = port['config']['access'] if 'voice' in port['config']: body['voiceVlan'] = port['config']['voice'] if 'tags' in port['config']: body['tags'] = port['config']['tags'] if 'native' in port['config']: if switchportMode == 'trunk': body['vlan'] = port['config']['native'] action = { 'resource' : '/devices/' + p_serial + '/switchPorts/' + str(port['number']), 'operation' : 'update', 'body' : body } success, batchId = queueActionBatch (p_apiKey, p_orgId, action) if not success: print('ERROR 14: Failed to queue action batch') if not batchId is None: batchIdList.append(batchId) if len(batchIdList) > 0: return batchIdList return None ### SECTION: Main function def main(argv): global API_BASE_URL #set default values for command line arguments arg_apikey = None arg_orgname = None arg_initfile = None #a default value that is not a valid filename arg_defuser = None #a default value that is not a valid username arg_defpass = None #a default value that is not a valid password arg_proxy = None try: opts, args = getopt.getopt(argv, 'hk:o:i:u:p:x:') except getopt.GetoptError: printHelpAndExit() for opt, arg in opts: if opt == '-h': printHelpAndExit() elif opt == '-k': arg_apikey = arg elif opt == '-o': arg_orgname = arg elif opt == '-i': arg_initfile = arg elif opt == '-u': arg_defuser = arg elif opt == '-p': arg_defpass = arg elif opt == '-x': arg_proxy = arg #check if all required parameters have been given if arg_apikey is None or arg_orgname is None or arg_initfile is None: printHelpAndExit() API_BASE_URL = API_BASE_URL_MEGA_PROXY if not arg_proxy is None: if arg_proxy == 'do_not_use_mega_proxy': API_BASE_URL = API_BASE_URL_NO_MEGA #load configuration file print('Reading init config file...') conversions, networks, devices = loadinitcfg(arg_initfile, arg_defuser, arg_defpass) if conversions is None or len(conversions) == 0: print('ERROR 15: No valid configuration in init file') sys.exit(2) #get organization id corresponding to org name provided by user print('Fetching dashboard organization...') orgId = getOrgId(arg_apikey, arg_orgname) if orgId is None: print('ERROR 16: Fetching organization id failed') sys.exit(2) #read configuration from source devices specified in init config print('Reading configuration from source devices...') for item in conversions: if item.sourceType is None: print('ERROR 17: No sourceType for sourceValue "%s"' % item.sourceValue) else: rawConfig = None if item.sourceType == 'file': rawConfig = loadCatalystConfigFile(item.sourceValue) elif item.sourceType == 'fqdn': rawConfig = loadCatalystConfigSsh (item.sourceValue, item.sourceUser, item.sourcePass) if not rawConfig is None: item.rawConfig = rawConfig else: print('ERROR 18: Unable to read configuration from source "%s"' % item.sourceValue) #parse hostname and other configuration from raw print('Parsing source configration...') for item in conversions: if item.rawConfig != None: item.hostname = parseHostname(item.rawConfig) item.portConfig = parsePortConfig(item.rawConfig) #check if networks already exist and create missing print('Creating networks...') existingNetworks = getNetworks(arg_apikey, orgId) if existingNetworks is None: sys.exit(2) flag_createdNetworks = False batchIds = [] for net in networks: networkFound = False for existing in existingNetworks: if existing['name'] == net['name']: if not existing['type'] in ['switch', 'combined']: print('ERROR 19: Existing network "%s" is of wrong type "%s"' % (existing['name'], existing['type'])) sys.exit(2) networkFound = True break if not networkFound: body = { 'name' : net['name'], 'type' : 'switch', 'tags' : 'migrate_cat3k' } action = { 'resource' : '/organizations/' + orgId + '/networks', 'operation' : 'create', 'body' : body } success, batchId = queueActionBatch (arg_apikey, orgId, action) if not success: print('ERROR 20: Failed to queue action batch') sys.exit(2) if not batchId is None: batchIds.append(batchId) flag_createdNetworks = True if flag_createdNetworks: success, batchId = queueActionBatch (arg_apikey, orgId, None, True) if not success: print('ERROR 21: Failed to queue action batch') sys.exit(2) if not batchId is None: batchIds.append(batchId) #check that all action batches have been completed before proceeding print('Waiting for action batches to complete...') result = waitForActionBatchesToComplete(arg_apikey, orgId, batchIds) if not result: print('ERROR 22: An action batch has failed to execute') sys.exit(2) existingNetworks = getNetworks(arg_apikey, orgId) if existingNetworks is None: sys.exit(2) #get org inventory to check if devices are already claimed inventory = getInventory(arg_apikey, orgId) if inventory is None: sys.exit(2) #claim devices into networks print('Claiming devices...') flag_claimedDevices = False batchIds = [] for device in devices: device['netId'] = None flag_deviceFoundAndClaimed = False for existingDev in inventory: if device['serial'] == existingDev['serial']: #check if device is associated to a network and if it is the correct one if not existingDev['networkId'] is None: for net in existingNetworks: if net['id'] == existingDev['networkId']: if net['name'] == device['networkName']: device['networkId'] = net['id'] flag_deviceFoundAndClaimed = True else: print('ERROR 23: Switch "%s" is in use in another network (%s)' % (device['serial'], net['name'])) sys.exit(2) break flag_deviceFound = True break for net in existingNetworks: if net['name'] == device['networkName']: device['networkId'] = net['id'] if not flag_deviceFoundAndClaimed: body = { 'serial' : device['serial'] } action = { 'resource' : '/networks/' + device['networkId'] + '/devices', 'operation' : 'claim', 'body' : body } success, batchId = queueActionBatch (arg_apikey, orgId, action) if not success: print('ERROR 24: Failed to queue action batch') sys.exit(2) if not batchId is None: batchIds.append(batchId) flag_claimedDevices = True if flag_claimedDevices: success, batchId = queueActionBatch (arg_apikey, orgId, None, True) if not success: print('ERROR 25: Failed to queue action batch') sys.exit(2) if not batchId is None: batchIds.append(batchId) #check that all action batches have been completed before proceeding print('Waiting for action batches to complete...') result = waitForActionBatchesToComplete(arg_apikey, orgId, batchIds) if not result: print('ERROR 26: An action batch has failed to execute') sys.exit(2) #refresh inventory to get device models of newly claimed switches inventory = getInventory(arg_apikey, orgId) if inventory is None: sys.exit(2) #calculate port counts for destination switches to prevent overflow for device in devices: for record in inventory: if device['serial'] == record['serial']: device['model'] = record['model'] copper, sfp = portCountsForSwitchModel(device['model']) if copper is None: print('ERROR 27: Device model "%s" is not supported by this script' % device['model']) sys.exit(2) device['copper'] = copper device['sfp'] = sfp break #submit config print('Configuring devices...') batchIds = [] for line in conversions: shortest = len(line.portConfig) deviceListLen = len(line.targetDevices) if deviceListLen < shortest: shortest = deviceListLen for i in range(shortest): targetNetwork = None copperPortCount = None sfpPortCount = None for device in devices: if device['serial'] == line.targetDevices[i]: targetNetwork = device['networkId'] copperPortCount = device['copper'] sfpPortCount = device['sfp'] break tempBatchIdList = sendHostnameToQueue(arg_apikey, orgId, targetNetwork, line.hostname, i, line.targetDevices[i]) if not tempBatchIdList is None: for id in tempBatchIdList: batchIds.append(id) tempBatchIdList = sendPortConfigToQueue(arg_apikey, orgId, line.portConfig[i], line.targetDevices[i], copperPortCount, sfpPortCount) if not tempBatchIdList is None: for id in tempBatchIdList: batchIds.append(id) success, batchId = queueActionBatch (arg_apikey, orgId, None, True) if not batchId is None: batchIds.append(batchId) #check that all action batches have been completed before proceeding print('Waiting for action batches to complete...') result = waitForActionBatchesToComplete(arg_apikey, orgId, batchIds) if not result: print('ERROR 28: An action batch has failed to execute') sys.exit(2) print('\nEnd of script.') if __name__ == '__main__': main(sys.argv[1:])