├── Dockerfile ├── README.md ├── docker-compose.yml ├── locationscanningv3format.json ├── meraki-sample-captive-portal ├── Dockerfile ├── env_user.py ├── meraki_sample_captive_portal.py ├── requirements.txt ├── static │ └── sample.css └── templates │ ├── click.html │ └── success.html ├── meraki-sample-location-scanning-receiver ├── Dockerfile ├── env_user.py ├── meraki_sample_location_scanning_receiver.py ├── requirements.txt ├── static │ ├── FiraGranVia.png │ ├── blue_circle.png │ ├── sample.css │ └── sample.js └── templates │ └── index.html ├── meraki-sample-webhook-receiver ├── Dockerfile ├── env_user.py ├── meraki_sample_webhook_receiver.py └── requirements.txt ├── meraki_cloud_simulator.py ├── merakicloudsimulator ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-36.pyc │ ├── alert_settings.cpython-36.pyc │ ├── excapsimulator.cpython-36.pyc │ ├── locationscanningsimulator.cpython-36.pyc │ ├── meraki_settings.cpython-36.pyc │ ├── sample_alert_messages.cpython-36.pyc │ └── webhooksimulator.cpython-36.pyc ├── alert_settings.py ├── excapsimulator.py ├── locationscanningsimulator.py ├── meraki_settings.py ├── sample_alert_messages.py ├── static │ ├── locationsimulator.js │ └── sample.css ├── templates │ ├── excap.html │ ├── excapconnected.html │ ├── index.html │ ├── locationscanning.html │ ├── locationscanningrunning.html │ └── webhook.html └── webhooksimulator.py ├── requirements.txt └── wt_vars.env /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | EXPOSE 5001 8 | 9 | ENTRYPOINT ["python", "meraki_cloud_simulator.py"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meraki Cloud Simulator 2 | Locally run Python 3.5+ Flask based application that provides HTTP POST simulations of Location Scanning, WebHook Alerts and Splash Page (Captive Portal) Integrations. 3 | 4 | The simulator can be run as a standalone python application or as Docker container in conjunction with all supporting samples (location scanning, webhook alerts, and captive portal). 5 | 6 | * [Run as Docker Container](#Run-as-Docker-Container) 7 | * [Run as Python Standalone](#Run-as-Python-Standalone) 8 | 9 | ## Run as Docker Container 10 | 11 | The webhook simulator and the captive portal simulator use Webex Teams to post updates. Before starting the application, add in your developer token and room ID (see: https://developer.webex.com) that you'd like to post notifications to. Those are added in the wt_vars.env file and will be pulled into the appropriate containters. You can then start all containers with: 12 | 13 | ``` 14 | docker-compose build 15 | docker-compose up 16 | ``` 17 | 18 | This will build and run four containers: 19 | 20 | * The simulator itself: web address is http://localhost:5001/go and will take you to a menu of available simulations 21 | * A sample location scanning receiver: web address is http://localhost:5002/go but in the container network it is identified as http://meraki-sample-location-scanning-receiver:5002. See [Sample Location Scanning Receiver](#Sample-Location-Scanning-Receiver) for instruction on use. 22 | * A sample webhook alert receiver: see [Sample Webhook Alert Receiver](#Sample-Webhook-Alert-Receiver) for instruction on use. 23 | * A sample captive portal: see [Sample Captive Portal](#Sample-Captive-Portal) for instruction on use. 24 | 25 | 26 | Head to http://localhost:5001/go to validate simulator is up and running and select the simulation you'd like to run (you can run all three at the same time). 27 | 28 | 29 | ## Run as Python Standalone 30 | This simulator can also be run as a standalone Python web service without using docker-compose. In this instance, the simulator and all samples will need to be started independently OR samples can be replaced with your custom applications. 31 | 32 | To run the simulator: 33 | 34 | * virtual envrionment: 35 | ``` 36 | python3 -m venv simulator 37 | source simulator/bin/activate 38 | ``` 39 | 40 | * Install dependencies 41 | ``` 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | * Go! 46 | ``` 47 | python meraki_cloud_simulator.py 48 | ``` 49 | 50 | Navigate to http://localhost:5001/go and select the simulator you'd like to use. 51 | 52 | 53 | ## Sample Location Scanning Receiver 54 | 55 | ### If running with docker-compose 56 | 57 | Navigate to http://localhost:5002/go to verify sample receiver is up and running. 58 | 59 | Navigate to http://localhost:5001/go and select Location Scanning Simulator. Enter number of clients and APs for the simulator provide location data, set the location of the desired map and the receiving services url to http://meraki-sample-location-scanning-receiver:5002 and hit Launch Simulator to run. Location data for that location, AP and client set will be generated sent to the receiver. To see updated locations navigate to http://localhost:5002/go and see blue dots for simulated device locations. 60 | 61 | ### If running as a Python standalone service with simulator sending data 62 | 63 | This sample can be run as a python standalone service as well. Use this if you're running the simulator as a standalone Python application. 64 | 65 | * virtual envrionment: 66 | ``` 67 | python3 -m venv location_receiver 68 | source location_receiver/bin/activate 69 | ``` 70 | 71 | * Install dependencies 72 | ``` 73 | pip install -r requirements.txt 74 | ``` 75 | 76 | * Go! 77 | ``` 78 | python meraki_sample_location_scanning_receiver.py -v simulator -s simulator 79 | ``` 80 | 81 | ### If running as a Python standalone service with your Meraki organization sending data 82 | 83 | This sample can be also used to collect data from the live Meraki platform rather than the simulator. If you are an administrator in a Meraki organization you can do the following. 84 | 85 | 1. Navigate to the [Meraki Dashboard](https://meraki.cisco.com). Login with your username and password 86 | 1. On the left rail, click on the **Networks** drop-down and make sure to choose the desired network. This changes the context to the network to which you'll configure the location scanning. 87 | 1. Under **Network**, select **Network-wide->General**. 88 | 1. Scroll down to **Location and Scanning** and verify the API is enabled. 89 | 1. Copy the validator key 90 | 91 | * virtual envrionment: 92 | ``` 93 | python3 -m venv location_receiver 94 | source location_receiver/bin/activate 95 | ``` 96 | 97 | * Install dependencies 98 | ``` 99 | pip install -r requirements.txt 100 | ``` 101 | 102 | * Go! 103 | ``` 104 | python meraki_sample_location_scanning_receiver.py -v -s 105 | ``` 106 | 107 | * Expose service using ngrok 108 | ``` 109 | ngrok http 5002 110 | ``` 111 | 112 | This will allow your service to be accessible to the public internet for testing. 113 | 114 | 1. Navigate to the [Meraki Dashboard](https://meraki.cisco.com). Login with your username and password 115 | 1. On the left rail, click on the **Networks** drop-down and make sure to choose the desired network. This changes the context to the network to which you'll configure the location scanning. 116 | 1. Under **Network**, select **Network-wide->General**. 117 | 1. Scroll down to **Location and Scanning** and verify the API is enabled. 118 | 1. Update publicly accessible url of the receiving application with the url generated from the **ngrok** tool in the **Post URL** and set the **Secret** to the same value you used to launch your receiver above. 119 | 1. Click **Validate** to verify connectivity. 120 | 1. If your server validates, then Meraki can send the `HTTPS POST` data to it. 121 | 1. Scroll to the bottom and click **Save** in the lower right corner. 122 | 123 | 124 | ## Sample Webhook Alert Receiver 125 | 126 | ### If running with docker-compose 127 | 128 | Navigate to http://localhost:5001/go and select Webhook Alerts Simulator. Select the desired alerts to collect and at the bottom fill out the 129 | 130 | ### If running as a Python standalone service with simulator sending data 131 | 132 | This sample can be run as a python standalone service as well. Use this if you're running the simulator as a standalone Python application. 133 | 134 | 135 | ### If running as a Python standalone service with your Meraki organization sending data 136 | 137 | 138 | ## Captive portal 139 | 140 | Enter the URL for the captive portal to be tested and the continuation URL. Upon hitting "Simulate WiFi Connection" the service will open a new tab to the captive portal as if your local client was connecting to a Meraki SSID and serving up a captive portal 141 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | meraki_cloud_simulator: 5 | build: . 6 | restart: on-failure 7 | container_name: meraki_cloud_simulator 8 | ports: 9 | - "5001:5001" 10 | volumes: 11 | - .:/meraki_cloud_simulator 12 | env_file: 13 | - wt_vars.env 14 | 15 | meraki-sample-location-scanning-receiver: 16 | build: ./meraki-sample-location-scanning-receiver 17 | restart: on-failure 18 | container_name: meraki-sample-location-scanning-receiver 19 | ports: 20 | - "5002:5002" 21 | volumes: 22 | - .:/meraki-sample-location-scanning-receiver 23 | env_file: 24 | - wt_vars.env 25 | depends_on: 26 | - meraki_cloud_simulator 27 | 28 | meraki-sample-webhook-receiver: 29 | build: ./meraki-sample-webhook-receiver 30 | restart: on-failure 31 | container_name: meraki-sample-webhook-receiver 32 | ports: 33 | - "5005:5005" 34 | volumes: 35 | - .:/meraki-sample-webhook-receiver 36 | env_file: 37 | - wt_vars.env 38 | depends_on: 39 | - meraki_cloud_simulator 40 | 41 | meraki-sample-captive-portal: 42 | build: ./meraki-sample-captive-portal 43 | restart: on-failure 44 | container_name: meraki-captive-portal 45 | ports: 46 | - "5004:5004" 47 | volumes: 48 | - .:/meraki-sample-captive-portal 49 | env_file: 50 | - wt_vars.env 51 | depends_on: 52 | - meraki_cloud_simulator -------------------------------------------------------------------------------- /meraki-sample-captive-portal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | EXPOSE 5004 8 | 9 | CMD ["python", "meraki_sample_captive_portal.py", "-n", "Simulated Network", "-s", "Simulated SSID", "-p", "simulatedpassword", "-d", "yes"] -------------------------------------------------------------------------------- /meraki-sample-captive-portal/env_user.py: -------------------------------------------------------------------------------- 1 | """Set your Environment Information once, not many times. 2 | 3 | The provided sample code in this repository will reference this file to get the 4 | needed information about you and your context to complete the labs. You 5 | provide this info here once and the scripts in this repository will access it 6 | as needed by the lab. 7 | 8 | TODO: To setup your `env_user.py` copy this file then edit and save your info 9 | 10 | $ cp env_user.template env_user.py 11 | 12 | 13 | Copyright (c) 2018 Cisco and/or its affiliates. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | import os 34 | 35 | # User Input 36 | 37 | WT_ACCESS_TOKEN = os.environ['WT_ACCESS_TOKEN'] 38 | WT_ROOM_ID = os.environ['WT_ROOM_ID'] 39 | MERAKI_API_KEY = "6bec40cf957de430a6f1f2baa056b99a4fac9ea0" 40 | 41 | # End User Input 42 | -------------------------------------------------------------------------------- /meraki-sample-captive-portal/meraki_sample_captive_portal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """External Captive Portal Web Server.""" 3 | 4 | """The provided sample code in this repository will reference this file to get the 5 | information needed to connect to your lab backend. You provide this info here 6 | once and the scripts in this repository will access it as needed by the lab. 7 | Copyright (c) 2019 Cisco and/or its affiliates. 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import getopt 26 | import json 27 | import os 28 | import sys 29 | from pprint import pprint 30 | 31 | import requests 32 | from flask import Flask, json, redirect, render_template, request 33 | from webexteamssdk import WebexTeamsAPI 34 | 35 | import env_user # noqa 36 | print("WT_ACCESS_TOKEN" + env_user.WT_ACCESS_TOKEN) 37 | print("WT_ROOM_ID" + env_user.WT_ROOM_ID) 38 | 39 | # Module Variables 40 | # MERAKI BASE URL 41 | # if running in docker-compose base_url = "http://meraki_cloud_simulator:5001" 42 | base_url = "" 43 | 44 | captive_portal_base_url = "http://localhost:5004" 45 | base_grant_url = "" 46 | user_continue_url = "" 47 | success_url = "" 48 | network_id = "" 49 | 50 | teams_api = WebexTeamsAPI(access_token=env_user.WT_ACCESS_TOKEN) 51 | 52 | app = Flask(__name__) 53 | 54 | 55 | # Meraki Dashboard API helper functions 56 | # Get Network ID based on Network name entry 57 | def get_network_id(network_name): 58 | """Get the network ID for a Meraki Network; searching by network name.""" 59 | global network_id 60 | global base_url 61 | 62 | # Retrieve the list of organizations accessible to your API token 63 | # MISSION TODO 64 | response = requests.get( 65 | base_url + "/organizations", 66 | headers={"X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY} 67 | ) 68 | # END MISSION SECTION 69 | response.raise_for_status() 70 | 71 | # Parse and print the JSON response 72 | orgs = response.json() 73 | pprint(orgs) 74 | 75 | # Search the organizations network lists for the network name we want 76 | for org in orgs: 77 | # MISSION TODO 78 | response = requests.get( 79 | # TODO: Add the API endpoint path to get the list of networks 80 | # (don't forget to add the organization ID) 81 | base_url + "/organizations/"+org["id"]+"/networks", 82 | headers={ 83 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 84 | "Content-Type": "application/json" 85 | }, 86 | ) 87 | # END MISSION SECTION 88 | response.raise_for_status() 89 | 90 | # Parse and print the JSON response 91 | networks = response.json() 92 | pprint(networks) 93 | 94 | for network in networks: 95 | if network["name"] == network_name: 96 | network_id = network["id"] 97 | return network_id 98 | 99 | 100 | def set_splash_page_settings(network_id, captive_portal_base_url): 101 | global base_url 102 | """Configure the splash page settings for a network.""" 103 | # MISSION TODO 104 | response = requests.put( 105 | # TODO: Add the API endpoint path to set the splash page settings 106 | # (don't forget to add the network ID) 107 | base_url + "/networks/"+network_id+"/ssids/0/splashSettings", 108 | headers={ 109 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 110 | "Content-Type": "application/json", 111 | }, 112 | json={ 113 | "splashPage": "Click-through splash page", 114 | "splashUrl": captive_portal_base_url + '/click', 115 | "useCustomUrl": True 116 | }, 117 | ) 118 | # END MISSION SECTION 119 | response.raise_for_status() 120 | 121 | 122 | def set_ssid_settings(network_id, wireless_name, wireless_password): 123 | global base_url 124 | """Configure an SSID to use the External Captive Portal.""" 125 | # MISSION TODO 126 | response = requests.put( 127 | # TODO: Add the API endpoint path to set SSID settings 128 | # (don't forget to add the network ID) 129 | base_url + "/networks/"+network_id+"/ssids/0", 130 | headers={ 131 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 132 | "Content-Type": "application/json" 133 | }, 134 | json={ 135 | "number": 0, 136 | "name": wireless_name, 137 | "enabled": True, 138 | "splashPage": "Click-through splash page", 139 | "ssidAdminAccessible": False, 140 | "authMode": "psk", 141 | "psk": wireless_password, 142 | "encryptionMode": "wpa", 143 | "wpaEncryptionMode": "WPA2 only", 144 | "ipAssignmentMode": "Bridge mode", 145 | "useVlanTagging": False, 146 | "walledGardenEnabled": True, 147 | "walledGardenRanges": "*.ngrok.io", 148 | "minBitrate": 11, 149 | "bandSelection": "5 GHz band only", 150 | "perClientBandwidthLimitUp": 0, 151 | "perClientBandwidthLimitDown": 0 152 | }, 153 | ) 154 | # END MISSION SECTION 155 | response.raise_for_status() 156 | 157 | 158 | # Flask micro-webservice URI endpoints 159 | @app.route("/click", methods=["GET"]) 160 | def get_click(): 161 | global base_url 162 | """Process GET requests to the /click URI; render the click.html page.""" 163 | global base_grant_url 164 | global user_continue_url 165 | global success_url 166 | 167 | host = request.host_url 168 | base_grant_url = request.args.get('base_grant_url') 169 | user_continue_url = request.args.get('user_continue_url') 170 | node_mac = request.args.get('node_mac') 171 | client_ip = request.args.get('client_ip') 172 | client_mac = request.args.get('client_mac') 173 | success_url = host + "success" 174 | 175 | return render_template( 176 | "click.html", 177 | client_ip=client_ip, 178 | client_mac=client_mac, 179 | node_mac=node_mac, 180 | user_continue_url=user_continue_url, 181 | success_url=success_url, 182 | ) 183 | 184 | 185 | @app.route("/login", methods=["POST"]) 186 | def get_login(): 187 | global base_url 188 | """Process POST requests to the /login URI; redirect to grant URL.""" 189 | redirect_url = base_grant_url+"?continue_url="+success_url 190 | return redirect(redirect_url, code=302) 191 | 192 | 193 | @app.route("/success", methods=["GET"]) 194 | def get_success(): 195 | global base_url 196 | """Process GET requests to the /success URI; render success.html.""" 197 | # MISSION TODO 198 | response = requests.get( 199 | # TODO: Add the API endpoint path to get splash page login attempts 200 | base_url + "/networks/"+network_id+"/splashLoginAttempts", 201 | headers={ 202 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 203 | "Content-Type": "application/json" 204 | }, 205 | ) 206 | # END MISSION SECTION 207 | response.raise_for_status() 208 | 209 | # Parse JSON Data 210 | splash_logins = response.text 211 | splash_logins = json.loads(splash_logins) 212 | splash_logins = "```json " + json.dumps(splash_logins, indent=2) 213 | 214 | # Send Message to WebEx Teams 215 | teams_api.messages.create( 216 | env_user.WT_ROOM_ID, 217 | markdown="Splash Login Attempts:\n" + splash_logins 218 | ) 219 | 220 | return render_template( 221 | "success.html", 222 | user_continue_url=user_continue_url, 223 | ) 224 | 225 | 226 | def parse_cli_args(argv): 227 | """Parse command line arguments.""" 228 | network_name = None 229 | ssid_name = None 230 | ssid_password = None 231 | in_docker = None 232 | 233 | try: 234 | opts, args = getopt.getopt( 235 | argv, 236 | "hn:s:p:d:", 237 | ["network=", "ssid=", "password=","in_docker="], 238 | ) 239 | except getopt.GetoptError: 240 | print(f"Usage: {__file__} -n network -s ssid -p password -d yes/no") 241 | sys.exit(2) 242 | for opt, arg in opts: 243 | if opt == "-h": 244 | print(f"Usage: {__file__} -n network -s ssid -p password -d yes/no") 245 | sys.exit() 246 | elif opt in ("-n", "--network"): 247 | network_name = arg 248 | elif opt in ("-s", "--ssid"): 249 | ssid_name = arg 250 | elif opt in ("-p", "--password"): 251 | ssid_password = arg 252 | elif opt in ("-d", "--in_docker"): 253 | in_docker = arg 254 | 255 | if network_name and ssid_name and ssid_password and in_docker: 256 | print("network: " + network_name) 257 | print("ssid: " + ssid_name) 258 | print("password: " + ssid_password) 259 | print("in_docker: " + in_docker) 260 | return [network_name, ssid_name, ssid_password, in_docker] 261 | else: 262 | print(f"Usage: {__file__} -n network -s ssid -p password -d yes/no") 263 | sys.exit(2) 264 | 265 | 266 | # If this script is the main being executed, configure the captive portal and 267 | # start the web server. 268 | if __name__ == "__main__": 269 | # Uncomment the following block if you are using the Meraki Cloud APIs and 270 | # and ngrok tunnels. 271 | 272 | # meraki_dashboard_api_base_url = "https://api.meraki.com/api/v0" 273 | # response = requests.get("http://127.0.0.1:4040/api/tunnels", verify=False) 274 | # response.raise_for_status() 275 | # tunnels = response.json()["tunnels"] 276 | # for tunnel in tunnels: 277 | # if tunnel['proto'] == 'https': 278 | # captive_portal_base_url = tunnel['public_url'] 279 | 280 | # Parse settings from CLI arguments and configure the captive portal using 281 | # the Meraki Dashboard APIs. 282 | args = parse_cli_args(sys.argv[1:]) 283 | in_docker = args[3] 284 | 285 | if in_docker == "yes": 286 | base_url = "http://meraki_cloud_simulator:5001" 287 | else: 288 | base_url = "http://localhost:5001" 289 | network_id = get_network_id(args[0]) 290 | ssid = args[1] 291 | password = args[2] 292 | 293 | set_ssid_settings(network_id, ssid, password) 294 | set_splash_page_settings(network_id, captive_portal_base_url) 295 | 296 | # Start the External Captive Portal web server 297 | app.run(host="0.0.0.0", port=5004, debug=False) 298 | -------------------------------------------------------------------------------- /meraki-sample-captive-portal/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | Flask 3 | webexteamssdk 4 | -------------------------------------------------------------------------------- /meraki-sample-captive-portal/static/sample.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | font-family: "proxima-nova-1","proxima-nova-2", "Helvetica Neue", Helvetica, verdana, sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | } 10 | 11 | 12 | #map { 13 | height: 100%; 14 | } 15 | /* Optional: Makes the sample page fill the window. */ 16 | #description { 17 | font-family: Roboto; 18 | font-size: 15px; 19 | font-weight: 300; 20 | } 21 | 22 | #infowindow-content .title { 23 | font-weight: bold; 24 | } 25 | 26 | #infowindow-content { 27 | display: none; 28 | } 29 | 30 | #map #infowindow-content { 31 | display: inline; 32 | } 33 | 34 | .pac-card { 35 | margin: 10px 10px 0 0; 36 | border-radius: 2px 0 0 2px; 37 | box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | outline: none; 40 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); 41 | background-color: #fff; 42 | font-family: Roboto; 43 | } 44 | 45 | #pac-container { 46 | padding-bottom: 12px; 47 | margin-right: 12px; 48 | } 49 | 50 | .pac-controls { 51 | display: inline-block; 52 | padding: 5px 11px; 53 | } 54 | 55 | .pac-controls label { 56 | font-family: Roboto; 57 | font-size: 13px; 58 | font-weight: 300; 59 | } 60 | 61 | #pac-input { 62 | background-color: #fff; 63 | font-family: Roboto; 64 | font-size: 15px; 65 | font-weight: 300; 66 | margin-left: 12px; 67 | padding: 0 11px 0 13px; 68 | text-overflow: ellipsis; 69 | width: 400px; 70 | } 71 | 72 | #pac-input:focus { 73 | border-color: #4d90fe; 74 | } 75 | 76 | #title { 77 | color: #fff; 78 | background-color: #4d90fe; 79 | font-size: 25px; 80 | font-weight: 500; 81 | padding: 6px 12px; 82 | } 83 | #target { 84 | width: 345px; 85 | } 86 | 87 | #masthead { 88 | height: 125px; 89 | width: 100%; 90 | position: relative; 91 | background: #FFFFFF; 92 | border-top: 4px solid #78be20; 93 | box-shadow: 0 2px 7px rgba(0,0,0,0.2); 94 | } 95 | 96 | #masthead-content { 97 | margin: 0 auto; 98 | position: relative; 99 | width: 80%; 100 | height: 100%; 101 | } 102 | 103 | #masthead-content img { 104 | float: left; 105 | margin: 32px; 106 | width: 165px; 107 | margin-left: 0; 108 | } 109 | 110 | #content { 111 | width: 80%; 112 | margin: 60px auto; 113 | padding: 40px; 114 | box-sizing: border-box; 115 | border-radius: 9px; 116 | background: #FAFAFA; 117 | } 118 | 119 | h1 { 120 | color: #78be20; 121 | font-weight: 100; 122 | font-size: 38px; 123 | margin-top: 0; 124 | letter-spacing: -1px; 125 | } 126 | 127 | .small { 128 | color: #6B6B6B; 129 | font-weight: 400; 130 | margin-bottom: 30px; 131 | font-size: 14px; 132 | } 133 | 134 | .bold { 135 | font-weight: 600; 136 | } 137 | 138 | button, input { 139 | width: 11%; 140 | } 141 | 142 | button { 143 | height: 35px; 144 | border: none; 145 | background: #737373; 146 | border-radius: 2px; 147 | box-sizing: border-box; 148 | color: white; 149 | font-family: "proxima-nova-1","proxima-nova-2", "Helvetica Neue", Helvetica, verdana, sans-serif; 150 | font-weight: 200; 151 | font-size: 14px; 152 | padding: 0; 153 | min-width: 70px; 154 | } 155 | 156 | button:hover{ 157 | background: #616060; 158 | } 159 | -------------------------------------------------------------------------------- /meraki-sample-captive-portal/templates/click.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Captive Portal Sample App 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |

Meraki Captive Portal Demo App

20 |
21 | Enter Your Email Address:

22 | 23 |
24 | 25 | 30 |
31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /meraki-sample-captive-portal/templates/success.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Captive Portal Sample App - Success 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |

Login Success

20 |

You've successfully logged into the WiFi, continue to here to access the internet

21 | 22 |
23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | EXPOSE 5002 8 | 9 | CMD ["python", "meraki_sample_location_scanning_receiver.py", "-v", "simulator", "-s", "simulator"] -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/env_user.py: -------------------------------------------------------------------------------- 1 | """Set your Environment Information once, not many times. 2 | 3 | The provided sample code in this repository will reference this file to get the 4 | needed information about you and your context to complete the labs. You 5 | provide this info here once and the scripts in this repository will access it 6 | as needed by the lab. 7 | 8 | TODO: To setup your `env_user.py` copy this file then edit and save your info 9 | 10 | $ cp env_user.template env_user.py 11 | 12 | 13 | Copyright (c) 2018 Cisco and/or its affiliates. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | import os 34 | 35 | # User Input 36 | 37 | WT_ACCESS_TOKEN = os.environ['WT_ACCESS_TOKEN'] 38 | WT_ROOM_ID = os.environ['WT_ROOM_ID'] 39 | MERAKI_API_KEY = "6bec40cf957de430a6f1f2baa056b99a4fac9ea0" 40 | 41 | # End User Input 42 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/meraki_sample_location_scanning_receiver.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | 3 | """ 4 | Cisco Meraki Location Scanning Receiver 5 | 6 | The provided sample code in this repository will reference this file to get the 7 | information needed to connect to your lab backend. You provide this info here 8 | once and the scripts in this repository will access it as needed by the lab. 9 | Copyright (c) 2019 Cisco and/or its affiliates. 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | # Libraries 28 | from pprint import pprint 29 | from flask import Flask 30 | from flask import json 31 | from flask import request 32 | from flask import render_template 33 | import sys, getopt 34 | import json 35 | 36 | ############## USER DEFINED SETTINGS ############### 37 | # MERAKI SETTINGS 38 | validator = "EnterYourValidator" 39 | secret = "EnterYourSecret" 40 | version = "2.0" 41 | locationdata = "Location Data Holder" 42 | #################################################### 43 | app = Flask(__name__) 44 | 45 | # Respond to Meraki with validator 46 | 47 | 48 | @app.route("/", methods=["GET"]) 49 | def get_validator(): 50 | print("validator sent to: ", request.environ["REMOTE_ADDR"]) 51 | return validator 52 | 53 | 54 | # Accept CMX JSON POST 55 | 56 | 57 | @app.route("/", methods=["POST"]) 58 | def get_locationJSON(): 59 | global locationdata 60 | 61 | if not request.json or not "data" in request.json: 62 | return ("invalid data", 400) 63 | 64 | locationdata = request.json 65 | pprint(locationdata, indent=1) 66 | print("Received POST from ", request.environ["REMOTE_ADDR"]) 67 | 68 | # Verify secret 69 | if locationdata["secret"] != secret: 70 | print("secret invalid:", locationdata["secret"]) 71 | return ("invalid secret", 403) 72 | 73 | else: 74 | print("secret verified: ", locationdata["secret"]) 75 | 76 | # Verify version 77 | if locationdata["version"] != version: 78 | print("invalid version") 79 | return ("invalid version", 400) 80 | 81 | else: 82 | print("version verified: ", locationdata["version"]) 83 | 84 | # Determine device type 85 | if locationdata["type"] == "DevicesSeen": 86 | print("WiFi Devices Seen") 87 | elif locationdata["type"] == "BluetoothDevicesSeen": 88 | print("Bluetooth Devices Seen") 89 | else: 90 | print("Unknown Device 'type'") 91 | return ("invalid device type", 403) 92 | 93 | # Return success message 94 | return "Location Scanning POST Received" 95 | 96 | 97 | @app.route("/go", methods=["GET"]) 98 | def get_go(): 99 | return render_template("index.html", **locals()) 100 | 101 | 102 | @app.route("/clients/", methods=["GET"]) 103 | def get_clients(): 104 | global locationdata 105 | if locationdata != "Location Data Holder": 106 | # pprint(locationdata["data"]["observations"], indent=1) 107 | return json.dumps(locationdata["data"]["observations"]) 108 | 109 | return "" 110 | 111 | 112 | @app.route("/clients/", methods=["GET"]) 113 | def get_individualclients(clientMac): 114 | global locationdata 115 | for client in locationdata["data"]["observations"]: 116 | if client["clientMac"] == clientMac: 117 | return json.dumps(client) 118 | 119 | return "" 120 | 121 | 122 | # Launch application with supplied arguments 123 | 124 | 125 | def main(argv): 126 | global validator 127 | global secret 128 | 129 | try: 130 | opts, args = getopt.getopt(argv, "hv:s:", ["validator=", "secret="]) 131 | except getopt.GetoptError: 132 | print("locationscanningreceiver.py -v -s ") 133 | sys.exit(2) 134 | for opt, arg in opts: 135 | if opt == "-h": 136 | print("locationscanningreceiver.py -v -s ") 137 | sys.exit() 138 | elif opt in ("-v", "--validator"): 139 | validator = arg 140 | elif opt in ("-s", "--secret"): 141 | secret = arg 142 | 143 | print("validator: " + validator) 144 | print("secret: " + secret) 145 | 146 | 147 | if __name__ == "__main__": 148 | main(sys.argv[1:]) 149 | app.run(host="0.0.0.0", port=5002, debug=False) 150 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.1 2 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/static/FiraGranVia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/meraki-sample-location-scanning-receiver/static/FiraGranVia.png -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/static/blue_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/meraki-sample-location-scanning-receiver/static/blue_circle.png -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/static/sample.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | font-family: "proxima-nova-1","proxima-nova-2", "Helvetica Neue", Helvetica, verdana, sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | } 10 | 11 | #masthead { 12 | height: 125px; 13 | width: 100%; 14 | position: relative; 15 | background: #FFFFFF; 16 | border-top: 4px solid #78be20; 17 | box-shadow: 0 2px 7px rgba(0,0,0,0.2); 18 | } 19 | 20 | #masthead-content { 21 | margin: 0 auto; 22 | position: relative; 23 | width: 80%; 24 | height: 100%; 25 | } 26 | 27 | #masthead-content img { 28 | float: left; 29 | margin: 32px; 30 | width: 165px; 31 | margin-left: 0; 32 | } 33 | 34 | #content { 35 | width: 80%; 36 | margin: 60px auto; 37 | padding: 40px; 38 | box-sizing: border-box; 39 | border-radius: 9px; 40 | background: #FAFAFA; 41 | } 42 | 43 | #mac-address { 44 | margin-bottom: 10px; 45 | } 46 | 47 | #mac-field { 48 | width: 30%; 49 | height: 35px; 50 | margin-bottom: 20px; 51 | padding-left: 13px; 52 | border: 1px solid #E6E6E6; 53 | border-radius: 2px; 54 | box-sizing: border-box; 55 | font-family: "proxima-nova-1","proxima-nova-2", "Helvetica Neue", Helvetica, verdana, sans-serif; 56 | font-size: 16px; 57 | font-weight: 100; 58 | min-width: 136px; 59 | } 60 | 61 | #map-wrapper { 62 | width: 100%; 63 | height: 700px; 64 | } 65 | 66 | #map-canvas { 67 | height: 100%; 68 | width: 100%; 69 | } 70 | 71 | 72 | h1 { 73 | color: #78be20; 74 | font-weight: 100; 75 | font-size: 38px; 76 | margin-top: 0; 77 | letter-spacing: -1px; 78 | } 79 | 80 | 81 | #last-mac { 82 | color: #6B6B6B; 83 | width: 100%; 84 | font-weight: 400; 85 | margin-bottom: 10px; 86 | font-size: 14px; 87 | } 88 | 89 | .small { 90 | color: #6B6B6B; 91 | font-weight: 400; 92 | margin-bottom: 30px; 93 | font-size: 14px; 94 | } 95 | 96 | .bold { 97 | font-weight: 600; 98 | } 99 | 100 | button, input { 101 | width: 11%; 102 | } 103 | 104 | button { 105 | height: 35px; 106 | border: none; 107 | background: #737373; 108 | border-radius: 2px; 109 | box-sizing: border-box; 110 | color: white; 111 | font-family: "proxima-nova-1","proxima-nova-2", "Helvetica Neue", Helvetica, verdana, sans-serif; 112 | font-weight: 200; 113 | font-size: 14px; 114 | padding: 0; 115 | min-width: 70px; 116 | } 117 | 118 | button:hover{ 119 | background: #616060; 120 | } 121 | 122 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/static/sample.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var map, // This is the Google map 3 | clientMarker, // The current marker when we are following a single client 4 | overlay, // Map Image Overlay 5 | clientUncertaintyCircle, // The circle describing that client's location uncertainty 6 | lastEvent, // The last scheduled polling task 7 | lastInfoWindowMac, // The last Mac displayed in a marker tooltip 8 | allMarkers = [], // The markers when we are in "View All" mode 9 | lastMac = "", // The last requested MAC to follow 10 | infoWindow = new google.maps.InfoWindow(), // The marker tooltip 11 | markerImage = new google.maps.MarkerImage('/static/blue_circle.png', 12 | new google.maps.Size(15, 15), 13 | new google.maps.Point(0, 0), 14 | new google.maps.Point(4.5, 4.5)); 15 | 16 | MerakiOverlay.prototype = new google.maps.OverlayView(); 17 | 18 | // Removes all markers 19 | function clearAll() { 20 | clientMarker.setMap(null); 21 | clientUncertaintyCircle.setMap(null); 22 | lastInfoWindowMac = ""; 23 | var m; 24 | while (allMarkers.length !== 0) { 25 | m = allMarkers.pop(); 26 | if (infoWindow.anchor === m) { 27 | lastInfoWindowMac = m.mac; 28 | } 29 | m.setMap(null); 30 | } 31 | } 32 | 33 | // Plots the location and uncertainty for a single MAC address 34 | function track(client) { 35 | clearAll(); 36 | if (client !== null && client.location !== null && !(typeof client.location === 'undefined')) { 37 | var pos = new google.maps.LatLng(client.location.lat, client.location.lng); 38 | if (client.manufacturer != null) { 39 | mfrStr = client.manufacturer + " "; 40 | } else { 41 | mfrStr = ""; 42 | } 43 | if (client.os != null) { 44 | osStr = " running " + client.os; 45 | } else { 46 | osStr = ""; 47 | } 48 | if (client.ssid != null) { 49 | ssidStr = " with SSID '" + client.ssid + "'"; 50 | } else { 51 | ssidStr = ""; 52 | } 53 | if (client.floors != null && client.floors !== "") { 54 | floorStr = " at '" + client.floors + "'" 55 | } else { 56 | floorStr = ""; 57 | } 58 | $('#last-mac').text(mfrStr + "'" + lastMac + "'" + osStr + ssidStr + 59 | " last seen on " + client.seenString + floorStr + 60 | " with uncertainty " + client.location.unc.toFixed(1) + " meters (reloading every 20 seconds)"); 61 | map.setCenter(pos); 62 | clientMarker.setMap(map); 63 | clientMarker.setPosition(pos); 64 | clientUncertaintyCircle = new google.maps.Circle({ 65 | map: map, 66 | center: pos, 67 | radius: client.location.unc, 68 | fillColor: 'RoyalBlue', 69 | fillOpacity: 0.25, 70 | strokeColor: 'RoyalBlue', 71 | strokeWeight: 1 72 | }); 73 | } else { 74 | $('#last-mac').text("Client '" + lastMac + "' could not be found"); 75 | } 76 | } 77 | 78 | // Looks up a single MAC address 79 | function lookup(mac) { 80 | $.getJSON('/clients/' + mac, function (response) { 81 | track(response); 82 | }); 83 | } 84 | 85 | // Adds a marker for a single client within the "view all" perspective 86 | function addMarker(client) { 87 | if (client !== null && client.location !== null && !(typeof client.location === 'undefined')){ 88 | var m = new google.maps.Marker({ 89 | position: new google.maps.LatLng(client.location.lat, client.location.lng), 90 | map: map, 91 | mac: client.clientMac, 92 | icon: markerImage 93 | }); 94 | google.maps.event.addListener(m, 'click', function () { 95 | infoWindow.setContent("
" + client.clientMac + "
(Follow this client)"); 97 | infoWindow.open(map, m); 98 | }); 99 | if (client.clientMac === lastInfoWindowMac) { 100 | infoWindow.open(map, m); 101 | } 102 | var pos = new google.maps.LatLng(client.location.lat, client.location.lng); 103 | map.setCenter(pos); 104 | clientMarker.setMap(map); 105 | clientMarker.setPosition(pos); 106 | allMarkers.push(m); 107 | } 108 | } 109 | 110 | // Displays markers for all clients 111 | function trackAll(clients) { 112 | clearAll(); 113 | if (clients.length === 0) { 114 | $('#last-mac').text("Found no clients (if you just started the web server, you may need to wait a few minutes to receive pushes from Meraki)"); 115 | } else { $('#last-mac').text("Found " + clients.length + " clients (reloading every 20 seconds)"); } 116 | clientUncertaintyCircle.setMap(null); 117 | for (var i = 0, len = clients.length; i < len; i++) { 118 | addMarker(clients[i]); 119 | } 120 | } 121 | 122 | // Looks up all MAC addresses 123 | function lookupAll() { 124 | $('#last-mac').text("Looking up all clients..."); 125 | $.getJSON('/clients/', function (response) { 126 | trackAll(response); 127 | }); 128 | } 129 | 130 | // Begins a task timer to reload a single MAC every 20 seconds 131 | function startLookup() { 132 | lastMac = $('#mac-field').val().trim(); 133 | if (lastEvent !== null) { window.clearInterval(lastEvent); } 134 | lookup(lastMac); 135 | lastEvent = window.setInterval(lookup, 20000, lastMac); 136 | } 137 | 138 | // Begins a task timer to reload all MACs every 20 seconds 139 | function startLookupAll() { 140 | if (lastEvent !== null) { window.clearInterval(lastEvent); } 141 | lastEvent = window.setInterval(lookupAll, 20000); 142 | lookupAll(); 143 | } 144 | 145 | // This is called after the DOM is loaded, so we can safely bind all the 146 | // listeners here. 147 | function initialize() { 148 | var center = new google.maps.LatLng(41.35384, 2.1340778); 149 | var mapOptions = { 150 | zoom: 18, 151 | center: center 152 | }; 153 | 154 | var bounds = new google.maps.LatLngBounds( 155 | new google.maps.LatLng(41.353341, 2.132165), 156 | new google.maps.LatLng(41.355241, 2.136965)); 157 | 158 | var srcImage = '/static/FiraGranVia.png'; 159 | 160 | map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 161 | 162 | clientMarker = new google.maps.Marker({ 163 | position: center, 164 | icon: markerImage 165 | }); 166 | clientUncertaintyCircle = new google.maps.Circle({ 167 | position: center 168 | }); 169 | 170 | overlay = new MerakiOverlay(bounds, srcImage, map); 171 | 172 | $('#track').click(startLookup).bind("enterKey", startLookup); 173 | 174 | $('#all').click(startLookupAll); 175 | 176 | $(document).on("click", ".client-filter", function (e) { 177 | e.preventDefault(); 178 | var mac = $(this).data('mac'); 179 | $('#mac-field').val(mac); 180 | startLookup(); 181 | }); 182 | 183 | startLookupAll(); 184 | 185 | 186 | } 187 | 188 | // Call the initialize function when the window loads 189 | $(window).load(initialize); 190 | //google.maps.event.addDomListener(window, 'load', initialize); 191 | }(jQuery)); 192 | 193 | 194 | /** @constructor */ 195 | 196 | function MerakiOverlay(bounds, image, map) { 197 | 198 | // Initialize all properties. 199 | this.bounds_ = bounds; 200 | this.image_ = image; 201 | this.map_ = map; 202 | 203 | // Define a property to hold the image's div. We'll 204 | // actually create this div upon receipt of the onAdd() 205 | // method so we'll leave it null for now. 206 | this.div_ = null; 207 | 208 | // Explicitly call setMap on this overlay. 209 | this.setMap(map); 210 | } 211 | 212 | /** 213 | * onAdd is called when the map's panes are ready and the overlay has been 214 | * added to the map. 215 | */ 216 | MerakiOverlay.prototype.onAdd = function() { 217 | 218 | var div = document.createElement('div'); 219 | div.style.borderStyle = 'none'; 220 | div.style.borderWidth = '0px'; 221 | div.style.position = 'absolute'; 222 | div.style.transform = 'rotate(63deg)'; 223 | 224 | // Create the img element and attach it to the div. 225 | var img = document.createElement('img'); 226 | img.src = this.image_; 227 | img.style.width = '25%'; 228 | img.style.height = '25%'; 229 | img.style.position = 'absolute'; 230 | div.appendChild(img); 231 | 232 | this.div_ = div; 233 | 234 | // Add the element to the "overlayLayer" pane. 235 | var panes = this.getPanes(); 236 | panes.overlayLayer.appendChild(div); 237 | }; 238 | 239 | MerakiOverlay.prototype.draw = function() { 240 | 241 | // We use the south-west and north-east 242 | // coordinates of the overlay to peg it to the correct position and size. 243 | // To do this, we need to retrieve the projection from the overlay. 244 | var overlayProjection = this.getProjection(); 245 | 246 | // Retrieve the south-west and north-east coordinates of this overlay 247 | // in LatLngs and convert them to pixel coordinates. 248 | // We'll use these coordinates to resize the div. 249 | var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest()); 250 | var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast()); 251 | 252 | // Resize the image's div to fit the indicated dimensions. 253 | var div = this.div_; 254 | div.style.left = sw.x + 'px'; 255 | div.style.top = ne.y + 'px'; 256 | div.style.width = (ne.x - sw.x) + 'px'; 257 | div.style.height = (sw.y - ne.y) + 'px'; 258 | }; 259 | 260 | // The onRemove() method will be called automatically from the API if 261 | // we ever set the overlay's map property to 'null'. 262 | MerakiOverlay.prototype.onRemove = function() { 263 | this.div_.parentNode.removeChild(this.div_); 264 | this.div_ = null; 265 | }; 266 | -------------------------------------------------------------------------------- /meraki-sample-location-scanning-receiver/templates/index.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Location Scanning Python Flask Server demo app 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |

Meraki Location Scanning Demo

21 |
22 |   23 |   24 | 25 |
26 |
27 |
Clients in the wrong place? Make sure your APs are placed properly in Dashboard.
28 |
29 |
30 |
31 |
32 | 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /meraki-sample-webhook-receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | 5 | RUN pip install -r requirements.txt 6 | 7 | EXPOSE 5005 8 | 9 | CMD ["python", "meraki_sample_webhook_receiver.py", "-n", "Simulated Network", "-s", "webbie", "-m", "Webbie", "-d", "yes"] -------------------------------------------------------------------------------- /meraki-sample-webhook-receiver/env_user.py: -------------------------------------------------------------------------------- 1 | """Set your Environment Information once, not many times. 2 | 3 | The provided sample code in this repository will reference this file to get the 4 | needed information about you and your context to complete the labs. You 5 | provide this info here once and the scripts in this repository will access it 6 | as needed by the lab. 7 | 8 | TODO: To setup your `env_user.py` copy this file then edit and save your info 9 | 10 | $ cp env_user.template env_user.py 11 | 12 | 13 | Copyright (c) 2018 Cisco and/or its affiliates. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | import os 34 | 35 | # User Input 36 | 37 | WT_ACCESS_TOKEN = os.environ['WT_ACCESS_TOKEN'] 38 | WT_ROOM_ID = os.environ['WT_ROOM_ID'] 39 | MERAKI_API_KEY = "6bec40cf957de430a6f1f2baa056b99a4fac9ea0" 40 | 41 | # End User Input 42 | -------------------------------------------------------------------------------- /meraki-sample-webhook-receiver/meraki_sample_webhook_receiver.py: -------------------------------------------------------------------------------- 1 | """The provided sample code in this repository will reference this file to get the 2 | information needed to connect to your lab backend. You provide this info here 3 | once and the scripts in this repository will access it as needed by the lab. 4 | Copyright (c) 2019 Cisco and/or its affiliates. 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 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | """ 21 | 22 | # Libraries 23 | from pprint import pprint 24 | from flask import Flask, json, request, render_template 25 | import sys, os, getopt, json 26 | from webexteamssdk import WebexTeamsAPI 27 | import requests 28 | 29 | import env_user # noqa 30 | print("WT_ACCESS_TOKEN" + env_user.WT_ACCESS_TOKEN) 31 | print("WT_ROOM_ID" + env_user.WT_ROOM_ID) 32 | 33 | # WEBEX TEAMS LIBRARY 34 | teamsapi = WebexTeamsAPI(access_token=env_user.WT_ACCESS_TOKEN) 35 | 36 | # MERAKI BASE URL 37 | # if running in docker-compose base_url = "http://meraki_cloud_simulator:5001" 38 | base_url = "" 39 | 40 | 41 | # Flask App 42 | app = Flask(__name__) 43 | 44 | # Webhook Receiver Code - Accepts JSON POST from Meraki and 45 | # Posts to WebEx Teams 46 | @app.route("/", methods=["POST"]) 47 | def get_webhook_json(): 48 | global webhook_data 49 | global base_url 50 | 51 | # Webhook Receiver 52 | webhook_data = request.json 53 | pprint(webhook_data, indent=1) 54 | webhook_data = json.dumps(webhook_data) 55 | # WebEx Teams can only handle so much text so limit to 1000 chars 56 | webhook_data = webhook_data[:1000] + '...' 57 | 58 | # Send Message to WebEx Teams 59 | teamsapi.messages.create( 60 | env_user.WT_ROOM_ID, 61 | text="Meraki Webhook Alert: " + webhook_data 62 | ) 63 | 64 | # Return success message 65 | return "WebHook POST Received" 66 | 67 | # Get Network ID based on Network name entry 68 | def get_network_id(network_wh): 69 | global base_url 70 | orgs = "" 71 | 72 | # Get Orgs that entered Meraki API Key has access to 73 | try: 74 | # MISSION TODO 75 | orgs = requests.get( 76 | base_url + "/organizations", 77 | headers={ 78 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 79 | } 80 | ) 81 | # Deserialize response text (str) to Python Dictionary object so 82 | # we can work with it 83 | orgs = json.loads(orgs.text) 84 | pprint(orgs) 85 | # END MISSION SECTION 86 | except Exception as e: 87 | pprint(e) 88 | 89 | # Now get a specific network based on name added on command line 90 | networks = "" 91 | if orgs != "": 92 | for org in orgs: 93 | try: 94 | # MISSION TODO 95 | networks = requests.get( 96 | base_url + "/organizations/"+org["id"]+"/networks", 97 | headers={ 98 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 99 | }) 100 | # Deserialize response text (str) to Python Dictionary object so 101 | # we can work with it 102 | networks = json.loads(networks.text) 103 | pprint(networks) 104 | # END MISSION SECTION 105 | except Exception as e: 106 | pprint(e) 107 | 108 | for network in networks: 109 | if network["name"] == network_wh: 110 | network_id = network["id"] 111 | return network_id 112 | 113 | return "No Network Found with that name" 114 | 115 | # Set the Webhook receiver in Meraki 116 | def set_webhook_receiver(network_id,url,secret,server_name): 117 | global base_url 118 | try: 119 | # MISSION TODO 120 | https_server_id = requests.post( 121 | base_url + "/networks/"+network_id+"/httpServers", 122 | headers = { 123 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 124 | "Content-Type": "application/json" 125 | }, 126 | data = json.dumps({ 127 | "name" : server_name, 128 | "url" : url, 129 | "sharedSecret" : secret 130 | })) 131 | pprint(https_server_id) 132 | # Deserialize response text (str) to Python Dictionary object so 133 | # we can work with it 134 | https_server_id = json.loads(https_server_id.text) 135 | pprint(https_server_id) 136 | return https_server_id['id'] 137 | # END MISSION SECTION 138 | except Exception as e: 139 | pprint(e) 140 | return "Setting https server fail" 141 | 142 | 143 | # Set the Alerts in Meraki (on set 'settingsChanged') 144 | def set_alerts(network_id,http_server_id): 145 | global base_url 146 | try: 147 | # MISSION TODO 148 | response = requests.put( 149 | base_url + "/networks/"+network_id+"/alertSettings", 150 | headers = { 151 | "X-Cisco-Meraki-API-Key": env_user.MERAKI_API_KEY, 152 | "Content-Type": "application/json" 153 | }, 154 | data = json.dumps( 155 | { 156 | "defaultDestinations": { 157 | "emails": [], 158 | "snmp": False, 159 | "allAdmins": False, 160 | "httpServerIds": [http_server_id] 161 | }, 162 | "alerts":[ 163 | { 164 | "type": "settingsChanged", 165 | "enabled": True, 166 | "alertDestinations": { 167 | "emails": [], 168 | "snmp": False, 169 | "allAdmins": False, 170 | "httpServerIds": [] 171 | }, 172 | "filters": {} 173 | } 174 | ] 175 | }) 176 | ) 177 | pprint(response) 178 | return "Alerts Set Successfully" 179 | # END MISSION SECTION 180 | except Exception as e: 181 | pprint(e) 182 | return "Alert Settings Failed" 183 | 184 | # Launch application with supplied arguments 185 | def main(argv): 186 | """Parse command line arguments.""" 187 | network = None 188 | secret = None 189 | server_name = None 190 | in_docker = None 191 | 192 | try: 193 | opts, args = getopt.getopt(argv, "hn:s:m:d:", ["network=", "secret=","server_name=","in_docker="]) 194 | except getopt.GetoptError: 195 | print("webhookreceiver.py -n network -s -m -d ") 196 | sys.exit(2) 197 | for opt, arg in opts: 198 | if opt == "-h": 199 | print("webhookreceiver.py -n network -s -m -d ") 200 | sys.exit() 201 | elif opt in ("-s", "--secret"): 202 | secret = arg 203 | elif opt in ("-n", "--network"): 204 | network = arg 205 | elif opt in ("-m", "--server_name"): 206 | server_name = arg 207 | elif opt in ("-d", "--in_docker"): 208 | in_docker = arg 209 | 210 | print("secret: " + secret) 211 | print("network: " + network) 212 | print("server_name: " + server_name) 213 | print("in_docker: " + in_docker) 214 | return [network,secret,server_name,in_docker] 215 | 216 | 217 | if __name__ == "__main__": 218 | args = main(sys.argv[1:]) 219 | 220 | ''' 221 | # Code to get ngrok tunnel info so we don't have to set it manually 222 | # This will set our "url" value to be passed to the webhook setup 223 | tunnels = requests.request("GET", \ 224 | "http://127.0.0.1:4040/api/tunnels", \ 225 | verify=False) 226 | 227 | tunnels = json.loads(tunnels.text) 228 | tunnels = tunnels["tunnels"] 229 | 230 | url = "" 231 | for tunnel in tunnels: 232 | if tunnel['proto'] == 'https': 233 | url = tunnel['public_url'] 234 | ''' 235 | 236 | # Configuration parameters 237 | in_docker = args[3] 238 | if in_docker == "yes": 239 | base_url = "http://meraki_cloud_simulator:5001" 240 | url = "http://meraki-sample-webhook-receiver:5005" 241 | else: 242 | base_url = "http://localhost:5001" 243 | url = "http://localhost:5005" 244 | network_id = get_network_id(args[0]) 245 | secret = args[1] 246 | server_name = args[2] 247 | server_id=set_webhook_receiver(network_id,url,secret,server_name) 248 | set_alerts(network_id,server_id) 249 | 250 | app.run(host="0.0.0.0", port=5005, debug=False) 251 | -------------------------------------------------------------------------------- /meraki-sample-webhook-receiver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | Flask 3 | webexteamssdk 4 | -------------------------------------------------------------------------------- /meraki_cloud_simulator.py: -------------------------------------------------------------------------------- 1 | """Cisco Meraki Cloud Simulator for External Captive Portal labs.""" 2 | 3 | from merakicloudsimulator import merakicloudsimulator 4 | from merakicloudsimulator.meraki_settings import ORGANIZATIONS, NETWORKS 5 | from flask import render_template,jsonify,abort 6 | 7 | # Module Constants & Simulated Cloud Data 8 | WEB_SERVER_HOSTNAME = "localhost" 9 | WEB_SERVER_BIND_IP = "0.0.0.0" 10 | WEB_SERVER_BIND_PORT = 5001 11 | 12 | @merakicloudsimulator.route("/go", methods=["GET"]) 13 | def meraki_simulator_go(): 14 | return render_template("index.html") 15 | 16 | # Flask micro-webservice API/URI endpoints 17 | @merakicloudsimulator.route("/organizations", methods=["GET"]) 18 | def get_org_id(): 19 | """Get a list of simulated organizations.""" 20 | return jsonify(ORGANIZATIONS) 21 | 22 | 23 | @merakicloudsimulator.route("/organizations//networks", methods=["GET"]) 24 | def get_networks(organization_id): 25 | """Get the list of networks for an organization.""" 26 | organization_networks = NETWORKS.get(organization_id) 27 | if organization_networks: 28 | return jsonify(organization_networks) 29 | else: 30 | abort(404) 31 | 32 | if __name__ == "__main__": 33 | print( 34 | f"\n>>> " 35 | f"Open your browser and browse to " 36 | f"http://{WEB_SERVER_HOSTNAME}:{WEB_SERVER_BIND_PORT}/go " 37 | f"to configure the captive portal and simulate a client login." 38 | f" <<<\n" 39 | ) 40 | 41 | # Start the web server 42 | merakicloudsimulator.run(host=WEB_SERVER_BIND_IP, port=WEB_SERVER_BIND_PORT, threaded=True, debug=False) -------------------------------------------------------------------------------- /merakicloudsimulator/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, render_template, redirect, jsonify, abort 2 | 3 | merakicloudsimulator = Flask(__name__) 4 | 5 | from merakicloudsimulator import meraki_settings 6 | from merakicloudsimulator import alert_settings 7 | from merakicloudsimulator import sample_alert_messages 8 | from merakicloudsimulator import excapsimulator 9 | from merakicloudsimulator import locationscanningsimulator 10 | from merakicloudsimulator import webhooksimulator 11 | -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/alert_settings.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/alert_settings.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/excapsimulator.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/excapsimulator.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/locationscanningsimulator.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/locationscanningsimulator.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/meraki_settings.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/meraki_settings.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/sample_alert_messages.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/sample_alert_messages.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/__pycache__/webhooksimulator.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/meraki_cloud_simulator/2bcb63049a345211a4bfa20ea0cfe88120c6bb23/merakicloudsimulator/__pycache__/webhooksimulator.cpython-36.pyc -------------------------------------------------------------------------------- /merakicloudsimulator/alert_settings.py: -------------------------------------------------------------------------------- 1 | http_servers = [] 2 | alert_settings = { 3 | "defaultDestinations": { 4 | "emails": [], 5 | "snmp": False, 6 | "allAdmins": False, 7 | "httpServerIds": [] 8 | }, 9 | "alerts": [ 10 | { 11 | "type": "gatewayDown", 12 | "enabled": False, 13 | "alertDestinations": { 14 | "emails": [], 15 | "snmp": False, 16 | "allAdmins": False, 17 | "httpServerIds": [] 18 | }, 19 | "filters": { 20 | "timeout": 60 21 | } 22 | }, 23 | { 24 | "type": "gatewayToRepeater", 25 | "enabled": False, 26 | "alertDestinations": { 27 | "emails": [], 28 | "snmp": False, 29 | "allAdmins": False, 30 | "httpServerIds": [] 31 | }, 32 | "filters": {} 33 | }, 34 | { 35 | "type": "repeaterDown", 36 | "enabled": False, 37 | "alertDestinations": { 38 | "emails": [], 39 | "snmp": False, 40 | "allAdmins": False, 41 | "httpServerIds": [] 42 | }, 43 | "filters": { 44 | "timeout": 60 45 | } 46 | }, 47 | { 48 | "type": "rogueAp", 49 | "enabled": False, 50 | "alertDestinations": { 51 | "emails": [], 52 | "snmp": False, 53 | "allAdmins": False, 54 | "httpServerIds": [] 55 | }, 56 | "filters": {} 57 | }, 58 | { 59 | "type": "settingsChanged", 60 | "enabled": False, 61 | "alertDestinations": { 62 | "emails": [], 63 | "snmp": False, 64 | "allAdmins": False, 65 | "httpServerIds": [] 66 | }, 67 | "filters": {} 68 | }, 69 | { 70 | "type": "vpnConnectivityChanged", 71 | "enabled": False, 72 | "alertDestinations": { 73 | "emails": [], 74 | "snmp": False, 75 | "allAdmins": False, 76 | "httpServerIds": [] 77 | }, 78 | "filters": {} 79 | }, 80 | { 81 | "type": "usageAlert", 82 | "enabled": False, 83 | "alertDestinations": { 84 | "emails": [], 85 | "snmp": False, 86 | "allAdmins": False, 87 | "httpServerIds": [] 88 | }, 89 | "filters": { 90 | "threshold": 1, 91 | "period": 1200 92 | } 93 | }, 94 | { 95 | "type": "ampMalwareDetected", 96 | "enabled": False, 97 | "alertDestinations": { 98 | "emails": [], 99 | "snmp": False, 100 | "allAdmins": False, 101 | "httpServerIds": [] 102 | }, 103 | "filters": {} 104 | }, 105 | { 106 | "type": "ampMalwareBlocked", 107 | "enabled": False, 108 | "alertDestinations": { 109 | "emails": [], 110 | "snmp": False, 111 | "allAdmins": False, 112 | "httpServerIds": [] 113 | }, 114 | "filters": {} 115 | }, 116 | { 117 | "type": "applianceDown", 118 | "enabled": False, 119 | "alertDestinations": { 120 | "emails": [], 121 | "snmp": False, 122 | "allAdmins": False, 123 | "httpServerIds": [] 124 | }, 125 | "filters": { 126 | "timeout": 5 127 | } 128 | }, 129 | { 130 | "type": "failoverEvent", 131 | "enabled": False, 132 | "alertDestinations": { 133 | "emails": [], 134 | "snmp": False, 135 | "allAdmins": False, 136 | "httpServerIds": [] 137 | }, 138 | "filters": {} 139 | }, 140 | { 141 | "type": "dhcpNoLeases", 142 | "enabled": False, 143 | "alertDestinations": { 144 | "emails": [], 145 | "snmp": False, 146 | "allAdmins": False, 147 | "httpServerIds": [] 148 | }, 149 | "filters": {} 150 | }, 151 | { 152 | "type": "rogueDhcp", 153 | "enabled": False, 154 | "alertDestinations": { 155 | "emails": [], 156 | "snmp": False, 157 | "allAdmins": False, 158 | "httpServerIds": [] 159 | }, 160 | "filters": {} 161 | }, 162 | { 163 | "type": "ipConflict", 164 | "enabled": False, 165 | "alertDestinations": { 166 | "emails": [], 167 | "snmp": False, 168 | "allAdmins": False, 169 | "httpServerIds": [] 170 | }, 171 | "filters": {} 172 | }, 173 | { 174 | "type": "cellularUpDown", 175 | "enabled": False, 176 | "alertDestinations": { 177 | "emails": [], 178 | "snmp": False, 179 | "allAdmins": False, 180 | "httpServerIds": [] 181 | }, 182 | "filters": {} 183 | }, 184 | { 185 | "type": "clientConnectivity", 186 | "enabled": False, 187 | "alertDestinations": { 188 | "emails": [], 189 | "snmp": False, 190 | "allAdmins": False, 191 | "httpServerIds": [] 192 | }, 193 | "filters": { 194 | "clients": [] 195 | } 196 | }, 197 | { 198 | "type": "vrrp", 199 | "enabled": False, 200 | "alertDestinations": { 201 | "emails": [], 202 | "snmp": False, 203 | "allAdmins": False, 204 | "httpServerIds": [] 205 | }, 206 | "filters": {} 207 | }, 208 | { 209 | "type": "portDown", 210 | "enabled": False, 211 | "alertDestinations": { 212 | "emails": [], 213 | "snmp": False, 214 | "allAdmins": False, 215 | "httpServerIds": [] 216 | }, 217 | "filters": { 218 | "timeout": 5, 219 | "selector": "any port" 220 | } 221 | }, 222 | { 223 | "type": "powerSupplyDown", 224 | "enabled": False, 225 | "alertDestinations": { 226 | "emails": [], 227 | "snmp": False, 228 | "allAdmins": False, 229 | "httpServerIds": [] 230 | }, 231 | "filters": {} 232 | }, 233 | { 234 | "type": "rpsBackup", 235 | "enabled": False, 236 | "alertDestinations": { 237 | "emails": [], 238 | "snmp": False, 239 | "allAdmins": False, 240 | "httpServerIds": [] 241 | }, 242 | "filters": {} 243 | }, 244 | { 245 | "type": "udldError", 246 | "enabled": False, 247 | "alertDestinations": { 248 | "emails": [], 249 | "snmp": False, 250 | "allAdmins": False, 251 | "httpServerIds": [] 252 | }, 253 | "filters": {} 254 | }, 255 | { 256 | "type": "portError", 257 | "enabled": False, 258 | "alertDestinations": { 259 | "emails": [], 260 | "snmp": False, 261 | "allAdmins": False, 262 | "httpServerIds": [] 263 | }, 264 | "filters": { 265 | "selector": "any port" 266 | } 267 | }, 268 | { 269 | "type": "portSpeed", 270 | "enabled": False, 271 | "alertDestinations": { 272 | "emails": [], 273 | "snmp": False, 274 | "allAdmins": False, 275 | "httpServerIds": [] 276 | }, 277 | "filters": { 278 | "selector": "any port" 279 | } 280 | }, 281 | { 282 | "type": "newDhcpServer", 283 | "enabled": False, 284 | "alertDestinations": { 285 | "emails": [], 286 | "snmp": False, 287 | "allAdmins": False, 288 | "httpServerIds": [] 289 | }, 290 | "filters": {} 291 | }, 292 | { 293 | "type": "switchDown", 294 | "enabled": False, 295 | "alertDestinations": { 296 | "emails": [], 297 | "snmp": False, 298 | "allAdmins": False, 299 | "httpServerIds": [] 300 | }, 301 | "filters": { 302 | "timeout": 5 303 | } 304 | }, 305 | { 306 | "type": "nodeHardwareFailure", 307 | "enabled": False, 308 | "alertDestinations": { 309 | "emails": [], 310 | "snmp": False, 311 | "allAdmins": False, 312 | "httpServerIds": [] 313 | }, 314 | "filters": {} 315 | }, 316 | { 317 | "type": "cameraDown", 318 | "enabled": False, 319 | "alertDestinations": { 320 | "emails": [], 321 | "snmp": False, 322 | "allAdmins": False, 323 | "httpServerIds": [] 324 | }, 325 | "filters": { 326 | "timeout": 60 327 | } 328 | } 329 | ] 330 | } -------------------------------------------------------------------------------- /merakicloudsimulator/excapsimulator.py: -------------------------------------------------------------------------------- 1 | """Cisco Meraki Cloud Simulator for External Captive Portal labs.""" 2 | from merakicloudsimulator import merakicloudsimulator 3 | from flask import request, render_template, redirect, jsonify, abort 4 | import random 5 | import requests 6 | from datetime import datetime 7 | 8 | # Module Variables 9 | captive_portal_url = "" 10 | user_continue_url = "" 11 | window = "" 12 | splash_logins = [] 13 | 14 | # Helper Functions 15 | def generate_fake_mac(): 16 | """Generate a fake MAC address.""" 17 | hex_characters = "0123456789abcdef" 18 | 19 | def random_byte(): 20 | """Generate a random byte.""" 21 | return random.choice(hex_characters) + random.choice(hex_characters) 22 | 23 | return ":".join(random_byte() for _ in range(6)) 24 | 25 | 26 | # Flask micro-webservice API/URI endpoints 27 | @merakicloudsimulator.route("/networks//ssids/", methods=["PUT"]) 28 | def put_ssid(network_id, ssid_id): 29 | """Simulate setting SSID configurations.""" 30 | print(f"Settings updated for network {network_id} ssid {ssid_id}.") 31 | return jsonify(request.json) 32 | 33 | 34 | @merakicloudsimulator.route( 35 | "/networks//ssids//splashSettings", 36 | methods=["PUT"], 37 | ) 38 | def put_splash(network_id, ssid_id): 39 | """Simulate setting Splash Page configurations.""" 40 | print(f"Splash settings updated for network {network_id} ssid {ssid_id}.") 41 | return jsonify(request.json) 42 | 43 | 44 | @merakicloudsimulator.route("/networks//splashLoginAttempts", methods=["GET"]) 45 | def get_splash_logins(network_id): 46 | """Get list of Splash Page logins.""" 47 | # We aren't associating specific logins with a network ID 48 | _ = network_id 49 | return jsonify(splash_logins) 50 | 51 | 52 | @merakicloudsimulator.route("/excap", methods=["GET"]) 53 | def excap_go(): 54 | """Process GET requests to the /excap URI; render the index.html page.""" 55 | return render_template("excap.html") 56 | 57 | 58 | @merakicloudsimulator.route("/connecttowifi", methods=["POST"]) 59 | def connect_to_wifi(): 60 | """Save captive portal details; redirect to the External Captive Portal.""" 61 | 62 | captive_portal_url = request.form["captive_portal_url"] 63 | base_grant_url = request.host_url + "splash/grant" 64 | user_continue_url = request.form["user_continue_url"] 65 | node_mac = generate_fake_mac() 66 | client_ip = request.remote_addr 67 | client_mac = generate_fake_mac() 68 | splash_click_time = datetime.utcnow().isoformat() 69 | full_url = ( 70 | captive_portal_url 71 | + "?base_grant_url=" + base_grant_url 72 | + "&user_continue_url=" + user_continue_url 73 | + "&node_mac=" + node_mac 74 | + "&client_ip=" + client_ip 75 | + "&client_mac=" + client_mac 76 | ) 77 | 78 | splash_logins.append( 79 | { 80 | "name": "Simulated Client", 81 | "login": "simulatedclient@meraki.com", 82 | "ssid": "Simulated SSID", 83 | "loginAt": splash_click_time, 84 | "gatewayDeviceMac": node_mac, 85 | "clientMac": client_mac, 86 | "clientId": client_ip, 87 | "authorization": "success", 88 | } 89 | ) 90 | 91 | return redirect(full_url, code=302) 92 | 93 | 94 | @merakicloudsimulator.route("/splash/grant", methods=["GET"]) 95 | def continue_to_url(): 96 | """Accept captive portal click-through; redirect to the continue URL.""" 97 | return redirect(request.args.get("continue_url"), code=302) 98 | -------------------------------------------------------------------------------- /merakicloudsimulator/locationscanningsimulator.py: -------------------------------------------------------------------------------- 1 | """Cisco Meraki Location Scanning Data simulator""" 2 | 3 | # Libraries 4 | from merakicloudsimulator import merakicloudsimulator 5 | from flask import request, render_template, redirect 6 | import random 7 | import datetime 8 | from time import sleep 9 | import requests 10 | import threading 11 | 12 | # module vars 13 | location_data = "" 14 | map_bounds = "" 15 | client_macs = [] 16 | ap_macs = [] 17 | ap_data = [] 18 | server_url = "" 19 | num_aps = 0 20 | num_clients = 0 21 | 22 | stop_location_thread = False 23 | location_thread = threading.Thread() 24 | 25 | def ap_cycle(num_aps,map_bounds): 26 | ap = 0 27 | 28 | while True: 29 | global stop_location_thread 30 | if stop_location_thread: 31 | print("Stopping Posting of Location Stream") 32 | break 33 | print("heading to postJSON " + str(ap)) 34 | post_json(ap) 35 | print("back from postJSON " + str(ap)) 36 | determine_seen_associated() 37 | update_location_data(ap,map_bounds) 38 | print("back from update" + str(ap)) 39 | print("sleeping") 40 | sleep(10) 41 | print("done sleeping") 42 | if ap == num_aps - 1: 43 | ap = 0 44 | else: 45 | ap += 1 46 | 47 | 48 | def manage_location_streaming_thread(num_aps,map_bounds): 49 | global stop_location_thread 50 | global location_thread 51 | 52 | if location_thread.isAlive(): 53 | print("location thread already started, killing an restarting") 54 | stop_location_thread = True 55 | location_thread.join() 56 | print('location thread killed') 57 | stop_location_thread = False 58 | location_thread = threading.Thread(target = ap_cycle,args=[num_aps,map_bounds], daemon=True) 59 | location_thread.start() 60 | else: 61 | print('location thread not started; starting...') 62 | stop_location_thread = False 63 | location_thread = threading.Thread(target = ap_cycle,args=[num_aps,map_bounds], daemon=True) 64 | location_thread.start() 65 | 66 | 67 | @merakicloudsimulator.route("/bounds/", methods=["GET"]) 68 | def set_location_bounds(map_bounds_in): 69 | global map_bounds 70 | 71 | map_bounds = map_bounds_in 72 | map_bounds = map_bounds.replace("(", "").replace(")", "").replace( 73 | " ", "" 74 | ).split( 75 | "," 76 | ) 77 | 78 | return "" 79 | 80 | 81 | # generate FAKE MAC addresses for number of clients requested 82 | 83 | 84 | def generate_client_macs(num_clients, num_aps): 85 | global client_macs 86 | 87 | for client in range(num_clients): 88 | client_mac = "" 89 | for mac_part in range(6): 90 | 91 | client_mac += "".join( 92 | random.choice("0123456789abcdef") for i in range(2) 93 | ) 94 | 95 | if mac_part < 5: 96 | client_mac += ":" 97 | else: 98 | client_macs.append( 99 | { 100 | "client_mac": client_mac, 101 | "associated": random.randint(0, 1), 102 | "ap_associated": random.randint(1, num_aps), 103 | } 104 | ) 105 | 106 | 107 | def determine_seen_associated(): 108 | global client_macs 109 | global ap_macs 110 | 111 | random.shuffle(client_macs) 112 | random.shuffle(ap_macs) 113 | 114 | for client_mac in client_macs: 115 | client_mac["associated"] = random.randint(0, 1) 116 | client_mac["ap_associated"] = random.randint(1, len(ap_macs)) 117 | 118 | for ap_mac in ap_macs: 119 | ap_mac["num_ap_clients_seen"] = random.randint(1, len(client_macs)) 120 | 121 | 122 | # generate FAKE MAC addresses for number of APs requested 123 | def generate_ap_macs(num_aps, num_clients): 124 | global ap_macs 125 | 126 | for ap in range(num_aps): 127 | ap_mac = "" 128 | for mac_part in range(6): 129 | ap_mac += "".join( 130 | random.choice("0123456789abcdef") for i in range(2) 131 | ) 132 | if mac_part < 5: 133 | ap_mac += ":" 134 | else: 135 | ap_macs.append( 136 | { 137 | "ap_mac": ap_mac, 138 | "num_ap_clients_seen": random.randint(1, num_clients), 139 | } 140 | ) 141 | 142 | 143 | # Kick off simulator and create baseline dataset 144 | @merakicloudsimulator.route("/locationscanning/", methods=["POST","GET"]) 145 | def generate_location_data(set): 146 | global server_url 147 | global map_bounds 148 | global client_macs 149 | global ap_macs 150 | global ap_data 151 | global num_aps 152 | global num_clients 153 | 154 | location_data = "" 155 | client_macs = [] 156 | ap_macs = [] 157 | ap_data = [] 158 | server_url = "" 159 | num_aps = 0 160 | num_clients = 0 161 | 162 | 163 | if request.method == "POST" and set == 'set': 164 | num_clients = int(request.form["num_clients"].lstrip().rstrip()) 165 | num_aps = int(request.form["num_aps"].lstrip().rstrip()) 166 | server_url = request.form["server_url"].lstrip().rstrip() 167 | 168 | device_list = [ 169 | {"os": "Android", "manufacturer": "Samsung"}, 170 | {"os": "iOS", "manufacturer": "Apple"}, 171 | {"os": "macOS", "manufacturer": "Apple"}, 172 | {"os": "Windows", "manufacturer": "Lenovo"}, 173 | {"os": "Linux", "manufacturer": "Nest"}, 174 | {"os": "Linux", "manufacturer": "Amazon"}, 175 | ] 176 | date_time_now = datetime.datetime.now() 177 | epoch = ( 178 | date_time_now - datetime.datetime.utcfromtimestamp(0) 179 | ).total_seconds() * 1000.0 180 | 181 | generate_client_macs(num_clients, num_aps) 182 | generate_ap_macs(num_aps, num_clients) 183 | 184 | # generate the client distribution per ap 185 | # any ap may see all probing and associated clients 186 | # Only one ap may see an associated client 187 | for ap_index, ap_mac in enumerate(ap_macs): 188 | 189 | observations = [] 190 | 191 | for client_index, client_mac in enumerate(client_macs): 192 | device = random.sample(device_list, 1) 193 | device = device[0] 194 | ipv4 = None 195 | ssid = None 196 | if client_mac["associated"] == 1 and client_mac[ 197 | "ap_associated" 198 | ] == ap_index: 199 | ipv4 = "192.168.0." + str(client_index) 200 | ssid = "SimulatorWifi" 201 | 202 | observations.append( 203 | { 204 | "clientMac": client_mac["client_mac"], 205 | "ipv4": ipv4, 206 | "ipv6": None, 207 | "location": { 208 | "lat": random.uniform( 209 | float(map_bounds[0]), float(map_bounds[2]) 210 | ), 211 | "lng": random.uniform( 212 | float(map_bounds[1]), float(map_bounds[3]) 213 | ), 214 | "unc": random.uniform(0, 10), 215 | "x": [], 216 | "y": [], 217 | }, 218 | "manufacturer": device["manufacturer"], 219 | "os": device["os"], 220 | "rssi": random.randint(25, 120), 221 | "seenEpoch": epoch, 222 | "seenTime": date_time_now.isoformat( 223 | sep="T" 224 | ), 225 | "ssid": ssid, 226 | } 227 | ) 228 | 229 | ap_data.append( 230 | { 231 | "data": { 232 | "apFloors": [], 233 | "apMac": ap_mac["ap_mac"], 234 | "apTags": [], 235 | "observations": observations, 236 | }, 237 | "secret": "simulator", 238 | "type": "DevicesSeen", 239 | "version": "2.0", 240 | } 241 | ) 242 | 243 | # Pass the AP array to cycle through them to 244 | manage_location_streaming_thread(num_aps,map_bounds) 245 | 246 | return render_template("locationscanningrunning.html", num_aps=num_aps, \ 247 | num_clients=num_clients, server_url=server_url, datavar=ap_data) 248 | elif set=='reset': 249 | return render_template("locationscanning.html", num_aps=num_aps, \ 250 | num_clients=num_clients, server_url=server_url, datavar=ap_data) 251 | else: 252 | return render_template("locationscanning.html", num_aps=num_aps, \ 253 | num_clients=num_clients, server_url=server_url, datavar=ap_data) 254 | 255 | def update_location_data(ap,map_bounds): 256 | global ap_data 257 | 258 | date_time_now = datetime.datetime.now() 259 | epoch = ( 260 | date_time_now - datetime.datetime.utcfromtimestamp(0) 261 | ).total_seconds() * 1000.0 262 | 263 | ap_instance = ap_data[ap] 264 | 265 | observations = ap_instance["data"]["observations"] 266 | 267 | for observation in observations: 268 | observation["location"]["lat"] = random.uniform( 269 | float(map_bounds[0]), float(map_bounds[2]) 270 | ) 271 | observation["location"]["lng"] = random.uniform( 272 | float(map_bounds[1]), float(map_bounds[3]) 273 | ) 274 | observation["location"]["unc"] = random.uniform(0, 10) 275 | observation["rssi"] = random.randint(25, 120) 276 | observation["seenEpoch"] = epoch 277 | observation["seenTime"] = date_time_now.isoformat( 278 | sep="T" 279 | ) 280 | 281 | ap_instance["data"]["observations"] = observations 282 | ap_data[ap] = ap_instance 283 | print("updated ap ") 284 | print(ap_data[ap]) 285 | 286 | def post_json(ap): 287 | global server_url 288 | global ap_data 289 | 290 | requests.post(server_url, json=ap_data[ap]) # post to listener 291 | print(ap_data[ap]) 292 | -------------------------------------------------------------------------------- /merakicloudsimulator/meraki_settings.py: -------------------------------------------------------------------------------- 1 | ORGANIZATIONS = [{"id": "1234567", "name": "Simulated Organization"}] 2 | 3 | # Networks indexed by organization ID. 4 | NETWORKS = { 5 | "1234567": [ 6 | { 7 | "id": "L_12345678910", 8 | "organizationId": "1234567", 9 | "name": "Simulated Network", 10 | "timeZone": "America/New_York", 11 | "tags": "", 12 | "productTypes": ["appliance", "switch", "wireless"], 13 | "type": "combined", 14 | "disableMyMerakiCom": False, 15 | "disableRemoteStatusPage": True, 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /merakicloudsimulator/sample_alert_messages.py: -------------------------------------------------------------------------------- 1 | alert_messages = { 2 | "settingsChanged":{ 3 | "version": "0.1", 4 | "sharedSecret": "foo", 5 | "sentAt": "2019-07-19T06:20:39.656975Z", 6 | "organizationId": "00000001", 7 | "organizationName": "Miles Monitoring Inc.", 8 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 9 | "networkId": "N_111111111111", 10 | "networkName": "Main Office", 11 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 12 | "alertId": "0000000000000000", 13 | "alertType": "Settings changed", 14 | "occurredAt": "2019-07-19T06:15:33.504142Z", 15 | "alertData": { 16 | "name": "Routing and DHCP", 17 | "url": "/manage/configure/switch_l3", 18 | "changes": { 19 | "createStaticRoute": { 20 | "label": "Added static route on SP-Warehouse", 21 | "newText": "10.10.10.0/24 -> 172.16.254.253", 22 | "changedBy": "Miles Meraki (Miles@Meraki.com)", 23 | "oldText": "", 24 | "ssidId": None 25 | } 26 | }, 27 | "userId": 578149602163763500 28 | }, 29 | "deviceSerial": "XXXX-XXXX-XXXX", 30 | "deviceMac": "00:00:00:aa:bb:cc", 31 | "deviceName": "Device Foo Bar", 32 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000" 33 | }, 34 | "vpnConnectivityChanged":{ 35 | "version": "0.1", 36 | "sharedSecret": "foo", 37 | "sentAt": "2019-07-19T06:48:28.731383Z", 38 | "organizationId": "00000001", 39 | "organizationName": "Miles Monitoring Inc.", 40 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 41 | "networkId": "N_111111111111", 42 | "networkName": "Main Office", 43 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 44 | "deviceSerial": "XXXX-XXXX-XXXX", 45 | "deviceMac": "00:00:00:aa:bb:cc", 46 | "deviceName": "Device Foo Bar", 47 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 48 | "alertId": "0000000000000000", 49 | "alertType": "VPN connectivity changed", 50 | "occurredAt": "2019-07-19T06:19:23.606000Z", 51 | "alertData": { 52 | "vpnType": "l2tpv3", 53 | "vap": "1", 54 | "onSecondary": "false", 55 | "secondaryEndpoint": "0.0.0.0", 56 | "primaryEndpoint": "1.1.1.1", 57 | "connectivity": "true" 58 | } 59 | }, 60 | "rogueAp":{ 61 | "version": "0.1", 62 | "sharedSecret": "foo", 63 | "sentAt": "2019-07-19T05:41:27.939986Z", 64 | "organizationId": "00000001", 65 | "organizationName": "Miles Monitoring Inc.", 66 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 67 | "networkId": "N_111111111111", 68 | "networkName": "Main Office", 69 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 70 | "deviceSerial": "XXXX-XXXX-XXXX", 71 | "deviceMac": "00:00:00:aa:bb:cc", 72 | "deviceName": "Device Foo Bar", 73 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 74 | "alertId": "0000000000000000", 75 | "alertType": "Rogue AP detected", 76 | "occurredAt": "2011-06-07T01:19:47.432747Z", 77 | "alertData": { 78 | "rssi": 1126462856796059000 79 | } 80 | }, 81 | "gatewayToRepeater":{ 82 | "version": "0.1", 83 | "sharedSecret": "foo", 84 | "sentAt": "2019-07-19T01:23:50.961355Z", 85 | "organizationId": "00000001", 86 | "organizationName": "Miles Monitoring Inc.", 87 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 88 | "networkId": "N_111111111111", 89 | "networkName": "Main Office", 90 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 91 | "deviceSerial": "XXXX-XXXX-XXXX", 92 | "deviceMac": "00:00:00:aa:bb:cc", 93 | "deviceName": "Device Foo Bar", 94 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 95 | "alertId": "0000000000000000", 96 | "alertType": "Gateway to repeater", 97 | "occurredAt": "2019-07-15T13:55:35.000000Z", 98 | "alertData": {} 99 | }, 100 | "failoverEvent":{ 101 | "version": "0.1", 102 | "sharedSecret": "foo", 103 | "sentAt": "2019-07-19T01:02:34.397815Z", 104 | "organizationId": "00000001", 105 | "organizationName": "Miles Monitoring Inc.", 106 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 107 | "networkId": "N_111111111111", 108 | "networkName": "Main Office", 109 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 110 | "deviceSerial": "XXXX-XXXX-XXXX", 111 | "deviceMac": "00:00:00:aa:bb:cc", 112 | "deviceName": "Device Foo Bar", 113 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 114 | "alertId": "0000000000000000", 115 | "alertType": "Uplink status changed", 116 | "occurredAt": "2019-07-19T01:02:01.543000Z", 117 | "alertData": { 118 | "uplink": "0" 119 | } 120 | }, 121 | "dhcpNoLeases":{ 122 | "version": "0.1", 123 | "sharedSecret": "foo", 124 | "sentAt": "2019-07-19T00:47:44.161980Z", 125 | "organizationId": "00000001", 126 | "organizationName": "Miles Monitoring Inc.", 127 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 128 | "networkId": "N_111111111111", 129 | "networkName": "Main Office", 130 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 131 | "deviceSerial": "XXXX-XXXX-XXXX", 132 | "deviceMac": "00:00:00:aa:bb:cc", 133 | "deviceName": "Device Foo Bar", 134 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 135 | "alertId": "0000000000000000", 136 | "alertType": "DHCP leases exhausted", 137 | "occurredAt": "2019-07-18T21:41:26.324000Z", 138 | "alertData": { 139 | "network": "192.168.1.0/24'192.168.1.254" 140 | } 141 | }, 142 | "ipConflict":{ 143 | "version": "0.1", 144 | "sharedSecret": "foo", 145 | "sentAt": "2019-07-19T01:28:25.959399Z", 146 | "organizationId": "00000001", 147 | "organizationName": "Miles Monitoring Inc.", 148 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 149 | "networkId": "N_111111111111", 150 | "networkName": "Main Office", 151 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 152 | "deviceSerial": "XXXX-XXXX-XXXX", 153 | "deviceMac": "00:00:00:aa:bb:cc", 154 | "deviceName": "Device Foo Bar", 155 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 156 | "alertId": "0000000000000000", 157 | "alertType": "Client IP conflict detected", 158 | "occurredAt": "2019-07-19T01:26:43.866000Z", 159 | "alertData": { 160 | "conflictingIp": "192.168.1.54", 161 | "contendingMac": "00:00:00:44:44:44" 162 | } 163 | }, 164 | "cellularUpDown":{ 165 | "version": "0.1", 166 | "sharedSecret": "foo", 167 | "sentAt": "2019-07-18T23:42:26.937287Z", 168 | "organizationId": "00000001", 169 | "organizationName": "Miles Monitoring Inc.", 170 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 171 | "networkId": "N_111111111111", 172 | "networkName": "Main Office", 173 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 174 | "deviceSerial": "XXXX-XXXX-XXXX", 175 | "deviceMac": "00:00:00:aa:bb:cc", 176 | "deviceName": "Device Foo Bar", 177 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 178 | "alertId": "0000000000000000", 179 | "alertType": "Cellular went down", 180 | "occurredAt": "2019-07-18T22:38:45.429000Z", 181 | "alertData": { 182 | "bytesIn": "1861", 183 | "provider": "AT&amp;T", 184 | "model": "AirCard 340U", 185 | "local": "192.168.99.1", 186 | "remote": "1.1.1.1", 187 | "connectTime": "2", 188 | "bytesOut": "1880", 189 | "connection": "4G" 190 | } 191 | }, 192 | "rogueDhcp":{ 193 | "version": "0.1", 194 | "sharedSecret": "foo", 195 | "sentAt": "2019-07-19T06:04:11.110859Z", 196 | "organizationId": "00000001", 197 | "organizationName": "Miles Monitoring Inc.", 198 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 199 | "networkId": "N_111111111111", 200 | "networkName": "Main Office", 201 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 202 | "deviceSerial": "XXXX-XXXX-XXXX", 203 | "deviceMac": "00:00:00:aa:bb:cc", 204 | "deviceName": "Device Foo Bar", 205 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 206 | "alertId": "0000000000000000", 207 | "alertType": "Rogue DHCP server detected", 208 | "occurredAt": "2019-07-19T01:30:40.382000Z", 209 | "alertData": { 210 | "eth": "bb:bb:bb:11:11:11", 211 | "ip": "10.20.2.62", 212 | "subnet": "0.0.0.0/0", 213 | "vlan": "3" 214 | } 215 | }, 216 | "vrrp":{ 217 | "version": "0.1", 218 | "sharedSecret": "foo", 219 | "sentAt": "2019-07-19T06:51:37.412365Z", 220 | "organizationId": "00000001", 221 | "organizationName": "Miles Monitoring Inc.", 222 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 223 | "networkId": "N_111111111111", 224 | "networkName": "Main Office", 225 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 226 | "deviceSerial": "XXXX-XXXX-XXXX", 227 | "deviceMac": "00:00:00:aa:bb:cc", 228 | "deviceName": "Device Foo Bar", 229 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 230 | "alertId": "0000000000000000", 231 | "alertType": "Failover event detected", 232 | "occurredAt": "2019-07-19T05:55:20.524000Z", 233 | "alertData": { 234 | "oldIfUp": "0", 235 | "oldMode": "detect", 236 | "oldPrio": "75", 237 | "electorState": "master", 238 | "mode": "detect", 239 | "prio": "75", 240 | "ifUp": "1" 241 | } 242 | }, 243 | "ampMalwareBlocked":{ 244 | "version": "0.1", 245 | "sharedSecret": "foo", 246 | "sentAt": "2019-07-18T22:59:22.887526Z", 247 | "organizationId": "00000001", 248 | "organizationName": "Miles Monitoring Inc.", 249 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 250 | "networkId": "N_111111111111", 251 | "networkName": "Main Office", 252 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 253 | "deviceSerial": "XXXX-XXXX-XXXX", 254 | "deviceMac": "00:00:00:aa:bb:cc", 255 | "deviceName": "Device Foo Bar", 256 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 257 | "alertId": "0000000000000000", 258 | "alertType": "Malware download blocked", 259 | "occurredAt": "2019-07-18T15:22:53.177000Z", 260 | "alertData": {} 261 | }, 262 | "ampMalwareDetected":{ 263 | "version": "0.1", 264 | "sharedSecret": "foo", 265 | "sentAt": "2019-07-18T22:59:55.022619Z", 266 | "organizationId": "00000001", 267 | "organizationName": "Miles Monitoring Inc.", 268 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 269 | "networkId": "N_111111111111", 270 | "networkName": "Main Office", 271 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 272 | "deviceSerial": "XXXX-XXXX-XXXX", 273 | "deviceMac": "00:00:00:aa:bb:cc", 274 | "deviceName": "Device Foo Bar", 275 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 276 | "alertId": "0000000000000000", 277 | "alertType": "Malware download detected", 278 | "occurredAt": "2019-07-18T15:29:30.270000Z", 279 | "alertData": { 280 | "eventType": "amp_malware_detected", 281 | "sha256": "ddcb8f357d86d11dfa3409f71a966e5076240445ca9825fb72e7386efc5582e4", 282 | "disposition": 3 283 | } 284 | }, 285 | "newDhcpServer":{ 286 | "version": "0.1", 287 | "sharedSecret": "foo", 288 | "sentAt": "2019-07-19T00:45:47.232115Z", 289 | "organizationId": "00000001", 290 | "organizationName": "Miles Monitoring Inc.", 291 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 292 | "networkId": "N_111111111111", 293 | "networkName": "Main Office", 294 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 295 | "deviceSerial": "XXXX-XXXX-XXXX", 296 | "deviceMac": "00:00:00:aa:bb:cc", 297 | "deviceName": "Device Foo Bar", 298 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 299 | "alertId": "0000000000000000", 300 | "alertType": "New DHCP server detected", 301 | "occurredAt": "2019-07-19T00:09:17.099000Z", 302 | "alertData": { 303 | "mac": "00:00:00:33:33:33", 304 | "ip": "10.197.134.2", 305 | "vlan": 104, 306 | "subnet": "10.197.132.0/24" 307 | } 308 | }, 309 | "powerSupplyDown":{ 310 | "version": "0.1", 311 | "sharedSecret": "foo", 312 | "sentAt": "2019-07-19T01:52:43.212176Z", 313 | "organizationId": "00000001", 314 | "organizationName": "Miles Monitoring Inc.", 315 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 316 | "networkId": "N_111111111111", 317 | "networkName": "Main Office", 318 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 319 | "deviceSerial": "XXXX-XXXX-XXXX", 320 | "deviceMac": "00:00:00:aa:bb:cc", 321 | "deviceName": "Device Foo Bar", 322 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 323 | "alertId": "0000000000000000", 324 | "alertType": "Power supply went down", 325 | "occurredAt": "2019-07-18T03:50:28.344000Z", 326 | "alertData": { 327 | "num": 2 328 | } 329 | }, 330 | "rpsBackup":{ 331 | "version": "0.1", 332 | "sharedSecret": "foo", 333 | "sentAt": "2019-07-19T06:10:01.707366Z", 334 | "organizationId": "00000001", 335 | "organizationName": "Miles Monitoring Inc.", 336 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 337 | "networkId": "N_111111111111", 338 | "networkName": "Main Office", 339 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 340 | "deviceSerial": "XXXX-XXXX-XXXX", 341 | "deviceMac": "00:00:00:aa:bb:cc", 342 | "deviceName": "Device Foo Bar", 343 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 344 | "alertId": "0000000000000000", 345 | "alertType": "Running on backup power", 346 | "occurredAt": "2019-06-25T14:59:57.928000Z", 347 | "alertData": { 348 | "num": 0 349 | } 350 | }, 351 | "udldError":{ 352 | "version": "0.1", 353 | "sharedSecret": "foo", 354 | "sentAt": "2019-07-19T06:26:25.538598Z", 355 | "organizationId": "00000001", 356 | "organizationName": "Miles Monitoring Inc.", 357 | "organizationUrl": "https://n1.meraki.com/o//manage/organization/overview", 358 | "networkId": "N_111111111111", 359 | "networkName": "Main Office", 360 | "networkUrl": "https://n1.meraki.com//n//manage/nodes/list", 361 | "deviceSerial": "XXXX-XXXX-XXXX", 362 | "deviceMac": "00:00:00:aa:bb:cc", 363 | "deviceName": "Device Foo Bar", 364 | "deviceUrl": "https://n1.meraki.com//n//manage/nodes/new_list/000000000000", 365 | "alertId": "0000000000000000", 366 | "alertType": "UDLD error", 367 | "occurredAt": "2019-07-18T18:40:25.914000Z", 368 | "alertData": { 369 | "errorType": "Unidirectional link (outbound fault)", 370 | "action": "none", 371 | "port": { 372 | "port": 52, 373 | "moduleSlot": 0, 374 | "modulePid": "" 375 | } 376 | } 377 | } 378 | } -------------------------------------------------------------------------------- /merakicloudsimulator/static/locationsimulator.js: -------------------------------------------------------------------------------- 1 | // This example adds a search box to a map, using the Google Place Autocomplete 2 | // feature. People can enter geographical searches. The search box will return a 3 | // pick list containing a mix of places and predicted search terms. 4 | 5 | // This example requires the Places library. Include the libraries=places 6 | // parameter when you first load the API. For example: 7 | // 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |

Meraki Captive Portal Simulator

21 |
22 | Enter Captive Portal URL:

23 | Enter Post Authorization URL:

24 | 25 |
26 |
27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /merakicloudsimulator/templates/excapconnected.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Captive Portal Simulator 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |

Meraki Captive Portal Simulator

21 | {{full_url}} 22 |
23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /merakicloudsimulator/templates/index.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Cloud Simulations 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 | 24 | 25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /merakicloudsimulator/templates/locationscanning.html: -------------------------------------------------------------------------------- 1 | 2 | {% block body %} 3 | 4 | 5 | Meraki Location Scanning Data Simulator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |

Meraki Location Scanning Data Simulator

22 | 23 |
24 | Number of APs: 25 | Number of Clients: 26 | Enter Server URL: 27 | 28 |
29 | 30 | 31 |
32 |
33 | 35 | 36 |
37 |
38 | 39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /merakicloudsimulator/templates/locationscanningrunning.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki Location Scanning Data Simulator Running 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 |

Meraki Location Scanning Data Simulator Running

19 |
20 | 21 |
22 |

Server: {{ server_url }}
23 | # APs: {{ num_aps }}
24 | # Clients {{ num_clients }}
25 |

26 |

Location Data: {{ datavar }}

27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /merakicloudsimulator/templates/webhook.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 | 3 | 4 | Meraki WebHook Alerts Simulator 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |

Meraki Webhook Simulator

20 |
21 |

Network-wide

22 | 23 | Configuration settings are changed
24 | A VPN connection comes up or goes down
25 | A rogue AP is detected
26 | 27 |

Wireless

28 | A gateway becomes a repeater
29 | 30 |

Security appliance

31 | The primary uplink status changes
32 | The DHCP lease pool is exhausted
33 | An IP conflict is detected
34 | Cellular connection state changes
35 | A rogue DHCP server is detected
36 | A warm spare failover occurs
37 | Malware is blocked
38 | Malware is downloaded
39 | 40 |

Switch

41 | A new DHCP server is detected on the network
42 | A power supply goes down
43 | A redundant power supply is powering a switch
44 | Unidirectional link detection (UDLD) errors exist on a port
45 | 46 |

HTTPS Receiver

47 | Name: URL: Shared Secret: Make default destination:

48 | 49 |
50 |
51 | 52 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /merakicloudsimulator/webhooksimulator.py: -------------------------------------------------------------------------------- 1 | """Cisco Meraki Cloud Simulator for External Captive Portal labs.""" 2 | from merakicloudsimulator import merakicloudsimulator 3 | from merakicloudsimulator.alert_settings import alert_settings, http_servers 4 | from merakicloudsimulator.sample_alert_messages import alert_messages 5 | from merakicloudsimulator.meraki_settings import ORGANIZATIONS, NETWORKS 6 | from flask import request, render_template, redirect, jsonify, abort 7 | import string 8 | import requests 9 | import random 10 | import json 11 | from datetime import datetime 12 | from time import sleep 13 | import threading 14 | 15 | stop_post_thread = False 16 | 17 | def post_webhook_alerts(): 18 | while True: 19 | print("in big while") 20 | global stop_post_thread 21 | if stop_post_thread: 22 | print("breaking while") 23 | break 24 | for alert in alert_settings["alerts"]: 25 | if stop_post_thread: 26 | print("breaking for loop") 27 | break 28 | if alert["enabled"] == True: 29 | alert_message = alert_messages[alert["type"]] 30 | for http_server in http_servers: 31 | alert_message["sharedSecret"] = http_server["sharedSecret"] 32 | alert_message["organizationId"] = ORGANIZATIONS[0]["id"] 33 | alert_message["organizationName"] = ORGANIZATIONS[0]["name"] 34 | alert_message["networkId"] = NETWORKS[ORGANIZATIONS[0]["id"]][0]["id"] 35 | alert_message["networkName"] = NETWORKS[ORGANIZATIONS[0]["id"]][0]["name"] 36 | alert_message["alertId"] = ''.join([random.choice(string.digits) for n in range(16)]) 37 | alert_message["sentAt"] = datetime.now().isoformat(sep='T') 38 | alert_message["occurredAt"] = datetime.now().isoformat(sep='T') 39 | requests.post(http_servers[0]["url"], json=alert_message) 40 | sleep(10) 41 | 42 | 43 | post_thread = threading.Thread(target = post_webhook_alerts, daemon=True) 44 | 45 | # Helper Functions 46 | def generate_fake_http_server_id(): 47 | """Generate a fake http_server_id.""" 48 | return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(36)]) 49 | 50 | 51 | # Flask micro-webservice API/URI endpoints 52 | @merakicloudsimulator.route("/networks//httpServers", methods=["GET"]) 53 | def get_http_servers(network_id): 54 | """Simulate getting httpServers configurations.""" 55 | print(f"Getting httpServers for {network_id}.") 56 | return jsonify(http_servers) 57 | 58 | @merakicloudsimulator.route("/networks//httpServers", methods=["POST"]) 59 | def post_httpServers(network_id): 60 | """Simulate setting httpServers configurations.""" 61 | print(f"Settings updated for network {network_id}.") 62 | new_server = request.json 63 | new_server_keys = new_server.keys() 64 | if "name" in new_server_keys and "url" in new_server_keys and "sharedSecret" in new_server_keys: 65 | new_server["id"] = generate_fake_http_server_id() 66 | new_server["networkId"] = network_id 67 | http_servers.append(new_server) 68 | return jsonify(new_server) 69 | else: 70 | abort(400) 71 | 72 | 73 | @merakicloudsimulator.route("/networks//alertSettings", 74 | methods=["GET"], 75 | ) 76 | def get_alert_settings(network_id): 77 | """Simulate getting alertSettings configurations.""" 78 | print(f"Getting alertSettings for {network_id}.") 79 | return jsonify(alert_settings) 80 | 81 | @merakicloudsimulator.route("/networks//alertSettings", 82 | methods=["PUT"], 83 | ) 84 | def put_alert_settings(network_id): 85 | global post_thread 86 | global stop_post_thread 87 | 88 | destination_set = False 89 | alert_set = False 90 | """Simulate setting alertSettings configurations.""" 91 | print(f"Setting alertSettings for {network_id}.") 92 | new_settings = request.json 93 | new_settings_keys = new_settings.keys() 94 | if "defaultDestinations" in new_settings_keys or "alerts" in new_settings_keys: 95 | if "defaultDestinations" in new_settings_keys: 96 | defaultDestinations_keys = new_settings["defaultDestinations"].keys() 97 | if "httpServerIds" in defaultDestinations_keys: 98 | alert_settings["defaultDestinations"]["httpServerIds"].clear() 99 | if len(new_settings["defaultDestinations"]["httpServerIds"]) > 0: 100 | alert_settings["defaultDestinations"]["httpServerIds"].append(new_settings["defaultDestinations"]["httpServerIds"][0]) 101 | destination_set = True 102 | else: 103 | abort(400) 104 | if "alerts" in new_settings_keys: 105 | for new_alert in new_settings["alerts"]: 106 | alert_keys = new_alert.keys() 107 | if "enabled" in alert_keys and "type" in alert_keys: 108 | alert_index = next((index for (index, alert) in enumerate(alert_settings["alerts"]) if alert["type"] == new_alert["type"]), None) 109 | alert_settings["alerts"][alert_index] = new_alert 110 | alert_set = True 111 | else: 112 | abort(400) 113 | else: 114 | abort(400) 115 | 116 | if destination_set and alert_set: 117 | print("destination set and alert set") 118 | if not post_thread.isAlive(): 119 | print("posting thread not started, starting") 120 | post_thread.start() 121 | else: 122 | print("posting thread already started, killing an restarting") 123 | stop_post_thread = True 124 | post_thread.join() 125 | print('post_thread killed') 126 | stop_post_thread = False 127 | post_thread = threading.Thread(target = post_webhook_alerts, daemon=True) 128 | post_thread.start() 129 | 130 | return jsonify(alert_settings) 131 | 132 | 133 | @merakicloudsimulator.route("/webhook", methods=["POST","GET"]) 134 | def webhooksettings(): 135 | global post_thread 136 | global stop_post_thread 137 | 138 | if request.method == 'POST': 139 | webhook_server_name = request.form["server_name"] 140 | webhook_server_url = request.form["server_url"] 141 | webhook_shared_secret = request.form["shared_secret"] 142 | webhook_default_destination = request.form.getlist("default_destination") 143 | 144 | if webhook_shared_secret != "" and webhook_server_name != "" and webhook_server_url != "": 145 | http_servers.clear() 146 | http_servers.append({ 147 | "name": webhook_server_name, 148 | "url": webhook_server_url, 149 | "sharedSecret": webhook_shared_secret, 150 | "id": generate_fake_http_server_id(), 151 | "networkId": NETWORKS[ORGANIZATIONS[0]["id"]][0]["id"] 152 | }) 153 | print(f"Alert webhook receiver added: \n{http_servers}") 154 | 155 | if len(webhook_default_destination) > 0 and len(http_servers) > 0: 156 | alert_settings["defaultDestinations"]["httpServerIds"].clear() 157 | alert_settings["defaultDestinations"]["httpServerIds"].\ 158 | append(http_servers[0]["id"]) 159 | print(f"Alert Destination Changed in GUI") 160 | 161 | webhook_checked_settings = request.form.getlist("checked_settings") 162 | for alert in alert_settings['alerts']: 163 | if alert["type"] in webhook_checked_settings: 164 | alert["enabled"] = True 165 | else: 166 | alert["enabled"] = False 167 | 168 | print(f"Alert Settings Changed in GUI: {alert_settings['alerts']}") 169 | if len(webhook_default_destination) > 0 and len(http_servers) > 0 and len(webhook_checked_settings) > 0: 170 | if not post_thread.isAlive(): 171 | print("posting thread not started, starting") 172 | post_thread.start() 173 | else: 174 | print("posting thread already started, killing an restarting") 175 | stop_post_thread = True 176 | post_thread.join() 177 | print('post_thread killed') 178 | stop_post_thread = False 179 | post_thread = threading.Thread(target = post_webhook_alerts, daemon=True) 180 | post_thread.start() 181 | else: 182 | if len(http_servers) > 0: 183 | webhook_server_name = http_servers[0]["name"] 184 | webhook_server_url = http_servers[0]["url"] 185 | webhook_shared_secret = http_servers[0]["sharedSecret"] 186 | else: 187 | webhook_server_name = "" 188 | webhook_server_url = "" 189 | webhook_shared_secret = "" 190 | 191 | print(alert_settings["alerts"]) 192 | webhook_checked_settings = [] 193 | for alert in alert_settings["alerts"]: 194 | if alert["enabled"]: 195 | webhook_checked_settings.append(alert["type"]) 196 | 197 | webhook_default_destination = [] 198 | if len(alert_settings["defaultDestinations"]["httpServerIds"]) > 0: 199 | webhook_default_destination.append("default_destination") 200 | 201 | 202 | return render_template("webhook.html", \ 203 | checked_settings=webhook_checked_settings, server_name=webhook_server_name, \ 204 | server_url=webhook_server_url, shared_secret=webhook_shared_secret, \ 205 | default_destination=webhook_default_destination) 206 | 207 | 208 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | requests -------------------------------------------------------------------------------- /wt_vars.env: -------------------------------------------------------------------------------- 1 | WT_ACCESS_TOKEN= 2 | WT_ROOM_ID= --------------------------------------------------------------------------------