diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..5c0c8d4
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,53 @@
+name: Docker Build & Publish
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ tags:
+ - '*'
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+
+jobs:
+ push:
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Docker buildx
+ uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
+
+ - name: Log into registry ${{ env.REGISTRY }}
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+ - name: Build and push Docker image
+ id: build-and-push
+ uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
+ with:
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc0d0ef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+.venv/
+.env
+.DS_Store
+*.pyc
+__pycache__/
+
+instance/
+
+.pytest_cache/
+.coverage
+htmlcov/
+
+dist/
+build/
+*.egg-info/
+
+.idea/*
+.idea/misc.xml
+
+data/temp.py
+*.iml
+
+.idea/misc.xml
+.idea/gve_devnet_meraki_seamless_sea_ssid.iml
+data/temp.py
+*.log
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..b86d304
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,45 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as maintainers of this Cisco Sample Code pledge to making participation with our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Showing empathy towards other people
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other interactions with this project that are not aligned to this Code of Conduct, or to ban temporarily or permanently any person for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project. Examples of representing a project include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Cisco SE GitHub team at ciscose-github@cisco.com. The team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project or Cisco SE Leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8370d5e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,11 @@
+# Cisco Sample Code
+
+This project, and the code contained herein, is provided for example and/or demonstration purposes by Cisco for use by our partners and customers in working with Cisco's products and services. While Cisco's customers and partners are free to use this code pursuant to the terms set forth in the [LICENSE][LICENSE], this is not an Open Source project as we are not seeking to build a community around this project and its capabilities.
+
+
+We do desire to provide functional and high-quality examples and demonstrations. If you should discover some bug, issue, or opportunity for enhancement with the code contained in this project, please do notify us by:
+
+1. **Reviewing Open Issues** to verify that the issue hasn't already been reported.
+2. **Opening a New Issue** to report the bug, issue, or enhancement opportunity.
+
+[LICENSE]: ../LICENSE
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7fcd78f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,8 @@
+FROM python:3.11-slim-buster
+WORKDIR /app
+COPY ./requirements.txt /app
+RUN pip install -r requirements.txt
+# Copy the current directory contents into the container at /app
+COPY ./src/meraki_seamless_sea_ssid/ .
+EXPOSE 8000
+CMD ["python", "./app.py"]
diff --git a/IMAGES/0image.png b/IMAGES/0image.png
new file mode 100644
index 0000000..1244d96
Binary files /dev/null and b/IMAGES/0image.png differ
diff --git a/IMAGES/app_startup.png b/IMAGES/app_startup.png
new file mode 100644
index 0000000..baeeeee
Binary files /dev/null and b/IMAGES/app_startup.png differ
diff --git a/IMAGES/boats_csv.png b/IMAGES/boats_csv.png
new file mode 100644
index 0000000..5c5459b
Binary files /dev/null and b/IMAGES/boats_csv.png differ
diff --git a/IMAGES/disable_ssid.png b/IMAGES/disable_ssid.png
new file mode 100644
index 0000000..85c88de
Binary files /dev/null and b/IMAGES/disable_ssid.png differ
diff --git a/IMAGES/enable_ssid.png b/IMAGES/enable_ssid.png
new file mode 100644
index 0000000..caf8806
Binary files /dev/null and b/IMAGES/enable_ssid.png differ
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..f915e03
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,81 @@
+CISCO SAMPLE CODE LICENSE
+ Version 1.1
+ Copyright (c) 2023 Cisco and/or its affiliates
+
+ These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo
+ source code and its associated documentation (together, the "Sample
+ Code"). By downloading, copying, modifying, compiling, or redistributing
+ the Sample Code, you accept and agree to be bound by the following terms
+ and conditions (the "License"). If you are accepting the License on
+ behalf of an entity, you represent that you have the authority to do so
+ (either you or the entity, "you"). Sample Code is not supported by Cisco
+ TAC and is not tested for quality or performance. This is your only
+ license to the Sample Code and all rights not expressly granted are
+ reserved.
+
+ 1. LICENSE GRANT: Subject to the terms and conditions of this License,
+ Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non-
+ transferable, non-sublicensable, royalty-free license to copy and
+ modify the Sample Code in source code form, and compile and
+ redistribute the Sample Code in binary/object code or other executable
+ forms, in whole or in part, solely for use with Cisco products and
+ services. For interpreted languages like Java and Python, the
+ executable form of the software may include source code and
+ compilation is not required.
+
+ 2. CONDITIONS: You shall not use the Sample Code independent of, or to
+ replicate or compete with, a Cisco product or service. Cisco products
+ and services are licensed under their own separate terms and you shall
+ not use the Sample Code in any way that violates or is inconsistent
+ with those terms (for more information, please visit:
+ www.cisco.com/go/terms).
+
+ 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample
+ Code, including all intellectual property rights therein, except with
+ respect to any third-party material that may be used in or by the
+ Sample Code. Any such third-party material is licensed under its own
+ separate terms (such as an open source license) and all use must be in
+ full accordance with the applicable license. This License does not
+ grant you permission to use any trade names, trademarks, service
+ marks, or product names of Cisco. If you provide any feedback to Cisco
+ regarding the Sample Code, you agree that Cisco, its partners, and its
+ customers shall be free to use and incorporate such feedback into the
+ Sample Code, and Cisco products and services, for any purpose, and
+ without restriction, payment, or additional consideration of any kind.
+ If you initiate or participate in any litigation against Cisco, its
+ partners, or its customers (including cross-claims and counter-claims)
+ alleging that the Sample Code and/or its use infringe any patent,
+ copyright, or other intellectual property right, then all rights
+ granted to you under this License shall terminate immediately without
+ notice.
+
+ 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION
+ WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR
+ DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL,
+ AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION,
+ PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGES.
+
+ 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES
+ ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT
+ WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY
+ LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND
+ WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR
+ CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-
+ INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY,
+ ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT
+ WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL
+ USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT
+ ERROR OR DEFECT.
+
+ 6. GENERAL: This License shall be governed by and interpreted in
+ accordance with the laws of the State of California, excluding its
+ conflict of laws provisions. You agree to comply with all applicable
+ United States export laws, rules, and regulations. If any provision of
+ this License is judged illegal, invalid, or otherwise unenforceable,
+ that provision shall be severed and the rest of the License shall
+ remain in full force and effect. No failure by Cisco to enforce any of
+ its rights related to the Sample Code or to a breach of this License
+ in a particular situation will act as a waiver of such rights. In the
+ event of any inconsistencies with any other terms, this License shall
+ take precedence.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..42f6810
--- /dev/null
+++ b/README.md
@@ -0,0 +1,110 @@
+# gve_devnet_meraki_seamless_sea_ssid
+This project presents an advanced solution for automating Cisco Meraki Wi-Fi networks, specifically
+designed to facilitate seamless sea-to-dock roaming with dynamic VLAN management. It leverages the
+power of the Meraki Dashboard API to provide real-time network configuration adjustments based on
+device connectivity, ensuring optimal network performance and user experience in maritime environments.
+
+## Contacts
+* Mark Orszycki
+
+## Solution Components
+* Meraki MS
+* Merak MR
+* Meraki API
+* Python
+* FastAPI
+
+
+## Prerequisites
+#### Meraki API Keys
+In order to use the Meraki API, you need to enable the API for your organization first. After enabling API access,
+you can generate an API key. Follow these instructions to enable API access and generate an API key:
+1. Login to the Meraki dashboard
+2. In the left-hand menu, navigate to `Organization > Settings > Dashboard API access`
+3. Click on `Enable access to the Cisco Meraki Dashboard API`
+4. Go to `My Profile > API access`
+5. Under API access, click on `Generate API key`
+6. Save the API key in a safe place. The API key will only be shown once for security purposes, so it is very important to take note of the key then. In case you lose the key, then you have to revoke the key and a generate a new key. Moreover, there is a limit of only two API keys per profile.
+
+> For more information on how to generate an API key, please click [here](https://developer.cisco.com/meraki/api-v1/#!authorization/authorization).
+
+> Note: You can add your account as Full Organization Admin to your organizations by following the instructions [here](https://documentation.meraki.com/General_Administration/Managing_Dashboard_Access/Managing_Dashboard_Administrators_and_Permissions).
+
+
+## Installation/Configuration
+1. Clone this repository with `git clone https://github.com/gve-sw/gve_devnet_meraki_seamless_sea_ssid.git`.
+2. Update .env with the following variables:
+ ```script
+ CSV_FILE = '/data/boats.csv'
+ MERAKI_BASE_URL=https://api.meraki.com/api/v1/
+ MERAKI_API_KEY=YOUR_MERAKI_API_KEY
+ MERAKI_NETWORK_ID=YOUR_MERAKI_NETWORK_ID
+ APP_NAME='Meraki Seamless Sea SSID'
+ UVICORN_LOG_LEVEL=warning
+ MY_PSK=YOUR_PSK
+ MERAKI_SSID_NAME=YOUR_SSID_NAME
+ ```
+3. Retrieving your Meraki Network ID:
+This project includes a setup.py script to assist in obtaining your Meraki Network ID. The script uses
+functions from meraki_funcs to interact with the Meraki API and save your network ID to the .env file.
+To get your Network_ID, navigate to src/meraki_seamless_sea_ssid and run:
+```script
+python3 setup.py
+```
+Follow the prompts to select your organization and network.
+The script will automatically update your .env file with the MERAKI_NETWORK_ID.
+
+### Webhook Configuration
+1. Run ngrok http 8000 to expose your local server.
+2. Create a Webhook in the Meraki dashboard (Network-wide > Alerts > Webhooks).
+3. Set the URL to your ngrok URL (ngrok_url/webhook).
+4. Configure alert settings to use the webhook for relevant events.
+
+### boats.csv file
+Input data for appropriately for each boat in boats.csv:
+![/IMAGES/boats_csv.png](/IMAGES/boats_csv.png)
+
+## Usage
+To run locally the program, use the command:
+```script
+uvicorn app:app --reload
+```
+
+To run locally the program & silence logs:
+```script
+uvicorn app:app --log-level warning
+```
+
+To run with Docker:
+```script
+With Docker: docker-compose up
+```
+
+# Screenshots
+Application Startup:
+![/IMAGES/app_startup.png](/IMAGES/app_startup.png)
+
+Enabling SSID upon webhook:
+![/IMAGES/enable_ssid.png](/IMAGES/enable_ssid.png)
+
+Disabling SSID upon webhook:
+![/IMAGES/disable_ssid.png](/IMAGES/disable_ssid.png)
+
+![/IMAGES/0image.png](/IMAGES/0image.png)
+
+
+### LICENSE
+
+Provided under Cisco Sample Code License, for details see [LICENSE](LICENSE.md)
+
+### CODE_OF_CONDUCT
+
+Our code of conduct is available [here](CODE_OF_CONDUCT.md)
+
+### CONTRIBUTING
+
+See our contributing guidelines [here](CONTRIBUTING.md)
+
+#### DISCLAIMER:
+Please note: This script is meant for demo purposes only. All tools/ scripts in this repo are released for use "AS IS" without any warranties of any kind, including, but not limited to their installation, use, or performance. Any use of these scripts and tools is at your own risk. There is no guarantee that they have been through thorough testing in a comparable environment and we are not responsible for any damage or data loss incurred with their use.
+You are responsible for reviewing and testing any scripts you run thoroughly before use in any non-testing environment.
\ No newline at end of file
diff --git a/data/boats.csv b/data/boats.csv
new file mode 100644
index 0000000..6834924
--- /dev/null
+++ b/data/boats.csv
@@ -0,0 +1,2 @@
+mac_address,ssid_number,vlan
+BOAT_MAC_ADDRESS, BOAT_SSID_NUMBER, BOAT_VLAN_NUMBER
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..8fb9957
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3.5"
+
+services:
+ gve_devnet_meraki_seamless_sea_ssid:
+ image: ghcr.io/gve-sw/gve_devnet_meraki_seamless_sea_ssid:latest
+ container_name: gve_devnet_meraki_seamless_sea_ssid
+ environment:
+ - APP_NAME= ${APP_NAME}
+ - CSV_FILE = ${CSV_FILE}
+ - MERAKI_BASE_URL = ${MERAKI_BASE_URL}
+ - MERAKI_API_KEY = ${MERAKI_API_KEY}
+ - UVICORN_LOG_LEVEL = ${UVICORN_LOG_LEVEL}
+# volumes:
+# - config.yaml:/app/config.yaml
+ restart: "always"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..edbc98c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,21 @@
+annotated-types==0.6.0
+anyio==3.7.1
+certifi==2023.11.17
+charset-normalizer==3.3.2
+click==8.1.7
+fastapi==0.104.1
+h11==0.14.0
+idna==3.4
+markdown-it-py==3.0.0
+mdurl==0.1.2
+pydantic==2.5.1
+pydantic_core==2.14.3
+Pygments==2.17.1
+python-dotenv==1.0.0
+requests==2.31.0
+rich==13.7.0
+sniffio==1.3.0
+starlette==0.27.0
+typing_extensions==4.8.0
+urllib3==2.1.0
+uvicorn==0.24.0.post1
diff --git a/src/meraki_seamless_sea_ssid/__init__.py b/src/meraki_seamless_sea_ssid/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/meraki_seamless_sea_ssid/app.py b/src/meraki_seamless_sea_ssid/app.py
new file mode 100644
index 0000000..b7e3432
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/app.py
@@ -0,0 +1,44 @@
+"""
+Copyright (c) 2023 Cisco and/or its affiliates.
+This software is licensed to you under the terms of the Cisco Sample
+Code License, Version 1.1 (the "License"). You may obtain a copy of the
+License at
+ https://developer.cisco.com/docs/licenses
+All use of the material herein must be in accordance with the terms of
+the License. All rights not expressly granted by the License are
+reserved. Unless required by applicable law or agreed to separately in
+writing, software distributed under the License is distributed on an "AS
+IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+or implied.
+"""
+from fastapi import FastAPI
+import uvicorn
+from routes import router as webhook_router
+from funcs import print_start_panel, print_exit_panel
+from meraki_funcs import *
+from config import Config
+
+
+def create_app() -> FastAPI:
+ fastapi_app = FastAPI()
+
+ @fastapi_app.on_event("startup")
+ async def on_startup():
+ print_start_panel()
+ Config.validate_env_variables()
+
+ # Initialize Meraki Dashboard
+ fastapi_app.state.meraki_dashboard = get_meraki_dashboard()
+
+ @fastapi_app.on_event("shutdown")
+ async def on_shutdown():
+ print_exit_panel()
+
+ fastapi_app.include_router(webhook_router)
+ return fastapi_app
+
+
+app = create_app()
+
+if __name__ == "__main__":
+ uvicorn.run("app:app", host="0.0.0.0", port=8000, log_level="warning", reload=True)
diff --git a/src/meraki_seamless_sea_ssid/config.py b/src/meraki_seamless_sea_ssid/config.py
new file mode 100644
index 0000000..d2f32bf
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/config.py
@@ -0,0 +1,78 @@
+import os
+import json
+from rich.console import Console
+from rich.table import Table
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+class Config:
+ """
+ The EnvironmentManager class is responsible for loading and validating the necessary environment variables
+ that the app relies on.
+
+ Attributes:
+
+
+ Methods:
+ validate_env_variables() - Validates that all required environment variables are set,
+ ignoring attributes related to the class internals or the os module.
+ """
+
+ # Construct the absolute path for dir
+ dir_path = os.path.dirname(os.path.realpath(__file__))
+
+ # Define mandatory environment variables
+ MANDATORY_ENV_VARS = ['MERAKI_API_KEY', 'MERAKI_BASE_URL']
+
+ # Construct the absolute path for boats.csv
+ CSV_FILE = os.path.join(dir_path, '..', '..', 'data', 'boats.csv')
+
+ MERAKI_API_KEY = os.getenv('MERAKI_API_KEY')
+ MERAKI_BASE_URL = os.getenv('MERAKI_BASE_URL')
+ MERAKI_NETWORK_ID = os.getenv('MERAKI_NETWORK_ID')
+
+ LOGGER_LEVEL = os.getenv('LOGGER_LEVEL', '').upper() or 'DEBUG'
+ MERAKI_SSID_NAME = os.getenv('MERAKI_SSID_NAME')
+ MY_PSK = os.getenv('MY_PSK')
+
+ APP_NAME = os.environ.get('APP_NAME') or "Update your APP_NAME in .env"
+
+ # For Handling SOME_INTEGER with a default value and error check (for integer type).
+ # try:
+ # SOME_INTEGER = int(os.getenv('SOME_INTEGER', '100')) # Default to 100 seconds if left blank
+ # except ValueError:
+ # TIMESPAN_IN_SECONDS = 100 # Default to 100 seconds if the provided value is invalid, so it won't break program.
+
+
+ @classmethod
+ def handle_error(cls, error_message):
+ """Handles errors by printing an error message and exiting the program."""
+ console = Console()
+ console.print(f"[bold red]Error:[/bold red] {error_message}", highlight=False)
+ exit(1)
+
+ @classmethod
+ def validate_env_variables(cls):
+ missing_vars = []
+ console = Console() # Instantiate a console object for rich
+
+ table = Table(title="Environment Variables")
+ table.add_column("Variable", justify="left", style="bright_white", width=30)
+ table.add_column("Value", style="bright_white", width=60)
+
+ for var_name, var_value in cls.__dict__.items():
+ if "os" in var_name or "__" in var_name or isinstance(var_value, classmethod) or var_name == 'MANDATORY_ENV_VARS':
+ continue
+ # Check if mandatory variables are set. if var_name in cls.MANDATORY_ENV_VARS and not var_value: Add variable to the table
+ table.add_row(var_name, str(var_value) if var_value not in [None, ""] else "Not Set")
+ if var_name in cls.MANDATORY_ENV_VARS and var_value in [None, ""]:
+ missing_vars.append(var_name)
+
+ # Display the table
+ console.print(table)
+
+ if missing_vars:
+ cls.handle_error(f"The following environment variables are not set: {', '.join(missing_vars)}")
diff --git a/src/meraki_seamless_sea_ssid/funcs.py b/src/meraki_seamless_sea_ssid/funcs.py
new file mode 100644
index 0000000..afa476c
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/funcs.py
@@ -0,0 +1,37 @@
+import csv
+from rich.console import Console
+from rich.panel import Panel
+from config import Config
+import sys
+
+# Constants; set in .env
+CSV_FILE = Config.CSV_FILE
+
+# Rich console
+console = Console()
+
+
+def signal_handler(sig, frame):
+ # Implement any clean-up tasks that may be necessary, i.e. closing files or database connections.
+ print_exit_panel()
+ sys.exit(0) # Exit the app gracefully
+
+
+def print_start_panel(app_name=Config.APP_NAME):
+ console.print(Panel.fit(f"[bold bright_green]{app_name}[/bold bright_green]"))
+
+
+def print_exit_panel():
+ console.print("\n")
+ console.print(Panel.fit("Shutting down...", title="[bright_red]Exit[/bright_red]"))
+
+
+def find_boat_in_csv(mac_address):
+ with open(CSV_FILE, mode='r') as file:
+ reader = csv.DictReader(file)
+ for row in reader:
+ if row['mac_address'] == mac_address:
+ return row['ssid_number'], row['vlan']
+ return None, None
+
+
diff --git a/src/meraki_seamless_sea_ssid/meraki_funcs.py b/src/meraki_seamless_sea_ssid/meraki_funcs.py
new file mode 100644
index 0000000..cd6c280
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/meraki_funcs.py
@@ -0,0 +1,152 @@
+from rich.prompt import Prompt
+from rich.panel import Panel
+from meraki.exceptions import APIError
+import meraki
+from rich.console import Console
+from config import Config
+import sys
+from funcs import find_boat_in_csv
+
+console = Console()
+
+# Constants; set in .env
+MERAKI_API_KEY = Config.MERAKI_API_KEY
+MERAKI_BASE_URL = Config.MERAKI_BASE_URL
+MERAKI_SSID_NAME = Config.MERAKI_SSID_NAME
+
+
+def get_meraki_dashboard():
+ try:
+ dashboard = meraki.DashboardAPI(api_key=Config.MERAKI_API_KEY, suppress_logging=False)
+ return dashboard
+ except Exception as e:
+ console.print(f"[bold red]Error initializing Meraki Dashboard: {e}[/bold red]")
+ sys.exit(1)
+
+
+def get_org_id(dashboard):
+ """
+ Fetch the org ID based on org name, or prompt the user to select
+ an organization if the name is left blank or is invalid. If there is only one
+ organization, it selects that organization automatically. Exits the script if
+ the organization is not found or if there's an error fetching the organizations.
+ """
+
+ with console.status("[bold green]Fetching Meraki Organizations....", spinner="dots"):
+ try:
+ orgs = dashboard.organizations.getOrganizations()
+ except APIError as e:
+ console.print(f"Failed to fetch organizations. Error: {e.message['errors'][0]}")
+ sys.exit(1)
+
+ console.print("[bold bright_green]Connected to Meraki dashboard!")
+ print(f"Found {len(orgs)} organization(s).")
+
+ # If one org, return early
+ if len(orgs) == 1:
+ print(f"Working with Org: {orgs[0]['name']}\n")
+ return orgs[0]["id"]
+ org_names = [org["name"] for org in orgs]
+ print("Available organizations:")
+ for org in orgs:
+ console.print(f"- {org['name']}")
+ console.print("[bold red]\nNote: Meraki organization names are case sensitive")
+ selection = Prompt.ask(
+ "Which organization should we use?", choices=org_names, show_choices=False
+ )
+ organization_name = selection # Update organization_name with the user's selection
+
+ for org in orgs:
+ if org["name"] == organization_name:
+ return org["id"]
+
+ console.print(f"[bold red]Organization with name '{organization_name}' not found.[/bold red]")
+ exit(1)
+
+
+def get_networks_in_org(dashboard, org_id, product_type=None):
+ """
+ Collect existing Meraki network names / IDs
+ """
+ console.print(Panel.fit("[bold bright_green]Retrieving Network(s) Information[/bold bright_green]", title="Step 3"))
+ # Fetching the networks before applying any filter.
+ try:
+ response = dashboard.organizations.getOrganizationNetworks(organizationId=org_id)
+ except Exception as e: # Handle exception for API call
+ console.print(f"[bold red]Failed to retrieve networks: {str(e)}[/bold red]")
+ sys.exit(1)
+
+ if product_type:
+ # Filter networks by product type
+ response = [network for network in response if product_type in network['productTypes']]
+
+ print(f"Found {len(response)} network(s).")
+ return response
+
+
+def setup_meraki_network(dashboard, ssid_number, meraki_network_id, psk, vlan):
+ """
+ Set up a Meraki network with the given SSID and VLAN.
+ """
+ change_ssid_status(dashboard, ssid_number, meraki_network_id, True, psk, vlan)
+
+
+def teardown_meraki_network(dashboard, ssid_number, meraki_network_id, psk):
+ """
+ Tear down a Meraki network with the given SSID.
+ """
+ change_ssid_status(dashboard, ssid_number, meraki_network_id, False, psk)
+
+
+def change_ssid_status(dashboard, ssid_number, meraki_network_id, status, psk=None, vlan=None):
+ """
+ Change the status of a specific SSID.
+ """
+ ssid_payload = {
+ 'name': f"{MERAKI_SSID_NAME}",
+ 'enabled': status,
+ 'authMode': "psk" if status else "open",
+ 'vlan': vlan if status else None
+ }
+
+ if status:
+ ssid_payload['psk'] = psk
+ ssid_payload['encryptionMode'] = 'wpa' # or 'wpa2', depending on your requirements
+
+ try:
+ response = dashboard.wireless.updateNetworkWirelessSsid(meraki_network_id, ssid_number, **ssid_payload)
+ console.print(f"[bright_green]Successfully {'enabled' if status else 'disabled'} SSID settings for {ssid_number}[/bright_green]")
+ except Exception as e:
+ console.print(f"[red]Failed to {'enable' if status else 'disable'} SSID settings for {ssid_number}. Error: {e}[/red]")
+
+
+async def handle_webhook(dashboard, data):
+ print("Webhook received:", data)
+
+ # Extract MAC address and connection status from the webhook data
+ mac_address = data.get('alertData', {}).get('mac')
+ is_connected = data.get('alertData', {}).get('connected') == 'true'
+
+ # is_connected = True # For testing webhook. Uncomment to enable SSID, comment out to disable SSID.
+ mac_address = "test_mac_address" # For testing webhook. This should match CSV file. Won't need later.
+
+ meraki_network_id = Config.MERAKI_NETWORK_ID # using NETWORK_ID from .env
+ psk = Config.MY_PSK
+
+ print(f'Using Meraki network id: {meraki_network_id}')
+
+ if mac_address:
+ ssid_number, vlan = find_boat_in_csv(mac_address)
+ print(f'SSID_NUMBER ME: {ssid_number}')
+ print(f'VLAN {vlan}')
+ if ssid_number and vlan:
+ if is_connected:
+ setup_meraki_network(dashboard, ssid_number, meraki_network_id, psk, vlan)
+ return {"message": f"Network setup for MAC {mac_address}"}
+ else:
+ teardown_meraki_network(dashboard, ssid_number, meraki_network_id, psk)
+ return {"message": f"Network teardown for MAC {mac_address}"}
+ else:
+ return {"message": f"No matching boat found for MAC address {mac_address}"}
+ else:
+ return {"message": "MAC address not found in webhook data"}
diff --git a/src/meraki_seamless_sea_ssid/routes.py b/src/meraki_seamless_sea_ssid/routes.py
new file mode 100644
index 0000000..2fa12bc
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/routes.py
@@ -0,0 +1,11 @@
+from fastapi import APIRouter, Request
+from meraki_funcs import handle_webhook
+
+router = APIRouter()
+
+
+@router.post("/webhook")
+async def webhook_handler(request: Request) -> object:
+ data = await request.json()
+ dashboard = request.app.state.meraki_dashboard
+ return await handle_webhook(dashboard, data)
diff --git a/src/meraki_seamless_sea_ssid/setup.py b/src/meraki_seamless_sea_ssid/setup.py
new file mode 100644
index 0000000..b95e934
--- /dev/null
+++ b/src/meraki_seamless_sea_ssid/setup.py
@@ -0,0 +1,80 @@
+"""
+Copyright (c) 2023 Cisco and/or its affiliates.
+This software is licensed to you under the terms of the Cisco Sample
+Code License, Version 1.1 (the "License"). You may obtain a copy of the
+License at
+ https://developer.cisco.com/docs/licenses
+All use of the material herein must be in accordance with the terms of
+the License. All rights not expressly granted by the License are
+reserved. Unless required by applicable law or agreed to separately in
+writing, software distributed under the License is distributed on an "AS
+IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+or implied.
+"""
+
+from meraki_funcs import get_meraki_dashboard, get_org_id, get_networks_in_org
+import os
+
+
+def save_network_id_to_env(network_id):
+ # Path to the .env file located two directories up
+ env_file = os.path.join('..', '..', '.env')
+
+ # Read the existing content of the .env file
+ if os.path.exists(env_file):
+ with open(env_file, 'r') as file:
+ lines = file.readlines()
+
+ # Update or add the MERAKI_NETWORK_ID variable
+ updated = False
+ for i, line in enumerate(lines):
+ if line.startswith('MERAKI_NETWORK_ID='):
+ lines[i] = f'MERAKI_NETWORK_ID={network_id}\n'
+ updated = True
+ break
+
+ if not updated:
+ lines.append(f'MERAKI_NETWORK_ID={network_id}\n')
+
+ # Write the updated content back to the .env file
+ with open(env_file, 'w') as file:
+ file.writelines(lines)
+ else:
+ # If the .env file doesn't exist, create it with the network ID
+ with open(env_file, 'w') as file:
+ file.write(f'MERAKI_NETWORK_ID={network_id}\n')
+
+ print(f"Network ID saved to {env_file}: {network_id}")
+
+
+def main():
+ dashboard = get_meraki_dashboard()
+ org_id = get_org_id(dashboard)
+
+ if org_id:
+ meraki_networks = get_networks_in_org(dashboard, org_id)
+ if meraki_networks:
+ # Display networks and let the user choose
+ for index, network in enumerate(meraki_networks):
+ print(f"{index + 1}. {network['name']} (ID: {network['id']})")
+
+ while True:
+ choice = input("Enter the number of the network you want to use: ")
+ try:
+ choice_index = int(choice) - 1
+ if 0 <= choice_index < len(meraki_networks):
+ selected_network_id = meraki_networks[choice_index]['id']
+ save_network_id_to_env(selected_network_id)
+ break
+ else:
+ print("Invalid selection. Please enter a number from the list.")
+ except ValueError:
+ print("Invalid input. Please enter a numeric value.")
+ else:
+ print("No networks found.")
+ else:
+ print("Organization ID not found.")
+
+
+if __name__ == "__main__":
+ main()