├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── dependabot.yml ├── images │ ├── colab-notebook-colab_env-import-Annotation_2020-08-05_163815.png │ ├── colab-notebook-colab_env-import-new-instance_Annotation_2020-08-05_163942.png │ ├── colab-notebook-new-cell-Annotation_2020-08-05_123920.png │ ├── colab-notebook-run-cell-Annotation_2020-08-05_143202.png │ ├── colab-notebook-selection-Annotation_2020-08-05_120229.png │ └── colab-notebook-warning-run-cell-Annotation_2020-08-05_143410.png └── workflows │ ├── codeql-analysis.yml │ ├── lint-library-generator.yml │ ├── publish-release.yml │ ├── regenerate-library.yml │ └── test-library.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── aio_get_pages_iterator.py ├── aio_ips2firewall_v1.py ├── aio_org_wide_clients_v1.py ├── apiData2CSV_v1.py ├── bulk_firmware_upgrade_manager.py ├── find_early_access_operations.py ├── get_pages_iterator.py ├── local_subnet_dumper.py ├── merakiApplianceVlanToL3SwitchInterfaceMigrator │ ├── mappings.json │ ├── merakiApplianceVlanToL3SwitchInterfaceMigrator.py │ └── settings.json ├── org_wide_clients_v1.py ├── organization_deleter.py └── wireless_rf_profiles_overview.py ├── generator ├── async_class_template.jinja2 ├── async_function_template.jinja2 ├── batch_class_template.jinja2 ├── batch_function_template.jinja2 ├── class_template.jinja2 ├── common.py ├── function_template.jinja2 ├── generate_library.py ├── generate_library_oasv2.py ├── generate_snippets.py └── readme.md ├── meraki ├── __init__.py ├── aio │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── administered.py │ │ ├── appliance.py │ │ ├── camera.py │ │ ├── campusGateway.py │ │ ├── cellularGateway.py │ │ ├── devices.py │ │ ├── insight.py │ │ ├── licensing.py │ │ ├── networks.py │ │ ├── organizations.py │ │ ├── sensor.py │ │ ├── sm.py │ │ ├── spaces.py │ │ ├── switch.py │ │ ├── wireless.py │ │ └── wirelessController.py │ └── rest_session.py ├── api │ ├── __init__.py │ ├── administered.py │ ├── appliance.py │ ├── batch │ │ ├── __init__.py │ │ ├── administered.py │ │ ├── appliance.py │ │ ├── camera.py │ │ ├── campusGateway.py │ │ ├── cellularGateway.py │ │ ├── devices.py │ │ ├── insight.py │ │ ├── licensing.py │ │ ├── networks.py │ │ ├── organizations.py │ │ ├── sensor.py │ │ ├── sm.py │ │ ├── spaces.py │ │ ├── switch.py │ │ ├── wireless.py │ │ └── wirelessController.py │ ├── camera.py │ ├── campusGateway.py │ ├── cellularGateway.py │ ├── devices.py │ ├── insight.py │ ├── licensing.py │ ├── networks.py │ ├── organizations.py │ ├── sensor.py │ ├── sm.py │ ├── spaces.py │ ├── switch.py │ ├── wireless.py │ └── wirelessController.py ├── common.py ├── config.py ├── exceptions.py ├── response_handler.py └── rest_session.py ├── notebooks ├── README.md ├── Uplink preference backup and restore │ ├── exampleBackups │ │ ├── downloaded_rules_workbook_2020-12-01 180649.837195.xlsx │ │ ├── downloaded_rules_workbook_2020-12-01 181014.158704.xlsx │ │ ├── downloaded_rules_workbook_2020-12-01 backup with wan and vpn uplink prefs and cpcs.xlsx │ │ ├── downloaded_rules_workbook_2020-12-01 rules removed.xlsx │ │ └── downloaded_rules_workbook_2020-12-03 live demo workbook.xlsx │ ├── merakiUplinkPreferenceBackup.ipynb │ └── merakiUplinkPreferenceRestore.ipynb ├── merakiOfflineSwitchFinder.ipynb └── merakiSSIDLimitsChecker.ipynb ├── poetry.lock ├── pyproject.toml ├── setup.py ├── tests ├── __init__.py ├── conftest.py └── test_dashboard_api_python_library.py └── uv.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug specific to the Meraki Python library 3 | about: > 4 | If you've hit a snag and you're confident the issue is with the library itself, 5 | e.g. not with the API or Meraki's servers, then report it here. Otherwise see the links below. 6 | title: '' 7 | labels: '' 8 | assignees: '' 9 | 10 | --- 11 | 12 | **Describe how you confirmed the issue is with the library, and not with the API itself, or a server-side issue of some other kind.** 13 | You can usually confirm it's an issue with the library itself (or with Python) if you've executed the same operations using other tools (e.g. Postman, or Python `requests` library) and if you only have the issue when using _this_ library. For example, if you can't reproduce it with Postman, then it might be a library issue. _On the other hand_, if you can reproduce it across platforms, then it's probably _not_ an issue with the library. In that case, please report the issue to Meraki support. 14 | 15 | **Python version installed** 16 | Which specific Python version are you using? 17 | 18 | **Meraki library version installed** 19 | Which specific version of the library are you using? 20 | 21 | **Have you reproduced the issue with the latest version of this library? And with the latest version of Python?** 22 | 23 | **OS Platform** 24 | Which OS has the problem? E.g. Linux, Windows 10, macOS 14, etc. 25 | 26 | **Describe the bug** 27 | A clear and concise description of what the bug is, and why it seems to be a bug. 28 | 29 | **How can we replicate the problem you're reporting?** 30 | Please provide enough steps so that we can reproduce the issue. It helps if you can share a link to the endpoint's page on [the interactive docs site](https://developer.cisco.com/meraki/api-v1/#!api-reference-overview). **We and Meraki Support will never need or ask for your API key or dashboard login credentials, so please don't share them.** 31 | 32 | For example, to reproduce: 33 | 1. Invoke X endpoint with Y request attributes and/or Z query parameters. 34 | 2. Expect outcome A. 35 | 36 | **Expected behavior** 37 | A clear and concise description of what you expected to happen instead, and why. 38 | 39 | **Code snippets** 40 | If applicable, add code snippets to help explain your problem. 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Meraki Interactive API documentation 4 | url: https://developer.cisco.com/meraki/api-v1/#!api-reference-overview 5 | about: > 6 | The docs are a great first step if you have questions about how specific API endpoints are expected to work. 7 | Find response schemas and even test out API calls in a demo environment, or with your own API token. 8 | - name: Meraki Community Developers & APIs forum 9 | url: https://community.meraki.com/t5/Developers-APIs/bd-p/api 10 | about: > 11 | The best place to ask questions about the Python library or Meraki APIs in general. How do I?-type questions 12 | go here. 13 | - name: Meraki Support 14 | url: https://meraki.cisco.com/meraki-support/overview/ 15 | about: > 16 | For any critical issues with dashboard APIs or documentation site, please contact Meraki Support. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "poetry" 4 | directory: "/" # Adjust if your files are in a subdirectory 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/images/colab-notebook-colab_env-import-Annotation_2020-08-05_163815.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-colab_env-import-Annotation_2020-08-05_163815.png -------------------------------------------------------------------------------- /.github/images/colab-notebook-colab_env-import-new-instance_Annotation_2020-08-05_163942.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-colab_env-import-new-instance_Annotation_2020-08-05_163942.png -------------------------------------------------------------------------------- /.github/images/colab-notebook-new-cell-Annotation_2020-08-05_123920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-new-cell-Annotation_2020-08-05_123920.png -------------------------------------------------------------------------------- /.github/images/colab-notebook-run-cell-Annotation_2020-08-05_143202.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-run-cell-Annotation_2020-08-05_143202.png -------------------------------------------------------------------------------- /.github/images/colab-notebook-selection-Annotation_2020-08-05_120229.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-selection-Annotation_2020-08-05_120229.png -------------------------------------------------------------------------------- /.github/images/colab-notebook-warning-run-cell-Annotation_2020-08-05_143410.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/.github/images/colab-notebook-warning-run-cell-Annotation_2020-08-05_143410.png -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '29 15 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/lint-library-generator.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | name: Lint Python library generator 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'main' 9 | - 'release' 10 | paths-ignore: 11 | - '.github/**' 12 | - 'examples/**' 13 | - 'meraki/aio/**' 14 | - 'notebooks/**' 15 | - 'README.md' 16 | - 'LICENSE' 17 | - 'setup.py' 18 | pull_request: 19 | branches: 20 | - 'main' 21 | - 'release' 22 | paths-ignore: 23 | - '.github/**' 24 | - 'examples/**' 25 | - 'meraki/aio/**' 26 | - 'notebooks/**' 27 | - 'README.md' 28 | - 'LICENSE' 29 | - 'setup.py' 30 | workflow_dispatch: 31 | 32 | jobs: 33 | 34 | build: 35 | 36 | runs-on: ubuntu-latest 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | python-version: ["3.10", "3.11", "3.12", "3.13"] 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Set up Python ${{ matrix.python-version }} 45 | uses: actions/setup-python@v4 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install poetry 52 | poetry lock 53 | poetry install --no-root 54 | poetry add flake8 55 | - name: Lint with flake8 56 | run: | 57 | # stop the build if there are Python syntax errors or undefined names 58 | poetry run flake8 ./generator/generate_library.py --count --select=E9,F63,F7,F82 --ignore=F405,W391,W291,C901,E501,E303,W293 --show-source --statistics 59 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 60 | poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 61 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release to PyPI 2 | on: 3 | release: 4 | types: [created] 5 | 6 | permissions: 7 | id-token: write 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set release name as environment variable 14 | run: echo "RELEASE_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install poetry 24 | poetry install --no-root 25 | poetry add build 26 | poetry add wheel 27 | - name: Build a binary wheel and a source tarball 28 | run: | 29 | poetry run python setup.py sdist bdist_wheel 30 | - name: Publish distribution to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | -------------------------------------------------------------------------------- /.github/workflows/regenerate-library.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate Python Library 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | library_version: 6 | description: 'The version of the new library' 7 | required: true 8 | api_version: 9 | description: 'The corresponding version of the API' 10 | required: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.12' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install poetry 25 | poetry lock 26 | poetry install --no-root 27 | - name: Delete folder 28 | run: | 29 | rm -rf meraki 30 | - name: Regenerate Python Library 31 | run: | 32 | poetry run python generator/generate_library.py -g true -v ${{ github.event.inputs.library_version }} -a ${{ github.event.inputs.api_version }} -o ${{ secrets.TEST_ORG_ID }} -k ${{ secrets.TEST_ORG_API_KEY }} 33 | - name: Set new version for Poetry 34 | run: | 35 | sed -i "s/^version = \".*\"/version = \"${{ github.event.inputs.library_version }}\"/" pyproject.toml 36 | poetry lock 37 | - name: Commit changes to new branch 38 | uses: EndBug/add-and-commit@v9 39 | with: 40 | author_name: GitHub Action 41 | author_email: support@meraki.com 42 | message: Auto-generated library v${{ github.event.inputs.library_version }} for API v${{ github.event.inputs.api_version }}. 43 | new_branch: release 44 | -------------------------------------------------------------------------------- /.github/workflows/test-library.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | name: Test Python library 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'main' 9 | - 'release' 10 | - 'dev_rest_session' 11 | paths-ignore: 12 | - '.github/**' 13 | - 'examples/**' 14 | - 'meraki/aio/**' 15 | - 'notebooks/**' 16 | - 'generator/**' 17 | - 'README.md' 18 | - 'LICENSE' 19 | - 'setup.py' 20 | pull_request: 21 | branches: 22 | - 'main' 23 | - 'release' 24 | - 'dev_rest_session' 25 | paths-ignore: 26 | - '.github/**' 27 | - 'examples/**' 28 | - 'meraki/aio/**' 29 | - 'notebooks/**' 30 | - 'generator/**' 31 | - 'README.md' 32 | - 'LICENSE' 33 | - 'setup.py' 34 | workflow_dispatch: 35 | 36 | 37 | jobs: 38 | build: 39 | 40 | runs-on: ubuntu-latest 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | python-version: ["3.10", "3.11", "3.12", "3.13"] 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Set up Python ${{ matrix.python-version }} 49 | uses: actions/setup-python@v4 50 | with: 51 | python-version: ${{ matrix.python-version }} 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install poetry 56 | poetry lock 57 | poetry install --no-root 58 | poetry add flake8 59 | poetry add pytest 60 | - name: Lint with flake8 61 | run: | 62 | # stop the build if there are Python syntax errors or undefined names 63 | poetry run flake8 . --count --select=E9,F63,F7,F82 --ignore=F405,W391,W291,C901,E501,E303,W293 --exclude=examples,generator --show-source --statistics 64 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 65 | poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 66 | - name: Test with pytest 67 | run: | 68 | poetry run pytest --apikey ${{ secrets.TEST_ORG_API_KEY }} --o ${{ secrets.TEST_ORG_ID }} 69 | 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # No PyCharm config files 3 | .idea/ 4 | venv/ 5 | __pycache__ 6 | /.pytest_cache/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cisco Systems, Inc. and/or its affiliates 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meraki Dashboard API Python Library 2 | 3 | The Meraki Dashboard API Python library provides all current 4 | Meraki [dashboard API](https://developer.cisco.com/meraki/api-v1/) calls to interface with the Cisco Meraki 5 | cloud-managed platform. Meraki generates the library based on dashboard API's OpenAPI spec to keep it up to date with 6 | the latest API releases, and provides the full source code for the library including the tools used to generate the 7 | library, if you are participating in the Early Access program or would like to contribute to the development of the 8 | library. Meraki welcomes constructive pull requests that maintain backwards compatibility with prior versions. The 9 | library requires Python 3.10+, receives support from the community, and you can install it 10 | via [PyPI](https://pypi.org/project/meraki/): 11 | 12 | pip install --upgrade meraki 13 | 14 | If you participate 15 | in [our Early Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808) 16 | and would like to use early access features via the library, 17 | you'll [find instructions in the generator readme](https://github.com/meraki/dashboard-api-python/tree/main/generator#readme). 18 | 19 | ## Features 20 | 21 | While you can make direct HTTP requests to dashboard API in any programming language or REST API client, using a client 22 | library can make it easier for you to focus on your specific use case, without the overhead of having to write functions 23 | to handle the dashboard API calls. The Python library can also take care of error handling, logging, retries, and other 24 | convenient processes and options for you automatically. 25 | 26 | * Support for all API endpoints, as it uses the [OpenAPI specification](https://api.meraki.com/api/v1/openapiSpec) to 27 | generate source code 28 | * Log all API requests made to a local file as well as on-screen console 29 | * Automatic retries upon 429 rate limit errors, using 30 | the [`Retry-After` field](https://developer.cisco.com/meraki/api-v1/#!rate-limit) within response headers 31 | * Get all (or a specified number of) pages of data with built-in pagination control 32 | * Tweak settings such as maximum retries, certificate path, suppress logging, and other options 33 | * Simulate POST/PUT/DELETE calls to preview first, so that network configuration does not get changed 34 | 35 | ## Setup 36 | 37 | 1. Enable API access in your Meraki dashboard organization and obtain an API 38 | key ([instructions](https://documentation.meraki.com/zGeneral_Administration/Other_Topics/The_Cisco_Meraki_Dashboard_API)) 39 | 40 | 2. Keep your API key safe and secure, as it is similar to a password for your dashboard. If publishing your Python code 41 | to a wider audience, please research secure handling of API keys. 42 | 43 | 3. Install the latest version of [Python 3](ttps://wiki.python.org/moin/BeginnersGuide/NonProgrammers) 44 | 45 | 4. Use _pip_ (or an alternative such as _easy_install_) to install the library from the 46 | Python [Package Index](https://pypi.org/project/meraki/): 47 | * `pip install meraki` 48 | * If you have both Python3 and Python2 installed, you may need to use `pip3` (so `pip3 install meraki`) along 49 | with `python3` on your system 50 | * If _meraki_ was previously installed, you can upgrade to the latest non-beta release 51 | with `pip install --upgrade meraki` 52 | 53 | 5. The library supports Meraki dashboard API v1. You can also specify the version of the library when installing with 54 | _pip_: 55 | * See the full [release history](https://pypi.org/project/meraki/#history) to pick the version you want, or 56 | use `pip install meraki==` without including a version number to display the list of available versions 57 | * Versions begin with _1_ (1.0.0b**z** for beta) 58 | * Specify the version you want with the install command; for example: `pip install meraki==1.34.0` 59 | * You can also see the version currently installed with `pip show meraki` 60 | * End-of-life v0 versions of the Python library begin with _0_ (0.**x**.**y**) and are not supported nor 61 | recommended. 62 | 63 | ## Usage 64 | 65 | 1. Export your API key as 66 | an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: 67 | 68 | ```shell 69 | export MERAKI_DASHBOARD_API_KEY=YOUR_KEY_HERE 70 | ``` 71 | 72 | 2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its 73 | inherent insecurity. 74 | 75 | 3. Single line of code to import and use the library goes at the top of your script: 76 | 77 | ```python 78 | import meraki 79 | ``` 80 | 81 | 4. Instantiate the client (API consumer class), optionally specifying any of the parameters available to set: 82 | 83 | ```python 84 | dashboard = meraki.DashboardAPI() 85 | ``` 86 | 87 | 5. Make dashboard API calls in your source code, using the format _client.scope.operation_, where _client_ is the name 88 | you defined in the previous step (**dashboard** above), _scope_ is the corresponding scope that represents the first 89 | tag from the OpenAPI spec, and _operation_ is the operation of the API endpoint. For example, to make a call to get 90 | the list of organizations accessible by the API key defined in step 1, use this function call: 91 | 92 | ```python 93 | my_orgs = dashboard.organizations.getOrganizations() 94 | ``` 95 | 96 | ### Examples 97 | 98 | You can find fully working example scripts in the **examples** folder. 99 | 100 | | Script | Purpose | 101 | |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 102 | | **org_wide_clients.py** | That code collects the clients of all networks, in all orgs to which the key has access. No changes are made, since only GET endpoints are called, and the data is written to local CSV output files. | 103 | 104 | ## AsyncIO 105 | 106 | **asyncio** is a library to write concurrent code using the **async/await** syntax. Special thanks to Heimo 107 | Stieg ([@coreGreenberet](https://github.com/coreGreenberet)) who has ported the API to asyncio. 108 | 109 | ### Installation on macOS 110 | 111 | If you use a Mac, then you may need to take 112 | [additional Python installation steps](https://bugs.python.org/issue43404) that aren't required on other platforms. This 113 | is [a limitation of macOS and not the library](https://github.com/meraki/dashboard-api-python/issues/226). This step is 114 | not required on Windows. 115 | 116 | ### Usage 117 | 118 | The usage is similiar to the sequential version above. However it has has some differences. 119 | 120 | 1. Export your API key as 121 | an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: 122 | 123 | ```shell 124 | export MERAKI_DASHBOARD_API_KEY=YOUR_KEY_HERE 125 | ``` 126 | 127 | 2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its 128 | inherent insecurity. 129 | 130 | 3. Single line of code to import and use the library goes at the top of your script: 131 | 132 | ```python 133 | import meraki.aio 134 | ``` 135 | 136 | 4. Instantiate the client (API consumer class), optionally specifying any of the parameters available to set: 137 | 138 | ```python 139 | async with meraki.aio.AsyncDashboardAPI() as aiomeraki: 140 | ``` 141 | The **async with** statement is important here to make sure, that the client sessions will be closed after using the 142 | api. 143 | 144 | 5. Make dashboard API calls in your source code, using the format await _client.section.operation_, where _client_ is 145 | the name you defined in the previous step (**aiomeraki** above), _section_ is the corresponding group (or tag from 146 | the OpenAPI spec) from the [API docs](https://developer.cisco.com/meraki/api/#/rest), and _operation_ is the name (or 147 | operation ID from OpenAPI) of the API endpoint. For example, to make a call to get the list of organizations 148 | accessible by the API key defined in step 1, use this function call: 149 | 150 | ```python 151 | my_orgs = await aiomeraki.organizations.getOrganizations() 152 | ``` 153 | 6. Run everything inside an event loop. 154 | 155 | ```python 156 | import asyncio 157 | 158 | if __name__ == "__main__": 159 | # replace my_async_entry_point with the name of your entry point method 160 | asyncio.run(my_async_entry_point()) 161 | ``` 162 | 163 | ### Examples 164 | 165 | You can find fully working example scripts in the **examples** folder. 166 | | Script | Purpose | 167 | |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 168 | | **aio_org_wide_clients.py** | That code is a asyncio port from org_wide_clients.py and collects the clients of all 169 | networks, in all orgs to which the key has access. No changes are made, since only GET endpoints are called, and the 170 | data is written to local CSV output files. | 171 | | **aio_ips2firewall.py** | That code will collect the source IP of security events and creates L7 firewall rules to 172 | block them. `usage: aio_ips2firewall.py [-h] -o ORGANIZATIONS [ORGANIZATIONS ...] [-f FILTER] [-s] [-d DAYS]` | 173 | 174 | ## Note for application developers and ecosystem partners 175 | 176 | We're so glad that you're leveraging our Python library. It's best practice to identify your application with every API 177 | request that you make. You can easily do this automatically just by following the format defined 178 | in [config.py](https://github.com/meraki/dashboard-api-python/blob/master/meraki/config.py) and passing the session 179 | kwarg: 180 | 181 | ``` Python 182 | MERAKI_PYTHON_SDK_CALLER 183 | ``` 184 | 185 | Unless you are an ecosystem partner, this identifier is optional. 186 | 187 | 1. If you are an ecosystem partner and you have questions about this requirement, please reach out to your ecosystem 188 | rep. 189 | 2. If you have any questions about the formatting, please ask your question by opening an issue in this repo. 190 | -------------------------------------------------------------------------------- /examples/aio_get_pages_iterator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import time 4 | 5 | import meraki.aio 6 | 7 | # Either input your API key below, or set an environment variable 8 | # for example, in Terminal on macOS: export MERAKI_DASHBOARD_API_KEY=66839003d2861bc302b292eb66d3b247709f2d0d 9 | api_key = "" 10 | 11 | ORGANIZATION_ID = "" 12 | NETWORK_ID = "" 13 | 14 | 15 | def timeit(func): 16 | async def process(func, *args, **params): 17 | if asyncio.iscoroutinefunction(func): 18 | print('this function is a coroutine: {}'.format(func.__name__)) 19 | return await func(*args, **params) 20 | else: 21 | print('this is not a coroutine') 22 | return func(*args, **params) 23 | 24 | async def helper(*args, **params): 25 | print('{}.time'.format(func.__name__)) 26 | start = time.time() 27 | result = await process(func, *args, **params) 28 | 29 | # Test normal function route... 30 | # result = await process(lambda *a, **p: print(*a, **p), *args, **params) 31 | 32 | print('>>>', time.time() - start) 33 | return result 34 | 35 | return helper 36 | 37 | 38 | @timeit 39 | async def getNetworksLegacy(aiomeraki: meraki.aio.AsyncDashboardAPI, perPage=5): 40 | count = 0 41 | for x in await aiomeraki.organizations.getOrganizationNetworks(organizationId=ORGANIZATION_ID, perPage=perPage, 42 | total_pages=-1): 43 | print(f"{x['id']} - {x['name']}") 44 | count = count + 1 45 | print(f"Found {count} networks") 46 | 47 | 48 | @timeit 49 | async def getNetworksIterator(aiomeraki: meraki.aio.AsyncDashboardAPI, perPage=5): 50 | count = 0 51 | async for x in aiomeraki.organizations.getOrganizationNetworks(organizationId=ORGANIZATION_ID, perPage=perPage, 52 | total_pages=-1): 53 | print(f"{x['id']} - {x['name']}") 54 | count = count + 1 55 | print(f"Found {count} networks") 56 | 57 | 58 | @timeit 59 | async def getNetworkEventsLegacy(aiomeraki: meraki.aio.AsyncDashboardAPI, perPage=5): 60 | count = 0 61 | result = await aiomeraki.networks.getNetworkEvents(networkId=NETWORK_ID, perPage=perPage, total_pages=50, 62 | productType="wireless") 63 | for x in result["events"]: 64 | print(f"{x['occurredAt']}") 65 | count = count + 1 66 | print(f"Found {count} events") 67 | 68 | 69 | @timeit 70 | async def getNetworkEventsIterator(aiomeraki: meraki.aio.AsyncDashboardAPI, perPage=5): 71 | count = 0 72 | async for x in aiomeraki.networks.getNetworkEvents(networkId=NETWORK_ID, perPage=perPage, total_pages=50, 73 | productType="wireless"): 74 | print(f"{x['occurredAt']}") 75 | count = count + 1 76 | print(f"Found {count} events") 77 | 78 | 79 | async def main(): 80 | parser = argparse.ArgumentParser(description='Example for demonstrating the use_iterator_for_get_pages parameter') 81 | 82 | # Instantiate a Meraki dashboard API session 83 | # NOTE: you have to use "async with" so that the session will be closed correctly at the end of the usage 84 | async with meraki.aio.AsyncDashboardAPI( 85 | api_key, 86 | base_url="https://api.meraki.com/api/v1", 87 | log_file_prefix=__file__[:-3], 88 | print_console=True, 89 | use_iterator_for_get_pages=True 90 | ) as aiomeraki_iterator: 91 | async with meraki.aio.AsyncDashboardAPI( 92 | api_key, 93 | base_url="https://api.meraki.com/api/v1", 94 | log_file_prefix=__file__[:-3], 95 | print_console=False, 96 | use_iterator_for_get_pages=False 97 | ) as aiomeraki_legacy: 98 | pass 99 | 100 | print("Test legacy") 101 | await getNetworksLegacy(aiomeraki_legacy) 102 | 103 | await asyncio.sleep(2) # just wait two seconds between the tests 104 | 105 | print("Test iterator") 106 | await getNetworksIterator(aiomeraki_iterator) 107 | 108 | print("-----------------------------------------------------------------------") 109 | print("-----------------------------------------------------------------------") 110 | print("-----------------------------------------------------------------------") 111 | print("-----------------------------------------------------------------------") 112 | 113 | print("Test legacy") 114 | await getNetworkEventsLegacy(aiomeraki_legacy) 115 | 116 | await asyncio.sleep(2) # just wait two seconds between the tests 117 | 118 | print("Test iterator") 119 | await getNetworkEventsIterator(aiomeraki_iterator) 120 | 121 | print("Script complete!") 122 | 123 | 124 | if __name__ == "__main__": 125 | loop = asyncio.get_event_loop() 126 | loop.run_until_complete(main()) 127 | -------------------------------------------------------------------------------- /examples/aio_ips2firewall_v1.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import ipaddress 4 | import sys 5 | from typing import Dict, List 6 | 7 | import meraki.aio 8 | 9 | # Either input your API key below, or set an environment variable 10 | # for example, in Terminal on macOS: export MERAKI_DASHBOARD_API_KEY=66839003d2861bc302b292eb66d3b247709f2d0d 11 | api_key = "" 12 | 13 | 14 | def removeSmallAmounts(ip_counts: Dict[str, int], filter: int): 15 | ret = ip_counts.copy() 16 | for k, v in ip_counts.items(): 17 | if v < filter: 18 | ret.pop(k) 19 | 20 | return ret 21 | 22 | 23 | async def analyzeOrganization(aiomeraki: meraki.aio.AsyncDashboardAPI, orgId: str, days: int) -> Dict[str, int]: 24 | ret = {} 25 | timespan = days * 24 * 60 * 60 26 | events = await aiomeraki.appliance.getOrganizationApplianceSecurityEvents(orgId, timespan=timespan, total_pages=-1) 27 | for e in events: 28 | ip, port = e["srcIp"].rsplit(":", 1) 29 | ip = ip.strip("[]") # remove brackets in case of ipv6 30 | if not ipaddress.ip_address(ip).is_private: # don't block private ip addresses on the public ip of the firewall 31 | ret[ip] = ret.get(ip, 0) + 1 32 | 33 | return ret 34 | 35 | 36 | async def updateFirewallrules(aiomeraki: meraki.aio.AsyncDashboardAPI, networkId: str, ip_list: List[str]): 37 | rules = await aiomeraki.appliance.getNetworkApplianceFirewallL7FirewallRules(networkId) 38 | 39 | rules = rules["rules"] 40 | # get the currently blocked ip ranges 41 | current_blocks = [x["value"] for x in rules if x["type"] == "ipRange"] 42 | 43 | new_blocks = current_blocks + list(set(ip_list) - set(current_blocks)) 44 | new_blocks = sorted(new_blocks) 45 | 46 | # generate new rules based on the list of total ip ranges to block 47 | rules_to_add = [{"policy": "deny", "type": "ipRange", "value": x} for x in new_blocks] 48 | 49 | # remove all currently blocked ip ranges 50 | rules = [x for x in rules if x["type"] != "ipRange"] 51 | 52 | rules = rules + rules_to_add 53 | 54 | await aiomeraki.appliance.updateNetworkApplianceFirewallL7FirewallRules(networkId, rules=rules) 55 | 56 | 57 | async def main(): 58 | parser = argparse.ArgumentParser(description='Block IP Addresses based on security events') 59 | parser.add_argument('-o', '--organization', type=str, nargs='+', dest="organizations", required=True, 60 | help='the name/id of the organization(s) you want to analyze/secure') 61 | parser.add_argument("-f", '--filter', dest='filter', type=int, default=5, 62 | help='how often must an attack be listed before it gets blocked') 63 | parser.add_argument("-s", '--save', dest='save', action='store_true', 64 | help='write the blocklist to all networks in the organization.') 65 | parser.add_argument("-d", '--days', dest='days', default=31, type=int, 66 | help='How many days should be analyzed.') 67 | 68 | if len(sys.argv) < 3: 69 | parser.print_help() 70 | return 71 | 72 | try: 73 | args = parser.parse_args() 74 | if args.days >= 365: 75 | print("days must be < 365") 76 | parser.print_help() 77 | return 78 | except SystemExit: 79 | return 80 | except: 81 | print("could not parse arguments") 82 | parser.print_help() 83 | return 84 | 85 | # Instantiate a Meraki dashboard API session 86 | # NOTE: you have to use "async with" so that the session will be closed correctly at the end of the usage 87 | async with meraki.aio.AsyncDashboardAPI( 88 | api_key, 89 | base_url="https://api.meraki.com/api/v1", 90 | log_file_prefix=__file__[:-3], 91 | print_console=False, 92 | ) as aiomeraki: 93 | # Get list of organizations to which API key has access 94 | organizations = await aiomeraki.organizations.getOrganizations() 95 | for x in organizations: 96 | if x["id"] in args.organizations or x["name"] in args.organizations: 97 | print(f"Analyzing organization {x['name']}") 98 | result = await analyzeOrganization(aiomeraki, x["id"], args.days) 99 | result = removeSmallAmounts(result, args.filter) 100 | sum = 0 101 | 102 | for k, v in result.items(): 103 | print(f"{k} attacked {v} times.") 104 | sum = sum + v 105 | print(f"Total attacks: {sum} from {len(result)} different IP adresses") 106 | 107 | # apply the found ip ranges to the firewall 108 | if args.save: 109 | for n in await aiomeraki.organizations.getOrganizationNetworks(x["id"]): 110 | print(f"Updating Network {n['name']}") 111 | await updateFirewallrules(aiomeraki, n["id"], result.keys()) 112 | 113 | print("Script complete!") 114 | 115 | 116 | if __name__ == "__main__": 117 | loop = asyncio.get_event_loop() 118 | loop.run_until_complete(main()) 119 | -------------------------------------------------------------------------------- /examples/aio_org_wide_clients_v1.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import csv 3 | import os 4 | from datetime import datetime 5 | 6 | import meraki.aio 7 | 8 | # Either input your API key below, or set an environment variable 9 | # for example, in Terminal on macOS: export MERAKI_DASHBOARD_API_KEY=093b24e85df15a3e66f1fc359f4c48493eaa1b73 10 | api_key = "" 11 | 12 | 13 | async def listNetworkClients(aiomeraki: meraki.aio.AsyncDashboardAPI, folder_name, network): 14 | print(f'Finding clients in network {network["name"]}') 15 | try: 16 | # Get list of clients on network, filtering on timespan of last 14 days 17 | clients = await aiomeraki.networks.getNetworkClients( 18 | network["id"], 19 | timespan=60 * 60 * 24 * 14, 20 | perPage=1000, 21 | total_pages="all", 22 | ) 23 | except meraki.AsyncAPIError as e: 24 | print(f"Meraki API error: {e}") 25 | except Exception as e: 26 | print(f"some other error: {e}") 27 | else: 28 | if clients: 29 | # Write to file 30 | file_name = f'{network["name"]}.csv' 31 | output_file = open( 32 | f"{folder_name}/{file_name}", mode="w", newline="\n" 33 | ) 34 | field_names = clients[0].keys() 35 | csv_writer = csv.DictWriter( 36 | output_file, 37 | field_names, 38 | delimiter=",", 39 | quotechar='"', 40 | quoting=csv.QUOTE_ALL, 41 | ) 42 | csv_writer.writeheader() 43 | csv_writer.writerows(clients) 44 | output_file.close() 45 | print( 46 | f"Successfully output {len(clients)} clients' data to file {file_name}" 47 | ) 48 | return network["name"], field_names 49 | return network["name"], None 50 | 51 | 52 | async def listOrganization(aiomeraki: meraki.aio.AsyncDashboardAPI, org): 53 | print(f'Analyzing organization {org["name"]}:') 54 | org_id = org["id"] 55 | 56 | # Get list of networks in organization 57 | try: 58 | networks = await aiomeraki.organizations.getOrganizationNetworks(org_id) 59 | except meraki.AsyncAPIError as e: 60 | print(f"Meraki API error: {e}") 61 | return org["name"] 62 | except Exception as e: 63 | print(f"some other error: {e}") 64 | return org["name"] 65 | 66 | # Create local folder 67 | todays_date = f"{datetime.now():%Y-%m-%d}" 68 | folder_name = f"Org {org_id} clients {todays_date}" 69 | if folder_name not in os.listdir(): 70 | os.mkdir(folder_name) 71 | 72 | # Iterate through networks 73 | total = len(networks) 74 | print(f"Iterating through {total} networks in organization {org_id}") 75 | 76 | # create a list of all networks in the organization so we can call them all concurrently 77 | networkClientsTasks = [listNetworkClients(aiomeraki, folder_name, net) for net in networks] 78 | for task in asyncio.as_completed(networkClientsTasks): 79 | networkname, field_names = await task 80 | print(f"finished network: {networkname}") 81 | 82 | # Stitch together one consolidated CSV per org 83 | output_file = open(f"{folder_name}.csv", mode="w", newline="\n") 84 | field_names = ['id', 'mac', 'description', 'ip', 'ip6', 'ip6Local', 'user', 'firstSeen', 'lastSeen', 'manufacturer', 85 | 'os', 'recentDeviceSerial', 'recentDeviceName', 'recentDeviceMac', 'ssid', 'vlan', 'switchport', 86 | 'usage', 'status', 'notes', 'smInstalled', 'groupPolicy8021x'] 87 | field_names.insert(0, "Network Name") 88 | field_names.insert(1, "Network ID") 89 | 90 | csv_writer = csv.DictWriter( 91 | output_file, 92 | field_names, 93 | delimiter=",", 94 | quotechar='"', 95 | quoting=csv.QUOTE_ALL, 96 | ) 97 | csv_writer.writeheader() 98 | for net in networks: 99 | file_name = f'{net["name"]}.csv' 100 | if file_name in os.listdir(folder_name): 101 | with open(f"{folder_name}/{file_name}") as input_file: 102 | csv_reader = csv.DictReader( 103 | input_file, 104 | delimiter=",", 105 | quotechar='"', 106 | quoting=csv.QUOTE_ALL, 107 | ) 108 | next(csv_reader) 109 | for row in csv_reader: 110 | row["Network Name"] = net["name"] 111 | row["Network ID"] = net["id"] 112 | csv_writer.writerow(row) 113 | return org["name"] 114 | 115 | 116 | async def main(): 117 | # Instantiate a Meraki dashboard API session 118 | # NOTE: you have to use "async with" so that the session will be closed correctly at the end of the usage 119 | async with meraki.aio.AsyncDashboardAPI( 120 | api_key, 121 | base_url="https://api.meraki.com/api/v1", 122 | log_file_prefix=__file__[:-3], 123 | print_console=False, 124 | ) as aiomeraki: 125 | # Get list of organizations to which API key has access 126 | organizations = await aiomeraki.organizations.getOrganizations() 127 | 128 | # create a list of all organizations so we can call them all concurrently 129 | organizationTasks = [listOrganization(aiomeraki, org) for org in organizations] 130 | for task in asyncio.as_completed(organizationTasks): 131 | # as_completed returns an iterator, so we just have to await the iterator and not call it 132 | organizationName = await task 133 | print(f"finished organization: {organizationName}") 134 | 135 | print("Script complete!") 136 | 137 | 138 | if __name__ == "__main__": 139 | loop = asyncio.get_event_loop() 140 | loop.run_until_complete(main()) 141 | -------------------------------------------------------------------------------- /examples/apiData2CSV_v1.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | import urllib.parse 6 | from datetime import datetime 7 | 8 | import meraki 9 | 10 | 11 | # This example pulls API calls from the passed in org_id from the last timespan 12 | # seconds, where the default timespan is 900 (hint 24 hours = 3600 seconds) and 13 | # generates a CSV file with the data. 14 | # 15 | # Either input your API key below by uncommenting line 10 and changing line 16 to api_key=API_KEY, 16 | # or set an environment variable (preferred) to define your API key. The former is insecure and not recommended. 17 | # For example, in Linux/macOS: export MERAKI_DASHBOARD_API_KEY=093b24e85df15a3e66f1fc359f4c48493eaa1b73 18 | # API_KEY = '093b24e85df15a3e66f1fc359f4c48493eaa1b73' 19 | # 20 | # Optionally, Cisco partners can set their BE GEO ID by using export BE_GEO_ID=XXXXXX 21 | # where XXXXX is a valid BE GEO ID. This is used for metrics collection. 22 | # 23 | # Optionally, a calling application can be set by using export MERAKI_PYTHON_SDK_CALLER=YYYYY 24 | # where YYYYY is a string identifying the application, script, or whatever piece of code 25 | # is callig the Meraki Python SDK 26 | 27 | 28 | def main(org_id, timespan): 29 | # Instantiate a Meraki dashboard API session 30 | dashboard = meraki.DashboardAPI( 31 | base_url='https://api.meraki.com/api/v1/', 32 | print_console=False, 33 | output_log=False, 34 | ) 35 | 36 | # Get list of API usage data and start the output csv string 37 | apiUsage = dashboard.organizations.getOrganizationApiRequests(org_id, timespan=timespan, total_pages=-1) 38 | csvString = 'method,host,path,queryString,tsDate,tsTime,responseCode,sourceIp,userAgent,' 39 | csvString += 'implementation,implementationVersion,distro,distroVersion,system,systemRelease,' 40 | csvString += 'cpu,be_geo_id,caller\r\n' 41 | cumulativeAPIcalls = 0; 42 | for use in apiUsage: 43 | csvString += use['method'] + ',' 44 | csvString += use['host'] + ',' 45 | csvString += use['path'] + ',' 46 | csvString += use['queryString'] + ',' 47 | csvString += use['ts'].split('T')[0] + ',' 48 | csvString += use['ts'].split('T')[1].replace('Z', '') + ',' 49 | csvString += str(use['responseCode']) + ',' 50 | csvString += use['sourceIp'] + ',' 51 | 52 | # Special User-Agent processing 53 | if 'python-meraki' in use['userAgent']: 54 | print(use['userAgent']) 55 | userAgent = use['userAgent'].split(' ') 56 | csvString += userAgent[0] + ',' 57 | if len(userAgent) > 1: 58 | if "implementation" in userAgent[1]: 59 | userAgentDict = json.loads(urllib.parse.unquote(userAgent[1])) 60 | if "implementation" in userAgentDict: 61 | csvString += userAgentDict['implementation']['name'] + ',' 62 | csvString += userAgentDict['implementation']['version'] + ',' 63 | else: 64 | csvString += ',,' 65 | if "distro" in userAgentDict: 66 | csvString += userAgentDict['distro']['name'] + ',' 67 | csvString += userAgentDict['distro']['version'] + ',' 68 | else: 69 | csvString += ',,' 70 | if "system" in userAgentDict: 71 | csvString += userAgentDict['system']['name'] + ',' 72 | csvString += userAgentDict['system']['release'] + ',' 73 | else: 74 | csvString += ',,' 75 | if "cpu" in userAgentDict: 76 | csvString += userAgentDict['cpu'] + ',' 77 | else: 78 | csvString += ',' 79 | if "be_geo_id" in userAgentDict: 80 | csvString += userAgentDict['be_geo_id'] + ',' 81 | else: 82 | csvString += ',' 83 | if "application" in userAgentDict: 84 | csvString += userAgentDict['application'] + ',' 85 | elif "caller" in userAgentDict: 86 | csvString += userAgentDict['caller'] + ',' 87 | else: 88 | csvString += ',' 89 | else: 90 | csvString += ',,,,,,,,,' 91 | else: 92 | csvString += ',,,,,,,,,' 93 | else: 94 | csvString += use['userAgent'] + ',' 95 | csvString += ',,,,,,,,,' 96 | 97 | csvString += '\r\n' 98 | 99 | # Output the file 100 | now = datetime.now() 101 | dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") 102 | filename = org_id + '_' + str(timespan) + '_' + dt_string + '.csv' 103 | file = open(filename, 'w') 104 | file.write(csvString) 105 | file.close() 106 | print('Results written to ' + filename) 107 | 108 | 109 | if __name__ == '__main__': 110 | # First check for API key 111 | if "MERAKI_DASHBOARD_API_KEY" not in os.environ: 112 | print('You must set the MERAKI_DASHBOARD_API_KEY variable') 113 | sys.exit() 114 | 115 | # Now check arguments 116 | parser = argparse.ArgumentParser(description='Generate a CSV file of Meraki API activity for an organization.') 117 | parser.add_argument('org_id', help='Organization id to pull API activity from') 118 | parser.add_argument('--timespan', type=int, default=900, 119 | help='The timespan (in seconds) for which the information will be fetched. Default = 900 (15 mins)') 120 | args = parser.parse_args() 121 | print('About to run with org_id: ' + args.org_id + ' and timespan: ' + str(args.timespan)) 122 | 123 | # Finally, let's roll 124 | start_time = datetime.now() 125 | main(args.org_id, args.timespan) 126 | end_time = datetime.now() 127 | print(f'\nScript complete, total runtime {end_time - start_time}') 128 | -------------------------------------------------------------------------------- /examples/bulk_firmware_upgrade_manager.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | import meraki 5 | 6 | ''' 7 | Cisco Meraki Bulk Firmware Upgrade Manager 8 | John M. Kuchta .:|:.:|:. https://github.com/TKIPisalegacycipher 9 | This script will pull network IDs from an org and then create asynchronous action batches. Each batch will contain, for 10 | each network, an action that will either schedule a new upgrade, or delay an existing upgrade datetime stamp by X days 11 | (configurable). Each batch can contain up to 100 actions, therefore, each batch can modify up to 100 networks. 12 | 13 | As always, you should read the docs before diving in. If you know how these features work, then it will be easier to 14 | understand and leverage this tool. 15 | 16 | Firmware upgrades endpoint: https://developer.cisco.com/meraki/api-v1/#!get-network-firmware-upgrades 17 | Action batches: https://developer.cisco.com/meraki/api-v1/#!action-batches-overview 18 | 19 | NB: Once you start the script, there are no confirmation prompts or previews, so test in a lab if necessary. 20 | 21 | NB: When the final batch has been submitted, depending on the batch size, it may take a few minutes to finish. Feeling 22 | creative? Then try extending this script (using existing code, for the most part) to confirm when the batches are 23 | complete. Feeling super creative? Wrap this behind a Flask frontend and have yourself a merry little GUI. 24 | ''' 25 | 26 | # init Meraki Python SDK session 27 | dashboard = meraki.DashboardAPI(suppress_logging=True, single_request_timeout=120) 28 | 29 | # Configurable options 30 | # Organization ID. Replace this with your actual organization ID. 31 | organization_id = 'YOUR ORG ID HERE' # Use your own organization ID. 32 | product_type = 'appliance' 33 | time_delta_in_days = 30 # Max is 1 month per the firmware upgrades endpoint docs 34 | actions_per_batch = 100 # Max number of actions to submit in a batch. 100 is the maximum. Bigger batches take longer. 35 | wait_factor = 0.33 # Wait factor for action batches when the action batch queue is full. 36 | 37 | # Firmware IDs; not needed for rescheduling, only for upgrading. If you plan to use this for upgrading, then you should 38 | # first GET the availableVersions IDs and use those here instead, since they have probably changed from the time this 39 | # was published. 40 | new_firmware_id = 2128 # Did you update this to your actual FW ID by GETing your availableFirmwareVersions? 41 | old_firmware_id = 2009 # Did you update this to your actual FW ID by GETing your availableFirmwareVersions? 42 | 43 | 44 | def time_formatter(date_time_stamp): 45 | # Basic time formatter to return strings that the API requires 46 | formatted_date_time_stamp = date_time_stamp.replace(microsecond=0).isoformat() + 'Z' 47 | return formatted_date_time_stamp 48 | 49 | 50 | # Time stamps 51 | utc_now = datetime.datetime.utcnow() 52 | utc_future = utc_now + datetime.timedelta(days=time_delta_in_days) 53 | utc_now_formatted = time_formatter(utc_now) 54 | utc_future_formatted = time_formatter(utc_future) 55 | 56 | action_reschedule_existing = { 57 | "products": { 58 | f"{product_type}": 59 | { 60 | "nextUpgrade": { 61 | "time": utc_future_formatted 62 | } 63 | } 64 | } 65 | } 66 | 67 | # Use this action to schedule a new upgrade. If you do not provide a time param (as shown above), it will execute 68 | # immediately. IMPORTANT: See API docs for more info before using this. 69 | action_schedule_new_upgrade = { 70 | "products": { 71 | f"{product_type}": 72 | { 73 | "nextUpgrade": { 74 | "time": utc_future_formatted, 75 | "toVersion": { 76 | "id": new_firmware_id 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | # GET the network list 84 | networks_list = dashboard.organizations.getOrganizationNetworks( 85 | organizationId=organization_id 86 | ) 87 | 88 | 89 | def format_single_action(resource, operation, body): 90 | # Combine a single set of batch components into an action 91 | action = { 92 | "resource": resource, 93 | "operation": operation, 94 | "body": body 95 | } 96 | 97 | return action 98 | 99 | 100 | def create_single_upgrade_action(network_id): 101 | # Create a single upgrade action 102 | # AB component parts, rename action 103 | action_resource = f'/networks/{network_id}/firmwareUpgrades' 104 | action_operation = 'update' 105 | # Choose whether to reschedule an existing or start a new upgrade 106 | action_body = action_reschedule_existing 107 | 108 | upgrade_action = format_single_action(action_resource, action_operation, action_body) 109 | 110 | return upgrade_action 111 | 112 | 113 | def run_an_action_batch(org_id, actions_list, synchronous=False): 114 | # Create and run an action batch 115 | batch_response = dashboard.organizations.createOrganizationActionBatch( 116 | organizationId=org_id, 117 | actions=actions_list, 118 | confirmed=True, 119 | synchronous=synchronous 120 | ) 121 | 122 | return batch_response 123 | 124 | 125 | def create_action_list(net_list): 126 | # Creates a list of actions and returns it 127 | # Iterate through the list of network IDs and create an action for each, then collect it 128 | list_of_actions = list() 129 | 130 | for network in net_list: 131 | # Create the action 132 | single_action = create_single_upgrade_action(network['id']) 133 | list_of_actions.append(single_action) 134 | 135 | return list_of_actions 136 | 137 | 138 | def batch_actions_splitter(batch_actions): 139 | # Split the list of actions into smaller lists of maximum 100 actions each 140 | # For each ID in range length of network_ids 141 | for i in range(0, len(batch_actions), actions_per_batch): 142 | # Create an index range for network_ids of 100 items: 143 | yield batch_actions[i:i + actions_per_batch] 144 | 145 | 146 | def action_batch_runner(batch_actions_lists, org_id): 147 | # Create an action batch for each list of actions 148 | # Store the responses 149 | responses = list() 150 | number_of_batches = len(batch_actions_lists) 151 | number_of_batches_submitted = 0 152 | 153 | # Make a batch for each list 154 | for batch_action_list in batch_actions_lists: 155 | action_batch_queue_checker(org_id) 156 | batch_response = run_an_action_batch(org_id, batch_action_list) 157 | responses.append(batch_response) 158 | number_of_batches_submitted += 1 159 | 160 | # Inform user of progress. 161 | print(f'Submitted batch {number_of_batches_submitted} of {number_of_batches}.') 162 | 163 | return responses 164 | 165 | 166 | def action_batch_queue_checker(org_id): 167 | all_action_batches = dashboard.organizations.getOrganizationActionBatches(organizationId=org_id) 168 | running_action_batches = [batch for batch in all_action_batches if 169 | batch['status']['completed'] is False and batch['status']['failed'] is False] 170 | total_running_actions = 0 171 | 172 | for batch in running_action_batches: 173 | batch_actions = len(batch['actions']) 174 | total_running_actions += batch_actions 175 | 176 | wait_seconds = total_running_actions * wait_factor 177 | 178 | while len(running_action_batches) > 4: 179 | print( 180 | f'There are already five action batches in progress with a total of {total_running_actions} running ' 181 | f'actions. Waiting {wait_seconds} seconds.') 182 | time.sleep(wait_seconds) 183 | print('Checking again.') 184 | 185 | all_action_batches = dashboard.organizations.getOrganizationActionBatches(organizationId=org_id) 186 | running_action_batches = [batch for batch in all_action_batches if 187 | batch['status']['completed'] is False and batch['status']['failed'] is False] 188 | total_running_actions = 0 189 | 190 | for batch in running_action_batches: 191 | batch_actions = len(batch['actions']) 192 | total_running_actions += batch_actions 193 | 194 | wait_seconds = total_running_actions * wait_factor 195 | 196 | 197 | # Create a list of upgrade actions 198 | upgrade_actions_list = create_action_list(networks_list) 199 | 200 | # Split the list into multiple lists of max 100 items each 201 | upgrade_actions_lists = list(batch_actions_splitter(upgrade_actions_list)) 202 | 203 | # Run the action batches to clone the networks 204 | upgraded_networks_responses = action_batch_runner(upgrade_actions_lists, organization_id) 205 | -------------------------------------------------------------------------------- /examples/find_early_access_operations.py: -------------------------------------------------------------------------------- 1 | import meraki 2 | 3 | # The organization you supply here should be enrolled in early access to use this script. 4 | # If your organization is not enrolled in early access, you will not see early access operations 5 | # in the OAS. 6 | ORGANIZATION_ID = "YOUR_ORG_ID_HERE" 7 | 8 | d = meraki.DashboardAPI(suppress_logging=True) 9 | 10 | oas = d.organizations.getOrganizationOpenapiSpec(ORGANIZATION_ID, version=3) 11 | 12 | paths = oas["paths"] 13 | 14 | operations = list() 15 | 16 | # parse paths, e.g. '/path/to/operation' 17 | for path in paths: 18 | # parse operation in each path, e.g. 'get' 19 | for key in paths[path].keys(): 20 | if "x-release-stage" in paths[path][key].keys(): 21 | operations.append( 22 | { 23 | "id": paths[path][key]["operationId"], 24 | "description": paths[path][key]["description"], 25 | "x-release-stage": paths[path][key]["x-release-stage"], 26 | } 27 | ) 28 | 29 | print(f"Total number of non-GA operations is {len(operations)}.") 30 | 31 | # Find the channels. We only want the distinct values. 32 | channels = list(set([operation["x-release-stage"] for operation in operations])) 33 | 34 | # Sort the channels. 35 | channels.sort() 36 | 37 | # How many channels? 38 | print(f"{len(channels)} channels found. The channels are {channels}.") 39 | 40 | # How many operations in each channel? 41 | for channel in channels: 42 | print( 43 | f'There are {len([operation for operation in operations if operation["x-release-stage"] == channel])}' 44 | f" operations in the {channel} channel." 45 | ) 46 | 47 | if input(f"Would you like to see the operations? y/N") == 'y': 48 | print(operations) 49 | else: 50 | print(f"Goodbye.") 51 | -------------------------------------------------------------------------------- /examples/get_pages_iterator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | 4 | import meraki 5 | 6 | # Either input your API key below, or set an environment variable 7 | # for example, in Terminal on macOS: export MERAKI_DASHBOARD_API_KEY=66839003d2861bc302b292eb66d3b247709f2d0d 8 | api_key = "" 9 | 10 | ORGANIZATION_ID = "" 11 | NETWORK_ID = "" 12 | 13 | 14 | def getNetworksLegacy(meraki: meraki.DashboardAPI, perPage=5): 15 | count = 0 16 | for x in meraki.organizations.getOrganizationNetworks(organizationId=ORGANIZATION_ID, perPage=perPage, 17 | total_pages=-1): 18 | print(f"{x['id']} - {x['name']}") 19 | count = count + 1 20 | print(f"Found {count} networks") 21 | 22 | 23 | def getNetworksIterator(meraki: meraki.DashboardAPI, perPage=5): 24 | count = 0 25 | for x in meraki.organizations.getOrganizationNetworks(organizationId=ORGANIZATION_ID, perPage=perPage, 26 | total_pages=-1): 27 | print(f"{x['id']} - {x['name']}") 28 | count = count + 1 29 | print(f"Found {count} networks") 30 | 31 | 32 | def getNetworkEventsLegacy(meraki: meraki.DashboardAPI, perPage=5): 33 | count = 0 34 | result = meraki.networks.getNetworkEvents(networkId=NETWORK_ID, perPage=perPage, total_pages=50, 35 | productType="wireless") 36 | for x in result["events"]: 37 | print(f"{x['occurredAt']}") 38 | count = count + 1 39 | print(f"Found {count} events") 40 | 41 | 42 | def getNetworkEventsIterator(meraki: meraki.DashboardAPI, perPage=5): 43 | count = 0 44 | for x in meraki.networks.getNetworkEvents(networkId=NETWORK_ID, perPage=perPage, total_pages=50, 45 | productType="wireless"): 46 | print(f"{x['occurredAt']}") 47 | count = count + 1 48 | print(f"Found {count} events") 49 | 50 | 51 | async def main(): 52 | parser = argparse.ArgumentParser(description='Example for demonstrating the use_iterator_for_get_pages parameter') 53 | 54 | # Instantiate a Meraki dashboard API session 55 | # NOTE: you have to use "async with" so that the session will be closed correctly at the end of the usage 56 | meraki_iterator = meraki.DashboardAPI( 57 | api_key, 58 | base_url="https://api.meraki.com/api/v1", 59 | log_file_prefix=__file__[:-3], 60 | print_console=True, 61 | use_iterator_for_get_pages=True 62 | ) 63 | 64 | meraki_legacy = meraki.DashboardAPI( 65 | api_key, 66 | base_url="https://api.meraki.com/api/v1", 67 | log_file_prefix=__file__[:-3], 68 | print_console=False, 69 | use_iterator_for_get_pages=False 70 | ) 71 | 72 | print("Test legacy") 73 | getNetworksLegacy(meraki_legacy) 74 | 75 | await asyncio.sleep(2) # just wait two seconds between the tests 76 | 77 | print("Test iterator") 78 | getNetworksIterator(meraki_iterator) 79 | 80 | print("-----------------------------------------------------------------------") 81 | print("-----------------------------------------------------------------------") 82 | print("-----------------------------------------------------------------------") 83 | print("-----------------------------------------------------------------------") 84 | 85 | print("Test legacy") 86 | getNetworkEventsLegacy(meraki_legacy) 87 | 88 | await asyncio.sleep(2) # just wait two seconds between the tests 89 | 90 | print("Test iterator") 91 | getNetworkEventsIterator(meraki_iterator) 92 | 93 | print("Script complete!") 94 | 95 | 96 | if __name__ == "__main__": 97 | loop = asyncio.get_event_loop() 98 | loop.run_until_complete(main()) 99 | -------------------------------------------------------------------------------- /examples/local_subnet_dumper.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import meraki 4 | 5 | """ 6 | 2023-11-13 7 | Author: John M. Kuchta ( TKIPisalegacycipher // https://github.com/TKIPisalegacycipher ) 8 | Requrires: Meraki library v1.39.0 or later. 9 | 10 | This script gathers all your appliances' local subnets from your organizations' networks and dumps them to a JSON file. 11 | You might find this handy for certain IPAM exercises. Subnets from appliances in "single LAN" mode will have VLAN 0. 12 | 13 | EXTRA CREDIT 14 | If you are feeling adventurous, or just want an opportunity to flex your Python skills, consider re-writing this script 15 | to work asynchronously, which can substantially improve the speed for large environments. You might start by gathering 16 | all the relevant information asynchronously, then doing the list comprehensions after the API calls are complete. 17 | """ 18 | 19 | # You can exclude specific organization or networks here. This is optional but recommended if you have a large 20 | # deployment including lots of irrelevant networks. 21 | excluded_org_ids = [] 22 | excluded_network_ids = [] 23 | 24 | # init session 25 | d = meraki.DashboardAPI() 26 | 27 | # gather orgs 28 | my_orgs = d.organizations.getOrganizations() 29 | my_orgs = [org for org in my_orgs if org["id"] not in excluded_org_ids] 30 | 31 | print(f"done gathering organizations") 32 | 33 | # gather networks 34 | my_networks = [ 35 | d.organizations.getOrganizationNetworks( 36 | organization["id"], total_pages=all 37 | ) 38 | for organization in my_orgs 39 | ] 40 | 41 | print(f"done gathering networks") 42 | 43 | my_appliance_networks = [ 44 | network 45 | for netlist in my_networks 46 | for network in netlist 47 | if network["id"] not in excluded_network_ids 48 | and "appliance" in network["productTypes"] 49 | ] 50 | 51 | print(f"done gathering appliance networks") 52 | 53 | # gather routed networks -- appliances in passthrough mode don't have local subnets 54 | my_appliance_routed_networks = [ 55 | network 56 | for network in my_appliance_networks 57 | if d.appliance.getNetworkApplianceSettings(network["id"])["deploymentMode"] 58 | == "routed" 59 | ] 60 | 61 | print(f"done gathering routed appliance networks") 62 | 63 | my_appliance_networks_with_vlans = [ 64 | network 65 | for network in my_appliance_routed_networks 66 | if d.appliance.getNetworkApplianceVlansSettings(network["id"])["vlansEnabled"] 67 | ] 68 | 69 | print(f"done gathering appliance network vlan settings") 70 | 71 | my_appliance_networks_without_vlans = [ 72 | network 73 | for network in my_appliance_routed_networks 74 | if network not in my_appliance_networks_with_vlans 75 | ] 76 | 77 | my_vlan_lists = [ 78 | { 79 | "organizationId": network["organizationId"], 80 | "networkId": network["id"], 81 | "vlans": d.appliance.getNetworkApplianceVlans(network["id"]), 82 | } 83 | for network in my_appliance_networks_with_vlans 84 | ] 85 | 86 | print(f"done gathering appliance network vlans") 87 | 88 | my_lans = [ 89 | { 90 | "organizationId": network["organizationId"], 91 | "networkId": network["id"], 92 | "lan": d.appliance.getNetworkApplianceSingleLan(network["id"]), 93 | } 94 | for network in my_appliance_networks_without_vlans 95 | ] 96 | 97 | print(f"done gathering appliance network lans") 98 | 99 | # unpack the subnets 100 | vlan_subnets = list() 101 | 102 | for item in my_vlan_lists: 103 | for vlan in item["vlans"]: 104 | this_subnet = dict() 105 | this_subnet["organizationId"] = item["organizationId"] 106 | this_subnet["networkId"] = vlan["networkId"] 107 | this_subnet["subnet"] = vlan["subnet"] 108 | this_subnet["vlanId"] = vlan["id"] 109 | this_subnet["applianceIp"] = vlan["applianceIp"] 110 | vlan_subnets.append(this_subnet) 111 | 112 | lan_subnets = list() 113 | 114 | for item in my_lans: 115 | this_subnet = dict() 116 | this_subnet["organizationId"] = item["organizationId"] 117 | this_subnet["networkId"] = item["networkId"] 118 | this_subnet["subnet"] = item["lan"]["subnet"] 119 | this_subnet["vlanId"] = 0 120 | this_subnet["applianceIp"] = item["lan"]["applianceIp"] 121 | lan_subnets.append(this_subnet) 122 | 123 | all_subnets = vlan_subnets + lan_subnets 124 | 125 | print("done assembling subnets") 126 | 127 | # dump the subnets to a JSON file 128 | json_object = json.dumps(all_subnets, indent=4) 129 | 130 | with open( 131 | "subnets.json", 132 | "w", 133 | ) as outfile: 134 | outfile.write(json_object) 135 | 136 | print("subnets dumped to subnets.json") 137 | -------------------------------------------------------------------------------- /examples/merakiApplianceVlanToL3SwitchInterfaceMigrator/mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "knownParams": [ 3 | { 4 | "names": { 5 | "old": "applianceIp", 6 | "new": null 7 | }, 8 | "purpose": "MX appliance IP interface address", 9 | "status": "deprecated", 10 | "newEndpoint": null 11 | }, 12 | { 13 | "names": { 14 | "old": "networkId", 15 | "new": null 16 | }, 17 | "purpose": "MX appliance network ID", 18 | "status": "deprecated", 19 | "newEndpoint": null 20 | }, 21 | { 22 | "names": { 23 | "old": "groupPolicyId", 24 | "new": null 25 | }, 26 | "status": "deprecated", 27 | "purpose": "MX appliance VLAN group policy ID", 28 | "newEndpoint": null 29 | }, 30 | { 31 | "names": { 32 | "old": "id", 33 | "new": "vlanId" 34 | }, 35 | "status": "renamed", 36 | "purpose": "VLAN ID of the interface VLAN", 37 | "newEndpoint": "interface" 38 | }, 39 | { 40 | "names": { 41 | "old": "name", 42 | "new": "name" 43 | }, 44 | "status": "reused", 45 | "purpose": "VLAN or interface name", 46 | "newEndpoint": "interface" 47 | }, 48 | { 49 | "names": { 50 | "old": "subnet", 51 | "new": "subnet" 52 | }, 53 | "status": "reused", 54 | "purpose": "VLAN or interface subnet in CIDR format", 55 | "newEndpoint": "interface" 56 | }, 57 | { 58 | "names": { 59 | "old": "dhcpBootOptionsEnabled", 60 | "new": "bootOptionsEnabled" 61 | }, 62 | "status": "renamed", 63 | "purpose": "DHCP boot options toggle", 64 | "newEndpoint": "interfaceDhcp" 65 | }, 66 | { 67 | "names": { 68 | "old": "reservedIpRanges", 69 | "new": "reservedIpRanges" 70 | }, 71 | "status": "reused", 72 | "newEndpoint": "interfaceDhcp" 73 | }, 74 | { 75 | "names": { 76 | "old": "fixedIpAssignments", 77 | "new": "fixedIpAssignments", 78 | "newSubParam": "mac" 79 | }, 80 | "status": "transformed", 81 | "transforms": [ 82 | { 83 | "type": "demote dynamic key", 84 | "action": "make subparam" 85 | }, 86 | { 87 | "type": "rename None", 88 | "action": "name" 89 | } 90 | ], 91 | "purpose": "DHCP reservation MAC", 92 | "newEndpoint": "interfaceDhcp" 93 | }, 94 | { 95 | "names": { 96 | "old": "dnsNameservers", 97 | "new": "dnsNameserversOption" 98 | }, 99 | "status": "transformed", 100 | "transforms": [ 101 | { 102 | "type": "rename mode", 103 | "action": "" 104 | }, 105 | { 106 | "type": "split delimited strings", 107 | "action": "\n" 108 | }, 109 | { 110 | "type": "add param", 111 | "action": "dnsCustomNameservers", 112 | "fallback": "custom" 113 | } 114 | ], 115 | "modes": [ 116 | { 117 | "old": "opendns", 118 | "new": "openDns" 119 | }, 120 | { 121 | "old": "google_dns", 122 | "new": "googlePublicDns" 123 | }, 124 | { 125 | "old": "upstream_dns", 126 | "new": "custom" 127 | } 128 | ], 129 | "purpose": "DNS Nameserver assignment mode", 130 | "newEndpoint": "interfaceDhcp" 131 | }, 132 | { 133 | "names": { 134 | "old": "dhcpHandling", 135 | "new": "dhcpMode" 136 | }, 137 | "status": "transformed", 138 | "transforms": [ 139 | { 140 | "type": "rename mode", 141 | "action": "" 142 | } 143 | ], 144 | "modes": [ 145 | { 146 | "old": "Do not respond to DHCP requests", 147 | "new": "dhcpDisabled" 148 | }, 149 | { 150 | "old": "Run a DHCP server", 151 | "new": "dhcpServer" 152 | }, 153 | { 154 | "old": "Relay DHCP to another server", 155 | "new": "dhcpRelay" 156 | } 157 | ], 158 | "purpose": "DHCP mode", 159 | "newEndpoint": "interfaceDhcp" 160 | } 161 | ] 162 | } -------------------------------------------------------------------------------- /examples/merakiApplianceVlanToL3SwitchInterfaceMigrator/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "organizationId": "YOUR ORG ID", 3 | "networkId": "YOUR NETWORK ID", 4 | "switchSerial": "YOUR SWITCH SERIAL", 5 | "applianceSerial": "YOUR APPLIANCE SERIAL", 6 | "vlans": { 7 | "native": { 8 | "id": 1, 9 | "defaultGateway": "10.1.1.1", 10 | "interfaceIp": "10.1.24.1" 11 | }, 12 | "others": [ 13 | { 14 | "id": 9, 15 | "interfaceIp": "192.168.9.2" 16 | }, 17 | { 18 | "id": 10, 19 | "interfaceIp": "192.168.10.2" 20 | }, 21 | { 22 | "id": 100, 23 | "interfaceIp": "192.168.100.2" 24 | } 25 | ] 26 | }, 27 | "fallbacks": [ 28 | { 29 | "knownParams": [ 30 | { 31 | "name": "dnsCustomNameservers", 32 | "mode": [ 33 | "10.1.5.1", 34 | "208.67.222.222", 35 | "208.67.220.220" 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /examples/org_wide_clients_v1.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from datetime import datetime 4 | 5 | import meraki 6 | 7 | 8 | # Either input your API key below by uncommenting line 10 and changing line 16 to api_key=API_KEY, 9 | # or set an environment variable (preferred) to define your API key. The former is insecure and not recommended. 10 | # For example, in Linux/macOS: export MERAKI_DASHBOARD_API_KEY=your-key-here 11 | # API_KEY = 'your-key-here' 12 | 13 | 14 | def main(): 15 | # Instantiate a Meraki dashboard API session 16 | dashboard = meraki.DashboardAPI( 17 | api_key='', 18 | base_url='https://api.meraki.com/api/v1/', 19 | output_log=True, 20 | log_file_prefix=os.path.basename(__file__)[:-3], 21 | log_path='', 22 | print_console=False 23 | ) 24 | 25 | # Get list of organizations to which API key has access 26 | organizations = dashboard.organizations.getOrganizations() 27 | 28 | # Iterate through list of orgs 29 | for org in organizations: 30 | print(f'\nAnalyzing organization {org["name"]}:') 31 | org_id = org['id'] 32 | 33 | # Get list of networks in organization 34 | try: 35 | networks = dashboard.organizations.getOrganizationNetworks(org_id) 36 | except meraki.APIError as e: 37 | print(f'Meraki API error: {e}') 38 | print(f'status code = {e.status}') 39 | print(f'reason = {e.reason}') 40 | print(f'error = {e.message}') 41 | continue 42 | except Exception as e: 43 | print(f'some other error: {e}') 44 | continue 45 | 46 | # Create local folder 47 | todays_date = f'{datetime.now():%Y-%m-%d}' 48 | folder_name = f'Org {org_id} clients {todays_date}' 49 | if folder_name not in os.listdir(): 50 | os.mkdir(folder_name) 51 | 52 | # Iterate through networks 53 | total = len(networks) 54 | counter = 1 55 | print(f' - iterating through {total} networks in organization {org_id}') 56 | for net in networks: 57 | print(f'Finding clients in network {net["name"]} ({counter} of {total})') 58 | try: 59 | # Get list of clients on network, filtering on timespan of last 14 days 60 | clients = dashboard.networks.getNetworkClients(net['id'], timespan=60 * 60 * 24 * 14, perPage=1000, 61 | total_pages='all') 62 | except meraki.APIError as e: 63 | print(f'Meraki API error: {e}') 64 | print(f'status code = {e.status}') 65 | print(f'reason = {e.reason}') 66 | print(f'error = {e.message}') 67 | except Exception as e: 68 | print(f'some other error: {e}') 69 | else: 70 | if clients: 71 | # Write to file 72 | file_name = f'{net["name"]}.csv' 73 | output_file = open(f'{folder_name}/{file_name}', mode='w', newline='\n') 74 | field_names = clients[0].keys() 75 | csv_writer = csv.DictWriter(output_file, field_names, delimiter=',', quotechar='"', 76 | quoting=csv.QUOTE_ALL) 77 | csv_writer.writeheader() 78 | csv_writer.writerows(clients) 79 | output_file.close() 80 | print(f' - found {len(clients)}') 81 | 82 | counter += 1 83 | 84 | # Stitch together one consolidated CSV per org 85 | output_file = open(f'{folder_name}.csv', mode='w', newline='\n') 86 | field_names = ['id', 'mac', 'description', 'ip', 'ip6', 'ip6Local', 'user', 'firstSeen', 'lastSeen', 87 | 'manufacturer', 'os', 'recentDeviceSerial', 'recentDeviceName', 'recentDeviceMac', 'ssid', 88 | 'vlan', 'switchport', 'usage', 'status', 'notes', 'smInstalled', 'groupPolicy8021x'] 89 | field_names.insert(0, "Network Name") 90 | field_names.insert(1, "Network ID") 91 | 92 | csv_writer = csv.DictWriter(output_file, field_names, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 93 | csv_writer.writeheader() 94 | for net in networks: 95 | file_name = f'{net["name"]}.csv' 96 | if file_name in os.listdir(folder_name): 97 | with open(f'{folder_name}/{file_name}') as input_file: 98 | csv_reader = csv.DictReader(input_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 99 | next(csv_reader) 100 | for row in csv_reader: 101 | row['Network Name'] = net['name'] 102 | row['Network ID'] = net['id'] 103 | csv_writer.writerow(row) 104 | 105 | 106 | if __name__ == '__main__': 107 | start_time = datetime.now() 108 | main() 109 | end_time = datetime.now() 110 | print(f'\nScript complete, total runtime {end_time - start_time}') 111 | -------------------------------------------------------------------------------- /examples/organization_deleter.py: -------------------------------------------------------------------------------- 1 | # organization_deleter.py 2 | # A script to clean up and delete obsolete organizations, especially for lab testing scenarios. 3 | # Deletes all networks, config templates, extra admins, and releases from inventory all devices 4 | # in the organizations being deleted. 5 | ############################### 6 | # WARNING 7 | # This script is highly destructive. It should not be used in production environments. 8 | ############################### 9 | 10 | import sys 11 | 12 | import meraki 13 | 14 | # This should be a list of string organization IDs you want to delete 15 | # E.g., ["1","2","3"] 16 | LIST_OF_ORGANIZATIONS_TO_DELETE = [] 17 | 18 | # This should be the single email address of the owner of the API key you're using to run this script. 19 | # E.g., "tam@shan.ter" 20 | EMAIL_ADDRESS_OF_API_KEY_OWNER = "" 21 | 22 | print( 23 | f"This script will delete all orgs in this list: {LIST_OF_ORGANIZATIONS_TO_DELETE}" 24 | ) 25 | confirmed = input(f"Are you sure you'd like to proceed? (yes/N)") 26 | 27 | # User will need to type yes to continue 28 | if confirmed != "yes": 29 | print("Aborting") 30 | sys.exit() 31 | 32 | # Init dashboard session 33 | d = meraki.DashboardAPI(suppress_logging=True) 34 | 35 | for organization in LIST_OF_ORGANIZATIONS_TO_DELETE: 36 | print(f"Deleting networks in organization (id: {organization})...") 37 | 38 | # get networks 39 | org_networks = d.organizations.getOrganizationNetworks(organization) 40 | count_networks = len(org_networks) 41 | print(f"There are {count_networks} networks to delete.") 42 | for network in org_networks: 43 | print(f"Deleting network (id: {network['id']})...") 44 | delete_network = d.networks.deleteNetwork(network["id"]) 45 | count_networks -= 1 46 | print(f"{count_networks} remaining.") 47 | print(f"Done deleting networks.") 48 | 49 | # get config templates 50 | org_templates = d.organizations.getOrganizationConfigTemplates(organization) 51 | count_templates = len(org_templates) 52 | for template in org_templates: 53 | print(f"Deleting config template (id: {template['id']})...") 54 | delete_network = d.organizations.deleteOrganizationConfigTemplate( 55 | organization, template["id"] 56 | ) 57 | count_templates -= 1 58 | print(f"{count_templates} remaining.") 59 | print(f"Done deleting config templates.") 60 | 61 | # get org inventory devices 62 | org_devices = d.organizations.getOrganizationInventoryDevices(organization) 63 | count_devices = len(org_devices) 64 | print(f"There are {count_devices} devices to release from inventory.") 65 | device_serials = [device["serial"] for device in org_devices] 66 | if len(device_serials) > 0: 67 | release_devices = d.organizations.releaseFromOrganizationInventory( 68 | organization, serials=device_serials 69 | ) 70 | print(f"Released {count_devices} devices from inventory.") 71 | print(f"Done releasing devices.") 72 | 73 | # get org admins 74 | org_admins = d.organizations.getOrganizationAdmins(organization) 75 | for admin in org_admins: 76 | if admin["email"] != EMAIL_ADDRESS_OF_API_KEY_OWNER: 77 | delete_admin = d.organizations.deleteOrganizationAdmin( 78 | organization, admin["id"] 79 | ) 80 | 81 | print(f"Done deleting networks and admins in organization (id: {organization}).") 82 | 83 | confirmed = input(f"Would you like to proceed with deleting the organizations? (yes/N)") 84 | 85 | if confirmed != "yes": 86 | print("Aborting") 87 | sys.exit() 88 | 89 | for organization in LIST_OF_ORGANIZATIONS_TO_DELETE: 90 | delete_organization = d.organizations.deleteOrganization(organization) 91 | print(f"Deleted organization (id: {organization}).") 92 | 93 | print(f"Done deleting organizations.") 94 | -------------------------------------------------------------------------------- /examples/wireless_rf_profiles_overview.py: -------------------------------------------------------------------------------- 1 | import meraki 2 | 3 | ### work in progress 4 | 5 | print( 6 | f"To use this tool, you must supply your organization ID. Your organization ID should be an integer." 7 | ) 8 | organization_id = "" 9 | # organization_id = input(f"Enter your organization ID:") 10 | 11 | all_rf_profiles = list() 12 | 13 | d = meraki.DashboardAPI(suppress_logging=True) 14 | 15 | # fetch RF profiles assignments by device 16 | print(f"Fetching RF profile assignments for the organization") 17 | rf_profiles_assignments = ( 18 | d.wireless.getOrganizationWirelessRfProfilesAssignmentsByDevice( 19 | organization_id, total_pages=all 20 | )["items"] 21 | ) 22 | 23 | # Find the top five profiles by how many devices are assigned to them 24 | counts = list() 25 | profiles = list() 26 | profiles_and_counts = list() 27 | 28 | profile_ids = list() 29 | for assignment in rf_profiles_assignments: 30 | if assignment["rfProfile"]["id"] not in profile_ids: 31 | profile_ids.append(assignment["rfProfile"]["id"]) 32 | assignment["rfProfile"]["count"] = 0 33 | profiles.append(assignment["rfProfile"]) 34 | 35 | # assemble the profiles 36 | for assignment in rf_profiles_assignments: 37 | # build the list of relevant profiles 38 | if assignment["rfProfile"]["id"] not in profiles: 39 | profiles.append(assignment["rfProfile"]) 40 | 41 | # create a count for each profile 42 | for profile in profiles: 43 | profile["count"] = 0 44 | 45 | # count up the number of times something's assigned 46 | # for assignment in rf_profiles_assignments: 47 | 48 | 49 | # fetch wireless networks 50 | print(f"Fetching wireless networks") 51 | networks = d.organizations.getOrganizationNetworks( 52 | organization_id, productTypes=["wireless"], total_pages=all 53 | ) 54 | wireless_networks = [ 55 | network for network in networks if "wireless" in network["productTypes"] 56 | ] 57 | 58 | print(f"Fetching RF profiles per network") 59 | rf_profiles_by_network = [ 60 | d.wireless.getNetworkWirelessRfProfiles(network["id"]) 61 | for network in wireless_networks 62 | ] 63 | 64 | # flatten the list 65 | for network in rf_profiles_by_network: 66 | for profile in network: 67 | all_rf_profiles.append(profile) 68 | 69 | print(f"Fetching RF profiles per network") 70 | print(f"Fetching RF profiles per network") 71 | 72 | print(1) 73 | -------------------------------------------------------------------------------- /generator/async_class_template.jinja2: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class Async{{ class_name }}: 5 | def __init__(self, session): 6 | super().__init__() 7 | self._session = session 8 | 9 | -------------------------------------------------------------------------------- /generator/async_function_template.jinja2: -------------------------------------------------------------------------------- 1 | async def {{ operation }}(self{% if function_definition|length > 0 %}{{ function_definition }}{% endif %}): 2 | """ 3 | **{{ description }}** 4 | {{ doc_url }} 5 | 6 | {% for d in descriptions %} 7 | - {{ d }} 8 | {% endfor %} 9 | """ 10 | 11 | {% if kwarg_line|length > 0 %} 12 | {{ kwarg_line }} 13 | 14 | {% endif %} 15 | {% if assert_blocks|length > 0 %} 16 | {% for param, values in assert_blocks %} 17 | if '{{ param }}' in kwargs: 18 | options = {{ values }} 19 | assert kwargs['{{ param }}'] in options, f'''"{{ param }}" cannot be "{kwargs['{{ param }}']}", & must be set to one of: {options}''' 20 | {% endfor %} 21 | 22 | {% endif %} 23 | metadata = { 24 | 'tags': {{ tags }}, 25 | 'operation': '{{ operation }}' 26 | } 27 | {% for param in path_params %} 28 | {{ param }} = urllib.parse.quote(str({{ param }}), safe='') 29 | {% endfor %} 30 | resource = f'{{ resource }}' 31 | 32 | {% if query_params|length > 0 %} 33 | query_params = [{% for param in query_params %}'{{ param }}', {% endfor %}] 34 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 35 | 36 | {% endif %} 37 | {% if array_params|length > 0 %} 38 | array_params = [{% for param in array_params %}'{{ param }}', {% endfor %}] 39 | for k, v in kwargs.items(): 40 | if k.strip() in array_params: 41 | params[f'{k.strip()}[]'] = kwargs[f'{k}'] 42 | params.pop(k.strip()) 43 | 44 | {% endif %} 45 | {% if body_params|length > 0 %} 46 | body_params = [{% for param in body_params %}'{{ param }}', {% endfor %}] 47 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 48 | 49 | {% endif %} 50 | {{ call_line }} 51 | 52 | -------------------------------------------------------------------------------- /generator/batch_class_template.jinja2: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatch{{ class_name }}(object): 5 | def __init__(self): 6 | super(ActionBatch{{ class_name }}, self).__init__() 7 | 8 | -------------------------------------------------------------------------------- /generator/batch_function_template.jinja2: -------------------------------------------------------------------------------- 1 | def {{ operation }}(self{% if function_definition|length > 0 %}{{ function_definition }}{% endif %}): 2 | """ 3 | **{{ description }}** 4 | {{ doc_url }} 5 | 6 | {% for d in descriptions %} 7 | - {{ d }} 8 | {% endfor %} 9 | """ 10 | 11 | {% if kwarg_line|length > 0 %} 12 | {{ kwarg_line }} 13 | 14 | {% endif %} 15 | {% if assert_blocks|length > 0 %} 16 | {% for param, values in assert_blocks %} 17 | if '{{ param }}' in kwargs: 18 | options = {{ values }} 19 | assert kwargs['{{ param }}'] in options, f'''"{{ param }}" cannot be "{kwargs['{{ param }}']}", & must be set to one of: {options}''' 20 | {% endfor %} 21 | 22 | {% endif %} 23 | metadata = { 24 | 'tags': {{ tags }}, 25 | 'operation': '{{ operation }}' 26 | } 27 | {% for param in path_params %} 28 | {{ param }} = urllib.parse.quote({{ param }}, safe='') 29 | {% endfor %} 30 | resource = f'{{ resource }}' 31 | 32 | {% if query_params|length > 0 %} 33 | query_params = [{% for param in query_params %}'{{ param }}', {% endfor %}] 34 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 35 | 36 | {% endif %} 37 | {% if array_params|length > 0 %} 38 | array_params = [{% for param in array_params %}'{{ param }}', {% endfor %}] 39 | for k, v in kwargs.items(): 40 | if k.strip() in array_params: 41 | params[f'{k.strip()}[]'] = kwargs[f'{k}'] 42 | params.pop(k.strip()) 43 | 44 | {% endif %} 45 | {% if body_params|length > 0 %} 46 | body_params = [{% for param in body_params %}'{{ param }}', {% endfor %}] 47 | {% if batch_operation != 'destroy'%} 48 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 49 | {% endif %} 50 | {% endif %} 51 | action = { 52 | "resource": resource, 53 | "operation": "{{ batch_operation }}", 54 | {% if body_params|length > 0 and batch_operation != 'destroy'%} 55 | "body": payload 56 | {% endif %} 57 | } 58 | {{ call_line }} 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /generator/class_template.jinja2: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class {{ class_name }}(object): 5 | def __init__(self, session): 6 | super({{ class_name }}, self).__init__() 7 | self._session = session 8 | 9 | -------------------------------------------------------------------------------- /generator/common.py: -------------------------------------------------------------------------------- 1 | def organize_spec(paths, scopes): 2 | operations = list() # list of operation IDs 3 | for path, methods in paths.items(): 4 | # method is the HTTP action, e.g., get, put, etc. 5 | for method in methods: 6 | # endpoint is the method for that specific path 7 | endpoint = paths[path][method] 8 | 9 | # the endpoint has tags 10 | tags = endpoint["tags"] 11 | 12 | # the endpoint has an operationId 13 | operation = endpoint["operationId"] 14 | 15 | # add the operation ID to the list 16 | operations.append(operation) 17 | 18 | # The endpoint has a scope defined by the first tag 19 | # There are a handful of operations that are currently mistagged 20 | # This helps ensure they are scoped to the correct module 21 | if len(tags) > 2: 22 | match tags[2]: 23 | case 'spaces': 24 | scope = 'spaces' 25 | case _: 26 | scope = tags[0] 27 | else: 28 | scope = tags[0] 29 | 30 | # Needs documentation 31 | if path not in scopes[scope]: 32 | scopes[scope][path] = {method: endpoint} 33 | # Needs documentation 34 | else: 35 | scopes[scope][path][method] = endpoint 36 | return operations, scopes 37 | -------------------------------------------------------------------------------- /generator/function_template.jinja2: -------------------------------------------------------------------------------- 1 | def {{ operation }}(self{% if function_definition|length > 0 %}{{ function_definition }}{% endif %}): 2 | """ 3 | **{{ description }}** 4 | {{ doc_url }} 5 | 6 | {% for d in descriptions %} 7 | - {{ d }} 8 | {% endfor %} 9 | """ 10 | 11 | {% if kwarg_line|length > 0 %} 12 | {{ kwarg_line }} 13 | 14 | {% endif %} 15 | {% if assert_blocks|length > 0 %} 16 | {% for param, values in assert_blocks %} 17 | if '{{ param }}' in kwargs: 18 | options = {{ values }} 19 | assert kwargs['{{ param }}'] in options, f'''"{{ param }}" cannot be "{kwargs['{{ param }}']}", & must be set to one of: {options}''' 20 | {% endfor %} 21 | 22 | {% endif %} 23 | metadata = { 24 | 'tags': {{ tags }}, 25 | 'operation': '{{ operation }}' 26 | } 27 | {% for param in path_params %} 28 | {{ param }} = urllib.parse.quote(str({{ param }}), safe='') 29 | {% endfor %} 30 | resource = f'{{ resource }}' 31 | 32 | {% if query_params|length > 0 %} 33 | query_params = [{% for param in query_params %}'{{ param }}', {% endfor %}] 34 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 35 | 36 | {% endif %} 37 | {% if array_params|length > 0 %} 38 | array_params = [{% for param in array_params %}'{{ param }}', {% endfor %}] 39 | for k, v in kwargs.items(): 40 | if k.strip() in array_params: 41 | params[f'{k.strip()}[]'] = kwargs[f'{k}'] 42 | params.pop(k.strip()) 43 | 44 | {% endif %} 45 | {% if body_params|length > 0 %} 46 | body_params = [{% for param in body_params %}'{{ param }}', {% endfor %}] 47 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 48 | 49 | {% endif %} 50 | {{ call_line }} 51 | 52 | -------------------------------------------------------------------------------- /generator/readme.md: -------------------------------------------------------------------------------- 1 | # Generating the Meraki Dashboard API Python Library 2 | 3 | Generally speaking, you will not need to generate this yourself. Simply use the official PyPI package 4 | via `pip install --update meraki`. 5 | 6 | However, if you participate in Early Access features, you may want to generate a library to match your org's spec. In 7 | which case, follow along. 8 | 9 | > **NB:** The generator requires Python 3.10 or later. 10 | 11 | 1. Clone this repo locally. 12 | 2. Open a terminal in this `generator` folder. 13 | 3. *Optional:* If you want to work with beta endpoints, then 14 | first [review the warnings, and then opt one of your orgs into the Early API Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808). 15 | 4. Run `python generate_library.py -v locally_generated -o YOUR_ORG_ID -k YOUR_API_KEY` making these replacements: 16 | 17 | * Replace `YOUR_ORG_ID` with the org ID you want to use as reference. Use the one opted into Early API access if you 18 | want the beta endpoints. 19 | * Replace `YOUR_API_KEY` with an API key that has org admin privileges on that org. 20 | * NB: Your local system may require minor syntax tweaks (e.g. Windows may require you prepend `generate_library.py` 21 | with `.\`) 22 | 23 | 5. You will now have a `meraki` module folder inside `generator`, which you can locally reference in your scripts. 24 | Simply copy the `meraki` folder to those projects which require it. 25 | 6. In some cases, if you've already installed the official library, your scripts may prefer that one over the local 26 | folder. If that happens, then calls to early access endpoints will fail. So, if necessary, uninstall any instances of 27 | the meraki package that may have been installed in your venv or system, or replace the version installed in your venv 28 | with that which you generated here. 29 | -------------------------------------------------------------------------------- /meraki/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from meraki.api.administered import Administered 5 | from meraki.api.appliance import Appliance 6 | # Batch class imports 7 | from meraki.api.batch import Batch 8 | from meraki.api.camera import Camera 9 | from meraki.api.cellularGateway import CellularGateway 10 | from meraki.api.devices import Devices 11 | from meraki.api.insight import Insight 12 | from meraki.api.licensing import Licensing 13 | from meraki.api.networks import Networks 14 | from meraki.api.organizations import Organizations 15 | from meraki.api.sensor import Sensor 16 | from meraki.api.sm import Sm 17 | from meraki.api.switch import Switch 18 | from meraki.api.wireless import Wireless 19 | # Config import 20 | from meraki.config import ( 21 | API_KEY_ENVIRONMENT_VARIABLE, 22 | DEFAULT_BASE_URL, 23 | SINGLE_REQUEST_TIMEOUT, 24 | CERTIFICATE_PATH, 25 | REQUESTS_PROXY, 26 | WAIT_ON_RATE_LIMIT, 27 | NGINX_429_RETRY_WAIT_TIME, 28 | ACTION_BATCH_RETRY_WAIT_TIME, 29 | NETWORK_DELETE_RETRY_WAIT_TIME, 30 | RETRY_4XX_ERROR, 31 | RETRY_4XX_ERROR_WAIT_TIME, 32 | MAXIMUM_RETRIES, 33 | OUTPUT_LOG, 34 | LOG_PATH, 35 | LOG_FILE_PREFIX, 36 | PRINT_TO_CONSOLE, 37 | SUPPRESS_LOGGING, 38 | INHERIT_LOGGING_CONFIG, 39 | SIMULATE_API_CALLS, 40 | BE_GEO_ID, 41 | MERAKI_PYTHON_SDK_CALLER, 42 | USE_ITERATOR_FOR_GET_PAGES, 43 | ) 44 | from meraki.rest_session import * 45 | 46 | __version__ = '2.0.3' 47 | __api_version__ = '1.58.0' 48 | 49 | 50 | class DashboardAPI(object): 51 | """ 52 | **Creates a persistent Meraki dashboard API session** 53 | 54 | - api_key (string): API key generated in dashboard; can also be set as an environment variable MERAKI_DASHBOARD_API_KEY 55 | - base_url (string): preceding all endpoint resources 56 | - single_request_timeout (integer): maximum number of seconds for each API call 57 | - certificate_path (string): path for TLS/SSL certificate verification if behind local proxy 58 | - requests_proxy (string): proxy server and port, if needed, for HTTPS 59 | - wait_on_rate_limit (boolean): retry if 429 rate limit error encountered? 60 | - nginx_429_retry_wait_time (integer): Nginx 429 retry wait time 61 | - action_batch_retry_wait_time (integer): action batch concurrency error retry wait time 62 | - network_delete_retry_wait_time (integer): network deletion concurrency error retry wait time 63 | - retry_4xx_error (boolean): retry if encountering other 4XX error (besides 429)? 64 | - retry_4xx_error_wait_time (integer): other 4XX error retry wait time 65 | - maximum_retries (integer): retry up to this many times when encountering 429s or other server-side errors 66 | - output_log (boolean): create an output log file? 67 | - log_path (string): path to output log; by default, working directory of script if not specified 68 | - log_file_prefix (string): log file name appended with date and timestamp 69 | - print_console (boolean): print logging output to console? 70 | - suppress_logging (boolean): disable all logging? you're on your own then! 71 | - inherit_logging_config (boolean): Inherits your own logger instance 72 | - simulate (boolean): simulate POST/PUT/DELETE calls to prevent changes? 73 | - be_geo_id (string): optional partner identifier for API usage tracking; can also be set as an environment variable BE_GEO_ID 74 | - caller (string): optional identifier for API usage tracking; can also be set as an environment variable MERAKI_PYTHON_SDK_CALLER 75 | - use_iterator_for_get_pages (boolean): list* methods will return an iterator with each object instead of a complete list with all items 76 | """ 77 | 78 | def __init__(self, 79 | api_key=None, 80 | base_url=DEFAULT_BASE_URL, 81 | single_request_timeout=SINGLE_REQUEST_TIMEOUT, 82 | certificate_path=CERTIFICATE_PATH, 83 | requests_proxy=REQUESTS_PROXY, 84 | wait_on_rate_limit=WAIT_ON_RATE_LIMIT, 85 | nginx_429_retry_wait_time=NGINX_429_RETRY_WAIT_TIME, 86 | action_batch_retry_wait_time=ACTION_BATCH_RETRY_WAIT_TIME, 87 | network_delete_retry_wait_time=NETWORK_DELETE_RETRY_WAIT_TIME, 88 | retry_4xx_error=RETRY_4XX_ERROR, 89 | retry_4xx_error_wait_time=RETRY_4XX_ERROR_WAIT_TIME, 90 | maximum_retries=MAXIMUM_RETRIES, 91 | output_log=OUTPUT_LOG, 92 | log_path=LOG_PATH, 93 | log_file_prefix=LOG_FILE_PREFIX, 94 | print_console=PRINT_TO_CONSOLE, 95 | suppress_logging=SUPPRESS_LOGGING, 96 | simulate=SIMULATE_API_CALLS, 97 | be_geo_id=BE_GEO_ID, 98 | caller=MERAKI_PYTHON_SDK_CALLER, 99 | use_iterator_for_get_pages=USE_ITERATOR_FOR_GET_PAGES, 100 | inherit_logging_config=INHERIT_LOGGING_CONFIG, 101 | ): 102 | 103 | # Check API key 104 | api_key = api_key or os.environ.get(API_KEY_ENVIRONMENT_VARIABLE) 105 | if not api_key: 106 | raise APIKeyError() 107 | 108 | # Pull the BE GEO ID from an environment variable if present 109 | be_geo_id = be_geo_id or os.environ.get('BE_GEO_ID') 110 | 111 | # Pull the caller from an environment variable if present 112 | caller = caller or os.environ.get('MERAKI_PYTHON_SDK_CALLER') 113 | 114 | use_iterator_for_get_pages = use_iterator_for_get_pages 115 | inherit_logging_config = inherit_logging_config 116 | 117 | # Configure logging 118 | if not suppress_logging: 119 | self._logger = logging.getLogger(__name__) 120 | 121 | if not inherit_logging_config: 122 | self._logger.setLevel(logging.DEBUG) 123 | 124 | formatter = logging.Formatter( 125 | fmt='%(asctime)s %(name)12s: %(levelname)8s > %(message)s', 126 | datefmt='%Y-%m-%d %H:%M:%S' 127 | ) 128 | handler_console = logging.StreamHandler() 129 | handler_console.setFormatter(formatter) 130 | 131 | if output_log: 132 | if log_path and log_path[-1] != '/': 133 | log_path += '/' 134 | self._log_file = f'{log_path}{log_file_prefix}_log__{datetime.now():%Y-%m-%d_%H-%M-%S}.log' 135 | handler_log = logging.FileHandler( 136 | filename=self._log_file 137 | ) 138 | handler_log.setFormatter(formatter) 139 | 140 | if output_log and not self._logger.hasHandlers(): 141 | self._logger.addHandler(handler_log) 142 | if print_console: 143 | handler_console.setLevel(logging.INFO) 144 | self._logger.addHandler(handler_console) 145 | elif print_console and not self._logger.hasHandlers(): 146 | self._logger.addHandler(handler_console) 147 | else: 148 | self._logger = None 149 | 150 | # Creates the API session 151 | self._session = RestSession( 152 | logger=self._logger, 153 | api_key=api_key, 154 | base_url=base_url, 155 | single_request_timeout=single_request_timeout, 156 | certificate_path=certificate_path, 157 | requests_proxy=requests_proxy, 158 | wait_on_rate_limit=wait_on_rate_limit, 159 | nginx_429_retry_wait_time=nginx_429_retry_wait_time, 160 | action_batch_retry_wait_time=action_batch_retry_wait_time, 161 | network_delete_retry_wait_time=network_delete_retry_wait_time, 162 | retry_4xx_error=retry_4xx_error, 163 | retry_4xx_error_wait_time=retry_4xx_error_wait_time, 164 | maximum_retries=maximum_retries, 165 | simulate=simulate, 166 | be_geo_id=be_geo_id, 167 | caller=caller, 168 | use_iterator_for_get_pages=use_iterator_for_get_pages, 169 | ) 170 | 171 | # API endpoints by section 172 | self.administered = Administered(self._session) 173 | self.organizations = Organizations(self._session) 174 | self.networks = Networks(self._session) 175 | self.devices = Devices(self._session) 176 | self.appliance = Appliance(self._session) 177 | self.camera = Camera(self._session) 178 | self.cellularGateway = CellularGateway(self._session) 179 | self.insight = Insight(self._session) 180 | self.licensing = Licensing(self._session) 181 | self.sensor = Sensor(self._session) 182 | self.sm = Sm(self._session) 183 | self.switch = Switch(self._session) 184 | self.wireless = Wireless(self._session) 185 | 186 | # Batch definitions 187 | self.batch = Batch() 188 | -------------------------------------------------------------------------------- /meraki/aio/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from meraki.aio.api.administered import AsyncAdministered 5 | from meraki.aio.api.appliance import AsyncAppliance 6 | from meraki.aio.api.camera import AsyncCamera 7 | from meraki.aio.api.cellularGateway import AsyncCellularGateway 8 | from meraki.aio.api.devices import AsyncDevices 9 | from meraki.aio.api.insight import AsyncInsight 10 | from meraki.aio.api.licensing import AsyncLicensing 11 | from meraki.aio.api.networks import AsyncNetworks 12 | from meraki.aio.api.organizations import AsyncOrganizations 13 | from meraki.aio.api.sensor import AsyncSensor 14 | from meraki.aio.api.sm import AsyncSm 15 | from meraki.aio.api.switch import AsyncSwitch 16 | from meraki.aio.api.wireless import AsyncWireless 17 | from meraki.aio.rest_session import * 18 | # Batch class imports 19 | from meraki.api.batch import Batch 20 | # Config import 21 | from meraki.config import ( 22 | API_KEY_ENVIRONMENT_VARIABLE, 23 | DEFAULT_BASE_URL, 24 | SINGLE_REQUEST_TIMEOUT, 25 | CERTIFICATE_PATH, 26 | REQUESTS_PROXY, 27 | WAIT_ON_RATE_LIMIT, 28 | NGINX_429_RETRY_WAIT_TIME, 29 | ACTION_BATCH_RETRY_WAIT_TIME, 30 | NETWORK_DELETE_RETRY_WAIT_TIME, 31 | RETRY_4XX_ERROR, 32 | RETRY_4XX_ERROR_WAIT_TIME, 33 | MAXIMUM_RETRIES, 34 | OUTPUT_LOG, 35 | LOG_PATH, 36 | LOG_FILE_PREFIX, 37 | PRINT_TO_CONSOLE, 38 | SUPPRESS_LOGGING, 39 | INHERIT_LOGGING_CONFIG, 40 | SIMULATE_API_CALLS, 41 | BE_GEO_ID, 42 | MERAKI_PYTHON_SDK_CALLER, 43 | USE_ITERATOR_FOR_GET_PAGES, 44 | AIO_MAXIMUM_CONCURRENT_REQUESTS, 45 | ) 46 | 47 | 48 | class AsyncDashboardAPI: 49 | """ 50 | **Creates a persistent Meraki dashboard API session** 51 | 52 | - api_key (string): API key generated in dashboard; can also be set as an environment variable MERAKI_DASHBOARD_API_KEY 53 | - base_url (string): preceding all endpoint resources 54 | - single_request_timeout (integer): maximum number of seconds for each API call 55 | - certificate_path (string): path for TLS/SSL certificate verification if behind local proxy 56 | - requests_proxy (string): proxy server and port, if needed, for HTTPS 57 | - wait_on_rate_limit (boolean): retry if 429 rate limit error encountered? 58 | - nginx_429_retry_wait_time (integer): Nginx 429 retry wait time 59 | - action_batch_retry_wait_time (integer): action batch concurrency error retry wait time 60 | - network_delete_retry_wait_time (integer): network deletion concurrency error retry wait time 61 | - retry_4xx_error (boolean): retry if encountering other 4XX error (besides 429)? 62 | - retry_4xx_error_wait_time (integer): other 4XX error retry wait time 63 | - maximum_retries (integer): retry up to this many times when encountering 429s or other server-side errors 64 | - output_log (boolean): create an output log file? 65 | - log_path (string): path to output log; by default, working directory of script if not specified 66 | - log_file_prefix (string): log file name appended with date and timestamp 67 | - print_console (boolean): print logging output to console? 68 | - suppress_logging (boolean): disable all logging? you're on your own then! 69 | - inherit_logging_config (boolean): Inherits your own logger instance 70 | - simulate (boolean): simulate POST/PUT/DELETE calls to prevent changes? 71 | - maximum_concurrent_requests (integer): number of concurrent API requests for asynchronous class 72 | - be_geo_id (string): optional partner identifier for API usage tracking; can also be set as an environment variable BE_GEO_ID 73 | - caller (string): optional identifier for API usage tracking; can also be set as an environment variable MERAKI_PYTHON_SDK_CALLER 74 | - use_iterator_for_get_pages (boolean): list* methods will return an iterator with each object instead of a complete list with all items 75 | """ 76 | 77 | def __init__(self, 78 | api_key=None, 79 | base_url=DEFAULT_BASE_URL, 80 | single_request_timeout=SINGLE_REQUEST_TIMEOUT, 81 | certificate_path=CERTIFICATE_PATH, 82 | requests_proxy=REQUESTS_PROXY, 83 | wait_on_rate_limit=WAIT_ON_RATE_LIMIT, 84 | nginx_429_retry_wait_time=NGINX_429_RETRY_WAIT_TIME, 85 | action_batch_retry_wait_time=ACTION_BATCH_RETRY_WAIT_TIME, 86 | network_delete_retry_wait_time=NETWORK_DELETE_RETRY_WAIT_TIME, 87 | retry_4xx_error=RETRY_4XX_ERROR, 88 | retry_4xx_error_wait_time=RETRY_4XX_ERROR_WAIT_TIME, 89 | maximum_retries=MAXIMUM_RETRIES, 90 | output_log=OUTPUT_LOG, 91 | log_path=LOG_PATH, 92 | log_file_prefix=LOG_FILE_PREFIX, 93 | print_console=PRINT_TO_CONSOLE, 94 | suppress_logging=SUPPRESS_LOGGING, 95 | simulate=SIMULATE_API_CALLS, 96 | be_geo_id=BE_GEO_ID, 97 | caller=MERAKI_PYTHON_SDK_CALLER, 98 | use_iterator_for_get_pages=USE_ITERATOR_FOR_GET_PAGES, 99 | inherit_logging_config=INHERIT_LOGGING_CONFIG, 100 | maximum_concurrent_requests=AIO_MAXIMUM_CONCURRENT_REQUESTS, 101 | ): 102 | 103 | # Check API key 104 | api_key = api_key or os.environ.get(API_KEY_ENVIRONMENT_VARIABLE) 105 | if not api_key: 106 | raise APIKeyError() 107 | 108 | # Pull the BE GEO ID from an environment variable if present 109 | be_geo_id = be_geo_id or os.environ.get('BE_GEO_ID') 110 | 111 | # Pull the caller from an environment variable if present 112 | caller = caller or os.environ.get('MERAKI_PYTHON_SDK_CALLER') 113 | 114 | use_iterator_for_get_pages = use_iterator_for_get_pages 115 | inherit_logging_config = inherit_logging_config 116 | 117 | # Configure logging 118 | if not suppress_logging: 119 | self._logger = logging.getLogger(__name__) 120 | 121 | if not inherit_logging_config: 122 | self._logger.setLevel(logging.DEBUG) 123 | 124 | formatter = logging.Formatter( 125 | fmt="%(asctime)s %(name)12s: %(levelname)8s > %(message)s", 126 | datefmt="%Y-%m-%d %H:%M:%S", 127 | ) 128 | handler_console = logging.StreamHandler() 129 | handler_console.setFormatter(formatter) 130 | 131 | if output_log: 132 | if log_path and log_path[-1] != "/": 133 | log_path += "/" 134 | self._log_file = f"{log_path}{log_file_prefix}_log__{datetime.now():%Y-%m-%d_%H-%M-%S}.log" 135 | handler_log = logging.FileHandler(filename=self._log_file) 136 | handler_log.setFormatter(formatter) 137 | 138 | if output_log and not self._logger.hasHandlers(): 139 | self._logger.addHandler(handler_log) 140 | if print_console: 141 | handler_console.setLevel(logging.INFO) 142 | self._logger.addHandler(handler_console) 143 | elif print_console and not self._logger.hasHandlers(): 144 | self._logger.addHandler(handler_console) 145 | else: 146 | self._logger = None 147 | 148 | # Creates the API session 149 | self._session = AsyncRestSession( 150 | logger=self._logger, 151 | api_key=api_key, 152 | base_url=base_url, 153 | single_request_timeout=single_request_timeout, 154 | certificate_path=certificate_path, 155 | requests_proxy=requests_proxy, 156 | wait_on_rate_limit=wait_on_rate_limit, 157 | nginx_429_retry_wait_time=nginx_429_retry_wait_time, 158 | action_batch_retry_wait_time=action_batch_retry_wait_time, 159 | network_delete_retry_wait_time=network_delete_retry_wait_time, 160 | retry_4xx_error=retry_4xx_error, 161 | retry_4xx_error_wait_time=retry_4xx_error_wait_time, 162 | maximum_retries=maximum_retries, 163 | simulate=simulate, 164 | be_geo_id=be_geo_id, 165 | caller=caller, 166 | use_iterator_for_get_pages=use_iterator_for_get_pages, 167 | maximum_concurrent_requests=maximum_concurrent_requests, 168 | ) 169 | 170 | # API endpoints by section 171 | self.administered = AsyncAdministered(self._session) 172 | self.organizations = AsyncOrganizations(self._session) 173 | self.networks = AsyncNetworks(self._session) 174 | self.devices = AsyncDevices(self._session) 175 | self.appliance = AsyncAppliance(self._session) 176 | self.camera = AsyncCamera(self._session) 177 | self.cellularGateway = AsyncCellularGateway(self._session) 178 | self.insight = AsyncInsight(self._session) 179 | self.licensing = AsyncLicensing(self._session) 180 | self.sensor = AsyncSensor(self._session) 181 | self.switch = AsyncSwitch(self._session) 182 | self.sm = AsyncSm(self._session) 183 | self.wireless = AsyncWireless(self._session) 184 | 185 | # Batch definitions 186 | self.batch = Batch() 187 | 188 | async def __aenter__(self): 189 | return self 190 | 191 | async def __aexit__(self, exc_type, exc, tb): 192 | await self._session.close() 193 | -------------------------------------------------------------------------------- /meraki/aio/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/meraki/aio/api/__init__.py -------------------------------------------------------------------------------- /meraki/aio/api/administered.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class AsyncAdministered: 5 | def __init__(self, session): 6 | super().__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def getAdministeredIdentitiesMe(self): 12 | """ 13 | **Returns the identity of the current user.** 14 | https://developer.cisco.com/meraki/api-v1/#!get-administered-identities-me 15 | 16 | """ 17 | 18 | metadata = { 19 | 'tags': ['administered', 'monitor', 'identities', 'me'], 20 | 'operation': 'getAdministeredIdentitiesMe' 21 | } 22 | resource = f'/administered/identities/me' 23 | 24 | return self._session.get(metadata, resource) 25 | 26 | 27 | 28 | def getAdministeredIdentitiesMeApiKeys(self): 29 | """ 30 | **List the non-sensitive metadata associated with the API keys that belong to the user** 31 | https://developer.cisco.com/meraki/api-v1/#!get-administered-identities-me-api-keys 32 | 33 | """ 34 | 35 | metadata = { 36 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 37 | 'operation': 'getAdministeredIdentitiesMeApiKeys' 38 | } 39 | resource = f'/administered/identities/me/api/keys' 40 | 41 | return self._session.get(metadata, resource) 42 | 43 | 44 | 45 | def generateAdministeredIdentitiesMeApiKeys(self): 46 | """ 47 | **Generates an API key for an identity** 48 | https://developer.cisco.com/meraki/api-v1/#!generate-administered-identities-me-api-keys 49 | 50 | """ 51 | 52 | metadata = { 53 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 54 | 'operation': 'generateAdministeredIdentitiesMeApiKeys' 55 | } 56 | resource = f'/administered/identities/me/api/keys/generate' 57 | 58 | return self._session.post(metadata, resource) 59 | 60 | 61 | 62 | def revokeAdministeredIdentitiesMeApiKeys(self, suffix: str): 63 | """ 64 | **Revokes an identity's API key, using the last four characters of the key** 65 | https://developer.cisco.com/meraki/api-v1/#!revoke-administered-identities-me-api-keys 66 | 67 | - suffix (string): Suffix 68 | """ 69 | 70 | metadata = { 71 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 72 | 'operation': 'revokeAdministeredIdentitiesMeApiKeys' 73 | } 74 | suffix = urllib.parse.quote(str(suffix), safe='') 75 | resource = f'/administered/identities/me/api/keys/{suffix}/revoke' 76 | 77 | return self._session.post(metadata, resource) 78 | 79 | -------------------------------------------------------------------------------- /meraki/aio/api/campusGateway.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class AsyncCampusGateway: 5 | def __init__(self, session): 6 | super().__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def createNetworkCampusGatewayCluster(self, networkId: str, name: str, uplinks: list, tunnels: list, nameservers: dict, portChannels: list, **kwargs): 12 | """ 13 | **Create a cluster and add campus gateways to it** 14 | https://developer.cisco.com/meraki/api-v1/#!create-network-campus-gateway-cluster 15 | 16 | - networkId (string): Network ID 17 | - name (string): Name of the new cluster 18 | - uplinks (array): Uplink interface settings of the cluster 19 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 20 | - nameservers (object): Nameservers of the cluster 21 | - portChannels (array): Port channel settings of the cluster 22 | - devices (array): Devices to be added to the cluster 23 | - notes (string): Notes about cluster with max size of 511 characters allowed 24 | """ 25 | 26 | kwargs.update(locals()) 27 | 28 | metadata = { 29 | 'tags': ['campusGateway', 'configure', 'clusters'], 30 | 'operation': 'createNetworkCampusGatewayCluster' 31 | } 32 | networkId = urllib.parse.quote(str(networkId), safe='') 33 | resource = f'/networks/{networkId}/campusGateway/clusters' 34 | 35 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 36 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 37 | 38 | return self._session.post(metadata, resource, payload) 39 | 40 | 41 | 42 | def updateNetworkCampusGatewayCluster(self, networkId: str, clusterId: str, **kwargs): 43 | """ 44 | **Update a cluster and add/remove campus gateways to/from it** 45 | https://developer.cisco.com/meraki/api-v1/#!update-network-campus-gateway-cluster 46 | 47 | - networkId (string): Network ID 48 | - clusterId (string): Cluster ID 49 | - name (string): Name of the cluster 50 | - uplinks (array): Uplink interface settings of the cluster 51 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 52 | - nameservers (object): Nameservers of the cluster 53 | - portChannels (array): Port channel settings of the cluster 54 | - devices (array): Devices in the cluster 55 | - notes (string): Notes about cluster with max size of 511 characters allowed 56 | """ 57 | 58 | kwargs.update(locals()) 59 | 60 | metadata = { 61 | 'tags': ['campusGateway', 'configure', 'clusters'], 62 | 'operation': 'updateNetworkCampusGatewayCluster' 63 | } 64 | networkId = urllib.parse.quote(str(networkId), safe='') 65 | clusterId = urllib.parse.quote(str(clusterId), safe='') 66 | resource = f'/networks/{networkId}/campusGateway/clusters/{clusterId}' 67 | 68 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 69 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 70 | 71 | return self._session.put(metadata, resource, payload) 72 | 73 | 74 | 75 | def getOrganizationCampusGatewayDevicesUplinksLocalOverridesByDevice(self, organizationId: str, total_pages=1, direction='next', **kwargs): 76 | """ 77 | **Uplink overrides configured locally on Campus Gateway devices in an organization.** 78 | https://developer.cisco.com/meraki/api-v1/#!get-organization-campus-gateway-devices-uplinks-local-overrides-by-device 79 | 80 | - organizationId (string): Organization ID 81 | - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages 82 | - direction (string): direction to paginate, either "next" (default) or "prev" page 83 | - serials (array): A list of serial numbers. The returned devices will be filtered to only include these serials. 84 | - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. 85 | - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 86 | - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 87 | """ 88 | 89 | kwargs.update(locals()) 90 | 91 | metadata = { 92 | 'tags': ['campusGateway', 'configure', 'devices', 'uplinks', 'localOverrides', 'byDevice'], 93 | 'operation': 'getOrganizationCampusGatewayDevicesUplinksLocalOverridesByDevice' 94 | } 95 | organizationId = urllib.parse.quote(str(organizationId), safe='') 96 | resource = f'/organizations/{organizationId}/campusGateway/devices/uplinks/localOverrides/byDevice' 97 | 98 | query_params = ['serials', 'perPage', 'startingAfter', 'endingBefore', ] 99 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 100 | 101 | array_params = ['serials', ] 102 | for k, v in kwargs.items(): 103 | if k.strip() in array_params: 104 | params[f'{k.strip()}[]'] = kwargs[f'{k}'] 105 | params.pop(k.strip()) 106 | 107 | return self._session.get_pages(metadata, resource, params, total_pages, direction) 108 | 109 | -------------------------------------------------------------------------------- /meraki/aio/api/insight.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class AsyncInsight: 5 | def __init__(self, session): 6 | super().__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def getNetworkInsightApplicationHealthByTime(self, networkId: str, applicationId: str, **kwargs): 12 | """ 13 | **Get application health by time** 14 | https://developer.cisco.com/meraki/api-v1/#!get-network-insight-application-health-by-time 15 | 16 | - networkId (string): Network ID 17 | - applicationId (string): Application ID 18 | - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 7 days from today. 19 | - t1 (string): The end of the timespan for the data. t1 can be a maximum of 7 days after t0. 20 | - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 7 days. The default is 2 hours. 21 | - resolution (integer): The time resolution in seconds for returned data. The valid resolutions are: 60, 300, 3600, 86400. The default is 300. 22 | """ 23 | 24 | kwargs.update(locals()) 25 | 26 | metadata = { 27 | 'tags': ['insight', 'monitor', 'applications', 'healthByTime'], 28 | 'operation': 'getNetworkInsightApplicationHealthByTime' 29 | } 30 | networkId = urllib.parse.quote(str(networkId), safe='') 31 | applicationId = urllib.parse.quote(str(applicationId), safe='') 32 | resource = f'/networks/{networkId}/insight/applications/{applicationId}/healthByTime' 33 | 34 | query_params = ['t0', 't1', 'timespan', 'resolution', ] 35 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 36 | 37 | return self._session.get(metadata, resource, params) 38 | 39 | 40 | 41 | def getOrganizationInsightApplications(self, organizationId: str): 42 | """ 43 | **List all Insight tracked applications** 44 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-applications 45 | 46 | - organizationId (string): Organization ID 47 | """ 48 | 49 | metadata = { 50 | 'tags': ['insight', 'configure', 'applications'], 51 | 'operation': 'getOrganizationInsightApplications' 52 | } 53 | organizationId = urllib.parse.quote(str(organizationId), safe='') 54 | resource = f'/organizations/{organizationId}/insight/applications' 55 | 56 | return self._session.get(metadata, resource) 57 | 58 | 59 | 60 | def getOrganizationInsightMonitoredMediaServers(self, organizationId: str): 61 | """ 62 | **List the monitored media servers for this organization** 63 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-monitored-media-servers 64 | 65 | - organizationId (string): Organization ID 66 | """ 67 | 68 | metadata = { 69 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 70 | 'operation': 'getOrganizationInsightMonitoredMediaServers' 71 | } 72 | organizationId = urllib.parse.quote(str(organizationId), safe='') 73 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers' 74 | 75 | return self._session.get(metadata, resource) 76 | 77 | 78 | 79 | def createOrganizationInsightMonitoredMediaServer(self, organizationId: str, name: str, address: str, **kwargs): 80 | """ 81 | **Add a media server to be monitored for this organization** 82 | https://developer.cisco.com/meraki/api-v1/#!create-organization-insight-monitored-media-server 83 | 84 | - organizationId (string): Organization ID 85 | - name (string): The name of the VoIP provider 86 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 87 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 88 | """ 89 | 90 | kwargs.update(locals()) 91 | 92 | metadata = { 93 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 94 | 'operation': 'createOrganizationInsightMonitoredMediaServer' 95 | } 96 | organizationId = urllib.parse.quote(str(organizationId), safe='') 97 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers' 98 | 99 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 100 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 101 | 102 | return self._session.post(metadata, resource, payload) 103 | 104 | 105 | 106 | def getOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str): 107 | """ 108 | **Return a monitored media server for this organization** 109 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-monitored-media-server 110 | 111 | - organizationId (string): Organization ID 112 | - monitoredMediaServerId (string): Monitored media server ID 113 | """ 114 | 115 | metadata = { 116 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 117 | 'operation': 'getOrganizationInsightMonitoredMediaServer' 118 | } 119 | organizationId = urllib.parse.quote(str(organizationId), safe='') 120 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 121 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 122 | 123 | return self._session.get(metadata, resource) 124 | 125 | 126 | 127 | def updateOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str, **kwargs): 128 | """ 129 | **Update a monitored media server for this organization** 130 | https://developer.cisco.com/meraki/api-v1/#!update-organization-insight-monitored-media-server 131 | 132 | - organizationId (string): Organization ID 133 | - monitoredMediaServerId (string): Monitored media server ID 134 | - name (string): The name of the VoIP provider 135 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 136 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 137 | """ 138 | 139 | kwargs.update(locals()) 140 | 141 | metadata = { 142 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 143 | 'operation': 'updateOrganizationInsightMonitoredMediaServer' 144 | } 145 | organizationId = urllib.parse.quote(str(organizationId), safe='') 146 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 147 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 148 | 149 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 150 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 151 | 152 | return self._session.put(metadata, resource, payload) 153 | 154 | 155 | 156 | def deleteOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str): 157 | """ 158 | **Delete a monitored media server from this organization** 159 | https://developer.cisco.com/meraki/api-v1/#!delete-organization-insight-monitored-media-server 160 | 161 | - organizationId (string): Organization ID 162 | - monitoredMediaServerId (string): Monitored media server ID 163 | """ 164 | 165 | metadata = { 166 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 167 | 'operation': 'deleteOrganizationInsightMonitoredMediaServer' 168 | } 169 | organizationId = urllib.parse.quote(str(organizationId), safe='') 170 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 171 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 172 | 173 | return self._session.delete(metadata, resource) 174 | 175 | -------------------------------------------------------------------------------- /meraki/aio/api/spaces.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class AsyncSpaces: 5 | def __init__(self, session): 6 | super().__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def removeOrganizationSpacesIntegration(self, organizationId: str): 12 | """ 13 | **Remove the Spaces integration from Meraki** 14 | https://developer.cisco.com/meraki/api-v1/#!remove-organization-spaces-integration 15 | 16 | - organizationId (string): Organization ID 17 | """ 18 | 19 | metadata = { 20 | 'tags': ['organizations', 'configure', 'spaces', 'integration'], 21 | 'operation': 'removeOrganizationSpacesIntegration' 22 | } 23 | organizationId = urllib.parse.quote(str(organizationId), safe='') 24 | resource = f'/organizations/{organizationId}/spaces/integration/remove' 25 | 26 | return self._session.post(metadata, resource) 27 | 28 | -------------------------------------------------------------------------------- /meraki/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/meraki/api/__init__.py -------------------------------------------------------------------------------- /meraki/api/administered.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class Administered(object): 5 | def __init__(self, session): 6 | super(Administered, self).__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def getAdministeredIdentitiesMe(self): 12 | """ 13 | **Returns the identity of the current user.** 14 | https://developer.cisco.com/meraki/api-v1/#!get-administered-identities-me 15 | 16 | """ 17 | 18 | metadata = { 19 | 'tags': ['administered', 'monitor', 'identities', 'me'], 20 | 'operation': 'getAdministeredIdentitiesMe' 21 | } 22 | resource = f'/administered/identities/me' 23 | 24 | return self._session.get(metadata, resource) 25 | 26 | 27 | 28 | def getAdministeredIdentitiesMeApiKeys(self): 29 | """ 30 | **List the non-sensitive metadata associated with the API keys that belong to the user** 31 | https://developer.cisco.com/meraki/api-v1/#!get-administered-identities-me-api-keys 32 | 33 | """ 34 | 35 | metadata = { 36 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 37 | 'operation': 'getAdministeredIdentitiesMeApiKeys' 38 | } 39 | resource = f'/administered/identities/me/api/keys' 40 | 41 | return self._session.get(metadata, resource) 42 | 43 | 44 | 45 | def generateAdministeredIdentitiesMeApiKeys(self): 46 | """ 47 | **Generates an API key for an identity** 48 | https://developer.cisco.com/meraki/api-v1/#!generate-administered-identities-me-api-keys 49 | 50 | """ 51 | 52 | metadata = { 53 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 54 | 'operation': 'generateAdministeredIdentitiesMeApiKeys' 55 | } 56 | resource = f'/administered/identities/me/api/keys/generate' 57 | 58 | return self._session.post(metadata, resource) 59 | 60 | 61 | 62 | def revokeAdministeredIdentitiesMeApiKeys(self, suffix: str): 63 | """ 64 | **Revokes an identity's API key, using the last four characters of the key** 65 | https://developer.cisco.com/meraki/api-v1/#!revoke-administered-identities-me-api-keys 66 | 67 | - suffix (string): Suffix 68 | """ 69 | 70 | metadata = { 71 | 'tags': ['administered', 'configure', 'identities', 'me', 'api', 'keys'], 72 | 'operation': 'revokeAdministeredIdentitiesMeApiKeys' 73 | } 74 | suffix = urllib.parse.quote(str(suffix), safe='') 75 | resource = f'/administered/identities/me/api/keys/{suffix}/revoke' 76 | 77 | return self._session.post(metadata, resource) 78 | 79 | -------------------------------------------------------------------------------- /meraki/api/batch/__init__.py: -------------------------------------------------------------------------------- 1 | from meraki.api.batch.organizations import ActionBatchOrganizations 2 | from meraki.api.batch.networks import ActionBatchNetworks 3 | from meraki.api.batch.devices import ActionBatchDevices 4 | from meraki.api.batch.appliance import ActionBatchAppliance 5 | from meraki.api.batch.camera import ActionBatchCamera 6 | from meraki.api.batch.cellularGateway import ActionBatchCellularGateway 7 | from meraki.api.batch.insight import ActionBatchInsight 8 | from meraki.api.batch.sensor import ActionBatchSensor 9 | from meraki.api.batch.sm import ActionBatchSm 10 | from meraki.api.batch.switch import ActionBatchSwitch 11 | from meraki.api.batch.wireless import ActionBatchWireless 12 | 13 | 14 | # Batch class 15 | class Batch: 16 | def __init__(self): 17 | # Action Batch helper API endpoints by section 18 | self.organizations = ActionBatchOrganizations() 19 | self.networks = ActionBatchNetworks() 20 | self.devices = ActionBatchDevices() 21 | self.appliance = ActionBatchAppliance() 22 | self.camera = ActionBatchCamera() 23 | self.cellularGateway = ActionBatchCellularGateway() 24 | self.insight = ActionBatchInsight() 25 | self.sensor = ActionBatchSensor() 26 | self.sm = ActionBatchSm() 27 | self.switch = ActionBatchSwitch() 28 | self.wireless = ActionBatchWireless() 29 | -------------------------------------------------------------------------------- /meraki/api/batch/administered.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchAdministered(object): 5 | def __init__(self): 6 | super(ActionBatchAdministered, self).__init__() 7 | 8 | -------------------------------------------------------------------------------- /meraki/api/batch/camera.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchCamera(object): 5 | def __init__(self): 6 | super(ActionBatchCamera, self).__init__() 7 | 8 | 9 | 10 | def updateDeviceCameraCustomAnalytics(self, serial: str, **kwargs): 11 | """ 12 | **Update custom analytics settings for a camera** 13 | https://developer.cisco.com/meraki/api-v1/#!update-device-camera-custom-analytics 14 | 15 | - serial (string): Serial 16 | - enabled (boolean): Enable custom analytics 17 | - artifactId (string): The ID of the custom analytics artifact 18 | - parameters (array): Parameters for the custom analytics workload 19 | """ 20 | 21 | kwargs.update(locals()) 22 | 23 | metadata = { 24 | 'tags': ['camera', 'configure', 'customAnalytics'], 25 | 'operation': 'updateDeviceCameraCustomAnalytics' 26 | } 27 | resource = f'/devices/{serial}/camera/customAnalytics' 28 | 29 | body_params = ['enabled', 'artifactId', 'parameters', ] 30 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 31 | action = { 32 | "resource": resource, 33 | "operation": "update", 34 | "body": payload 35 | } 36 | return action 37 | 38 | 39 | 40 | 41 | 42 | 43 | def updateDeviceCameraQualityAndRetention(self, serial: str, **kwargs): 44 | """ 45 | **Update quality and retention settings for the given camera** 46 | https://developer.cisco.com/meraki/api-v1/#!update-device-camera-quality-and-retention 47 | 48 | - serial (string): Serial 49 | - profileId (string): The ID of a quality and retention profile to assign to the camera. The profile's settings will override all of the per-camera quality and retention settings. If the value of this parameter is null, any existing profile will be unassigned from the camera. 50 | - motionBasedRetentionEnabled (boolean): Boolean indicating if motion-based retention is enabled(true) or disabled(false) on the camera. 51 | - audioRecordingEnabled (boolean): Boolean indicating if audio recording is enabled(true) or disabled(false) on the camera 52 | - restrictedBandwidthModeEnabled (boolean): Boolean indicating if restricted bandwidth is enabled(true) or disabled(false) on the camera. This setting does not apply to MV2 cameras. 53 | - quality (string): Quality of the camera. Can be one of 'Standard', 'High', 'Enhanced' or 'Ultra'. Not all qualities are supported by every camera model. 54 | - resolution (string): Resolution of the camera. Can be one of '1280x720', '1920x1080', '1080x1080', '2112x2112', '2880x2880', '2688x1512' or '3840x2160'.Not all resolutions are supported by every camera model. 55 | - motionDetectorVersion (integer): The version of the motion detector that will be used by the camera. Only applies to Gen 2 cameras. Defaults to v2. 56 | """ 57 | 58 | kwargs.update(locals()) 59 | 60 | if 'quality' in kwargs: 61 | options = ['Enhanced', 'High', 'Standard', 'Ultra'] 62 | assert kwargs['quality'] in options, f'''"quality" cannot be "{kwargs['quality']}", & must be set to one of: {options}''' 63 | if 'resolution' in kwargs: 64 | options = ['1080x1080', '1280x720', '1920x1080', '2112x2112', '2688x1512', '2880x2880', '3840x2160'] 65 | assert kwargs['resolution'] in options, f'''"resolution" cannot be "{kwargs['resolution']}", & must be set to one of: {options}''' 66 | if 'motionDetectorVersion' in kwargs: 67 | options = [1, 2] 68 | assert kwargs['motionDetectorVersion'] in options, f'''"motionDetectorVersion" cannot be "{kwargs['motionDetectorVersion']}", & must be set to one of: {options}''' 69 | 70 | metadata = { 71 | 'tags': ['camera', 'configure', 'qualityAndRetention'], 72 | 'operation': 'updateDeviceCameraQualityAndRetention' 73 | } 74 | resource = f'/devices/{serial}/camera/qualityAndRetention' 75 | 76 | body_params = ['profileId', 'motionBasedRetentionEnabled', 'audioRecordingEnabled', 'restrictedBandwidthModeEnabled', 'quality', 'resolution', 'motionDetectorVersion', ] 77 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 78 | action = { 79 | "resource": resource, 80 | "operation": "update", 81 | "body": payload 82 | } 83 | return action 84 | 85 | 86 | 87 | 88 | 89 | 90 | def updateDeviceCameraSense(self, serial: str, **kwargs): 91 | """ 92 | **Update sense settings for the given camera** 93 | https://developer.cisco.com/meraki/api-v1/#!update-device-camera-sense 94 | 95 | - serial (string): Serial 96 | - senseEnabled (boolean): Boolean indicating if sense(license) is enabled(true) or disabled(false) on the camera 97 | - mqttBrokerId (string): The ID of the MQTT broker to be enabled on the camera. A value of null will disable MQTT on the camera 98 | - audioDetection (object): The details of the audio detection config. 99 | - detectionModelId (string): The ID of the object detection model 100 | """ 101 | 102 | kwargs.update(locals()) 103 | 104 | metadata = { 105 | 'tags': ['camera', 'configure', 'sense'], 106 | 'operation': 'updateDeviceCameraSense' 107 | } 108 | resource = f'/devices/{serial}/camera/sense' 109 | 110 | body_params = ['senseEnabled', 'mqttBrokerId', 'audioDetection', 'detectionModelId', ] 111 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 112 | action = { 113 | "resource": resource, 114 | "operation": "update", 115 | "body": payload 116 | } 117 | return action 118 | 119 | 120 | 121 | 122 | 123 | 124 | def updateDeviceCameraVideoSettings(self, serial: str, **kwargs): 125 | """ 126 | **Update video settings for the given camera** 127 | https://developer.cisco.com/meraki/api-v1/#!update-device-camera-video-settings 128 | 129 | - serial (string): Serial 130 | - externalRtspEnabled (boolean): Boolean indicating if external rtsp stream is exposed 131 | """ 132 | 133 | kwargs.update(locals()) 134 | 135 | metadata = { 136 | 'tags': ['camera', 'configure', 'video', 'settings'], 137 | 'operation': 'updateDeviceCameraVideoSettings' 138 | } 139 | resource = f'/devices/{serial}/camera/video/settings' 140 | 141 | body_params = ['externalRtspEnabled', ] 142 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 143 | action = { 144 | "resource": resource, 145 | "operation": "update", 146 | "body": payload 147 | } 148 | return action 149 | 150 | 151 | 152 | 153 | 154 | 155 | def updateDeviceCameraWirelessProfiles(self, serial: str, ids: dict): 156 | """ 157 | **Assign wireless profiles to the given camera. Incremental updates are not supported, all profile assignment need to be supplied at once.** 158 | https://developer.cisco.com/meraki/api-v1/#!update-device-camera-wireless-profiles 159 | 160 | - serial (string): Serial 161 | - ids (object): The ids of the wireless profile to assign to the given camera 162 | """ 163 | 164 | kwargs = locals() 165 | 166 | metadata = { 167 | 'tags': ['camera', 'configure', 'wirelessProfiles'], 168 | 'operation': 'updateDeviceCameraWirelessProfiles' 169 | } 170 | resource = f'/devices/{serial}/camera/wirelessProfiles' 171 | 172 | body_params = ['ids', ] 173 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 174 | action = { 175 | "resource": resource, 176 | "operation": "update", 177 | "body": payload 178 | } 179 | return action 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /meraki/api/batch/campusGateway.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchCampusGateway(object): 5 | def __init__(self): 6 | super(ActionBatchCampusGateway, self).__init__() 7 | 8 | 9 | 10 | def createNetworkCampusGatewayCluster(self, networkId: str, name: str, uplinks: list, tunnels: list, nameservers: dict, portChannels: list, **kwargs): 11 | """ 12 | **Create a cluster and add campus gateways to it** 13 | https://developer.cisco.com/meraki/api-v1/#!create-network-campus-gateway-cluster 14 | 15 | - networkId (string): Network ID 16 | - name (string): Name of the new cluster 17 | - uplinks (array): Uplink interface settings of the cluster 18 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 19 | - nameservers (object): Nameservers of the cluster 20 | - portChannels (array): Port channel settings of the cluster 21 | - devices (array): Devices to be added to the cluster 22 | - notes (string): Notes about cluster with max size of 511 characters allowed 23 | """ 24 | 25 | kwargs.update(locals()) 26 | 27 | metadata = { 28 | 'tags': ['campusGateway', 'configure', 'clusters'], 29 | 'operation': 'createNetworkCampusGatewayCluster' 30 | } 31 | resource = f'/networks/{networkId}/campusGateway/clusters' 32 | 33 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 34 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 35 | action = { 36 | "resource": resource, 37 | "operation": "create", 38 | "body": payload 39 | } 40 | return action 41 | 42 | 43 | 44 | 45 | 46 | 47 | def updateNetworkCampusGatewayCluster(self, networkId: str, clusterId: str, **kwargs): 48 | """ 49 | **Update a cluster and add/remove campus gateways to/from it** 50 | https://developer.cisco.com/meraki/api-v1/#!update-network-campus-gateway-cluster 51 | 52 | - networkId (string): Network ID 53 | - clusterId (string): Cluster ID 54 | - name (string): Name of the cluster 55 | - uplinks (array): Uplink interface settings of the cluster 56 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 57 | - nameservers (object): Nameservers of the cluster 58 | - portChannels (array): Port channel settings of the cluster 59 | - devices (array): Devices in the cluster 60 | - notes (string): Notes about cluster with max size of 511 characters allowed 61 | """ 62 | 63 | kwargs.update(locals()) 64 | 65 | metadata = { 66 | 'tags': ['campusGateway', 'configure', 'clusters'], 67 | 'operation': 'updateNetworkCampusGatewayCluster' 68 | } 69 | resource = f'/networks/{networkId}/campusGateway/clusters/{clusterId}' 70 | 71 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 72 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 73 | action = { 74 | "resource": resource, 75 | "operation": "update", 76 | "body": payload 77 | } 78 | return action 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /meraki/api/batch/devices.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchDevices(object): 5 | def __init__(self): 6 | super(ActionBatchDevices, self).__init__() 7 | 8 | 9 | 10 | def updateDevice(self, serial: str, **kwargs): 11 | """ 12 | **Update the attributes of a device** 13 | https://developer.cisco.com/meraki/api-v1/#!update-device 14 | 15 | - serial (string): Serial 16 | - name (string): The name of a device 17 | - tags (array): The list of tags of a device 18 | - lat (number): The latitude of a device 19 | - lng (number): The longitude of a device 20 | - address (string): The address of a device 21 | - notes (string): The notes for the device. String. Limited to 255 characters. 22 | - moveMapMarker (boolean): Whether or not to set the latitude and longitude of a device based on the new address. Only applies when lat and lng are not specified. 23 | - switchProfileId (string): The ID of a switch template to bind to the device (for available switch templates, see the 'Switch Templates' endpoint). Use null to unbind the switch device from the current profile. For a device to be bindable to a switch template, it must (1) be a switch, and (2) belong to a network that is bound to a configuration template. 24 | - floorPlanId (string): The floor plan to associate to this device. null disassociates the device from the floorplan. 25 | """ 26 | 27 | kwargs.update(locals()) 28 | 29 | metadata = { 30 | 'tags': ['devices', 'configure'], 31 | 'operation': 'updateDevice' 32 | } 33 | resource = f'/devices/{serial}' 34 | 35 | body_params = ['name', 'tags', 'lat', 'lng', 'address', 'notes', 'moveMapMarker', 'switchProfileId', 'floorPlanId', ] 36 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 37 | action = { 38 | "resource": resource, 39 | "operation": "update", 40 | "body": payload 41 | } 42 | return action 43 | 44 | 45 | 46 | 47 | 48 | 49 | def createDeviceLiveToolsLedsBlink(self, serial: str, duration: int, **kwargs): 50 | """ 51 | **Enqueue a job to blink LEDs on a device. This endpoint has a rate limit of one request every 10 seconds.** 52 | https://developer.cisco.com/meraki/api-v1/#!create-device-live-tools-leds-blink 53 | 54 | - serial (string): Serial 55 | - duration (integer): The duration in seconds to blink LEDs. 56 | - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret 57 | """ 58 | 59 | kwargs.update(locals()) 60 | 61 | metadata = { 62 | 'tags': ['devices', 'liveTools', 'leds', 'blink'], 63 | 'operation': 'createDeviceLiveToolsLedsBlink' 64 | } 65 | resource = f'/devices/{serial}/liveTools/leds/blink' 66 | 67 | body_params = ['duration', 'callback', ] 68 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 69 | action = { 70 | "resource": resource, 71 | "operation": "blink", 72 | "body": payload 73 | } 74 | return action 75 | 76 | 77 | 78 | 79 | 80 | 81 | def createDeviceLiveToolsThroughputTest(self, serial: str, **kwargs): 82 | """ 83 | **Enqueue a job to test a device throughput, the test will run for 10 secs to test throughput. This endpoint has a rate limit of one request every five seconds per device.** 84 | https://developer.cisco.com/meraki/api-v1/#!create-device-live-tools-throughput-test 85 | 86 | - serial (string): Serial 87 | - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret 88 | """ 89 | 90 | kwargs.update(locals()) 91 | 92 | metadata = { 93 | 'tags': ['devices', 'liveTools', 'throughputTest'], 94 | 'operation': 'createDeviceLiveToolsThroughputTest' 95 | } 96 | resource = f'/devices/{serial}/liveTools/throughputTest' 97 | 98 | body_params = ['callback', ] 99 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 100 | action = { 101 | "resource": resource, 102 | "operation": "test", 103 | "body": payload 104 | } 105 | return action 106 | 107 | 108 | 109 | 110 | 111 | 112 | def updateDeviceManagementInterface(self, serial: str, **kwargs): 113 | """ 114 | **Update the management interface settings for a device** 115 | https://developer.cisco.com/meraki/api-v1/#!update-device-management-interface 116 | 117 | - serial (string): Serial 118 | - wan1 (object): WAN 1 settings 119 | - wan2 (object): WAN 2 settings (only for MX devices) 120 | """ 121 | 122 | kwargs.update(locals()) 123 | 124 | metadata = { 125 | 'tags': ['devices', 'configure', 'managementInterface'], 126 | 'operation': 'updateDeviceManagementInterface' 127 | } 128 | resource = f'/devices/{serial}/managementInterface' 129 | 130 | body_params = ['wan1', 'wan2', ] 131 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 132 | action = { 133 | "resource": resource, 134 | "operation": "update", 135 | "body": payload 136 | } 137 | return action 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /meraki/api/batch/insight.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchInsight(object): 5 | def __init__(self): 6 | super(ActionBatchInsight, self).__init__() 7 | 8 | 9 | 10 | def createOrganizationInsightMonitoredMediaServer(self, organizationId: str, name: str, address: str, **kwargs): 11 | """ 12 | **Add a media server to be monitored for this organization. Only valid for organizations with Meraki Insight.** 13 | https://developer.cisco.com/meraki/api-v1/#!create-organization-insight-monitored-media-server 14 | 15 | - organizationId (string): Organization ID 16 | - name (string): The name of the VoIP provider 17 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 18 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 19 | """ 20 | 21 | kwargs.update(locals()) 22 | 23 | metadata = { 24 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 25 | 'operation': 'createOrganizationInsightMonitoredMediaServer' 26 | } 27 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers' 28 | 29 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 30 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 31 | action = { 32 | "resource": resource, 33 | "operation": "create", 34 | "body": payload 35 | } 36 | return action 37 | 38 | 39 | 40 | 41 | 42 | 43 | def updateOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str, **kwargs): 44 | """ 45 | **Update a monitored media server for this organization. Only valid for organizations with Meraki Insight.** 46 | https://developer.cisco.com/meraki/api-v1/#!update-organization-insight-monitored-media-server 47 | 48 | - organizationId (string): Organization ID 49 | - monitoredMediaServerId (string): Monitored media server ID 50 | - name (string): The name of the VoIP provider 51 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 52 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 53 | """ 54 | 55 | kwargs.update(locals()) 56 | 57 | metadata = { 58 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 59 | 'operation': 'updateOrganizationInsightMonitoredMediaServer' 60 | } 61 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 62 | 63 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 64 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 65 | action = { 66 | "resource": resource, 67 | "operation": "update", 68 | "body": payload 69 | } 70 | return action 71 | 72 | 73 | 74 | 75 | 76 | 77 | def deleteOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str): 78 | """ 79 | **Delete a monitored media server from this organization. Only valid for organizations with Meraki Insight.** 80 | https://developer.cisco.com/meraki/api-v1/#!delete-organization-insight-monitored-media-server 81 | 82 | - organizationId (string): Organization ID 83 | - monitoredMediaServerId (string): Monitored media server ID 84 | """ 85 | 86 | metadata = { 87 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 88 | 'operation': 'deleteOrganizationInsightMonitoredMediaServer' 89 | } 90 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 91 | 92 | action = { 93 | "resource": resource, 94 | "operation": "destroy", 95 | } 96 | return action 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /meraki/api/batch/licensing.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchLicensing(object): 5 | def __init__(self): 6 | super(ActionBatchLicensing, self).__init__() 7 | 8 | -------------------------------------------------------------------------------- /meraki/api/batch/sensor.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchSensor(object): 5 | def __init__(self): 6 | super(ActionBatchSensor, self).__init__() 7 | 8 | 9 | 10 | def createDeviceSensorCommand(self, serial: str, operation: str): 11 | """ 12 | **Sends a command to a sensor** 13 | https://developer.cisco.com/meraki/api-v1/#!create-device-sensor-command 14 | 15 | - serial (string): Serial 16 | - operation (string): Operation to run on the sensor. 'enableDownstreamPower', 'disableDownstreamPower', and 'cycleDownstreamPower' turn power on/off to the device that is connected downstream of an MT40 power monitor. 'refreshData' causes an MT15 or MT40 device to upload its latest readings so that they are immediately available in the Dashboard API. 17 | """ 18 | 19 | kwargs = locals() 20 | 21 | if 'operation' in kwargs: 22 | options = ['cycleDownstreamPower', 'disableDownstreamPower', 'enableDownstreamPower', 'refreshData'] 23 | assert kwargs['operation'] in options, f'''"operation" cannot be "{kwargs['operation']}", & must be set to one of: {options}''' 24 | 25 | metadata = { 26 | 'tags': ['sensor', 'configure', 'commands'], 27 | 'operation': 'createDeviceSensorCommand' 28 | } 29 | resource = f'/devices/{serial}/sensor/commands' 30 | 31 | body_params = ['operation', ] 32 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 33 | action = { 34 | "resource": resource, 35 | "operation": "create", 36 | "body": payload 37 | } 38 | return action 39 | 40 | 41 | 42 | 43 | 44 | 45 | def updateDeviceSensorRelationships(self, serial: str, **kwargs): 46 | """ 47 | **Assign one or more sensor roles to a given sensor or camera device.** 48 | https://developer.cisco.com/meraki/api-v1/#!update-device-sensor-relationships 49 | 50 | - serial (string): Serial 51 | - livestream (object): A role defined between an MT sensor and an MV camera that adds the camera's livestream to the sensor's details page. Snapshots from the camera will also appear in alert notifications that the sensor triggers. 52 | """ 53 | 54 | kwargs.update(locals()) 55 | 56 | metadata = { 57 | 'tags': ['sensor', 'configure', 'relationships'], 58 | 'operation': 'updateDeviceSensorRelationships' 59 | } 60 | resource = f'/devices/{serial}/sensor/relationships' 61 | 62 | body_params = ['livestream', ] 63 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 64 | action = { 65 | "resource": resource, 66 | "operation": "update", 67 | "body": payload 68 | } 69 | return action 70 | 71 | 72 | 73 | 74 | 75 | 76 | def createNetworkSensorAlertsProfile(self, networkId: str, name: str, conditions: list, **kwargs): 77 | """ 78 | **Creates a sensor alert profile for a network.** 79 | https://developer.cisco.com/meraki/api-v1/#!create-network-sensor-alerts-profile 80 | 81 | - networkId (string): Network ID 82 | - name (string): Name of the sensor alert profile. 83 | - conditions (array): List of conditions that will cause the profile to send an alert. 84 | - schedule (object): The sensor schedule to use with the alert profile. 85 | - recipients (object): List of recipients that will receive the alert. 86 | - serials (array): List of device serials assigned to this sensor alert profile. 87 | - includeSensorUrl (boolean): Include dashboard link to sensor in messages (default: true). 88 | - message (string): A custom message that will appear in email and text message alerts. 89 | """ 90 | 91 | kwargs.update(locals()) 92 | 93 | metadata = { 94 | 'tags': ['sensor', 'configure', 'alerts', 'profiles'], 95 | 'operation': 'createNetworkSensorAlertsProfile' 96 | } 97 | resource = f'/networks/{networkId}/sensor/alerts/profiles' 98 | 99 | body_params = ['name', 'schedule', 'conditions', 'recipients', 'serials', 'includeSensorUrl', 'message', ] 100 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 101 | action = { 102 | "resource": resource, 103 | "operation": "create", 104 | "body": payload 105 | } 106 | return action 107 | 108 | 109 | 110 | 111 | 112 | 113 | def updateNetworkSensorAlertsProfile(self, networkId: str, id: str, **kwargs): 114 | """ 115 | **Updates a sensor alert profile for a network.** 116 | https://developer.cisco.com/meraki/api-v1/#!update-network-sensor-alerts-profile 117 | 118 | - networkId (string): Network ID 119 | - id (string): ID 120 | - name (string): Name of the sensor alert profile. 121 | - schedule (object): The sensor schedule to use with the alert profile. 122 | - conditions (array): List of conditions that will cause the profile to send an alert. 123 | - recipients (object): List of recipients that will receive the alert. 124 | - serials (array): List of device serials assigned to this sensor alert profile. 125 | - includeSensorUrl (boolean): Include dashboard link to sensor in messages (default: true). 126 | - message (string): A custom message that will appear in email and text message alerts. 127 | """ 128 | 129 | kwargs.update(locals()) 130 | 131 | metadata = { 132 | 'tags': ['sensor', 'configure', 'alerts', 'profiles'], 133 | 'operation': 'updateNetworkSensorAlertsProfile' 134 | } 135 | resource = f'/networks/{networkId}/sensor/alerts/profiles/{id}' 136 | 137 | body_params = ['name', 'schedule', 'conditions', 'recipients', 'serials', 'includeSensorUrl', 'message', ] 138 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 139 | action = { 140 | "resource": resource, 141 | "operation": "update", 142 | "body": payload 143 | } 144 | return action 145 | 146 | 147 | 148 | 149 | 150 | 151 | def deleteNetworkSensorAlertsProfile(self, networkId: str, id: str): 152 | """ 153 | **Deletes a sensor alert profile from a network.** 154 | https://developer.cisco.com/meraki/api-v1/#!delete-network-sensor-alerts-profile 155 | 156 | - networkId (string): Network ID 157 | - id (string): ID 158 | """ 159 | 160 | metadata = { 161 | 'tags': ['sensor', 'configure', 'alerts', 'profiles'], 162 | 'operation': 'deleteNetworkSensorAlertsProfile' 163 | } 164 | resource = f'/networks/{networkId}/sensor/alerts/profiles/{id}' 165 | 166 | action = { 167 | "resource": resource, 168 | "operation": "destroy", 169 | } 170 | return action 171 | 172 | 173 | 174 | 175 | 176 | 177 | def updateNetworkSensorMqttBroker(self, networkId: str, mqttBrokerId: str, enabled: bool): 178 | """ 179 | **Update the sensor settings of an MQTT broker. To update the broker itself, use /networks/{networkId}/mqttBrokers/{mqttBrokerId}.** 180 | https://developer.cisco.com/meraki/api-v1/#!update-network-sensor-mqtt-broker 181 | 182 | - networkId (string): Network ID 183 | - mqttBrokerId (string): Mqtt broker ID 184 | - enabled (boolean): Set to true to enable MQTT broker for sensor network 185 | """ 186 | 187 | kwargs = locals() 188 | 189 | metadata = { 190 | 'tags': ['sensor', 'configure', 'mqttBrokers'], 191 | 'operation': 'updateNetworkSensorMqttBroker' 192 | } 193 | resource = f'/networks/{networkId}/sensor/mqttBrokers/{mqttBrokerId}' 194 | 195 | body_params = ['enabled', ] 196 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 197 | action = { 198 | "resource": resource, 199 | "operation": "update", 200 | "body": payload 201 | } 202 | return action 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /meraki/api/batch/sm.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchSm(object): 5 | def __init__(self): 6 | super(ActionBatchSm, self).__init__() 7 | 8 | 9 | 10 | def deleteNetworkSmUserAccessDevice(self, networkId: str, userAccessDeviceId: str): 11 | """ 12 | **Delete a User Access Device** 13 | https://developer.cisco.com/meraki/api-v1/#!delete-network-sm-user-access-device 14 | 15 | - networkId (string): Network ID 16 | - userAccessDeviceId (string): User access device ID 17 | """ 18 | 19 | metadata = { 20 | 'tags': ['sm', 'configure', 'userAccessDevices'], 21 | 'operation': 'deleteNetworkSmUserAccessDevice' 22 | } 23 | resource = f'/networks/{networkId}/sm/userAccessDevices/{userAccessDeviceId}' 24 | 25 | action = { 26 | "resource": resource, 27 | "operation": "destroy", 28 | } 29 | return action 30 | 31 | 32 | 33 | 34 | 35 | 36 | def createOrganizationSmAdminsRole(self, organizationId: str, name: str, **kwargs): 37 | """ 38 | **Create a Limited Access Role** 39 | https://developer.cisco.com/meraki/api-v1/#!create-organization-sm-admins-role 40 | 41 | - organizationId (string): Organization ID 42 | - name (string): The name of the Limited Access Role 43 | - scope (string): The scope of the Limited Access Role 44 | - tags (array): The tags of the Limited Access Role 45 | """ 46 | 47 | kwargs.update(locals()) 48 | 49 | if 'scope' in kwargs: 50 | options = ['all_tags', 'some', 'without_all_tags', 'without_some'] 51 | assert kwargs['scope'] in options, f'''"scope" cannot be "{kwargs['scope']}", & must be set to one of: {options}''' 52 | 53 | metadata = { 54 | 'tags': ['sm', 'configure', 'admins', 'roles'], 55 | 'operation': 'createOrganizationSmAdminsRole' 56 | } 57 | resource = f'/organizations/{organizationId}/sm/admins/roles' 58 | 59 | body_params = ['name', 'scope', 'tags', ] 60 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 61 | action = { 62 | "resource": resource, 63 | "operation": "create", 64 | "body": payload 65 | } 66 | return action 67 | 68 | 69 | 70 | 71 | 72 | 73 | def updateOrganizationSmAdminsRole(self, organizationId: str, roleId: str, **kwargs): 74 | """ 75 | **Update a Limited Access Role** 76 | https://developer.cisco.com/meraki/api-v1/#!update-organization-sm-admins-role 77 | 78 | - organizationId (string): Organization ID 79 | - roleId (string): Role ID 80 | - name (string): The name of the Limited Access Role 81 | - scope (string): The scope of the Limited Access Role 82 | - tags (array): The tags of the Limited Access Role 83 | """ 84 | 85 | kwargs.update(locals()) 86 | 87 | if 'scope' in kwargs: 88 | options = ['all_tags', 'some', 'without_all_tags', 'without_some'] 89 | assert kwargs['scope'] in options, f'''"scope" cannot be "{kwargs['scope']}", & must be set to one of: {options}''' 90 | 91 | metadata = { 92 | 'tags': ['sm', 'configure', 'admins', 'roles'], 93 | 'operation': 'updateOrganizationSmAdminsRole' 94 | } 95 | resource = f'/organizations/{organizationId}/sm/admins/roles/{roleId}' 96 | 97 | body_params = ['name', 'scope', 'tags', ] 98 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 99 | action = { 100 | "resource": resource, 101 | "operation": "update", 102 | "body": payload 103 | } 104 | return action 105 | 106 | 107 | 108 | 109 | 110 | 111 | def deleteOrganizationSmAdminsRole(self, organizationId: str, roleId: str): 112 | """ 113 | **Delete a Limited Access Role** 114 | https://developer.cisco.com/meraki/api-v1/#!delete-organization-sm-admins-role 115 | 116 | - organizationId (string): Organization ID 117 | - roleId (string): Role ID 118 | """ 119 | 120 | metadata = { 121 | 'tags': ['sm', 'configure', 'admins', 'roles'], 122 | 'operation': 'deleteOrganizationSmAdminsRole' 123 | } 124 | resource = f'/organizations/{organizationId}/sm/admins/roles/{roleId}' 125 | 126 | action = { 127 | "resource": resource, 128 | "operation": "destroy", 129 | } 130 | return action 131 | 132 | 133 | 134 | 135 | 136 | 137 | def updateOrganizationSmSentryPoliciesAssignments(self, organizationId: str, items: list): 138 | """ 139 | **Update an Organizations Sentry Policies using the provided list. Sentry Policies are ordered in descending order of priority (i.e. highest priority at the bottom, this is opposite the Dashboard UI). Policies not present in the request will be deleted.** 140 | https://developer.cisco.com/meraki/api-v1/#!update-organization-sm-sentry-policies-assignments 141 | 142 | - organizationId (string): Organization ID 143 | - items (array): Sentry Group Policies for the Organization keyed by Network Id 144 | """ 145 | 146 | kwargs = locals() 147 | 148 | metadata = { 149 | 'tags': ['sm', 'configure', 'sentry', 'policies', 'assignments'], 150 | 'operation': 'updateOrganizationSmSentryPoliciesAssignments' 151 | } 152 | resource = f'/organizations/{organizationId}/sm/sentry/policies/assignments' 153 | 154 | body_params = ['items', ] 155 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 156 | action = { 157 | "resource": resource, 158 | "operation": "update", 159 | "body": payload 160 | } 161 | return action 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /meraki/api/batch/spaces.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchSpaces(object): 5 | def __init__(self): 6 | super(ActionBatchSpaces, self).__init__() 7 | 8 | 9 | 10 | def removeOrganizationSpacesIntegration(self, organizationId: str): 11 | """ 12 | **Remove the Spaces integration from Meraki** 13 | https://developer.cisco.com/meraki/api-v1/#!remove-organization-spaces-integration 14 | 15 | - organizationId (string): Organization ID 16 | """ 17 | 18 | metadata = { 19 | 'tags': ['organizations', 'configure', 'spaces', 'integration'], 20 | 'operation': 'removeOrganizationSpacesIntegration' 21 | } 22 | resource = f'/organizations/{organizationId}/spaces/integration/remove' 23 | 24 | action = { 25 | "resource": resource, 26 | "operation": "integration", 27 | } 28 | return action 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /meraki/api/batch/wirelessController.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class ActionBatchWirelessController(object): 5 | def __init__(self): 6 | super(ActionBatchWirelessController, self).__init__() 7 | 8 | -------------------------------------------------------------------------------- /meraki/api/campusGateway.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class CampusGateway(object): 5 | def __init__(self, session): 6 | super(CampusGateway, self).__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def createNetworkCampusGatewayCluster(self, networkId: str, name: str, uplinks: list, tunnels: list, nameservers: dict, portChannels: list, **kwargs): 12 | """ 13 | **Create a cluster and add campus gateways to it** 14 | https://developer.cisco.com/meraki/api-v1/#!create-network-campus-gateway-cluster 15 | 16 | - networkId (string): Network ID 17 | - name (string): Name of the new cluster 18 | - uplinks (array): Uplink interface settings of the cluster 19 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 20 | - nameservers (object): Nameservers of the cluster 21 | - portChannels (array): Port channel settings of the cluster 22 | - devices (array): Devices to be added to the cluster 23 | - notes (string): Notes about cluster with max size of 511 characters allowed 24 | """ 25 | 26 | kwargs.update(locals()) 27 | 28 | metadata = { 29 | 'tags': ['campusGateway', 'configure', 'clusters'], 30 | 'operation': 'createNetworkCampusGatewayCluster' 31 | } 32 | networkId = urllib.parse.quote(str(networkId), safe='') 33 | resource = f'/networks/{networkId}/campusGateway/clusters' 34 | 35 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 36 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 37 | 38 | return self._session.post(metadata, resource, payload) 39 | 40 | 41 | 42 | def updateNetworkCampusGatewayCluster(self, networkId: str, clusterId: str, **kwargs): 43 | """ 44 | **Update a cluster and add/remove campus gateways to/from it** 45 | https://developer.cisco.com/meraki/api-v1/#!update-network-campus-gateway-cluster 46 | 47 | - networkId (string): Network ID 48 | - clusterId (string): Cluster ID 49 | - name (string): Name of the cluster 50 | - uplinks (array): Uplink interface settings of the cluster 51 | - tunnels (array): Tunnel interface settings of the cluster: Reuse uplink or specify tunnel interface 52 | - nameservers (object): Nameservers of the cluster 53 | - portChannels (array): Port channel settings of the cluster 54 | - devices (array): Devices in the cluster 55 | - notes (string): Notes about cluster with max size of 511 characters allowed 56 | """ 57 | 58 | kwargs.update(locals()) 59 | 60 | metadata = { 61 | 'tags': ['campusGateway', 'configure', 'clusters'], 62 | 'operation': 'updateNetworkCampusGatewayCluster' 63 | } 64 | networkId = urllib.parse.quote(str(networkId), safe='') 65 | clusterId = urllib.parse.quote(str(clusterId), safe='') 66 | resource = f'/networks/{networkId}/campusGateway/clusters/{clusterId}' 67 | 68 | body_params = ['name', 'uplinks', 'tunnels', 'nameservers', 'portChannels', 'devices', 'notes', ] 69 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 70 | 71 | return self._session.put(metadata, resource, payload) 72 | 73 | 74 | 75 | def getOrganizationCampusGatewayDevicesUplinksLocalOverridesByDevice(self, organizationId: str, total_pages=1, direction='next', **kwargs): 76 | """ 77 | **Uplink overrides configured locally on Campus Gateway devices in an organization.** 78 | https://developer.cisco.com/meraki/api-v1/#!get-organization-campus-gateway-devices-uplinks-local-overrides-by-device 79 | 80 | - organizationId (string): Organization ID 81 | - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages 82 | - direction (string): direction to paginate, either "next" (default) or "prev" page 83 | - serials (array): A list of serial numbers. The returned devices will be filtered to only include these serials. 84 | - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. 85 | - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 86 | - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. 87 | """ 88 | 89 | kwargs.update(locals()) 90 | 91 | metadata = { 92 | 'tags': ['campusGateway', 'configure', 'devices', 'uplinks', 'localOverrides', 'byDevice'], 93 | 'operation': 'getOrganizationCampusGatewayDevicesUplinksLocalOverridesByDevice' 94 | } 95 | organizationId = urllib.parse.quote(str(organizationId), safe='') 96 | resource = f'/organizations/{organizationId}/campusGateway/devices/uplinks/localOverrides/byDevice' 97 | 98 | query_params = ['serials', 'perPage', 'startingAfter', 'endingBefore', ] 99 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 100 | 101 | array_params = ['serials', ] 102 | for k, v in kwargs.items(): 103 | if k.strip() in array_params: 104 | params[f'{k.strip()}[]'] = kwargs[f'{k}'] 105 | params.pop(k.strip()) 106 | 107 | return self._session.get_pages(metadata, resource, params, total_pages, direction) 108 | 109 | -------------------------------------------------------------------------------- /meraki/api/insight.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class Insight(object): 5 | def __init__(self, session): 6 | super(Insight, self).__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def getNetworkInsightApplicationHealthByTime(self, networkId: str, applicationId: str, **kwargs): 12 | """ 13 | **Get application health by time** 14 | https://developer.cisco.com/meraki/api-v1/#!get-network-insight-application-health-by-time 15 | 16 | - networkId (string): Network ID 17 | - applicationId (string): Application ID 18 | - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 7 days from today. 19 | - t1 (string): The end of the timespan for the data. t1 can be a maximum of 7 days after t0. 20 | - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 7 days. The default is 2 hours. 21 | - resolution (integer): The time resolution in seconds for returned data. The valid resolutions are: 60, 300, 3600, 86400. The default is 300. 22 | """ 23 | 24 | kwargs.update(locals()) 25 | 26 | metadata = { 27 | 'tags': ['insight', 'monitor', 'applications', 'healthByTime'], 28 | 'operation': 'getNetworkInsightApplicationHealthByTime' 29 | } 30 | networkId = urllib.parse.quote(str(networkId), safe='') 31 | applicationId = urllib.parse.quote(str(applicationId), safe='') 32 | resource = f'/networks/{networkId}/insight/applications/{applicationId}/healthByTime' 33 | 34 | query_params = ['t0', 't1', 'timespan', 'resolution', ] 35 | params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} 36 | 37 | return self._session.get(metadata, resource, params) 38 | 39 | 40 | 41 | def getOrganizationInsightApplications(self, organizationId: str): 42 | """ 43 | **List all Insight tracked applications** 44 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-applications 45 | 46 | - organizationId (string): Organization ID 47 | """ 48 | 49 | metadata = { 50 | 'tags': ['insight', 'configure', 'applications'], 51 | 'operation': 'getOrganizationInsightApplications' 52 | } 53 | organizationId = urllib.parse.quote(str(organizationId), safe='') 54 | resource = f'/organizations/{organizationId}/insight/applications' 55 | 56 | return self._session.get(metadata, resource) 57 | 58 | 59 | 60 | def getOrganizationInsightMonitoredMediaServers(self, organizationId: str): 61 | """ 62 | **List the monitored media servers for this organization** 63 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-monitored-media-servers 64 | 65 | - organizationId (string): Organization ID 66 | """ 67 | 68 | metadata = { 69 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 70 | 'operation': 'getOrganizationInsightMonitoredMediaServers' 71 | } 72 | organizationId = urllib.parse.quote(str(organizationId), safe='') 73 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers' 74 | 75 | return self._session.get(metadata, resource) 76 | 77 | 78 | 79 | def createOrganizationInsightMonitoredMediaServer(self, organizationId: str, name: str, address: str, **kwargs): 80 | """ 81 | **Add a media server to be monitored for this organization** 82 | https://developer.cisco.com/meraki/api-v1/#!create-organization-insight-monitored-media-server 83 | 84 | - organizationId (string): Organization ID 85 | - name (string): The name of the VoIP provider 86 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 87 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 88 | """ 89 | 90 | kwargs.update(locals()) 91 | 92 | metadata = { 93 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 94 | 'operation': 'createOrganizationInsightMonitoredMediaServer' 95 | } 96 | organizationId = urllib.parse.quote(str(organizationId), safe='') 97 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers' 98 | 99 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 100 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 101 | 102 | return self._session.post(metadata, resource, payload) 103 | 104 | 105 | 106 | def getOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str): 107 | """ 108 | **Return a monitored media server for this organization** 109 | https://developer.cisco.com/meraki/api-v1/#!get-organization-insight-monitored-media-server 110 | 111 | - organizationId (string): Organization ID 112 | - monitoredMediaServerId (string): Monitored media server ID 113 | """ 114 | 115 | metadata = { 116 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 117 | 'operation': 'getOrganizationInsightMonitoredMediaServer' 118 | } 119 | organizationId = urllib.parse.quote(str(organizationId), safe='') 120 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 121 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 122 | 123 | return self._session.get(metadata, resource) 124 | 125 | 126 | 127 | def updateOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str, **kwargs): 128 | """ 129 | **Update a monitored media server for this organization** 130 | https://developer.cisco.com/meraki/api-v1/#!update-organization-insight-monitored-media-server 131 | 132 | - organizationId (string): Organization ID 133 | - monitoredMediaServerId (string): Monitored media server ID 134 | - name (string): The name of the VoIP provider 135 | - address (string): The IP address (IPv4 only) or hostname of the media server to monitor 136 | - bestEffortMonitoringEnabled (boolean): Indicates that if the media server doesn't respond to ICMP pings, the nearest hop will be used in its stead. 137 | """ 138 | 139 | kwargs.update(locals()) 140 | 141 | metadata = { 142 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 143 | 'operation': 'updateOrganizationInsightMonitoredMediaServer' 144 | } 145 | organizationId = urllib.parse.quote(str(organizationId), safe='') 146 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 147 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 148 | 149 | body_params = ['name', 'address', 'bestEffortMonitoringEnabled', ] 150 | payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} 151 | 152 | return self._session.put(metadata, resource, payload) 153 | 154 | 155 | 156 | def deleteOrganizationInsightMonitoredMediaServer(self, organizationId: str, monitoredMediaServerId: str): 157 | """ 158 | **Delete a monitored media server from this organization** 159 | https://developer.cisco.com/meraki/api-v1/#!delete-organization-insight-monitored-media-server 160 | 161 | - organizationId (string): Organization ID 162 | - monitoredMediaServerId (string): Monitored media server ID 163 | """ 164 | 165 | metadata = { 166 | 'tags': ['insight', 'configure', 'monitoredMediaServers'], 167 | 'operation': 'deleteOrganizationInsightMonitoredMediaServer' 168 | } 169 | organizationId = urllib.parse.quote(str(organizationId), safe='') 170 | monitoredMediaServerId = urllib.parse.quote(str(monitoredMediaServerId), safe='') 171 | resource = f'/organizations/{organizationId}/insight/monitoredMediaServers/{monitoredMediaServerId}' 172 | 173 | return self._session.delete(metadata, resource) 174 | 175 | -------------------------------------------------------------------------------- /meraki/api/spaces.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | 4 | class Spaces(object): 5 | def __init__(self, session): 6 | super(Spaces, self).__init__() 7 | self._session = session 8 | 9 | 10 | 11 | def removeOrganizationSpacesIntegration(self, organizationId: str): 12 | """ 13 | **Remove the Spaces integration from Meraki** 14 | https://developer.cisco.com/meraki/api-v1/#!remove-organization-spaces-integration 15 | 16 | - organizationId (string): Organization ID 17 | """ 18 | 19 | metadata = { 20 | 'tags': ['organizations', 'configure', 'spaces', 'integration'], 21 | 'operation': 'removeOrganizationSpacesIntegration' 22 | } 23 | organizationId = urllib.parse.quote(str(organizationId), safe='') 24 | resource = f'/organizations/{organizationId}/spaces/integration/remove' 25 | 26 | return self._session.post(metadata, resource) 27 | 28 | -------------------------------------------------------------------------------- /meraki/common.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from meraki.exceptions import * 3 | import re 4 | import sys 5 | import urllib.parse 6 | 7 | 8 | def check_python_version(): 9 | # Check minimum Python version 10 | 11 | if not ( 12 | int(platform.python_version_tuple()[0]) == 3 13 | and int(platform.python_version_tuple()[1]) >= 10 14 | ): 15 | message = ( 16 | f"This library requires Python 3.10 at minimum. Python versions 3.8 and below are EOL as of October 2024" 17 | f" or earlier. End of life Python versions no longer receive security updates since reaching end of life" 18 | f" and of support per the Python maintainers. Your interpreter version is: {platform.python_version()}. " 19 | f"Please consult the readme at your convenience: https://github.com/meraki/dashboard-api-python " 20 | f"Additional details: " 21 | f"python_version_tuple()[0] = {platform.python_version_tuple()[0]}; " 22 | f"python_version_tuple()[1] = {platform.python_version_tuple()[1]} " 23 | ) 24 | 25 | raise PythonVersionError(message) 26 | 27 | 28 | def validate_user_agent(be_geo_id, caller): 29 | # Generate extended portion of the User Agent 30 | # Validate that it follows the expected format 31 | user_agent = dict() 32 | 33 | allowed_format_in_regex = r'^[A-Za-z0-9]+(?:/[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*(-[a-z]+)?)? [A-Za-z-0-9]+$' 34 | 35 | if caller and re.match(allowed_format_in_regex, caller): 36 | user_agent["caller"] = caller 37 | elif be_geo_id and re.match(allowed_format_in_regex, be_geo_id): 38 | user_agent["caller"] = be_geo_id 39 | else: 40 | if caller: 41 | message = "Please follow the user agent format prescribed in our User Agents guide, available here:" 42 | doc_link = "https://developer.cisco.com/meraki/api-v1/user-agents-overview/" 43 | raise SessionInputError("MERAKI_PTYHON_SDK_CALLER", caller, message, doc_link) 44 | elif be_geo_id: 45 | message = "Use of be_geo_id is deprecated. Please use the argument MERAKI_PTYHON_SDK_CALLER instead." 46 | doc_link = "https://developer.cisco.com/meraki/api-v1/user-agents-overview/" 47 | raise SessionInputError("BE_GEO_ID", caller, message, doc_link) 48 | else: 49 | user_agent["caller"] = "unidentified" 50 | 51 | caller_string = f'Caller/({user_agent["caller"]})' 52 | 53 | return caller_string 54 | 55 | 56 | def reject_v0_base_url(self): 57 | if 'v0' in self._base_url: 58 | sys.exit(f'This library does not support dashboard API v0 ({self._base_url} was configured as the base' 59 | f' URL). API v0 has been end of life since 2020 August 5.') 60 | elif self._base_url[-1] == '/': 61 | self._base_url = self._base_url[:-1] 62 | 63 | 64 | def iterator_for_get_pages_bool(self): 65 | return self._use_iterator_for_get_pages 66 | 67 | 68 | def use_iterator_for_get_pages_setter(self, value): 69 | if value: 70 | self.get_pages = self._get_pages_iterator 71 | else: 72 | self.get_pages = self._get_pages_legacy 73 | 74 | self._use_iterator_for_get_pages = value 75 | 76 | 77 | def validate_base_url(self, url): 78 | allowed_domains = ['meraki.com', 'meraki.ca', 'meraki.cn', 'meraki.in', 'gov-meraki.com'] 79 | parsed_url = urllib.parse.urlparse(url) 80 | if any(domain in parsed_url.netloc for domain in allowed_domains): 81 | abs_url = url 82 | else: 83 | abs_url = self._base_url + url 84 | return abs_url 85 | 86 | -------------------------------------------------------------------------------- /meraki/config.py: -------------------------------------------------------------------------------- 1 | # Package Constants 2 | 3 | # Meraki dashboard API key, set either at instantiation or as an environment variable 4 | API_KEY_ENVIRONMENT_VARIABLE = "MERAKI_DASHBOARD_API_KEY" 5 | 6 | # Base URL preceding all endpoint resources 7 | DEFAULT_BASE_URL = "https://api.meraki.com/api/v1" 8 | 9 | # Alternate base URLs 10 | CANADA_BASE_URL = "https://api.meraki.ca/api/v1" 11 | CHINA_BASE_URL = "https://api.meraki.cn/api/v1" 12 | INDIA_BASE_URL = "https://api.meraki.in/api/v1" 13 | UNITED_STATES_FED_BASE_URL = "https://api.gov-meraki.com/api/v1" 14 | 15 | # Maximum number of seconds for each API call 16 | SINGLE_REQUEST_TIMEOUT = 60 17 | 18 | # Path for TLS/SSL certificate verification if behind local proxy 19 | CERTIFICATE_PATH = "" 20 | 21 | # Proxy server and port, if needed, for HTTPS 22 | REQUESTS_PROXY = "" 23 | 24 | # Retry if 429 rate limit error encountered? 25 | # Please note, setting to False means your application will not retry upon a 429. Not intended for production apps. 26 | WAIT_ON_RATE_LIMIT = True 27 | 28 | # Nginx 429 retry wait time 29 | NGINX_429_RETRY_WAIT_TIME = 60 30 | 31 | # Action batch concurrency error retry wait time 32 | ACTION_BATCH_RETRY_WAIT_TIME = 60 33 | 34 | # Network deletion concurrency error retry wait time 35 | NETWORK_DELETE_RETRY_WAIT_TIME = 240 36 | 37 | # Retry if encountering other 4XX error (besides 429)? 38 | RETRY_4XX_ERROR = False 39 | 40 | # Other 4XX error retry wait time 41 | RETRY_4XX_ERROR_WAIT_TIME = 60 42 | 43 | # Retry up to this many times when encountering 429s or other server-side errors 44 | MAXIMUM_RETRIES = 2 45 | 46 | # Create an output log file? 47 | OUTPUT_LOG = True 48 | 49 | # Path to output log; by default, working directory of script if not specified 50 | LOG_PATH = "" 51 | 52 | # Log file name appended with date and timestamp 53 | LOG_FILE_PREFIX = "meraki_api_" 54 | 55 | # Print output logging to console? 56 | PRINT_TO_CONSOLE = True 57 | 58 | # Disable all logging? You're on your own then! 59 | SUPPRESS_LOGGING = False 60 | 61 | # You might integrate the library in an application with a predefined logging scheme. If so, you may not need the 62 | # library's default logging handlers, formatters etc.--instead, you can inherit an external logger instance. 63 | INHERIT_LOGGING_CONFIG = False 64 | 65 | # Use iterator for pages. May offer improved performance in some instances. Off by default for backwards compatibility. 66 | USE_ITERATOR_FOR_GET_PAGES = False 67 | 68 | # Simulate POST/PUT/DELETE calls to prevent changes? 69 | SIMULATE_API_CALLS = False 70 | 71 | # Number of concurrent API requests for asynchronous class 72 | AIO_MAXIMUM_CONCURRENT_REQUESTS = 8 73 | 74 | # Legacy partner identifier for API usage tracking; can also be set as an environment variable BE_GEO_ID 75 | # This is no longer used. Please use MERAKI_PYTHON_SDK_CALLER instead. 76 | BE_GEO_ID = "" 77 | 78 | # Optional identifier for API usage tracking; can also be set as an environment variable MERAKI_PYTHON_SDK_CALLER 79 | # It's good practice to use this to identify your application using the format: 80 | # CamelCasedApplicationName/OptionalVersionNumber CamelCasedVendorName 81 | # Please note: 82 | # 1. Application name precedes vendor name in all cases. 83 | # 2. If your application or vendor name normally contains spaces or special casing, you should omit them in favor of 84 | # normal CamelCasing here. 85 | # 3. The slash and version number are optional. Leave both out if you like. 86 | # 4. The slash is a forward slash, '/' -- not a backslash. 87 | # 5. Don't use the 'Meraki' or 'Cisco' names in your application name here. Maybe in general? I'm a config file, not a 88 | # lawyer. 89 | # Example 1: if your application is named 'Mambo', version number is 5.0, and your vendor/company name is Vega, then 90 | # you would use, at minimum: 'Mambo Vega'. Optionally: 'Mambo/5.0 Vega'. 91 | # Example 2: if your application is named 'Sunshine Rainbows', and company name is 'hunter2 for Life', and if you 92 | # don't want to report version number, then you would use, at minimum: 'SunshineRainbows hunter2ForLife' 93 | # The choice is yours as long as you follow the format. You should **not** include other information in this string. 94 | # If you are an official ecosystem partner, this is required. 95 | # For more guidance, please refer to https://developer.cisco.com/meraki/api-v1/user-agents-overview/ 96 | MERAKI_PYTHON_SDK_CALLER = "" 97 | -------------------------------------------------------------------------------- /meraki/exceptions.py: -------------------------------------------------------------------------------- 1 | # API key error 2 | class APIKeyError(Exception): 3 | def __init__(self): 4 | self.message = "Meraki API key needs to be defined" 5 | super(APIKeyError, self).__init__(self.message) 6 | 7 | def __repr__(self): 8 | return self.message 9 | 10 | 11 | class APIResponseError(Exception): 12 | """ 13 | Exception class raised from HTTP class methods. Used as a single catch-all error for any possible 14 | requests exception error that might happen during communication with Meraki API to simplify caller coding. 15 | """ 16 | 17 | def __init__(self, obj_name, status_code, error_msg): 18 | self.obj_name = obj_name 19 | self.status_code = status_code 20 | self.reason = error_msg 21 | 22 | def exc_message(self): 23 | return ( 24 | f'HTTP call within object "{self.obj_name}" failed. Status code is "{self.status_code}". ' 25 | f'Error message is: "{self.reason}".' 26 | ) 27 | 28 | def json(self): 29 | return dict(error=self.reason, status_code=self.status_code) 30 | 31 | def __str__(self): 32 | return self.exc_message() 33 | 34 | 35 | # To catch exceptions while making API calls 36 | class APIError(Exception): 37 | def __init__(self, metadata, response): 38 | self.response = response 39 | self.tag = metadata["tags"][0] 40 | self.operation = metadata["operation"] 41 | self.status = ( 42 | self.response.status_code 43 | if self.response is not None and self.response.status_code 44 | else None 45 | ) 46 | self.reason = ( 47 | self.response.reason 48 | if self.response is not None and self.response.reason 49 | else None 50 | ) 51 | try: 52 | self.message = ( 53 | self.response.json() 54 | if self.response is not None and self.response.json() 55 | else None 56 | ) 57 | except ValueError: 58 | self.message = self.response.content[:100].decode("UTF-8").strip() 59 | if ( 60 | isinstance(self.message, str) 61 | and self.status == 404 62 | and self.reason == "Not Found" 63 | ): 64 | self.message += ( 65 | "please wait a minute if the key or org was just newly created." 66 | ) 67 | super(APIError, self).__init__( 68 | f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}" 69 | ) 70 | 71 | def __repr__(self): 72 | return f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}" 73 | 74 | 75 | # To catch exceptions while making AIO API calls 76 | class AsyncAPIError(Exception): 77 | def __init__(self, metadata, response, message): 78 | self.response = response 79 | self.tag = metadata["tags"][0] 80 | self.operation = metadata["operation"] 81 | self.status = ( 82 | response.status if response is not None and response.status else None 83 | ) 84 | self.reason = ( 85 | response.reason if response is not None and response.reason else None 86 | ) 87 | self.message = message 88 | if isinstance(self.message, str): 89 | self.message = self.message.strip() 90 | if self.status == 404 and self.reason == "Not Found": 91 | self.message += ( 92 | "please wait a minute if the key or org was just newly created." 93 | ) 94 | 95 | super().__init__( 96 | f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}" 97 | ) 98 | 99 | def __repr__(self): 100 | return f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}" 101 | 102 | 103 | class PythonVersionError(Exception): 104 | """Exception raised for unsupported Python versions.""" 105 | 106 | def __init__(self, message): 107 | self.message = message 108 | 109 | super().__init__(self.message) 110 | 111 | 112 | class SessionInputError(Exception): 113 | """Exception raised for unsupported session inputs.""" 114 | 115 | def __init__(self, argument, value, message, doc_link): 116 | self.argument = argument 117 | self.value = value 118 | self.message = message 119 | self.doc_link = doc_link 120 | 121 | super().__init__(f'{self.message} {self.doc_link}') -------------------------------------------------------------------------------- /meraki/response_handler.py: -------------------------------------------------------------------------------- 1 | def handle_3xx(self, response): 2 | abs_url = response.headers['Location'] 3 | substring = 'meraki.com/api/v' 4 | if substring not in abs_url: 5 | substring = 'meraki.cn/api/v' 6 | self._base_url = abs_url[:abs_url.find(substring) + len(substring) + 1] 7 | return abs_url 8 | 9 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Getting started with Jupyter notebooks 2 | 3 | ## Overview 4 | 5 | Jupyter notebooks are documents that take coding in Python (and potentially other languages) to a whole new level. They are technically just fancy JSON files with a `.ipynb` extension, but with a Jupyter notebook and a compatible IDE, you can: 6 | 7 | 1. Write, run and re-run code in a block-by-block basis, while keeping all variables in an active and *interactive* kernel space. Those blocks are called "cells." If you're unfamiliar with Python debugging, it may be easier to use notebooks than debuggers! 8 | 2. Combine executable code, rich text, HTML, and even images in a single document, going way beyond standard Python comments and inserting HTML- or Markdown-formatted documentation inline with code in a way that never interferes with the operation of your code. 9 | 3. Rearrange code and Markdown cells aribitrarily for verification, experimentation, or pure curiosity. 10 | 4. Much more, if you are willing to learn! 11 | 12 | Jupyter notebooks are a product of Project Jupyter, a nonprofit organization dedicated to open standards and interactive computing. Read more on [their official website](https://jupyter.org/). 13 | 14 | In relation to the Meraki Python SDK, we will focus on using notebooks to write and document Python, of course! To get started, there's a few things you should know. 15 | 16 | ## Prerequisites 17 | 18 | Jupyter notebooks (hereafter referred to as simply "notebooks") can be a great way to learn and experiment with Python code, and in fact are often used in scenarios where the users running and maintaining the code have minimal coding and/or Python experience. As a result, you will find that coding in a notebook offers certain advantages you might not have in a traditional IDE. 19 | 20 | That said, to open and create a notebook, you will need a compatible editor. There are several free options. 21 | 22 | * Project Jupyter offers [JupyterLab](https://jupyter.org/install.html), a web-based IDE with a local installer for running on a host machine (installation required) 23 | * Microsoft offers [Visual Studio Code](https://code.visualstudio.com/), a powerful, multi-language IDE which has native support for notebooks and Python (installation required) 24 | * Google offers [Colaboratory](https://colab.research.google.com/), a completely web-based notebook IDE which requires no download whatsoever. Google calls their notebooks "Colab notebooks," but they work in much the same way. To learn more, see [Overview of Colab](https://colab.research.google.com/notebooks/basic_features_overview.ipynb). 25 | * JetBrains offers [PyCharm Professional](https://www.jetbrains.com/pycharm/). Unfortunately, the free version of PyCharm does not support notebooks. 26 | 27 | Regardless of how you choose to create them, notebooks are stored as `.ipnyb` files to distinguish them from standard `.py` Python scripts. In this guide, we will focus on using Google Colaboratory since it requires no download, but in some cases we will offer relevant instructions for other IDEs. In any case, if you prefer VS Code or JupyterLab, you can open the same notebook files covered in either IDE. 28 | 29 | ## Setting up your environment variables 30 | 31 | Any use of the API requires the use of an _API key_ (also called _API token_) for authentication. Technically, you could include your API key in plaintext in your notebooks or Python scripts, but that would be like storing your password in plaintext, and make it risky to share or publish your work. A more secure approach is to store your API key in your local environment variables. The Meraki SDK will automatically check your `env` for the appropriate variable and use it if it exists. 32 | 33 | ### Using Colab 34 | 35 | To simulate environment variables in Colab, we'll use the `colab-env` package. To set it up, you'll need a (free) Google account. `colab-env` creates a file called `vars.env` in your Google Drive to store the variables, and we'll use Python to add variables to it. 36 | 37 | 1. Open [Colaboratory](https://colab.research.google.com/) in your browser. 38 | 2. Sign in with a Google account if prompted. 39 | 3. By default, Colab opens the "Welcome to Colaboratory" notebook. 40 | 4. At the top, create a new code cell and paste in the following code, then run the cell. It will give you a link to log into your Google account: 41 | 42 | ```python 43 | %pip install colab-env -qU 44 | import colab_env 45 | import os 46 | ``` 47 | 48 | 5. Click the link, complete the authentication, and copy the long code it gives you. 49 | 6. Paste the code into the form field provided by the code cell, then hit `Enter` or `Return`. 50 | 7. You will then get one of the following outputs, depending on whether you've used the module with your Google account before: 51 | 52 | | ![Colab import colab_env output with new vars.env](/.github/images/colab-notebook-colab_env-import-new-instance_Annotation_2020-08-05_163942.png) | ![Colab import colab_env output with existing vars.env](/.github/images/colab-notebook-colab_env-import-Annotation_2020-08-05_163815.png) | 53 | |:--:|:--:| 54 | | *First time* | *When `vars.env` exists* | 55 | 56 | 8. In a new cell, paste in the following code block, and replace `YOUR_API_KEY_HERE` with your actual API key: 57 | 58 | ```python 59 | colab_env.envvar_handler.add_env(envname="MERAKI_DASHBOARD_API_KEY",envval="YOUR_API_KEY_HERE") 60 | print(os.getenv('MERAKI_DASHBOARD_API_KEY')) 61 | ``` 62 | 63 | 9. Run the cell. You should see your API key printed in the output. If you do, that means your environment is configured! 64 | 65 | ### Using VS Code on Windows 66 | 67 | 1. Click `Start` > `This PC` (type it if necessary) > Right-click on `This PC` > `Manage` 68 | 2. Near top left, `Advanced system settings` > `Environment Variables...` 69 | 3. Under the box labeled `User variables for YOUR_USERNAME`, `New...`. _NB: Avoid using the second, lower `New...` button under `System variables`. Others have access to that information._ 70 | 4. For `Variable name:`, type MERAKI_DASHBOARD_API_KEY 71 | 5. For `Variable value:`, paste in your actual API key 72 | 6. `OK` 73 | 7. Reboot your computer. This ensures the value is available to any program that calls it. 74 | 75 | ### Using VS Code on Mac 76 | 77 | Depending on your version of macOS, your default shell is either bash or zsh. User variables are stored in `~/.bash_profile` or `~/.zsh_profile` respectively. To find out which shell you're using, open Terminal, then run `echo $SHELL`. 78 | 79 | 1. If `~/.bash_profile` or `~/.zsh_profile` doesn't already exist, create the one relevant to your shell, e.g. if you have zsh, then run `touch ~/.zsh_profile` in Terminal. 80 | 2. Add this line to it, replacing the placeholder with your own API key: `export MERAKI_DASHBOARD_API_KEY=YOUR_API_KEY_HERE` 81 | 3. Save the file. 82 | 4. Reboot your computer. This ensures the value is available to any program that calls it. 83 | 84 | ## Create your first notebook 85 | 86 | ### Using Colab 87 | 88 | When using Colab, the steps are as follows: 89 | 90 | 1. Open [Colaboratory](https://colab.research.google.com/) in your browser. 91 | 2. Sign in with a Google account if prompted. 92 | 3. You're done! By default, Colab opens the "Welcome to Colaboratory" notebook. 93 | 94 | ### Using VS Code 95 | 96 | When using VS Code, the steps are as follows: 97 | 98 | 1. Install [Python 3.x locally](https://www.python.org/). As of the time of this writing, Python 3.8.5 is current. 99 | 2. Install [VS Code locally](https://code.visualstudio.com/#alt-downloads). 100 | 3. Install [VS Code's Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python). 101 | 4. __File__ > __New File__, then save the file with extension `.ipynb`. 102 | 103 | For a more detailed overview, please read [Microsoft's guide](https://code.visualstudio.com/docs/python/jupyter-support#:~:text=You%20can%20create%20a%20Jupyter,edit%20and%20run%20code%20cells.). 104 | 105 | ## Open an existing notebook 106 | 107 | ### Using Colab 108 | 109 | Click __File__ > __Open notebook__ 110 | 111 | You can manually upload notebooks you've saved locally, or you can clone a notebook from a public GitHub repository. In this case, let's clone a notebook from the Meraki GitHub. 112 | 113 | Click GitHub, then type `meraki` in the search field, then click in the deadspace to leave the field. Colab automatically lists Meraki's GitHub repos. Choose `dashboard-api-python`. Then choose the desired notebook from the list. 114 | 115 | ![Colab notebook selection screenshot](/.github/images/colab-notebook-selection-Annotation_2020-08-05_120229.png) 116 | 117 | Colab will open the notebook and you can get started! 118 | 119 | ### Using VS Code 120 | 121 | 1. Download the notebook(s) you'd like to use locally. When working with VS Code, it's helpful to create a "workspace" folder free of any other files. 122 | 2. Click __File__ > __Open Folder__ 123 | 3. Choose the folder where your notebook files are located, confirm the prompt, then select the notebook you'd like to use from the Explorer pane. 124 | 125 | ## Installing dependencies 126 | 127 | ### Using Colab 128 | 129 | Unlike local Python environments, Colab environments are not persistent. Therefore, whenever we expect to use a third-party package in Colab, we need to install it first using `pip`. The syntax is as follows: 130 | 131 | ```python 132 | %pip install PACKAGE_NAME 133 | ``` 134 | 135 | For example, to install the Meraki SDK: 136 | 137 | ```python 138 | %pip install meraki 139 | ``` 140 | 141 | If necessary, create a new code cell at the top of the notebook, paste in that code, and run it before working with the rest of your notebook. 142 | 143 | ### Using VS Code 144 | 145 | At a terminal, run: 146 | 147 | ```shell 148 | pip install meraki 149 | ``` 150 | 151 | Other packages can be installed the same way using the relevant package name. Try it with `tablib`! 152 | 153 | ## Creating new cells in a notebook 154 | 155 | ### Using Colab 156 | 157 | Depending on whether you'd like a new cell for code or text, click `+ Code` or `+ Text` at top left. 158 | 159 | ![Colab notebook new cell screenshot](/.github/images/colab-notebook-new-cell-Annotation_2020-08-05_123920.png) 160 | 161 | ## Writing code in a notebook 162 | 163 | ### Using Colab or VS Code 164 | 165 | After you have either created a new code cell, or clicked into an existing one, you can write Python like you normally would. You can put as much or as little into a cell, but for our purposes we've segmented the code into logical blocks for ease of consumption. 166 | 167 | ## Running code in a notebook 168 | 169 | ### Using Colab GUI 170 | 171 | Press the Run button at the top left of the code cell you'd like to run. 172 | 173 | ![Colab notebook run button](/.github/images/colab-notebook-run-cell-Annotation_2020-08-05_143202.png) 174 | 175 | ### Using Colab or VS Code hotkeys 176 | 177 | Notebook IDEs typically offer the hotkey `Shift + Enter` to run the current selected cell. This applies to both Colab and VS Code. 178 | 179 | ### Colab authorship warning 180 | 181 | If you receive a warning, review the warning, then click `Run Anyway`: 182 | 183 | ![Colab notebook authorship warning](/.github/images/colab-notebook-warning-run-cell-Annotation_2020-08-05_143410.png) 184 | 185 | ## Toggling a cell's mode between code and text 186 | 187 | ### Using Colab 188 | 189 | #### Convert code to text 190 | 191 | Select the cell, then hold `Ctrl` or `⌘` and type `MM`. 192 | 193 | #### Convert text to code 194 | 195 | Select the cell, then hold `Ctrl` or `⌘` and type `MY`. 196 | 197 | ### Using VS Code 198 | 199 | Select the cell, then at the top of the cell click either `M` to convert to text, or `{}` to convert to code. -------------------------------------------------------------------------------- /notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 180649.837195.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 180649.837195.xlsx -------------------------------------------------------------------------------- /notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 181014.158704.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 181014.158704.xlsx -------------------------------------------------------------------------------- /notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 backup with wan and vpn uplink prefs and cpcs.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 backup with wan and vpn uplink prefs and cpcs.xlsx -------------------------------------------------------------------------------- /notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 rules removed.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-01 rules removed.xlsx -------------------------------------------------------------------------------- /notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-03 live demo workbook.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meraki/dashboard-api-python/a026860357c997689c79295c9cac6040cbc13808/notebooks/Uplink preference backup and restore/exampleBackups/downloaded_rules_workbook_2020-12-03 live demo workbook.xlsx -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "meraki" 3 | version = "2.0.3" 4 | description = "Meraki library for Python" 5 | authors = [ 6 | {name = "Cisco Meraki",email = "api-feedback@meraki.net"} 7 | ] 8 | license = {text = "MIT License"} 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "requests (>=2.32.2,<3.0.0)", 13 | "aiohttp (>=3.11.18,<4.0.0)", 14 | "jinja2 (==3.1.6)", 15 | "pytest (>=8.3.5,<9.0.0)" 16 | ] 17 | 18 | [project.urls] 19 | homepage = "https://github.com/meraki/dashboard-api-python" 20 | repository = "https://github.com/meraki/dashboard-api-python" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=2.0.0,<3.0.0","setuptools>=78.1.1,<79.0.0"] 24 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for meraki""" 2 | 3 | import os.path 4 | import re 5 | from setuptools import setup, find_packages 6 | 7 | HERE = os.path.abspath(os.path.dirname(__file__)) 8 | PACKAGE_INIT = os.path.abspath(os.path.join('meraki', '__init__.py')) 9 | 10 | with open(os.path.join(HERE, 'README.md')) as fid: 11 | README = fid.read() 12 | 13 | 14 | def find_version(fname): 15 | """Attempts to find the version number in the file names fname. 16 | Raises RuntimeError if not found. 17 | """ 18 | version = '' 19 | with open(fname, 'r') as fp: 20 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') 21 | for line in fp: 22 | m = reg.match(line) 23 | if m: 24 | version = m.group(1) 25 | break 26 | if not version: 27 | raise RuntimeError('Cannot find version information') 28 | return version 29 | 30 | 31 | __version__ = find_version(PACKAGE_INIT) 32 | 33 | 34 | setup( 35 | name='meraki', 36 | version=__version__, 37 | packages=find_packages(exclude=["tests"]), 38 | include_package_data=True, 39 | install_requires=['requests', 'aiohttp'], 40 | keywords=['meraki', 'dashboard', 'cisco'], 41 | description='Cisco Meraki Dashboard API library', 42 | long_description=README, 43 | long_description_content_type='text/markdown', 44 | url='https://github.com/meraki/dashboard-api-python', 45 | author='Cisco Meraki', 46 | author_email='api-feedback@meraki.net', 47 | license='MIT', 48 | classifiers=[ 49 | 'License :: OSI Approved :: MIT License', 50 | 'Programming Language :: Python :: 3', 51 | ], 52 | python_requires='>=3.10' 53 | ) 54 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('.') 3 | sys.path.append('./meraki') 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | parser.addoption("--apikey", action="store", default="") 3 | parser.addoption("--o", action="store", default="") 4 | -------------------------------------------------------------------------------- /tests/test_dashboard_api_python_library.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import random 3 | 4 | import pytest 5 | 6 | import meraki 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def api_key(pytestconfig): 11 | # Replace with a valid Meraki API key 12 | return pytestconfig.getoption("apikey") 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def dashboard(api_key): 17 | return meraki.DashboardAPI(api_key, suppress_logging=True, network_delete_retry_wait_time=1000, 18 | maximum_retries=1000, caller='PytestForPythonLibrary Meraki') 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def org_id(pytestconfig): 23 | # Replace with a valid organization id 24 | return pytestconfig.getoption("o") 25 | 26 | 27 | @pytest.fixture(scope='session') 28 | def version_salt(): 29 | python_version = platform.python_version() 30 | salt = str(random.randint(1, 17381738)) 31 | return f'{python_version} {salt}' 32 | 33 | 34 | @pytest.fixture(scope='session') 35 | def network(dashboard, org_id, version_salt): 36 | # Replace with network details 37 | name = f"_GitHubAction Test Network {version_salt}" 38 | product_types = ["appliance", "switch", "wireless", "systemsManager", "sensor"] 39 | network_kwargs = { 40 | "tags": ["test_tag", "github", "shouldBeDeleted"], 41 | "timezone": "America/Los_Angeles" 42 | } 43 | 44 | created_network = dashboard.organizations.createOrganizationNetwork(org_id, name, product_types, **network_kwargs) 45 | yield created_network 46 | 47 | 48 | def test_get_administered_identities_me(dashboard): 49 | me = dashboard.administered.getAdministeredIdentitiesMe() 50 | assert me is not None 51 | assert isinstance(me["name"], str) 52 | assert me["authentication"]["api"]["key"]["created"] 53 | 54 | 55 | def test_get_organizations(dashboard): 56 | organizations = dashboard.organizations.getOrganizations() 57 | assert organizations is not None 58 | assert len(organizations) > 0 59 | 60 | 61 | def test_get_organization(dashboard, org_id): 62 | organization = dashboard.organizations.getOrganization(org_id) 63 | assert isinstance(organization, dict) 64 | assert isinstance(organization["id"], str) 65 | 66 | 67 | def test_create_network(dashboard, org_id, network, version_salt): 68 | assert network is not None 69 | assert network['name'] == f"_GitHubAction Test Network {version_salt}" 70 | 71 | 72 | def test_get_networks(dashboard, org_id): 73 | networks = dashboard.organizations.getOrganizationNetworks(org_id) 74 | assert networks is not None 75 | assert len(networks) > 0 76 | 77 | 78 | def test_update_network(dashboard, network): 79 | # Replace with updated network details 80 | new_name = f"{network['name']} new" 81 | updated_network_data = { 82 | "name": new_name, 83 | "tags": ["updated_test_tag", "github", "shouldBeDeleted"] 84 | } 85 | updated_network = dashboard.networks.updateNetwork(network['id'], **updated_network_data) 86 | assert updated_network is not None 87 | assert updated_network['name'] == new_name 88 | 89 | 90 | def test_create_organization_policy_objects(dashboard, org_id, network, version_salt): 91 | policy_objects = [ 92 | { 93 | "name": f"Ham {version_salt}".replace('.', '-'), 94 | "category": "network", 95 | "type": "cidr", 96 | "cidr": "10.51.1.253", 97 | "networkIds": [network["id"]] 98 | }, 99 | { 100 | "name": f"Hamlet {version_salt}".replace('.', '-'), 101 | "category": "network", 102 | "type": "cidr", 103 | "cidr": "10.17.38.0/24" 104 | } 105 | ] 106 | 107 | for policy_object in policy_objects: 108 | new_object = dashboard.organizations.createOrganizationPolicyObject(org_id, **policy_object) 109 | assert new_object is not None 110 | assert isinstance(new_object["id"], str) 111 | 112 | 113 | def test_get_organization_policy_objects(dashboard, org_id): 114 | policy_objects = dashboard.organizations.getOrganizationPolicyObjects(org_id) 115 | assert policy_objects is not None 116 | assert len(policy_objects) > 0 117 | 118 | 119 | def test_get_network_appliance_l3_firewall_rules(dashboard, network): 120 | rules = dashboard.appliance.getNetworkApplianceFirewallL3FirewallRules(network["id"]) 121 | assert rules is not None 122 | assert len(rules) > 0 123 | 124 | 125 | def test_update_network_appliance_vlan_settings(dashboard, network): 126 | response = dashboard.appliance.updateNetworkApplianceVlansSettings(network["id"], vlansEnabled=True) 127 | assert response is not None 128 | assert response["vlansEnabled"] 129 | 130 | 131 | def test_create_network_appliance_vlan(dashboard, network): 132 | name = "testy_vlan" 133 | name2 = "home_base" 134 | new_vlans = [{ 135 | "id": 51, 136 | "name": name, 137 | "subnet": "10.51.1.0/24", 138 | "applianceIp": "10.51.1.1" 139 | }, 140 | { 141 | "id": 1738, 142 | "name": name2, 143 | "subnet": "10.17.38.0/24", 144 | "applianceIp": "10.17.38.1" 145 | } 146 | ] 147 | for vlan in new_vlans: 148 | new_vlan = dashboard.appliance.createNetworkApplianceVlan(network["id"], **vlan) 149 | assert new_vlan is not None 150 | assert len(new_vlan) > 0 151 | assert new_vlan["name"] == vlan["name"] 152 | 153 | 154 | def test_update_l3_firewall_rules(dashboard, org_id, network, version_salt): 155 | # get all policy objects 156 | all_policy_objects = dashboard.organizations.getOrganizationPolicyObjects(org_id) 157 | 158 | # only interact with the ones created for this test run 159 | policy_objects = [policy_object for policy_object in all_policy_objects 160 | if f"{version_salt}".replace('.', '-') in policy_object['name']] 161 | new_rules = { 162 | "rules": [ 163 | { 164 | "comment": "HamByIP", 165 | "policy": "deny", 166 | "protocol": "tcp", 167 | "srcPort": "1738", 168 | "srcCidr": "VLAN(1738).*", 169 | "destPort": "1928", 170 | "destCidr": f"OBJ({policy_objects[0]['id']})", 171 | "syslogEnabled": False 172 | }, 173 | { 174 | "comment": "Ham", 175 | "policy": "deny", 176 | "protocol": "tcp", 177 | "srcPort": "Any", 178 | "srcCidr": f"OBJ({policy_objects[1]['id']})", 179 | "destPort": "Any", 180 | "destCidr": "11.1.1.1/32", 181 | "syslogEnabled": False 182 | } 183 | ] 184 | } 185 | updated_rules = dashboard.appliance.updateNetworkApplianceFirewallL3FirewallRules(network["id"], 186 | **new_rules)["rules"] 187 | assert updated_rules is not None 188 | assert len(updated_rules) == 3 189 | assert updated_rules[0]["comment"] == "HamByIP" 190 | assert updated_rules[1]["comment"] == "Ham" 191 | 192 | 193 | def test_delete_policy_objects(dashboard, org_id, version_salt): 194 | # get all policy objects 195 | all_policy_objects = dashboard.organizations.getOrganizationPolicyObjects(org_id) 196 | 197 | # only interact with the ones this test run created 198 | for policy_object in all_policy_objects: 199 | if f'{version_salt}'.replace('.', '-') in policy_object['name']: 200 | response = dashboard.organizations.deleteOrganizationPolicyObject(org_id, policy_object["id"]) 201 | assert response is None 202 | 203 | # ensure this one's policy objects are cleaned up 204 | remaining_policy_objects = dashboard.organizations.getOrganizationPolicyObjects(org_id) 205 | missed_policy_objects = [policy_object for policy_object in remaining_policy_objects 206 | if f'{version_salt}'.replace('.', '-') in policy_object['name']] 207 | assert len(missed_policy_objects) == 0 208 | 209 | 210 | 211 | def test_delete_network(dashboard, network): 212 | response = dashboard.networks.deleteNetwork(network['id']) 213 | assert response is None 214 | --------------------------------------------------------------------------------