read_me = '''This is a Python 3 script to provision template-based networks with manually defined VLAN subnets
 to Meraki dashboard.
 
Syntax:
  python provision_sites.py -k <api key> -o <org name> -i <input file> [-n <net type> -u <update mode> -x <proxy mode>]
  
Mandatory parameters:
  -k <api key>          : Your Meraki Dashboard API key
  -o <org name>         : The name of the dashboard organization you want to provision the sites into
  -i <input file>       : Name of the CSV file containing info for the networks to be created
  
Optional parameters:
  -n <net type>         : Product types to create networks for. Must match templates used. Valid options:
                            -n appliance                  MX/Z3 appliances only (default)
                            -n appliance-wireless         MX/Z3 appliances and MR Wi-Fi access points
                            -n appliance-switch           MX/Z3 appliances and MS switches
                            -n appliance-wireless-switch  MX/Z3 appliances, MR Wi-Fi APs and MS switches
  -u <update mode>      : Whether to update existing or fail script if the organization already contains networks
                           with names that match the ones in the input file. Valid forms:
                            -u fail                       Interrupts script if network is not new (default)
                            -u update                     Attempts to update existing networks to match input file
  -x <proxy mode>       : 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 provision_sites.py -k 1234 -o "Big Industries Inc" -i site_info.csv
  
Example input CSV file and CSV generator Excel sheet here:
  https://github.com/meraki/automation-scripts/tree/master/provision_sites
  
Required Python 3 modules:
  Requests     : http://docs.python-requests.org
  
  After installing Python, you can install these additional modules using pip with the following commands:
    pip install requests
    
Notes:
  * Depending on your operating system, the commands for python and pip may be "python3" and "pip3" instead
  * Use double quotes to enter names containing spaces in the Windows command line
  * For the script to work, VLANs to be modified will need to be set to "unique" subnetting in dashboard
  * The script executes most configuration tasks as action batches for scalability. If one task fails, its 
     whole batch will fail with it. Refer to the script's command line output for which tasks have been grouped
     together as batches and whether executing a batch has produced errors
  * Including the location (street address) of your network to the input file will result in devices being
     repositioned on the world map to match this address. If you wish to prevent this for a network, leave
     this cell blank
'''

import sys, getopt, requests, json, time, datetime, ipaddress


### SECTION: GLOBAL VARIABLES: MODIFY TO CHANGE SCRIPT BEHAVIOUR


#Used in merakiRequestThrottler() to avoid hitting dashboard API max request rate
API_EXEC_DELAY              = 0.21 

#connect and read timeouts for the Requests module in seconds
REQUESTS_CONNECT_TIMEOUT    = 90
REQUESTS_READ_TIMEOUT       = 90

#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: 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          = []


### SECTION: CLASS DEFINITIONS


class c_network:
    def __init__(self):
        self.name           = None
        self.id             = None
        self.templateName   = None
        self.templateId     = None
        self.skipBinding    = False
        self.location       = None
        self.serials        = []
        self.vlanOverrides  = {}
#end class


### SECTION: GENERAL USE FUNCTIONS


def printHelpAndExit():
    print(read_me)
    sys.exit(0)


def killScript():
    print('ERROR 01: Execution interrupted.')
    sys.exit(2)


def debugDumpNetwork(p_network):
    dump = []
    dump.append(p_network.id)
    dump.append(p_network.name)
    dump.append(p_network.location)
    dump.append(p_network.templateId)
    dump.append(p_network.templateName)
    dump.append(p_network.vlanOverrides)
    dump.append(p_network.serials)
    print(dump)


def parseVlansFromHeader(p_line, p_delimeter):
    returnList = []
    splitLine = p_line.split(p_delimeter)
    for item in splitLine:
        stripped = item.strip()
        if stripped.startswith('VLAN'):
            if len(stripped) > 5:
                if stripped[5] != 'X':
                    try:
                        number = int(stripped[5:])
                        returnList.append(number)
                    except:
                        print('ERROR 02: Invalid VLAN number "%s" in input file' % stripped[5:])
                        return None
                else:
                    #push flag to ignore this column, as it is disabled by user choice
                    returnList.append(None)
            else:
                print('ERROR 03: Invalid VLAN number definition in input file')
                return None
            
    return returnList
    
    
