├── switchport_auditing ├── config.json └── switchport_audit.py ├── topusers ├── README.md ├── templates │ └── index.html └── topusers.py ├── LICENSE.md ├── Installing Python on Windows.txt ├── migration_init_file.txt ├── organization_id.py ├── README.md ├── invlist.py ├── listip.py ├── copynetworks.py ├── setlocation_legacy.py ├── movedevices.py ├── copyswitchcfg.py ├── deployappliance.py ├── copymxvlans.py ├── clientcount.py ├── setlocation.py ├── deploydevices.py └── migratecomware.py /switchport_auditing/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlan": 609 3 | } -------------------------------------------------------------------------------- /topusers/README.md: -------------------------------------------------------------------------------- 1 | You will need both the script itself and the "templates" folder containing index.html for the script to run. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mihail Papazoglou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Installing Python on Windows.txt: -------------------------------------------------------------------------------- 1 | Download Python 3 for Windows: https://www.python.org/downloads/windows/ 2 | If you plan to use Python for SNMP requests pick version 3.5.3. PySNMP will not install easily on Python 3.6 3 | 4 | Make sure your "python" and "python/scripts" folders are in %PATH%. For example if installing to "C:\python", edit %PATH% to include: 5 | c:\python 6 | c:\python\scripts 7 | 8 | Useful modules (install these): 9 | Requests: send/receive information to/from REST APIs. To install, run this command: pip install requests 10 | PySNMP: interact with SNMP devices. To install, run this command: pip install pysnmp 11 | Paramiko: interact with devices using SSH. Used in migration scripts. 12 | Installation info here: http://www.paramiko.org/installing.html 13 | 14 | Python Meraki API library and examples: 15 | https://github.com/meraki/provisioning-lib 16 | 17 | A great Python editor can be found here: 18 | https://notepad-plus-plus.org/ 19 | 20 | Google Geocoding API, how it works, how to enable: 21 | https://developers.google.com/maps/documentation/geocoding/intro 22 | 23 | Visual C++ Build Tools needed to install PyCrypto and PySNMP (v14.0 aka 2015): 24 | http://landinghub.visualstudio.com/visual-cpp-build-tools 25 | 26 | You should consider replacing PyCrypto (installed along with PySNMP) with PyCryptodome, which is considered more secure. I haven't tried this yet. 27 | -------------------------------------------------------------------------------- /migration_init_file.txt: -------------------------------------------------------------------------------- 1 | #This is a sample Comware migration initialization file with device mappings for migratecomware.py 2 | # 3 | #Syntax: 4 | # * Blank lines and lines only containing whitespace will be ignored. 5 | # * Use lines beginning with # as comments. These lines will be ignored. 6 | # * Use "net=Network_name" to define a network. A network definition line must exist before any 7 | # device definition lines. 8 | # * Device definition lines. These lines define the IP address of the original Comware switch, 9 | # the Meraki MS switch serial number the configuration will be transferred to and optionally 10 | # a SSH username and password to log into the Comware device. If username and password are 11 | # omitted, default credentials will be used. These lines can have four forms: 12 | # 13 | # 14 | # file 15 | # 16 | #Examples of net definition and device definition lines, commented out: 17 | # 18 | #net=Migrated headquarters network 19 | #10.1.1.20 AAAA-BBBB-CCCC admin admin 20 | #10.1.1.21 AAAA-BBBB-DDDD admin@system admin123 21 | #file myconfig.cfg BBBB-CCCC-DDDD 22 | # 23 | #net=Migrated branch network 24 | #192.168.10.10 AAAA-BBBB-EEEE 25 | 26 | net=New migrated network 27 | 10.54.25.13 AAAA-BBBB-CCCC cisco@system cisco 28 | 1.1.1.1 sadd-235u-hf92 29 | file myfile.txt kasa-832r-dajs 30 | 31 | net=Not Empty net 32 | 3.3.3.3 sajk-jfaa-afff 33 | 34 | net=Second migrated network 35 | 10.54.25.12 AAAA-BBBB-DDDD 36 | -------------------------------------------------------------------------------- /topusers/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MX usage report 7 | 15 | 16 | 17 |

Scan for new appliances

18 |

Network top users report

19 | 20 |
21 |

22 | {{ form.netname.label }} 23 | {{ form.netname }} 24 |

25 |

{{ form.submit() }}

26 |
27 | 28 | {% with messages = get_flashed_messages() %} 29 | {% if messages %} 30 |
    31 | {% for message in messages %} 32 |
  • {{ message }}
  • 33 | {% endfor %} 34 |
35 | {% endif %} 36 | {% endwith %} 37 | 38 | {% if output %} 39 |


Last {{ tshort }} minutes:

40 | 41 | {% for line in output.short %} 42 | 43 | {% for item in line %} 44 | 47 | {% endfor %} 48 | 49 | {% endfor %} 50 |
Total usage kBDownload kBUpload kBDescriptionDHCP hostnameMAC addressIP addressVLAN
45 | {{ item }} 46 |
51 |


Last {{ tmid }} minutes:

52 | 53 | {% for line in output.mid %} 54 | 55 | {% for item in line %} 56 | 59 | {% endfor %} 60 | 61 | {% endfor %} 62 |
Total usage kBDownload kBUpload kBDescriptionDHCP hostnameMAC addressIP addressVLAN
57 | {{ item }} 58 |
63 |


Last {{ tlong }} minutes:

64 | 65 | {% for line in output.long %} 66 | 67 | {% for item in line %} 68 | 71 | {% endfor %} 72 | 73 | {% endfor %} 74 |
Total usage kBDownload kBUpload kBDescriptionDHCP hostnameMAC addressIP addressVLAN
69 | {{ item }} 70 |
75 |
76 |

Report completed at {{ output.timestamp }}.

