text: # This is a script to create a new customer account into Dashboard by cloning an existing customer,
  #  claim their initial order into their Organization, add all of their devices into a new network and
  #  bind that network to a template. Switch templates must be eligible for auto-bind (Auto-bind is not valid
  #  unless the switch template has at least one profile and has at most one profile per switch model.)
  #
  # You need to have Python 3 and the Requests module installed. You
  #  can download the module here: https://github.com/kennethreitz/requests
  #  or install it using pip.
  #
  # To run the script, enter:
  #  python deploycustomer.py -k <API k> -s <src> -d <dst> -o <ord> [-c <cfgt> -a <adr> -t <tag> -g <gogl k> -m <mode>]
  #
  # PREREQUISITES:
  #  * Source org must have at least one template configured
  #  * All templates in source org must be type "wireless switch appliance"
  #  * All templates in source org must be eligible for switch autobind
  #  * To use the Google Maps API, you must have a Google API key and the following APIs must be enabled:
  #     1. Google Maps Geocoding API
  #     2. Google Maps Time Zone API
  #
  # PLEASE NOTE:
  #  Organizations created through API can take several hours to be synced across the different nodes/shards
  #   that dashboard is segmented into. This means there may be latency in the orgs appearing in the MSP
  #   portal, depending on which shard you access the portal from. If you need to make changes through 
  #   the dashboard GUI to organizations created through this script immediately after creation, please first
  #   navigate to the source organization used for cloning and then the MSP portal, so that you access the 
  #   MSP portal instance running on that node.
  #
  # To make script chaining easier, all lines containing informational messages to the user
  #  start with the character @
  
  import sys, getopt, requests, json, time, re, datetime
  
  class c_devicedata:
      def __init__(self):
          self.serial     = ''
          self.model      = ''
  #end class   
  
  #Used for time.sleep(API_EXEC_DELAY). Delay added to avoid hitting dashboard API max request rate
  API_EXEC_DELAY = 0.21
  
  def printusertext(p_message):
      #prints a line of text that is meant for the user to read
      #do not process these lines when chaining scripts
      print('@ %s' % p_message)
  
  def printhelp():
      #prints help text
  
      printusertext('Script to automatically create a new customer account and set initial config.')
      printusertext('')
      printusertext('Usage:')
      printusertext('python deploycustomer.py -k <API k> -s <src> -d <dst> -o <ord> [-c <cfgt> -a <adr> -t <tag> -g <gogl k> -m <mode>]')
      printusertext('')
      printusertext('Mandatory arguments:')
      printusertext(' * -k <API k> : Your Meraki Dashboard API key')
      printusertext(' * -s <src>   : Source organization name (the one to be cloned)')
      printusertext(' * -d <dst>   : Destination organization name (new customer)')
      printusertext(' * -o <ord>   : Space separated list of order numbers and/or serial numbers to be claimed')
      printusertext('Optional arguments:')
      printusertext(' * -c <cfgt>  : Name of configuration template to be used')
      printusertext(' * -a <adr>   : Street address of customer headquarters')
      printusertext(' * -t <tag>   : Network tags to apply (for custom admin access)')
      printusertext(' * -g <gogl k>: Google API key for dynamic timezone lookups')
      printusertext(' * -m <mode>  : Resolve conflicts if destination org name already exists. Valid options:')
      printusertext('       -m fail_on_error  : Stop script if conflict detected (default mode)')
      printusertext('       -m add_timestamp  : Create new unique org name by adding timestamp')
      printusertext('       -m modify_existing: Ignore conflict and use existing org as destination')
      printusertext('')
      printusertext('Example:')
      printusertext(' python deploycustomer.py -k 1234 -s TemplateOrg -d "Tasty Bread Inc" -o 4567')
      printusertext('')
      printusertext('Read script comments for source org prerequisites.')
      printusertext('')
      printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.')
      
  def getorgid(p_apikey, p_orgname):
      #looks up org id for a specific org name
      #on failure returns 'null'
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 01: Unable to contact Meraki cloud')
          sys.exit(2)
      
      if r.status_code != requests.codes.ok:
          return 'null'
      
      rjson = r.json()
      
      for record in rjson:
          if record['name'] == p_orgname:
              return record['id']
      return('null')
      
  def getshardhost(p_apikey, p_orgid):
      #patch
      return("api-mp.meraki.com")
      
  def getnwid(p_apikey, p_shardhost, p_orgid, p_nwname):
      #looks up network id for a network name
      #on failure returns 'null'
  
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 03: Unable to contact Meraki cloud')
          sys.exit(2)
          
      if r.status_code != requests.codes.ok:
          return 'null'
      
      rjson = r.json()
      
      for record in rjson:
          if record['name'] == p_nwname:
              return record['id']
      return('null') 
      
  def createnw(p_apikey, p_shardhost, p_dstorg, p_nwdata):
      #creates network if one does not already exist with the same name
      
      #check if network exists
      getnwresult = getnwid(p_apikey, p_shardhost, p_dstorg, p_nwdata['name'])
      if getnwresult != 'null':
          printusertext('WARNING: Skipping network "%s" (Already exists)' % p_nwdata['name'])
          return('null')
      
      if p_nwdata['type'] == 'combined':
          #find actual device types
          nwtype = 'wireless switch appliance'
      else:
          nwtype = p_nwdata['type']
      if nwtype != 'systems manager':
          time.sleep(API_EXEC_DELAY)
          try:
              r = requests.post('https://%s/api/v0/organizations/%s/networks' % (p_shardhost, p_dstorg), data=json.dumps({'timeZone': p_nwdata['timeZone'], 'tags': p_nwdata['tags'], 'name': p_nwdata['name'], 'organizationId': p_dstorg, 'type': nwtype}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
          except:
              printusertext('ERROR 04: Unable to contact Meraki cloud')
              sys.exit(2)
      else:
          printusertext('WARNING: Skipping network "%s" (Cannot create SM networks)' % p_nwdata['name'])
          return('null')
          
      return('ok')
      
  def updatenw(p_apikey, p_shardhost, p_nwid, p_field, p_value):
      #updates network data    
          
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.put('https://%s/api/v0/networks/%s' % (p_shardhost, p_nwid), data=json.dumps({p_field: p_value}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 05: Unable to contact Meraki cloud')
          sys.exit(2)
              
      if r.status_code != requests.codes.ok:
          return ('null')
      
      return('ok')
      
  def gettemplateid(p_apikey, p_shardhost, p_orgid, p_tname):
      #looks up config template id for a config template name
      #on failure returns 'null'
  
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/organizations/%s/configTemplates' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 06: Unable to contact Meraki cloud')
          sys.exit(2)
      
      if r.status_code != requests.codes.ok:
          return 'null'
      
      rjson = r.json()
      
      for record in rjson:
          if record['name'] == p_tname:
              return record['id']
      return('null') 
      
  def getrandomtemplateid(p_apikey, p_shardhost, p_orgid):
      #returns a template ID that belongs to target org
          
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/organizations/%s/configTemplates' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 07: Unable to contact Meraki cloud')
          sys.exit(2)
      
      if r.status_code != requests.codes.ok:
          return 'null'
          
      rjson = r.json()
          
      for record in rjson:
          return record['id']
      
      return ('null')
      
  def getnwssids(p_apikey, p_shardhost, p_nwid):
      #returns all SSIDs in network or template
          
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/networks/%s/ssids' % (p_shardhost, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 08: Unable to contact Meraki cloud')
          sys.exit(2)
      
      returnvalue = []
      if r.status_code != requests.codes.ok:
          returnvalue.append({'number':0, 'name':'getnwssids_FlagOperationFailed'})
          return(returnvalue)
      
      rjson = r.json()
      
      return(rjson)
      
  def updatessidname(p_apikey, p_shardhost, p_nwid, p_ssidnumber, p_name):
      #Modifies SSID names in a template
          
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.put('https://%s/api/v0/networks/%s/ssids/%d' % (p_shardhost, p_nwid, p_ssidnumber), data=json.dumps({'name':p_name}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 09: Unable to contact Meraki cloud')
          sys.exit(2)
              
      if r.status_code != requests.codes.ok:
          return ('null')
      
      return('ok')
      
  def bindnw(p_apikey, p_shardhost, p_nwid, p_templateid, p_autobind):
      #binds a network to a template
      
      if p_autobind:
          autobindvalue = 'true'
      else:
          autobindvalue = 'false'
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/networks/%s/bind' % (p_shardhost, p_nwid), data=json.dumps({'configTemplateId': p_templateid, 'autoBind': autobindvalue}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 10: Unable to contact Meraki cloud')
          sys.exit(2)
          
      if r.status_code != requests.codes.ok:
          return 'null'
          
      return('ok')
      
  def claimdeviceorg(p_apikey, p_shardhost, p_orgid, p_device):
      #claims a device into an org without adding to a network
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/organizations/%s/claim' % (p_shardhost, p_orgid), data=json.dumps({'serial': p_device}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 11: Unable to contact Meraki cloud')
          sys.exit(2)
      
      return(0)
      
  def claimlicenseorg(p_apikey, p_shardhost, p_orgid, p_licensekey):
      #claims a license key into an org
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/organizations/%s/claim' % (p_shardhost, p_orgid), data=json.dumps({'licenseKey': p_licensekey, 'licenseMode': 'addDevices'}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 12: Unable to contact Meraki cloud')
          sys.exit(2)
      
      return(0)
      
  def claimorderorg(p_apikey, p_shardhost, p_orgid, p_ordernum):
      #claims a device into an org without adding to a network
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/organizations/%s/claim' % (p_shardhost, p_orgid), data=json.dumps({'order': p_ordernum, 'licenseMode': 'addDevices'}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 13: Unable to contact Meraki cloud')
          sys.exit(2)
      
      return(0)
      
  def claimdevice(p_apikey, p_shardhost, p_nwid, p_device):
      #claims a device into a network
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/networks/%s/devices/claim' % (p_shardhost, p_nwid), data=json.dumps({'serial': p_device}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 14: Unable to contact Meraki cloud')
          sys.exit(2)
      
      return(0)
      
  def getdeviceinfo(p_apikey, p_shardhost, p_nwid, p_serial):
      #returns info for a single device
      #on failure returns lone device record, with serial number 'null'
  
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/networks/%s/devices/%s' % (p_shardhost, p_nwid, p_serial), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 15: Unable to contact Meraki cloud')
          sys.exit(2)
      
      returnvalue = []
      if r.status_code != requests.codes.ok:
          returnvalue = {'serial':'null', 'model':'null'}
          return(returnvalue)
      
      rjson = r.json()
      
      return(rjson) 
      
  def setdevicedata(p_apikey, p_shardhost, p_nwid, p_device, p_field, p_value, p_movemarker):
      #modifies value of device record. Returns the new value
      #on failure returns one device record, with all values 'null'
      #p_movemarker is boolean: True/False
      
      movevalue = "false"
      if p_movemarker:
          movevalue = "true"
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.put('https://%s/api/v0/networks/%s/devices/%s' % (p_shardhost, p_nwid, p_device), data=json.dumps({p_field: p_value, 'moveMapMarker': movevalue}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 16: Unable to contact Meraki cloud')
          sys.exit(2)
              
      if r.status_code != requests.codes.ok:
          return ('null')
      
      return('ok')
  
  def getorgdeviceinfo (p_apikey, p_shardhost, p_orgid, p_device):
      #gets basic device info from org inventory. device does not need to be part of a network
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/organizations/%s/inventory' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 17: Unable to contact Meraki cloud')
          sys.exit(2)
      
      returnvalue = {}
      if r.status_code != requests.codes.ok:
          returnvalue = {'serial':'null', 'model':'null'}
          return(returnvalue)
      
      rjson = r.json()
      
      foundserial = False
      for record in rjson:
          if record['serial'] == p_device:
              foundserial = True
              returnvalue = {'mac': record['mac'], 'serial': record['serial'], 'networkId': record['networkId'], 'model': record['model'], 'claimedAt': record['claimedAt'], 'publicIp': record['publicIp']}
                  
      if not foundserial:
          returnvalue = {'serial':'null', 'model':'null'}
      return(returnvalue) 
      
  def getorginventory(p_apikey, p_shardhost, p_orgid):
      #returns full org inventory
  
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.get('https://%s/api/v0/organizations/%s/inventory' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 18: Unable to contact Meraki cloud')
          sys.exit(2)
      
      returnvalue = []
      if r.status_code != requests.codes.ok:
          return(returnvalue) #return empty table
      
      rjson = r.json()
      
      return (rjson)
      
  def clonecreateorg(p_apikey, p_shardhost, p_srcorgid, p_dstorgname):
      #Creates a new organization by cloning an existing one
      
      time.sleep(API_EXEC_DELAY)
      try:
          r = requests.post('https://%s/api/v0/organizations/%s/clone' % (p_shardhost, p_srcorgid), data=json.dumps({'name': p_dstorgname}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'})
      except:
          printusertext('ERROR 19: Unable to contact Meraki cloud')
          sys.exit(2)
              
      return (0)
      
  def getgoogletimezone(p_googlekey, p_address):
      #returns the timezone associated to a specified address by using Google Maps APIs
      try:
          r = requests.get('https://maps.googleapis.com/maps/api/geocode/json?address=%s&key=%s' % (p_address, p_googlekey) )
      except:
          printusertext('WARNING: Unable to contact Google cloud')
          return('null')
          
      rjson = r.json()
      
      if rjson['status'] != 'OK':
          return('null')
  
      glatitude  = rjson['results'][0]['geometry']['location']['lat']
      glongitude = rjson['results'][0]['geometry']['location']['lng']
      
      try:
          s = requests.get('https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s&timestamp=%f&key=%s' % (glatitude, glongitude, time.time(), p_googlekey) )
      except:
          printusertext('WARNING: Unable to contact Google cloud')
          return('null')
  
      sjson = s.json()
      
      if sjson['status'] == 'OK':
          return(sjson['timeZoneId'])
  
      return('null')
      
  def main(argv):
      #set default values for command line arguments
      arg_apikey     = ''
      arg_srcorg     = ''
      arg_dstorg     = ''
      arg_orderstr   = ''
      arg_template   = ''
      arg_address    = ''
      arg_nwtags     = ''
      arg_googlekey  = ''
      arg_modeignore = ''
          
      #get command line arguments
      try:
          opts, args = getopt.getopt(argv, 'hk:s:d:o:c:a:t:g:m:')
      except getopt.GetoptError:
          printhelp()
          sys.exit(2)
      
      for opt, arg in opts:
          if   opt == '-h':
              printhelp()
              sys.exit()
          elif opt == '-k':
              arg_apikey     = arg
          elif opt == '-s':
              arg_srcorg     = arg
          elif opt == '-d':
              arg_dstorg     = arg
          elif opt == '-o':
              arg_orderstr   = arg
          elif opt == '-c':
              arg_template   = arg
          elif opt == '-a':
              arg_address    = arg
          elif opt == '-t':
              arg_nwtags     = arg
          elif opt == '-g':
              arg_googlekey  = arg
          elif opt == '-m':
              arg_modeignore = arg
                        
      #check if all parameters are required parameters have been given
      if arg_apikey == '' or arg_srcorg == '' or arg_dstorg == '' or arg_orderstr == '':
          printhelp()
          sys.exit(2)
      
      #set flags:
      mode_notemplate  = False
      if arg_template == '':
          mode_notemplate  = True
          
      mode_noaddress   = False
      if arg_address == '':
          mode_noaddress   = True
          
      flag_mode_arg_invalid = True
      mode_fail_on_error    = True
      mode_add_timestamp    = False
      mode_modify_existing  = False
      if   arg_modeignore == '':
          flag_mode_arg_invalid = False
      elif arg_modeignore == 'fail_on_error':
          flag_mode_arg_invalid = False
      elif arg_modeignore == 'add_timestamp':
          flag_mode_arg_invalid = False
          mode_add_timestamp    = True
      elif arg_modeignore == 'modify_existing':
          flag_mode_arg_invalid = False
          mode_modify_existing  = True
          
      if flag_mode_arg_invalid:   
          printusertext('ERROR 33: Argument -m <mode> is invalid')
          sys.exit(2)
          
      #get source organization id corresponding to org name provided by user
      #source org must exist
      srcorgid = getorgid(arg_apikey, arg_srcorg)
      if srcorgid == 'null':
          printusertext('ERROR 20: Fetching source organization ID failed')
          sys.exit(2)
          
      #clean up suspicious characters from destination org name
      pattern = re.compile('([^\s\w]|_)+')
      dstorgname = pattern.sub('', arg_dstorg)
      dstorgname = dstorgname.strip()
      MAX_RENAME_TRIES = 10
      #get destination organization id corresponding to org name provided by user
      #destination org must NOT exist
      dstorgid = getorgid(arg_apikey, dstorgname)
      if dstorgid != 'null':
          if mode_add_timestamp:
              for i in range (0, MAX_RENAME_TRIES):
                  time.sleep(2)
                  timestamp = ' {:%Y%m%d %H%M%S}'.format(datetime.datetime.now())
                  dstorgname = arg_dstorg + timestamp
                  dstorgid = getorgid(arg_apikey, dstorgname)
                  if dstorgid == 'null':
                      break
              if i == MAX_RENAME_TRIES:
                  printusertext('ERROR 21: Unable to assign unique destination org name')
                  sys.exit(2)    
          elif mode_modify_existing:
              printusertext('INFO: Modifying existing destination org')
          else:
              #default: fail on error
              printusertext('ERROR 22: Destination org name is already taken (%s)' % dstorgname)
              sys.exit(2)
          
      printusertext('INFO: Destination org name is "%s"' % dstorgname)
      
      #clone org based on source
      if not mode_modify_existing:
          clonecreateorg(arg_apikey, 'dashboard.meraki.com', srcorgid, dstorgname)
      dstorgid = getorgid(arg_apikey, dstorgname)
      if dstorgid == 'null':
          printusertext('ERROR 23: Unable to create destination org')
          sys.exit(2)
                  
      #get shard URL where destination org is stored
      #this call sometimes fails. implementing a try-verify-wait-repeat loop
      MAX_SHARD_RESOLVE_TRIES = 10
      flag_unabletoresolveshard = True
      
      for i in range (0, MAX_SHARD_RESOLVE_TRIES):
          
          dstshardhost = getshardhost(arg_apikey, dstorgid)
          if dstshardhost == 'null':
              time.sleep(API_EXEC_DELAY*(i+1))
          else:
              flag_unabletoresolveshard = False
              break
      
      if flag_unabletoresolveshard:
          printusertext('ERROR 24: Fetching Meraki cloud shard FQDN failed')
          sys.exit(2)
               
      #identify serials, orders and claim accordingly
      printusertext('INFO: Claiming orders and serial numbers')
      
      #get serial numbers from parameter -s
      claimlist = arg_orderstr.split(" ")
      
      for i in range (0, len(claimlist) ):
          if len(claimlist[i]) > 4: #prevent next line from crashing if invalid argument
              if claimlist[i][4] == '-': #string is a device serial or license
                  claimdeviceorg(arg_apikey, dstshardhost, dstorgid, claimlist[i])
          
                  #check if device has been claimed successfully
                  deviceinfo = getorgdeviceinfo (arg_apikey, dstshardhost, dstorgid, claimlist[i])
                  if deviceinfo['serial'] == 'null':
                      printusertext('INFO: Unable to claim %s as a device' % claimlist[i])
                      claimlicenseorg(arg_apikey, dstshardhost, dstorgid, claimlist[i])
              else: #string is an order number
                  claimorderorg(arg_apikey, dstshardhost, dstorgid, claimlist[i])
          else:
              printusertext('WARNING: Serial number %s is not valid' % claimlist[i])
              
      #get inventory
      orginventory = getorginventory(arg_apikey, dstshardhost, dstorgid)
          
      inventorylen = len(orginventory)
      if inventorylen == 0:
          printusertext('ERROR 25: Destination org does not contain any devices')
          sys.exit(2)
          
      #from inventory pick all LAN devices (MS, MR) and the largest MX/Z. leave the rest be
      devicetypes = {'mx': False, 'ms': False, 'mr': False}
      devices = []
      largestmx = c_devicedata()
      largestmx.model = '0'
      for orgdevice in orginventory:
          if orgdevice['model'][:2] == 'MX' or orgdevice['model'][:1] == 'Z':
              devicetypes['mx'] = True
              if int(re.search(r'\d+', orgdevice['model']).group()) > int(re.search(r'\d+', largestmx.model).group()):
                  largestmx.serial = orgdevice['serial']
                  largestmx.model  = orgdevice['model']
          elif orgdevice['model'][:2] == 'MS':
              devicetypes['ms'] = True
              devices.append(c_devicedata())
              devices[len(devices)-1].serial = orgdevice['serial']
              devices[len(devices)-1].model  = orgdevice['model']
          elif orgdevice['model'][:2] == 'MR':
              devicetypes['mr'] = True
              devices.append(c_devicedata())
              devices[len(devices)-1].serial = orgdevice['serial']
              devices[len(devices)-1].model  = orgdevice['model']
              
      #if found a suitable MX, append it to the end of the devices list to optimize claiming code
      if largestmx.model != '0':
          devices.append(c_devicedata())
          devices[len(devices)-1].serial = largestmx.serial
          devices[len(devices)-1].model  = largestmx.model
              
      #build network type string for network creation
      nwtypestring = ''
      if devicetypes['mr']:
          nwtypestring += 'wireless '
      if devicetypes['ms']:
          nwtypestring += 'switch '
      if devicetypes['mx']:
          nwtypestring += 'appliance'
      nwtypestring = nwtypestring.strip()
      
      #create correct network type as HQ network
      printusertext('INFO: Creating headquarters network')    
          
      #compile parameters to create network
      #timeZone will be overwritten by template
      hqnwname = arg_dstorg + ' HQ'
      nwparams = {'name': hqnwname, 'timeZone': 'Europe/Helsinki', 'tags': arg_nwtags, 'organizationId': dstorgid, 'type': nwtypestring}
                  
      #create network and get its ID
      createstatus = createnw (arg_apikey, dstshardhost, dstorgid, nwparams)
      if createstatus == 'null' and not mode_modify_existing:
          printusertext('ERROR 26: Unable to create network')
          sys.exit(2)
      nwid = getnwid(arg_apikey, dstshardhost, dstorgid, hqnwname)
      if nwid == 'null':
          printusertext('ERROR 27: Unable to get ID for new network')
          sys.exit(2)    
      
      if arg_template != '':
          #get template ID for template name argument
          templateid = gettemplateid(arg_apikey, dstshardhost, dstorgid, arg_template)
          if templateid == 'null':
              printusertext('ERROR 28: Unable to find template: ' + arg_template)
              sys.exit(2)  
      else:    
          #if argument omitted just select the first one
          templateid = getrandomtemplateid(arg_apikey, dstshardhost, dstorgid)
          if templateid == 'null':
              printusertext('ERROR 29: Unable to find a config template')
              sys.exit(2)  
              
      #claim into HQ network the largest MX, all switches, all wireless
      printusertext('INFO: Adding devices to network')
      
      for device in devices:
          #claim device into newly created network
          claimdevice(arg_apikey, dstshardhost, nwid, device.serial)
      
          #check if device has been claimed successfully
          deviceinfo = getdeviceinfo(arg_apikey, dstshardhost, nwid, device.serial)
          if deviceinfo['serial'] == 'null':
              printusertext('ERROR 30: Claiming or moving device unsuccessful')
              sys.exit(2)
                 
      printusertext('INFO: Editing template and binding network')
      
      #Rewrite template SSID names
      #get template SSIDs
      templatessids = getnwssids(arg_apikey, dstshardhost, templateid)
      if templatessids[0] == 'getnwssids_FlagOperationFailed':
          printusertext('ERROR 31: Unable get template SSIDs')
          sys.exit(2)        
      #find the ones that are enabled and rewrite as needed
      ssidname = []
      for ssid in templatessids:
          if ssid['enabled'] == True:
              ssidname = "%s %s" % (dstorgname, ssid['name'])
              updatessidname(arg_apikey, dstshardhost, templateid, ssid['number'], ssidname)
                              
      #bind network to template. If switches in template, attempt to autobind them
      bindstatus = bindnw(arg_apikey, dstshardhost, nwid, templateid, devicetypes['ms'])
      if bindstatus == 'null' and not mode_modify_existing:
          printusertext('ERROR 32: Unable to bind network to template')
          sys.exit(2)
                             
      # THE REST IS BEST EFFORT STUFF. MOVED TO END OF SCRIPT SINCE THEIR FAILURE IS NOT CRITICAL
      
      printusertext('INFO: Setting device hostnames and locations')
      
      MAX_ADDRESS_SET_TRIES = 10 #how many times to try setting device address before failing
  
      for device in devices:
          #set device hostname
          ### DEVICE HOSTNAME IS DEFINED AS "<MODEL>_<SERIAL>". MODIFY LINE BELOW TO CHANGE ###
          hostname = deviceinfo['model'] + '_' + device.serial
          setdevicedata(arg_apikey, dstshardhost, nwid, device.serial, 'name', hostname, False)
          
          #if street address is given as a parameter, set device location
          #this API call does not always go through, so it is implemented in a "try-verify-wait" loop
          flag_unabletosetaddress = True
          if arg_address != 'null':
              for i in range (0, MAX_ADDRESS_SET_TRIES):
                  setdevicedata(arg_apikey, dstshardhost, nwid, device.serial, 'address', arg_address, True)
                  #check if device address has been set correctly
                  deviceinfo = getdeviceinfo(arg_apikey, dstshardhost, nwid, device.serial)
                  if deviceinfo['serial'] == 'null':
                      time.sleep(API_EXEC_DELAY*(i+1))
                  elif deviceinfo['address'] != '':
                      flag_unabletosetaddress = False
                      break
                  else:
                      time.sleep(API_EXEC_DELAY*(i+1))
              if flag_unabletosetaddress:
                  printusertext('WARNING: Unable to set address for device %s' % device.serial)
                             
      #attempt to override template timezone by fetching the right one from Google API    
      flag_unabletosettime = True
      if arg_googlekey != '' and arg_address != '':
          printusertext('INFO: Setting time zone')
          gtimezone = getgoogletimezone(arg_googlekey, arg_address)
          if gtimezone != 'null':
              udstatus = updatenw(arg_apikey, dstshardhost, nwid, 'timeZone', gtimezone)
              if udstatus == 'ok':
                  flag_unabletosettime = False
          if flag_unabletosettime:
              printusertext('WARNING: Unable to set time zone using Google Maps API')
                  
      printusertext('INFO: End of script.')
              
  if __name__ == '__main__':
      main(sys.argv[1:])