def parseNetwork(p_line, p_delimeter, p_vlans):

    network     = c_network()
    splitLine   = p_line.split(p_delimeter)
    lenSplit    = len(splitLine)
    lenVlans    = len(p_vlans)
    
    if lenSplit < (lenVlans + 4):
        print(p_line)
        print('ERROR 04: Invalid network definition in input file')
        return None
        
    network.name            = splitLine[0].strip()
    network.location        = splitLine[1].strip()
    network.templateName    = splitLine[2].strip()
    
    for i in range(lenVlans):
        label = p_vlans[i]
        if not label is None:
            cellValue = splitLine[i+3].strip()
            if cellValue != '':
                network.vlanOverrides[str(label)] = cellValue
            
    serials = splitLine[lenSplit-1].split(' ')
    
    for item in serials:
        sItem = item.strip()
        if sItem != '':
            network.serials.append(sItem)
        
    return network
    

def loadCsv(p_fileName):
    
    try:
        f = open(p_fileName, 'r')    
    except:
        print('ERROR 05: Unable to open input file')
        return None
        
    delimeter       = ';'
    headerNotFound  = True
    vlanNumbers     = None
    networksList    = []
    templatesList   = []
        
    for line in f:
        stripped = line.strip()
        if len(stripped) > 0:
            if not stripped[0] in ['#', '"']:
                if headerNotFound and stripped.startswith('meta:delimeter-detector-line'):
                    #this is a delimeter detector line
                    if len(stripped) > 28 and stripped[28] in [',', ';']:
                        delimeter = stripped[28]
                    else:
                        print('ERROR 06: Invalid delimeter detector line in input file')
                        return None
                        
                elif headerNotFound and stripped.startswith('Network name'):
                    #this is a table header line
                    headerNotFound = False
                    vlanNumbers = parseVlansFromHeader(stripped, delimeter)
                    
                    if vlanNumbers is None:
                        print('ERROR 07: VLAN numbers defined incorrectly in input file')
                        return None
                    
                else:
                    #this is a network definition line                    
                    network = parseNetwork(stripped, delimeter, vlanNumbers)
                    
                    if network is None:
                        print('ERROR 08: Invalid network definition')
                        return None
                                                
                    networksList.append(network)
                                        
    f.close()
    
    return networksList
    
    
def changeSubnet(p_oldNet, p_newNet, p_oldRouterIp):
    oldNet      = ipaddress.ip_network(p_oldNet, False)
    oldPrefix   = str(oldNet.prefixlen)
    try:
        newNet  = ipaddress.ip_network(p_newNet + '/' + oldPrefix, False)
    except:
        print('ERROR 09: Invalid subnet %s/%s' % (p_newNet, oldPrefix))
        return None, None
        
    oldHosts    = list(oldNet.hosts())
    oldRouterIp = ipaddress.IPv4Address(p_oldRouterIp)
    routerIndex = oldHosts.index(oldRouterIp)
    newHosts    = list(newNet.hosts())
    newRouterIp = newHosts[routerIndex]
        
    return newNet, newRouterIp
    
    