77 | {% endif %} 78 | 79 | 80 | -------------------------------------------------------------------------------- /organization_id.py: -------------------------------------------------------------------------------- 1 | read_me = '''This is a Python 3 script to print your organizationId for a given name. Usage: 2 | python organization_id.py -k -o 3 | ''' 4 | 5 | import sys, getopt, requests, time, datetime 6 | 7 | #SECTION: GLOBAL VARIABLES: MODIFY TO CHANGE SCRIPT BEHAVIOUR 8 | 9 | API_EXEC_DELAY = 0.21 #Used in merakiRequestThrottler() to avoid hitting dashboard API max request rate 10 | 11 | #connect and read timeouts for the Requests module in seconds 12 | REQUESTS_CONNECT_TIMEOUT = 90 13 | REQUESTS_READ_TIMEOUT = 90 14 | 15 | #SECTION: GLOBAL VARIABLES AND CLASSES: DO NOT MODIFY 16 | 17 | LAST_MERAKI_REQUEST = datetime.datetime.now() #used by merakiRequestThrottler() 18 | API_BASE_URL = 'https://api-mp.meraki.com/api/v0' 19 | API_BASE_URL_MEGA_PROXY = 'https://api-mp.meraki.com/api/v0' 20 | API_BASE_URL_NO_MEGA = 'https://api.meraki.com/api/v0' 21 | 22 | 23 | ### SECTION: General functions 24 | 25 | 26 | def printHelpAndExit(): 27 | print(read_me) 28 | sys.exit(0) 29 | 30 | 31 | ### SECTION: Functions for interacting with Dashboard 32 | 33 | 34 | def merakiRequestThrottler(): 35 | #prevents hitting max request rate shaper of the Meraki Dashboard API 36 | global LAST_MERAKI_REQUEST 37 | 38 | if (datetime.datetime.now()-LAST_MERAKI_REQUEST).total_seconds() < (API_EXEC_DELAY): 39 | time.sleep(API_EXEC_DELAY) 40 | 41 | LAST_MERAKI_REQUEST = datetime.datetime.now() 42 | return 43 | 44 | 45 | def getOrgId(p_apiKey, p_orgName): 46 | #returns the organizations' list for a specified admin, with filters applied 47 | 48 | merakiRequestThrottler() 49 | try: 50 | 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) ) 51 | except: 52 | return None 53 | 54 | if r.status_code != requests.codes.ok: 55 | return None 56 | 57 | rjson = r.json() 58 | 59 | for org in rjson: 60 | if org['name'] == p_orgName: 61 | return org['id'] 62 | 63 | return None 64 | 65 | ### SECTION: Main function 66 | 67 | 68 | def main(argv): 69 | global API_BASE_URL 70 | 71 | #set default values for command line arguments 72 | arg_apikey = None 73 | arg_orgname = None 74 | 75 | try: 76 | opts, args = getopt.getopt(argv, 'hk:o:') 77 | except getopt.GetoptError: 78 | printHelpAndExit() 79 | 80 | for opt, arg in opts: 81 | if opt == '-h': 82 | printHelpAndExit() 83 | elif opt == '-k': 84 | arg_apikey = arg 85 | elif opt == '-o': 86 | arg_orgname = arg 87 | 88 | #check if all required parameters have been given 89 | if arg_apikey is None or arg_orgname is None: 90 | printHelpAndExit() 91 | 92 | API_BASE_URL = API_BASE_URL_MEGA_PROXY 93 | 94 | #get organization id corresponding to org name provided by user 95 | orgId = getOrgId(arg_apikey, arg_orgname) 96 | if orgId is None: 97 | print('ERROR 16: Fetching organization id failed') 98 | sys.exit(2) 99 | 100 | print(orgId) 101 | 102 | if __name__ == '__main__': 103 | main(sys.argv[1:]) 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meraki-python 2 | Meraki Dashboard API scripts in Python 3 | -------------------------------------- 4 | 5 | Here you can find Meraki Dashboard API scripts written for Python 3. 6 | 7 | Also have a look at the official Meraki GitHub for more content and updated versions of my older scripts: https://github.com/meraki/automation-scripts 8 | 9 | Files contained in this repository: 10 | ----------------------------------- 11 | 12 | **Installing Python on Windows.txt:** General info for installing Python 3 on Windows 13 | 14 | **copymxvlans.py:** This script can be used to export MX VLAN configuration of a source org to a file and import it to a destination org. The script will look for the exact same network names as they were in the source org. Use copynetworks.py and movedevices.py to migrate networks and devices if needed. 15 | 16 | **copynetworks.py:** Copies networks and their base attributes from one organization to another. Does not move devices over or copy individual device configuration. Combined networks will be copied as "wireless switch appliance". 17 | 18 | **copyswitchcfg.py:** This script can be used to export switchport configuration of a source org to a file and import it to a destination org. The script will look for the exact same network names and device serial numbers, as they were in the source org. Use copynetworks.py and movedevices.py to migrate networks and devices if needed. 19 | 20 | **deployappliance.py:** This script claims a single Security Appliance or Teleworker Gateway into an organization, creates a new network for it and binds that network to an existing template. 21 | 22 | **deploydevices.py:** This script claims multiple devices and licenses into an organization, creates a new network for them and binds that network to an existing template. Initial config, including hostnames and street address/map markers are set for the devices. 23 | 24 | **invlist.py:** Creates a list of all serial numbers and models of devices that are part of a Meraki network for an organization with a given name. Can print to Stdout or file. 25 | 26 | **listip.py:** Almost exactly the same as invlist.py, but also prints the "lanIp" of the device. If the device has no "lanIp", it prints "None" for that field instead. 27 | 28 | **migratecomware.py:** Proof of concept script that migrates legacy switch infrastructure based on Comware (HPE A-series) to Meraki MS switches. Comware switch configurations can be provided as files, or by entering the IP address and SSH credentials of the source device. A valid initialization configuration file must be provided, where source devices are mapped to target Meraki serial numbers. Please see migration_init_file.txt in this repository for an example of such a file. This version of the script only supports Comware-based switches and a limited set of Layer 2 switchport commands. The script could be expanded to cover more commands and other CLI-based switch families. 29 | 30 | **migration_init_file.txt:** Example init config file for migratecomware.py. 31 | 32 | **movedevices.py:** This script that can be used to move all devices from one organization to another. The script will only process devices that are part of a network. The networks of the source org need to exist in the destination org too. Use copynetworks.py if needed to create them. 33 | 34 | **setlocation.py:** Sets the street address and optionally the map marker of all devices in a network or organization. To be more easily clickable, devices will be placed in a spiral around a seed location. There is an option to preserve marker location for MR access points, to avoid breaking wireless map layout. 35 | 36 | **setlocation_legacy.py:** Sets the street address of all devices in a given network to a given value. The intent of this script is to quickly fix address misconfigurations on large networks. The script has been updated from its initial version to use the Google Geocoding API to calculate a reasonable new positions for device map markers. This is a legacy script that is preserved as an example of integrating the Meraki Dashboard API with info extracted from a Google API. Please see setlocation.py for an improved version of the script that does not require a Google API key. 37 | 38 | More info about the scripts can be found inline as comments. 39 | -------------------------------------------------------------------------------- /invlist.py: -------------------------------------------------------------------------------- 1 | # This script prints a list of all in-use devices in an organization 2 | # to sdtout or a file (Devices which are part of a network are considered in-use). 3 | # The fields printed are 'serial' and 'model' separated by a comma (,). 4 | # 5 | # You need to have Python 3 and the Requests module installed. You 6 | # can download the module here: https://github.com/kennethreitz/requests 7 | # or install it using pip. 8 | # 9 | # To run the script, enter: 10 | # python invlist.py -k -o [-f ] 11 | # 12 | # If option -f is not defined, the script will print to stdout. 13 | # 14 | # To make script chaining easier, all lines not containing a 15 | # device record start with the character @ 16 | # 17 | # This file was last modified on 2017-02-23 18 | 19 | import sys, getopt, requests, json 20 | 21 | def printusertext(p_message): 22 | #prints a line of text that is meant for the user to read 23 | #do not process these lines when chaining scripts 24 | print('@ %s' % p_message) 25 | 26 | def printhelp(): 27 | #prints help text 28 | 29 | printusertext("This is a script that prints a list of an organization's devices to sdtout or a file.") 30 | printusertext('') 31 | printusertext('Usage:') 32 | printusertext('python invlist.py -k -o [-f ]') 33 | printusertext('') 34 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 35 | 36 | def getorgid(p_apikey, p_orgname): 37 | #looks up org id for a specific org name 38 | #on failure returns 'null' 39 | 40 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 41 | 42 | if r.status_code != requests.codes.ok: 43 | return 'null' 44 | 45 | rjson = r.json() 46 | 47 | for record in rjson: 48 | if record['name'] == p_orgname: 49 | return record['id'] 50 | return('null') 51 | 52 | def getshardurl(p_apikey, p_orgid): 53 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 54 | # when making API calls with API accounts that can access multiple orgs. 55 | #On failure returns 'null' 56 | 57 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 58 | 59 | if r.status_code != requests.codes.ok: 60 | return 'null' 61 | 62 | rjson = r.json() 63 | 64 | return(rjson['hostname']) 65 | 66 | def getnwlist(p_apikey, p_shardurl, p_orgid): 67 | #returns a list of all networks in an organization 68 | #on failure returns a single record with 'null' name and id 69 | 70 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 71 | 72 | returnvalue = [] 73 | if r.status_code != requests.codes.ok: 74 | returnvalue.append({'name': 'null', 'id': 'null'}) 75 | return(returnvalue) 76 | 77 | return(r.json()) 78 | 79 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 80 | #returns a list of all devices in a network 81 | 82 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 83 | 84 | returnvalue = [] 85 | if r.status_code != requests.codes.ok: 86 | returnvalue.append({'serial': 'null', 'model': 'null'}) 87 | return(returnvalue) 88 | 89 | return(r.json()) 90 | 91 | 92 | def main(argv): 93 | #get command line arguments 94 | arg_apikey = 'null' 95 | arg_orgname = 'null' 96 | arg_filepath = 'null' 97 | 98 | try: 99 | opts, args = getopt.getopt(argv, 'hk:o:f:') 100 | except getopt.GetoptError: 101 | printhelp() 102 | sys.exit(2) 103 | 104 | for opt, arg in opts: 105 | if opt == '-h': 106 | printhelp() 107 | sys.exit() 108 | elif opt == '-k': 109 | arg_apikey = arg 110 | elif opt == '-o': 111 | arg_orgname = arg 112 | elif opt == '-f': 113 | arg_filepath = arg 114 | 115 | if arg_apikey == 'null' or arg_orgname == 'null': 116 | printhelp() 117 | sys.exit(2) 118 | 119 | #get organization id corresponding to org name provided by user 120 | orgid = getorgid(arg_apikey, arg_orgname) 121 | if orgid == 'null': 122 | printusertext('ERROR: Fetching organization failed') 123 | sys.exit(2) 124 | 125 | #get shard URL where Org is stored 126 | shardurl = getshardurl(arg_apikey, orgid) 127 | if shardurl == 'null': 128 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 129 | sys.exit(2) 130 | 131 | #get network list for fetched org id 132 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 133 | 134 | if nwlist[0]['id'] == 'null': 135 | printusertext('ERROR: Fetching network list failed') 136 | sys.exit(2) 137 | 138 | #if user selected to print in file, set flag & open for writing 139 | filemode = False 140 | if arg_filepath != 'null': 141 | try: 142 | f = open(arg_filepath, 'w') 143 | except: 144 | printusertext('ERROR: Unable to open output file for writing') 145 | sys.exit(2) 146 | filemode = True 147 | 148 | devicelist = [] 149 | for nwrecord in nwlist: 150 | #get devices' list 151 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 152 | #append list to file or stdout 153 | if filemode: 154 | for i in range (0, len(devicelist)): 155 | try: 156 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 157 | f.write('%s,%s\n' % (devicelist[i]['serial'], devicelist[i]['model'])) 158 | except: 159 | printusertext('ERROR: Unable to write device info to file') 160 | sys.exit(2) 161 | else: 162 | for i in range (0, len(devicelist)): 163 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 164 | print('%s,%s' % (devicelist[i]['serial'], devicelist[i]['model'])) 165 | 166 | if __name__ == '__main__': 167 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /listip.py: -------------------------------------------------------------------------------- 1 | # This script prints a list of all in-use devices in an organization 2 | # to sdtout or a file (Devices which are part of a network are considered in-use). 3 | # The fields printed are 'serial', 'model' and 'lanIp' separated by a comma (,). 4 | # 5 | # You need to have Python 3 and the Requests module installed. You 6 | # can download the module here: https://github.com/kennethreitz/requests 7 | # or install it using pip. 8 | # 9 | # To run the script, enter: 10 | # python listip.py -k -o [-f ] 11 | # 12 | # If option -f is not defined, the script will print to stdout. 13 | # 14 | # To make script chaining easier, all lines not containing a 15 | # device record start with the character @ 16 | # 17 | # This file was last modified on 2017-03-24 18 | 19 | import sys, getopt, requests, json 20 | 21 | def printusertext(p_message): 22 | #prints a line of text that is meant for the user to read 23 | #do not process these lines when chaining scripts 24 | print('@ %s' % p_message) 25 | 26 | def printhelp(): 27 | #prints help text 28 | 29 | printusertext("This is a script that prints a list of an organization's devices to sdtout or a file.") 30 | printusertext('') 31 | printusertext('Usage:') 32 | printusertext('python invlist.py -k -o [-f ]') 33 | printusertext('') 34 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 35 | 36 | def getorgid(p_apikey, p_orgname): 37 | #looks up org id for a specific org name 38 | #on failure returns 'null' 39 | 40 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 41 | 42 | if r.status_code != requests.codes.ok: 43 | return 'null' 44 | 45 | rjson = r.json() 46 | 47 | for record in rjson: 48 | if record['name'] == p_orgname: 49 | return record['id'] 50 | return('null') 51 | 52 | def getshardurl(p_apikey, p_orgid): 53 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 54 | # when making API calls with API accounts that can access multiple orgs. 55 | #On failure returns 'null' 56 | 57 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 58 | 59 | if r.status_code != requests.codes.ok: 60 | return 'null' 61 | 62 | rjson = r.json() 63 | 64 | return(rjson['hostname']) 65 | 66 | def getnwlist(p_apikey, p_shardurl, p_orgid): 67 | #returns a list of all networks in an organization 68 | #on failure returns a single record with 'null' name and id 69 | 70 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 71 | 72 | returnvalue = [] 73 | if r.status_code != requests.codes.ok: 74 | returnvalue.append({'name': 'null', 'id': 'null'}) 75 | return(returnvalue) 76 | 77 | return(r.json()) 78 | 79 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 80 | #returns a list of all devices in a network 81 | 82 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 83 | 84 | returnvalue = [] 85 | if r.status_code != requests.codes.ok: 86 | returnvalue.append({'serial': 'null', 'model': 'null'}) 87 | return(returnvalue) 88 | 89 | return(r.json()) 90 | 91 | def getnwvlanips(p_apikey, p_shardurl, p_nwid): 92 | #returns MX VLANs for a network 93 | r = requests.get('https://%s/api/v0/networks/%s/vlans' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 94 | 95 | returnvalue = [] 96 | if r.status_code != requests.codes.ok: 97 | returnvalue.append({'id': 'null'}) 98 | return(returnvalue) 99 | 100 | return(r.json()) 101 | 102 | 103 | def main(argv): 104 | #get command line arguments 105 | arg_apikey = 'null' 106 | arg_orgname = 'null' 107 | arg_filepath = 'null' 108 | 109 | try: 110 | opts, args = getopt.getopt(argv, 'hk:o:f:') 111 | except getopt.GetoptError: 112 | printhelp() 113 | sys.exit(2) 114 | 115 | for opt, arg in opts: 116 | if opt == '-h': 117 | printhelp() 118 | sys.exit() 119 | elif opt == '-k': 120 | arg_apikey = arg 121 | elif opt == '-o': 122 | arg_orgname = arg 123 | elif opt == '-f': 124 | arg_filepath = arg 125 | 126 | if arg_apikey == 'null' or arg_orgname == 'null': 127 | printhelp() 128 | sys.exit(2) 129 | 130 | #get organization id corresponding to org name provided by user 131 | orgid = getorgid(arg_apikey, arg_orgname) 132 | if orgid == 'null': 133 | printusertext('ERROR: Fetching organization failed') 134 | sys.exit(2) 135 | 136 | #get shard URL where Org is stored 137 | shardurl = getshardurl(arg_apikey, orgid) 138 | if shardurl == 'null': 139 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 140 | sys.exit(2) 141 | 142 | #get network list for fetched org id 143 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 144 | 145 | if nwlist[0]['id'] == 'null': 146 | printusertext('ERROR: Fetching network list failed') 147 | sys.exit(2) 148 | 149 | #if user selected to print in file, set flag & open for writing 150 | filemode = False 151 | if arg_filepath != 'null': 152 | try: 153 | f = open(arg_filepath, 'w') 154 | except: 155 | printusertext('ERROR: Unable to open output file for writing') 156 | sys.exit(2) 157 | filemode = True 158 | 159 | devicelist = [] 160 | recordstring = [] 161 | vlanips = [] 162 | for nwrecord in nwlist: 163 | #get devices' list 164 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 165 | #append list to file or stdout 166 | for i in range (0, len(devicelist)): 167 | #START: MODIFY THESE LINES TO CHANGE OUTPUT FORMAT 168 | #create string to be printed if filemode, a '\n' will be added later 169 | #use try-except so that code does not crash if lanIp, wan1Ip or wan2Ip are missing 170 | recordstring = devicelist[i]['serial'] + ',' + devicelist[i]['model'] 171 | try: 172 | if (len(devicelist[i]['lanIp']) > 4): 173 | recordstring += ',' + devicelist[i]['lanIp'] 174 | except: 175 | pass 176 | try: 177 | if (len(devicelist[i]['wan1Ip']) > 4): 178 | recordstring += ',' + devicelist[i]['wan1Ip'] 179 | except: 180 | pass 181 | try: 182 | if (len(devicelist[i]['wan2Ip']) > 4): 183 | recordstring += ',' + devicelist[i]['wan2Ip'] 184 | except: 185 | pass 186 | 187 | #if the device is an MX or Z1, LAN interface IPs will be listed under network VLANs 188 | if (devicelist[i]['model'].startswith('MX') or devicelist[i]['model'].startswith('Z1')): 189 | vlanips = getnwvlanips(arg_apikey, shardurl, nwrecord['id']) 190 | if vlanips[0]['id'] != 'null': 191 | for j in range (0, len(vlanips)): 192 | recordstring = recordstring + ',' + vlanips[j]['applianceIp'] 193 | #END: MODIFY THESE LINES TO CHANGE OUTPUT FORMAT 194 | 195 | #print record to file or stdout 196 | if filemode: 197 | recordstring += '\n' 198 | try: 199 | f.write(recordstring) 200 | except: 201 | printusertext('ERROR: Unable to write device info to file') 202 | sys.exit(2) 203 | else: 204 | print(recordstring) 205 | 206 | if __name__ == '__main__': 207 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /copynetworks.py: -------------------------------------------------------------------------------- 1 | # This exports all networks and their base attributes from an organization to a file 2 | # and then imports them to another organization. 3 | # 4 | # You need to have Python 3 and the Requests module installed. You 5 | # can download the module here: https://github.com/kennethreitz/requests 6 | # or install it using pip. 7 | # 8 | # To run the script, enter: 9 | # python copynetworks.py -k [-s ] [-d ] [-f ] 10 | # 11 | # Parameters '-s', '-d' and '-f' are optional, but at least two of them must be given. 12 | # 13 | # ** If '-s' and '-d' are given, data will be copied from src org to dst org 14 | # ** If '-s' and '-f' are given, data will be dumped from src org to file 15 | # ** If '-d' and '-f' are given, data will be imported from file to dst org 16 | # 17 | # To make script chaining easier, all lines containing informational messages to the user 18 | # start with the character @ 19 | # 20 | # This file was last modified on 2017-04-06 21 | 22 | import sys, getopt, requests, json 23 | 24 | def printusertext(p_message): 25 | #prints a line of text that is meant for the user to read 26 | #do not process these lines when chaining scripts 27 | print('@ %s' % p_message) 28 | 29 | def printhelp(): 30 | #prints help text 31 | 32 | printusertext('This is a script that copies networks and their base attributes from a source organization') 33 | printusertext('to another, called the destination organization. Both source, destination org and file ') 34 | printusertext('parameters are optional, but at least two of them must be given.') 35 | printusertext('') 36 | printusertext('Usage:') 37 | printusertext('python copynetworks.py -k [-s ] [-d ] [-f ]') 38 | printusertext('') 39 | printusertext(" ** If '-s' and '-d' are given, data will be copied from src org to dst org") 40 | printusertext(" ** If '-s' and '-f' are given, data will be dumped from src org to file") 41 | printusertext(" ** If '-d' and '-f' are given, data will be imported from file to dst org") 42 | printusertext('') 43 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 44 | 45 | def getorgid(p_apikey, p_orgname): 46 | #looks up org id for a specific org name 47 | #on failure returns 'null' 48 | 49 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 50 | 51 | if r.status_code != requests.codes.ok: 52 | return 'null' 53 | 54 | rjson = r.json() 55 | 56 | 57 | for record in rjson: 58 | if record['name'] == p_orgname: 59 | return record['id'] 60 | return('null') 61 | 62 | def getshardurl(p_apikey, p_orgid): 63 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 64 | # when making API calls with API accounts that can access multiple orgs. 65 | #On failure returns 'null' 66 | 67 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 68 | 69 | if r.status_code != requests.codes.ok: 70 | return 'null' 71 | 72 | rjson = r.json() 73 | 74 | return(rjson['hostname']) 75 | 76 | def getnwlist(p_apikey, p_shardurl, p_orgid): 77 | #returns a list of all networks in an organization 78 | #on failure returns a single record with 'null' name and id 79 | 80 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 81 | 82 | returnvalue = [] 83 | if r.status_code != requests.codes.ok: 84 | returnvalue.append({'name': 'null', 'id': 'null'}) 85 | return(returnvalue) 86 | 87 | return(r.json()) 88 | 89 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 90 | #looks up network id for a network name 91 | #on failure returns 'null' 92 | 93 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 94 | 95 | if r.status_code != requests.codes.ok: 96 | return 'null' 97 | 98 | rjson = r.json() 99 | 100 | for record in rjson: 101 | if record['name'] == p_nwname: 102 | return record['id'] 103 | return('null') 104 | 105 | def createnw (p_apikey, p_shardurl, p_dstorg, p_nwdata): 106 | #creates network if one does not already exist with the same name 107 | 108 | #check if network exists 109 | getnwresult = getnwid(p_apikey, p_shardurl, p_dstorg, p_nwdata['name']) 110 | if getnwresult != 'null': 111 | printusertext('WARNING: Skipping network "%s" (Already exists)' % p_nwdata['name']) 112 | return('null') 113 | 114 | if p_nwdata['type'] == 'combined': 115 | #find actual device types 116 | nwtype = 'wireless switch appliance' 117 | else: 118 | nwtype = p_nwdata['type'] 119 | if nwtype != 'systems manager': 120 | r = requests.post('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, 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'}) 121 | else: 122 | printusertext('WARNING: Skipping network "%s" (Cannot create SM networks)' % p_nwdata['name']) 123 | 124 | return('ok') 125 | 126 | 127 | def main(argv): 128 | #get command line arguments 129 | arg_apikey = 'null' 130 | arg_srcorg = 'null' 131 | arg_dstorg = 'null' 132 | arg_filepath = 'null' 133 | 134 | try: 135 | opts, args = getopt.getopt(argv, 'hk:s:d:f:') 136 | except getopt.GetoptError: 137 | printhelp() 138 | sys.exit(2) 139 | 140 | for opt, arg in opts: 141 | if opt == '-h': 142 | printhelp() 143 | sys.exit() 144 | elif opt == '-k': 145 | arg_apikey = arg 146 | elif opt == '-s': 147 | arg_srcorg = arg 148 | elif opt == '-d': 149 | arg_dstorg = arg 150 | elif opt == '-f': 151 | arg_filepath = arg 152 | 153 | #count how many optional parameters have been given 154 | optionscounter = 0 155 | if arg_srcorg != 'null': 156 | optionscounter += 1 157 | if arg_dstorg != 'null': 158 | optionscounter += 1 159 | if arg_filepath != 'null': 160 | optionscounter += 1 161 | 162 | if arg_apikey == 'null' or optionscounter < 2: 163 | printhelp() 164 | sys.exit(2) 165 | 166 | #get source organization id corresponding to org name provided by user 167 | mode_gotsource = True 168 | if arg_srcorg == 'null': 169 | mode_gotsource = False 170 | else: 171 | srcorgid = getorgid(arg_apikey, arg_srcorg) 172 | if srcorgid == 'null': 173 | printusertext('ERROR: Fetching source organization failed') 174 | sys.exit(2) 175 | #get shard URL where Org is stored 176 | srcshardurl = getshardurl(arg_apikey, srcorgid) 177 | if srcshardurl == 'null': 178 | printusertext('ERROR: Fetching Meraki cloud shard URL for source org failed') 179 | printusertext(' Does it have API access enabled?') 180 | sys.exit(2) 181 | 182 | #get destination organization id corresponding to org name provided by user 183 | mode_gotdestination = True 184 | if arg_dstorg == 'null': 185 | mode_gotdestination = False 186 | else: 187 | dstorgid = getorgid(arg_apikey, arg_dstorg) 188 | if dstorgid == 'null': 189 | printusertext('ERROR: Fetching destination organization failed') 190 | sys.exit(2) 191 | #get shard URL where Org is stored 192 | dstshardurl = getshardurl(arg_apikey, dstorgid) 193 | if dstshardurl == 'null': 194 | printusertext('ERROR: Fetching Meraki cloud shard URL for destination org failed') 195 | printusertext(' Does it have API access enabled?') 196 | sys.exit(2) 197 | 198 | #if user gave a source, fetch networks and their attributes from src org 199 | if mode_gotsource: 200 | nwlist = getnwlist(arg_apikey, srcshardurl, srcorgid) 201 | 202 | if nwlist[0]['id'] == 'null': 203 | printusertext('ERROR: Fetching network list from source org failed') 204 | sys.exit(2) 205 | 206 | #open buffer file for writing 207 | mode_gotfile = True 208 | if arg_filepath == 'null': 209 | mode_gotfile = False 210 | if mode_gotfile: 211 | #if source given, open file for writing (output) 212 | if mode_gotsource: 213 | try: 214 | f = open(arg_filepath, 'w') 215 | except: 216 | printusertext('ERROR: Unable to open file for writing') 217 | sys.exit(2) 218 | #if source omitted, open file for reading (input) 219 | else: 220 | try: 221 | f = open(arg_filepath, 'r') 222 | except: 223 | printusertext('ERROR: Unable to open file for reading') 224 | sys.exit(2) 225 | 226 | #if user gave a source and a file, dump source org networks to file 227 | if mode_gotsource and mode_gotfile: 228 | try: 229 | json.dump(nwlist, f) 230 | except: 231 | printusertext('ERROR: Writing to output file failed') 232 | sys.exit(2) 233 | 234 | #if user did not give source, but gave file, load networks list from file 235 | if not(mode_gotsource) and mode_gotfile: 236 | try: 237 | nwlist = json.load(f) 238 | except: 239 | printusertext('ERROR: Reading from input file failed') 240 | sys.exit(2) 241 | 242 | #if user gave destination org, create networks according to nwlist content 243 | if mode_gotdestination: 244 | i = 0 245 | for i in range (0, len(nwlist)): 246 | createnw (arg_apikey, dstshardurl, dstorgid, nwlist[i]) 247 | 248 | #reached end of script 249 | printusertext('End of script.') 250 | 251 | if __name__ == '__main__': 252 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /setlocation_legacy.py: -------------------------------------------------------------------------------- 1 | # This is a simple script that updates the street address for all 2 | # devices in a network to a value given as a command line parameter. 3 | # 4 | # This is an earlier version of script setlocation.py that was created before the option to move 5 | # map markers when changing the street address of a device via API was introduced. Please refer to 6 | # setlocation.py for a more efficient way to set device location, without the need for a Google API 7 | # key. This script has been preserved as an example of how multiple APIs can be combined. 8 | # 9 | # You need to have Python 3 and the Requests module installed. You 10 | # can download the module here: https://github.com/kennethreitz/requests 11 | # 12 | # To use parameter "-g", you will need to have the Google Geocoding API activated. 13 | # Read more and activate your key here: 14 | # https://developers.google.com/maps/documentation/geocoding/intro 15 | # 16 | # To run the script, enter: 17 | # python setlocation_legacy.py -k [-g ] -o -n -l 18 | # 19 | # Parameter "-g" is optional. If a valid Google API key is provided, the script will attempt to geocode the address 20 | # and move the map markers close to the street address defined in parameter "-l". 21 | # 22 | # This file was last modified on 2017-07-04 23 | 24 | 25 | 26 | import sys, getopt, requests, json 27 | 28 | def printhelp(): 29 | #prints help text 30 | 31 | print('This script updates the street address of every device in a network to a value given as a parameter.') 32 | print('Map markers will also be moved close to this address if a Google API key is given (parameter "-g").') 33 | print('') 34 | print('Script syntax:') 35 | print('python setlocation_legacy.py -k [-g ] -o -n -l ') 36 | print('') 37 | print('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive') 38 | 39 | def getgooglecoordinates(p_googlekey, p_address): 40 | #looks up for the Geocoordinates of an address 41 | # in the Google Geolocation API 42 | 43 | r = requests.get('https://maps.googleapis.com/maps/api/geocode/json?address=%s&key=%s' % (p_address, p_googlekey) ) 44 | 45 | rjson = r.json() 46 | 47 | returnvalue = {} 48 | if (r.status_code != requests.codes.ok) or (rjson['status'] != 'OK'): 49 | returnvalue['status'] = 'null' 50 | return(returnvalue) 51 | 52 | return(rjson) 53 | 54 | def getorgid(p_apikey, p_orgname): 55 | #looks up org id for a specific org name 56 | #on failure returns 'null' 57 | 58 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 59 | 60 | if r.status_code != requests.codes.ok: 61 | return 'null' 62 | 63 | rjson = r.json() 64 | 65 | for record in rjson: 66 | if record['name'] == p_orgname: 67 | return record['id'] 68 | return('null') 69 | 70 | def getshardurl(p_apikey, p_orgid): 71 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 72 | # when making API calls with API accounts that can access multiple orgs. 73 | #On failure returns 'null' 74 | 75 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 76 | 77 | if r.status_code != requests.codes.ok: 78 | return 'null' 79 | 80 | rjson = r.json() 81 | 82 | return(rjson['hostname']) 83 | 84 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 85 | #looks up network id for a network name 86 | #on failure returns 'null' 87 | 88 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 89 | 90 | if r.status_code != requests.codes.ok: 91 | return 'null' 92 | 93 | rjson = r.json() 94 | 95 | for record in rjson: 96 | if record['name'] == p_nwname: 97 | return record['id'] 98 | return('null') 99 | 100 | def getnwdevices(p_apikey, p_shardurl, p_nwid): 101 | #returns list of devices in a network 102 | #on failure returns list with one device record, with all values 'null' 103 | 104 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 105 | 106 | returnvalue = [] 107 | if r.status_code != requests.codes.ok: 108 | returnvalue.append({'lat':0.0,'lng':0.0,'address':'null','lanIp':'null','serial':'null','mac':'null','tags':'null','name':'null','model':'null','networkId':'null'}) 109 | return(returnvalue) 110 | 111 | rjson = r.json() 112 | 113 | return(rjson) 114 | 115 | def setdevicedata(p_apikey, p_shardurl, p_nwid, p_devserial, p_field, p_value): 116 | #modifies value of device record. Returns the new value 117 | #on failure returns one device record, with all values 'null' 118 | 119 | r = requests.put('https://%s/api/v0/networks/%s/devices/%s' % (p_shardurl, p_nwid, p_devserial), data=json.dumps({p_field: p_value}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 120 | 121 | if r.status_code != requests.codes.ok: 122 | return ('null') 123 | 124 | return('ok') 125 | 126 | def setdeviceaddress(p_apikey, p_shardurl, p_nwid, p_devserial, p_address, p_lat, p_lng): 127 | r = requests.put('https://%s/api/v0/networks/%s/devices/%s' % (p_shardurl, p_nwid, p_devserial), data=json.dumps({'address' : p_address, 'lat' : p_lat, 'lng' : p_lng}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 128 | 129 | if r.status_code != requests.codes.ok: 130 | return ('null') 131 | 132 | return('ok') 133 | 134 | def mappositions(p_latseed, p_lngseed, p_length, p_increment): 135 | #returns a list of map position data for placing 136 | #devices in in a cluster around a seed location 137 | #may return up to 3 extra location records. These should be ignored 138 | 139 | mpos = {'lat': [p_latseed], 'lng': [p_lngseed]} 140 | 141 | step = 1 142 | #MODIFY latmod AND lngmod IF AP CLUSTER LAT AND LNG DISTANCES ARE NOT PROPORTIONAL 143 | latmod = p_increment * 0.5 144 | lngmod = -1*p_increment 145 | i = 0 146 | 147 | while (len(mpos['lng']) < p_length): 148 | for j in range (i, step): 149 | mpos['lng'].append(mpos['lng'][len(mpos['lng'])-1]) 150 | mpos['lat'].append(mpos['lat'][len(mpos['lat'])-1] + latmod) 151 | for j in range (i, step): 152 | mpos['lng'].append(mpos['lng'][len(mpos['lng'])-1] + lngmod) 153 | mpos['lat'].append(mpos['lat'][len(mpos['lat'])-1]) 154 | step += 2 155 | latmod = -1 * latmod 156 | lngmod = -1 * lngmod 157 | i += 1 158 | 159 | return(mpos) 160 | 161 | def main(argv): 162 | 163 | #get command line arguments 164 | arg_apikey = 'null' 165 | arg_googlekey = 'null' 166 | arg_orgname = 'null' 167 | arg_nwname = 'null' 168 | arg_location = 'null' 169 | 170 | try: 171 | opts, args = getopt.getopt(argv, 'hk:g:o:n:l:') 172 | except getopt.GetoptError: 173 | printhelp() 174 | sys.exit(2) 175 | 176 | for opt, arg in opts: 177 | if opt == '-h': 178 | printhelp() 179 | sys.exit() 180 | elif opt == '-k': 181 | arg_apikey = arg 182 | elif opt == '-g': 183 | arg_googlekey = arg 184 | elif opt == '-o': 185 | arg_orgname = arg 186 | elif opt == '-n': 187 | arg_nwname = arg 188 | elif opt == '-l': 189 | arg_location = arg 190 | 191 | if arg_apikey == 'null' or arg_orgname == 'null' or arg_nwname == 'null' or arg_location == 'null': 192 | printhelp() 193 | sys.exit(2) 194 | 195 | #check if Google API key has been provided, flag if not 196 | if arg_googlekey != 'null': 197 | #get X,Y coordinates corresponding to street address 198 | gresponse = getgooglecoordinates(arg_googlekey, arg_location) 199 | if gresponse['status'] == 'null': 200 | print('Unable to get coordinates for street address using Google Geocoding API') 201 | sys.exit(2) 202 | 203 | glatitude = gresponse['results'][0]['geometry']['location']['lat'] 204 | glongitude = gresponse['results'][0]['geometry']['location']['lng'] 205 | else: 206 | #no google key received, flag it 207 | glatitude = 'null' 208 | glongitude = 'null' 209 | 210 | #get organization id corresponding to org name provided by user 211 | orgid = getorgid(arg_apikey, arg_orgname) 212 | if orgid == 'null': 213 | print('Fetching organization failed') 214 | sys.exit(2) 215 | 216 | #get shard URL where Org is stored 217 | shardurl = getshardurl(arg_apikey, orgid) 218 | if shardurl == 'null': 219 | print('ERROR: Fetching Meraki cloud shard URL failed') 220 | sys.exit(2) 221 | 222 | # get network id corresponding to nw name provided by user 223 | nwid = getnwid(arg_apikey, shardurl, orgid, arg_nwname) 224 | if nwid == 'null': 225 | print('Fetching network failed') 226 | sys.exit(2) 227 | 228 | #compile a list of device info for network 229 | nwdevicelist = getnwdevices(arg_apikey, shardurl, nwid) 230 | 231 | if nwdevicelist[0]['serial'] == 'null': 232 | print('Fetching devices failed') 233 | sys.exit(2) 234 | 235 | #update all devices in list one by one 236 | #check if Google API key has been provided or not. set address and (lat,lng) accordingly 237 | mappos = {} 238 | if glongitude != 'null': 239 | #calculate map positions for devices to place them in a nice cluster around the geocoded address 240 | mappos = mappositions(float(glatitude), float(glongitude), len(nwdevicelist), 0.00005) 241 | 242 | print('Setting street address for network ID %s to %s (%s, %s)' % (nwid, arg_location, glatitude, glongitude)) 243 | 244 | for i in range (0, len(nwdevicelist)): 245 | print('Updating device %s...' % (nwdevicelist[i]['serial'])) 246 | setdeviceaddress(arg_apikey, shardurl, nwid, nwdevicelist[i]['serial'], arg_location, '%f' % mappos['lat'][i], '%f' % mappos['lng'][i]) 247 | else: 248 | print('Setting street address for network ID %s to %s' % (nwid, arg_location)) 249 | 250 | for i in range (0, len(nwdevicelist)): 251 | print('Updating device %s...' % (nwdevicelist[i]['serial'])) 252 | setdevicedata(arg_apikey, shardurl, nwid, nwdevicelist[i]['serial'], 'address', arg_location) 253 | 254 | print('End of script') 255 | 256 | if __name__ == '__main__': 257 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /movedevices.py: -------------------------------------------------------------------------------- 1 | # This script that can be used to move all devices from one organization to another. 2 | # The script will only process devices that are part of a network. 3 | # It has 3 modes of operation: 4 | # # python movedevices.py -k -o -m export -f 5 | # This mode will export a list of all devices in an organization and the networks 6 | # they belong to to a file. This is the default mode. 7 | # # python movedevices.py -k -o -m remove -f 8 | # Export a list of all devices in an org by network and remove them from their networks. 9 | # Please note that this will not unclaim the devices from the original org, since unclaiming 10 | # via API is not supported at time of writing. 11 | # # python movedevices.py -k -o -m import -f 12 | # Import all devices listed in to the specified organization. 13 | # 14 | # You need to have Python 3 and the Requests module installed. You 15 | # can download the module here: https://github.com/kennethreitz/requests 16 | # or install it using pip. 17 | # 18 | # To run the script, enter: 19 | # python movedevices.py -k -o [-m export/remove/import] -f 20 | # 21 | # If option -r is not defined, devices will not be removed from their networks. 22 | # If option -m is not defined, export mode will be assumed. The 3 valid forms of this parameter are: 23 | # -m export 24 | # -m remove 25 | # -m import 26 | # 27 | # To make script chaining easier, all lines not containing a 28 | # device record start with the character @ 29 | # 30 | # This file was last modified on 2017-03-31 31 | 32 | import sys, getopt, requests, json 33 | 34 | def printusertext(p_message): 35 | #prints a line of text that is meant for the user to read 36 | #do not process these lines when chaining scripts 37 | print('@ %s' % p_message) 38 | 39 | def printhelp(): 40 | #prints help text 41 | 42 | printusertext('This script that can be used to move all devices from one organization to another.') 43 | printusertext('The script will only process devices that are part of a network.') 44 | printusertext('') 45 | printusertext('Usage:') 46 | printusertext('python movedevices.py -k -o [-m export/remove/import] -f ') 47 | printusertext('') 48 | printusertext('The script has 3 modes of operation:') 49 | printusertext(' # python movedevices.py -k -o -m export -f ') 50 | printusertext(' This mode will export a list of all devices in an organization and the networks') 51 | printusertext(' they belong to to a file. This is the default mode.') 52 | printusertext(' # python movedevices.py -k -o -m remove -f ') 53 | printusertext(' Export a list of all devices in an org by network and remove them from their networks.') 54 | printusertext(' Please note that this will not unclaim the devices from the original org, since unclaiming') 55 | printusertext(' via API is not supported at time of writing.') 56 | printusertext(' # python movedevices.py -k -o -m import -f ') 57 | printusertext(' Import all devices listed in to the specified organization.') 58 | printusertext('') 59 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 60 | 61 | def getorgid(p_apikey, p_orgname): 62 | #looks up org id for a specific org name 63 | #on failure returns 'null' 64 | 65 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 66 | 67 | if r.status_code != requests.codes.ok: 68 | return 'null' 69 | 70 | rjson = r.json() 71 | 72 | for record in rjson: 73 | if record['name'] == p_orgname: 74 | return record['id'] 75 | return('null') 76 | 77 | def getshardurl(p_apikey, p_orgid): 78 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 79 | # when making API calls with API accounts that can access multiple orgs. 80 | #On failure returns 'null' 81 | 82 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 83 | 84 | if r.status_code != requests.codes.ok: 85 | return 'null' 86 | 87 | rjson = r.json() 88 | 89 | return(rjson['hostname']) 90 | 91 | def getnwlist(p_apikey, p_shardurl, p_orgid): 92 | #returns a list of all networks in an organization 93 | #on failure returns a single record with 'null' name and id 94 | 95 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 96 | 97 | returnvalue = [] 98 | if r.status_code != requests.codes.ok: 99 | returnvalue.append({'name': 'null', 'id': 'null'}) 100 | return(returnvalue) 101 | 102 | return(r.json()) 103 | 104 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 105 | #returns a list of all devices in a network 106 | 107 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 108 | 109 | returnvalue = [] 110 | if r.status_code != requests.codes.ok: 111 | returnvalue.append({'serial': 'null', 'model': 'null'}) 112 | return(returnvalue) 113 | 114 | return(r.json()) 115 | 116 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 117 | #looks up network id for a network name 118 | #on failure returns 'null' 119 | 120 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 121 | 122 | if r.status_code != requests.codes.ok: 123 | return 'null' 124 | 125 | rjson = r.json() 126 | 127 | for record in rjson: 128 | if record['name'] == p_nwname: 129 | return record['id'] 130 | return('null') 131 | 132 | def removedevicefromnw(p_apikey, p_shardurl, p_nwid, p_devserial): 133 | #removes a device from its parent network. does not unclaim it 134 | 135 | r = requests.post('https://%s/api/v0/networks/%s/devices/%s/remove' % (p_shardurl, p_nwid, p_devserial), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 136 | 137 | return (0) 138 | 139 | def claimdevice(p_apikey, p_shardurl, p_nwid, p_devserial): 140 | #claims a device into an org 141 | 142 | r = requests.post('https://%s/api/v0/networks/%s/devices/claim' % (p_shardurl, p_nwid), data=json.dumps({'serial': p_devserial}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 143 | 144 | return (0) 145 | 146 | def main(argv): 147 | #set default values for command line arguments 148 | arg_apikey = 'null' 149 | arg_orgname = 'null' 150 | arg_mode = 'export' 151 | arg_filepath = 'null' 152 | 153 | #get command line arguments 154 | try: 155 | opts, args = getopt.getopt(argv, 'hk:o:m:f:') 156 | except getopt.GetoptError: 157 | printhelp() 158 | sys.exit(2) 159 | 160 | for opt, arg in opts: 161 | if opt == '-h': 162 | printhelp() 163 | sys.exit() 164 | elif opt == '-k': 165 | arg_apikey = arg 166 | elif opt == '-o': 167 | arg_orgname = arg 168 | elif opt == '-m': 169 | arg_mode = arg 170 | elif opt == '-f': 171 | arg_filepath = arg 172 | 173 | #check if parameter -m has one of three valid values. blank is also OK, as export is default 174 | mode_import = False 175 | mode_remove = False 176 | modenotvalid = True 177 | if arg_mode == 'import': 178 | modenotvalid = False 179 | mode_import = True 180 | elif arg_mode == 'export': 181 | modenotvalid = False 182 | elif arg_mode == 'remove': 183 | modenotvalid = False 184 | mode_remove = True 185 | 186 | #check if all parameters are required parameters have been given 187 | if arg_apikey == 'null' or arg_orgname == 'null' or arg_filepath == 'null' or modenotvalid: 188 | printhelp() 189 | sys.exit(2) 190 | 191 | #get organization id corresponding to org name provided by user 192 | orgid = getorgid(arg_apikey, arg_orgname) 193 | if orgid == 'null': 194 | printusertext('ERROR: Fetching organization failed') 195 | sys.exit(2) 196 | 197 | #get shard URL where Org is stored 198 | shardurl = getshardurl(arg_apikey, orgid) 199 | if shardurl == 'null': 200 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 201 | sys.exit(2) 202 | 203 | #get network list for fetched org id 204 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 205 | 206 | if nwlist[0]['id'] == 'null': 207 | printusertext('ERROR: Fetching network list failed') 208 | sys.exit(2) 209 | 210 | #if export or remove mode, open file for writing. if import mode, open file for reading 211 | if mode_import: 212 | #if parameter -m import, open file for reading 213 | try: 214 | f = open(arg_filepath, 'r') 215 | except: 216 | printusertext('ERROR: Unable to open file for reading') 217 | sys.exit(2) 218 | else: 219 | #if parameter -m export or remove, open file for reading 220 | try: 221 | f = open(arg_filepath, 'w') 222 | except: 223 | printusertext('ERROR: Unable to open file for writing') 224 | sys.exit(2) 225 | 226 | if not(mode_import): 227 | devicelist = [] 228 | for nwrecord in nwlist: 229 | #write network name to file 230 | try: 231 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 232 | f.write('#%s\n' % nwrecord['name']) 233 | except: 234 | printusertext('ERROR: Unable to write network info to file') 235 | sys.exit(2) 236 | #get devices' list 237 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 238 | #append list to file or stdout 239 | for i in range (0, len(devicelist)): 240 | try: 241 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 242 | f.write('%s\n' % (devicelist[i]['serial'])) 243 | except: 244 | printusertext('ERROR: Unable to write device info to file') 245 | sys.exit(2) 246 | if mode_remove: 247 | removedevicefromnw(arg_apikey, shardurl, nwrecord['id'], devicelist[i]['serial']) 248 | else: 249 | #parameter -m import 250 | printusertext('Attempting to claim devices. If nothing happens, please check that the device') 251 | printusertext('serial numbers have been unclaimed and wait before running again. It can take') 252 | printusertext('up to a few hours for a device to become available for claiming.') 253 | 254 | nwname = 'null' 255 | nwid = 'null' 256 | devserial = 'null' 257 | for line in f: 258 | if line[0] == '#': 259 | nwname = line[1:].rstrip() 260 | nwid = getnwid(arg_apikey, shardurl, orgid, nwname) 261 | if nwid == 'null': 262 | printusertext('ERROR: Network "%s" does not exist in target org.' % nwname) 263 | sys.exit(2) 264 | else: 265 | devserial = line.rstrip() 266 | claimdevice(arg_apikey, shardurl, nwid, devserial) 267 | 268 | printusertext('End of script.') 269 | 270 | if __name__ == '__main__': 271 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /copyswitchcfg.py: -------------------------------------------------------------------------------- 1 | # This is a script to migrate the switchport configuration of one organization to another. 2 | # 3 | # The script can be used to export switchport configuration of a source org to a file and 4 | # import it to a destination org. The script will look for the exact same network names and 5 | # device serial numbers, as they were in the source org. Use copynetworks.py and movedevices.py 6 | # to migrate networks and devices if needed. The recommended migration workflow is: 7 | # * Copy networks with copynetworks.py 8 | # * Export device info with movedevices.py -m export 9 | # * Export switchport configuration with copyswitchcfg.py -m export 10 | # * Run additional export scripts 11 | # * Remove devices from networks with movedevices.py -m remove 12 | # * Unclaim devices manually and wait for them to become claimable again 13 | # * Import device info with movedevices.py -m import 14 | # * Import switchport configuration with copyswitchcfg.py -m import 15 | # * Run additional import scripts 16 | # 17 | # The script will only process devices that are part of a network. 18 | # It has 2 modes of operation: 19 | # * python copyswitchcfg.py -k -o -m export -f 20 | # This mode will export switchport configuration of all swithces in the org to a file. 21 | # This is the default mode. 22 | # * python copyswitchcfg.py -k -o -m import -f 23 | # Import all switchport configuration in to the specified organization. 24 | # 25 | # You need to have Python 3 and the Requests module installed. You 26 | # can download the module here: https://github.com/kennethreitz/requests 27 | # or install it using pip. 28 | # 29 | # To run the script, enter: 30 | # python copyswitchcfg.py -k -o [-m export/import] -f 31 | # 32 | # If option -m is not defined, export mode will be assumed. The 2 valid forms of this parameter are: 33 | # -m export 34 | # -m import 35 | # 36 | # To make script chaining easier, all lines containing informational messages to the user 37 | # start with the character @ 38 | # 39 | # This file was last modified on 2017-04-03 40 | 41 | import sys, getopt, requests, json 42 | 43 | def printusertext(p_message): 44 | #prints a line of text that is meant for the user to read 45 | #do not process these lines when chaining scripts 46 | print('@ %s' % p_message) 47 | 48 | def printhelp(): 49 | #prints help text 50 | 51 | printusertext('# This is a script to migrate the switchport configuration of one organization to another.') 52 | printusertext('') 53 | printusertext('Usage:') 54 | printusertext('python copyswitchcfg.py -k -o [-m export/import] -f ') 55 | printusertext('') 56 | printusertext('If option -m is not defined, export mode will be assumed.') 57 | printusertext('The 2 valid forms of this parameter are:') 58 | printusertext(' -m export') 59 | printusertext(' -m import') 60 | printusertext('') 61 | printusertext(' # python copyswitchcfg.py -k -o -m export -f ') 62 | printusertext(' This mode will export switchport configuration of all swithces in the org to a file.') 63 | printusertext(' This is the default mode.') 64 | printusertext(' # python copyswitchcfg.py -k -o -m import -f ') 65 | printusertext(' Import all switchport configuration in to the specified organization.') 66 | printusertext('') 67 | printusertext('The script will only process devices that are part of a network.') 68 | printusertext('') 69 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 70 | 71 | def getorgid(p_apikey, p_orgname): 72 | #looks up org id for a specific org name 73 | #on failure returns 'null' 74 | 75 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 76 | 77 | if r.status_code != requests.codes.ok: 78 | return 'null' 79 | 80 | rjson = r.json() 81 | 82 | for record in rjson: 83 | if record['name'] == p_orgname: 84 | return record['id'] 85 | return('null') 86 | 87 | def getshardurl(p_apikey, p_orgid): 88 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 89 | # when making API calls with API accounts that can access multiple orgs. 90 | #On failure returns 'null' 91 | 92 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 93 | 94 | if r.status_code != requests.codes.ok: 95 | return 'null' 96 | 97 | rjson = r.json() 98 | 99 | return(rjson['hostname']) 100 | 101 | def getnwlist(p_apikey, p_shardurl, p_orgid): 102 | #returns a list of all networks in an organization 103 | #on failure returns a single record with 'null' name and id 104 | 105 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 106 | 107 | returnvalue = [] 108 | if r.status_code != requests.codes.ok: 109 | returnvalue.append({'name': 'null', 'id': 'null'}) 110 | return(returnvalue) 111 | 112 | return(r.json()) 113 | 114 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 115 | #returns a list of all devices in a network 116 | 117 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 118 | 119 | returnvalue = [] 120 | if r.status_code != requests.codes.ok: 121 | returnvalue.append({'serial': 'null', 'model': 'null'}) 122 | return(returnvalue) 123 | 124 | return(r.json()) 125 | 126 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 127 | #looks up network id for a network name 128 | #on failure returns 'null' 129 | 130 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 131 | 132 | if r.status_code != requests.codes.ok: 133 | return 'null' 134 | 135 | rjson = r.json() 136 | 137 | for record in rjson: 138 | if record['name'] == p_nwname: 139 | return record['id'] 140 | return('null') 141 | 142 | def getswitchports(p_apikey, p_shardurl, p_devserial): 143 | #returns switchport info for a device 144 | 145 | r = requests.get('https://%s/api/v0/devices/%s/switchPorts' % (p_shardurl, p_devserial), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 146 | 147 | returnvalue = [] 148 | if r.status_code != requests.codes.ok: 149 | returnvalue.append({'number': 'null'}) 150 | return(returnvalue) 151 | 152 | return(r.json()) 153 | 154 | def setswportconfig(p_apikey, p_shardurl, p_devserial, p_portnum, p_portcfg): 155 | #sets switchport configuration to match table given as parameter 156 | 157 | r = requests.put('https://%s/api/v0/devices/%s/switchPorts/%s' % (p_shardurl, p_devserial, p_portnum), data=json.dumps({'isolationEnabled': p_portcfg['isolationEnabled'], 'rstpEnabled': p_portcfg['rstpEnabled'], 'enabled': p_portcfg['enabled'], 'stpGuard': p_portcfg['stpGuard'], 'accessPolicyNumber': p_portcfg['accessPolicyNumber'], 'type': p_portcfg['type'], 'allowedVlans': p_portcfg['allowedVlans'], 'poeEnabled': p_portcfg['poeEnabled'], 'name': p_portcfg['name'], 'tags': p_portcfg['tags'], 'number': p_portcfg['number'], 'vlan': p_portcfg['vlan'], 'voiceVlan': p_portcfg['voiceVlan']}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 158 | 159 | return (0) 160 | 161 | def main(argv): 162 | #set default values for command line arguments 163 | arg_apikey = 'null' 164 | arg_orgname = 'null' 165 | arg_mode = 'export' 166 | arg_filepath = 'null' 167 | 168 | #get command line arguments 169 | try: 170 | opts, args = getopt.getopt(argv, 'hk:o:m:f:') 171 | except getopt.GetoptError: 172 | printhelp() 173 | sys.exit(2) 174 | 175 | for opt, arg in opts: 176 | if opt == '-h': 177 | printhelp() 178 | sys.exit() 179 | elif opt == '-k': 180 | arg_apikey = arg 181 | elif opt == '-o': 182 | arg_orgname = arg 183 | elif opt == '-m': 184 | arg_mode = arg 185 | elif opt == '-f': 186 | arg_filepath = arg 187 | 188 | #check if parameter -m has one a valid value. blank is also OK, as export is default 189 | mode_export = True 190 | modenotvalid = True 191 | if arg_mode == 'import': 192 | modenotvalid = False 193 | mode_export = False 194 | elif arg_mode == 'export': 195 | modenotvalid = False 196 | 197 | #check if all parameters are required parameters have been given 198 | if arg_apikey == 'null' or arg_orgname == 'null' or arg_filepath == 'null' or modenotvalid: 199 | printhelp() 200 | sys.exit(2) 201 | 202 | #get organization id corresponding to org name provided by user 203 | orgid = getorgid(arg_apikey, arg_orgname) 204 | if orgid == 'null': 205 | printusertext('ERROR: Fetching organization failed') 206 | sys.exit(2) 207 | 208 | #get shard URL where Org is stored 209 | shardurl = getshardurl(arg_apikey, orgid) 210 | if shardurl == 'null': 211 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 212 | sys.exit(2) 213 | 214 | #get network list for fetched org id 215 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 216 | 217 | if nwlist[0]['id'] == 'null': 218 | printusertext('ERROR: Fetching network list failed') 219 | sys.exit(2) 220 | 221 | #if export mode, open file for writing. if import mode, open file for reading 222 | if mode_export: 223 | #if parameter -m export, open file for writing 224 | try: 225 | f = open(arg_filepath, 'w') 226 | except: 227 | printusertext('ERROR: Unable to open file for writing') 228 | sys.exit(2) 229 | else: 230 | #if parameter -m import, open file for reading 231 | try: 232 | f = open(arg_filepath, 'r') 233 | except: 234 | printusertext('ERROR: Unable to open file for reading') 235 | sys.exit(2) 236 | 237 | #define list for all switchports for source org 238 | orgswitchports = [] 239 | 240 | if mode_export: 241 | #devices in network 242 | devicelist = [] 243 | #switchports in a single device 244 | devswitchports = [] 245 | 246 | for nwrecord in nwlist: 247 | #all switchports in a single network 248 | nwswitchports = [] 249 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 250 | for devrecord in devicelist: 251 | #get switchports in device 252 | devswitchports = getswitchports(arg_apikey, shardurl, devrecord['serial']) 253 | #devswitchports [0]['number'] will be 'null' if anything went wrong (device not an MS switch, etc) 254 | if devswitchports [0]['number'] != 'null': 255 | #append dev switchports to network list 256 | nwswitchports.append( {'serial': devrecord['serial'], 'devports' : devswitchports} ) 257 | if len(nwswitchports) > 0: 258 | orgswitchports.append( {'network': nwrecord['name'], 'nwports': nwswitchports} ) 259 | else: 260 | printusertext('WARNING: Skipping network "%s": No switchports' % nwrecord['name']) 261 | 262 | #write org switchports' list to file 263 | try: 264 | json.dump(orgswitchports, f) 265 | except: 266 | printusertext('ERROR: Writing to output file failed') 267 | sys.exit(2) 268 | else: 269 | #import mode 270 | 271 | #read org switchports' list from file 272 | try: 273 | orgswitchports = json.load(f) 274 | except: 275 | printusertext('ERROR: Reading from file failed') 276 | sys.exit(2) 277 | 278 | #upload switchport configuration to Dashboard 279 | for nwrecord in orgswitchports: 280 | for devrecord in nwrecord['nwports']: 281 | printusertext('INFO: Configuring device %s' % devrecord['serial']) 282 | for swport in devrecord['devports']: 283 | setswportconfig(arg_apikey, shardurl, devrecord['serial'], swport['number'], swport) 284 | 285 | printusertext('End of script.') 286 | 287 | if __name__ == '__main__': 288 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /deployappliance.py: -------------------------------------------------------------------------------- 1 | # This is a script to claim a device into Dashboard, create a network for it and bind 2 | # the network to a pre-existing template. 3 | # 4 | # You need to have Python 3 and the Requests module installed. You 5 | # can download the module here: https://github.com/kennethreitz/requests 6 | # or install it using pip. 7 | # 8 | # To run the script, enter: 9 | # python deployappliance.py -k -o -s -n -c [-t ] [-a ] [-m ignore_error] 10 | # 11 | # To make script chaining easier, all lines containing informational messages to the user 12 | # start with the character @ 13 | # 14 | # This file was last modified on 2017-06-30 15 | 16 | import sys, getopt, requests, json 17 | 18 | def printusertext(p_message): 19 | #prints a line of text that is meant for the user to read 20 | #do not process these lines when chaining scripts 21 | print('@ %s' % p_message) 22 | 23 | def printhelp(): 24 | #prints help text 25 | 26 | printusertext('') 27 | printusertext('This is a script to claim a device into Dashboard, create a new network for it and bind') 28 | printusertext('the network to a pre-existing template.') 29 | printusertext('') 30 | printusertext('To run the script, enter:') 31 | printusertext('python deployappliance.py -k -o -s -n -c [-t ] [-a ] [-m ignore_error]') 32 | printusertext('') 33 | printusertext(': Your Meraki Dashboard API key') 34 | printusertext(': Name of the Meraki Dashboard Organization to modify') 35 | printusertext(': Serial number of the device to claim') 36 | printusertext(': Name the new network will have') 37 | printusertext(': Name of the config template the new network will bound to') 38 | printusertext('-t : Optional parameter. If defined, network will be tagged with the given tags') 39 | printusertext('-a : Optional parameter. If defined, device will be moved to given street address') 40 | printusertext('-m ignore_error: Optional parameter. If defined, the script will not stop if network exists') 41 | printusertext('') 42 | printusertext('Example:') 43 | printusertext('python deployappliance.py -k 1234 -o MyCustomer -s XXXX-YYYY-ZZZZ -n NewBranch -c MyCfgTemplate') 44 | printusertext('') 45 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 46 | 47 | def getorgid(p_apikey, p_orgname): 48 | #looks up org id for a specific org name 49 | #on failure returns 'null' 50 | 51 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 52 | 53 | if r.status_code != requests.codes.ok: 54 | return 'null' 55 | 56 | rjson = r.json() 57 | 58 | for record in rjson: 59 | if record['name'] == p_orgname: 60 | return record['id'] 61 | return('null') 62 | 63 | def getshardurl(p_apikey, p_orgid): 64 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 65 | # when making API calls with API accounts that can access multiple orgs. 66 | #On failure returns 'null' 67 | 68 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 69 | 70 | if r.status_code != requests.codes.ok: 71 | return 'null' 72 | 73 | rjson = r.json() 74 | 75 | return(rjson['hostname']) 76 | 77 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 78 | #looks up network id for a network name 79 | #on failure returns 'null' 80 | 81 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 82 | 83 | if r.status_code != requests.codes.ok: 84 | return 'null' 85 | 86 | rjson = r.json() 87 | 88 | for record in rjson: 89 | if record['name'] == p_nwname: 90 | return record['id'] 91 | return('null') 92 | 93 | def createnw(p_apikey, p_shardurl, p_dstorg, p_nwdata): 94 | #creates network if one does not already exist with the same name 95 | 96 | #check if network exists 97 | getnwresult = getnwid(p_apikey, p_shardurl, p_dstorg, p_nwdata['name']) 98 | if getnwresult != 'null': 99 | printusertext('WARNING: Skipping network "%s" (Already exists)' % p_nwdata['name']) 100 | return('null') 101 | 102 | if p_nwdata['type'] == 'combined': 103 | #find actual device types 104 | nwtype = 'wireless switch appliance' 105 | else: 106 | nwtype = p_nwdata['type'] 107 | if nwtype != 'systems manager': 108 | r = requests.post('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, 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'}) 109 | else: 110 | printusertext('WARNING: Skipping network "%s" (Cannot create SM networks)' % p_nwdata['name']) 111 | return('null') 112 | 113 | return('ok') 114 | 115 | def gettemplateid(p_apikey, p_shardurl, p_orgid, p_tname): 116 | #looks up config template id for a config template name 117 | #on failure returns 'null' 118 | 119 | r = requests.get('https://%s/api/v0/organizations/%s/configTemplates' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 120 | 121 | if r.status_code != requests.codes.ok: 122 | return 'null' 123 | 124 | rjson = r.json() 125 | 126 | for record in rjson: 127 | if record['name'] == p_tname: 128 | return record['id'] 129 | return('null') 130 | 131 | def bindnw(p_apikey, p_shardurl, p_nwid, p_templateid): 132 | #binds a network to a template 133 | 134 | r = requests.post('https://%s/api/v0/networks/%s/bind' % (p_shardurl, p_nwid), data=json.dumps({'configTemplateId': p_templateid}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 135 | 136 | if r.status_code != requests.codes.ok: 137 | return 'null' 138 | 139 | return('ok') 140 | 141 | def claimdevice(p_apikey, p_shardurl, p_nwid, p_devserial): 142 | #claims a device into an org 143 | 144 | r = requests.post('https://%s/api/v0/networks/%s/devices/claim' % (p_shardurl, p_nwid), data=json.dumps({'serial': p_devserial}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 145 | 146 | return(0) 147 | 148 | def getdeviceinfo(p_apikey, p_shardurl, p_nwid, p_serial): 149 | #returns info for a single device 150 | #on failure returns lone device record, with serial number 'null' 151 | 152 | r = requests.get('https://%s/api/v0/networks/%s/devices/%s' % (p_shardurl, p_nwid, p_serial), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 153 | 154 | returnvalue = [] 155 | if r.status_code != requests.codes.ok: 156 | returnvalue = {'serial':'null'} 157 | return(returnvalue) 158 | 159 | rjson = r.json() 160 | 161 | return(rjson) 162 | 163 | def setdevicedata(p_apikey, p_shardurl, p_nwid, p_devserial, p_field, p_value, p_movemarker): 164 | #modifies value of device record. Returns the new value 165 | #on failure returns one device record, with all values 'null' 166 | #p_movemarker is boolean: True/False 167 | 168 | movevalue = "false" 169 | if p_movemarker: 170 | movevalue = "true" 171 | 172 | r = requests.put('https://%s/api/v0/networks/%s/devices/%s' % (p_shardurl, p_nwid, p_devserial), data=json.dumps({p_field: p_value, 'moveMapMarker': movevalue}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 173 | 174 | if r.status_code != requests.codes.ok: 175 | return ('null') 176 | 177 | return('ok') 178 | 179 | def main(argv): 180 | #set default values for command line arguments 181 | arg_apikey = 'null' 182 | arg_orgname = 'null' 183 | arg_serial = 'null' 184 | arg_nwname = 'null' 185 | arg_template = 'null' 186 | arg_modexisting = 'null' 187 | arg_address = 'null' 188 | arg_nwtags = 'null' 189 | 190 | #get command line arguments 191 | # python deployappliance.py -k -o -s -n -c [-t ] [-a ] [-m ignore_error] 192 | try: 193 | opts, args = getopt.getopt(argv, 'hk:o:s:n:c:m:a:t:') 194 | except getopt.GetoptError: 195 | printhelp() 196 | sys.exit(2) 197 | 198 | for opt, arg in opts: 199 | if opt == '-h': 200 | printhelp() 201 | sys.exit() 202 | elif opt == '-k': 203 | arg_apikey = arg 204 | elif opt == '-o': 205 | arg_orgname = arg 206 | elif opt == '-s': 207 | arg_serial = arg 208 | elif opt == '-n': 209 | arg_nwname = arg 210 | elif opt == '-c': 211 | arg_template = arg 212 | elif opt == '-m': 213 | arg_modexisting = arg 214 | elif opt == '-a': 215 | arg_address = arg 216 | elif opt == '-t': 217 | arg_nwtags = arg 218 | 219 | #check if all parameters are required parameters have been given 220 | if arg_apikey == 'null' or arg_orgname == 'null' or arg_serial == 'null' or arg_nwname == 'null' or arg_template == 'null': 221 | printhelp() 222 | sys.exit(2) 223 | 224 | #set optional flag to ignore error if network already exists 225 | stoponerror = True 226 | if arg_modexisting == 'ignore_error': 227 | stoponerror = False 228 | 229 | #get organization id corresponding to org name provided by user 230 | orgid = getorgid(arg_apikey, arg_orgname) 231 | if orgid == 'null': 232 | printusertext('ERROR: Fetching organization failed') 233 | sys.exit(2) 234 | 235 | #get shard URL where Org is stored 236 | shardurl = getshardurl(arg_apikey, orgid) 237 | if shardurl == 'null': 238 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 239 | sys.exit(2) 240 | 241 | #make sure that a network does not already exist with the same name 242 | nwid = getnwid(arg_apikey, shardurl, orgid, arg_nwname) 243 | if nwid != 'null' and stoponerror: 244 | printusertext('ERROR: Network with that name already exists') 245 | sys.exit(2) 246 | 247 | #get template ID for template name argument 248 | templateid = gettemplateid(arg_apikey, shardurl, orgid, arg_template) 249 | if templateid == 'null': 250 | printusertext('ERROR: Unable to find template' + arg_template) 251 | sys.exit(2) 252 | 253 | #gather parameters to create network 254 | #valid values for parameter 'type': 'wireless', 'switch', 'appliance', 'combined', 'wireless switch', etc 255 | nwtags = '' 256 | if arg_nwtags != 'null': 257 | nwtags = arg_nwtags 258 | nwparams = {'name': arg_nwname, 'timeZone': 'Europe/Helsinki', 'tags': nwtags, 'organizationId': orgid, 'type': 'appliance'} 259 | 260 | #create network and get its ID 261 | if nwid == 'null': 262 | createstatus = createnw (arg_apikey, shardurl, orgid, nwparams) 263 | if createstatus == 'null': 264 | printusertext('ERROR: Unable to create network') 265 | sys.exit(2) 266 | nwid = getnwid(arg_apikey, shardurl, orgid, arg_nwname) 267 | if nwid == 'null': 268 | printusertext('ERROR: Unable to get ID for new network') 269 | sys.exit(2) 270 | 271 | #bind network to template 272 | bindstatus = bindnw(arg_apikey, shardurl, nwid, templateid) 273 | if bindstatus == 'null' and stoponerror: 274 | printusertext('ERROR: Unable to bind network to template') 275 | sys.exit(2) 276 | 277 | #claim device into newly created network 278 | claimdevice(arg_apikey, shardurl, nwid, arg_serial) 279 | 280 | #check if device has been claimed successfully 281 | deviceinfo = getdeviceinfo(arg_apikey, shardurl, nwid, arg_serial) 282 | if deviceinfo['serial'] == 'null': 283 | printusertext('ERROR: Claiming or moving device unsuccessful') 284 | sys.exit(2) 285 | 286 | #set device hostname 287 | hostname = deviceinfo['model'] + '_' + arg_serial 288 | setdevicedata(arg_apikey, shardurl, nwid, arg_serial, 'name', hostname, False) 289 | 290 | #if street address is given as a parameter, set device location 291 | if arg_address != 'null': 292 | setdevicedata(arg_apikey, shardurl, nwid, arg_serial, 'address', arg_address, True) 293 | 294 | printusertext('End of script.') 295 | 296 | if __name__ == '__main__': 297 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /copymxvlans.py: -------------------------------------------------------------------------------- 1 | # This is a script to migrate the MX VLAN configuration of one organization to another. 2 | # 3 | # The script can be used to export MX VLAN configuration of a source org to a file and 4 | # import it to a destination org. The script will look for the exact same network names 5 | # as they were in the source org. Use copynetworks.py and movedevices.py 6 | # to migrate networks and devices if needed. The recommended migration workflow is: 7 | # * Copy networks with copynetworks.py 8 | # * Export device info with movedevices.py -m export 9 | # * Export MX VLAN configuration with copymxvlans.py -m export 10 | # * Run additional export scripts 11 | # * Remove devices from networks with movedevices.py -m remove 12 | # * Unclaim devices manually and wait for them to become claimable again 13 | # * Import device info with movedevices.py -m import 14 | # * Manually enable VLAN support on every MX you want to copy VLANs for 15 | # * Import MX VLAN configuration with copymxvlans.py -m importall or copymxvlans.py -m importnew 16 | # * Run additional import scripts 17 | # 18 | # The script will only process devices that are part of a network. 19 | # It has 3 modes of operation: 20 | # * python copymxvlans.py -k -o -m export -f 21 | # This mode will export MX VLAN configuration of all MX appliances in the org to a file. 22 | # This is the default mode. 23 | # * python copymxvlans.py -k -o -m importall -f 24 | # Import all MX VLAN configuration in to the specified organization. If a VLAN exists, it 25 | # will be updated to match the configuration in . 26 | # * python copymxvlans.py -k -o -m importnew -f 27 | # Import MX VLAN configuration in to the specified organization for VLANs that don't already 28 | # exist in destination org. If a VLAN exists, it will be skipped. 29 | # 30 | # You need to have Python 3 and the Requests module installed. You 31 | # can download the module here: https://github.com/kennethreitz/requests 32 | # or install it using pip. 33 | # 34 | # To run the script, enter: 35 | # python copymxvlans.py -k -o [-m export/importall/importnew] -f 36 | # 37 | # If option -m is not defined, export mode will be assumed. The 3 valid forms of this parameter are: 38 | # -m export 39 | # -m importall 40 | # -m importnew 41 | # 42 | # To make script chaining easier, all lines containing informational messages to the user 43 | # start with the character @ 44 | # 45 | # This file was last modified on 2017-05-10 46 | 47 | import sys, getopt, requests, json 48 | 49 | def printusertext(p_message): 50 | #prints a line of text that is meant for the user to read 51 | #do not process these lines when chaining scripts 52 | print('@ %s' % p_message) 53 | 54 | def printhelp(): 55 | #prints help text 56 | 57 | printusertext('') 58 | printusertext('This is a script to migrate the MX VLAN configuration of one organization to another.') 59 | printusertext('For import modes to work, the target MX will need to have VLAN support enabled.') 60 | printusertext('') 61 | printusertext('Usage:') 62 | printusertext('python copymxvlans.py -k -o [-m export/importall/importnew] -f ') 63 | printusertext('') 64 | printusertext('If option -m is not defined, export mode will be assumed.') 65 | printusertext('The 3 valid forms of this parameter are:') 66 | printusertext(' -m export') 67 | printusertext(' -m importall') 68 | printusertext(' -m importnew') 69 | printusertext('') 70 | printusertext(' # python copymxvlans.py -k -o -m export -f ') 71 | printusertext(' This mode will export MX VLAN configuration of all MX appliances in the org to a file.') 72 | printusertext(' This is the default mode.') 73 | printusertext(' # python copymxvlans.py -k -o -m importall -f ') 74 | printusertext(' Import all MX VLAN configuration in to the specified organization. Update') 75 | printusertext(' VLANs to match configuration found in file.') 76 | printusertext(' # python copymxvlans.py -k -o -m importnew -f ') 77 | printusertext(' Import MX VLAN configuration in to the specified organization. Skip VLANs') 78 | printusertext(' that already exist.') 79 | printusertext('') 80 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 81 | 82 | def getorgid(p_apikey, p_orgname): 83 | #looks up org id for a specific org name 84 | #on failure returns 'null' 85 | 86 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 87 | 88 | if r.status_code != requests.codes.ok: 89 | return 'null' 90 | 91 | rjson = r.json() 92 | 93 | for record in rjson: 94 | if record['name'] == p_orgname: 95 | return record['id'] 96 | return('null') 97 | 98 | def getshardurl(p_apikey, p_orgid): 99 | #Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 100 | # when making API calls with API accounts that can access multiple orgs. 101 | #On failure returns 'null' 102 | 103 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 104 | 105 | if r.status_code != requests.codes.ok: 106 | return 'null' 107 | 108 | rjson = r.json() 109 | 110 | return(rjson['hostname']) 111 | 112 | def getnwlist(p_apikey, p_shardurl, p_orgid): 113 | #returns a list of all networks in an organization 114 | #on failure returns a single record with 'null' name and id 115 | 116 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 117 | 118 | returnvalue = [] 119 | if r.status_code != requests.codes.ok: 120 | returnvalue.append({'name': 'null', 'id': 'null'}) 121 | return(returnvalue) 122 | 123 | return(r.json()) 124 | 125 | def getnwid(p_apikey, p_shardurl, p_orgid, p_nwname): 126 | #looks up network id for a network name 127 | #on failure returns 'null' 128 | 129 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 130 | 131 | if r.status_code != requests.codes.ok: 132 | return 'null' 133 | 134 | rjson = r.json() 135 | 136 | for record in rjson: 137 | if record['name'] == p_nwname: 138 | return record['id'] 139 | return('null') 140 | 141 | def getvlanlist(p_apikey, p_shardurl, p_nwid): 142 | #returns list of all MX VLANs in a network 143 | 144 | r = requests.get('https://%s/api/v0/networks/%s/vlans' % (p_shardurl, p_nwid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 145 | 146 | returnvalue = [] 147 | if r.status_code != requests.codes.ok: 148 | returnvalue.append({'id': 'null'}) 149 | return(returnvalue) 150 | 151 | return(r.json()) 152 | 153 | def getvlandetails(p_apikey, p_shardurl, p_nwid, p_vlanid): 154 | #returns details for specified VLAN in specified network 155 | 156 | #UNTESTED 157 | 158 | r = requests.get('https://%s/api/v0/networks/%s/vlans/%s' % (p_shardurl, p_nwid, p_vlanid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 159 | 160 | if r.status_code != requests.codes.ok: 161 | return({'id': 'null'}) 162 | 163 | return(r.json()) 164 | 165 | def createvlan(p_apikey, p_shardurl, p_nwid, p_vlandata): 166 | #creates a new MX VLAN into the specified network 167 | 168 | #UNTESTED 169 | 170 | r = requests.post('https://%s/api/v0/networks/%s/vlans' % (p_shardurl, p_nwid), data=json.dumps({'id': p_vlandata['id'], 'name': p_vlandata['name'], 'applianceIp': p_vlandata['applianceIp'], 'subnet': p_vlandata['subnet'], 'fixedIpAssignments': p_vlandata['fixedIpAssignments'], 'reservedIpRanges': p_vlandata['reservedIpRanges'], 'dnsNameservers': p_vlandata['dnsNameservers']}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 171 | 172 | return (0) 173 | 174 | def updatevlan(p_apikey, p_shardurl, p_nwid, p_vlandata): 175 | #updates an existing MX VLAN in the specified org 176 | 177 | #UNTESTED 178 | 179 | r = requests.put('https://%s/api/v0/networks/%s/vlans/%s' % (p_shardurl, p_nwid, p_vlandata['id']), data=json.dumps({'name': p_vlandata['name'], 'applianceIp': p_vlandata['applianceIp'], 'subnet': p_vlandata['subnet'], 'fixedIpAssignments': p_vlandata['fixedIpAssignments'], 'reservedIpRanges': p_vlandata['reservedIpRanges'], 'dnsNameservers': p_vlandata['dnsNameservers']}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 180 | 181 | return (0) 182 | 183 | def main(argv): 184 | #set default values for command line arguments 185 | arg_apikey = 'null' 186 | arg_orgname = 'null' 187 | arg_mode = 'export' 188 | arg_filepath = 'null' 189 | 190 | #get command line arguments 191 | try: 192 | opts, args = getopt.getopt(argv, 'hk:o:m:f:') 193 | except getopt.GetoptError: 194 | printhelp() 195 | sys.exit(2) 196 | 197 | for opt, arg in opts: 198 | if opt == '-h': 199 | printhelp() 200 | sys.exit() 201 | elif opt == '-k': 202 | arg_apikey = arg 203 | elif opt == '-o': 204 | arg_orgname = arg 205 | elif opt == '-m': 206 | arg_mode = arg 207 | elif opt == '-f': 208 | arg_filepath = arg 209 | 210 | #check if parameter -m has one a valid value. blank is also OK, as export is default 211 | mode_export = True 212 | modenotvalid = True 213 | mode_updateexisting = False 214 | if arg_mode == 'importall': 215 | modenotvalid = False 216 | mode_export = False 217 | mode_updateexisting = True 218 | if arg_mode == 'importnew': 219 | modenotvalid = False 220 | mode_export = False 221 | elif arg_mode == 'export': 222 | modenotvalid = False 223 | 224 | #check if all parameters are required parameters have been given 225 | if arg_apikey == 'null' or arg_orgname == 'null' or arg_filepath == 'null' or modenotvalid: 226 | printhelp() 227 | sys.exit(2) 228 | 229 | #get organization id corresponding to org name provided by user 230 | orgid = getorgid(arg_apikey, arg_orgname) 231 | if orgid == 'null': 232 | printusertext('ERROR: Fetching organization failed') 233 | sys.exit(2) 234 | 235 | #get shard URL where Org is stored 236 | shardurl = getshardurl(arg_apikey, orgid) 237 | if shardurl == 'null': 238 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 239 | sys.exit(2) 240 | 241 | #get network list for fetched org id 242 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 243 | 244 | if nwlist[0]['id'] == 'null': 245 | printusertext('ERROR: Fetching network list failed') 246 | sys.exit(2) 247 | 248 | #if export mode, open file for writing. if import mode, open file for reading 249 | if mode_export: 250 | #if parameter -m export, open file for writing 251 | try: 252 | f = open(arg_filepath, 'w') 253 | except: 254 | printusertext('ERROR: Unable to open file for writing') 255 | sys.exit(2) 256 | else: 257 | #if parameter -m import, open file for reading 258 | try: 259 | f = open(arg_filepath, 'r') 260 | except: 261 | printusertext('ERROR: Unable to open file for reading') 262 | sys.exit(2) 263 | 264 | #define list for all VLANS for source/destination org 265 | orgvlans = [] 266 | 267 | if mode_export: 268 | #iterate all networks in org and gather their VLANs in a list. write list to file 269 | for nwrecord in nwlist: 270 | #VLANs in network 271 | nwvlanlist = getvlanlist(arg_apikey, shardurl, nwrecord['id']) 272 | if nwvlanlist[0]['id'] == 'null': 273 | printusertext('WARNING: Skipping network "%s": No MX VLANs' % nwrecord['name']) 274 | else: 275 | orgvlans.append( {'nwname': nwrecord['name'], 'nwvlans': nwvlanlist} ) 276 | 277 | #write org MX VLANs' list to file 278 | try: 279 | json.dump(orgvlans, f) 280 | except: 281 | printusertext('ERROR: Writing to output file failed') 282 | sys.exit(2) 283 | else: 284 | #import mode 285 | 286 | #read org VLANs' list from file 287 | try: 288 | orgvlans = json.load(f) 289 | except: 290 | printusertext('ERROR: Reading from file failed') 291 | sys.exit(2) 292 | 293 | #upload MX VLAN configuration to Dashboard 294 | for nwrecord in orgvlans: 295 | #network ID might be different in destination org. get network id 296 | nwid = getnwid(arg_apikey, shardurl, orgid, nwrecord['nwname']) 297 | if nwid == 'null': 298 | printusertext('WARNING: Skipping network "%s": Does not exist' % nwrecord['nwname']) 299 | else: 300 | printusertext('INFO: Processing network "%s"' % nwrecord['nwname']) 301 | for vlanrecord in nwrecord['nwvlans']: 302 | #if VLAN exists, update it. else create VLAN 303 | vlanfound = getvlandetails(arg_apikey, shardurl, nwid, vlanrecord['id']) 304 | if vlanfound['id'] == 'null': 305 | #VLAN does not exist, create it 306 | createvlan(arg_apikey, shardurl, nwid, vlanrecord) 307 | elif mode_updateexisting == True: 308 | #if VLAN exists and user has selected to update existing 309 | updatevlan(arg_apikey, shardurl, nwid, vlanrecord) 310 | else: 311 | #if VLAN exists and user has selected to skip existing 312 | printusertext('WARNING: Skipping VLAN %s in network "%s": Already exists' % (vlanrecord['id'], nwrecord['nwname']) ) 313 | 314 | printusertext('End of script.') 315 | 316 | if __name__ == '__main__': 317 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /switchport_auditing/switchport_audit.py: -------------------------------------------------------------------------------- 1 | # --- MODULE CORE START --- 2 | 3 | import time 4 | 5 | from urllib.parse import urlencode 6 | from requests import Session, utils 7 | 8 | class NoRebuildAuthSession(Session): 9 | def rebuild_auth(self, prepared_request, response): 10 | """ 11 | This method is intentionally empty. Needed to prevent auth header stripping on redirect. More info: 12 | https://stackoverflow.com/questions/60358216/python-requests-post-request-dropping-authorization-header 13 | """ 14 | 15 | API_MAX_RETRIES = 3 16 | API_CONNECT_TIMEOUT = 60 17 | API_TRANSMIT_TIMEOUT = 60 18 | API_STATUS_RATE_LIMIT = 429 19 | API_RETRY_DEFAULT_WAIT = 3 20 | 21 | #Set to True or False to enable/disable console logging of sent API requests 22 | FLAG_REQUEST_VERBOSE = True 23 | 24 | API_BASE_URL = "https://api.meraki.com/api/v1" 25 | 26 | def merakiRequest(p_apiKey, p_httpVerb, p_endpoint, p_additionalHeaders=None, p_queryItems=None, 27 | p_requestBody=None, p_verbose=False, p_retry=0): 28 | #returns success, errors, responseHeaders, responseBody 29 | 30 | if p_retry > API_MAX_RETRIES: 31 | if(p_verbose): 32 | print("ERROR: Reached max retries") 33 | return False, None, None, None 34 | 35 | bearerString = "Bearer " + str(p_apiKey) 36 | headers = {"Authorization": bearerString} 37 | if not p_additionalHeaders is None: 38 | headers.update(p_additionalHeaders) 39 | 40 | query = "" 41 | if not p_queryItems is None: 42 | qArrayFix = {} 43 | for item in p_queryItems: 44 | if isinstance(p_queryItems[item], list): 45 | qArrayFix["%s[]" % item] = p_queryItems[item] 46 | else: 47 | qArrayFix[item] = p_queryItems[item] 48 | query = "?" + urlencode(qArrayFix, True) 49 | url = API_BASE_URL + p_endpoint + query 50 | 51 | verb = p_httpVerb.upper() 52 | 53 | session = NoRebuildAuthSession() 54 | 55 | verbs = { 56 | 'DELETE' : { 'function': session.delete, 'hasBody': False }, 57 | 'GET' : { 'function': session.get, 'hasBody': False }, 58 | 'POST' : { 'function': session.post, 'hasBody': True }, 59 | 'PUT' : { 'function': session.put, 'hasBody': True } 60 | } 61 | 62 | try: 63 | if(p_verbose): 64 | print(verb, url) 65 | 66 | if verb in verbs: 67 | if verbs[verb]['hasBody'] and not p_requestBody is None: 68 | r = verbs[verb]['function']( 69 | url, 70 | headers = headers, 71 | json = p_requestBody, 72 | timeout = (API_CONNECT_TIMEOUT, API_TRANSMIT_TIMEOUT) 73 | ) 74 | else: 75 | r = verbs[verb]['function']( 76 | url, 77 | headers = headers, 78 | timeout = (API_CONNECT_TIMEOUT, API_TRANSMIT_TIMEOUT) 79 | ) 80 | else: 81 | return False, None, None, None 82 | except: 83 | return False, None, None, None 84 | 85 | if(p_verbose): 86 | print(r.status_code) 87 | 88 | success = r.status_code in range (200, 299) 89 | errors = None 90 | responseHeaders = None 91 | responseBody = None 92 | 93 | if r.status_code == API_STATUS_RATE_LIMIT: 94 | retryInterval = API_RETRY_DEFAULT_WAIT 95 | if "Retry-After" in r.headers: 96 | retryInterval = r.headers["Retry-After"] 97 | if "retry-after" in r.headers: 98 | retryInterval = r.headers["retry-after"] 99 | 100 | if(p_verbose): 101 | print("INFO: Hit max request rate. Retrying %s after %s seconds" % (p_retry+1, retryInterval)) 102 | time.sleep(int(retryInterval)) 103 | success, errors, responseHeaders, responseBody = merakiRequest(p_apiKey, p_httpVerb, p_endpoint, p_additionalHeaders, 104 | p_queryItems, p_requestBody, p_verbose, p_retry+1) 105 | return success, errors, responseHeaders, responseBody 106 | 107 | try: 108 | rjson = r.json() 109 | except: 110 | rjson = None 111 | 112 | if not rjson is None: 113 | if "errors" in rjson: 114 | errors = rjson["errors"] 115 | if(p_verbose): 116 | print(errors) 117 | else: 118 | responseBody = rjson 119 | 120 | if "Link" in r.headers: 121 | parsedLinks = utils.parse_header_links(r.headers["Link"]) 122 | for link in parsedLinks: 123 | if link["rel"] == "next": 124 | if(p_verbose): 125 | print("Next page:", link["url"]) 126 | splitLink = link["url"].split("/api/v1") 127 | success, errors, responseHeaders, nextBody = merakiRequest(p_apiKey, p_httpVerb, splitLink[1], 128 | p_additionalHeaders=p_additionalHeaders, 129 | p_requestBody=p_requestBody, 130 | p_verbose=p_verbose) 131 | if success: 132 | if not responseBody is None: 133 | responseBody = responseBody + nextBody 134 | else: 135 | responseBody = None 136 | 137 | return success, errors, responseHeaders, responseBody 138 | 139 | # --- MODULE CORE END --- 140 | 141 | # getOrganizations 142 | # 143 | # Description: List the organizations that the user has privileges on 144 | # Endpoint: GET /organizations 145 | # 146 | # Endpoint documentation: https://developer.cisco.com/meraki/api-v1/#!get-organizations 147 | 148 | 149 | def getOrganizations(apiKey): 150 | url = "/organizations" 151 | success, errors, headers, response = merakiRequest(apiKey, "get", url, p_verbose=FLAG_REQUEST_VERBOSE) 152 | return success, errors, response 153 | 154 | # getOrganizationInventoryDevices 155 | # 156 | # Description: Return the device inventory for an organization 157 | # Endpoint: GET /organizations/{organizationId}/inventory/devices 158 | # 159 | # Endpoint documentation: https://developer.cisco.com/meraki/api-v1/#!get-organization-inventory-devices 160 | # 161 | # Query parameters: 162 | # perPage: Integer. The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. 163 | # startingAfter: String. A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 164 | # endingBefore: String. A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 165 | # usedState: String. Filter results by used or unused inventory. Accepted values are 'used' or 'unused'. 166 | # search: String. Search for devices in inventory based on serial number, mac address, or model. 167 | # macs: Array. Search for devices in inventory based on mac addresses. 168 | # networkIds: Array. Search for devices in inventory based on network ids. 169 | # serials: Array. Search for devices in inventory based on serials. 170 | # models: Array. Search for devices in inventory based on model. 171 | # orderNumbers: Array. Search for devices in inventory based on order numbers. 172 | # tags: Array. Filter devices by tags. The filtering is case-sensitive. If tags are included, 'tagsFilterType' should also be included (see below). 173 | # tagsFilterType: String. To use with 'tags' parameter, to filter devices which contain ANY or ALL given tags. Accepted values are 'withAnyTags' or 'withAllTags', default is 'withAnyTags'. 174 | # productTypes: Array. Filter devices by product type. Accepted values are appliance, camera, cellularGateway, sensor, switch, systemsManager, and wireless. 175 | # licenseExpirationDate: String. Filter devices by license expiration date, ISO 8601 format. To filter with a range of dates, use 'licenseExpirationDate[