├── .gitignore ├── CiscoLive └── UploadCustomWebhookTemplate.py ├── Installing Python on Windows.txt ├── LICENSE.md ├── README.md ├── RadiusCertSurvey ├── RadiusCertSurvey.py ├── RadiusCertSurveyResults.csv └── readme.md ├── addroutes ├── addroutes.py └── routes.txt ├── android_patch_audit ├── android_patch_audit.py └── config.yaml ├── asa_cryptomap_converter ├── README.md ├── asa_config_parser_module.py └── cryptomap_converter.py ├── audit_client_tracking.py ├── auto-cycle-port ├── auto-cycle-port.py └── config.yaml ├── auto_combine_networks.py ├── auto_reboot.py ├── autovpn_tunnel_count.py ├── backup_configs ├── backup_GET_operations.csv ├── backup_configs.py ├── defaults │ ├── alerts_settings.json │ ├── alerts_settings_template.json │ ├── appliance_connectivity_monitoring_destinations.json │ ├── appliance_content_filtering.json │ ├── appliance_firewall_cellular_firewall_rules.json │ ├── appliance_firewall_firewalled_services.json │ ├── appliance_firewall_l3_firewall_rules.json │ ├── appliance_security_intrusion.json │ ├── appliance_security_malware.json │ ├── appliance_single_lan.json │ ├── appliance_traffic_shaping.json │ ├── appliance_traffic_shaping_rules.json │ ├── appliance_traffic_shaping_uplink_bandwidth.json │ ├── appliance_traffic_shaping_uplink_bandwidth_2.json │ ├── appliance_traffic_shaping_uplink_selection.json │ ├── appliance_vpn_site_to_site_vpn.json │ ├── cellular_gateway_connectivity_monitoring_destinations.json │ ├── cellular_gateway_dhcp.json │ ├── cellular_gateway_subnet_pool.json │ ├── cellular_gateway_uplink.json │ ├── management_interface.json │ ├── settings.json │ ├── snmp.json │ ├── switch_access_control_lists.json │ ├── switch_dhcp_server_policy.json │ ├── switch_dscp_to_cos_mappings.json │ ├── switch_mtu.json │ ├── switch_routing_multicast.json │ ├── switch_settings.json │ ├── switch_settings_template.json │ ├── switch_stp.json │ ├── traffic_analysis.json │ ├── wireless_settings.json │ ├── wireless_ssid_firewall_l3_firewall_rules_ssid_0.json │ ├── wireless_ssid_splash_settings_ssid_0.json │ ├── wireless_ssid_traffic_shaping_rules_ssid_0.json │ └── wireless_ssids.json ├── requirements.txt ├── restore_configs.py └── restore_operations.csv ├── bssid.py ├── checksubnets.py ├── clientcount.py ├── clients_in_ip_range.py ├── clone_port_configs.py ├── cloneprovision ├── cloneprovision.py └── input.txt ├── copymxvlans.py ├── copynetworks.py ├── copyswitchcfg.py ├── daily_crossings.py ├── deployappliance.py ├── deploycustomer.py ├── deploydevices.py ├── deviceupdownstatus.py ├── disable_status_page.py ├── eos_scanner.py ├── export_mx_l3.py ├── export_mx_s2svpn.py ├── find_clients.py ├── find_ports.py ├── firmware_lock ├── config.yaml.example └── firmware_lock.py ├── getNetworks.rb ├── get_license_capacities_csv.py ├── get_license_info.py ├── getbeacons.py ├── googletimezonetest.py ├── import_mx_l3.py ├── import_mx_s2svpn.py ├── inventorycsv.py ├── invlist.py ├── latest_devices.py ├── license_counts_csv.py ├── listip.py ├── manage_firmware └── firmware_upgrade_manager.py ├── manageadmins.py ├── merakidevicecounts.py ├── merakilicensealert.py ├── mi_bom_tool.py ├── migrate_cat3k ├── migrate_cat3k.py └── migrate_cat3k_init_example.txt ├── migrate_devices └── migrate_devices.py ├── migrate_networks ├── config.yaml └── migrate_networks.py ├── migratecomware.py ├── migration_init_file.txt ├── movedevices.py ├── mr_mqtt_monitoring.py ├── mv_gp.py ├── mx_firewall_control ├── README.md ├── mxfirewallcontrol.py └── mxfirewallcontrol_example_input_file.txt ├── nodejs_sdk_builder ├── endpoint.template ├── nodejs_sdk_builder.py ├── readme.md └── sdk_core.template ├── offline_logging ├── README.md ├── config.yaml └── offline_logging.py ├── org_subnets.py ├── orgclientscsv.py ├── postman_collection_generator.py ├── provision_sites ├── provision_sites.py ├── provision_sites_example_input_file.csv └── provision_sites_input_file_generator.xlsx ├── reboot.py ├── removetemplate.py ├── report_statuses.py ├── rotate_psks └── psk_rotator.py ├── secure_connect ├── Private_Applications_CSV_Import │ ├── README.md │ ├── privateApplicationsImport.csv │ └── privateApplicationsImport.py └── Remote_Access_Logs_Analyzer │ ├── README.md │ └── remoteAccessLogsAnalyzer.py ├── setSwitchPortOnMacOui ├── cmdlist.txt ├── ouilist.txt └── setSwitchPortOnMacOui.py ├── set_client_tracking.py ├── setlocation.py ├── setlocation_legacy.py ├── setssidvlanid.py ├── tag_all_ports.py ├── topusers ├── templates │ └── index.html └── topusers.py ├── update_ports.py ├── uplink.py ├── usagestats.py ├── usagestats_initconfig.txt └── usagestats_manual.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | backup_configs/.DS_Store 4 | backup_configs/defaults/.DS_Store 5 | 6 | # Ignore firmware_lock configuration file 7 | firmware_lock/config.yaml 8 | -------------------------------------------------------------------------------- /CiscoLive/UploadCustomWebhookTemplate.py: -------------------------------------------------------------------------------- 1 | # this script, for a given 2 | # - API key 3 | # - NetworkID 4 | # - Webhook Template Name 5 | # creates a new custom webhook template 6 | # API information available here: https://developer.cisco.com/meraki/webhooks/#!custom-payload-templates-overview 7 | # precreated templates available here: https://github.com/meraki/webhook-payload-templates 8 | # further training available here: https://developer.cisco.com/learning/labs/meraki-webhook-template-editor-intro/create-your-custom-webhook-template/ 9 | # postman collection: https://www.postman.com/meraki-api/workspace/cisco-meraki-s-public-workspace/collection/897512-c65299ed-39a5-4b02-bb4e-933c738bfcdf?action=share&creator=897512&ctx=documentation 10 | 11 | import sys, requests, json, getopt 12 | 13 | 14 | def main(argv): 15 | ARG_APIKEY = '' 16 | ARG_NAME = '' 17 | ARG_NETWORKID = '' 18 | 19 | try: 20 | opts, args = getopt.getopt(argv, 'hk:n:i:') 21 | except getopt.GetoptError: 22 | printhelp() 23 | sys.exit(2) 24 | 25 | for opt, arg in opts: 26 | if opt == '-h': 27 | printhelp() 28 | sys.exit() 29 | elif opt == '-k': 30 | ARG_APIKEY = arg 31 | elif opt == '-n': 32 | ARG_NETWORKID = arg 33 | elif opt == '-i': 34 | ARG_NAME = arg 35 | 36 | # check that all mandatory arguments have been given 37 | if ARG_APIKEY == '' or ARG_NETWORKID == '' or ARG_NAME == '': 38 | printhelp() 39 | sys.exit(2) 40 | 41 | webhookTemplatePayload = "{\n\"version\": \"0.1\",\n\"sharedSecret\": \"{{sharedSecret}}\",\n\"sentAt\": \"{{sentAt}}\",\n\"organizationId\": \"{{organizationId}}\",\n\"organizationName\": \"{{organizationName}}\",\n\"organizationUrl\": \"{{organizationUrl}}\",\n\"networkId\": \"{{networkId}}\",\n\"networkName\": \"{{networkName}}\",\n\"networkUrl\": \"{{networkUrl}}\",\n\"networkTags\": {{ networkTags | jsonify }},\n\"deviceSerial\": \"{{deviceSerial}}\",\n\"deviceMac\": \"{{deviceMac}}\",\n\"deviceName\": \"{{deviceName}}\",\n\"deviceUrl\": \"{{deviceUrl}}\",\n\"deviceTags\": {{ deviceTags | jsonify }},\n\"deviceModel\": \"{{deviceModel}}\",\n\"alertId\": \"{{alertId}}\",\n\"alertType\": \"{{alertType}}\",\n\"alertTypeId\": \"{{alertTypeId}}\",\n\"alertLevel\": \"{{alertLevel}}\",\n\"occurredAt\": \"{{occurredAt}}\",\n\"alertData\": {{ alertData | jsonify }}\n}\n" 42 | 43 | url = "https://api.meraki.com/api/v1/networks/" + ARG_NETWORKID + "/webhooks/payloadTemplates" 44 | 45 | payload = json.dumps({ 46 | "name": ARG_NAME, 47 | "body": webhookTemplatePayload 48 | }) 49 | 50 | headers = { 51 | "Content-Type": "application/json", 52 | "Accept": "application/json", 53 | "X-Cisco-Meraki-API-Key": ARG_APIKEY 54 | } 55 | 56 | response = requests.request("POST", url, headers=headers, data=payload) 57 | 58 | print(response.status_code) 59 | print(response.text) 60 | 61 | 62 | def printhelp(): 63 | print("this script, for a given") 64 | print("-k API key") 65 | print("-n NetworkID") 66 | print("-i Webhook Template Name") 67 | print("creates a new custom webhook template") 68 | print("The only editing it needs is the webhook template payload") 69 | print("") 70 | print("API information available here: https://developer.cisco.com/meraki/webhooks/#!custom-payload-templates-overview") 71 | print("precreated templates available here: https://github.com/meraki/webhook-payload-templates") 72 | print("further training available here: https://developer.cisco.com/learning/labs/meraki-webhook-template-editor-intro/create-your-custom-webhook-template/") 73 | print("postman collection: https://www.postman.com/meraki-api/workspace/cisco-meraki-s-public-workspace/collection/897512-c65299ed-39a5-4b02-bb4e-933c738bfcdf?action=share&creator=897512&ctx=documentation") 74 | 75 | 76 | if __name__ == '__main__': 77 | main(sys.argv[1:]) 78 | -------------------------------------------------------------------------------- /Installing Python on Windows.txt: -------------------------------------------------------------------------------- 1 | Download Python 3 for Windows: https://www.python.org/downloads/windows/ 2 | 3 | Make sure your "python" and "python/scripts" folders are in %PATH%. For example if installing to "C:\python", edit %PATH% to include: 4 | c:\python 5 | c:\python\scripts 6 | 7 | Useful modules (install these): 8 | Requests: send/receive information to/from REST APIs. To install, run this command: pip install requests 9 | Meraki: some of the scripts are built to use the Meraki module. To install it run: pip install meraki 10 | Paramiko: interact with devices using SSH. Used in migration scripts. 11 | Installation info here: http://www.paramiko.org/installing.html 12 | PySNMP: interact with SNMP devices. To install, run this command: pip install pysnmp 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 | When installing PySNMP via pip on Python 3.6, you get PyCryptodomeX instead of PyCrypto, which is a good thing. 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019, Cisco Systems, Inc. and/or its affiliates 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 | -------------------------------------------------------------------------------- /RadiusCertSurvey/RadiusCertSurvey.py: -------------------------------------------------------------------------------- 1 | # This script, for a given API key, and network ID, does a survey of devices that has the 2 | # radius.meraki.com cert and flags devices that have an out of date cert (the date of which) 3 | # is requested from the user 4 | 5 | # please ensure that you also have created the RadiusCertSurveyResults.csv file 6 | # 7 | # Mandatory arguments: 8 | # -k : Your Meraki Dashboard API Key 9 | # -n networkID : Your Meraki network ID 10 | # optional arguments: 11 | # -v : turn on verbose mode 12 | 13 | # Pre requisites: 14 | # Meraki library : pip install meraki : https://developer.cisco.com/meraki/api/#/python/getting-started 15 | 16 | import meraki 17 | from datetime import datetime 18 | import logging, sys, getopt 19 | 20 | def main(argv): 21 | 22 | print("Meraki Library version: ") 23 | print(meraki.__version__) 24 | 25 | arg_apikey = False 26 | 27 | fileForResults = "RadiusCertSurveyResults.csv" 28 | 29 | loggingEnabled = False 30 | verbose = False 31 | 32 | try: 33 | opts, args = getopt.getopt(argv, 'k:n:vl') 34 | except getopt.GetOptError: 35 | printhelp(argv) 36 | sys.exit(2) 37 | 38 | for opt, arg in opts: 39 | if opt == '-k': 40 | arg_apikey = arg 41 | elif opt == '-n': 42 | arg_networkId = arg 43 | elif opt == '-v': 44 | verbose = True 45 | elif opt == '-l': 46 | loggingEnabled = True 47 | 48 | while True: 49 | try: 50 | # Note: Python 2.x users should use raw_input, the equivalent of 3.x's input 51 | print("Please enter the expected radius.meraki.com expiration date") 52 | rawDateInput = input("in the format of YYYY-MM-DD : ") 53 | expirationDate = datetime.strptime(rawDateInput, "%Y-%m-%d").date() 54 | 55 | except ValueError: 56 | print("The date really does need to be in the format of YYYY-MM-DD.") 57 | # better try again... Return to the start of the loop 58 | continue 59 | else: 60 | # input successfully converted to a date 61 | break 62 | 63 | # Create Meraki Client Object and initialise 64 | client = meraki.DashboardAPI(api_key=arg_apikey) 65 | 66 | # the URL parameter gives us a shortcut directly into Meraki Dashboard 67 | devices = client.sm.getNetworkSmDevices(networkId=arg_networkId, fields=["url"]) 68 | 69 | for device in devices: 70 | deviceId = device["id"] 71 | deviceInfo = "***********************************************************************************************\n" 72 | deviceInfo = deviceInfo + device["name"] + " serial : " + device["serialNumber"] + "\n" 73 | deviceInfo = deviceInfo + "URL to device : " + device["url"] 74 | 75 | certs = client.sm.getNetworkSmDeviceCerts(networkId=arg_networkId, deviceId=deviceId) 76 | if len(certs) > 0: 77 | updatedCert = False 78 | 79 | certsArray = [] # we'll add the certs to this just in case the user has verbose turned on 80 | for cert in certs: 81 | if cert["name"] == "radius.meraki.com": 82 | certExpDate = datetime.strptime(cert["notValidAfter"], "%Y-%m-%dT%H:%M:%S.%f%z").date() 83 | certsArray += str(certExpDate) + " " 84 | if certExpDate > expirationDate: 85 | updatedCert = True 86 | if updatedCert: 87 | if verbose: 88 | print(deviceInfo) 89 | writeToFile(fileForResults, deviceInfo) 90 | writeToFile(fileForResults, certsArray) 91 | print(certsArray) 92 | writeToFile(fileForResults, "we've got an updated cert\n") 93 | print("we've got an updated cert\n") 94 | else: 95 | print(deviceInfo) 96 | writeToFile(fileForResults, deviceInfo) 97 | writeToFile(fileForResults, certsArray) 98 | print(certsArray) 99 | writeToFile(fileForResults, "bad news, no updated cert\n") 100 | print("bad news, no updated cert\n") 101 | 102 | 103 | def printhelp(): 104 | # prints help information 105 | print('This is a script that, for a given network and API key') 106 | print('Asks the user for a radius.meraki.com certificate expiration date') 107 | print('For every managed device, it then gets all of the radius.meraki.com certs for that device') 108 | print('And highlights any devices that do NOT have the update cert') 109 | print('Its probably best that you use a cert date of EXPECTED-1, so...') 110 | print('If the radius.meraki.com cert expiration date is 2023-11-30, input 2023-11-29') 111 | print('') 112 | print('Mandatory arguments:') 113 | print(' -k : Your Meraki Dashboard API key') 114 | print(' -n network ID : Your Meraki Dashboard Network ID that has your managed devices in') 115 | print('Optional arguments:') 116 | print(' -v :Turn on Verbose mode') 117 | 118 | 119 | def writeToLog(MessageToLog, toLog): 120 | if toLog: 121 | logging.warning(MessageToLog) 122 | 123 | 124 | def writeToFile(passedFile, messagetoWrite): 125 | openFileForRead = open(passedFile, 'r') 126 | fileContents = openFileForRead.read() 127 | openFileForRead.close() 128 | openedFile = open(passedFile, 'w') 129 | openedFile.writelines(fileContents) 130 | openedFile.writelines('\n') 131 | openedFile.writelines(messagetoWrite) 132 | openedFile.close() 133 | 134 | 135 | if __name__ == '__main__': 136 | main(sys.argv[1:]) 137 | -------------------------------------------------------------------------------- /RadiusCertSurvey/RadiusCertSurveyResults.csv: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /RadiusCertSurvey/readme.md: -------------------------------------------------------------------------------- 1 | This script, for a given API key, and network ID, does a survey of devices that has the 2 | radius.meraki.com cert and flags devices that have an out of date cert (the date of which) 3 | is requested from the user 4 | 5 | Mandatory arguments: 6 | 7 | -k : Your Meraki Dashboard API Key 8 | 9 | -n networkID : Your Meraki network ID 10 | 11 | optional arguments: 12 | 13 | -v : turn on verbose mode 14 | 15 | Pre requisites: 16 | Meraki library : pip install meraki : https://developer.cisco.com/meraki/api/#/python/getting-started 17 | 18 | Included files: 19 | 20 | RadiusCertSurvey.csv The output file 21 | 22 | RadiusCertSurveyResults.py The Python scrips 23 | 24 | readme.md This file 25 | -------------------------------------------------------------------------------- /addroutes/addroutes.py: -------------------------------------------------------------------------------- 1 | readMe = """ 2 | This is a script to add static routes to a network from a CSV file. 3 | 4 | Usage: 5 | python addroutes.py -k -o -n -f 6 | 7 | Example: 8 | python addroutes.py -k 1234 -o "My Beautiful Company" -n "VPN Hub" -f routes.csv 9 | 10 | Input file example: 11 | https://github.com/meraki/automation-scripts/blob/master/addroutes/routes.txt 12 | 13 | Notes: 14 | * In Windows, use double quotes ("") to enter command line parameters containing spaces. 15 | * This script was built for Python 3.7.1. 16 | * Depending on your operating system, the command to start python can be either "python" or "python3". 17 | 18 | Required Python modules: 19 | Requests : http://docs.python-requests.org 20 | After installing Python, you can install these additional modules using pip with the following commands: 21 | pip install requests 22 | 23 | Depending on your operating system, the command can be "pip3" instead of "pip". 24 | """ 25 | 26 | import sys, getopt, requests, time, datetime, ipaddress, json 27 | 28 | #SECTION: GLOBAL VARIABLES: MODIFY TO CHANGE SCRIPT BEHAVIOUR 29 | 30 | API_EXEC_DELAY = 0.21 #Used in merakiRequestThrottler() to avoid hitting dashboard API max request rate 31 | 32 | #connect and read timeouts for the Requests module in seconds 33 | REQUESTS_CONNECT_TIMEOUT = 90 34 | REQUESTS_READ_TIMEOUT = 90 35 | 36 | #SECTION: GLOBAL VARIABLES AND CLASSES: DO NOT MODIFY 37 | 38 | LAST_MERAKI_REQUEST = datetime.datetime.now() #used by merakiRequestThrottler() 39 | 40 | 41 | #SECTION: General use functions 42 | 43 | 44 | def merakiRequestThrottler(): 45 | #prevents hitting max request rate shaper of the Meraki Dashboard API 46 | global LAST_MERAKI_REQUEST 47 | 48 | if (datetime.datetime.now()-LAST_MERAKI_REQUEST).total_seconds() < (API_EXEC_DELAY): 49 | time.sleep(API_EXEC_DELAY) 50 | 51 | LAST_MERAKI_REQUEST = datetime.datetime.now() 52 | return 53 | 54 | 55 | def printhelp(): 56 | print(readMe) 57 | 58 | 59 | def loadFile(p_fileName): 60 | returnValue = [] 61 | 62 | try: 63 | f = open(p_fileName, 'r') 64 | 65 | for line in f: 66 | if len(line) > 0: 67 | stripped = line.strip() 68 | if len(stripped) > 0: 69 | if stripped[0] != "#": 70 | returnValue.append(stripped) 71 | 72 | f.close() 73 | except: 74 | print('ERROR 01: Error loading file "%s"' % p_fileName) 75 | return None 76 | 77 | return returnValue 78 | 79 | 80 | def parseCSV(p_csv): 81 | retval = [] 82 | unnamed = 0 83 | for line in p_csv: 84 | splitStr = line.split(',') 85 | lenSplit = len(splitStr) 86 | try: 87 | if splitStr[0].find('/') == -1: 88 | raise 'Destination must contain a "/"' 89 | destination = str(ipaddress.IPv4Network(splitStr[0].strip(), False)) 90 | gateway = str(ipaddress.IPv4Address(splitStr[1].strip())) 91 | except: 92 | print('ERROR 02: Input file line not valid: "%s"' % line) 93 | return None 94 | if len(splitStr) > 2: 95 | name = splitStr[2].strip() 96 | else: 97 | name = "addroutes.py " + str(unnamed) 98 | unnamed += 1 99 | 100 | retval.append({"name":name, "subnet":destination, "gatewayIp":gateway}) 101 | 102 | return retval 103 | 104 | 105 | #SECTION: Meraki Dashboard API communication functions 106 | 107 | 108 | def getOrgId(p_apiKey, p_orgName): 109 | #returns the organizations' list for a specified admin, with filters applied 110 | 111 | merakiRequestThrottler() 112 | try: 113 | r = requests.get('https://api.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 114 | except: 115 | return(None) 116 | 117 | if r.status_code != requests.codes.ok: 118 | return(None) 119 | 120 | rjson = r.json() 121 | 122 | for org in rjson: 123 | if org['name'] == p_orgName: 124 | return org['id'] 125 | 126 | return(None) 127 | 128 | 129 | def getNetId(p_apiKey, p_orgId, p_shard, p_netName): 130 | merakiRequestThrottler() 131 | 132 | requestUrl = "https://%s/api/v0/organizations/%s/networks" % (p_shard, p_orgId) 133 | 134 | try: 135 | r = requests.get(requestUrl, headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 136 | except: 137 | return(None) 138 | 139 | if r.status_code != requests.codes.ok: 140 | return(None) 141 | 142 | rjson = r.json() 143 | 144 | for net in rjson: 145 | if net['name'] == p_netName: 146 | return net['id'] 147 | 148 | return(None) 149 | 150 | 151 | def getShardHost(p_apiKey, p_orgId): 152 | #patch 153 | return("api.meraki.com") 154 | 155 | 156 | def addRoute(p_apiKey, p_shard, p_netId, p_routeData): 157 | 158 | merakiRequestThrottler() 159 | 160 | requestUrl = "https://%s/api/v0/networks/%s/staticRoutes" % (p_shard, p_netId) 161 | 162 | if True: 163 | r = requests.post(requestUrl, data=json.dumps(p_routeData), headers={'X-Cisco-Meraki-API-Key': p_apiKey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 164 | else: 165 | print('ERROR 03: Unable to contact Meraki cloud') 166 | return False 167 | 168 | if r.status_code >= 400: 169 | print ("ERROR 04: %s" % r.json()['errors'][0]) 170 | return False 171 | return True 172 | 173 | 174 | #SECTION: main 175 | 176 | def main(argv): 177 | argApiKey = "" 178 | argOrgName = "" 179 | argNetName = "" 180 | argFile = "" 181 | 182 | #get command line arguments 183 | try: 184 | opts, args = getopt.getopt(argv, 'hk:o:n:f:') 185 | except getopt.GetoptError: 186 | printhelp() 187 | sys.exit(2) 188 | 189 | for opt, arg in opts: 190 | if opt == '-h': 191 | printhelp() 192 | sys.exit() 193 | elif opt == '-k': 194 | argApiKey = arg 195 | elif opt == '-o': 196 | argOrgName = arg 197 | elif opt == '-n': 198 | argNetName = arg 199 | elif opt == '-f': 200 | argFile = arg 201 | 202 | #make sure all mandatory arguments have been given 203 | if argApiKey == '' or argOrgName == '' or argNetName == '' or argFile == '': 204 | printhelp() 205 | sys.exit(2) 206 | 207 | rawFile = loadFile(argFile) 208 | if rawFile is None: 209 | sys.exit(2) 210 | 211 | routeList = parseCSV(rawFile) 212 | if routeList is None: 213 | sys.exit(2) 214 | 215 | orgId = getOrgId(argApiKey, argOrgName) 216 | if orgId is None: 217 | print("ERROR 05: Unable to fetch organization Id") 218 | sys.exit(2) 219 | print ('Organization ID is %s' % orgId) 220 | 221 | shard = getShardHost(argApiKey, orgId) 222 | if shard is None: 223 | print("WARNING: Shard host is None") 224 | shard = "api.meraki.com" 225 | 226 | netId = getNetId(argApiKey, orgId, shard, argNetName) 227 | if netId is None: 228 | print("ERROR 06: Unable to fetch network Id") 229 | sys.exit(2) 230 | print ('Network ID is %s' % netId) 231 | 232 | for route in routeList: 233 | print("Adding route: %s" % json.dumps(route)) 234 | r = addRoute(argApiKey, shard, netId, route) 235 | 236 | 237 | if __name__ == '__main__': 238 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /addroutes/routes.txt: -------------------------------------------------------------------------------- 1 | # Lines beginning with # are comments and will not be processed. 2 | # 3 | # Valid subnet definitions: 4 | # /, , 5 | # /, 6 | # /, , 7 | # /, 8 | # Examples: 9 | # 192.168.100.33/255.255.255.0, 192.168.128.3, User network 10 | # 10.33.33.20/24, 192.168.128.4, Lab network 11 | 12 | 192.168.100.33/255.255.255.0, 192.168.128.3, User network 13 | 10.33.33.20/24, 192.168.128.4, Lab network -------------------------------------------------------------------------------- /android_patch_audit/config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for android_patch_audit.py. You can find the script here: 2 | # https://github.com/meraki/automation-scripts/blob/master/android_patch_audit/android_patch_audit.py 3 | 4 | # You can find an example configuration file here: 5 | # https://github.com/meraki/automation-scripts/blob/master/android_patch_audit/config.yaml 6 | 7 | # For instructions on how to generate a Meraki Dashboard API key and enable API access for your 8 | # organization, please read this documentation article: 9 | # https://documentation.meraki.com/General_Administration/Other_Topics/Cisco_Meraki_Dashboard_API 10 | 11 | general: 12 | # The Meraki Dashboard API key of your administrator account 13 | apiKey: 1234 14 | 15 | # The name of the Meraki dashboard organization you want to interact with 16 | organizationName: Big Industries Inc 17 | 18 | # By default the script will run multiple scans at scheduled intervals until manually terminated 19 | # If you want the script to only run once and then quit, set enableScheduler to false 20 | enableScheduler: true 21 | runIntervalHours: 24 22 | 23 | # How many devices you want the script to attempt to process at a time. The larger the batch size, 24 | # the less time one scan cycle will take. Increase the value of processingBatchSize to have a scan 25 | # cycle complete faster, or decrease it if API requests for tagging devices start failing due to 26 | # the URL query string being too long 27 | processingBatchSize: 100 28 | 29 | policy: 30 | # How many days old an Android device's security patch can be before it is marked as a violation 31 | maximumAndroidSecurityPatchAgeDays: 60 32 | 33 | reporting: 34 | # Whether to print out a list of violating devices. Set enabled: false to disable 35 | enabled: true 36 | 37 | enforcement: 38 | # Options for automatically adding and removing tags to devices to reflect their compliance/violation status 39 | 40 | tagCompliantDevices: 41 | # Apply tag to compliant devices. The tag is removed for devices that are no longer compliant 42 | # Set "enabled: true" or "enabled: false" to enable or disable this feature and "tag" to specify 43 | # the tag to be added 44 | enabled: true 45 | tag: android-patch-compliant 46 | 47 | tagViolatingDevices: 48 | # Apply tag to violating devices. The tag is removed for devices that are no longer violating 49 | # Set "enabled: true" or "enabled: false" to enable or disable this feature and "tag" to specify 50 | # the tag to be added 51 | enabled: true 52 | tag: android-patch-violating -------------------------------------------------------------------------------- /asa_cryptomap_converter/README.md: -------------------------------------------------------------------------------- 1 | # ASA Cryptomap Converter 2 | 3 | A tool to import cryptomap-based site-to-site VPN tunnel entries from an ASA configuration file to a Meraki dashboard organization. 4 | -------------------------------------- 5 | 6 | # Important notes 7 | 8 | * The script will only import VPN tunnel configuration as third-party VPN peers, not firewalling rules 9 | * The script has been built as a MVP to convert a very specific ASA 9.8(4)20 configuration. Using it with other configurations may require modification of the script 10 | * The script consists of two files, **cryptomap_converter.py** and **asa_config_parser_module.py**, which need to be in the same folder for the script to run. The one you run to initiate the script is **cryptomap_converter.py** 11 | 12 | # Prerequisites 13 | 14 | 1. Install Python 3 and the requests module. You can find them here 15 | * https://www.python.org/ 16 | * https://docs.python-requests.org 17 | 18 | 2. Enable site-to-site VPN in Meraki dashboard by making your MX security appliance a hub or a spoke: https://documentation.meraki.com/MX/Site-to-site_VPN/Site-to-Site_VPN_Settings 19 | 3. If you want to limit availability of the converted tunnels to certain MXs in your organization, create a network tag and associate it with their networks: https://documentation.meraki.com/General_Administration/Organizations_and_Networks/Organization_Menu/Manage_Tags 20 | 21 | 22 | # Usage instructions 23 | 24 | Script syntax, Windows: 25 | ``` 26 | python cryptomap_converter.py [-k ] [-o ] [-f ] [-t ] 27 | ``` 28 | 29 | Script syntax, Linux and Mac: 30 | ``` 31 | python3 cryptomap_converter.py [-k ] [-o ] [-f ] [-t ] 32 | ``` 33 | 34 | Optional arguments: 35 | * `-k `: Your Meraki Dashboard API key. If omitted, the script will try to use one stored in OS environment variable MERAKI_DASHBOARD_API_KEY 36 | * `-o `: The name of the organization to pull the OpenAPI spec from. This parameter can be omitted if your API key can only access one organization 37 | * `-f `: The name of the ASA configuration input file. If omitted, "asa.cfg" will be used as default 38 | * `-t `: The name of the network tag you want to make the converted tunnels available to. If omitted, default availability is "All networks" 39 | 40 | Example, convert configuration stored in file "asa.cfg" into organization with name "Big Industries Inc" and 41 | make it available to MXs in networks tagged "asa-vpn": 42 | ``` 43 | python cryptomap_converter.py -k 1234 -o "Big Industries Inc" -t asa-vpn 44 | ``` 45 | -------------------------------------------------------------------------------- /auto-cycle-port/config.yaml: -------------------------------------------------------------------------------- 1 | # YAML configuration file for auto-cycle-port.yaml 2 | # You can find the script here: https://github.com/meraki/automation-scripts/tree/master/auto-cycle-port 3 | 4 | # Your Meraki Dashboard API key. It needs to have full org-wide privileges 5 | apiKey: 1234 6 | 7 | # The name of the organization you wish to run this script for 8 | organizationName: Big Industries Inc 9 | 10 | # The model of device you want to track up/down status of. For example: MG21 11 | trackedDeviceModel: MG21 12 | 13 | # The number of the switchport to cycle. The switch needs to be part of the same network 14 | # as the tracked device. Only one switch per network will be modified. 15 | targetPortNumber: 8 16 | 17 | # How often to scan for unreachable devices and cycle ports 18 | scanIntervalMinutes: 15 19 | 20 | # Path to the log file to use for persistent log output. Filename only will place the file in 21 | # the same folder as the script 22 | logFilePath: auto-cycle.log 23 | -------------------------------------------------------------------------------- /backup_configs/defaults/alerts_settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultDestinations": { 3 | "emails": [], 4 | "snmp": false, 5 | "allAdmins": true, 6 | "httpServerIds": [] 7 | }, 8 | "alerts": [ 9 | { 10 | "type": "gatewayDown", 11 | "enabled": false, 12 | "alertDestinations": { 13 | "emails": [], 14 | "snmp": false, 15 | "allAdmins": false, 16 | "httpServerIds": [] 17 | }, 18 | "filters": { 19 | "timeout": 60 20 | } 21 | }, 22 | { 23 | "type": "gatewayToRepeater", 24 | "enabled": false, 25 | "alertDestinations": { 26 | "emails": [], 27 | "snmp": false, 28 | "allAdmins": false, 29 | "httpServerIds": [] 30 | }, 31 | "filters": {} 32 | }, 33 | { 34 | "type": "repeaterDown", 35 | "enabled": false, 36 | "alertDestinations": { 37 | "emails": [], 38 | "snmp": false, 39 | "allAdmins": false, 40 | "httpServerIds": [] 41 | }, 42 | "filters": { 43 | "timeout": 60 44 | } 45 | }, 46 | { 47 | "type": "rogueAp", 48 | "enabled": false, 49 | "alertDestinations": { 50 | "emails": [], 51 | "snmp": false, 52 | "allAdmins": false, 53 | "httpServerIds": [] 54 | }, 55 | "filters": {} 56 | }, 57 | { 58 | "type": "settingsChanged", 59 | "enabled": false, 60 | "alertDestinations": { 61 | "emails": [], 62 | "snmp": false, 63 | "allAdmins": false, 64 | "httpServerIds": [] 65 | }, 66 | "filters": {} 67 | }, 68 | { 69 | "type": "vpnConnectivityChange", 70 | "enabled": false, 71 | "alertDestinations": { 72 | "emails": [], 73 | "snmp": false, 74 | "allAdmins": false, 75 | "httpServerIds": [] 76 | }, 77 | "filters": {} 78 | }, 79 | { 80 | "type": "usageAlert", 81 | "enabled": false, 82 | "alertDestinations": { 83 | "emails": [], 84 | "snmp": false, 85 | "allAdmins": false, 86 | "httpServerIds": [] 87 | }, 88 | "filters": { 89 | "threshold": 104857600, 90 | "period": 1200 91 | } 92 | }, 93 | { 94 | "type": "weeklyPresence", 95 | "enabled": false, 96 | "alertDestinations": { 97 | "emails": [], 98 | "snmp": false, 99 | "allAdmins": false, 100 | "httpServerIds": [] 101 | }, 102 | "filters": {} 103 | }, 104 | { 105 | "type": "ampMalwareDetected", 106 | "enabled": true, 107 | "alertDestinations": { 108 | "emails": [], 109 | "snmp": false, 110 | "allAdmins": false, 111 | "httpServerIds": [] 112 | }, 113 | "filters": {} 114 | }, 115 | { 116 | "type": "ampMalwareBlocked", 117 | "enabled": false, 118 | "alertDestinations": { 119 | "emails": [], 120 | "snmp": false, 121 | "allAdmins": false, 122 | "httpServerIds": [] 123 | }, 124 | "filters": {} 125 | }, 126 | { 127 | "type": "applianceDown", 128 | "enabled": false, 129 | "alertDestinations": { 130 | "emails": [], 131 | "snmp": false, 132 | "allAdmins": false, 133 | "httpServerIds": [] 134 | }, 135 | "filters": { 136 | "timeout": 60 137 | } 138 | }, 139 | { 140 | "type": "failoverEvent", 141 | "enabled": false, 142 | "alertDestinations": { 143 | "emails": [], 144 | "snmp": false, 145 | "allAdmins": false, 146 | "httpServerIds": [] 147 | }, 148 | "filters": {} 149 | }, 150 | { 151 | "type": "dhcpNoLeases", 152 | "enabled": false, 153 | "alertDestinations": { 154 | "emails": [], 155 | "snmp": false, 156 | "allAdmins": false, 157 | "httpServerIds": [] 158 | }, 159 | "filters": {} 160 | }, 161 | { 162 | "type": "rogueDhcp", 163 | "enabled": false, 164 | "alertDestinations": { 165 | "emails": [], 166 | "snmp": false, 167 | "allAdmins": false, 168 | "httpServerIds": [] 169 | }, 170 | "filters": {} 171 | }, 172 | { 173 | "type": "ipConflict", 174 | "enabled": false, 175 | "alertDestinations": { 176 | "emails": [], 177 | "snmp": false, 178 | "allAdmins": false, 179 | "httpServerIds": [] 180 | }, 181 | "filters": {} 182 | }, 183 | { 184 | "type": "cellularUpDown", 185 | "enabled": false, 186 | "alertDestinations": { 187 | "emails": [], 188 | "snmp": false, 189 | "allAdmins": false, 190 | "httpServerIds": [] 191 | }, 192 | "filters": {} 193 | }, 194 | { 195 | "type": "clientConnectivity", 196 | "enabled": false, 197 | "alertDestinations": { 198 | "emails": [], 199 | "snmp": false, 200 | "allAdmins": false, 201 | "httpServerIds": [] 202 | }, 203 | "filters": { 204 | "clients": [] 205 | } 206 | }, 207 | { 208 | "type": "vrrp", 209 | "enabled": false, 210 | "alertDestinations": { 211 | "emails": [], 212 | "snmp": false, 213 | "allAdmins": false, 214 | "httpServerIds": [] 215 | }, 216 | "filters": {} 217 | }, 218 | { 219 | "type": "portDown", 220 | "enabled": false, 221 | "alertDestinations": { 222 | "emails": [], 223 | "snmp": false, 224 | "allAdmins": false, 225 | "httpServerIds": [] 226 | }, 227 | "filters": { 228 | "timeout": 60, 229 | "selector": "any port" 230 | } 231 | }, 232 | { 233 | "type": "powerSupplyDown", 234 | "enabled": false, 235 | "alertDestinations": { 236 | "emails": [], 237 | "snmp": false, 238 | "allAdmins": false, 239 | "httpServerIds": [] 240 | }, 241 | "filters": {} 242 | }, 243 | { 244 | "type": "rpsBackup", 245 | "enabled": false, 246 | "alertDestinations": { 247 | "emails": [], 248 | "snmp": false, 249 | "allAdmins": false, 250 | "httpServerIds": [] 251 | }, 252 | "filters": {} 253 | }, 254 | { 255 | "type": "udldError", 256 | "enabled": false, 257 | "alertDestinations": { 258 | "emails": [], 259 | "snmp": false, 260 | "allAdmins": false, 261 | "httpServerIds": [] 262 | }, 263 | "filters": {} 264 | }, 265 | { 266 | "type": "portError", 267 | "enabled": false, 268 | "alertDestinations": { 269 | "emails": [], 270 | "snmp": false, 271 | "allAdmins": false, 272 | "httpServerIds": [] 273 | }, 274 | "filters": { 275 | "selector": "any port" 276 | } 277 | }, 278 | { 279 | "type": "portSpeed", 280 | "enabled": false, 281 | "alertDestinations": { 282 | "emails": [], 283 | "snmp": false, 284 | "allAdmins": false, 285 | "httpServerIds": [] 286 | }, 287 | "filters": { 288 | "selector": "any port" 289 | } 290 | }, 291 | { 292 | "type": "newDhcpServer", 293 | "enabled": false, 294 | "alertDestinations": { 295 | "emails": [], 296 | "snmp": false, 297 | "allAdmins": false, 298 | "httpServerIds": [] 299 | }, 300 | "filters": {} 301 | }, 302 | { 303 | "type": "switchDown", 304 | "enabled": false, 305 | "alertDestinations": { 306 | "emails": [], 307 | "snmp": false, 308 | "allAdmins": false, 309 | "httpServerIds": [] 310 | }, 311 | "filters": { 312 | "timeout": 60 313 | } 314 | } 315 | ] 316 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_connectivity_monitoring_destinations.json: -------------------------------------------------------------------------------- 1 | { 2 | "destinations": [ 3 | { 4 | "ip": "8.8.8.8", 5 | "description": "Google", 6 | "default": true 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_content_filtering.json: -------------------------------------------------------------------------------- 1 | { 2 | "urlCategoryListSize": "fullList", 3 | "blockedUrlCategories": [], 4 | "blockedUrlPatterns": [], 5 | "allowedUrlPatterns": [] 6 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_firewall_cellular_firewall_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "comment": "Default rule", 5 | "policy": "allow", 6 | "protocol": "Any", 7 | "srcPort": "Any", 8 | "srcCidr": "Any", 9 | "destPort": "Any", 10 | "destCidr": "Any", 11 | "syslogEnabled": false 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_firewall_firewalled_services.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "service": "ICMP", 4 | "access": "unrestricted" 5 | }, 6 | { 7 | "service": "web", 8 | "access": "blocked" 9 | } 10 | ] -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_firewall_l3_firewall_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "comment": "Default rule", 5 | "policy": "allow", 6 | "protocol": "Any", 7 | "srcPort": "Any", 8 | "srcCidr": "Any", 9 | "destPort": "Any", 10 | "destCidr": "Any", 11 | "syslogEnabled": false 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_security_intrusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "disabled" 3 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_security_malware.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "disabled", 3 | "allowedUrls": [], 4 | "allowedFiles": [] 5 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_single_lan.json: -------------------------------------------------------------------------------- 1 | { 2 | "subnet": "192.168.128.0/24", 3 | "applianceIp": "192.168.128.1" 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_traffic_shaping.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalBandwidthLimits": { 3 | "limitUp": 0, 4 | "limitDown": 0 5 | } 6 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_traffic_shaping_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultRulesEnabled": true, 3 | "rules": [] 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_traffic_shaping_uplink_bandwidth.json: -------------------------------------------------------------------------------- 1 | { 2 | "bandwidthLimits": { 3 | "wan1": { 4 | "limitUp": 1000000, 5 | "limitDown": 1000000 6 | }, 7 | "cellular": { 8 | "limitUp": null, 9 | "limitDown": null 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_traffic_shaping_uplink_bandwidth_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "bandwidthLimits": { 3 | "wan1": { 4 | "limitUp": 1000000, 5 | "limitDown": 1000000 6 | }, 7 | "wan2": { 8 | "limitUp": 1000000, 9 | "limitDown": 1000000 10 | }, 11 | "cellular": { 12 | "limitUp": null, 13 | "limitDown": null 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_traffic_shaping_uplink_selection.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultUplink": "wan1", 3 | "loadBalancingEnabled": false, 4 | "wanTrafficUplinkPreferences": [], 5 | "vpnTrafficUplinkPreferences": [] 6 | } -------------------------------------------------------------------------------- /backup_configs/defaults/appliance_vpn_site_to_site_vpn.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "none" 3 | } -------------------------------------------------------------------------------- /backup_configs/defaults/cellular_gateway_connectivity_monitoring_destinations.json: -------------------------------------------------------------------------------- 1 | { 2 | "destinations": [ 3 | { 4 | "ip": "8.8.8.8", 5 | "default": true, 6 | "description": "Google" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /backup_configs/defaults/cellular_gateway_dhcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "dhcpLeaseTime": "1 day", 3 | "dnsNameservers": "upstream_dns", 4 | "dnsCustomNameservers": [] 5 | } -------------------------------------------------------------------------------- /backup_configs/defaults/cellular_gateway_subnet_pool.json: -------------------------------------------------------------------------------- 1 | { 2 | "cidr": "172.31.128.0/24", 3 | "mask": 26, 4 | "subnets": [] 5 | } -------------------------------------------------------------------------------- /backup_configs/defaults/cellular_gateway_uplink.json: -------------------------------------------------------------------------------- 1 | { 2 | "bandwidthLimits": { 3 | "limitUp": null, 4 | "limitDown": null 5 | } 6 | } -------------------------------------------------------------------------------- /backup_configs/defaults/management_interface.json: -------------------------------------------------------------------------------- 1 | { 2 | "wan1": { 3 | "usingStaticIp": false, 4 | "vlan": null 5 | } 6 | } -------------------------------------------------------------------------------- /backup_configs/defaults/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "localStatusPageEnabled": true, 3 | "remoteStatusPageEnabled": false 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/snmp.json: -------------------------------------------------------------------------------- 1 | { 2 | "access": "none" 3 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_access_control_lists.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "comment": "Default rule", 5 | "policy": "allow", 6 | "ipVersion": "any", 7 | "protocol": "any", 8 | "srcCidr": "any", 9 | "srcPort": "any", 10 | "dstCidr": "any", 11 | "dstPort": "any", 12 | "vlan": "any" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_dhcp_server_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultPolicy": "allow", 3 | "blockedServers": [] 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_dscp_to_cos_mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": [ 3 | { 4 | "dscp": 0, 5 | "cos": 0, 6 | "title": "default" 7 | }, 8 | { 9 | "dscp": 10, 10 | "cos": 0, 11 | "title": "AF11" 12 | }, 13 | { 14 | "dscp": 18, 15 | "cos": 1, 16 | "title": "AF21" 17 | }, 18 | { 19 | "dscp": 26, 20 | "cos": 2, 21 | "title": "AF31" 22 | }, 23 | { 24 | "dscp": 34, 25 | "cos": 3, 26 | "title": "AF41" 27 | }, 28 | { 29 | "dscp": 46, 30 | "cos": 3, 31 | "title": "EF voice" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_mtu.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultMtuSize": 9578, 3 | "overrides": [] 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_routing_multicast.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSettings": { 3 | "igmpSnoopingEnabled": true, 4 | "floodUnknownMulticastTrafficEnabled": true 5 | }, 6 | "overrides": [] 7 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlan": 1, 3 | "useCombinedPower": false, 4 | "powerExceptions": [] 5 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlan": 1 3 | } -------------------------------------------------------------------------------- /backup_configs/defaults/switch_stp.json: -------------------------------------------------------------------------------- 1 | { 2 | "rstpEnabled": true, 3 | "stpBridgePriority": [] 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/traffic_analysis.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "basic", 3 | "customPieChartItems": [] 4 | } -------------------------------------------------------------------------------- /backup_configs/defaults/wireless_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "meshingEnabled": true, 3 | "ipv6BridgeEnabled": true, 4 | "locationAnalyticsEnabled": true, 5 | "ledLightsOn": true, 6 | "upgradeStrategy": "minimizeUpgradeTime" 7 | } -------------------------------------------------------------------------------- /backup_configs/defaults/wireless_ssid_firewall_l3_firewall_rules_ssid_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "comment": "Wireless clients accessing LAN", 5 | "policy": "deny", 6 | "protocol": "Any", 7 | "destPort": "Any", 8 | "destCidr": "Local LAN" 9 | }, 10 | { 11 | "comment": "Default rule", 12 | "policy": "allow", 13 | "protocol": "Any", 14 | "destPort": "Any", 15 | "destCidr": "Any" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /backup_configs/defaults/wireless_ssid_splash_settings_ssid_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "ssidNumber": 0, 3 | "splashMethod": "None", 4 | "splashUrl": null, 5 | "useSplashUrl": false, 6 | "splashTimeout": 1440, 7 | "redirectUrl": null, 8 | "useRedirectUrl": false, 9 | "welcomeMessage": null, 10 | "splashLogo": { 11 | "md5": null 12 | }, 13 | "splashImage": { 14 | "md5": null 15 | }, 16 | "splashPrepaidFront": { 17 | "md5": null 18 | } 19 | } -------------------------------------------------------------------------------- /backup_configs/defaults/wireless_ssid_traffic_shaping_rules_ssid_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "trafficShapingEnabled": true, 3 | "defaultRulesEnabled": true, 4 | "rules": [] 5 | } -------------------------------------------------------------------------------- /backup_configs/requirements.txt: -------------------------------------------------------------------------------- 1 | meraki==1.40.1 2 | pyyaml==5.4 3 | requests==2.32.0 4 | -------------------------------------------------------------------------------- /bssid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import meraki 4 | from pathlib import Path 5 | import json 6 | 7 | read_me = ''' 8 | A Python 3 script to pull of the enabled BSSID from an Organization. 9 | 10 | Required Python modules: 11 | meraki 1.48.0 or higher 12 | 13 | Usage: 14 | bssid.py 15 | 16 | If you have only one Organization, it will find all BSSID 17 | and create a csv for each network in Documents/BSSID/Organization 18 | 19 | If you have multiple Organizations, it will ask you which org to run against 20 | 21 | API Key 22 | requires you to have your API key in env vars as 'MERAKI_DASHBOARD_API_KEY' 23 | 24 | ''' 25 | 26 | p = Path.home() 27 | loc = p / 'Documents' / 'BSSID' 28 | 29 | dashboard = meraki.DashboardAPI(suppress_logging=True) 30 | 31 | 32 | def base_folder(): 33 | ''' 34 | Check if the root folder exists and create it if not 35 | ''' 36 | if not Path.is_dir(loc): 37 | Path.mkdir(loc) 38 | 39 | 40 | def get_orgs(): 41 | ''' 42 | get a list of organizations the user has access to and return that dict 43 | ''' 44 | orgs = dashboard.organizations.getOrganizations() 45 | org_dict = {} 46 | for i in orgs: 47 | org_dict[i['id']] = i['name'] 48 | return org_dict 49 | 50 | 51 | def find_org(org_dict): 52 | ''' 53 | If only one organizaiton exists, use that org_id 54 | ''' 55 | if len(org_dict) == 1: 56 | org_id = org_dict[0]['id'] 57 | org_name = org_dict[0]['name'] 58 | else: 59 | ''' 60 | If there are multiple organizations, ask the use which one to use 61 | then store that information to be used 62 | ''' 63 | org_id = input( 64 | f"Please type the number of the Organization you want to find " 65 | f"the bssid in{json.dumps(org_dict, indent=4)}" "\n") 66 | org_name = org_dict.get(org_id) 67 | return org_id, org_name 68 | 69 | 70 | def org_folder(org_name): 71 | ''' 72 | check if the organizaiton folder exists, create if not 73 | ''' 74 | loc2 = Path.joinpath(loc, org_name) 75 | if not Path.is_dir(loc2): 76 | Path.mkdir(loc2) 77 | 78 | 79 | def get_networks(org_id): 80 | net_list = dashboard.organizations.getOrganizationNetworks( 81 | org_id, total_pages='all') 82 | print(net_list) 83 | return net_list 84 | 85 | 86 | def find_networks(net_list): 87 | net_ids = {} 88 | for i in net_list: 89 | if 'wireless' in i['productTypes']: 90 | net_ids[i['id']] = i['name'] 91 | return net_ids 92 | 93 | 94 | def get_bssid(org_id, net_ids): 95 | ''' 96 | dump the BSSID list for the organization 97 | ''' 98 | bssid_dict = dashboard.wireless.getOrganizationWirelessSsidsStatusesByDevice\ 99 | (org_id, total_pages='all') 100 | return bssid_dict 101 | 102 | 103 | def file_writer(bssid_dict, net_ids, org_name): 104 | print(f'writing BSSID to file') 105 | for k, v in net_ids.items(): 106 | net_name = v 107 | file = f'{loc}/{org_name}/{net_name}.csv' 108 | with open(file, mode='w') as f: 109 | f.write(f"AP Name , SSID Name , Frequency , BSSID, AP Serial" + "\n") 110 | for ap in bssid_dict['items']: 111 | network = ap['network']['name'] 112 | if net_name == network: 113 | for bss in ap['basicServiceSets']: 114 | f.write(f"{ap['name']}, " 115 | f"{bss['ssid']['name']}, " 116 | f"{bss['radio']['band']} GHz, " 117 | f"{bss['bssid']}, " 118 | f"{ap['serial']}" + "\n") 119 | print(f'Your file {net_name}.csv has been created in {loc}/{org_name}') 120 | 121 | 122 | def main(): 123 | base_folder() 124 | org_dict = get_orgs() 125 | org_id, org_name = find_org(org_dict) 126 | org_folder(org_name) 127 | net_list = get_networks(org_id) 128 | net_ids = find_networks(net_list) 129 | bssid_dict = get_bssid(org_id, net_ids) 130 | file_writer(bssid_dict, net_ids, org_name) 131 | 132 | 133 | if __name__ == '__main__': 134 | main() 135 | -------------------------------------------------------------------------------- /clone_port_configs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 6 | 7 | Fill out OLD_MODEL, NEW_MODEL, and optionally SKIPPED_NAMES below (lines 29-31) 8 | OLD_MODEL = model number of switch to be swapped out 9 | NEW_MODEL = model number of switch to be swapped in 10 | SKIPPED_NAMES = exact names of any new model switches to be left untouched 11 | 12 | Install both requests & Meraki Dashboard API Python modules: 13 | pip[3] install --upgrade requests 14 | pip[3] install --upgrade meraki 15 | 16 | === DESCRIPTION === 17 | This script iterates through the org's networks that are tagged with the label 18 | "migrate". For each of these networks' OLD_MODEL switch, a corresponding 19 | NEW_MODEL with the same name is expected in the network. The switch port 20 | configuration per port per OLD_MODEL is then copied over to the corresponding 21 | port on the NEW_MODEL. Remove the network tag/label "migrate" afterwards. 22 | 23 | === USAGE === 24 | python[3] clone_port_configs.py -k -o [-m ] 25 | The optional -m parameter is either "simulate" (default) to only print changes, 26 | or "commit" to also apply those changes to Dashboard. 27 | ''' 28 | 29 | OLD_MODEL = 'MS225-48FP' 30 | NEW_MODEL = 'MS250-48FP' 31 | SKIPPED_NAMES = ['EXACT name of switch'] 32 | 33 | from datetime import datetime 34 | import getopt 35 | import logging 36 | import sys 37 | from meraki import meraki 38 | 39 | # Prints READ_ME help message for user to read 40 | def print_help(): 41 | lines = READ_ME.split('\n') 42 | for line in lines: 43 | print('# {0}'.format(line)) 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | def configure_logging(): 48 | logging.basicConfig( 49 | filename='{}_log_{:%Y%m%d_%H%M%S}.txt'.format(sys.argv[0].split('.')[0], datetime.now()), 50 | level=logging.DEBUG, 51 | format='%(asctime)s: %(levelname)7s: [%(name)s]: %(message)s', 52 | datefmt='%Y-%m-%d %H:%M:%S' 53 | ) 54 | 55 | 56 | def main(argv): 57 | # Set default values for command line arguments 58 | api_key = org_id = arg_mode = None 59 | 60 | # Get command line arguments 61 | try: 62 | opts, args = getopt.getopt(argv, 'hk:o:m:') 63 | except getopt.GetoptError: 64 | print_help() 65 | sys.exit(2) 66 | for opt, arg in opts: 67 | if opt == '-h': 68 | print_help() 69 | sys.exit() 70 | elif opt == '-k': 71 | api_key = arg 72 | elif opt == '-o': 73 | org_id = arg 74 | elif opt == '-m': 75 | arg_mode = arg 76 | 77 | # Check if all required parameters have been input 78 | if api_key == None or org_id == None: 79 | print_help() 80 | sys.exit(2) 81 | 82 | # Assign default mode to "simulate" unless "commit" specified 83 | if arg_mode != 'commit': 84 | arg_mode = 'simulate' 85 | 86 | # Get list of current networks in org 87 | networks = meraki.getnetworklist(api_key, org_id) 88 | 89 | # Iterate through all networks 90 | for network in networks: 91 | 92 | # Skip if network does not have the tag "migrate" 93 | if network['tags'] is None or 'migrate' not in network['tags']: 94 | continue 95 | 96 | # Iterate through a "migrate" network's switches 97 | devices = meraki.getnetworkdevices(api_key, network['id']) 98 | 99 | # Use two dictionaries to keep track of names (keys) and serials (values) 100 | old_switches = {} 101 | new_switches = {} 102 | for device in devices: 103 | if device['model'] == OLD_MODEL: 104 | old_switches[device['name']] = device['serial'] 105 | elif device['model'] == NEW_MODEL: 106 | new_switches[device['name']] = device['serial'] 107 | 108 | # Check to make sure there actually are new switches in this network 109 | if len(new_switches) == 0: 110 | logger.error('{0} has no {1} switches, so skipping'.format(network['name'], NEW_MODEL)) 111 | continue 112 | else: 113 | logger.info('Cloning configs for network {0}'.format(network['name'])) 114 | 115 | # For networks where new switches have been added with matching names 116 | for name in new_switches.keys(): 117 | if name in SKIPPED_NAMES: 118 | continue 119 | 120 | # Lookup serial numbers 121 | old_switch = old_switches[name] 122 | new_switch = new_switches[name] 123 | logger.info('Cloning configs from {0} {1} to {2} {3}'.format(OLD_MODEL, old_switch, NEW_MODEL, new_switch)) 124 | 125 | # Port 1 through 54 (48 LAN, 4 uplinks, 2 stacking, +1 for range ending index) 126 | for port in range(1, 48+4+2+1): 127 | config = meraki.getswitchportdetail(api_key, old_switch, port) 128 | 129 | # Clone corresponding new switch 130 | if arg_mode == 'commit': 131 | # Tags needed to be input as a list 132 | if config['tags'] is not None: 133 | tags = config['tags'].split() 134 | else: 135 | tags = [] 136 | 137 | # Access type port 138 | if config['type'] == 'access': 139 | meraki.updateswitchport(api_key, new_switch, port, 140 | name=config['name'], tags=tags, enabled=config['enabled'], 141 | porttype=config['type'], vlan=config['vlan'], voicevlan=config['voiceVlan'], 142 | poe=config['poeEnabled'], isolation=config['isolationEnabled'], rstp=config['rstpEnabled'], 143 | stpguard=config['stpGuard'], accesspolicynum=config['accessPolicyNumber']) 144 | # Trunk type port 145 | elif config['type'] == 'trunk': 146 | meraki.updateswitchport(api_key, new_switch, port, 147 | name=config['name'], tags=tags, enabled=config['enabled'], 148 | porttype=config['type'], vlan=config['vlan'], allowedvlans=config['allowedVlans'], 149 | poe=config['poeEnabled'], isolation=config['isolationEnabled'], rstp=config['rstpEnabled'], 150 | stpguard=config['stpGuard']) 151 | logger.info('Switch port {0} config cloned'.format(port)) 152 | else: 153 | logger.info('Switch port {0} config clone simulated'.format(port)) 154 | 155 | 156 | if __name__ == '__main__': 157 | # Configure logging to stdout 158 | configure_logging() 159 | # Define a Handler which writes INFO messages or higher to the sys.stderr 160 | console = logging.StreamHandler() 161 | console.setLevel(logging.INFO) 162 | # Set a format which is simpler for console use 163 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 164 | # Tell the handler to use this format 165 | console.setFormatter(formatter) 166 | # Add the handler to the root logger 167 | logging.getLogger('').addHandler(console) 168 | 169 | # Output to logfile/console starting inputs 170 | start_time = datetime.now() 171 | logger.info('Started script at {0}'.format(start_time)) 172 | inputs = sys.argv[1:] 173 | try: 174 | key_index = inputs.index('-k') 175 | except ValueError: 176 | print_help() 177 | sys.exit(2) 178 | inputs.pop(key_index+1) 179 | inputs.pop(key_index) 180 | logger.info('Input parameters: {0}'.format(inputs)) 181 | 182 | main(sys.argv[1:]) 183 | 184 | # Finish output to logfile/console 185 | end_time = datetime.now() 186 | logger.info('Ended script at {0}'.format(end_time)) 187 | logger.info(f'Total run time = {end_time - start_time}') 188 | -------------------------------------------------------------------------------- /cloneprovision/input.txt: -------------------------------------------------------------------------------- 1 | # Example input file for cloneprovision.py. Get the script here: 2 | # https://github.com/meraki/automation-scripts/blob/master/cloneprovision/cloneprovision.py 3 | # 4 | # Lines beginning with # are comments and will be ignored. 5 | # 6 | # Input data format: 7 | # ,,:,:,etc... 8 | # 9 | # Example: 10 | # AAAA-BBBB-CCCC,New branch,10:192.168.1.0/24,20:192.168.2.0/24,30:192.168.3.0/24 11 | 12 | AAAA-BBBB-CCCC, New branch, 10:192.168.0.0/24 13 | BBBB-CCCC-DDDD, Sales office, 10:192.168.10.0/24, 20:10.10.10.0/24 14 | -------------------------------------------------------------------------------- /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 | import sys, getopt, requests, json 21 | 22 | def printusertext(p_message): 23 | #prints a line of text that is meant for the user to read 24 | #do not process these lines when chaining scripts 25 | print('@ %s' % p_message) 26 | 27 | def printhelp(): 28 | #prints help text 29 | 30 | printusertext('This is a script that copies networks and their base attributes from a source organization') 31 | printusertext('to another, called the destination organization. Both source, destination org and file ') 32 | printusertext('parameters are optional, but at least two of them must be given.') 33 | printusertext('') 34 | printusertext('Usage:') 35 | printusertext('python copynetworks.py -k [-s ] [-d ] [-f ]') 36 | printusertext('') 37 | printusertext(" ** If '-s' and '-d' are given, data will be copied from src org to dst org") 38 | printusertext(" ** If '-s' and '-f' are given, data will be dumped from src org to file") 39 | printusertext(" ** If '-d' and '-f' are given, data will be imported from file to dst org") 40 | printusertext('') 41 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 42 | 43 | def getorgid(p_apikey, p_orgname): 44 | #looks up org id for a specific org name 45 | #on failure returns 'null' 46 | 47 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 48 | 49 | if r.status_code != requests.codes.ok: 50 | return 'null' 51 | 52 | rjson = r.json() 53 | 54 | 55 | for record in rjson: 56 | if record['name'] == p_orgname: 57 | return record['id'] 58 | return('null') 59 | 60 | def getshardurl(p_apikey, p_orgid): 61 | #quick-n-dirty patch 62 | return("api.meraki.com") 63 | 64 | def getnwlist(p_apikey, p_shardurl, p_orgid): 65 | #returns a list of all networks in an organization 66 | #on failure returns a single record with 'null' name and id 67 | 68 | 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'}) 69 | 70 | returnvalue = [] 71 | if r.status_code != requests.codes.ok: 72 | returnvalue.append({'name': 'null', 'id': 'null'}) 73 | return(returnvalue) 74 | 75 | return(r.json()) 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 | 112 | return('ok') 113 | 114 | 115 | def main(argv): 116 | #get command line arguments 117 | arg_apikey = 'null' 118 | arg_srcorg = 'null' 119 | arg_dstorg = 'null' 120 | arg_filepath = 'null' 121 | 122 | try: 123 | opts, args = getopt.getopt(argv, 'hk:s:d:f:') 124 | except getopt.GetoptError: 125 | printhelp() 126 | sys.exit(2) 127 | 128 | for opt, arg in opts: 129 | if opt == '-h': 130 | printhelp() 131 | sys.exit() 132 | elif opt == '-k': 133 | arg_apikey = arg 134 | elif opt == '-s': 135 | arg_srcorg = arg 136 | elif opt == '-d': 137 | arg_dstorg = arg 138 | elif opt == '-f': 139 | arg_filepath = arg 140 | 141 | #count how many optional parameters have been given 142 | optionscounter = 0 143 | if arg_srcorg != 'null': 144 | optionscounter += 1 145 | if arg_dstorg != 'null': 146 | optionscounter += 1 147 | if arg_filepath != 'null': 148 | optionscounter += 1 149 | 150 | if arg_apikey == 'null' or optionscounter < 2: 151 | printhelp() 152 | sys.exit(2) 153 | 154 | #get source organization id corresponding to org name provided by user 155 | mode_gotsource = True 156 | if arg_srcorg == 'null': 157 | mode_gotsource = False 158 | else: 159 | srcorgid = getorgid(arg_apikey, arg_srcorg) 160 | if srcorgid == 'null': 161 | printusertext('ERROR: Fetching source organization failed') 162 | sys.exit(2) 163 | #get shard URL where Org is stored 164 | srcshardurl = getshardurl(arg_apikey, srcorgid) 165 | if srcshardurl == 'null': 166 | printusertext('ERROR: Fetching Meraki cloud shard URL for source org failed') 167 | printusertext(' Does it have API access enabled?') 168 | sys.exit(2) 169 | 170 | #get destination organization id corresponding to org name provided by user 171 | mode_gotdestination = True 172 | if arg_dstorg == 'null': 173 | mode_gotdestination = False 174 | else: 175 | dstorgid = getorgid(arg_apikey, arg_dstorg) 176 | if dstorgid == 'null': 177 | printusertext('ERROR: Fetching destination organization failed') 178 | sys.exit(2) 179 | #get shard URL where Org is stored 180 | dstshardurl = getshardurl(arg_apikey, dstorgid) 181 | if dstshardurl == 'null': 182 | printusertext('ERROR: Fetching Meraki cloud shard URL for destination org failed') 183 | printusertext(' Does it have API access enabled?') 184 | sys.exit(2) 185 | 186 | #if user gave a source, fetch networks and their attributes from src org 187 | if mode_gotsource: 188 | nwlist = getnwlist(arg_apikey, srcshardurl, srcorgid) 189 | 190 | if nwlist[0]['id'] == 'null': 191 | printusertext('ERROR: Fetching network list from source org failed') 192 | sys.exit(2) 193 | 194 | #open buffer file for writing 195 | mode_gotfile = True 196 | if arg_filepath == 'null': 197 | mode_gotfile = False 198 | if mode_gotfile: 199 | #if source given, open file for writing (output) 200 | if mode_gotsource: 201 | try: 202 | f = open(arg_filepath, 'w') 203 | except: 204 | printusertext('ERROR: Unable to open file for writing') 205 | sys.exit(2) 206 | #if source omitted, open file for reading (input) 207 | else: 208 | try: 209 | f = open(arg_filepath, 'r') 210 | except: 211 | printusertext('ERROR: Unable to open file for reading') 212 | sys.exit(2) 213 | 214 | #if user gave a source and a file, dump source org networks to file 215 | if mode_gotsource and mode_gotfile: 216 | try: 217 | json.dump(nwlist, f) 218 | except: 219 | printusertext('ERROR: Writing to output file failed') 220 | sys.exit(2) 221 | 222 | #if user did not give source, but gave file, load networks list from file 223 | if not(mode_gotsource) and mode_gotfile: 224 | try: 225 | nwlist = json.load(f) 226 | except: 227 | printusertext('ERROR: Reading from input file failed') 228 | sys.exit(2) 229 | 230 | #if user gave destination org, create networks according to nwlist content 231 | if mode_gotdestination: 232 | i = 0 233 | for i in range (0, len(nwlist)): 234 | createnw (arg_apikey, dstshardurl, dstorgid, nwlist[i]) 235 | 236 | #reached end of script 237 | printusertext('End of script.') 238 | 239 | if __name__ == '__main__': 240 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /export_mx_l3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 with Meraki dashboard API Python library @ 6 | https://github.com/meraki/dashboard-api-python/ 7 | pip[3] install --upgrade meraki 8 | 9 | === DESCRIPTION === 10 | Exports CSV of MX L3 outbound firewall rules. 11 | 12 | === USAGE === 13 | python[3] export_mx_l3.py [-k ] -n 14 | 15 | API key can also be exported as an environment variable named 16 | MERAKI_DASHBOARD_API_KEY 17 | ''' 18 | 19 | 20 | import csv 21 | from datetime import datetime 22 | import getopt 23 | import os 24 | import sys 25 | 26 | import meraki 27 | 28 | 29 | # Prints READ_ME help message for user to read 30 | def print_help(): 31 | lines = READ_ME.split('\n') 32 | for line in lines: 33 | print('# {0}'.format(line)) 34 | 35 | 36 | def main(argv): 37 | # Set default values for command line arguments 38 | api_key = net_id = None 39 | 40 | # Get command line arguments 41 | try: 42 | opts, args = getopt.getopt(argv, 'hk:n:') 43 | except getopt.GetoptError: 44 | print_help() 45 | sys.exit(2) 46 | for opt, arg in opts: 47 | if opt == '-h': 48 | print_help() 49 | sys.exit() 50 | elif opt == '-k': 51 | api_key = arg 52 | elif opt == '-n': 53 | net_id = arg 54 | 55 | # Check if all required parameters have been input 56 | if (api_key == None and os.getenv('MERAKI_DASHBOARD_API_KEY') == None) or net_id == None: 57 | print_help() 58 | sys.exit(2) 59 | 60 | # Set the CSV output file and write the header row 61 | time_now = f'{datetime.now():%Y-%m-%d_%H-%M-%S}' 62 | file_name = f'mx_l3fw_rules__{time_now}.csv' 63 | output_file = open(file_name, mode='w', newline='\n') 64 | field_names = ['policy','protocol','srcCidr','srcPort','destCidr','destPort','comment','logging'] 65 | csv_writer = csv.writer(output_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 66 | csv_writer.writerow(field_names) 67 | 68 | # Dashboard API library class 69 | m = meraki.DashboardAPI(api_key=api_key, log_file_prefix=__file__[:-3]) 70 | 71 | # Read configuration of MX L3 firewall rules 72 | fw_rules = m.appliance.getNetworkApplianceFirewallL3FirewallRules(net_id) 73 | 74 | if 'rules' in fw_rules and len(fw_rules['rules']) > 0: 75 | # Loop through each firewall rule and write to CSV 76 | for rule in fw_rules['rules']: 77 | csv_row = [rule['policy'], rule['protocol'], rule['srcCidr'], rule['srcPort'], rule['destCidr'], rule['destPort'], rule['comment'], rule['syslogEnabled']] 78 | csv_writer.writerow(csv_row) 79 | 80 | output_file.close() 81 | print(f'Export completed to file {file_name}') 82 | else: 83 | print(f'No firewall rules to export') 84 | 85 | 86 | if __name__ == '__main__': 87 | main(sys.argv[1:]) 88 | -------------------------------------------------------------------------------- /export_mx_s2svpn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 6 | 7 | Install Requests, PyYAML and Meraki Dashboard API Python modules: 8 | pip[3] install --upgrade requests 9 | pip[3] install --upgrade meraki 10 | pip[3] install --upgrade pyyaml 11 | 12 | === DESCRIPTION === 13 | Exports YAML of MX site-to-site VPN rules from Dashboard network. 14 | 15 | === USAGE === 16 | python export_mx_s2svpn.py -k -o 17 | ''' 18 | 19 | 20 | from datetime import datetime 21 | import getopt 22 | import sys 23 | import os 24 | import meraki 25 | import yaml 26 | 27 | # Prints READ_ME help message for user to read 28 | def print_help(): 29 | lines = READ_ME.split('\n') 30 | for line in lines: 31 | print('# {0}'.format(line)) 32 | 33 | 34 | def main(argv): 35 | # Set default values for command line arguments 36 | api_key = org_id = None 37 | 38 | # Get command line arguments 39 | try: 40 | opts, args = getopt.getopt(argv, 'hk:o:') 41 | except getopt.GetoptError: 42 | print_help() 43 | sys.exit(2) 44 | for opt, arg in opts: 45 | if opt == '-h': 46 | print_help() 47 | sys.exit() 48 | elif opt == '-k': 49 | api_key = arg 50 | elif opt == '-o': 51 | org_id = arg 52 | 53 | # Check if all required parameters have been input 54 | if api_key == None or org_id == None: 55 | print_help() 56 | sys.exit(2) 57 | 58 | dashboard = meraki.DashboardAPI( 59 | api_key=api_key, 60 | base_url='https://api.meraki.com/api/v1/', 61 | output_log=True, 62 | log_file_prefix=os.path.basename(__file__)[:-3], 63 | log_path='', 64 | print_console=False 65 | ) 66 | 67 | # Set the output file 68 | timenow = '{:%Y%m%d_%H%M%S}'.format(datetime.now()) 69 | filename = 'mx_s2svpnfw_rules_{0}.yaml'.format(timenow) 70 | 71 | # Read Dashboard configuration of MX L3 firewall rules 72 | fw_rules = dashboard.appliance.getOrganizationApplianceVpnVpnFirewallRules(org_id) 73 | 74 | print(fw_rules) 75 | 76 | with open(filename, 'w') as file: 77 | documents = yaml.dump(fw_rules, file) 78 | 79 | print('Export completed to file {0}'.format(filename)) 80 | 81 | 82 | if __name__ == '__main__': 83 | inputs = sys.argv[1:] 84 | try: 85 | key_index = inputs.index('-k') 86 | except ValueError: 87 | print_help() 88 | sys.exit(2) 89 | 90 | main(sys.argv[1:]) 91 | -------------------------------------------------------------------------------- /firmware_lock/config.yaml.example: -------------------------------------------------------------------------------- 1 | # Configuration file for script "firmware_lock.py" 2 | # You can find the latest version of the script and an example configuration file here: 3 | # https://github.com/meraki/automation-scripts/tree/master/firmware_lock 4 | 5 | general: 6 | # Your Meraki Dashboard API key. You can find more information about API keys here: 7 | # https://documentation.meraki.com/General_Administration/Other_Topics/Cisco_Meraki_Dashboard_API 8 | apiKey: 1234 9 | 10 | # Set applyToAllOrganizations to true to apply firmware lock settings to all organizations your API key can access 11 | # If set to true, organizationName will be ignored 12 | applyToAllOrganizations: false 13 | 14 | # If not using applyToAllOrganizations: true, edit the line below to match the name of the organization you want to lock 15 | organizationName: Big Industries Inc 16 | 17 | # Set runOnce to true to run the script once and exit (suitable for use in a cron job). If set to true, scanIntervalHours 18 | # will be ignored 19 | runOnce: false 20 | scanIntervalHours: 6 21 | 22 | lockTrain: 23 | # Use the options below to lock a product type to a specific release train, or leave a value blank to have it unlocked. 24 | # Valid values: 25 | # stable 26 | # beta 27 | # release candidate 28 | # If both a lockTrain and a lockVersion configuration is applied for a product type, lockVersion takes precedence 29 | MG: 30 | MR: 31 | MS: 32 | MT: 33 | MV: 34 | MX: stable 35 | 36 | lockVersion: 37 | # Use the options below to lock a product type to a specific release version, or leave a value blank to have it unlocked. 38 | # If both a lockTrain and a lockVersion configuration is applied for a product type, lockVersion takes precedence 39 | MG: 40 | MR: 41 | MS: 42 | MT: 43 | MV: 44 | MX: 15.44 45 | -------------------------------------------------------------------------------- /getNetworks.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday_middleware' 3 | require 'json' 4 | 5 | # Define the following vars in your bash/zsh profile 6 | # 7 | # export DASHBOARD_API_KEY='api-key-here' 8 | # export DASHBOARD_API_SHARD_ID='XX' 9 | # export DASHBOARD_API_ORG_ID='X' 10 | 11 | dash_api_key = ENV['DASHBOARD_API_KEY'] 12 | dash_org_id = ENV['DASHBOARD_API_ORG_ID'] 13 | dash_shard_id = ENV['DASHBOARD_API_SHARD_ID'] 14 | 15 | 16 | conn = Faraday.new(:url => "https://#{dash_shard_id}.meraki.com") do |faraday| 17 | faraday.request :url_encoded 18 | faraday.response :json 19 | faraday.adapter Faraday.default_adapter 20 | end 21 | 22 | response = conn.get do |request| 23 | request.url "api/v0/organizations/#{dash_org_id}/networks" 24 | request.headers['X-Cisco-Meraki-API-Key'] = "#{dash_api_key}" 25 | request.headers['Content-Type'] = 'application/json' 26 | end 27 | 28 | hash_array = response.body 29 | 30 | hash_array.each do |x| 31 | puts "#{x['id']} :: #{x['name']}" 32 | end -------------------------------------------------------------------------------- /get_license_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import meraki 4 | from pathlib import Path 5 | import json 6 | 7 | read_me = ''' 8 | A Python 3 script to find the license status of an Organization. 9 | 10 | Required Python modules: 11 | meraki 1.48.0 or higher 12 | 13 | Usage: 14 | get_license_info.py 15 | 16 | If you have only one Organization, it will get the license status. 17 | 18 | If you have multiple Organizations, it will ask you which org to run against 19 | 20 | API Key 21 | requires you to have your API key in env vars as 'MERAKI_DASHBOARD_API_KEY' 22 | 23 | ''' 24 | 25 | p = Path.home() 26 | loc = p / 'Documents' / 'Meraki License' 27 | 28 | dashboard = meraki.DashboardAPI(suppress_logging=True) 29 | 30 | 31 | def base_folder(): 32 | ''' 33 | Check if the root folder exists and create it if not 34 | ''' 35 | if not Path.is_dir(loc): 36 | Path.mkdir(loc) 37 | 38 | 39 | def get_orgs(): 40 | ''' 41 | get a list of organizations the user has access to and return that dict 42 | ''' 43 | orgs = dashboard.organizations.getOrganizations() 44 | org_dict = {} 45 | for i in orgs: 46 | org_dict[i['id']] = i['name'] 47 | return org_dict 48 | 49 | 50 | def find_org(org_dict): 51 | ''' 52 | If only one organizaiton exists, use that org_id 53 | ''' 54 | if len(org_dict) == 1: 55 | org_id = org_dict[0]['id'] 56 | org_name = org_dict[0]['name'] 57 | else: 58 | ''' 59 | If there are multiple organizations, ask the use which one to use 60 | then store that information to be used 61 | ''' 62 | org_id = input( 63 | f"Please type the number of the Organization you want to find " 64 | f"the bssid in{json.dumps(org_dict, indent=4)}" "\n") 65 | org_name = org_dict.get(org_id) 66 | return org_id, org_name 67 | 68 | 69 | def get_license(org_id): 70 | lic_info = dashboard.organizations.getOrganizationLicensesOverview(org_id) 71 | return lic_info 72 | 73 | 74 | def file_writer(lic_info, org_name): 75 | print(f'writing License Information to file') 76 | file = f'{loc}/{org_name}.csv' 77 | status = lic_info['status'] 78 | expiration = lic_info['expirationDate'].replace(',', '') 79 | with open(file, mode='w') as f: 80 | f.write(f"Status, Expiration Date" + "\n") 81 | f.write(f"{status}, {expiration}" + "\n" + "\n") 82 | f.write(f"Licensed Devices" + "\n") 83 | for k, v in lic_info['licensedDeviceCounts'].items(): 84 | f.write(f"{k}, {v}" + "\n") 85 | print(f'Your file {org_name}.csv has been created in {loc}') 86 | 87 | 88 | def main(): 89 | base_folder() 90 | org_dict = get_orgs() 91 | org_id, org_name = find_org(org_dict) 92 | lic_info = get_license(org_id) 93 | file_writer(lic_info, org_name) 94 | 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /getbeacons.py: -------------------------------------------------------------------------------- 1 | # This script prints a list of all bluetooth beacons in an organization 2 | # to terminal/sdtout or a file (Devices which are part of a network are considered in-use). 3 | # The fields printed are separated by a comma (,) and include": 4 | # uuid, major, minor, name, address, lat lng 5 | # 6 | # You need to have Python 3 and the Requests module installed. You 7 | # can download the module here: https://github.com/kennethreitz/requests 8 | # or install it using pip. 9 | # 10 | # To run the script, enter: 11 | # python getbeacons.py -k -o [-f ] 12 | # 13 | # If option -f is not defined, the script will print to stdout. 14 | # 15 | # To make script chaining easier, all lines not containing a 16 | # device record start with the character @ 17 | # 18 | # This file was last modified on 2017-10-26 19 | 20 | import sys, getopt, requests, json 21 | 22 | 23 | def printusertext(p_message): 24 | # prints a line of text that is meant for the user to read 25 | # do not process these lines when chaining scripts 26 | print('@ %s' % p_message) 27 | 28 | 29 | def printhelp(): 30 | # prints help text 31 | 32 | printusertext("This is a script that prints a list of an organization's devices to sdtout or a file.") 33 | printusertext('') 34 | printusertext('Usage:') 35 | printusertext('python getbeacons.py -k -o [-f ]') 36 | printusertext('') 37 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 38 | 39 | 40 | def getorgid(p_apikey, p_orgname): 41 | # looks up org id for a specific org name 42 | # on failure returns 'null' 43 | 44 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', 45 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 46 | 47 | if r.status_code != requests.codes.ok: 48 | return 'null' 49 | 50 | rjson = r.json() 51 | 52 | for record in rjson: 53 | if record['name'] == p_orgname: 54 | return record['id'] 55 | return ('null') 56 | 57 | 58 | def getshardurl(p_apikey, p_orgid): 59 | # Looks up shard URL for a specific org. Use this URL instead of 'dashboard.meraki.com' 60 | # when making API calls with API accounts that can access multiple orgs. 61 | # On failure returns 'null' 62 | 63 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations/%s/snmp' % p_orgid, 64 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 65 | 66 | if r.status_code != requests.codes.ok: 67 | return 'null' 68 | 69 | rjson = r.json() 70 | 71 | return (rjson['hostname']) 72 | 73 | 74 | def getnwlist(p_apikey, p_shardurl, p_orgid): 75 | # returns a list of all networks in an organization 76 | # on failure returns a single record with 'null' name and id 77 | 78 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardurl, p_orgid), 79 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 80 | 81 | returnvalue = [] 82 | if r.status_code != requests.codes.ok: 83 | returnvalue.append({'name': 'null', 'id': 'null'}) 84 | return (returnvalue) 85 | 86 | return (r.json()) 87 | 88 | 89 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 90 | # returns a list of all devices in a network 91 | 92 | r = requests.get('https://%s/api/v0/networks/%s/devices' % (p_shardurl, p_nwid), 93 | 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({'lat': 'null', 'lng': '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 | for nwrecord in nwlist: 161 | # get devices' list 162 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 163 | # append list to file or stdout 164 | if filemode: 165 | for i in range(0, len(devicelist)): 166 | if 'beaconIdParams' in devicelist[i]: 167 | try: 168 | # MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 169 | f.write('%s,%d,%d,%s,%s,%d,%d\n' % (devicelist[i]['beaconIdParams']['uuid'], devicelist[i]['beaconIdParams']['major'], devicelist[i]['beaconIdParams']['minor'], devicelist[i]['name'], devicelist[i]['address'], devicelist[i]['lat'], devicelist[i]['lng'])) 170 | except: 171 | printusertext('ERROR: Unable to write device info to file') 172 | sys.exit(2) 173 | else: 174 | for i in range(0, len(devicelist)): 175 | if 'beaconIdParams' in devicelist[i]: 176 | # MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 177 | print('%s,%d,%d,%s,%s,%d,%d' % (devicelist[i]['beaconIdParams']['uuid'], devicelist[i]['beaconIdParams']['major'], devicelist[i]['beaconIdParams']['minor'], devicelist[i]['name'], devicelist[i]['address'], devicelist[i]['lat'], devicelist[i]['lng'])) 178 | 179 | 180 | if __name__ == '__main__': 181 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /googletimezonetest.py: -------------------------------------------------------------------------------- 1 | # This is an example script that gets the time zone that corresponds to a street address by using 2 | # Google Maps APIs. You can use this code to set network timezones in your Meraki Dashboard API scripts. 3 | # 4 | # This file was last modified on 2017-09-15 5 | 6 | import sys, getopt, requests, json, time 7 | 8 | def printhelp(): 9 | #prints help text 10 | 11 | print('Prints the time zone that corresponds to a street address by using Google Maps APIs') 12 | print('Syntax:') 13 | print(' python googletimezonetest.py -g -a
') 14 | print('') 15 | print('To successfully run this script you will need to have the following Google API services enabled:') 16 | print(' * Google Maps Geocoding API') 17 | print(' * Google Maps Time Zone API') 18 | print('') 19 | print('To enable Google API services visit: https://console.developers.google.com') 20 | 21 | def getgoogletimezone(p_googlekey, p_address): 22 | r = requests.get('https://maps.googleapis.com/maps/api/geocode/json?address=%s&key=%s' % (p_address, p_googlekey) ) 23 | 24 | rjson = r.json() 25 | 26 | if rjson['status'] != 'OK': 27 | return('null') 28 | 29 | glatitude = rjson['results'][0]['geometry']['location']['lat'] 30 | glongitude = rjson['results'][0]['geometry']['location']['lng'] 31 | 32 | s = requests.get('https://maps.googleapis.com/maps/api/timezone/json?location=%s,%s×tamp=%f&key=%s' % (glatitude, glongitude, time.time(), p_googlekey) ) 33 | 34 | sjson = s.json() 35 | 36 | if sjson['status'] == 'OK': 37 | return(sjson['timeZoneId']) 38 | 39 | return('null') 40 | 41 | def main(argv): 42 | 43 | #get command line arguments 44 | arg_address = '' 45 | arg_googlekey = '' 46 | 47 | try: 48 | opts, args = getopt.getopt(argv, 'hg:a:') 49 | except getopt.GetoptError: 50 | printhelp() 51 | sys.exit(2) 52 | 53 | for opt, arg in opts: 54 | if opt == '-h': 55 | printhelp() 56 | sys.exit() 57 | elif opt == '-g': 58 | arg_googlekey = arg 59 | elif opt == '-a': 60 | arg_address = arg 61 | 62 | if arg_googlekey == '' or arg_address == '': 63 | printhelp() 64 | sys.exit(2) 65 | 66 | gresponse = getgoogletimezone(arg_googlekey, arg_address) 67 | 68 | print(gresponse) 69 | 70 | 71 | 72 | if __name__ == '__main__': 73 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /import_mx_l3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 with Meraki dashboard API Python library @ 6 | https://github.com/meraki/dashboard-api-python/ 7 | pip[3] install --upgrade meraki 8 | 9 | === DESCRIPTION === 10 | Imports MX L3 outbound firewall rules from CSV file. 11 | Note that if there is a final "default rule" with logging enabled, then a 12 | syslog server needs to be configured on the Network-wide > General page. 13 | 14 | === USAGE === 15 | python[3] import_mx_l3.py [-k ] -n -f [-m ] 16 | The -f parameter is the path to the CSV file with the new MX L3 firewall rules. 17 | The optional -m parameter is either "simulate" (default) to only print changes, 18 | or "commit" to also apply those changes to the dashboard network. 19 | 20 | API key can also be exported as an environment variable named 21 | MERAKI_DASHBOARD_API_KEY 22 | ''' 23 | 24 | 25 | import csv 26 | from datetime import datetime 27 | import getopt 28 | import os 29 | import sys 30 | import ipaddress 31 | 32 | import meraki 33 | 34 | 35 | # Prints READ_ME help message for user to read 36 | def print_help(): 37 | lines = READ_ME.split('\n') 38 | for line in lines: 39 | print('# {0}'.format(line)) 40 | 41 | 42 | def main(argv): 43 | # Set default values for command line arguments 44 | api_key = net_id = arg_file = arg_mode = None 45 | 46 | # Get command line arguments 47 | try: 48 | opts, args = getopt.getopt(argv, 'hk:n:f:m:') 49 | except getopt.GetoptError: 50 | print_help() 51 | sys.exit(2) 52 | for opt, arg in opts: 53 | if opt == '-h': 54 | print_help() 55 | sys.exit() 56 | elif opt == '-k': 57 | api_key = arg 58 | elif opt == '-n': 59 | net_id = arg 60 | elif opt == '-f': 61 | arg_file = arg 62 | elif opt == '-m': 63 | arg_mode = arg 64 | 65 | # Check if all required parameters have been input 66 | if (api_key == None and os.getenv('MERAKI_DASHBOARD_API_KEY') == None) or net_id == None or arg_file == None: 67 | print_help() 68 | sys.exit(2) 69 | 70 | # Assign default mode to "simulate" unless "commit" specified 71 | if arg_mode != 'commit': 72 | arg_mode = 'simulate' 73 | 74 | # Read CSV input file, and skip header row 75 | input_file = open(arg_file) 76 | csv_reader = csv.reader(input_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 77 | next(csv_reader, None) 78 | print(f'Reading file {arg_file}') 79 | 80 | # Loop through each firewall rule from CSV file and build PUT data 81 | fw_rules = [] 82 | for row in csv_reader: 83 | rule = dict({'policy': row[0], 'protocol': row[1], 'srcCidr': row[2], 'srcPort': row[3], 'destCidr': row[4], 'destPort': row[5], 'comment': row[6], 'syslogEnabled': (row[7] == True or row[7] == 'True' or row[7] == 'true')}) 84 | 85 | # Append implied "/32" for IP addresses for just one host 86 | try: 87 | ip = ipaddress.ip_address(rule['srcCidr']) 88 | if not '/' in rule['srcCidr']: 89 | rule['srcCidr'] += '/32' 90 | except: 91 | pass 92 | try: 93 | ip = ipaddress.ip_address(rule['destCidr']) 94 | if not '/' in rule['destCidr']: 95 | rule['destCidr'] += '/32' 96 | except: 97 | pass 98 | print(rule) 99 | 100 | fw_rules.append(rule) 101 | old_rules = list(fw_rules) 102 | print(f'Processed all {len(fw_rules)} rules of file {arg_file}') 103 | 104 | # Check if last (default) rule exists, and if so, remove and check for default logging 105 | default_rule_exists = False 106 | default_logging = False 107 | last_rule = {'comment': 'Default rule', 'policy': 'allow', 'protocol': 'Any', 'srcPort': 'Any', 'srcCidr': 'Any', 'destPort': 'Any', 'destCidr': 'Any'} 108 | if all(item in fw_rules[-1].items() for item in last_rule.items()): 109 | default_rule_exists = True 110 | default_logging = (fw_rules.pop()['syslogEnabled'] == True) 111 | 112 | # Dashboard API library class 113 | m = meraki.DashboardAPI(api_key=api_key, log_file_prefix=__file__[:-3], simulate=(arg_mode == 'simulate')) 114 | 115 | # Update MX L3 firewall rules 116 | print(f'Attempting update/simulation of firewall rules to network {net_id}') 117 | m.appliance.updateNetworkApplianceFirewallL3FirewallRules(net_id, rules=fw_rules, syslogDefaultRule=default_logging) 118 | 119 | # Confirm whether changes were successfully made 120 | if arg_mode == 'commit': 121 | new_rules = m.appliance.getNetworkApplianceFirewallL3FirewallRules(net_id)['rules'] 122 | if default_rule_exists and new_rules[:-1] == old_rules[:-1]: 123 | print('Update successful!') 124 | elif not(default_rule_exists) and new_rules[:-1] == old_rules: 125 | print('Update successful!') 126 | else: 127 | print('Uh oh, something went wrong...') 128 | 129 | 130 | if __name__ == '__main__': 131 | main(sys.argv[1:]) 132 | -------------------------------------------------------------------------------- /import_mx_s2svpn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 6 | 7 | Install Requests, PyYAML and Meraki Dashboard API Python modules: 8 | pip[3] install --upgrade requests 9 | pip[3] install --upgrade meraki 10 | pip[3] install --upgrade pyyaml 11 | 12 | === DESCRIPTION === 13 | Imports YAML of MX site-to-site VPN firewall rules into Dashboard network. Note 14 | that if logging is enabled for any rule, then a syslog server needs to be 15 | configured on the Network-wide > General page. 16 | 17 | === USAGE === 18 | python import_mx_s2svpn.py -k -o -f [-m ] 19 | The -f parameter is the path to the YAML file with MX S2S VPN firewall rules. 20 | The optional -m parameter is either "simulate" (default) to only print changes, 21 | or "commit" to also apply those changes to Dashboard. 22 | ''' 23 | 24 | from datetime import datetime 25 | import getopt 26 | import sys 27 | import os 28 | import meraki 29 | import yaml 30 | 31 | # Prints READ_ME help message for user to read 32 | def print_help(): 33 | lines = READ_ME.split('\n') 34 | for line in lines: 35 | print('# {0}'.format(line)) 36 | 37 | 38 | def main(argv): 39 | # Set default values for command line arguments 40 | api_key = org_id = arg_file = arg_mode = None 41 | 42 | # Get command line arguments 43 | try: 44 | opts, args = getopt.getopt(argv, 'hk:o:f:m:') 45 | except getopt.GetoptError: 46 | print_help() 47 | sys.exit(2) 48 | for opt, arg in opts: 49 | if opt == '-h': 50 | print_help() 51 | sys.exit() 52 | elif opt == '-k': 53 | api_key = arg 54 | elif opt == '-o': 55 | org_id = arg 56 | elif opt == '-f': 57 | arg_file = arg 58 | elif opt == '-m': 59 | arg_mode = arg 60 | 61 | # Check if all required parameters have been input 62 | if api_key == None or org_id == None or arg_file == None: 63 | print_help() 64 | sys.exit(2) 65 | 66 | # Assign default mode to "simulate" unless "commit" specified 67 | if arg_mode != 'commit': 68 | arg_mode = 'simulate' 69 | 70 | dashboard = meraki.DashboardAPI( 71 | api_key=api_key, 72 | base_url='https://api.meraki.com/api/v1/', 73 | output_log=True, 74 | log_file_prefix=os.path.basename(__file__)[:-3], 75 | log_path='', 76 | print_console=False 77 | ) 78 | 79 | # Read input file 80 | with open(arg_file) as file: 81 | loaded_rules = yaml.full_load(file) 82 | 83 | #Remove default allow rule, if it exists in loaded rules 84 | default_allow_rule = {'comment': 'Default rule', 'destCidr': 'Any', 'destPort': 'Any', 85 | 'policy': 'allow', 'protocol': 'Any', 'srcCidr': 'Any', 'srcPort': 'Any'} 86 | 87 | last_line = loaded_rules['rules'][len(loaded_rules['rules'])-1] 88 | 89 | matched_default = True 90 | for key in default_allow_rule: 91 | if key in last_line: 92 | if last_line[key] != default_allow_rule[key]: 93 | matched_default = False 94 | 95 | if matched_default: 96 | processed_rules = loaded_rules['rules'][:-1] 97 | else: 98 | processed_rules = loaded_rules['rules'] 99 | 100 | if arg_mode == 'commit': 101 | print("\nCOMMIT MODE ENABLED\n") 102 | result = dashboard.appliance.updateOrganizationApplianceVpnVpnFirewallRules(org_id, rules=processed_rules) 103 | print("Configuration updated. Result:\n\n%s" % result) 104 | else: 105 | print("\nSIMULATION MODE ENABLED\n") 106 | print('Use "-m commit" to apply the following VPN FW rules:\n') 107 | print(processed_rules) 108 | 109 | if __name__ == '__main__': 110 | inputs = sys.argv[1:] 111 | try: 112 | key_index = inputs.index('-k') 113 | except ValueError: 114 | print_help() 115 | sys.exit(2) 116 | 117 | main(sys.argv[1:]) 118 | -------------------------------------------------------------------------------- /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 | import sys, getopt, requests, json 18 | 19 | def printusertext(p_message): 20 | #prints a line of text that is meant for the user to read 21 | #do not process these lines when chaining scripts 22 | print('@ %s' % p_message) 23 | 24 | def printhelp(): 25 | #prints help text 26 | 27 | printusertext("This is a script that prints a list of an organization's devices to sdtout or a file.") 28 | printusertext('') 29 | printusertext('Usage:') 30 | printusertext('python invlist.py -k -o [-f ]') 31 | printusertext('') 32 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 33 | 34 | def getorgid(p_apikey, p_orgname): 35 | #looks up org id for a specific org name 36 | #on failure returns 'null' 37 | 38 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 39 | 40 | if r.status_code != requests.codes.ok: 41 | return 'null' 42 | 43 | rjson = r.json() 44 | 45 | for record in rjson: 46 | if record['name'] == p_orgname: 47 | return record['id'] 48 | return('null') 49 | 50 | def getshardurl(p_apikey, p_orgid): 51 | #patch 52 | return("api.meraki.com") 53 | 54 | def getnwlist(p_apikey, p_shardurl, p_orgid): 55 | #returns a list of all networks in an organization 56 | #on failure returns a single record with 'null' name and id 57 | 58 | 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'}) 59 | 60 | returnvalue = [] 61 | if r.status_code != requests.codes.ok: 62 | returnvalue.append({'name': 'null', 'id': 'null'}) 63 | return(returnvalue) 64 | 65 | return(r.json()) 66 | 67 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 68 | #returns a list of all devices in a network 69 | 70 | 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'}) 71 | 72 | returnvalue = [] 73 | if r.status_code != requests.codes.ok: 74 | returnvalue.append({'serial': 'null', 'model': 'null'}) 75 | return(returnvalue) 76 | 77 | return(r.json()) 78 | 79 | 80 | def main(argv): 81 | #get command line arguments 82 | arg_apikey = 'null' 83 | arg_orgname = 'null' 84 | arg_filepath = 'null' 85 | 86 | try: 87 | opts, args = getopt.getopt(argv, 'hk:o:f:') 88 | except getopt.GetoptError: 89 | printhelp() 90 | sys.exit(2) 91 | 92 | for opt, arg in opts: 93 | if opt == '-h': 94 | printhelp() 95 | sys.exit() 96 | elif opt == '-k': 97 | arg_apikey = arg 98 | elif opt == '-o': 99 | arg_orgname = arg 100 | elif opt == '-f': 101 | arg_filepath = arg 102 | 103 | if arg_apikey == 'null' or arg_orgname == 'null': 104 | printhelp() 105 | sys.exit(2) 106 | 107 | #get organization id corresponding to org name provided by user 108 | orgid = getorgid(arg_apikey, arg_orgname) 109 | if orgid == 'null': 110 | printusertext('ERROR: Fetching organization failed') 111 | sys.exit(2) 112 | 113 | #get shard URL where Org is stored 114 | shardurl = getshardurl(arg_apikey, orgid) 115 | if shardurl == 'null': 116 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 117 | sys.exit(2) 118 | 119 | #get network list for fetched org id 120 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 121 | 122 | if nwlist[0]['id'] == 'null': 123 | printusertext('ERROR: Fetching network list failed') 124 | sys.exit(2) 125 | 126 | #if user selected to print in file, set flag & open for writing 127 | filemode = False 128 | if arg_filepath != 'null': 129 | try: 130 | f = open(arg_filepath, 'w') 131 | except: 132 | printusertext('ERROR: Unable to open output file for writing') 133 | sys.exit(2) 134 | filemode = True 135 | 136 | devicelist = [] 137 | for nwrecord in nwlist: 138 | #get devices' list 139 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 140 | #append list to file or stdout 141 | if filemode: 142 | for i in range (0, len(devicelist)): 143 | try: 144 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 145 | f.write('%s,%s\n' % (devicelist[i]['serial'], devicelist[i]['model'])) 146 | except: 147 | printusertext('ERROR: Unable to write device info to file') 148 | sys.exit(2) 149 | else: 150 | for i in range (0, len(devicelist)): 151 | #MODIFY THE LINE BELOW TO CHANGE OUTPUT FORMAT 152 | print('%s,%s' % (devicelist[i]['serial'], devicelist[i]['model'])) 153 | 154 | if __name__ == '__main__': 155 | 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 | import sys, getopt, requests, json 18 | 19 | def printusertext(p_message): 20 | #prints a line of text that is meant for the user to read 21 | #do not process these lines when chaining scripts 22 | print('@ %s' % p_message) 23 | 24 | def printhelp(): 25 | #prints help text 26 | 27 | printusertext("This is a script that prints a list of an organization's devices to sdtout or a file.") 28 | printusertext('') 29 | printusertext('Usage:') 30 | printusertext('python invlist.py -k -o [-f ]') 31 | printusertext('') 32 | printusertext('Use double quotes ("") in Windows to pass arguments containing spaces. Names are case-sensitive.') 33 | 34 | def getorgid(p_apikey, p_orgname): 35 | #looks up org id for a specific org name 36 | #on failure returns 'null' 37 | 38 | r = requests.get('https://dashboard.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}) 39 | 40 | if r.status_code != requests.codes.ok: 41 | return 'null' 42 | 43 | rjson = r.json() 44 | 45 | for record in rjson: 46 | if record['name'] == p_orgname: 47 | return record['id'] 48 | return('null') 49 | 50 | def getshardurl(p_apikey, p_orgid): 51 | #patch 52 | return("api.meraki.com") 53 | 54 | def getnwlist(p_apikey, p_shardurl, p_orgid): 55 | #returns a list of all networks in an organization 56 | #on failure returns a single record with 'null' name and id 57 | 58 | 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'}) 59 | 60 | returnvalue = [] 61 | if r.status_code != requests.codes.ok: 62 | returnvalue.append({'name': 'null', 'id': 'null'}) 63 | return(returnvalue) 64 | 65 | return(r.json()) 66 | 67 | def getdevicelist(p_apikey, p_shardurl, p_nwid): 68 | #returns a list of all devices in a network 69 | 70 | 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'}) 71 | 72 | returnvalue = [] 73 | if r.status_code != requests.codes.ok: 74 | returnvalue.append({'serial': 'null', 'model': 'null'}) 75 | return(returnvalue) 76 | 77 | return(r.json()) 78 | 79 | def getnwvlanips(p_apikey, p_shardurl, p_nwid): 80 | #returns MX VLANs for a network 81 | 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'}) 82 | 83 | returnvalue = [] 84 | if r.status_code != requests.codes.ok: 85 | returnvalue.append({'id': 'null'}) 86 | return(returnvalue) 87 | 88 | return(r.json()) 89 | 90 | 91 | def main(argv): 92 | #get command line arguments 93 | arg_apikey = 'null' 94 | arg_orgname = 'null' 95 | arg_filepath = 'null' 96 | 97 | try: 98 | opts, args = getopt.getopt(argv, 'hk:o:f:') 99 | except getopt.GetoptError: 100 | printhelp() 101 | sys.exit(2) 102 | 103 | for opt, arg in opts: 104 | if opt == '-h': 105 | printhelp() 106 | sys.exit() 107 | elif opt == '-k': 108 | arg_apikey = arg 109 | elif opt == '-o': 110 | arg_orgname = arg 111 | elif opt == '-f': 112 | arg_filepath = arg 113 | 114 | if arg_apikey == 'null' or arg_orgname == 'null': 115 | printhelp() 116 | sys.exit(2) 117 | 118 | #get organization id corresponding to org name provided by user 119 | orgid = getorgid(arg_apikey, arg_orgname) 120 | if orgid == 'null': 121 | printusertext('ERROR: Fetching organization failed') 122 | sys.exit(2) 123 | 124 | #get shard URL where Org is stored 125 | shardurl = getshardurl(arg_apikey, orgid) 126 | if shardurl == 'null': 127 | printusertext('ERROR: Fetching Meraki cloud shard URL failed') 128 | sys.exit(2) 129 | 130 | #get network list for fetched org id 131 | nwlist = getnwlist(arg_apikey, shardurl, orgid) 132 | 133 | if nwlist[0]['id'] == 'null': 134 | printusertext('ERROR: Fetching network list failed') 135 | sys.exit(2) 136 | 137 | #if user selected to print in file, set flag & open for writing 138 | filemode = False 139 | if arg_filepath != 'null': 140 | try: 141 | f = open(arg_filepath, 'w') 142 | except: 143 | printusertext('ERROR: Unable to open output file for writing') 144 | sys.exit(2) 145 | filemode = True 146 | 147 | devicelist = [] 148 | recordstring = [] 149 | vlanips = [] 150 | for nwrecord in nwlist: 151 | #get devices' list 152 | devicelist = getdevicelist(arg_apikey, shardurl, nwrecord['id']) 153 | #append list to file or stdout 154 | for i in range (0, len(devicelist)): 155 | #START: MODIFY THESE LINES TO CHANGE OUTPUT FORMAT 156 | #create string to be printed if filemode, a '\n' will be added later 157 | #use try-except so that code does not crash if lanIp, wan1Ip or wan2Ip are missing 158 | recordstring = devicelist[i]['serial'] + ',' + devicelist[i]['model'] 159 | try: 160 | if (len(devicelist[i]['lanIp']) > 4): 161 | recordstring += ',' + devicelist[i]['lanIp'] 162 | except: 163 | pass 164 | try: 165 | if (len(devicelist[i]['wan1Ip']) > 4): 166 | recordstring += ',' + devicelist[i]['wan1Ip'] 167 | except: 168 | pass 169 | try: 170 | if (len(devicelist[i]['wan2Ip']) > 4): 171 | recordstring += ',' + devicelist[i]['wan2Ip'] 172 | except: 173 | pass 174 | 175 | #if the device is an MX or Z1, LAN interface IPs will be listed under network VLANs 176 | if (devicelist[i]['model'].startswith('MX') or devicelist[i]['model'].startswith('Z1')): 177 | vlanips = getnwvlanips(arg_apikey, shardurl, nwrecord['id']) 178 | if vlanips[0]['id'] != 'null': 179 | for j in range (0, len(vlanips)): 180 | recordstring = recordstring + ',' + vlanips[j]['applianceIp'] 181 | #END: MODIFY THESE LINES TO CHANGE OUTPUT FORMAT 182 | 183 | #print record to file or stdout 184 | if filemode: 185 | recordstring += '\n' 186 | try: 187 | f.write(recordstring) 188 | except: 189 | printusertext('ERROR: Unable to write device info to file') 190 | sys.exit(2) 191 | else: 192 | print(recordstring) 193 | 194 | if __name__ == '__main__': 195 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /manage_firmware/firmware_upgrade_manager.py: -------------------------------------------------------------------------------- 1 | import meraki 2 | import datetime 3 | import time 4 | 5 | ''' 6 | Cisco Meraki Firmware Upgrade Manager 7 | John M. Kuchta .:|:.:|:. https://github.com/TKIPisalegacycipher 8 | This script will pull network IDs from an org and then create asynchronous action batches. Each batch will contain, for 9 | each network, an action that will delay the upgrade datetime stamp by X days (configurable). Each batch can contain up 10 | to 100 actions, therefore, each batch can modify up to 100 networks. 11 | 12 | As always, you should read the docs before diving in. If you know how these features work, then it will be easier to 13 | understand and leverage this tool. 14 | 15 | Firmware upgrades endpoint: https://developer.cisco.com/meraki/api-v1/#!get-network-firmware-upgrades 16 | Action batches: https://developer.cisco.com/meraki/api-v1/#!action-batches-overview 17 | 18 | NB: Once you start the script, there are no confirmation prompts or previews, so test in a lab if necessary. 19 | 20 | NB: When the final batch has been submitted, depending on the batch size, it may take a few minutes to finish. Feeling 21 | creative? Then try extending this script (using existing code, for the most part) to confirm when the batches are 22 | complete. Feeling super creative? Wrap this behind a Flask frontend and have yourself a merry little GUI. 23 | ''' 24 | 25 | # init Meraki Python SDK session 26 | dashboard = meraki.DashboardAPI(suppress_logging=True, single_request_timeout=120) 27 | 28 | 29 | # Configurable options 30 | # Organization ID. Replace this with your actual organization ID. 31 | organization_id = 'YOUR ORG ID HERE' # Use your own organization ID. 32 | time_delta_in_days = 30 # Max is 1 month per the firmware upgrades endpoint docs 33 | actions_per_batch = 100 # Max number of actions to submit in a batch. 100 is the maximum. Bigger batches take longer. 34 | wait_factor = 0.33 # Wait factor for action batches when the action batch queue is full. 35 | 36 | # Firmware IDs; not needed for rescheduling, only for upgrading. If you plan to use this for upgrading, then you should 37 | # first GET the availableVersions IDs and use those here instead, since they have probably changed from the time this 38 | # was published. 39 | mx_new_firmware_id = 2128 # Did you update this to your actual FW ID by GETing your availableFirmwareVersions? 40 | mx_old_firmware_id = 2009 # Did you update this to your actual FW ID by GETing your availableFirmwareVersions? 41 | 42 | 43 | def time_formatter(date_time_stamp): 44 | # Basic time formatter to return strings that the API requires 45 | formatted_date_time_stamp = date_time_stamp.replace(microsecond=0).isoformat() + 'Z' 46 | return formatted_date_time_stamp 47 | 48 | 49 | # Time stamps 50 | utc_now = datetime.datetime.utcnow() 51 | utc_future = utc_now + datetime.timedelta(days=time_delta_in_days) 52 | utc_now_formatted = time_formatter(utc_now) 53 | utc_future_formatted = time_formatter(utc_future) 54 | 55 | 56 | action_reschedule_existing = { 57 | "products": { 58 | "appliance": 59 | { 60 | "nextUpgrade": { 61 | "time": utc_future_formatted 62 | } 63 | } 64 | } 65 | } 66 | 67 | # Use this action to schedule a new upgrade. If you do not provide a time param (as shown above), it will execute 68 | # immediately. IMPORTANT: See API docs for more info before using this. 69 | action_schedule_new_upgrade = { 70 | "products": { 71 | "appliance": 72 | { 73 | "nextUpgrade": { 74 | "time": utc_future_formatted, 75 | "toVersion": { 76 | "id": mx_new_firmware_id 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | 84 | # GET the network list 85 | networks_list = dashboard.organizations.getOrganizationNetworks( 86 | organizationId=organization_id 87 | ) 88 | 89 | 90 | def format_single_action(resource, operation, body): 91 | # Combine a single set of batch components into an action 92 | action = { 93 | "resource": resource, 94 | "operation": operation, 95 | "body": body 96 | } 97 | 98 | return action 99 | 100 | 101 | def create_single_upgrade_action(network_id): 102 | # Create a single upgrade action 103 | # AB component parts, rename action 104 | action_resource = f'/networks/{network_id}/firmwareUpgrades' 105 | action_operation = 'update' 106 | # Choose whether to reschedule an existing or start a new upgrade 107 | action_body = action_schedule_new_upgrade 108 | 109 | upgrade_action = format_single_action(action_resource, action_operation, action_body) 110 | 111 | return upgrade_action 112 | 113 | 114 | def run_an_action_batch(org_id, actions_list, synchronous=False): 115 | # Create and run an action batch 116 | batch_response = dashboard.organizations.createOrganizationActionBatch( 117 | organizationId=org_id, 118 | actions=actions_list, 119 | confirmed=True, 120 | synchronous=synchronous 121 | ) 122 | 123 | return batch_response 124 | 125 | 126 | def create_action_list(net_list): 127 | # Creates a list of actions and returns it 128 | # Iterate through the list of network IDs and create an action for each, then collect it 129 | list_of_actions = list() 130 | 131 | for network in net_list: 132 | # Create the action 133 | single_action = create_single_upgrade_action(network['id']) 134 | list_of_actions.append(single_action) 135 | 136 | return list_of_actions 137 | 138 | 139 | def batch_actions_splitter(batch_actions): 140 | # Split the list of actions into smaller lists of maximum 100 actions each 141 | # For each ID in range length of network_ids 142 | for i in range(0, len(batch_actions), actions_per_batch): 143 | # Create an index range for network_ids of 100 items: 144 | yield batch_actions[i:i + actions_per_batch] 145 | 146 | 147 | def action_batch_runner(batch_actions_lists, org_id): 148 | # Create an action batch for each list of actions 149 | # Store the responses 150 | responses = list() 151 | number_of_batches = len(batch_actions_lists) 152 | number_of_batches_submitted = 0 153 | wait_seconds = int(30) 154 | 155 | # Make a batch for each list 156 | for batch_action_list in batch_actions_lists: 157 | action_batch_queue_checker(org_id) 158 | batch_response = run_an_action_batch(org_id, batch_action_list) 159 | responses.append(batch_response) 160 | number_of_batches_submitted += 1 161 | 162 | # Inform user of progress. 163 | print(f'Submitted batch {number_of_batches_submitted} of {number_of_batches}.') 164 | 165 | return responses 166 | 167 | 168 | def action_batch_queue_checker(org_id): 169 | all_action_batches = dashboard.organizations.getOrganizationActionBatches(organizationId=org_id) 170 | running_action_batches = [batch for batch in all_action_batches if batch['status']['completed'] is False and batch['status']['failed'] is False] 171 | total_running_actions = 0 172 | 173 | for batch in running_action_batches: 174 | batch_actions = len(batch['actions']) 175 | total_running_actions += batch_actions 176 | 177 | wait_seconds = total_running_actions * wait_factor 178 | 179 | while len(running_action_batches) > 4: 180 | print(f'There are already five action batches in progress with a total of {total_running_actions} running actions. Waiting {wait_seconds} seconds.') 181 | time.sleep(wait_seconds) 182 | print('Checking again.') 183 | 184 | all_action_batches = dashboard.organizations.getOrganizationActionBatches(organizationId=org_id) 185 | running_action_batches = [batch for batch in all_action_batches if batch['status']['completed'] is False and batch['status']['failed'] is False] 186 | total_running_actions = 0 187 | 188 | for batch in running_action_batches: 189 | batch_actions = len(batch['actions']) 190 | total_running_actions += batch_actions 191 | 192 | wait_seconds = total_running_actions * wait_factor 193 | 194 | 195 | # Create a list of upgrade actions 196 | upgrade_actions_list = create_action_list(networks_list) 197 | 198 | # Split the list into multiple lists of max 100 items each 199 | upgrade_actions_lists = list(batch_actions_splitter(upgrade_actions_list)) 200 | 201 | # Run the action batches to clone the networks 202 | upgraded_networks_responses = action_batch_runner(upgrade_actions_lists, organization_id) 203 | -------------------------------------------------------------------------------- /merakidevicecounts.py: -------------------------------------------------------------------------------- 1 | readMe = ''' 2 | This is a script to count device types in multiple orgs. 3 | 4 | Usage: 5 | python merakidevicecounts.py -k -f 6 | 7 | Arguments: 8 | -k : Your Meraki Dashboard API key 9 | -f : File with list of organizations to be counted. Use "-f /all" to count all organizations 10 | 11 | Examples: 12 | python merakidevicecounts.py -k 1234 -f /all 13 | python merakidevicecounts.py -k 1234 -f orglist.txt 14 | 15 | Creating an input file: 16 | Use a text editor to create a text file, where organization names are listed one per line 17 | ''' 18 | 19 | import sys, getopt, requests, json, time, datetime 20 | 21 | class c_devicedata: 22 | def __init__(self): 23 | self.serial = '' 24 | self.model = '' 25 | #end class 26 | 27 | class c_organizationdata: 28 | def __init__(self): 29 | self.name = '' 30 | self.id = '' 31 | self.shardhost = '' 32 | self.devices = [] 33 | self.skipMe = False 34 | #end class 35 | 36 | API_EXEC_DELAY = 0.21 #Used in merakiRequestThrottler() to avoid hitting dashboard API max request rate 37 | 38 | #connect and read timeouts for the Requests module in seconds 39 | REQUESTS_CONNECT_TIMEOUT = 60 40 | REQUESTS_READ_TIMEOUT = 60 41 | 42 | LAST_MERAKI_REQUEST = datetime.datetime.now() #used by merakiRequestThrottler() 43 | 44 | def printhelp(): 45 | #prints help text 46 | 47 | print(readMe) 48 | sys.exit(0) 49 | 50 | def merakiRequestThrottler(): 51 | #prevents hitting max request rate shaper of the Meraki Dashboard API 52 | global LAST_MERAKI_REQUEST 53 | 54 | if (datetime.datetime.now()-LAST_MERAKI_REQUEST).total_seconds() < (API_EXEC_DELAY): 55 | time.sleep(API_EXEC_DELAY) 56 | 57 | LAST_MERAKI_REQUEST = datetime.datetime.now() 58 | return 59 | 60 | def getorglist(p_apikey): 61 | #returns the organizations' list for a specified admin 62 | #DEBUG unfinished untested 63 | 64 | merakiRequestThrottler() 65 | try: 66 | r = requests.get('https://api.meraki.com/api/v0/organizations', 67 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 68 | 'Content-Type': 'application/json'}, 69 | timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)) 70 | except: 71 | print('ERROR 00: Unable to contact Meraki cloud') 72 | sys.exit(2) 73 | 74 | returnvalue = [] 75 | if r.status_code != requests.codes.ok: 76 | returnvalue.append({'id':'null'}) 77 | return returnvalue 78 | 79 | rjson = r.json() 80 | 81 | return(rjson) 82 | 83 | def getorgid(p_apikey, p_orgname): 84 | #looks up org id for a specific org name 85 | #on failure returns 'null' 86 | 87 | merakiRequestThrottler() 88 | try: 89 | r = requests.get('https://api.meraki.com/api/v0/organizations', 90 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 91 | 'Content-Type': 'application/json'}, 92 | timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)) 93 | except: 94 | print('ERROR 01: Unable to contact Meraki cloud') 95 | sys.exit(2) 96 | 97 | if r.status_code != requests.codes.ok: 98 | return 'null' 99 | 100 | rjson = r.json() 101 | 102 | for record in rjson: 103 | if record['name'] == p_orgname: 104 | return record['id'] 105 | return('null') 106 | 107 | 108 | def getorginventory(p_apikey, p_orgid): 109 | #returns full org inventory 110 | 111 | merakiRequestThrottler() 112 | try: 113 | r = requests.get('https://api.meraki.com/api/v0/organizations/%s/inventory' % p_orgid, 114 | headers={'X-Cisco-Meraki-API-Key': p_apikey, 115 | 'Content-Type': 'application/json'}, 116 | timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)) 117 | except: 118 | print('ERROR 04: Unable to contact Meraki cloud') 119 | return None 120 | 121 | returnvalue = [] 122 | if r.status_code != requests.codes.ok: 123 | return None 124 | 125 | rjson = r.json() 126 | 127 | return (rjson) 128 | 129 | def main(argv): 130 | #python mxfirewallcontrol -k -o [-f ] [-c ] [-m ] 131 | 132 | #set default values for command line arguments 133 | arg_apikey = '' 134 | arg_file = '' 135 | 136 | #get command line arguments 137 | try: 138 | opts, args = getopt.getopt(argv, 'hk:f:') 139 | except getopt.GetoptError: 140 | printhelp() 141 | 142 | for opt, arg in opts: 143 | if opt == '-h': 144 | printhelp() 145 | elif opt == '-k': 146 | arg_apikey = arg 147 | elif opt == '-f': 148 | arg_file = arg 149 | 150 | #check if all parameters are required parameters have been given 151 | if arg_apikey == '' or arg_file == '': 152 | printhelp() 153 | 154 | #set flags 155 | flag_processall = False 156 | if arg_file == '/all': 157 | flag_processall = True 158 | 159 | #compile list of organizations to be processed 160 | orglist = [] 161 | if flag_processall: 162 | orgjson = getorglist(arg_apikey) 163 | 164 | i = 0 165 | for record in orgjson: 166 | orglist.append(c_organizationdata()) 167 | orglist[i].name = record['name'] 168 | orglist[i].id = record['id'] 169 | i += 1 170 | 171 | else: 172 | #open input file file for reading 173 | try: 174 | f = open(arg_file, 'r') 175 | except: 176 | print('ERROR 05: Unable to open file for reading') 177 | sys.exit(2) 178 | #read org names info from file 179 | for line in f: 180 | stripped = line.strip() 181 | if len(stripped) > 0: 182 | orglist.append(c_organizationdata()) 183 | orglist[len(orglist)-1].name = stripped 184 | orgid = getorgid(arg_apikey, stripped) 185 | if orgid != 'null': 186 | orglist[len(orglist)-1].id = orgid 187 | else: 188 | print('ERROR 06: Unable to resolve org ID for org name "%s"' % stripped) 189 | f.close() 190 | 191 | for orgrecord in orglist: 192 | orginventory = getorginventory(arg_apikey, orgrecord.id) 193 | if not orginventory is None: 194 | for returnitem in orginventory: 195 | orgrecord.devices.append(c_devicedata()) 196 | orgrecord.devices[len(orgrecord.devices)-1].serial = returnitem['serial'] 197 | orgrecord.devices[len(orgrecord.devices)-1].model = returnitem['model'] 198 | else: 199 | print('Skipping org "%s": unable to fetch inventory' % orgrecord.name) 200 | orgrecord.skipMe = True 201 | 202 | 203 | for item in orglist: 204 | if not item.skipMe: 205 | print ('') 206 | print ('Devices in org "%s"' % item.name) 207 | for device in item.devices: 208 | print('%s %s' % (device.serial, device.model)) 209 | 210 | 211 | 212 | #calculate + print device counts 213 | print('') 214 | 215 | total_count_mr = 0 216 | total_count_ms = 0 217 | total_count_mx = 0 218 | total_count_z = 0 219 | 220 | for item in orglist: 221 | if not item.skipMe: 222 | count_mr = 0 223 | count_ms = 0 224 | count_mx = 0 225 | count_z = 0 226 | print ('Device counts for org "%s"' % item.name) 227 | for device in item.devices: 228 | if device.model[:2] == 'MR': 229 | count_mr += 1 230 | elif device.model[:2] == 'MS': 231 | count_ms += 1 232 | elif device.model[:2] == 'MX': 233 | count_mx += 1 234 | elif device.model[:1] == 'Z' : 235 | count_z += 1 236 | total_count_mr += count_mr 237 | total_count_ms += count_ms 238 | total_count_mx += count_mx 239 | total_count_z += count_z 240 | print('MR: %d' % count_mr) 241 | print('MS: %d' % count_ms) 242 | print('MX: %d' % count_mx) 243 | print('Z : %d' % count_z) 244 | 245 | #print total device counts for all orgs 246 | 247 | print('') 248 | print('Total device counts for all orgs in "%s"' % arg_file) 249 | print('MR: %d' % total_count_mr) 250 | print('MS: %d' % total_count_ms) 251 | print('MX: %d' % total_count_mx) 252 | print('Z : %d' % total_count_z) 253 | 254 | 255 | print('INFO: End of script.') 256 | 257 | if __name__ == '__main__': 258 | main(sys.argv[1:]) 259 | -------------------------------------------------------------------------------- /migrate_cat3k/migrate_cat3k_init_example.txt: -------------------------------------------------------------------------------- 1 | #This is a sample Catalyst migration initialization file with device mappings for migrate_cat3k.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 Catalyst 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 Catalyst device. If username and password are 11 | # omitted, default credentials will be used. These lines can have four forms: 12 | # 13 | # 14 | # file 15 | # * File names should NOT contain spaces 16 | # * To migrate a multi-linecard switch or a switch stack, separate their serial numbers with commas, leaving no spaces between them 17 | # 18 | #Examples of net definition and device definition lines, commented out: 19 | # 20 | #net=Migrated headquarters network 21 | #10.1.1.20 AAAA-BBBB-CCCC admin admin 22 | #10.1.1.21 AAAA-BBBB-DDDD admin@system admin123 23 | #file myconfig.cfg BBBB-CCCC-DDDD 24 | # 25 | #net=Migrated branch network 26 | #192.168.10.10 AAAA-BBBB-EEEE 27 | # 28 | #net=Network for switch stack 29 | #3.3.3.3 DDDD-EEEE-FFFF,EEEE-FFFF-GGGG,FFFF-GGGG-HHHH 30 | 31 | net=Migration test network 32 | 192.168.100.250 ABCD-ABCD-ABCD cisco cisco 33 | file IOS-3750X-config.txt BADC-BADC-BADC 34 | 35 | -------------------------------------------------------------------------------- /migrate_networks/config.yaml: -------------------------------------------------------------------------------- 1 | organizationNames: 2 | source: "MODIFY THIS TEXT TO MATCH THE NAME OF YOUR SOURCE ORGANIZATION" 3 | destination: "MODIFY THIS TEXT TO MATCH THE NAME OF YOUR DESTINATION ORGANIZATION" 4 | 5 | enabledTasks: 6 | copyPolicyObjects: true 7 | copyVpnFirewallRules: true 8 | createNetworks: true 9 | refreshTimeZones: true 10 | copyMxRoutingMode: true 11 | copyMxVlans: true 12 | copyMxStaticRoutes: true 13 | copyMxFirewallRules: true 14 | copyMxTrafficShaping: true 15 | copyMrSsids: true 16 | copyMrFirewallRules: true 17 | copyMrTrafficShapingRules: true 18 | copyAlerts: true 19 | copySiteToSiteVpnConfig: true 20 | 21 | configModifications: 22 | overwriteSsidRadiusSecret: 23 | enabled: true 24 | newRadiusSecret: "MODIFY THIS TEXT TO MATCH YOUR RADIUS SECRET" 25 | overwriteSiteToSiteVpnHub: 26 | enabled: false 27 | oldHubNetworkName: "MODIFY THIS TEXT TO MATCH HUB NETWORK NAME IN SOURCE ORGANIZATION" 28 | newHubNetworkName: "MODIFY THIS TEXT TO MATCH HUB NETWORK NAME IN DESTINATION ORGANIZATION" 29 | ipConflictVlanTempSubnetPrefix: "1.1.1." 30 | 31 | networkFilters: 32 | filterSourceNetworksByTag: true 33 | sourceNetworkTagsList: 34 | - "process-this" 35 | 36 | excludeDestinationNetworksByTag: false 37 | destinationNetworkTagsList: 38 | - "do-not-process" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mr_mqtt_monitoring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | === PREREQUISITES === 5 | Run in Python 3.6+ 6 | Install MQTT package: pip[3] install paho-mqtt 7 | 8 | === DESCRIPTION === 9 | Monitor connectivity of specific clients using MR's MQTT data stream. 10 | 11 | === USAGE === 12 | Run with an input file of clients' MAC addresses to monitor, one per line. 13 | python[3] mr_mqtt_monitoring.py -f client_addresses.txt 14 | """ 15 | 16 | 17 | import argparse 18 | from datetime import datetime 19 | import json 20 | from subprocess import Popen 21 | 22 | import paho.mqtt.client as mqtt 23 | 24 | 25 | MOBILE_CLIENTS_STATE = {} 26 | OVERALL_STATE = 'AWAY' 27 | LAST_PRINT_TIME = None 28 | 29 | 30 | def parse_arguments(parser): 31 | parser.add_argument('-f', '--file', help='Input file of MAC addresses, one per line') 32 | parser.exit 33 | args = parser.parse_args() 34 | return args.file 35 | 36 | 37 | # The callback for when the client receives a CONNACK response from the server 38 | def on_connect(client, user_data, flags, rc): 39 | global LAST_PRINT_TIME 40 | LAST_PRINT_TIME = datetime.utcnow() 41 | print(LAST_PRINT_TIME) 42 | 43 | print(f'Connected with result code {rc}') 44 | 45 | # Subscribing in on_connect() means that if we lose the connection and 46 | # reconnect then subscriptions will be renewed. 47 | client.subscribe(f'/meraki/mr') 48 | 49 | 50 | # The callback for when a PUBLISH message is received from the server 51 | def on_message(client, user_data, msg): 52 | global MOBILE_CLIENTS_STATE, OVERALL_STATE, LAST_PRINT_TIME 53 | 54 | # Process incoming MQTT data 55 | mqtt_data = json.loads(msg.payload) 56 | client = mqtt_data['clientMac'] 57 | if client in MOBILE_CLIENTS_STATE: 58 | client_timestamp = mqtt_data['timestamp'] 59 | MOBILE_CLIENTS_STATE[client]['last_seen'] = client_timestamp 60 | 61 | # Update state of each monitored client 62 | current_time = datetime.utcnow() 63 | for data in MOBILE_CLIENTS_STATE.values(): 64 | if 'last_seen' in data and data['last_seen']: 65 | delta = current_time - datetime.fromisoformat(data['last_seen']) 66 | 67 | # If device is not seen for 5 seconds, mark as disconnected 68 | if delta.seconds < 5: 69 | data['connected'] = True 70 | else: 71 | data['connected'] = False 72 | else: 73 | data['connected'] = False 74 | 75 | # Update overall state 76 | home_clients = {k for k, v in MOBILE_CLIENTS_STATE.items() if v['connected']} 77 | if len(home_clients) == 0: 78 | if OVERALL_STATE == 'HOME': 79 | Popen(f'python3 turn_off_lights.py', shell=True) 80 | OVERALL_STATE = 'AWAY' 81 | else: 82 | OVERALL_STATE = 'HOME' 83 | 84 | # Display state to end user to see visually, at most once per second 85 | # if client in MOBILE_CLIENTS_STATE: 86 | # print(MOBILE_CLIENTS_STATE) 87 | delta = current_time - LAST_PRINT_TIME 88 | # print(delta.microseconds) 89 | if delta.seconds >= 1 or delta.microseconds >= 9 * 10 ** 5: 90 | LAST_PRINT_TIME = current_time 91 | print(MOBILE_CLIENTS_STATE) 92 | 93 | 94 | def main(): 95 | # Process input parameters 96 | parser = argparse.ArgumentParser() 97 | input_file = parse_arguments(parser) 98 | if not input_file: 99 | parser.exit(2, parser.print_help()) 100 | 101 | # Add to global variable for tracking state 102 | with open(input_file) as fp: 103 | mac_addresses = fp.readlines() 104 | mobile_clients = [mac.strip().upper() for mac in mac_addresses] 105 | for client in mobile_clients: 106 | MOBILE_CLIENTS_STATE[client] = {} 107 | 108 | # Start MQTT client 109 | client = mqtt.Client() 110 | user_data = {} 111 | client.user_data_set(user_data) 112 | client.on_connect = on_connect 113 | client.on_message = on_message 114 | client.connect('localhost', 1883) 115 | 116 | # Blocking call that processes network traffic, dispatches callbacks and 117 | # handles reconnecting. 118 | # Other loop*() functions are available that give a threaded interface and a 119 | # manual interface. 120 | client.loop_forever() 121 | 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /mv_gp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | READ_ME = ''' 4 | === PREREQUISITES === 5 | Run in Python 3 6 | 7 | Install both requests & Meraki Dashboard API Python modules: 8 | pip[3] install requests [--upgrade] 9 | pip[3] install meraki [--upgrade] 10 | 11 | === DESCRIPTION === 12 | This script finds all MV cameras with a specified tag, and then iterates 13 | through all networks to apply an exisitng group policy (enforced by the MX) 14 | to the applicable cameras as client devices. 15 | 16 | === USAGE === 17 | python mv_gp.py -k -o -t -p [-m ] 18 | The -t parameter specifies the required tag that needs to be present on the MV 19 | camera, and -p the name of the MX group policy to be applied. 20 | The optional -m parameter is either "simulate" (default) to only print changes, 21 | or "commit" to also apply those changes to Dashboard. 22 | ''' 23 | 24 | 25 | import getopt 26 | import logging 27 | import sys 28 | from datetime import datetime 29 | from meraki import meraki 30 | 31 | # Prints READ_ME help message for user to read 32 | def print_help(): 33 | lines = READ_ME.split('\n') 34 | for line in lines: 35 | print('# {0}'.format(line)) 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | def configure_logging(): 40 | logging.basicConfig( 41 | filename='mv_gp_log_{:%Y%m%d_%H%M%S}.txt'.format(datetime.now()), 42 | level=logging.DEBUG, 43 | format='%(asctime)s: %(levelname)7s: [%(name)s]: %(message)s', 44 | datefmt='%Y-%m-%d %H:%M:%S' 45 | ) 46 | 47 | 48 | def main(argv): 49 | # Set default values for command line arguments 50 | api_key = org_id = arg_tag = arg_policy = arg_mode = None 51 | 52 | # Get command line arguments 53 | try: 54 | opts, args = getopt.getopt(argv, 'hk:o:t:p:m:') 55 | except getopt.GetoptError: 56 | print_help() 57 | sys.exit(2) 58 | for opt, arg in opts: 59 | if opt == '-h': 60 | print_help() 61 | sys.exit() 62 | elif opt == '-k': 63 | api_key = arg 64 | elif opt == '-o': 65 | org_id = arg 66 | elif opt == '-t': 67 | arg_tag = arg 68 | elif opt == '-p': 69 | arg_policy = arg 70 | elif opt == '-m': 71 | arg_mode = arg 72 | 73 | # Check if all required parameters have been input 74 | if api_key == None or org_id == None or arg_tag == None or arg_policy == None: 75 | print_help() 76 | sys.exit(2) 77 | 78 | # Assign default mode to "simulate" unless "commit" specified 79 | if arg_mode != 'commit': 80 | arg_mode = 'simulate' 81 | 82 | # Get org's inventory 83 | inventory = meraki.getorginventory(api_key, org_id) 84 | 85 | # Filter for only MV devices 86 | cameras = [device for device in inventory if device['model'][:2] in ('MV') and device['networkId'] is not None] 87 | 88 | # Gather the networks (IDs) where cameras have been added 89 | camera_network_ids = set([camera['networkId'] for camera in cameras]) 90 | logger.info('Found a total of {0} cameras added to {1} networks in this Dashboard organization'.format(len(cameras), len(camera_network_ids))) 91 | 92 | # Iterate through camera networks and find cameras with specified tag 93 | camera_macs = [] 94 | for net_id in camera_network_ids: 95 | devices = meraki.getnetworkdevices(api_key, net_id) 96 | for device in devices: 97 | if device['model'][:2] == 'MV' and 'tags' in device and arg_tag in device['tags']: 98 | camera_macs.append(device['mac']) 99 | logger.info('Found {0} cameras with the tag "{1}"'.format(len(camera_macs), arg_tag)) 100 | 101 | # Get list of all networks in org 102 | networks = meraki.getnetworklist(api_key, org_id) 103 | 104 | # Iterate through all networks, looking for cameras as clients, and apply group policy 105 | for network in networks: 106 | # Get the Meraki devices in this network 107 | devices = meraki.getnetworkdevices(api_key, network['id']) 108 | 109 | # Filter for just the first two characters of each device model 110 | device_models = [device['model'][:2] for device in devices] 111 | 112 | # Is there an MX here? If so, get its index in the list of devices 113 | if 'MX' in device_models: 114 | # We found the MX device in the network 115 | mx_device = devices[device_models.index('MX')] 116 | else: 117 | # No MX in this network, doesn't make sense to apply a group policy to wired clients (cameras), so move on 118 | continue 119 | 120 | # Get list of MX clients 121 | clients = meraki.getclients(api_key, mx_device['serial'], timestamp=2592000) 122 | 123 | # Filter for MAC addresses of these clients 124 | client_macs = [client['mac'] for client in clients] 125 | 126 | # Cameras in this network = intersection of clients in this network and cameras in the org 127 | network_cameras = set(client_macs).intersection(camera_macs) 128 | 129 | # Assign group policy to these cameras in the network 130 | if network_cameras: 131 | # Gather group policies of network 132 | gps = meraki.getgrouppolicies(api_key, network['id']) 133 | 134 | # Get human-readable names of all group policies 135 | gp_names = [gp['name'] for gp in gps] 136 | 137 | # Look for the group policy 138 | gp_camera = gps[gp_names.index(arg_policy)] 139 | 140 | # Assign that group policy (by ID) to the camera by MAC address 141 | for mac in network_cameras: 142 | if arg_mode == 'commit': 143 | meraki.updateclientpolicy(api_key, network['id'], mac, policy='group', policyid=gp_camera['groupPolicyId']) 144 | logger.info('Assigning group policy "{0}" on network "{1}" for MV camera {2}'.format(arg_policy, network['name'], mac)) 145 | else: 146 | logger.info('Simulating group policy "{0}" on network "{1}" for MV camera {2}'.format(arg_policy, network['name'], mac)) 147 | 148 | 149 | if __name__ == '__main__': 150 | # Configure logging to stdout 151 | configure_logging() 152 | # Define a Handler which writes INFO messages or higher to the sys.stderr 153 | console = logging.StreamHandler() 154 | console.setLevel(logging.INFO) 155 | # Set a format which is simpler for console use 156 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 157 | # Tell the handler to use this format 158 | console.setFormatter(formatter) 159 | # Add the handler to the root logger 160 | logging.getLogger('').addHandler(console) 161 | 162 | # Output to logfile/console starting inputs 163 | start_time = datetime.now() 164 | logger.info('Started script at {0}'.format(start_time)) 165 | inputs = sys.argv[1:] 166 | key_index = inputs.index('-k') 167 | inputs.pop(key_index+1) 168 | inputs.pop(key_index) 169 | logger.info('Input parameters: {0}'.format(inputs)) 170 | 171 | main(sys.argv[1:]) 172 | 173 | # Finish output to logfile/console 174 | end_time = datetime.now() 175 | logger.info('Ended script at {0}'.format(end_time)) 176 | logger.info(f'Total run time = {end_time - start_time}') 177 | -------------------------------------------------------------------------------- /mx_firewall_control/mxfirewallcontrol_example_input_file.txt: -------------------------------------------------------------------------------- 1 | {"protocol":"any", "srcPort":"Any", "srcCidr":"192.168.128.1", "destPort":"Any", "destCidr":"any", "policy":"deny", "syslogEnabled":false, "comment":"Line 1"} 2 | {"protocol":"any", "srcPort":"Any", "srcCidr":"192.168.128.2", "destPort":"Any", "destCidr":"any", "policy":"deny", "syslogEnabled":false, "comment":"Line 2"} 3 | {"protocol":"any", "srcPort":"Any", "srcCidr":"192.168.128.3", "destPort":"Any", "destCidr":"any", "policy":"deny", "syslogEnabled":false, "comment":"Line 3"} 4 | {"protocol":"any", "srcPort":"Any", "srcCidr":"192.168.128.4", "destPort":"Any", "destCidr":"any", "policy":"deny", "syslogEnabled":false, "comment":"Line 4"} 5 | -------------------------------------------------------------------------------- /nodejs_sdk_builder/endpoint.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* DOCS TITLE *//* DOCS ENDPOINT *//* OFFICIAL DOCS LINK *//* DOCS QUERY *//* DOCS BODY*/ 4 | 5 | /* ENDPOINT ID */ (/* PARAMETERS */) { 6 | var self = this; 7 | return new Promise(function (resolve, reject) { 8 | self.request(self, /* METHOD */, /* RESOURCE PATH *//* CONFIG */) 9 | .then(function(response) { 10 | resolve(response); 11 | }) 12 | .catch(function(error) { 13 | reject(error); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /nodejs_sdk_builder/readme.md: -------------------------------------------------------------------------------- 1 | # nodejs_sdk_builder.py 2 | A Python 3 script that generates a Meraki Dashboard API SDK for Node.js. 3 | 4 | # Project overview 5 | The project consists of a Python 3 script and two template files, `sdk_core.template` and `endpoint.template`. It uses these files, as well as information retrieved from the Meraki Dashboard API to create a Javascript SDK. 6 | 7 | Key features: 8 | * Handles request timeouts, rate limiter status codes and multi-page responses in a tidy manner 9 | * Includes a retry function for when hitting the rate limiter 10 | * Returns a Promise that is resolved or rejected based on the API response, and can be handled with ".then" and ".catch" 11 | 12 | # Running the script 13 | * Install Python 3 if you have not done so already. If installing on Windows, it is recommended to select the "Add to PATH" option during installation 14 | * Install the requests Python 3 module. The easiest way to do this is to run: 15 | ``` 16 | Windows: 17 | pip install requests 18 | 19 | Linux/Mac: 20 | pip3 install requests 21 | ``` 22 | 23 | * Run the script, replacing `` with your Meraki Dashboard API key: 24 | ``` 25 | Windows: 26 | python nodejs_sdk_builder.py -k 27 | 28 | Linux/Mac: 29 | python3 nodejs_sdk_builder.py -k 30 | ``` 31 | 32 | * By default, the script will pull the list of available endpoints from the first organization accessible by your administrator account. If you want to specify which organization to pick, use this form instead, replacing `` with your Meraki Dashboard API key and `` with the name of the organization you want the script to use: 33 | ``` 34 | Windows: 35 | python nodejs_sdk_builder.py -k -o 36 | 37 | Linux/Mac: 38 | python3 nodejs_sdk_builder.py -k -o 39 | ``` 40 | 41 | # Using the generated SDK 42 | The script creates a Meraki Dashboard API SDK module for Node.js as a single file in the same directory as the script itself. By default, the output file will be named `Meraki_.js`. 43 | 44 | The SDK requires the Axios package: https://www.npmjs.com/package/axios 45 | 46 | How to use the output module: 47 | * Copy the module to the same directory as your Node.js code 48 | * Rename the file to `Meraki.js` 49 | * Add the following lines to your code, replacing `` with your Meraki Dashboard API key: 50 | ``` 51 | const Meraki = require('./Meraki'); 52 | var api = new Meraki.MerakiClass(""); 53 | ``` 54 | * After that, you can use the endpoint methods like in this example: 55 | ``` 56 | api.getOrganizations() 57 | .then(function(response){ 58 | // Code for handling request success 59 | console.log(response); 60 | }).catch(function (error) { 61 | // Code for handling request error 62 | console.log(error); 63 | }); 64 | ``` 65 | 66 | How to find endpoints in the module file: 67 | * Go to the Meraki Dashboard API documentation page: https://developer.cisco.com/meraki/api-v1/ 68 | * Find the endpoint that you want to use 69 | * Copy its Operation Id and locate it in the module file using the search function of your text editor 70 | 71 | General structure of endpoints in the generated SDK: 72 | ``` 73 | MerakiClass.(, , , ) 74 | 75 | ``` 76 | 77 | Every endpoint method has a unique name that corresponds to the operation to be carried out by the Meraki Dashboard: 78 | * ``: This is the Operation Id of the endpoint, as specified in the Meraki Dashboard API documentation page 79 | 80 | Depending on the endpoint, it can require additional arguments to function. Refer to the particular endpoint method for its additional arguments. They can be the following: 81 | * ``, ``: The URL of the endpoint you are using might contain variable parts. For example, getOrganizationNetworks requires an organizationId. If needed, these are mandatory 82 | * ``: If the endpoint you are using has the option to receive additional parameters as a query string, they can be provided using this argument object. See example below on how to use it 83 | * ``: If the endpoint you are using has the option to receive additional parameters as a request body, they can be provided using this argument object. See example below on how to use it 84 | 85 | Using an endpoint that has query string parameter options: 86 | ``` 87 | const Meraki = require('./Meraki'); 88 | var api = new Meraki.MerakiClass("12345678"); 89 | var serial = "AAAA-BBBB-CCCC"; 90 | var query = { timespan: 10000 }; 91 | api.getDeviceClients(serial, query) 92 | .then(function(response){ 93 | // Code for handling request success 94 | console.log(response); 95 | }).catch(function (error) { 96 | // Code for handling request error 97 | console.log(error); 98 | }); 99 | ``` 100 | 101 | Using an endpoint that has request body parameter options: 102 | ``` 103 | const Meraki = require('./Meraki'); 104 | var api = new Meraki.MerakiClass("12345678"); 105 | var organizationId = "87654321"; 106 | var body = { name: "New network" }; 107 | api.createOrganizationNetwork (organizationId, body) 108 | .then(function(response){ 109 | // Code for handling request success 110 | console.log(response); 111 | }).catch(function (error) { 112 | // Code for handling request error 113 | console.log(error); 114 | }); 115 | ``` 116 | 117 | The endpoint methods return a Promise that is resolved or rejected with the following structure: 118 | ``` 119 | { 120 | success: , 121 | status: , 122 | data: , 123 | errors: 124 | } 125 | ``` 126 | * `success`: This is a boolean that flags if the request was successful or not. True indicates that the request was sent and the response had a HTTP status code of 2xx. False indicates failure to communicate with the Meraki Dashboard or a HTTP status code of 4xx/5xx 127 | * `status`: The HTTP status code returned by the Meraki Dashboard 128 | * `data`: The response body returned by the Meraki Dashboard, if the request was successful 129 | * `errors`: The error explanations returned by the Meraki Dashboard, if any 130 | 131 | # Useful links 132 | * The official Meraki API developer page: https://developer.cisco.com/meraki 133 | * Meraki Dashboard API quick reference with alpha/beta endpoint highlighting: https://github.com/mpapazog/meraki-diff-docs 134 | -------------------------------------------------------------------------------- /offline_logging/README.md: -------------------------------------------------------------------------------- 1 | # offline_logging.py 2 | A Python 3 script to log data from the Meraki dashboard into an external database periodically. 3 | 4 | # The use case 5 | This script is an example of how to cover use cases such as: 6 | * A policy requiring data to be stored in a particular physical location, outside of the Meraki cloud 7 | * A need for longer storage time than what the Meraki cloud provides 8 | * A need for storing regular timestamped snapshots of particular data 9 | 10 | # Project overview 11 | The project consists of a Python 3 script that interacts with a MongoDB database. While other types of deployment are also possible, the instructions in this document focus on how to install and integrate the script with a MongoDB Community Server running on the same server as the script. 12 | 13 | # Components 14 | To use this project, you will need the following: 15 | * A Meraki organization with API access enabled and a Dashboard API key: https://documentation.meraki.com/General_Administration/Other_Topics/Cisco_Meraki_Dashboard_API 16 | * Python 3: https://www.python.org/downloads/ 17 | * The Requests module for Python 3: https://requests.readthedocs.io 18 | * The PyYAML module for Python 3: https://pyyaml.org/ 19 | * The PyMongo module for Python 3: https://pymongo.readthedocs.io 20 | * MongoDB. You can get the Community Server edition for free here: https://www.mongodb.com/try/download/community 21 | * A tool to view your database. You can install MongoDB Compass along with the MongoDB Community Server 22 | 23 | # Installation and startup 24 | * Install Python 3. If installing on Windows, it is recommended to select the "Add to PATH" option during installation 25 | * Install the required third party Python 3 modules. The easiest way to do this is to run: 26 | ``` 27 | Windows: 28 | pip install requests 29 | pip install pyyaml 30 | pip install pymongo 31 | 32 | Linux/Mac: 33 | pip3 install requests 34 | pip3 install pyyaml 35 | pip3 install pymongo 36 | ``` 37 | * Install MongoDB and MongoDB Compass 38 | * Copy **offline_logging.py** and **config.yaml** into a folder on your server 39 | * Open **config.yaml** with a text editor. It is a configuration file the follows the YAML 1.1 format (https://yaml.org/spec/1.1/). Edit the following items in it: 40 | * ...find the **meraki_dashboard_api** section and modify the values for **api_key** and **organization_id** to match your environment. If you do not know the ID of your organization, you can use the interactive tools on this page to find it: https://developer.cisco.com/meraki/api-v1/#!get-organizations 41 | * ...find the **sources** section and define which networks to include in scans. Networks can be included by name, id, network tag, or you can set the **include_all_networks** boolean flag to scan everything. Refer to the examples in the config file for the correct format 42 | * ...find the **endpoints** section and see which items can be logged. Every item has a boolean attribute named **enabled** which can be used to turn the item on or off. To find out more about what exactly each item logs, search for its name on the Meraki Dashboard API documentation page: https://developer.cisco.com/meraki/api-v1/#!overview 43 | * Save your changes to **config.yaml** 44 | * Run the script: 45 | ``` 46 | Windows: 47 | python offline_logging.py -c config.yaml 48 | 49 | Linux/Mac: 50 | python3 offline_logging.py -c config.yaml 51 | ``` 52 | 53 | # Verifying results 54 | Use MongoDB Compass to view the contents of your database. 55 | 56 | # Useful links 57 | The official Meraki API developer page: https://developer.cisco.com/meraki 58 | -------------------------------------------------------------------------------- /offline_logging/config.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample configuration file for offline_logging.py 2 | # You can find the latest version of the script, as well as an up-to-date sample configuration file here: 3 | # https://github.com/meraki/automation-scripts/tree/master/offline_logging 4 | 5 | # This comfiguration file uses YAML 1.1 format: https://yaml.org/spec/1.1/ 6 | 7 | # How often to scan Meraki dashboard for updated info, in minutes. Minumum: 5, maximum: 43000 8 | scan_interval_minutes: 60 9 | meraki_dashboard_api: 10 | # Modify this value to match your Meraki Dashboard API key 11 | api_key: 1234 12 | 13 | # Modify this value to match the organizationId of the organization you are logging data from 14 | # To find your organizationId, by calling this endpoint: https://developer.cisco.com/meraki/api-v1/#!get-organizations 15 | organization_id: 4567 16 | mongodb: 17 | host: localhost 18 | port: 27017 19 | database_name: meraki 20 | 21 | # Which networks to include in scans. If a network has a name, id or tag that matches any of the items in the lists below, 22 | # it will be included in scans. Alternatively, you can set "include_all_networks: true" to log all networks 23 | sources: 24 | network_names: #list 25 | - "Headquarters" 26 | - "Stockholm Branch" 27 | network_ids: #list 28 | network_tags: #list 29 | - "logging" 30 | include_all_networks: false 31 | 32 | # Which endpoints of the Meraki dashboard API to scan. Operation names match Operation Ids in the API: 33 | # https://developer.cisco.com/meraki/api-v1 34 | # Set "enabled: true" for the ones you want to scan and "enabled: false" for the ones you want to omit 35 | 36 | endpoints: 37 | getNetworkClients: 38 | enabled: true 39 | # whether to skip clients with MAC address manufacturer "Meraki" or "Cisco Meraki" 40 | ignore_manufacturer_meraki: true 41 | collection: networkClients 42 | mode: append 43 | getNetworkClientsApplicationUsage: 44 | # requires getNetworkClients 45 | # Note that this will not work correctly for short scan intervals. 20min+ is recommended 46 | enabled: true 47 | collection: networkClientsApplicationUsage 48 | mode: append 49 | getNetworkClientTrafficHistory: 50 | # requires getNetworkClients 51 | # This can be very slow in large environments, since every client needs to be fetched individually 52 | # and log entries on long-running networks can be huge, even hitting the MongoDB 16MB/document limit 53 | enabled: false 54 | # Splits large traffic history arrays to multiple pages to be able to write them into the database. 55 | # Lower this value if you are getting "document too large" errors, or increase to have fewer documents 56 | # per client 57 | max_history_records_per_document: 10000 58 | collection: networkClientTrafficHistory 59 | mode: update 60 | getNetworkMerakiAuthUsers: 61 | enabled: true 62 | # whether to log template users, if network is bound to a config template 63 | include_template_users: true 64 | collection: networkMerakiAuthUsers 65 | mode: update 66 | getOrganizationAdmins: 67 | enabled: true 68 | collection: organizationAdmins 69 | mode: update 70 | getNetworkSmDevices: 71 | enabled: true 72 | collection: networkSmDevices 73 | mode: update 74 | # Set "filter_by_device_tag_enabled" to true to only log devices with a specific device tag 75 | # Set the device tag to be matched in "target_device_tag" 76 | filter_by_device_tag_enabled: false 77 | target_device_tag: logging -------------------------------------------------------------------------------- /provision_sites/provision_sites_example_input_file.csv: -------------------------------------------------------------------------------- 1 | #INPUT FILE GENERATOR FOR provision_sites.py;;;;;;;;;;;;;; 2 | #;;;;;;;;;;;;;; 3 | #Do not remove, modify or reorder the line below;;;;;;;;;;;;;; 4 | meta:delimeter-detector-line;;;;;;;;;;;;;; 5 | #;;;;;;;;;;;;;; 6 | #Format of this file:;;;;;;;;;;;;;; 7 | "# * Lines beginning with # or "" are comments and will be ignored";;;;;;;;;;;;;; 8 | # * Starting with row 20, one line equals one created network;;;;;;;;;;;;;; 9 | # * Columns D through N are used to define manual subnet overrides;;;;;;;;;;;;;; 10 | # * The last column contains an optional space-separated list of serial numbers to be claimed into the network;;;;;;;;;;;;;; 11 | #;;;;;;;;;;;;;; 12 | #How to use:;;;;;;;;;;;;;; 13 | # * In cells D19 through N19, replace X with the number of the VLAN you want to manually override the subnet of. Any columns left as VLAN X will be ignored;;;;;;;;;;;;;; 14 | # * If needed, you can add more VLAN columns before the serial numbers' column;;;;;;;;;;;;;; 15 | "# * The ""Street address"" cell directs the script to reset physical location information for any claimed devices to this value. Leave blank to skip";;;;;;;;;;;;;; 16 | # * Enter site info as shown in the example row. Define subnet overrides as the network IP address with no mask or prefix. Leave cells blank to skip;;;;;;;;;;;;;; 17 | "# * Export file as a CSV. Either use "","" or "";"" as a delimeter (but not both). Use the CSV as an input file to run provision_sites.py";;;;;;;;;;;;;; 18 | #;;;;;;;;;;;;;; 19 | Network name;Street address;Template name;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;VLAN X;Device serial numbers to claim into network, separated by spaces 20 | Example Net (modify or delete this row);Example str 1;store-blueprint;192.168.0.0;;;;;;;;;;;AAAA-BBBB-CCCC 21 | -------------------------------------------------------------------------------- /provision_sites/provision_sites_input_file_generator.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/automation-scripts/2f1ad9c81cfa8f947f9cc9ca126e818faaa3abb1/provision_sites/provision_sites_input_file_generator.xlsx -------------------------------------------------------------------------------- /reboot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import meraki 4 | import sys 5 | 6 | 7 | dashboard = meraki.DashboardAPI(suppress_logging=True) 8 | 9 | 10 | def reboot(file): 11 | with open(file, 'r') as device: 12 | for i in device: 13 | serial = i.strip() 14 | response = dashboard.devices.rebootDevice(serial) 15 | print(f'Device {serial} was rebooted {response}') 16 | sys.exit() 17 | 18 | 19 | if __name__ == '__main__': 20 | try: 21 | sys.argv[1] 22 | except IndexError: 23 | print("Please provide a file with serial numbers") 24 | sys.exit() 25 | else: 26 | file = ' '.join(sys.argv[1:]) 27 | reboot(file) 28 | 29 | read_me = ''' 30 | A Python 3 script to reboot Meraki Devices. 31 | Required Python modules: 32 | meraki 33 | 34 | Usage: 35 | bssid.py file.txt 36 | 37 | "file" should be a txt with the list of serail numbers that need to be rebooted 38 | one on each line 39 | 40 | serial1 41 | serial2 42 | serial3 43 | 44 | API Key 45 | requires you to have your API key in env vars as 'MERAKI_DASHBOARD_API_KEY' 46 | ''' 47 | -------------------------------------------------------------------------------- /report_statuses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | === PREREQUISITES === 5 | Run in Python 3.6+ 6 | 7 | Install Meraki Python library: pip[3] install --upgrade meraki 8 | 9 | === DESCRIPTION === 10 | This script reports the org-wide appliances' uplink and devices's statuses. 11 | 12 | === USAGE === 13 | python[3] report_statuses.py -k -o 14 | 15 | Use "-o /all" to iterate through all organizations 16 | """ 17 | 18 | 19 | import argparse 20 | import csv 21 | import meraki 22 | import os 23 | 24 | 25 | def parse_arguments(parser): 26 | parser.add_argument('-k', '--key', help='Dashboard API key. If omitted, will use environment variable MERAKI_DASHBOARD_API_KEY') 27 | parser.add_argument('-o', '--org', help='Organization ID. Use "-o /all" to iterate through all organizations') 28 | parser.exit 29 | args = parser.parse_args() 30 | return args.key, args.org 31 | 32 | def getApiKey(argument): 33 | if not argument is None: 34 | return str(argument) 35 | return os.environ.get("MERAKI_DASHBOARD_API_KEY", None) 36 | 37 | def main(): 38 | # Check if all required parameters have been specified 39 | parser = argparse.ArgumentParser() 40 | tmp_api_key, arg_org_id = parse_arguments(parser) 41 | 42 | api_key = getApiKey(tmp_api_key) 43 | 44 | if not(api_key and arg_org_id): 45 | parser.exit(2, parser.print_help()) 46 | 47 | # Make API calls to retrieve data 48 | dashboard = meraki.DashboardAPI(api_key) 49 | 50 | org_id_list = [] 51 | flag_multi_org = False 52 | if arg_org_id == '/all': 53 | flag_multi_org = True 54 | orgs_result = dashboard.organizations.getOrganizations() 55 | for org in orgs_result: 56 | org_id_list.append(org['id']) 57 | else: 58 | org_id_list.append(arg_org_id) 59 | 60 | for org_id in org_id_list: 61 | try: 62 | appliance_statuses = dashboard.appliance.getOrganizationApplianceUplinkStatuses(org_id, total_pages='all') 63 | device_statuses = dashboard.organizations.getOrganizationDevicesStatuses(org_id, total_pages='all') 64 | networks = dashboard.organizations.getOrganizationNetworks(org_id, total_pages='all') 65 | except Exception as e: 66 | print(str(e)) 67 | continue 68 | devices_by_serial = {d['serial']: d['name'] for d in device_statuses} 69 | networks_by_id = {n['id']: n['name'] for n in networks} 70 | 71 | # Output appliance statuses file 72 | output_file = 'appliance_statuses' 73 | if flag_multi_org: 74 | output_file += '_' + org_id 75 | output_file += '.csv' 76 | field_names = ['name', 'serial', 'model', 'network', 'networkId', 'lastReportedAt', 77 | 'wan1_status', 'wan1_ip', 'wan1_gateway', 'wan1_publicIp', 'wan1_primaryDns', 'wan1_secondaryDns', 78 | 'wan1_ipAssignedBy', 'wan2_status', 'wan2_ip', 'wan2_gateway', 'wan2_publicIp', 'wan2_primaryDns', 79 | 'wan2_secondaryDns', 'wan2_ipAssignedBy', 'cellular_status', 'cellular_ip', 'cellular_provider', 'highAvailability', 80 | 'cellular_publicIp', 'cellular_model', 'cellular_signalStat', 'cellular_connectionType', 'cellular_apn'] 81 | with open(output_file, mode='w', newline='\n') as fp: 82 | csv_writer = csv.DictWriter(fp, field_names, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 83 | csv_writer.writeheader() 84 | for status in appliance_statuses: 85 | status.update( 86 | { 87 | 'name': devices_by_serial[status['serial']], 88 | 'network': networks_by_id[status['networkId']] 89 | } 90 | ) 91 | 92 | # Flatten objects/dictionaries, without requiring a third-party library 93 | interfaces = [uplink['interface'] for uplink in status['uplinks']] 94 | if 'wan1' in interfaces: 95 | wan1 = status['uplinks'][interfaces.index('wan1')] 96 | status.update( 97 | { 98 | 'wan1_status': wan1['status'], 99 | 'wan1_ip': wan1['ip'], 100 | 'wan1_gateway': wan1['gateway'], 101 | 'wan1_publicIp': wan1['publicIp'], 102 | 'wan1_primaryDns': wan1['primaryDns'], 103 | 'wan1_secondaryDns': wan1['secondaryDns'], 104 | 'wan1_ipAssignedBy': wan1['ipAssignedBy'] 105 | } 106 | ) 107 | if 'wan2' in interfaces: 108 | wan2 = status['uplinks'][interfaces.index('wan2')] 109 | status.update( 110 | { 111 | 'wan2_status': wan2['status'], 112 | 'wan2_ip': wan2['ip'], 113 | 'wan2_gateway': wan2['gateway'], 114 | 'wan2_publicIp': wan2['publicIp'], 115 | 'wan2_primaryDns': wan2['primaryDns'], 116 | 'wan2_secondaryDns': wan2['secondaryDns'], 117 | 'wan2_ipAssignedBy': wan2['ipAssignedBy'] 118 | } 119 | ) 120 | if 'cellular' in interfaces: 121 | cellular = status['uplinks'][interfaces.index('cellular')] 122 | status.update( 123 | { 124 | 'cellular_status': cellular['status'], 125 | 'cellular_ip': cellular['ip'], 126 | 'cellular_provider': cellular['provider'], 127 | 'cellular_publicIp': cellular['publicIp'], 128 | 'cellular_model': cellular['model'], 129 | 'cellular_signalStat': cellular['signalStat'], 130 | 'cellular_connectionType': cellular['connectionType'], 131 | 'cellular_apn': cellular['apn'] 132 | } 133 | ) 134 | status.pop('uplinks') 135 | #print(status) 136 | csv_writer.writerow(status) 137 | 138 | # Output device statuses file 139 | output_file = 'device_statuses' 140 | if flag_multi_org: 141 | output_file += '_' + org_id 142 | output_file += '.csv' 143 | field_names = ['name', 'serial', 'model', 'network', 'networkId', 'mac', 'publicIp', 'status', 'lastReportedAt', 'lanIp', 'gateway', 'ipType', 144 | 'primaryDns', 'secondaryDns', 'productType', 'tags', 'usingCellularFailover', 'wan1Ip', 'wan1Gateway', 'wan1IpType', 145 | 'wan1PrimaryDns', 'wan1SecondaryDns', 'wan2Ip', 'wan2Gateway', 'wan2IpType', 'wan2PrimaryDns', 'wan2SecondaryDns', 'components', 146 | 'configurationUpdatedAt'] 147 | with open(output_file, mode='w', newline='\n') as fp: 148 | csv_writer = csv.DictWriter(fp, field_names, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 149 | csv_writer.writeheader() 150 | for status in device_statuses: 151 | status.update({'network': networks_by_id[status['networkId']]}) 152 | print(status) 153 | csv_writer.writerow(status) 154 | 155 | 156 | if __name__ == '__main__': 157 | main() 158 | -------------------------------------------------------------------------------- /rotate_psks/psk_rotator.py: -------------------------------------------------------------------------------- 1 | import meraki 2 | import smtplib, ssl 3 | import os 4 | import time 5 | from xkcdpass import xkcd_password as xp 6 | 7 | ''' 8 | Cisco Meraki PSK Rotator 9 | John M. Kuchta .:|:.:|:. https://github.com/TKIPisalegacycipher 10 | Goals 11 | * Generate random PSK 12 | * Update one specific SSID's PSK 13 | * Email one specific address a notification of the change 14 | 15 | If you plan to use Gmail's SMTP server, please consult https://support.google.com/a/answer/176600 before use. 16 | There are a few caveats to using it, and you might have errors unless you follow the directions in that doc, 17 | and update this code to use the settings that are right for your organization. 18 | 19 | USAGE 20 | Update the SMTP configuration to match your SMTP server 21 | Update the Meraki dashboard configuration section to match your desired outcome 22 | Update the password generator configuration to meet your complexity needs 23 | Test in a test organization with test networks to confirm desired functionality 24 | The rest is history! 25 | ''' 26 | 27 | # SMTP configuration 28 | # Please note: Gmail SMTP servers can be tricky to use. 29 | # Please read https://support.google.com/a/answer/176600 before using Gmail. 30 | smtp = dict() 31 | smtp['port'] = 465 32 | smtp['fqdn'] = 'smtp.gmail.com' # Please consult https://support.google.com/a/answer/176600 before using Gmail SMTP. 33 | smtp['username'] = os.environ['PYTHON_SMTP_USERNAME'] 34 | smtp['password'] = os.environ['PYTHON_SMTP_PASSWORD'] 35 | 36 | # Meraki dashboard configuration 37 | ORGANIZATION_ID = 'YOUR ORGANIZATION ID HERE' 38 | SSID_NUMBER = 10 # choose the single SSID number that you want to update across the entire org. 39 | ACTIONS_PER_BATCH = 100 # leave at 100 for max efficiency 40 | RECIPIENT_EMAIL = 'YOUR_RECIPIENT@EMAIL.HERE' 41 | 42 | # Password generator configuration 43 | # Generated password will take the form of several human-readable words. Max and min length apply to each word. 44 | MINIMUM_LENGTH = 4 45 | MAXIMUM_LENGTH = 7 46 | NUMBER_OF_WORDS = 3 47 | 48 | 49 | # Email methods 50 | def create_smtp_server(fqdn=smtp['fqdn'], port=smtp['port'], username=smtp['username'], password=smtp['password']): 51 | # Returns logged-in SMTP server object 52 | # Create SSL context 53 | ssl_context = ssl.create_default_context() 54 | # Log in to SMTP server 55 | with smtplib.SMTP_SSL(fqdn, port, context=ssl_context) as smtp_server: 56 | smtp_server.login(username, password) 57 | 58 | return smtp_server 59 | 60 | 61 | def send_email(server, recipient, message, sender=smtp['username']): 62 | # Sends an email to the specified recipients 63 | send = server.sendmail(sender, recipient, message) 64 | return send 65 | 66 | 67 | # Action and action batch methods 68 | def check_batch_queue(dashboard, organizationId): 69 | pending_action_batches = dashboard.organizations.getOrganizationActionBatches(organizationId=organizationId, 70 | status='pending') 71 | active_action_batches = [batch for batch in pending_action_batches if batch['confirmed']] 72 | batch_queue_is_full = True if len(active_action_batches) > 4 else False 73 | return pending_action_batches, active_action_batches, batch_queue_is_full 74 | 75 | 76 | def add_to_batch_queue(dashboard, organizationId, new_batches, wait_time=5, confirmed=True): 77 | # Adds batches to the queue and returns the responses when they have been submitted. 78 | pending_action_batches, active_action_batches, batch_queue_is_full = check_batch_queue(dashboard, organizationId) 79 | remaining_new_batches = len(new_batches) 80 | batch_responses = list() 81 | 82 | while remaining_new_batches: 83 | while batch_queue_is_full and confirmed: 84 | print(f'There are already {len(active_action_batches)} active action batches. Waiting {wait_time} ' 85 | f'seconds before trying again.') 86 | time.sleep(wait_time) 87 | 88 | pending_action_batches, active_action_batches, batch_queue_is_full = check_batch_queue(dashboard, organizationId) 89 | 90 | print(f'Creating the next action batch. {remaining_new_batches} action batches remain.') 91 | batch_response = dashboard.organizations.createOrganizationActionBatch(**new_batches.pop(0)) 92 | batch_responses.append(batch_response) 93 | pending_action_batches, active_action_batches, batch_queue_is_full = check_batch_queue(dashboard, organizationId) 94 | remaining_new_batches = len(new_batches) 95 | 96 | return batch_responses 97 | 98 | 99 | def group_actions(actions_list, actions_per_batch): 100 | # Groups actions into lists of appropriate size 101 | # Returns a list generator 102 | total_actions = len(actions_list) 103 | for i in range(0, total_actions, actions_per_batch): 104 | yield actions_list[i:i + actions_per_batch] 105 | 106 | 107 | def create_batches(organizationId, actions_list, actions_per_batch=ACTIONS_PER_BATCH, 108 | synchronous=False, confirmed=True): 109 | # Groups actions, then create and optionally run batches 110 | # Validate the actions_per_batch and synchronous arguments 111 | if actions_per_batch > 100: 112 | print('One asynchronous action batch may contain up to a maximum of 100 actions. Using maximum instead.') 113 | actions_per_batch = 100 114 | elif actions_per_batch > 20 and synchronous: 115 | print('One synchronous action batch may contain up to a maximum of 20 actions. Using maximum instead.') 116 | actions_per_batch = 20 117 | 118 | # Group the actions into lists of appropriate size 119 | grouped_actions_list = list(group_actions(actions_list, actions_per_batch)) 120 | created_batches = list() 121 | 122 | # Add each new batch to the new_batches list 123 | for action_list in grouped_actions_list: 124 | batch = { 125 | "organizationId": organizationId, 126 | "actions": action_list, 127 | "synchronous": synchronous, 128 | "confirmed": confirmed 129 | } 130 | created_batches.append(batch) 131 | 132 | return created_batches 133 | 134 | 135 | # SSID & PSK methods 136 | def create_action_for_each_network(networks_list, create_action, **kwargs): 137 | # Returns a list of one action per network in an organization 138 | actions = list() 139 | for network in networks_list: 140 | action = create_action(networkId=network['id'], **kwargs) 141 | actions.append(action) 142 | 143 | return actions 144 | 145 | 146 | def initialize(): 147 | # Initialize settings 148 | # Initialize Dashboard connection 149 | dashboard = meraki.DashboardAPI( 150 | caller="PSKRotator/1.0 Kuchta Meraki" 151 | ) 152 | 153 | # Get the networks list 154 | networks_list = dashboard.organizations.getOrganizationNetworks( 155 | organizationId=ORGANIZATION_ID 156 | ) 157 | 158 | # PASSPHRASE/PSK GENERATION 159 | # Password generator configuration 160 | password_word_list = xp.generate_wordlist(min_length=MINIMUM_LENGTH, max_length=MAXIMUM_LENGTH) 161 | 162 | # Generate today's PSK 163 | new_psk = xp.generate_xkcdpassword(password_word_list, numwords=NUMBER_OF_WORDS) 164 | 165 | return dashboard, networks_list, new_psk 166 | 167 | 168 | def change_psks(dashboard, organizationId, networks_list, ssid_number, new_psk): 169 | # Specify the bulk action 170 | change_psk_action = dashboard.batch.wireless.updateNetworkWirelessSsid 171 | 172 | # Format the action kwargs 173 | change_psk_kwargs = { 174 | 'number': ssid_number, 175 | 'psk': new_psk 176 | } 177 | 178 | print('Creating actions_for_networks...') 179 | actions_for_networks = create_action_for_each_network(networks_list, change_psk_action, **change_psk_kwargs) 180 | 181 | print(f'The total number of actions in your batches is {len(actions_for_networks)}. Creating the batches...') 182 | new_batches = create_batches(organizationId, actions_for_networks) 183 | 184 | print(f'The number of new action batches is {len(new_batches)}. Adding batches to queue...') 185 | added_batch_responses = add_to_batch_queue(dashboard, organizationId, new_batches) 186 | 187 | return added_batch_responses 188 | 189 | 190 | # main 191 | if __name__ == "__main__": 192 | dashboard, networks_list, new_psk = initialize() 193 | responses = change_psks(dashboard, ORGANIZATION_ID, networks_list, SSID_NUMBER, new_psk) 194 | smtp_server = create_smtp_server() 195 | smtp_server.connect(smtp['fqdn'], smtp['port']) 196 | smtp_server.ehlo() 197 | smtp_server.login(smtp['username'], smtp['password']) 198 | email = send_email(smtp_server, RECIPIENT_EMAIL, f'The new Meraki SSID PSK is "{new_psk}".\nThank you for ' 199 | f'choosing Meraki!') 200 | 201 | print(f'The new Meraki SSID PSK is "{new_psk}".\nThank you for choosing Meraki!') 202 | 203 | -------------------------------------------------------------------------------- /secure_connect/Private_Applications_CSV_Import/README.md: -------------------------------------------------------------------------------- 1 | # Meraki Secure Connect Private Applications Import Script 2 | 3 | This Python script helps you to import Meraki Secure Connect private applications from a CSV file into your Meraki Dashboard. It automates the process of creating private applications with various configurations. 4 | 5 | ## Features 6 | 7 | - Imports private applications from a CSV file into your Meraki Dashboard 8 | - Validates CSV file format and content 9 | - Skips unnecessary fields if they have no value in the CSV file 10 | - Aborts the script and provides an error message if required fields are missing 11 | 12 | ## Requirements 13 | 14 | - Python 3.x 15 | - `requests` library 16 | - `colorama` library 17 | 18 | To install required libraries, run: 19 | 20 | ```bash 21 | pip3 install requests colorama 22 | ``` 23 | 24 | ## Usage 25 | 26 | 1. Clone or download the repository. 27 | 2. Prepare a CSV file containing private application data following the format specified below. 28 | 3. Run the script: 29 | 30 | ```bash 31 | python3 meraki_private_app_import.py 32 | ``` 33 | 34 | 4. Follow the prompts to enter your Meraki API key and organization ID. 35 | 5. The script will validate the CSV file and import the private applications into your Meraki Dashboard. 36 | 37 | ## CSV File Format 38 | 39 | The CSV file should have the following columns: 40 | 41 | - `name` (required): Name of the private application 42 | - `description`: Description of the private application 43 | - `destinationAddr1` (required): First destination IP address or subnet 44 | - `destinationAddr2`: Second destination IP address or subnet (optional) 45 | - `protocol` (required): Protocol for the private application (TCP or UDP) 46 | - `ports`: Ports for the private application 47 | - `accessType` (required): Access type for the private application (Allowed or Denied) 48 | - `appProtocol`: Application protocol 49 | - `externalFQDN`: External FQDN for the private application 50 | - `sni`: SNI for the private application 51 | - `sslVerificationEnabled`: Enable SSL verification (True or False) 52 | - `applicationGroupIds`: Comma-separated list of application group IDs 53 | 54 | Example CSV file: 55 | 56 | ``` 57 | name,description,destinationAddr1,destinationAddr2,protocol,ports,accessType,appProtocol,sni,externalFQDN,sslVerificationEnabled,applicationGroupIds 58 | Jira TEST-1,Jira App For My Org.1,142.6.0.0/32,192.168.1.10/32,TCP,80-82,browser,https,xyz123.jira1.com,https://jira1-5001.ztna.ciscoplus.com,TRUE,"83, 114" 59 | Jira TEST-2,Jira App For My Org.2,152.6.0.0/32,192.168.1.20/32,TCP,80-82,browser,https,xyz123.jira2.com,https://jira2-5001.ztna.ciscoplus.com,TRUE,"83, 114" 60 | Jira TEST-3,Jira App For My Org.3,162.6.0.0/32,192.168.1.30/32,TCP,80-82,browser,https,xyz123.jira3.com,https://jira3-5001.ztna.ciscoplus.com,TRUE,"83, 114" 61 | Jira TEST-4,Jira App For My Org.4,172.6.0.0/32,192.168.1.40/32,UDP,80-82,network,https,xyz123.jira4.com,https://jira4-5001.ztna.ciscoplus.com,FALSE,"83, 114" 62 | Jira TEST-5,Jira App For My Org.5,182.6.0.0/32,192.168.1.50/32,UDP,80-82,network,https,xyz123.jira5.com,https://jira5-5001.ztna.ciscoplus.com,FALSE,"83, 114" 63 | Jira TEST-6,,192.6.0.0/32,,TCP,80,network,,,,, 64 | ``` 65 | 66 | ## Maintainers & Contributors 67 | 68 | [Yossi Meloch](mailto:ymeloch@cisco.com) 69 | 70 | ## Acknowledgements 71 | 72 | - [Cisco Meraki](https://www.meraki.com/) for providing a robust and easy-to-use API 73 | 74 | Please note that this script is provided "as is" without warranty of any kind, either expressed or implied, including limitation warranties of merchantability, fitness for a particular purpose, and noninfringement. Use at your own risk. -------------------------------------------------------------------------------- /secure_connect/Private_Applications_CSV_Import/privateApplicationsImport.csv: -------------------------------------------------------------------------------- 1 | name,description,destinationAddr1,destinationAddr2,protocol,ports,accessType,appProtocol,sni,externalFQDN,sslVerificationEnabled,applicationGroupIds 2 | Jira TEST-1,Jira App For My Org.1,142.6.0.0/32,192.168.1.10/32,TCP,80-82,browser,https,xyz123.jira1.com,https://jira1-5001.ztna.ciscoplus.com,TRUE,"83, 114" 3 | Jira TEST-2,Jira App For My Org.2,152.6.0.0/32,192.168.1.20/32,TCP,80-82,browser,https,xyz123.jira2.com,https://jira2-5001.ztna.ciscoplus.com,TRUE,"83, 114" 4 | Jira TEST-3,Jira App For My Org.3,162.6.0.0/32,192.168.1.30/32,TCP,80-82,browser,https,xyz123.jira3.com,https://jira3-5001.ztna.ciscoplus.com,TRUE,"83, 114" 5 | Jira TEST-4,Jira App For My Org.4,172.6.0.0/32,192.168.1.40/32,UDP,80-82,network,https,xyz123.jira4.com,https://jira4-5001.ztna.ciscoplus.com,FALSE,"83, 114" 6 | Jira TEST-5,Jira App For My Org.5,182.6.0.0/32,192.168.1.50/32,UDP,80-82,network,https,xyz123.jira5.com,https://jira5-5001.ztna.ciscoplus.com,FALSE,"83, 114" 7 | Jira TEST-6,,192.6.0.0/32,,TCP,80,network,,,,, -------------------------------------------------------------------------------- /secure_connect/Private_Applications_CSV_Import/privateApplicationsImport.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import getpass 3 | import csv 4 | import json 5 | import os 6 | from colorama import Fore, Style, init 7 | 8 | # Initialize colorama for Windows compatibility 9 | init() 10 | 11 | def print_welcome_message(): 12 | print(f"\n{Fore.GREEN}Welcome to Meraki Secure Connect Private Applications Import Script{Style.RESET_ALL}") 13 | print(f"{Fore.GREEN}==================================================================={Style.RESET_ALL}\n") 14 | 15 | def get_credentials(): 16 | api_key = getpass.getpass('[1/2] Meraki API Key (MASKED): ') 17 | org_id = input('[2/2] Meraki Org ID: ') 18 | return api_key, org_id 19 | 20 | # function `validate_csv_line` checks if the required fields "name", "destinationAddr1", "protocol", and "accessType" have values. 21 | # If any value is missing, the script will abort and print out an error message specifying the missing field and line number. 22 | 23 | def validate_csv_line(line, line_number): 24 | required_fields = ["name", "destinationAddr1", "protocol", "accessType"] 25 | for field in required_fields: 26 | if not line[field]: 27 | print(f"{Fore.RED}Error: Missing value for '{field}' in line {line_number}.{Style.RESET_ALL}") 28 | return False 29 | return True 30 | 31 | def read_csv_file(file_path): 32 | print("\n[x] Validating CSV file location") 33 | if not os.path.exists(file_path): 34 | print(f"{Fore.RED}Error: CSV file '{file_path}' does not exist.{Style.RESET_ALL}") 35 | return None 36 | 37 | print(f"{Fore.BLUE}[x] Executing Script{Style.RESET_ALL}") 38 | with open(file_path, mode='r') as csv_file: 39 | csv_reader = csv.DictReader(csv_file) 40 | payload_data_list = [] 41 | for line_number, line in enumerate(csv_reader, start=1): 42 | if validate_csv_line(line, line_number): 43 | payload_data_list.append(line) 44 | else: 45 | return None 46 | return payload_data_list 47 | 48 | def create_payload(payload_data): 49 | # Construct the payload from CSV data 50 | destinations = { 51 | "destinationAddr": [payload_data["destinationAddr1"]], 52 | "protocolPorts": [ 53 | { 54 | "protocol": payload_data["protocol"], 55 | "ports": payload_data["ports"] 56 | } 57 | ], 58 | "accessType": payload_data["accessType"] 59 | } 60 | 61 | # This updates the `create_payload` function to conditionally include `payload_data["destinationAddr2"]` 62 | # in the `destinationAddr` list only if it has a value. 63 | # The script will ignore the "destinationAddr2" value if it has no value in the CSV file and process only the "destinationAddr1" value. 64 | # The same can be done with other values/keys. 65 | 66 | if payload_data["destinationAddr2"]: 67 | destinations["destinationAddr"].append(payload_data["destinationAddr2"]) 68 | 69 | payload = { 70 | "name": payload_data["name"], 71 | "destinations": [destinations], 72 | } 73 | 74 | # conditionally includes the fields "description", "appProtocol", "externalFQDN", "sni", "sslVerificationEnabled" and "applicationGroupIds" 75 | # in the payload only if they have values in the CSV file. The script will ignore these values if they have no value in the CSV file. 76 | 77 | if payload_data["description"]: 78 | payload["description"] = payload_data["description"] 79 | 80 | if payload_data["appProtocol"]: 81 | payload["appProtocol"] = payload_data["appProtocol"] 82 | 83 | if payload_data["externalFQDN"]: 84 | payload["externalFQDN"] = payload_data["externalFQDN"] 85 | 86 | if payload_data["sni"]: 87 | payload["sni"] = payload_data["sni"] 88 | 89 | if payload_data["sslVerificationEnabled"]: 90 | payload["sslVerificationEnabled"] = payload_data["sslVerificationEnabled"].lower() == 'true' 91 | 92 | if payload_data["applicationGroupIds"]: 93 | payload["applicationGroupIds"] = [int(x) for x in payload_data["applicationGroupIds"].split(',')] 94 | 95 | return payload 96 | 97 | def send_request(api_key, org_id, payload): 98 | # Send POST request to Meraki API and handle exceptions 99 | try: 100 | url = f"https://api.meraki.com/api/v1/organizations/{org_id}/secureConnect/privateApplications" 101 | headers = { 102 | "Content-Type": "application/json", 103 | "Accept": "application/json", 104 | "X-Cisco-Meraki-API-Key": api_key 105 | } 106 | response = requests.post(url, headers=headers, data=json.dumps(payload)) 107 | response.raise_for_status() 108 | return response.json(), response.status_code 109 | except requests.exceptions.RequestException as e: 110 | print(f"{Fore.RED}Error: {e}{Style.RESET_ALL}") 111 | return None, None 112 | 113 | def main(): 114 | print_welcome_message() 115 | api_key, org_id = get_credentials() 116 | payload_data_list = read_csv_file('privateApplicationsImport.csv') 117 | 118 | if payload_data_list is None: 119 | print(f"{Fore.RED}Exiting the script.{Style.RESET_ALL}") 120 | return 121 | 122 | for index, payload_data in enumerate(payload_data_list, start=1): 123 | payload = create_payload(payload_data) 124 | output, status_code = send_request(api_key, org_id, payload) 125 | if output is not None and status_code is not None: 126 | print(f"\t{Fore.GREEN}[{index}] Success: Application '{output['name']}' with ID '{output['applicationId']}' created.{Style.RESET_ALL}") 127 | else: 128 | print(f"\t{Fore.YELLOW}[{index}] Skipping the current payload due to an error.{Style.RESET_ALL}") 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /secure_connect/Remote_Access_Logs_Analyzer/README.md: -------------------------------------------------------------------------------- 1 | # Meraki Secure Connect Remote Access Logs Analyzer 2 | 3 | This script fetches and analyzes Meraki Secure Connect remote access logs from the Meraki API and generates statistics for selected columns. The results are displayed in a color-coded table, and the data can be saved to a CSV file. 4 | 5 | ## Requirements 6 | 7 | - Python 3.6+ 8 | - `pandas` library 9 | - `termcolor` library 10 | - `requests` library 11 | 12 | Install required libraries using the following command: 13 | 14 | ```bash 15 | pip3 install pandas termcolor requests 16 | ``` 17 | 18 | ## Usage 19 | 20 | Run the script from the command line: 21 | 22 | ```bash 23 | python3 remoteAccessLogsAnalyzer.py 24 | ``` 25 | 26 | You will be prompted to enter your Meraki organization ID, API key, and the desired date range (in epoch time). 27 | 28 | The script will fetch remote access logs from the Meraki API, parse them, and generate a DataFrame with the following columns: 29 | 30 | - Timestamp 31 | - Connect Timestamp 32 | - Connection Event 33 | - OS Version 34 | - AnyConnect Version 35 | - Internal IP 36 | - External IP 37 | 38 | The Connection Event and OS Version columns are color-coded for better readability. "connected" values are displayed in green, while "disconnected" values are displayed in red. Windows OS versions are displayed in yellow, and other OS versions are displayed in blue. 39 | 40 | The script will also generate statistics for the following columns: 41 | 42 | - OS Version 43 | - AnyConnect Version 44 | - Connection Event 45 | - Internal IP 46 | 47 | The generated data can be saved to a CSV file with the creation date in the file name. 48 | 49 | ## Maintainers & Contributors 50 | 51 | [Yossi Meloch](mailto:ymeloch@cisco.com) 52 | 53 | ## Acknowledgements 54 | 55 | - [Cisco Meraki](https://www.meraki.com/) for providing a robust and easy-to-use API 56 | 57 | Please note that this script is provided "as is" without warranty of any kind, either expressed or implied, including limitation warranties of merchantability, fitness for a particular purpose, and noninfringement. Use at your own risk. 58 | -------------------------------------------------------------------------------- /secure_connect/Remote_Access_Logs_Analyzer/remoteAccessLogsAnalyzer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import getpass 3 | import time 4 | import pandas as pd 5 | from termcolor import colored 6 | 7 | pd.set_option('display.max_columns', None) # Display all columns 8 | pd.set_option('display.max_colwidth', None) # Don't truncate column content 9 | pd.set_option('display.max_rows', None) # Display all rows 10 | pd.set_option('display.width', None) # Adjust display width to fit all columns 11 | 12 | def input_epoch_time(prompt): 13 | """Helper function to prompt the user for an epoch time input.""" 14 | while True: 15 | user_input = input(prompt) 16 | try: 17 | epoch_time = int(user_input) 18 | return epoch_time 19 | except ValueError: 20 | print("Invalid input. Please enter a valid epoch time.") 21 | 22 | def save_dataframe_to_csv(df): 23 | """Save the given DataFrame to a CSV file with the creation date in the file name.""" 24 | file_name = f"meraki_log_{time.strftime('%Y-%m-%d_%H-%M-%S')}.csv" 25 | df.to_csv(file_name, index=False) 26 | print(f"Data saved to {file_name}") 27 | 28 | def main(): 29 | """Main function to fetch and analyze Meraki remote access logs.""" 30 | # User input for Meraki organization ID 31 | organization_id = input("Enter your Meraki organization ID: ") 32 | 33 | # User input (masked) for Meraki API key 34 | api_key = getpass.getpass("Enter your Meraki API key: ") 35 | 36 | # User input for start and end date in epoch time 37 | starting_after = input_epoch_time("Enter the start date in epoch time: ") 38 | ending_before = input_epoch_time("Enter the end date in epoch time: ") 39 | 40 | # Verify that the input time should be less than 30 days 41 | time_difference = ending_before - starting_after 42 | if time_difference > 30 * 24 * 60 * 60: 43 | print("Error: The time range should be less than 30 days.") 44 | return 45 | 46 | # Construct the API request 47 | base_url = 'https://api.meraki.com/api/v1' 48 | headers = { 49 | 'Authorization': f'Bearer {api_key}', 50 | 'Content-Type': 'application/json' 51 | } 52 | endpoint = f'/organizations/{organization_id}/secureConnect/remoteAccessLog' 53 | params = { 54 | 't0': starting_after, 55 | 't1': ending_before 56 | } 57 | 58 | # Send the API request and process the response 59 | response = requests.get(base_url + endpoint, headers=headers, params=params) 60 | if response.status_code == 200: 61 | nested_data = response.json() 62 | events = nested_data['data'] 63 | df_events = pd.json_normalize(events) 64 | 65 | # Convert timestamp columns to datetime format 66 | if 'timestamp' in df_events.columns: 67 | df_events['timestamp'] = pd.to_datetime(df_events['timestamp'], unit='s') 68 | if 'connecttimestamp' in df_events.columns: 69 | df_events['connecttimestamp'] = df_events['connecttimestamp'].apply( 70 | lambda x: int(float(x)) if not pd.isna(x) else x 71 | ) 72 | df_events['connecttimestamp'] = pd.to_datetime(df_events['connecttimestamp'], unit='s') 73 | 74 | # Color "connected" values in green and "disconnected" values in red 75 | if 'connectionevent' in df_events.columns: 76 | df_events['connectionevent'] = df_events['connectionevent'].apply( 77 | lambda x: colored(x, 'green') if x == 'connected' else colored(x, 'red') 78 | ) 79 | 80 | # Color "win-" values in orange and "mac-" values in blue 81 | if 'osversion' in df_events.columns: 82 | df_events['osversion'] = df_events['osversion'].apply( 83 | lambda x: colored(x, 'yellow') if x.startswith('win-') else colored(x, 'blue') 84 | ) 85 | 86 | # Display all columns in expanded form 87 | with pd.option_context('display.max_columns', None): 88 | print(df_events) 89 | 90 | # Generate statistics for selected columns 91 | columns = ['osversion', 'anyconnectversion', 'connectionevent', 'internalip'] 92 | for col in columns: 93 | stats = df_events[col].value_counts() 94 | print(f"\n{col.capitalize()} Statistics:\n{stats}\n") 95 | 96 | # Save DataFrame to CSV file with creation date in the file name 97 | save_dataframe_to_csv(df_events) 98 | else: 99 | print(f"Error: {response.status_code}") 100 | 101 | 102 | if __name__ == '__main__': 103 | main() -------------------------------------------------------------------------------- /setSwitchPortOnMacOui/cmdlist.txt: -------------------------------------------------------------------------------- 1 | vlan: 30 2 | tags: phone-port -------------------------------------------------------------------------------- /setSwitchPortOnMacOui/ouilist.txt: -------------------------------------------------------------------------------- 1 | d8:24:bd 2 | 1c:6a:7a -------------------------------------------------------------------------------- /setlocation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import meraki 4 | import sys 5 | import json 6 | import argparse 7 | 8 | 9 | dashboard = meraki.DashboardAPI(suppress_logging=True) 10 | 11 | 12 | def parse_options(): 13 | parser = argparse.ArgumentParser( 14 | prog='Meraki Location Updater', 15 | formatter_class=argparse.RawDescriptionHelpFormatter, 16 | description=''' 17 | A Python 3 script to set the location of all devices in a Network 18 | 19 | Usage: 20 | setlocation.py -n "Network Name" -l "Address of the site" 21 | **Please note the quotes around the Network Name and Address 22 | 23 | API Key 24 | requires you to have your API key in env vars as 25 | 'MERAKI_DASHBOARD_API_KEY' 26 | ''') 27 | parser.add_argument('-n', '--name', 28 | metavar='name', 29 | help="Network to be updated", 30 | required=True) 31 | parser.add_argument('-l', '--location', 32 | metavar='location', 33 | help="Address of the Site", 34 | required=True) 35 | parser.add_argument('-v', '--version', 36 | action='version', 37 | version='%(prog)s 2.0') 38 | if len(sys.argv) == 1: 39 | parser.print_help(sys.stderr) 40 | sys.exit(1) 41 | args = parser.parse_args() 42 | net_name = str(args.name) 43 | new_location = str(args.location) 44 | return net_name, new_location 45 | 46 | 47 | def get_orgs(): 48 | orgs = dashboard.organizations.getOrganizations() 49 | org_list = {} 50 | for dic in orgs: 51 | org_list[dic['id']] = dic['name'] 52 | return org_list 53 | 54 | 55 | def find_org(org_list): 56 | if len(org_list) == 1: 57 | org_id = org_list[0]['id'] 58 | else: 59 | org_id = input( 60 | f'Please type the number of the Organization that has the network ' 61 | f'you would like to update{json.dumps(org_list, indent = 4)}' "\n") 62 | return org_id 63 | 64 | 65 | def get_networks(org_id): 66 | net_list = dashboard.organizations.getOrganizationNetworks( 67 | org_id, total_pages='all') 68 | return net_list 69 | 70 | 71 | def find_networks(net_list, net_name): 72 | found_network = False 73 | for dic in net_list: 74 | if dic['name'] == net_name: 75 | found_network = True 76 | net_id = dic['id'] 77 | if not found_network: 78 | print(f'We did not find {net_name} in your organization(s)') 79 | sys.exit() 80 | return net_id 81 | 82 | 83 | def get_serials(net_id): 84 | serials_list = [] 85 | devices = dashboard.networks.getNetworkDevices(net_id) 86 | for dic in devices: 87 | serials_list.append(dic['serial']) 88 | return serials_list 89 | 90 | 91 | def update_address(serials_list, new_location, net_name): 92 | for serial in serials_list: 93 | dashboard.devices.updateDevice( 94 | serial, 95 | address=new_location, 96 | moveMapMarker='true') 97 | print(f'All devices in {net_name} have been updated to {new_location}') 98 | 99 | 100 | def main(): 101 | net_name, new_location = parse_options() 102 | org_list = get_orgs() 103 | org_id = find_org(org_list) 104 | net_list = get_networks(org_id) 105 | net_id = find_networks(net_list, net_name) 106 | serials_list = get_serials(net_id) 107 | update_address(serials_list, new_location, net_name) 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /setssidvlanid.py: -------------------------------------------------------------------------------- 1 | # This is a script to set the VLAN on an SSID. Usage: 2 | # python setssidvlanid.py -k -o -n -v [-t ] 3 | # 4 | # Mandatory arguments: 5 | # -k Your Meraki Dashboard API key 6 | # -o Name of the organization you want to be processed 7 | # -n Name of the SSID to be processed 8 | # -v New VLAN ID (number) for the SSID 9 | # Optional argument: 10 | # -t Only process networks tagged 11 | # 12 | # Example: 13 | # python setssidvlanid.py -k 1234 -o "Meraki Inc" -n "Meraki corp" -v 10 14 | # 15 | # The script requires the Requests module. To install it via pip: 16 | # pip install requests 17 | # 18 | # To pass parameters containing spaces in Windows, use double quotes "". 19 | # 20 | # This file was last modified on 2018-03-23 21 | 22 | import sys, getopt, requests, json, time 23 | from datetime import datetime 24 | 25 | #Used for time.sleep(API_EXEC_DELAY). Delay added to avoid hitting dashboard API max request rate 26 | API_EXEC_DELAY = 0.21 27 | 28 | #connect and read timeouts for the Requests module 29 | REQUESTS_CONNECT_TIMEOUT = 30 30 | REQUESTS_READ_TIMEOUT = 30 31 | 32 | #used by merakirequestthrottler(). DO NOT MODIFY 33 | LAST_MERAKI_REQUEST = datetime.now() 34 | 35 | def merakirequestthrottler(): 36 | #makes sure there is enough time between API requests to Dashboard not to hit shaper 37 | global LAST_MERAKI_REQUEST 38 | 39 | if (datetime.now()-LAST_MERAKI_REQUEST).total_seconds() < API_EXEC_DELAY: 40 | time.sleep(API_EXEC_DELAY) 41 | 42 | LAST_MERAKI_REQUEST = datetime.now() 43 | return 44 | 45 | 46 | def printusertext(p_message): 47 | #prints a line of text that is meant for the user to read 48 | #do not process these lines when chaining scripts 49 | print('@ %s' % p_message) 50 | 51 | 52 | def printhelp(): 53 | #prints help text 54 | 55 | printusertext('This is a script to set the VLAN on an SSID. Usage:') 56 | printusertext(' python setssidvlanid.py -k -o -n -v [-t ]') 57 | printusertext('') 58 | printusertext('Example:') 59 | printusertext(' python setssidvlanid.py -k 1234 -o "Meraki Inc" -n "Meraki corp" -v 10') 60 | printusertext('') 61 | printusertext('To pass parameters containing spaces in Windows, use double quotes "".') 62 | 63 | 64 | def getorgid(p_apikey, p_orgname): 65 | #looks up org id for a specific org name 66 | #on failure returns 'null' 67 | 68 | merakirequestthrottler() 69 | try: 70 | r = requests.get('https://api.meraki.com/api/v0/organizations', headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)) 71 | except: 72 | printusertext('ERROR 01: Unable to contact Meraki cloud') 73 | sys.exit(2) 74 | 75 | if r.status_code != requests.codes.ok: 76 | return 'null' 77 | 78 | rjson = r.json() 79 | 80 | for record in rjson: 81 | if record['name'] == p_orgname: 82 | return record['id'] 83 | return('null') 84 | 85 | 86 | def getnwlist(p_apikey, p_shardhost, p_orgid): 87 | #returns a list of all networks in an organization 88 | #on failure returns a single record with 'null' name and id 89 | 90 | merakirequestthrottler() 91 | try: 92 | r = requests.get('https://%s/api/v0/organizations/%s/networks' % (p_shardhost, p_orgid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 93 | except: 94 | printusertext('ERROR 02: Unable to contact Meraki cloud') 95 | sys.exit(2) 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 | 105 | def getssids(p_apikey, p_shardhost, p_netid): 106 | #returns a list of all MR SSIDs in a network 107 | 108 | merakirequestthrottler() 109 | try: 110 | r = requests.get('https://%s/api/v0/networks/%s/ssids' % (p_shardhost, p_netid), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 111 | except: 112 | printusertext('ERROR 03: Unable to contact Meraki cloud') 113 | sys.exit(2) 114 | 115 | returnvalue = [] 116 | if r.status_code != requests.codes.ok: 117 | returnvalue.append({'number': 'null'}) 118 | return(returnvalue) 119 | 120 | return(r.json()) 121 | 122 | 123 | def setssidattribute(p_apikey, p_shardhost, p_netid, p_ssidnum, p_attribute, p_value): 124 | #writes one attribute to one SSID 125 | 126 | merakirequestthrottler() 127 | 128 | try: 129 | r = requests.put('https://%s/api/v0/networks/%s/ssids/%s' % (p_shardhost, p_netid, p_ssidnum), data=json.dumps({p_attribute: p_value}), headers={'X-Cisco-Meraki-API-Key': p_apikey, 'Content-Type': 'application/json'}, timeout=(REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT) ) 130 | except: 131 | printusertext('ERROR 04: Unable to contact Meraki cloud') 132 | sys.exit(2) 133 | 134 | if r.status_code != requests.codes.ok: 135 | return ('null') 136 | 137 | return('ok') 138 | 139 | 140 | def main(argv): 141 | printusertext('INFO: Script started at %s' % datetime.now()) 142 | 143 | # python setssidvlanid.py -k -o -n -v [-t ] 144 | 145 | #initialize variables for command line arguments 146 | arg_apikey = '' 147 | arg_orgname = '' 148 | arg_ssidname = '' 149 | arg_vlanid = '' 150 | arg_nettag = '' 151 | 152 | #get command line arguments 153 | try: 154 | opts, args = getopt.getopt(argv, 'hk:o:n:v:t:') 155 | except getopt.GetoptError: 156 | printhelp() 157 | sys.exit(2) 158 | 159 | for opt, arg in opts: 160 | if opt == '-h': 161 | printhelp() 162 | sys.exit() 163 | elif opt == '-k': 164 | arg_apikey = arg 165 | elif opt == '-o': 166 | arg_orgname = arg 167 | elif opt == '-n': 168 | arg_ssidname = arg 169 | elif opt == '-v': 170 | arg_vlanid = arg 171 | elif opt == '-t': 172 | arg_nettag = arg 173 | 174 | #check if all required parameters have been given 175 | if arg_apikey == '' or arg_orgname == '' or arg_ssidname == '' or arg_vlanid == '': 176 | printhelp() 177 | sys.exit(2) 178 | 179 | #set operational mode flags (True/False) 180 | flag_modematchtag = (arg_nettag != '') 181 | 182 | #resolve orgid 183 | orgid = getorgid(arg_apikey, arg_orgname) 184 | if orgid == 'null': 185 | printusertext('ERROR 05: Unable to find org named "%s"' % arg_orgname) 186 | sys.exit(2) 187 | 188 | #get networks' list for orgid 189 | netlist = getnwlist(arg_apikey, 'api.meraki.com', orgid) 190 | 191 | if netlist[0]['id'] == 'null': 192 | printusertext('ERROR 06: Error retrieving net list for org id "%s"' % orgid) 193 | sys.exit(2) 194 | 195 | for net in netlist: 196 | #check that network has required tag, if user has given one 197 | flag_tagmatchsuccess = True 198 | if flag_modematchtag: 199 | if net['tags'] is None: 200 | flag_tagmatchsuccess = False 201 | else: 202 | if net['tags'].find(arg_nettag) == -1: 203 | flag_tagmatchsuccess = False 204 | 205 | if flag_tagmatchsuccess: 206 | #get SSID list 207 | ssidlist = getssids(arg_apikey, 'api.meraki.com', net['id']) 208 | 209 | if ssidlist[0]['number'] == 'null': 210 | printusertext('WARNING: Skipping network "%s": No MR config' % net['name']) 211 | else: 212 | flag_ssidnotfound = True 213 | #get SSID number corresponding to SSID name 214 | for ssid in ssidlist: 215 | if ssid['name'] == arg_ssidname: 216 | printusertext('INFO: Setting VLAN ID "%s" for net "%s", SSID %s "%s"' % (arg_vlanid, net['name'], ssid['number'], ssid['name'])) 217 | 218 | #set VLAN for correct SSID number 219 | if ssid['ipAssignmentMode'] in ['Layer 3 roaming with a concentrator', 'VPN']: 220 | flag_ssidnotfound = False 221 | setssidattribute(arg_apikey, 'api.meraki.com', net['id'], ssid['number'], 'vlanId', arg_vlanid) 222 | 223 | break #SSID name is unique, so no need to check the rest 224 | 225 | if flag_ssidnotfound: 226 | printusertext('WARNING: Skipping network "%s": no SSID "%s" or wrong addressing mode' % (net['name'], arg_ssidname)) 227 | 228 | else: 229 | printusertext('WARNING: Skipping network "%s": No tag "%s"' % (net['name'], arg_nettag)) 230 | 231 | printusertext('INFO: Reached end of script at %s' % datetime.now()) 232 | 233 | if __name__ == '__main__': 234 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /uplink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | ''' 4 | DEPRECATED > use report_statuses.py script instead please! 5 | 6 | === PREREQUISITES === 7 | Run in Python 3 8 | 9 | Install requests library, via macOS terminal: 10 | pip3 install requests 11 | 12 | login.py has these two lines, with the API key from your Dashboard profile (upper-right email login > API access), and organization ID to call (https://dashboard.meraki.com/api/v0/organizations); separated into different file for security. 13 | api_key = '[API_KEY]' 14 | org_id = '[ORG_ID]' 15 | 16 | Usage: 17 | python3 uplink.py 18 | 19 | === DESCRIPTION === 20 | Iterates through all devices, and exports to two CSV files: one for appliance (MX, Z1, Z3, vMX100) networks to collect WAN uplink information, and the other for all other devices (MR, MS, MC, MV) with local uplink info. 21 | 22 | Possible statuses: 23 | Active: active and working WAN port 24 | Ready: standby but working WAN port, not the preferred WAN port 25 | Failed: was working at some point but not anymore 26 | Not connected: nothing was ever connected, no cable plugged in 27 | For load balancing, both WAN links would show active. 28 | 29 | For any questions, please contact Shiyue (Shay) Cheng, shiychen@cisco.com 30 | ''' 31 | 32 | import csv 33 | import datetime 34 | import json 35 | import requests 36 | import sys 37 | 38 | def get_network_name(network_id, networks): 39 | return [element for element in networks if network_id == element['id']][0]['name'] 40 | 41 | 42 | if __name__ == '__main__': 43 | # Import API key and org ID from login.py 44 | try: 45 | import login 46 | (API_KEY, ORG_ID) = (login.api_key, login.org_id) 47 | except ImportError: 48 | API_KEY = input('Enter your Dashboard API key: ') 49 | ORG_ID = input('Enter your organization ID: ') 50 | 51 | 52 | # Find all appliance networks (MX, Z1, Z3, vMX100) 53 | session = requests.session() 54 | headers = {'X-Cisco-Meraki-API-Key': API_KEY, 'Content-Type': 'application/json'} 55 | try: 56 | name = json.loads(session.get('https://api.meraki.com/api/v0/organizations/' + ORG_ID, headers=headers).text)['name'] 57 | except: 58 | sys.exit('Incorrect API key or org ID, as no valid data returned') 59 | networks = json.loads(session.get('https://api.meraki.com/api/v0/organizations/' + ORG_ID + '/networks', headers=headers).text) 60 | inventory = json.loads(session.get('https://api.meraki.com/api/v0/organizations/' + ORG_ID + '/inventory', headers=headers).text) 61 | appliances = [device for device in inventory if device['model'][:2] in ('MX', 'Z1', 'Z3', 'vM') and device['networkId'] is not None] 62 | devices = [device for device in inventory if device not in appliances and device['networkId'] is not None] 63 | 64 | 65 | # Output CSV of appliances' info 66 | today = datetime.date.today() 67 | csv_file1 = open(name + ' appliances -' + str(today) + '.csv', 'w', encoding='utf-8') 68 | fieldnames = ['Network', 'Device', 'Serial', 'MAC', 'Model', 'WAN1 Status', 'WAN1 IP', 'WAN1 Gateway', 'WAN1 Public IP', 'WAN1 DNS', 'WAN1 Static', 'WAN2 Status', 'WAN2 IP', 'WAN2 Gateway', 'WAN2 Public IP', 'WAN2 DNS', 'WAN2 Static', 'Cellular Status', 'Cellular IP', 'Cellular Provider', 'Cellular Public IP', 'Cellular Model', 'Cellular Connection', 'Performance'] 69 | writer = csv.DictWriter(csv_file1, fieldnames=fieldnames, restval='') 70 | writer.writeheader() 71 | 72 | # Iterate through appliances 73 | for appliance in appliances: 74 | network_name = get_network_name(appliance['networkId'], networks) 75 | print('Looking into network ' + network_name) 76 | device_name = json.loads(session.get('https://api.meraki.com/api/v0/networks/' + appliance['networkId'] + '/devices/' + appliance['serial'], headers=headers).text)['name'] 77 | try: 78 | perfscore = json.loads(session.get('https://api.meraki.com/api/v0/networks/' + appliance['networkId'] + '/devices/' + appliance['serial'] + '/performance', headers=headers).text)['perfScore'] 79 | except: 80 | perfscore = None 81 | try: 82 | print('Found appliance ' + device_name) 83 | except: 84 | print('Found appliance ' + appliance['serial']) 85 | uplinks_info = dict.fromkeys(['WAN1', 'WAN2', 'Cellular']) 86 | uplinks_info['WAN1'] = dict.fromkeys(['interface', 'status', 'ip', 'gateway', 'publicIp', 'dns', 'usingStaticIp']) 87 | uplinks_info['WAN2'] = dict.fromkeys(['interface', 'status', 'ip', 'gateway', 'publicIp', 'dns', 'usingStaticIp']) 88 | uplinks_info['Cellular'] = dict.fromkeys(['interface', 'status', 'ip', 'provider', 'publicIp', 'model', 'connectionType']) 89 | uplinks = json.loads(session.get('https://api.meraki.com/api/v0/networks/' + appliance['networkId'] + '/devices/' + appliance['serial'] + '/uplink', headers=headers).text) 90 | for uplink in uplinks: 91 | if uplink['interface'] == 'WAN 1': 92 | for key in uplink.keys(): 93 | uplinks_info['WAN1'][key] = uplink[key] 94 | elif uplink['interface'] == 'WAN 2': 95 | for key in uplink.keys(): 96 | uplinks_info['WAN2'][key] = uplink[key] 97 | elif uplink['interface'] == 'Cellular': 98 | for key in uplink.keys(): 99 | uplinks_info['Cellular'][key] = uplink[key] 100 | if perfscore != None: 101 | writer.writerow({'Network': network_name, 'Device': device_name, 'Serial': appliance['serial'], 'MAC': appliance['mac'], 'Model': appliance['model'], 'WAN1 Status': uplinks_info['WAN1']['status'], 'WAN1 IP': uplinks_info['WAN1']['ip'], 'WAN1 Gateway': uplinks_info['WAN1']['gateway'], 'WAN1 Public IP': uplinks_info['WAN1']['publicIp'], 'WAN1 DNS': uplinks_info['WAN1']['dns'], 'WAN1 Static': uplinks_info['WAN1']['usingStaticIp'], 'WAN2 Status': uplinks_info['WAN2']['status'], 'WAN2 IP': uplinks_info['WAN2']['ip'], 'WAN2 Gateway': uplinks_info['WAN2']['gateway'], 'WAN2 Public IP': uplinks_info['WAN2']['publicIp'], 'WAN2 DNS': uplinks_info['WAN2']['dns'], 'WAN2 Static': uplinks_info['WAN2']['usingStaticIp'], 'Cellular Status': uplinks_info['Cellular']['status'], 'Cellular IP': uplinks_info['Cellular']['ip'], 'Cellular Provider': uplinks_info['Cellular']['provider'], 'Cellular Public IP': uplinks_info['Cellular']['publicIp'], 'Cellular Model': uplinks_info['Cellular']['model'], 'Cellular Connection': uplinks_info['Cellular']['connectionType'], 'Performance': perfscore}) 102 | else: 103 | writer.writerow({'Network': network_name, 'Device': device_name, 'Serial': appliance['serial'], 'MAC': appliance['mac'], 'Model': appliance['model'], 'WAN1 Status': uplinks_info['WAN1']['status'], 'WAN1 IP': uplinks_info['WAN1']['ip'], 'WAN1 Gateway': uplinks_info['WAN1']['gateway'], 'WAN1 Public IP': uplinks_info['WAN1']['publicIp'], 'WAN1 DNS': uplinks_info['WAN1']['dns'], 'WAN1 Static': uplinks_info['WAN1']['usingStaticIp'], 'WAN2 Status': uplinks_info['WAN2']['status'], 'WAN2 IP': uplinks_info['WAN2']['ip'], 'WAN2 Gateway': uplinks_info['WAN2']['gateway'], 'WAN2 Public IP': uplinks_info['WAN2']['publicIp'], 'WAN2 DNS': uplinks_info['WAN2']['dns'], 'WAN2 Static': uplinks_info['WAN2']['usingStaticIp'], 'Cellular Status': uplinks_info['Cellular']['status'], 'Cellular IP': uplinks_info['Cellular']['ip'], 'Cellular Provider': uplinks_info['Cellular']['provider'], 'Cellular Public IP': uplinks_info['Cellular']['publicIp'], 'Cellular Model': uplinks_info['Cellular']['model'], 'Cellular Connection': uplinks_info['Cellular']['connectionType']}) 104 | csv_file1.close() 105 | 106 | 107 | # Output CSV of all other devices' info 108 | csv_file2 = open(name + ' other devices -' + str(today) + '.csv', 'w', encoding='utf-8') 109 | fieldnames = ['Network', 'Device', 'Serial', 'MAC', 'Model', 'Status', 'IP', 'Gateway', 'Public IP', 'DNS', 'VLAN', 'Static'] 110 | writer = csv.DictWriter(csv_file2, fieldnames=fieldnames, restval='') 111 | writer.writeheader() 112 | 113 | # Iterate through all other devices 114 | for device in devices: 115 | network_name = get_network_name(device['networkId'], networks) 116 | print('Looking into network ' + network_name) 117 | device_name = json.loads(session.get('https://api.meraki.com/api/v0/networks/' + device['networkId'] + '/devices/' + device['serial'], headers=headers).text)['name'] 118 | try: 119 | print('Found device ' + device_name) 120 | except: 121 | print('Found device ' + device['serial']) 122 | uplink_info = dict.fromkeys(['interface', 'status', 'ip', 'gateway', 'publicIp', 'dns', 'vlan', 'usingStaticIp']) 123 | uplink = json.loads(session.get('https://api.meraki.com/api/v0/networks/' + device['networkId'] + '/devices/' + device['serial'] + '/uplink', headers=headers).text) 124 | 125 | # Blank uplink for devices that are down or meshed APs 126 | if uplink == []: 127 | continue 128 | # All other devices have single uplink 129 | else: 130 | uplink = uplink[0] 131 | for key in uplink.keys(): 132 | uplink_info[key] = uplink[key] 133 | writer.writerow({'Network': network_name, 'Device': device_name, 'Serial': device['serial'], 'MAC': device['mac'], 'Model': device['model'], 'Status': uplink_info['status'], 'IP': uplink_info['ip'], 'Gateway': uplink_info['gateway'], 'Public IP': uplink_info['publicIp'], 'DNS': uplink_info['dns'], 'VLAN': uplink_info['vlan'], 'Static': uplink_info['usingStaticIp']}) 134 | csv_file2.close() 135 | -------------------------------------------------------------------------------- /usagestats_initconfig.txt: -------------------------------------------------------------------------------- 1 | [GROUPS] 2 | groupname=First 3 | subnet=10.0.0.0/8 4 | 5 | groupname=Second 6 | subnet=192.168.0.0/16 7 | 8 | [OPTIONS] 9 | #filter=dtag:statsmachine -------------------------------------------------------------------------------- /usagestats_manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/automation-scripts/2f1ad9c81cfa8f947f9cc9ca126e818faaa3abb1/usagestats_manual.pdf --------------------------------------------------------------------------------