### SECTION: FUNCTIONS FOR MERAKI DASHBOARD COMMUNICATION


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 getOrgTemplates(p_apiKey, p_orgId):
    #returns the organizations' list for a specified admin, with filters applied
        
    merakiRequestThrottler()
    try:
        r = requests.get( API_BASE_URL + '/organizations/%s/configTemplates' % p_orgId, 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()
        
    return rjson
    
    
def getOrgNetworks(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 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('Submitting action batch:')
        print(ACTION_BATCH_QUEUE)
        
        batchId = createActionBatch (p_apiKey, p_orgId, ACTION_BATCH_QUEUE)     
        ACTION_BATCH_QUEUE = []
        
        if not batchId is None:
            print('Submitted with batchId %s' % batchId)
            return (True, str(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 11: 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 12: 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 sendCreateNetworkToActionBatchQueue(p_apiKey, p_orgId, p_name, p_type):    
            
    body = {
        'name'    : p_name,
        'type'    : p_type
    }
    action = {
        'resource'  : '/organizations/' + p_orgId + '/networks',
        'operation' : 'create',
        'body'      : body
    }    
    success, batchId = queueActionBatch (p_apiKey, p_orgId, action)
    if not success:
        print('ERROR 13: Failed to queue action batch')
        
    return success, batchId
    
    
def sendUpdateVlanSubnetToActionBatchQueue(p_apiKey, p_orgId, p_networkId, p_vlanId, p_vlanSubnet, p_routerIp):    
            
    body = {
        'subnet'        : p_vlanSubnet,
        'applianceIp'   : p_routerIp
    }
    action = {
        'resource'  : '/networks/' + p_networkId + '/vlans/' + p_vlanId,
        'operation' : 'update',
        'body'      : body
    }    
    success, batchId = queueActionBatch (p_apiKey, p_orgId, action)
    if not success:
        print('ERROR 14: Failed to queue action batch')
        
    return success, batchId
    
    
def sendClaimDeviceToActionBatchQueue(p_apiKey, p_orgId, p_networkId, p_serial):    
            
    body = {
        'serial'        : p_serial
    }
    action = {
        'resource'  : '/networks/' + p_networkId + '/devices',
        'operation' : 'claim',
        'body'      : body
    }    
    success, batchId = queueActionBatch (p_apiKey, p_orgId, action)
    if not success:
        print('ERROR 15: Failed to queue action batch')
        
    return success, batchId
    
    
def sendUpdateDeviceLocationToActionBatchQueue(p_apiKey, p_orgId, p_networkId, p_serial, p_location):  
    
    body = {
        'address'       : p_location,
        'moveMapMarker' : True
    }
    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 16: Failed to queue action batch')
        
    return success, batchId
    
    
def bindNetworkToTemplate (p_apiKey, p_networkId, p_templateId):   
    merakiRequestThrottler()
    
    payload = json.dumps(
        {
            'configTemplateId': p_templateId, 
            'autoBind':False
        }
    )
        
    try:
        r = requests.post( API_BASE_URL + '/networks/%s/bind' % p_networkId, data=payload, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT))
    except:
        return False
    
    if 200 <= r.status_code < 300:
        return True
      
    return False
    
    
def getNetworkVlans(p_apiKey, p_networkId):
    merakiRequestThrottler()
    try:
        r = requests.get( API_BASE_URL + '/networks/%s/vlans' % p_networkId, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) )
    except:
        print('ERROR 17: Unable to get VLANs for network id %s' % p_networkId)
        return(None)
    
    if r.status_code != requests.codes.ok:
        return(None)
    
    return(r.json())
    
    
def getOrgInventory(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 18: Unable to get inventory')
        return(None)
    
    if r.status_code != requests.codes.ok:
        return(None)
    
    return(r.json())


### SECTION: MAIN


def main(argv):
    global API_BASE_URL
    
    #set default values for command line arguments
    arg_apikey          = None
    arg_orgname         = None
    arg_initfile        = None
    arg_nettype         = None
    arg_proxy           = None
    arg_updateExisting  = None
    
    try:
        opts, args = getopt.getopt(argv, 'hk:o:i:n:u: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 == '-n':
            arg_nettype         = arg
        elif opt == '-u':
            arg_updateExisting  = 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()
        
    netType = 'appliance '
    if not arg_nettype is None:
        if arg_nettype.find('wireless') != -1:
            netType += 'wireless '
        if arg_nettype.find('switch') != -1:
            netType += 'switch '
        
    flag_doNotUpdateExisting = True
    if (not arg_updateExisting is None) and arg_updateExisting == 'update':
        flag_doNotUpdateExisting = False
        
    API_BASE_URL = API_BASE_URL_MEGA_PROXY
    if (not arg_proxy is None) and arg_proxy == 'do-not-use-mega-proxy':
        API_BASE_URL = API_BASE_URL_NO_MEGA
        

    print('Reading input file...')
    networks = loadCsv(arg_initfile)
    
    if networks is None:
        killScript()
        
    print('Fetching organization info...')
    orgId = getOrgId(arg_apikey, arg_orgname)
    
    if orgId is None:
        print('ERROR 19: Unable to resolve organization Id')
        killScript()
         
    orgTemplates = getOrgTemplates(arg_apikey, orgId)
    
    if orgTemplates is None:
        print('ERROR 20: Unable to fetch organization templates')
        killScript()
        
    for net in networks:
        nameNotFound = True
        for item in orgTemplates:
            if net.templateName == item['name']:
                nameNotFound = False
                net.templateId = item['id']        
                break
        if nameNotFound:
            print('ERROR 21: Template with name "%s" not found in org' % name)
            killScript()
                            
    orgNetworks = getOrgNetworks(arg_apikey, orgId)
    
    if orgNetworks is None:
        print('ERROR 22: Unable to fetch organization networks')
        killScript()
            
    print('Creating networks...')
    batchIdList = []
    for net in networks:
        nameNotFound = True
        for item in orgNetworks:
            if net.name == item['name']:
                nameNotFound = False
                net.id = item['id']
                if 'configTemplateId' in item:
                    if not item['configTemplateId'] is None:
                        if item['configTemplateId'] != net.templateId:
                            print('ERROR 23: Network "%s" is already bound to a different template' % net.name)
                            killScript()
                        net.skipBinding = True
                break
        if nameNotFound:
            success, batchId = sendCreateNetworkToActionBatchQueue(arg_apikey, orgId, net.name, netType)
            if success:
                if not batchId is None:
                    batchIdList.append(batchId)
            else:
                print('ERROR 24: Error queueing network creation to action batch')
                killScript()
        elif flag_doNotUpdateExisting:
            print('ERROR 25: Network with name "%s" already exists. Use "-u update" to override' % net.name)
            killScript()
            
    #if unsubmitted net creation commands, submit last batch
    success, batchId = queueActionBatch (arg_apikey, orgId, None, True)
    if success:
        if not batchId is None:
            batchIdList.append(batchId)
    else:
        print('ERROR 26: Error submitting action batch')
        killScript()
        
    if len(batchIdList) > 0:
        #check that all action batches have been completed before proceeding
        print('Waiting for action batches to complete...')
        success = waitForActionBatchesToComplete(arg_apikey, orgId, batchIdList)
        
        if not success:
            print('ERROR 27: An action batch has failed to execute')
            killScript()
        
    #Resolve network Ids of new networks
    orgNetworks = getOrgNetworks(arg_apikey, orgId)
    
    if orgNetworks is None:
        print('ERROR 28: Unable to fetch organization networks')
        killScript()
        
    for net in networks:
        nameNotFound = True
        for item in orgNetworks:
            if net.name == item['name']:
                nameNotFound = False
                net.id = item['id']
                break
        if nameNotFound:
            print('ERROR 29: Failed to create network "%s" (%s)' % (net.name, net.id))
            killScript()    

    print('Binding networks to templates...')
    for net in networks:
        if not net.skipBinding:
            bindNetworkToTemplate (arg_apikey, net.id, net.templateId)
        
    #Clear batchIdList to only check for VLAN/device claim batch success at the end of the script
    batchIdList = []
    
    #Update VLAN parameters
    print('Updating VLAN IP parameters...')
    for net in networks:
        if len(net.vlanOverrides) > 0:
            #Get VLANs from dashboard to resolve correct netmask and MX host IP address
            orgNetVlans = getNetworkVlans(arg_apikey, net.id)
            if orgNetVlans is None:
                print('ERROR 30: Failed to get VLAN info for network "%s" (%s)' % (net.name, net.id))
            else:
                for vlan in net.vlanOverrides:
                    vlanNotFound = True
                    for orgVlan in orgNetVlans:
                        if str(orgVlan['id']) == vlan:
                            vlanNotFound = False
                            #Calculate new subnet/prefix and appliance VLAN default gateway IP address and submit to dashboard
                            newSubnet, newRouterIp = changeSubnet(orgVlan['subnet'], net.vlanOverrides[vlan], orgVlan['applianceIp'])
                            if not newSubnet is None:
                                if str(newSubnet) != str(orgVlan['subnet']) and str(newRouterIp) != str(orgVlan['applianceIp']):
                                    success, batchId = sendUpdateVlanSubnetToActionBatchQueue(arg_apikey, orgId, net.id, vlan, str(newSubnet), str(newRouterIp))
                                    if success:
                                        if not batchId is None:
                                            batchIdList.append(batchId)
                                    else:
                                        print('ERROR 31: Error submitting action batch')
                            else:
                                print('ERROR 32: Subnet for net "%s" VLAN %s must be in form x.x.x.x with no mask/prefix' % (net.name, vlan))
                            break
                    if vlanNotFound:
                        print('ERROR 33: Template for network "%s" does not contain VLAN %s' % (net.name, vlan))
                        
    #Check if any networks have devices to claim
    aNetworkHasDevicesToClaim   = False
    gotDeviceClaimConflicts     = False
    for net in networks:
        if len(net.serials) > 0:
            aNetworkHasDevicesToClaim = True
            break
            
    if aNetworkHasDevicesToClaim:
        print('Claiming devices...')
        
        inventory = getOrgInventory(arg_apikey, orgId)
        if not inventory is None:
            for net in networks:
                for serial in net.serials:                 
                    deviceIsAvailable = True
                    deviceBelongsToAnotherNetwork = False
                    for orgDevice in inventory:
                        if orgDevice['serial'] == serial:   
                            if 'networkId' in orgDevice and not orgDevice['networkId'] is None:
                                deviceIsAvailable = False
                                if orgDevice['networkId'] != net.id:
                                    deviceBelongsToAnotherNetwork = True
                            break
                    
                    if deviceIsAvailable:
                        success, batchId = sendClaimDeviceToActionBatchQueue(arg_apikey, orgId, net.id, serial)
                        if success:
                            if not batchId is None:
                                batchIdList.append(batchId)
                        else:
                            print('ERROR 34: Error submitting action batch')
                    else:
                        if deviceBelongsToAnotherNetwork:
                            gotDeviceClaimConflicts = True
                            print('ERROR 35: Device %s belongs to another network' % serial)
                             
        else:
            print('ERROR 37: Failed to fetch organization inventory')
                
    #if unsubmitted VLAN update or device config commands, submit last batch
    success, batchId = queueActionBatch (arg_apikey, orgId, None, True)
    if success:
        if not batchId is None:
            batchIdList.append(batchId)
    else:
        print('ERROR 38: Error submitting action batch')
    
    #Get the status of device claim batches before continuing to next step. Endpoints to update devices are not available
    # before devices are claimed into a network, and will cause an error if called prematurely
    if len(batchIdList) > 0:
        print('Waiting for action batches to complete...')
        success = waitForActionBatchesToComplete(arg_apikey, orgId, batchIdList)
        
        if not success:
            print('ERROR 40: An action batch has failed to execute')
            killScript()
        
    if aNetworkHasDevicesToClaim and not gotDeviceClaimConflicts:
        print('Updating device location information...')
        batchIdList = []
        for net in networks:
            if not (net.location is None or net.location.strip() == ''):
                for serial in net.serials:
                    success, batchId = sendUpdateDeviceLocationToActionBatchQueue(arg_apikey, orgId, net.id, serial, net.location)
                    if success:
                        if not batchId is None:
                            batchIdList.append(batchId)
                    else:
                        print('ERROR 36: Error submitting action batch') 
                        
        #if unsubmitted location updates, submit last batch
        success, batchId = queueActionBatch (arg_apikey, orgId, None, True)
        if success:
            if not batchId is None:
                batchIdList.append(batchId)
        else:
            print('ERROR 39: Error submitting action batch')
        
        #Get the status of any remaining action batches before exiting
        if len(batchIdList) > 0:
            print('Waiting for action batches to complete...')
            waitForActionBatchesToComplete(arg_apikey, orgId, batchIdList)
            
    print('End of script.')

if __name__ == '__main__':
    main(sys.argv[1:])