├── requirements.txt ├── .gitattributes ├── v-tool ├── figures │ ├── figure_1.jpg │ ├── figure_10.jpg │ ├── figure_11.jpg │ ├── figure_2.jpg │ ├── figure_3.jpg │ ├── figure_4.jpg │ ├── figure_5.jpg │ ├── figure_6.jpg │ ├── figure_7.jpg │ ├── figure_8.jpg │ └── figure_9.jpg ├── V-Tool-API-Example-Guide.docx ├── Token-Org-URL.txt └── preReqInstallPythonVtool.py ├── scripts ├── configuration │ ├── config_webhook_settings.json │ └── config_auto_site_assignment.py ├── sites │ ├── site_conf_wlan_settings.json │ ├── site_conf_psk.py │ ├── import_psk.py │ └── site_conf_wlan.py ├── exports │ ├── compare_org_events_summary.py │ └── export_inventory.py ├── orgs │ ├── admins │ │ └── import_admins.py │ └── org_complete_backup.py ├── reports │ ├── report_rogues.py │ ├── report_app_usage.py │ ├── report_sites.py │ └── report_inventory_site_notes.py ├── devices │ ├── switches │ │ └── toggle_poe.py │ ├── aps │ │ └── configure_ap_mgmt_vlan.py │ └── gateways │ │ └── fix_gateway_backup_firmware.py └── nac │ └── import_user_macs.py ├── LICENSE ├── .gitignore ├── CODE_OF_CONDUCT.md ├── .dccache ├── CONTRIBUTING.md └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | mistapi 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /v-tool/figures/figure_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_1.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_10.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_11.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_2.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_3.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_4.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_5.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_6.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_7.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_8.jpg -------------------------------------------------------------------------------- /v-tool/figures/figure_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/figures/figure_9.jpg -------------------------------------------------------------------------------- /v-tool/V-Tool-API-Example-Guide.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmunzer/mist_library/HEAD/v-tool/V-Tool-API-Example-Guide.docx -------------------------------------------------------------------------------- /scripts/configuration/config_webhook_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://myserver.local/webhooks", 3 | "enabled": false, 4 | "splunk_token": "", 5 | "secret": "mprzx74ACMqM", 6 | "name": "demo", 7 | "type": "http-post", 8 | "verify_cert": true, 9 | "headers": {} 10 | } -------------------------------------------------------------------------------- /v-tool/Token-Org-URL.txt: -------------------------------------------------------------------------------- 1 | token= 2 | org_id= 3 | base_url=https://api.mist.com/api/v1 #Global01 4 | #base_url=https://api.gc1.mist.com/api/v1 #Global02 5 | #base_url=https://api.ac2.mist.com/api/v1 #Global03 6 | #base_url=https://api.gc2.mist.com/api/v1 #Global04 7 | #base_url=https://api.gc4.mist.com/api/v1 #Global05 8 | #base_url=https://api.eu.mist.com/api/v1 #EMEA01 9 | #base_url=https://api.gc3.mist.com/api/v1 #EMEA02 10 | #base_url=https://api.ac6.mist.com/api/v1 #EMEA03 11 | #base_url=https://api.ac5.mist.com/api/v1 #APAC5 12 | #get token for integration =https://api.mist.com/api/v1/self/apitokens 13 | 14 | -------------------------------------------------------------------------------- /scripts/sites/site_conf_wlan_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ssid": "mistworkshop", 3 | "enabled": true, 4 | "auth": { 5 | "type": "psk", 6 | "psk": "mistworkshop" 7 | }, 8 | "roam_mode": "none", 9 | "band": "both", 10 | "band_steer": false, 11 | "rateset": { 12 | "24": { 13 | "min_rssi": 0, 14 | "template": "high-density" 15 | }, 16 | "5": { 17 | "min_rssi": -70, 18 | "template": "high-density" 19 | } 20 | }, 21 | "disable_11ax": false, 22 | "vlan_enabled": false, 23 | "hide_ssid": false, 24 | "isolation": false, 25 | "arp_filter": false, 26 | "limit_bcast": false, 27 | "allow_mdns": false, 28 | "apply_to": "site", 29 | "wxtag_ids": [], 30 | "ap_ids": [], 31 | "schedule": { 32 | "enabled": false 33 | }, 34 | "qos": { 35 | "overwrite": false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## MIT LICENSE 2 | 3 | Copyright (c) 2023 Thomas Munzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /scripts/sites/site_conf_psk.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Written by Thomas Munzer (tmunzer@juniper.net) 3 | Github repository: https://github.com/tmunzer/Mist_library/ 4 | ''' 5 | 6 | 7 | #### IMPORTS ##### 8 | import logging 9 | import sys 10 | 11 | MISTAPI_MIN_VERSION = "0.44.1" 12 | 13 | try: 14 | import mistapi 15 | from mistapi.__logger import console 16 | except: 17 | print(""" 18 | Critical: 19 | \"mistapi\" package is missing. Please use the pip command to install it. 20 | 21 | # Linux/macOS 22 | python3 -m pip install mistapi 23 | 24 | # Windows 25 | py -m pip install mistapi 26 | """) 27 | sys.exit(2) 28 | 29 | #### PARAMETERS ##### 30 | PSK = {"name":'myUser', "passphrase":'myBadPassword', "ssid":'mySSID', "usage":'multi'} 31 | LOG_FILE = "./sites_scripts.log" 32 | ENV_FILE = "./.env" 33 | #### LOGS #### 34 | LOGGER = logging.getLogger(__name__) 35 | #### FUNCTIONS ##### 36 | 37 | def start(apisession): 38 | site_id = mistapi.cli.select_site(apisession, allow_many=False) 39 | mistapi.api.v1.sites.psks.createSitePsk(apisession, site_id, PSK) 40 | response = mistapi.api.v1.sites.psks.listSitePsks(apisession, site_id) 41 | psks = mistapi.get_all(apisession, response) 42 | mistapi.cli.pretty_print(psks) 43 | 44 | ##### ENTRY POINT #### 45 | 46 | if __name__ == "__main__": 47 | #### LOGS #### 48 | logging.basicConfig(filename=LOG_FILE, filemode='w') 49 | LOGGER.setLevel(logging.DEBUG) 50 | ### START ### 51 | APISESSION = mistapi.APISession(env_file=ENV_FILE) 52 | APISESSION.login() 53 | start(APISESSION) 54 | -------------------------------------------------------------------------------- /v-tool/preReqInstallPythonVtool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | =============================================================================== 4 | Pre-Requisites Installation Script for Upgrade Management Script 5 | =============================================================================== 6 | Version: 1.0 7 | Date: February 13, 2025 8 | Author: SV 9 | 10 | Description: 11 | ------------ 12 | This script installs the following required Python packages for the Upgrade Management 13 | Script: 14 | - requests (Used for making HTTP requests to the API) 15 | - pandas (Used for data manipulation and Excel file operations) 16 | - openpyxl (Used for reading and writing Excel files) 17 | 18 | Standard library modules used in this script: 19 | - os (For operating system interactions) 20 | - sys (For accessing system-specific parameters and functions) 21 | - subprocess (For invoking pip3 via the command line) 22 | 23 | Prerequisites: 24 | -------------- 25 | - Python 3.6 or higher must be installed. 26 | - This script uses pip3 to install required packages. 27 | - To run the script, use: 28 | python3 preReqInstallPython.py 29 | 30 | Usage: 31 | ------ 32 | Run the script from the command line to automatically install the required packages: 33 | python3 preReqInstallPython.py 34 | 35 | =============================================================================== 36 | """ 37 | 38 | import subprocess 39 | import sys 40 | 41 | # List of required packages to install via pip3 42 | packages = [ 43 | "requests", 44 | "pandas", 45 | "openpyxl" 46 | ] 47 | 48 | def install_package(package): 49 | """ 50 | Installs the given package using pip3. 51 | """ 52 | print(f"Installing {package} ...") 53 | try: 54 | # Use python3 -m pip install ... to ensure pip3 is used 55 | subprocess.check_call([sys.executable, "-m", "pip", "install", package]) 56 | print(f"[SUCCESS] {package} installed successfully.\n") 57 | except subprocess.CalledProcessError as e: 58 | print(f"[ERROR] Failed to install {package}. Error: {e}") 59 | sys.exit(1) 60 | 61 | def main(): 62 | """ 63 | Main function to install all required packages. 64 | """ 65 | print("Starting installation of required packages...\n") 66 | for pkg in packages: 67 | install_package(pkg) 68 | print("All required packages have been installed successfully.") 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # credential files 2 | config.py 3 | session.py 4 | floorplans/ 5 | # script generated files 6 | *.png 7 | *.csv 8 | backup 9 | org_backup 10 | site_backup 11 | rg_conf_file*.json 12 | org_inventory_file.json 13 | 14 | # others 15 | launch.json 16 | settings.json 17 | test.py 18 | 19 | .DS_Store 20 | 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | pip-wheel-metadata/ 44 | share/python-wheels/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | MANIFEST 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .nox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage.xml 69 | *.cover 70 | .hypothesis/ 71 | .pytest_cache/ 72 | 73 | # Translations 74 | *.mo 75 | *.pot 76 | 77 | # Django stuff: 78 | *.log 79 | local_settings.py 80 | db.sqlite3 81 | 82 | # Flask stuff: 83 | instance/ 84 | .webassets-cache 85 | 86 | # Scrapy stuff: 87 | .scrapy 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # Jupyter Notebook 96 | .ipynb_checkpoints 97 | 98 | # IPython 99 | profile_default/ 100 | ipython_config.py 101 | 102 | # pyenv 103 | .python-version 104 | 105 | # pipenv 106 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 107 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 108 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 109 | # install all needed dependencies. 110 | #Pipfile.lock 111 | 112 | # celery beat schedule file 113 | celerybeat-schedule 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | *.py~ 145 | test2.py 146 | .dccache 147 | scripts/orgs/org_conf_deploy_v2.py 148 | scripts/devices/switches/cli_commit_events/ -------------------------------------------------------------------------------- /scripts/sites/import_psk.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Written by Thomas Munzer (tmunzer@juniper.net) 3 | Github repository: https://github.com/tmunzer/Mist_library/ 4 | 5 | This script will import PSKs from a CSV file to one or multiple sites. 6 | Usage: 7 | python3 import_psk.py path_to_the_csv_file.csv 8 | 9 | CSV file format: 10 | 11 | pskName1,pskValue1,Wlan1 12 | pskName2,pskValue2,Wlan2 13 | 14 | ''' 15 | 16 | #### IMPORTS ##### 17 | import sys 18 | import csv 19 | import logging 20 | 21 | MISTAPI_MIN_VERSION = "0.44.1" 22 | 23 | try: 24 | import mistapi 25 | from mistapi.__logger import console 26 | except: 27 | print(""" 28 | Critical: 29 | \"mistapi\" package is missing. Please use the pip command to install it. 30 | 31 | # Linux/macOS 32 | python3 -m pip install mistapi 33 | 34 | # Windows 35 | py -m pip install mistapi 36 | """) 37 | sys.exit(2) 38 | 39 | #### PARAMETERS ##### 40 | LOG_FILE = "./scripts.log" 41 | ENV_FILE = "./.env" 42 | #### LOGS #### 43 | LOGGER = logging.getLogger(__name__) 44 | 45 | #### FUNCTIONS ##### 46 | 47 | def import_psk(apisession, site_id, psks): 48 | print("") 49 | print("".center(80, "-")) 50 | print(f"Starting PSKs import for site {site_id}".center(80, "-")) 51 | print("") 52 | for psk in psks: 53 | print(f'PSK {psk["username"]}') 54 | body = { 55 | "username":psk["username"], 56 | "passphrase":psk["passphrase"], 57 | "ssid":psk["ssid"] 58 | } 59 | result = mistapi.api.v1.sites.psks.createSitePsk(apisession, site_id, body=body).data 60 | mistapi.cli.pretty_print(result) 61 | 62 | def read_csv(csv_file): 63 | print("") 64 | print("".center(80, "-")) 65 | print(f"Opening CSV file {csv_file}".center(80, "-")) 66 | print("") 67 | psks = [] 68 | try: 69 | with open(sys.argv[1], 'r') as my_file: 70 | ppsk_file = csv.reader(my_file, delimiter=',') 71 | ppsk_file = [[c.replace("\ufeff", "") for c in row] for row in ppsk_file] 72 | for row in ppsk_file: 73 | username = row[0] 74 | passphrase = row[1] 75 | ssid = row[2] 76 | psks.append({"username": username,"passphrase": passphrase,"ssid": ssid}) 77 | return psks 78 | except: 79 | print("Error while opening the CSV file... Aborting") 80 | 81 | def list_psks(apisession, site_id): 82 | print("") 83 | print("".center(80, "-")) 84 | print(f"List of current PSKs for site {site_id}".center(80, "-")) 85 | print("") 86 | response = mistapi.api.v1.sites.psks.listSitePsks(apisession, site_id) 87 | psks = mistapi.get_all(apisession, response) 88 | mistapi.cli.pretty_print(psks) 89 | 90 | 91 | def start(apisession): 92 | site_ids = mistapi.cli.select_site(apisession, allow_many=True) 93 | print("") 94 | print("".center(80, "-")) 95 | print(site_ids) 96 | 97 | psks = read_csv(sys.argv[1]) 98 | 99 | for site_id in site_ids: 100 | import_psk(apisession, site_id, psks) 101 | 102 | for site_id in site_ids: 103 | list_psks(apisession, site_id) 104 | 105 | #### SCRIPT ENTRYPOINT ##### 106 | if __name__ == "__main__": 107 | #### LOGS #### 108 | logging.basicConfig(filename=LOG_FILE, filemode='w') 109 | LOGGER.setLevel(logging.DEBUG) 110 | ### START ### 111 | APISESSION = mistapi.APISession(env_file=ENV_FILE) 112 | APISESSION.login() 113 | start(APISESSION) 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tmunzer@juniper.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.dccache: -------------------------------------------------------------------------------- 1 | {"/Users/tmunzer/4_dev/Mist/mist_library/scripts/exports/export_inventory.py":[10503,1677509346223.2495,"e7a9974574ded3222d8a95541cd7642d8246209b3e181a1fcf0aa26fdcaa75e8"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/exports/export_search.py":[50508,1677008238504.406,"6278840fb3b6ae29d9cab84f5b3a01d33c872f841eb745f028ffca0adc669643"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/reports/report_app_usage.py":[9275,1675961548911.968,"e8236876c4c938bbecd5ee3ca364c16be0cacbca3f2db18c023bdbef6ffbc155"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/reports/report_bssids.py":[9012,1675961554648.989,"9830a86ddf453e81a9a947c2df5204ce572712358cd0ed0148e3ba396155b7ac"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/reports/report_rogues.py":[9033,1675961554650.8918,"3e86b6cfb2d0599ab06359313f9f4cee2f806dab5829c7a32067662d012208ca"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/reports/report_switch_snapshot.py":[11299,1675961554650.958,"aeaa7aaef32281f06c0fdd9c599f69ee80b937f4832daca215b2294cb8fb02f2"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/reports/report_wlans.py":[5856,1675933963691.9958,"7aa36f7bc7c0db8a9c7592960a4f4c8b0763441fb93028e5fb68b9463a5124dc"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/admins_import.py":[4705,1677059182912.431,"80e855c1da3c8ff9fe4eb01d8f4f734e31b1e2f3e2d79b5756fd9906964de637"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/assign_inventory.py":[19143,1679038826643.5806,"4312f51f71aaa4dab87e0a25b6540e5e6c8fbac398dbbf8d5fafa00d03d3ceca"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/configure_ap_auto_upgrade.py":[4843,1677053504209.3894,"0903c9f985dd3ad7835289d3fbc846c7c4290dbed5b71a095918423f9234e96a"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/configure_auto_site_assignment.py":[11408,1675961542833.2292,"249ae57edf9ce7c3623253982cba3bdf1c69e66959849c9c86cc0ba7d027bf03"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_clone.py":[17340,1677691043242.407,"eb4e8e8fff93af7fca24bb336feab152e3418680894a2c64ab6344badc4206d7"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_complete_backup.py":[10720,1677865625579.286,"b90600311bff2ccc90c96dfa4d1950c0d04bcf81dbb1d08688e39aa912ab887c"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_conf_backup.py":[20909,1677092939348.7646,"35cb694ed6e1b3de0c364b73f0bca60ad01e3b3b980ad71f08173961b7264890"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_conf_deploy.py":[37734,1679594740809.2305,"0692abb60c7b94465c5fae4d9e0517db87856436a942d4cdf09f80438d9b0e65"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_conf_deploy_only.py":[36095,1679594729529.1064,"b7295db0ae5039624f38b3bed83546ffa9ca26bb7e37cacd6950800ac7eb3e57"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_conf_zeroise.py":[15412,1676372763305.2349,"d199ba43782684dd7d4396e89677eade55f44b839252f67272f5dfce8f2ad77d"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_inventory_backup.py":[19806,1678089694392.9531,"f7b2c0c9242c71de4e301822e1c5a82da46be382257075b53fa13be7da3183cb"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_inventory_deploy.py":[45239,1677484895376.9302,"e5913e44e3bb18c0310ba45588546f35c1e6a015709504571ad00a3d41e359b2"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_inventory_restore_pictures.py":[29916,1677484533465.5293,"8bda25aa7b9ee2447d9a0a1f7225d4143c91a70912baaa3e975b61de5e00b0df"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/orgs/org_migration.py":[21045,1677691387435.9124,"83f0b124060091e82400239f985d07efc62bbd78045236c05c0c29b716b9a780"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/import_sites.py":[32001,1678972181288.2024,"ba8b8147beed48cebf17e91046e6935024995aa5cc335715abc126b726ee12cf"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/site_conf_backup.py":[15356,1676291266383.9922,"9030e0c0cb5132fd13b261ff51f3c73cc27ad3c762821ed391f868f633e956c4"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/site_conf_psk.py":[956,1675933963741.3247,"38118966ae248b2a6ad64f506adbd1bc1b25181ab7491772cb3add33232ae447"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/site_conf_psk_import_csv.py":[2574,1675933963748.7458,"fe3e48b662ee098d63995dc132e640486153e616dba0eb2ff5572fa0ccf85fe2"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/site_conf_webhook.py":[6715,1675933963749.6323,"d2e1aafa84840c7e29beadb3527d93da5f68ffc8679451b0904561ada9184177"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/site_conf_wlan.py":[4665,1675933963751.0168,"02962b9af134c954e7f3cdf0c6130c9e0f235b2a90635e2cba4570cb72cfdee6"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/devices/aps/configure_ap_mgmt_vlan.py":[11604,1676881687079.4246,"9608aaee82fd1c3a62c07ec558046c457197082b750e71f9f169cccf6a8638dc"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/devices/switches/toggle_poe.py":[12283,1676884258043.2393,"512cbf70fcb45ac5e8e7fd01ac68e729a386d856edca4a9115e2050c37cd9bd9"],"/Users/tmunzer/4_dev/Mist/mist_library/scripts/sites/not_migrated_to_mistapi/site_conf_restore.py":[20105,1675933963739.149,"3c49dfdc5c42498a2f1c4fdcaf235a17789cdb37b4490602f6bc4d3d9015626e"]} -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing to the mist_library repository. 2 | 3 | This document explains how to get started, how to report issues, and the expectations for pull requests so changes are safe, testable and easy to review. 4 | 5 | ## Quick start (local) 6 | 7 | 1. Fork the repository and create a feature branch from `master`: 8 | 9 | - Use a short, descriptive branch name: `fix/-` or `feat/-`. 10 | 11 | 2. Create and activate a Python virtual environment (macOS/Linux): 12 | 13 | ```bash 14 | python -m venv .venv 15 | source .venv/bin/activate 16 | python -m pip install --upgrade pip 17 | python -m pip install -r requirements.txt 18 | ``` 19 | 20 | 3. Run a script's help to confirm the environment is working: 21 | 22 | ```bash 23 | python scripts/orgs/org_conf_deploy.py -h 24 | ``` 25 | 26 | ## Environment / secrets 27 | 28 | - The repository's scripts expect an environment file (default `~/.mist_env`). 29 | - Never commit credentials, tokens, or secret files to the repository or a PR. Remove them from diffs immediately if accidentally included and rotate the credentials. 30 | - If you need to demonstrate a flow, provide a sanitized example env file (see README for an example) and avoid real tokens. 31 | 32 | ## Reporting issues 33 | 34 | When opening an issue, include: 35 | 36 | - A short, descriptive title. 37 | - Steps to reproduce the problem or exact command you ran. 38 | - The script path and line number (if applicable). 39 | - Your Python version and `mistapi` version (run `python3 -c "import mistapi; print(mistapi.__version__)"`). 40 | - Relevant, sanitized logs or error messages. 41 | 42 | Good issues are reproducible and include minimal data necessary for debugging. 43 | 44 | ## Pull requests (PRs) 45 | 46 | Best practices for PRs: 47 | 48 | - One logical change per PR. 49 | - Keep PRs small and focused. 50 | - Include tests or a short smoke-check when you change behavior. 51 | - Update `README.md` or script header docstrings when you change behavior or add new scripts. 52 | - Update or add `MISTAPI_MIN_VERSION` in script headers if the change requires a newer `mistapi` version. 53 | - If a change may be destructive (mass deletes, claims), require a CLI `--dry-run` first and document risks in the PR description. 54 | 55 | Suggested PR checklist (include in the PR description): 56 | 57 | - [ ] I have run the relevant script(s) locally and verified the change (or added a smoke test). 58 | - [ ] There are no credentials in the diff. 59 | - [ ] I updated the README or script header where applicable. 60 | - [ ] I updated `MISTAPI_MIN_VERSION` in scripts that require a newer mistapi. 61 | - [ ] I added tests or a reproducible example for non-trivial logic. 62 | 63 | ## Coding style and tests 64 | 65 | - Language: Python 3.8+ (some scripts assume newer language features; prefer 3.10+). 66 | - Keep changes consistent with existing code style (short, well-documented CLI scripts). Use type hints where appropriate. 67 | - Add or update docstrings at the top of script files explaining the purpose, options and example calls. 68 | - Where practical, include a small unit test or a smoke test (follow the repository's existing patterns). If you add tests, include the commands to run them in the PR. 69 | 70 | ## Logging and debug output 71 | 72 | - Many scripts use numeric logging levels (e.g., `CONSOLE_LOG_LEVEL`, `LOGGING_LOG_LEVEL`). Preserve those options when editing scripts. 73 | - Avoid printing sensitive data to logs. 74 | 75 | ## Adding new scripts 76 | 77 | When adding a new script, follow these guidelines: 78 | 79 | - Place the script in the appropriate `scripts/` subfolder. 80 | - Add a clear module-level docstring with purpose, usage examples and the default `ENV_FILE` value (e.g., `ENV_FILE = "~/.mist_env"`). 81 | - If the script depends on specific `mistapi` features, include `MISTAPI_MIN_VERSION = "x.y.z"` near the top. 82 | - Provide at least one usage example in the header and verify `-h` output is helpful. 83 | - Add the script to the `README` index or propose `SCRIPTS_INDEX.md` generation if the list becomes large. 84 | 85 | ## Security and sensitive operations 86 | 87 | - Mark destructive changes in the PR title and description (e.g., `dangerous`, `destructive`) and provide a dry-run mode. 88 | - If a change can modify many objects in a Mist org, include a clear rollback or validation strategy in the PR. 89 | 90 | ## Licensing and copyright 91 | 92 | - The project is licensed under the MIT License. By contributing you agree that your contribution can be distributed under the repository license. 93 | 94 | ## Maintainers and reviews 95 | 96 | - PRs are reviewed on GitHub. Expect maintainers to ask for small, incremental improvements during review. 97 | - If you want to take on larger work (refactor, test-suite addition), open an issue first describing scope and plan. 98 | 99 | --- 100 | 101 | Thanks for contributing — clear, small PRs with examples and no secrets make reviews fast and safe. 102 | -------------------------------------------------------------------------------- /scripts/sites/site_conf_wlan.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Written by Thomas Munzer (tmunzer@juniper.net) 3 | Github repository: https://github.com/tmunzer/Mist_library/ 4 | 5 | This script can be used to list/add/delete an SSID from Org/Site 6 | ''' 7 | 8 | #### IMPORTS #### 9 | import json 10 | import sys 11 | import logging 12 | 13 | MISTAPI_MIN_VERSION = "0.55.5" 14 | 15 | try: 16 | import mistapi 17 | from mistapi.__logger import console 18 | except: 19 | print(""" 20 | Critical: 21 | \"mistapi\" package is missing. Please use the pip command to install it. 22 | 23 | # Linux/macOS 24 | python3 -m pip install mistapi 25 | 26 | # Windows 27 | py -m pip install mistapi 28 | """) 29 | sys.exit(2) 30 | 31 | #### PARAMETERS ##### 32 | LOG_FILE = "./sites_scripts.log" 33 | DEFAULT_WLAN_FILE = "./site_conf_wlan_settings.json" 34 | ENV_FILE = "./.env" 35 | #### LOGS #### 36 | LOGGER = logging.getLogger(__name__) 37 | 38 | 39 | def add_wlan(apisession, site_id): 40 | wlan_file = input(f"Path to the WLAN configuration JSON file (default: {DEFAULT_WLAN_FILE}): ") 41 | if wlan_file == "": 42 | wlan_file = DEFAULT_WLAN_FILE 43 | try: 44 | with open(wlan_file, "r") as f: 45 | wlan = json.load(f) 46 | except: 47 | print("Error while loading the configuration file... exiting...") 48 | sys.exit(255) 49 | try: 50 | wlan_json = json.dumps(wlan) 51 | except: 52 | print("Error while loading the wlan settings from the file... exiting...") 53 | sys.exit(255) 54 | mistapi.api.v1.sites.wlans.createSiteWlan(apisession, site_id, wlan_json) 55 | 56 | def remove_wlan(apisession, site_id): 57 | wlans = mistapi.api.v1.sites.wlans.listSiteWlans(apisession, site_id).data 58 | resp = -1 59 | while True: 60 | print() 61 | print("Available WLANs:") 62 | i = 0 63 | for wlan in wlans: 64 | print(f"{i}) {wlan['ssid']} (id: {wlan['id']})") 65 | i+=1 66 | print() 67 | resp = input(f"Which WLAN do you want to delete (0-{i-1}, or q to quit)? ") 68 | if resp.lower() == "q": 69 | sys.exit(0) 70 | else: 71 | try: 72 | resp_num = int(resp) 73 | if resp_num >= 0 and resp_num <= i: 74 | wlan = wlans[resp_num] 75 | print() 76 | confirmation = input(f"Are you sure you want to delete WLAN {wlan['ssid']} (y/N)? ") 77 | if confirmation.lower() == "y": 78 | break 79 | else: 80 | print(f"{resp_num} is not part of the possibilities.") 81 | except: 82 | print("Only numbers are allowed.") 83 | mistapi.api.v1.sites.wlans.deleteSiteWlan(apisession, site_id, wlan["id"]) 84 | 85 | 86 | def display_wlan(apisession, site_id): 87 | fields = ["id","ssid", "enabled", "auth", "auth_servers", "acct_servers", "band", "interface", "vlan_id", "dynamic_vlan", "hide_ssid"] 88 | site_wlans = mistapi.api.v1.sites.wlans.listSiteWlanDerived(apisession, site_id).data 89 | mistapi.cli.display_list_of_json_as_table(site_wlans, fields) 90 | 91 | def start_site_conf_wlan(apisession, site_id): 92 | while True: 93 | print() 94 | print(" ===================") 95 | print(" == CURRENT WLANS ==") 96 | display_wlan(apisession, site_id) 97 | print(" ===================") 98 | print() 99 | actions = ["add WLAN", "remove WLAN"] 100 | print("What do you want to do:") 101 | i = -1 102 | for action in actions: 103 | i+= 1 104 | print(f"{i}) {action}") 105 | print() 106 | resp = input(f"Choice (0-{i}, q to quit): ") 107 | if resp.lower() == "q": 108 | sys.exit(0) 109 | else: 110 | try: 111 | resp_num = int(resp) 112 | except: 113 | print("Only numbers are allowed.") 114 | if resp_num >= 0 and resp_num <= i: 115 | if actions[resp_num] == "add WLAN": 116 | add_wlan(apisession, site_id) 117 | print() 118 | print(" ========================") 119 | print(" == WLANS AFTER CHANGE ==") 120 | display_wlan(apisession, site_id) 121 | print(" ========================") 122 | break 123 | elif actions[resp_num] == "remove WLAN": 124 | remove_wlan(apisession, site_id) 125 | print() 126 | print(" ========================") 127 | print(" == WLANS AFTER CHANGE ==") 128 | display_wlan(apisession, site_id) 129 | print(" ========================") 130 | break 131 | else: 132 | print(f"{resp_num} is not part of the possibilities.") 133 | 134 | def start(apisession): 135 | org_id = mistapi.cli.select_org(apisession)[0] 136 | site_id = mistapi.cli.select_site(apisession, org_id=org_id, allow_many=False)[0] 137 | start_site_conf_wlan(apisession, site_id) 138 | 139 | ##### ENTRY POINT #### 140 | 141 | if __name__ == "__main__": 142 | #### LOGS #### 143 | logging.basicConfig(filename=LOG_FILE, filemode='w') 144 | LOGGER.setLevel(logging.DEBUG) 145 | ### START ### 146 | APISESSION = mistapi.APISession(env_file=ENV_FILE) 147 | APISESSION.login() 148 | start(APISESSION) 149 | -------------------------------------------------------------------------------- /scripts/exports/compare_org_events_summary.py: -------------------------------------------------------------------------------- 1 | """ 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | Python script to compare two different version of summaries generated by the 12 | export_org_events.py script. 13 | This can be used to easily detect new sites referenced. 14 | 15 | ------- 16 | Options: 17 | -h, --help display this help 18 | -n, --new= latest export file 19 | -o, --old= oldest export file 20 | 21 | ------- 22 | Examples: 23 | python3 ./compare_org_events_summary \ 24 | -n org_events_summary_2024-09-08T23.35.51.csv 25 | -o org_events_summary_2024-09-07T23.31.25.csv 26 | 27 | """ 28 | 29 | #### IMPORTS #### 30 | import sys 31 | import csv 32 | import getopt 33 | import tabulate 34 | 35 | def _load_data(file:str): 36 | """ 37 | format summaries 38 | output should be something like 39 | data{"site_a": {"link": "...", "event_types": ["e_type_a", "e_type_b"]}, "site_b": ...} 40 | """ 41 | try: 42 | with open(file, 'r') as f: 43 | data = {} 44 | headers = {} 45 | rows = csv.reader(f, delimiter=",") 46 | i = -1 47 | for row in rows: 48 | i+=1 49 | if i == 0: 50 | pass 51 | elif i == 1: 52 | for header_name in row: 53 | if header_name not in ["site", "link"]: 54 | headers[header_name] = row.index(header_name) 55 | else: 56 | event_types = [] 57 | for header_name, header_index in headers.items(): 58 | try: 59 | val = int(row[header_index]) 60 | if val > 0: 61 | event_types.append(header_name) 62 | except: 63 | pass 64 | if event_types: 65 | event_types = sorted(event_types) 66 | data[row[0]] = { 67 | "link": row[1], 68 | "event_types": event_types 69 | } 70 | return data 71 | except: 72 | print(f"Error: Unable to read {file}") 73 | sys.exit(1) 74 | 75 | def _compare(new:str, old:str): 76 | """ 77 | create a diff. 78 | add a new line with the site name, site link, and the list of new events for the site 79 | """ 80 | new_data = _load_data(new) 81 | old_data = _load_data(old) 82 | diff = [] 83 | for site_name, new_site_data in new_data.items(): 84 | new_events = [] 85 | new_site_events = new_site_data.get("event_types") 86 | if old_data.get(site_name): 87 | old_site_events = old_data[site_name].get("event_types") 88 | for event_type in new_site_events: 89 | if event_type not in old_site_events: 90 | new_events.append(event_type) 91 | else: 92 | for event_type in new_site_events: 93 | new_events.append(event_type) 94 | if new_events: 95 | diff.append([ 96 | site_name, 97 | new_site_data.get("link"), 98 | ", ".join(new_events) 99 | ]) 100 | return diff 101 | 102 | def _save(new:str, old: str, data:list): 103 | filename = new.replace("summary", "diff") 104 | with open(filename, "w") as f: 105 | csv_writer = csv.writer(f) 106 | csv_writer.writerow([f"#compare between {new} and {old}"]) 107 | csv_writer.writerow(["#site","link", "new events"]) 108 | for r in data: 109 | csv_writer.writerow(r) 110 | return filename 111 | 112 | def start( 113 | new: str, 114 | old: str 115 | ): 116 | if not new: 117 | print("Error: missing new file parameter") 118 | if not old: 119 | print("Error: missing old file parameter") 120 | diff = _compare(new, old) 121 | filename = _save(new, old, diff) 122 | print() 123 | print(" Result ".center(80, "-")) 124 | print(tabulate.tabulate(diff[1:], headers={ 125 | "site":"site", 126 | "link":"link", 127 | "new events":"new events" 128 | })) 129 | print() 130 | print(f"Saved into {filename}") 131 | 132 | def usage(message: str = None): 133 | """Function to display Help""" 134 | print( 135 | f""" 136 | ------------------------------------------------------------------------------- 137 | 138 | Written by Thomas Munzer (tmunzer@juniper.net) 139 | Github repository: https://github.com/tmunzer/Mist_library/ 140 | 141 | This script is licensed under the MIT License. 142 | 143 | ------------------------------------------------------------------------------- 144 | 145 | Python script to compare two different version of summaries generated by the 146 | export_org_events.py script. 147 | This can be used to easily detect new sites referenced. 148 | 149 | ------- 150 | Options: 151 | -h, --help display this help 152 | -n, --new= latest export file 153 | -o, --old= oldest export file 154 | 155 | ------- 156 | Examples: 157 | Examples: 158 | python3 ./compare_org_events_summary \ 159 | -n org_events_summary_2024-09-08T23.35.51.csv 160 | -o org_events_summary_2024-09-07T23.31.25.csv 161 | 162 | """ 163 | ) 164 | if message: 165 | print(f"ERROR: {message}") 166 | sys.exit(0) 167 | 168 | 169 | ##################################################################### 170 | #### SCRIPT ENTRYPOINT #### 171 | if __name__ == "__main__": 172 | try: 173 | opts, args = getopt.getopt( 174 | sys.argv[1:], 175 | "hn:o:", 176 | [ 177 | "help", 178 | "new=", 179 | "old=", 180 | ], 181 | ) 182 | except getopt.GetoptError as err: 183 | print(err) 184 | usage() 185 | 186 | NEW = None 187 | OLD = None 188 | for o, a in opts: # type: ignore 189 | if o in ["-h", "--help"]: 190 | usage() 191 | elif o in ["-n", "--new"]: 192 | NEW = a 193 | elif o in ["-o", "--old"]: 194 | OLD = a 195 | else: 196 | assert False, "unhandled option" 197 | 198 | 199 | start(NEW, OLD) 200 | -------------------------------------------------------------------------------- /scripts/orgs/admins/import_admins.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to invite/add administrators from a CSV file. 11 | 12 | It is recommended to use an environment file to store the required information 13 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 14 | information about the available parameters). 15 | 16 | ------- 17 | Requirements: 18 | mistapi: https://pypi.org/project/mistapi/ 19 | 20 | ------- 21 | Usage: 22 | This script requires one parameters pointing to the CSV file. 23 | The CSV file must have 3 columns: email, first name, last name. 24 | The organization and the admin roles/scopes will be asked by the script. 25 | 26 | ------- 27 | CSV Example: 28 | owkenobi@unknown.com,Obi-Wan,Kenobi 29 | pamidala@unknown.com,Padme,Amidala 30 | 31 | 32 | 33 | ------- 34 | Example: 35 | python3 import_admins.py " 36 | 37 | ''' 38 | #### IMPORTS #### 39 | import sys 40 | import csv 41 | import logging 42 | 43 | MISTAPI_MIN_VERSION = "0.44.1" 44 | 45 | try: 46 | import mistapi 47 | from mistapi.__logger import console 48 | except: 49 | print(""" 50 | Critical: 51 | \"mistapi\" package is missing. Please use the pip command to install it. 52 | 53 | # Linux/macOS 54 | python3 -m pip install mistapi 55 | 56 | # Windows 57 | py -m pip install mistapi 58 | """) 59 | sys.exit(2) 60 | 61 | #### PARAMETERS ##### 62 | csv_separator = "," 63 | privileges = [] 64 | env_file="~/.mist_env" 65 | log_file = "./script.log" 66 | 67 | ##################################################################### 68 | #### LOGS #### 69 | LOGGER = logging.getLogger(__name__) 70 | 71 | #### CONSTANTS #### 72 | roles = {"s": "admin", "n": "write", "o": "read", "h":"helpdesk"} 73 | 74 | #### FUNCTIONS #### 75 | def define_privileges(apisession, org_id): 76 | ''' 77 | Generate the privilege parameters for the specified orgs. 78 | Will ask if the privileges have to be applied to the entire org or to a specific site/s, and the privilege level. 79 | There is no return value, the new privileges are stored into the global "privilege" variable 80 | ''' 81 | role = "" 82 | while role not in roles: 83 | role = input("Which level of privilege at the org level (\"s\" for Super Admin, \"n\" for Network Admin,\"o\" for observer,\"h\" for helpdesk)? ") 84 | while True: 85 | all_sites = input("Do you want to select specific sites (y/N)? ") 86 | if all_sites.lower()=="y": 87 | site_ids = mistapi.cli.select_site(apisession, org_id, True) 88 | for site_id in site_ids: 89 | privileges.append({"scope": "site", "org_id": org_id, "site_id": site_id, "role":roles[role]}) 90 | break 91 | elif all_sites.lower() == "n" or all_sites == "": 92 | site_ids = mistapi.api.v1.orgs.sites.listOrgSites(apisession, org_id) 93 | site_id="" 94 | for site in site_ids.data: 95 | if "site_id" in site: 96 | site_id = site["site_id"] 97 | break 98 | privileges.append({"scope": "org", "org_id": org_id, "site_id":site_id, "role":roles[role]}) 99 | break 100 | 101 | def import_admins(apisession, file_path, org_id): 102 | ''' 103 | create all the administrators from the "file_path" file. 104 | ''' 105 | print(f"Opening CSV file {file_path}") 106 | try: 107 | with open(file_path, 'r') as my_file: 108 | invite_file = csv.reader(my_file, delimiter=csv_separator) 109 | for row in invite_file: 110 | body = { 111 | "email": row[0], 112 | "first_name":row[1], 113 | "last_name": row[2], 114 | "privileges": privileges 115 | } 116 | print(', '.join(row).ljust(80), end="", flush=True) 117 | try: 118 | response = mistapi.api.v1.orgs.invites.inviteOrgAdmin(apisession, org_id, body) 119 | if response.status_code == 200: 120 | print("\033[92m\u2714\033[0m") 121 | else: 122 | print('\033[31m\u2716\033[0m') 123 | except: 124 | print('\033[31m\u2716\033[0m') 125 | except Exception as e: 126 | print("Error while opening the CSV file... Aborting") 127 | print(e) 128 | 129 | def check_mistapi_version(): 130 | """Check if the installed mistapi version meets the minimum requirement.""" 131 | 132 | current_version = mistapi.__version__.split(".") 133 | required_version = MISTAPI_MIN_VERSION.split(".") 134 | 135 | try: 136 | for i, req in enumerate(required_version): 137 | if current_version[int(i)] > req: 138 | break 139 | if current_version[int(i)] < req: 140 | raise ImportError( 141 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 142 | f"but version {mistapi.__version__} is installed." 143 | ) 144 | except ImportError as e: 145 | LOGGER.critical(str(e)) 146 | LOGGER.critical("Please use the pip command to update it.") 147 | LOGGER.critical("") 148 | LOGGER.critical(" # Linux/macOS") 149 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 150 | LOGGER.critical("") 151 | LOGGER.critical(" # Windows") 152 | LOGGER.critical(" py -m pip install --upgrade mistapi") 153 | print( 154 | f""" 155 | Critical:\r\n 156 | {e}\r\n 157 | Please use the pip command to update it. 158 | # Linux/macOS 159 | python3 -m pip install --upgrade mistapi 160 | # Windows 161 | py -m pip install --upgrade mistapi 162 | """ 163 | ) 164 | sys.exit(2) 165 | finally: 166 | LOGGER.info( 167 | '"mistapi" package version %s is required, ' 168 | "you are currently using version %s.", 169 | MISTAPI_MIN_VERSION, 170 | mistapi.__version__ 171 | ) 172 | 173 | ##################################################################### 174 | ##### ENTRY POINT #### 175 | if __name__ == "__main__": 176 | #### LOGS #### 177 | logging.basicConfig(filename=log_file, filemode='w') 178 | LOGGER.setLevel(logging.DEBUG) 179 | check_mistapi_version() 180 | 181 | ### START ### 182 | file_path = sys.argv[1] 183 | apisession = mistapi.APISession(env_file=env_file) 184 | apisession.login() 185 | 186 | org_id = mistapi.cli.select_org(apisession)[0] 187 | 188 | define_privileges(apisession, org_id) 189 | import_admins(apisession, file_path, org_id) 190 | 191 | admins = mistapi.api.v1.orgs.admins.listOrgAdmins(apisession, org_id).data 192 | 193 | -------------------------------------------------------------------------------- /scripts/reports/report_rogues.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to generate a Rogue AP report. 11 | 12 | ------- 13 | Requirements: 14 | mistapi: https://pypi.org/project/mistapi/ 15 | 16 | ------- 17 | Usage: 18 | This script can be run as is (without parameters), or with the options below. 19 | If no options are defined, or if options are missing, the missing options will 20 | be asked by the script or the default values will be used. 21 | 22 | It is recommended to use an environment file to store the required information 23 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 24 | information about the available parameters). 25 | 26 | ------- 27 | Options: 28 | -h, --help display this help 29 | -s, --site_id= Set the site_id 30 | -d, --duration= Duration (10m, 1h, 1d, 1w) 31 | default is 1d 32 | -r, --rogue_types= Types of rogues to include in the report, comma separated 33 | possible values: spoof, lan, honeypot, others 34 | default is spoof,lan,honeypot,others 35 | -f, --out_file= define the filepath/filename where to save the data 36 | default is "./report_rogues.csv" 37 | -l, --log_file= define the filepath/filename where to write the logs 38 | default is "./script.log" 39 | -e, --env= define the env file to use (see mistapi env file documentation 40 | here: https://pypi.org/project/mistapi/) 41 | default is "~/.mist_env" 42 | 43 | ------- 44 | Examples: 45 | python3 ./report_rogues.py 46 | python3 ./report_rogues.py --site_ids=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --duration=4d -r lan,spoof 47 | 48 | ''' 49 | 50 | #### IMPORTS ##### 51 | import sys 52 | import csv 53 | import getopt 54 | import logging 55 | 56 | MISTAPI_MIN_VERSION = "0.44.1" 57 | 58 | try: 59 | import mistapi 60 | from mistapi.__logger import console 61 | except: 62 | print(""" 63 | Critical: 64 | \"mistapi\" package is missing. Please use the pip command to install it. 65 | 66 | # Linux/macOS 67 | python3 -m pip install mistapi 68 | 69 | # Windows 70 | py -m pip install mistapi 71 | """) 72 | sys.exit(2) 73 | 74 | 75 | #### LOGS #### 76 | LOGGER = logging.getLogger(__name__) 77 | out=sys.stdout 78 | 79 | #### PARAMETERS ##### 80 | 81 | rogue_types = [ "honeypot", "lan", "others", "spoof"] 82 | duration = "1d" 83 | log_file = "./script.log" 84 | csv_delimiter = "," 85 | csv_file = "./report_rogues.csv" 86 | env_file = "~/.mist_env" 87 | 88 | 89 | ############################################################################### 90 | ### PROGRESS BAR 91 | def _progress_bar_update(count:int, total:int, size:int): 92 | if total == 0: 93 | return 94 | if count > total: 95 | count = total 96 | x = int(size*count/total) 97 | out.write(f"Progress: ".ljust(10)) 98 | out.write(f"[{'█'*x}{'.'*(size-x)}]") 99 | out.write(f"{count}/{total}\r".rjust(79 - size - 10)) 100 | out.flush() 101 | 102 | def _progress_bar_end(total:int, size:int): 103 | if total == 0: 104 | return 105 | _progress_bar_update(total, total, size) 106 | out.write("\n") 107 | out.flush() 108 | 109 | ############################################################################### 110 | #### FUNCTIONS #### 111 | 112 | def _process_rogues(rogues:list, rogue__type:str, site_name:str, site_id:str): 113 | for rogue in rogues: 114 | rogue["type"] = rogue__type 115 | rogue["site_id"] = site_id 116 | rogue["site_name"] = site_name 117 | return rogues 118 | 119 | def _get_rogues(mist_session, site_id:str, site_name:str, rogue__type:str, site_rogues:list=[]): 120 | response = mistapi.api.v1.sites.insights.listSiteRogueAPs(mist_session, site_id, type=rogue__type, limit=1000, duration=duration) 121 | site_rogues = site_rogues + _process_rogues(response.data["results"], rogue__type, site_name, site_id) 122 | while response.next: 123 | response = mistapi.get_next(mist_session, response) 124 | site_rogues = site_rogues + _process_rogues(response.data["results"], rogue__type, site_name, site_id) 125 | return site_rogues 126 | 127 | def _process_sites(mist_session, site_ids): 128 | i = 0 129 | total = len(site_ids) * len(rogue_types) 130 | rogues = [] 131 | _progress_bar_update(i, total, 50) 132 | for site_id in site_ids: 133 | site_name = mistapi.api.v1.sites.sites.getSiteInfo(mist_session, site_id).data["name"] 134 | for rogue__type in rogue_types: 135 | rogues = rogues + _get_rogues(mist_session, site_id, site_name, rogue__type) 136 | i+=1 137 | _progress_bar_update(i, total, 50) 138 | _progress_bar_end(total, 50) 139 | return rogues 140 | 141 | ### SAVE REPORT 142 | def _save_as_csv( data:list, duration:int): 143 | headers=[] 144 | size = 50 145 | total = len(data) 146 | print(" Saving Data ".center(80, "-")) 147 | print() 148 | print("Generating CSV Headers ".ljust(80,".")) 149 | i = 0 150 | for entry in data: 151 | for key in entry: 152 | if not key in headers: 153 | headers.append(key) 154 | i += 1 155 | _progress_bar_update(i, total, size) 156 | _progress_bar_end(total, size) 157 | print() 158 | print("Saving to file ".ljust(80,".")) 159 | i = 0 160 | with open(csv_file, "w", encoding='UTF8', newline='') as f: 161 | csv_writer = csv.writer(f) 162 | csv_writer.writerow([f"#Rogue Report for the last {duration}"]) 163 | csv_writer.writerow(headers) 164 | for entry in data: 165 | tmp=[] 166 | for header in headers: 167 | tmp.append(entry.get(header, "")) 168 | csv_writer.writerow(tmp) 169 | i += 1 170 | _progress_bar_update(i, total, size) 171 | _progress_bar_end(total, size) 172 | print() 173 | 174 | ############################################################################### 175 | ### USAGE 176 | def usage(): 177 | print(''' 178 | ------------------------------------------------------------------------------- 179 | 180 | Written by Thomas Munzer (tmunzer@juniper.net) 181 | Github repository: https://github.com/tmunzer/Mist_library/ 182 | 183 | This script is licensed under the MIT License. 184 | 185 | ------------------------------------------------------------------------------- 186 | Python script generates a Rogue AP report. 187 | 188 | ------- 189 | Requirements: 190 | mistapi: https://pypi.org/project/mistapi/ 191 | 192 | ------- 193 | Usage: 194 | This script can be run as is (without parameters), or with the options below. 195 | If no options are defined, or if options are missing, the missing options will 196 | be asked by the script or the default values will be used. 197 | 198 | It is recommended to use an environment file to store the required information 199 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 200 | information about the available parameters). 201 | 202 | ------- 203 | Options: 204 | -h, --help display this help 205 | -s, --site_id= Set the site_id 206 | -d, --duration= Duration (10m, 1h, 1d, 1w) 207 | default is 1d 208 | -r, --rogue_types= Types of rogues to include in the report, comma separated 209 | possible values: spoof, lan, honeypot, others 210 | default is spoof,lan,honeypot,others 211 | -f, --out_file= define the filepath/filename where to save the data 212 | default is "./report_rogues.csv" 213 | -l, --log_file= define the filepath/filename where to write the logs 214 | default is "./script.log" 215 | -e, --env= define the env file to use (see mistapi env file documentation 216 | here: https://pypi.org/project/mistapi/) 217 | default is "~/.mist_env" 218 | 219 | ------- 220 | Examples: 221 | python3 ./report_rogues.py 222 | python3 ./report_rogues.py --site_ids=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --duration=4d -r lan,spoof 223 | 224 | ''') 225 | sys.exit(0) 226 | 227 | def check_mistapi_version(): 228 | """Check if the installed mistapi version meets the minimum requirement.""" 229 | 230 | current_version = mistapi.__version__.split(".") 231 | required_version = MISTAPI_MIN_VERSION.split(".") 232 | 233 | try: 234 | for i, req in enumerate(required_version): 235 | if current_version[int(i)] > req: 236 | break 237 | if current_version[int(i)] < req: 238 | raise ImportError( 239 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 240 | f"but version {mistapi.__version__} is installed." 241 | ) 242 | except ImportError as e: 243 | LOGGER.critical(str(e)) 244 | LOGGER.critical("Please use the pip command to update it.") 245 | LOGGER.critical("") 246 | LOGGER.critical(" # Linux/macOS") 247 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 248 | LOGGER.critical("") 249 | LOGGER.critical(" # Windows") 250 | LOGGER.critical(" py -m pip install --upgrade mistapi") 251 | print( 252 | f""" 253 | Critical:\r\n 254 | {e}\r\n 255 | Please use the pip command to update it. 256 | # Linux/macOS 257 | python3 -m pip install --upgrade mistapi 258 | # Windows 259 | py -m pip install --upgrade mistapi 260 | """ 261 | ) 262 | sys.exit(2) 263 | finally: 264 | LOGGER.info( 265 | '"mistapi" package version %s is required, ' 266 | "you are currently using version %s.", 267 | MISTAPI_MIN_VERSION, 268 | mistapi.__version__ 269 | ) 270 | 271 | 272 | 273 | ############################################################################### 274 | ### ENTRY POINT 275 | if __name__ == "__main__": 276 | try: 277 | opts, args = getopt.getopt(sys.argv[1:], "hs:d:l:f:c:e:t:r:", ["help", "site_ids=", "duration=", "out_file=", "env=", "log_file=", "rogue_types="]) 278 | except getopt.GetoptError as err: 279 | console.error(err) 280 | usage() 281 | 282 | site_ids=None 283 | query_params={} 284 | for o, a in opts: 285 | if o in ["-h", "--help"]: 286 | usage() 287 | elif o in ["-s", "--site_ids"]: 288 | site_ids = a.split(",") 289 | elif o in ["-d", "--duration"]: 290 | duration = a 291 | elif o in ["-r", "--rogue_types"]: 292 | rogue_types = a.split(",") 293 | elif o in ["-f", "--out_file"]: 294 | csv_file=a 295 | elif o in ["-e", "--env"]: 296 | env_file=a 297 | elif o in ["-l", "--log_file"]: 298 | log_file = a 299 | 300 | else: 301 | assert False, "unhandled option" 302 | 303 | #### LOGS #### 304 | logging.basicConfig(filename=log_file, filemode='w') 305 | LOGGER.setLevel(logging.DEBUG) 306 | check_mistapi_version() 307 | ### MIST SESSION ### 308 | mist_session = mistapi.APISession(env_file=env_file) 309 | mist_session.login() 310 | if not site_ids: 311 | site_ids = mistapi.cli.select_site(mist_session, allow_many=True) 312 | ### START ### 313 | print(" Process Started ".center(80, '-')) 314 | data = _process_sites(mist_session, site_ids) 315 | 316 | 317 | print(" Process Done ".center(80, '-')) 318 | mistapi.cli.pretty_print(data) 319 | _save_as_csv(data, duration) 320 | 321 | 322 | -------------------------------------------------------------------------------- /scripts/reports/report_app_usage.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to generate a report of the application usage on a specific site 11 | 12 | ------- 13 | Requirements: 14 | mistapi: https://pypi.org/project/mistapi/ 15 | 16 | ------- 17 | Usage: 18 | This script can be run as is (without parameters), or with the options below. 19 | If no options are defined, or if options are missing, the missing options will 20 | be asked by the script or the default values will be used. 21 | 22 | It is recommended to use an environment file to store the required information 23 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 24 | information about the available parameters). 25 | 26 | ------- 27 | Options: 28 | -h, --help display this help 29 | -s, --site_id= required for Site reports. Set the site_id 30 | -d, --duration= Hours to report 31 | default is 96 32 | -l, --log_file= define the filepath/filename where to write the logs 33 | default is "./script.log" 34 | -f, --out_file= define the filepath/filename where to save the data 35 | default is "./report_app_usage.csv" 36 | -e, --env= define the env file to use (see mistapi env file documentation 37 | here: https://pypi.org/project/mistapi/) 38 | default is "~/.mist_env" 39 | 40 | ------- 41 | Examples: 42 | python3 ./report_app_usage.py 43 | python3 ./report_app_usage.py --site_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --duration=48 44 | 45 | ''' 46 | 47 | #### IMPORTS #### 48 | import logging 49 | import csv 50 | import datetime 51 | import sys 52 | import getopt 53 | 54 | MISTAPI_MIN_VERSION = "0.44.1" 55 | 56 | try: 57 | import mistapi 58 | from mistapi.__logger import console 59 | except: 60 | print(""" 61 | Critical: 62 | \"mistapi\" package is missing. Please use the pip command to install it. 63 | 64 | # Linux/macOS 65 | python3 -m pip install mistapi 66 | 67 | # Windows 68 | py -m pip install mistapi 69 | """) 70 | sys.exit(2) 71 | 72 | 73 | #### PARAMETERS ##### 74 | 75 | log_file = "./script.log" 76 | hours_to_report = 96 77 | csv_delimiter = "," 78 | csv_file = "report_app_usage.csv" 79 | env_file = "~/.mist_env" 80 | 81 | #### LOGS #### 82 | LOGGER = logging.getLogger(__name__) 83 | out=sys.stdout 84 | 85 | ############################################################################### 86 | ### PROGRESS BAR 87 | def _progress_bar_update(count:int, total:int, size:int): 88 | if total == 0: 89 | return 90 | elif count > total: 91 | count = total 92 | x = int(size*count/total) 93 | out.write(f"Progress: ".ljust(10)) 94 | out.write(f"[{'█'*x}{'.'*(size-x)}]") 95 | out.write(f"{count}/{total}\r".rjust(79 - size - 10)) 96 | out.flush() 97 | 98 | def _progress_bar_end(total:int, size:int): 99 | if total == 0: 100 | return 101 | _progress_bar_update(total, total, size) 102 | out.write("\n") 103 | out.flush() 104 | 105 | ############################################################################### 106 | ### FUNCTIONS 107 | 108 | def _get_clients_list(mist_session, site_id): 109 | clients = mistapi.api.v1.sites.stats.listSiteWirelessClientsStats(mist_session, site_id).data 110 | return clients 111 | 112 | 113 | def _get_site_name(mist_session, site_id): 114 | site_info = mistapi.api.v1.sites.sites.getSiteInfo(mist_session, site_id).data 115 | return site_info["name"] 116 | 117 | def _convert_numbers(size): 118 | # 2**10 = 1024 119 | power = 2**10 120 | n = 0 121 | power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} 122 | while size > power: 123 | size /= power 124 | n += 1 125 | size = round(size, 2) 126 | return "%s %sB" %(size, power_labels[n]) 127 | 128 | def _generate_site_report(mist_session, site_name, site_id, start, stop, interval): 129 | app_usage = [] 130 | clients = _get_clients_list(mist_session, site_id) 131 | console.info("%s clients to process... Please wait..." %(len(clients))) 132 | i=0 133 | _progress_bar_update(0, len(clients), 50) 134 | for client in clients: 135 | client_mac = client["mac"] 136 | if "username" in client: client_username = client["username"] 137 | else: client_username = "" 138 | if "hostname" in client: client_hostname = client["hostname"] 139 | else: client_hostname = "" 140 | client_app = mistapi.api.v1.sites.insights.getSiteInsightMetricsForClient(mist_session, site_id, client_mac=client_mac, start=start, end=stop, interval=interval, metric="top-app-by-bytes").data 141 | tmp={"site name": site_name, "site id": site_id, "client mac": client_mac, "username": client_username, "hostname": client_hostname} 142 | for app in client_app.get("results", []): 143 | usage = _convert_numbers(app["total_bytes"]) 144 | tmp[app["app"]] = usage 145 | i+=1 146 | _progress_bar_update(i, len(clients), 50) 147 | app_usage.append(tmp) 148 | _progress_bar_end(len(clients), 50) 149 | return app_usage 150 | 151 | ### SAVE REPORT 152 | def _save_report(app_usage): 153 | console.info("Saving to file %s..." %(csv_file)) 154 | fields = [] 155 | for row in app_usage: 156 | for key in row: 157 | if not key in fields: fields.append(key) 158 | 159 | with open(csv_file, 'w') as output_file: 160 | dict_writer = csv.DictWriter(output_file, restval="-", fieldnames=fields, delimiter=csv_delimiter) 161 | dict_writer.writeheader() 162 | dict_writer.writerows(app_usage) 163 | console.info("File %s saved!" %(csv_file)) 164 | 165 | ### GENERATE REPORT 166 | def generate_report(mist_session, site_ids, time): 167 | app_usage = [] 168 | if type(site_ids) == str: 169 | site_ids = [ site_ids] 170 | for site_id in site_ids: 171 | site_name = _get_site_name(mist_session, site_id) 172 | console.info("Processing site %s (id %s)" %(site_name, site_id)) 173 | app_usage += _generate_site_report(mist_session, site_name, site_id, time["start"], time["stop"], time["interval"]) 174 | mistapi.cli.pretty_print(app_usage) 175 | _save_report(app_usage) 176 | 177 | def _ask_period(hours): 178 | now = datetime.datetime.now() 179 | start = round((datetime.datetime.now() - datetime.timedelta(hours=hours)).timestamp(), 0) 180 | stop = round(now.timestamp(), 0) 181 | interval = 3600 182 | return {"start": start, "stop": stop, "interval": interval} 183 | 184 | ############################################################################### 185 | ### USAGE 186 | def usage(): 187 | print(""" 188 | ------------------------------------------------------------------------------- 189 | 190 | Written by Thomas Munzer (tmunzer@juniper.net) 191 | Github repository: https://github.com/tmunzer/Mist_library/ 192 | 193 | This script is licensed under the MIT License. 194 | 195 | ------------------------------------------------------------------------------- 196 | Python script to generate a report of the application usage on a specific site 197 | 198 | Requirements: 199 | mistapi: https://pypi.org/project/mistapi/ 200 | 201 | Usage: 202 | This script can be run as is (without parameters), or with the options below. 203 | If no options are defined, or if options are missing, the missing options will 204 | be asked by the script or the default values will be used. 205 | 206 | It is recommended to use an environment file to store the required information 207 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 208 | information about the available parameters). 209 | 210 | Options: 211 | -h, --help display this help 212 | -s, --site_id= required for Site reports. Set the site_id 213 | -d, --duration= Hours to report 214 | default is 96 215 | -l, --log_file= define the filepath/filename where to write the logs 216 | default is "./script.log" 217 | -f, --out_file= define the filepath/filename where to save the data 218 | default is "./report_app_usage.csv" 219 | -e, --env= define the env file to use (see mistapi env file documentation 220 | here: https://pypi.org/project/mistapi/) 221 | default is "~/.mist_env" 222 | 223 | Examples: 224 | python3 ./report_app_usage.py 225 | python3 ./report_app_usage.py --site_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --duration=48 226 | 227 | """) 228 | sys.exit(0) 229 | 230 | def check_mistapi_version(): 231 | """Check if the installed mistapi version meets the minimum requirement.""" 232 | 233 | current_version = mistapi.__version__.split(".") 234 | required_version = MISTAPI_MIN_VERSION.split(".") 235 | 236 | try: 237 | for i, req in enumerate(required_version): 238 | if current_version[int(i)] > req: 239 | break 240 | if current_version[int(i)] < req: 241 | raise ImportError( 242 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 243 | f"but version {mistapi.__version__} is installed." 244 | ) 245 | except ImportError as e: 246 | LOGGER.critical(str(e)) 247 | LOGGER.critical("Please use the pip command to update it.") 248 | LOGGER.critical("") 249 | LOGGER.critical(" # Linux/macOS") 250 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 251 | LOGGER.critical("") 252 | LOGGER.critical(" # Windows") 253 | LOGGER.critical(" py -m pip install --upgrade mistapi") 254 | print( 255 | f""" 256 | Critical:\r\n 257 | {e}\r\n 258 | Please use the pip command to update it. 259 | # Linux/macOS 260 | python3 -m pip install --upgrade mistapi 261 | # Windows 262 | py -m pip install --upgrade mistapi 263 | """ 264 | ) 265 | sys.exit(2) 266 | finally: 267 | LOGGER.info( 268 | '"mistapi" package version %s is required, ' 269 | "you are currently using version %s.", 270 | MISTAPI_MIN_VERSION, 271 | mistapi.__version__ 272 | ) 273 | 274 | 275 | ############################################################################### 276 | ### ENTRY POINT 277 | if __name__ == "__main__": 278 | try: 279 | opts, args = getopt.getopt(sys.argv[1:], "hs:d:f:e:l:", ["help", "site_id=", "duration=", "out_file=", "env=", "log_file="]) 280 | except getopt.GetoptError as err: 281 | console.error(err) 282 | usage() 283 | 284 | site_id=None 285 | query_params={} 286 | for o, a in opts: 287 | if o in ["-h", "--help"]: 288 | usage() 289 | elif o in ["-s", "--site_id"]: 290 | site_id = a 291 | elif o in ["-d", "--duration"]: 292 | try: 293 | hours_to_report = int(a) 294 | except: 295 | console.error(f"Duration value \"{a}\" is not valid") 296 | usage() 297 | elif o in ["-f", "--out_file"]: 298 | csv_file=a 299 | elif o in ["-e", "--env"]: 300 | env_file=a 301 | elif o in ["-l", "--log_file"]: 302 | log_file = a 303 | 304 | else: 305 | assert False, "unhandled option" 306 | 307 | #### LOGS #### 308 | logging.basicConfig(filename=log_file, filemode='w') 309 | LOGGER.setLevel(logging.DEBUG) 310 | check_mistapi_version() 311 | ### MIST SESSION ### 312 | mist_session = mistapi.APISession(env_file=env_file) 313 | mist_session.login() 314 | if not site_id: 315 | site_id = mistapi.cli.select_site(mist_session, allow_many=True) 316 | ### START ### 317 | time = _ask_period(hours_to_report) 318 | generate_report(mist_session, site_id, time) 319 | -------------------------------------------------------------------------------- /scripts/devices/switches/toggle_poe.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to enable/disable/toggle PoE for a specified Port Profile in a 11 | Switch Template. 12 | 13 | This script will not change/create/delete/touch any existing objects. It will 14 | just retrieve every single object from the organization. 15 | 16 | ------- 17 | Requirements: 18 | mistapi: https://pypi.org/project/mistapi/ 19 | 20 | ------- 21 | Usage: 22 | This script can be run as is (without parameters), or with the options below. 23 | If no options are defined, or if options are missing, the missing options will 24 | be asked by the script or the default values will be used. 25 | 26 | It is recommended to use an environment file to store the required information 27 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 28 | information about the available parameters). 29 | 30 | ------- 31 | Options: 32 | -o, --org_id= Set the org_id 33 | -t, --tmpl_id= Set the Switch template_id 34 | -p, --profile= Set the Port Profile name 35 | -a, --action= Set the action to execupte. It must be one of the following: 36 | - status: Retrieve the PoE status 37 | - on: Enable PoE on the port profile 38 | - off: Disable PoE on the port profile 39 | - toggle: toogle PoE on the port profile (i.e. turn it off if 40 | currently enable, turn it on if currently enabled) 41 | -e, --env= define the env file to use (see mistapi env file documentation 42 | here: https://pypi.org/project/mistapi/) 43 | default is "~/.mist_env" 44 | 45 | ------- 46 | Examples: 47 | python3 ./toggle_poe.py 48 | python3 ./toggle_poe.py -o 203d3d02-xxxx-xxxx-xxxx-76896a3330f4 -t 2c57044e-xxxx-xxxx-xxxx-69374b32a070 -p ap -a toggle 49 | 50 | ''' 51 | 52 | #### IMPORTS #### 53 | import logging 54 | import sys 55 | import getopt 56 | 57 | MISTAPI_MIN_VERSION = "0.55.5" 58 | 59 | try: 60 | import mistapi 61 | from mistapi.__logger import console 62 | except: 63 | print(""" 64 | Critical: 65 | \"mistapi\" package is missing. Please use the pip command to install it. 66 | 67 | # Linux/macOS 68 | python3 -m pip install mistapi 69 | 70 | # Windows 71 | py -m pip install mistapi 72 | """) 73 | sys.exit(2) 74 | 75 | 76 | ##################################################################### 77 | #### PARAMETERS ##### 78 | log_file = "./script.log" 79 | env_file = "~/.mist_env" 80 | 81 | ##################################################################### 82 | #### LOGS #### 83 | logger = logging.getLogger(__name__) 84 | 85 | ##################################################################### 86 | #### FUNCTIONS #### 87 | def _get_port_usages(apisession:mistapi.APISession, org_id:str, tmpl_id:str): 88 | print("Retrieving data from Mist ".ljust(79, "."), end="", flush=True) 89 | try: 90 | res = mistapi.api.v1.orgs.networktemplates.getOrgNetworkTemplate(apisession, org_id, tmpl_id) 91 | disabled = res.data.get("port_usages", {}) 92 | print("\033[92m\u2714\033[0m") 93 | return disabled 94 | except: 95 | print('\033[31m\u2716\033[0m') 96 | sys.exit(4) 97 | 98 | def _status(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str): 99 | port_usages = _get_port_usages(apisession, org_id, tmpl_id) 100 | print("Extracting profile data ".ljust(79, "."), end="", flush=True) 101 | profile = port_usages.get(profile, {}) 102 | if not profile: 103 | print('\033[31m\u2716\033[0m') 104 | print(f"Profile {profile} not found") 105 | else: 106 | print("\033[92m\u2714\033[0m") 107 | return profile.get("poe_disabled", False) 108 | 109 | def _display_status(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str): 110 | disabled = _status(apisession, org_id, tmpl_id, profile) 111 | print() 112 | print(" Result ".center(80, "-")) 113 | print() 114 | if disabled == True: 115 | print(f"PoE Current status: DISABLED") 116 | else: 117 | print(f"PoE Current status: ENABLED") 118 | 119 | def _update(apisession:mistapi.APISession, org_id:str, tmpl_id:str, port_usages:dict): 120 | try: 121 | print("Updating PoE Status ".ljust(79, "."), end="", flush=True) 122 | res = mistapi.api.v1.orgs.networktemplates.updateOrgNetworkTemplate(apisession, org_id, tmpl_id, {"port_usages": port_usages}) 123 | if res.status_code == 200: 124 | print("\033[92m\u2714\033[0m") 125 | else: 126 | raise Exception 127 | except: 128 | print('\033[31m\u2716\033[0m') 129 | sys.exit(5) 130 | 131 | def _change(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str, disabled:bool): 132 | port_usages = _get_port_usages(apisession, org_id, tmpl_id) 133 | print("Extracting profile data ".ljust(79, "."), end="", flush=True) 134 | if not profile in port_usages: 135 | print('\033[31m\u2716\033[0m') 136 | print(f"Profile {profile} not found") 137 | else: 138 | print("\033[92m\u2714\033[0m") 139 | port_usages[profile]["poe_disabled"] = disabled 140 | _update(apisession, org_id, tmpl_id, port_usages) 141 | 142 | def _toggle(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str): 143 | port_usages = _get_port_usages(apisession, org_id, tmpl_id) 144 | print("Extracting profile data ".ljust(79, "."), end="", flush=True) 145 | if not profile in port_usages: 146 | print('\033[31m\u2716\033[0m') 147 | print(f"Profile {profile} not found") 148 | else: 149 | print("\033[92m\u2714\033[0m") 150 | port_usages[profile]["poe_disabled"] = not port_usages[profile]["poe_disabled"] 151 | _update(apisession, org_id, tmpl_id, port_usages) 152 | 153 | 154 | #################### 155 | ## MENU 156 | def _show_menu(header:str, menu:list) -> str: 157 | print() 158 | print("".center(80, "-")) 159 | resp=None 160 | menu = sorted(menu, key=str.casefold) 161 | while True: 162 | print(f"{header}") 163 | i=0 164 | for entry in menu: 165 | print(f"{i}) {entry}") 166 | i+=1 167 | resp = input(f"Please select an option (0-{i-1}, q to quit): ") 168 | if resp.lower() == "q": 169 | sys.exit(0) 170 | else: 171 | try: 172 | resp=int(resp) 173 | except: 174 | console.error("Please enter a number\r\n ") 175 | if resp < 0 or resp >= i: 176 | console.error(f"Please enter a number between 0 and {i -1}.") 177 | else: 178 | return menu[resp] 179 | 180 | def _check_parameters(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str, action:str): 181 | if not org_id: 182 | org_id = mistapi.cli.select_org(apisession)[0] 183 | 184 | if not tmpl_id: 185 | response = mistapi.api.v1.orgs.networktemplates.listOrgNetworkTemplates(apisession, org_id) 186 | templates = mistapi.get_all(apisession, response) 187 | template_names = {} 188 | template_menu = [] 189 | for template in templates: 190 | template_menu.append(template.get("name")) 191 | template_names[template.get("name")] = template["id"] 192 | tmpl_name = _show_menu("Please select a Switch Template", template_menu) 193 | tmpl_id = template_names[tmpl_name] 194 | 195 | if not profile: 196 | template_settings = mistapi.api.v1.orgs.networktemplates.getOrgNetworkTemplate(apisession, org_id, tmpl_id).data 197 | port_usages = template_settings.get("port_usages") 198 | if not port_usages: 199 | console.error("There is no profile to update in this switch template") 200 | sys.exit(2) 201 | else: 202 | profile_names = [] 203 | for port_usage in port_usages: 204 | profile_names.append(port_usage) 205 | profile = _show_menu("Please select a Profile", profile_names) 206 | 207 | if not action: 208 | action = _show_menu("Please select an Action", ["status", "on", "off", "toggle"]) 209 | if action != "status": 210 | action_str = action 211 | if action != "toggle": 212 | action_str = f"turn {action}" 213 | while True: 214 | print() 215 | confirm = input(f"Do you confirm you want to {action_str} PoE on port profile {profile} (y/n)?") 216 | if confirm.lower() == "y": 217 | break 218 | elif confirm.lower() == "n": 219 | sys.exit(0) 220 | return org_id, tmpl_id, profile, action 221 | 222 | def start(apisession:mistapi.APISession, org_id:str, tmpl_id:str, profile:str, action:str): 223 | org_id, tmpl_id, profile, action = _check_parameters(apisession, org_id, tmpl_id, profile, action) 224 | print(" Processing ".center(80, "-")) 225 | print() 226 | if action == "status": 227 | _display_status(apisession, org_id, tmpl_id, profile) 228 | if action == "off": 229 | _change(apisession, org_id, tmpl_id, profile, True) 230 | _display_status(apisession, org_id, tmpl_id, profile) 231 | if action == "on": 232 | _change(apisession, org_id, tmpl_id, profile, False) 233 | _display_status(apisession, org_id, tmpl_id, profile) 234 | if action == "toggle": 235 | _toggle(apisession, org_id, tmpl_id, profile) 236 | _display_status(apisession, org_id, tmpl_id, profile) 237 | 238 | ##################################################################### 239 | ##### USAGE #### 240 | def usage(): 241 | print(''' 242 | ------------------------------------------------------------------------------- 243 | 244 | Written by Thomas Munzer (tmunzer@juniper.net) 245 | Github repository: https://github.com/tmunzer/Mist_library/ 246 | 247 | This script is licensed under the MIT License. 248 | 249 | ------------------------------------------------------------------------------- 250 | Python script to enable/disable/toggle PoE for a specified Port Profile in a 251 | Switch Template. 252 | 253 | This script will not change/create/delete/touch any existing objects. It will 254 | just retrieve every single object from the organization. 255 | 256 | ------- 257 | Requirements: 258 | mistapi: https://pypi.org/project/mistapi/ 259 | 260 | ------- 261 | Usage: 262 | This script can be run as is (without parameters), or with the options below. 263 | If no options are defined, or if options are missing, the missing options will 264 | be asked by the script or the default values will be used. 265 | 266 | It is recommended to use an environment file to store the required information 267 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 268 | information about the available parameters). 269 | 270 | ------- 271 | Options: 272 | -o, --org_id= Set the org_id 273 | -t, --tmpl_id= Set the Switch template_id 274 | -p, --profile= Set the Port Profile name 275 | -a, --action= Set the action to execupte. It must be one of the following: 276 | - status: Retrieve the PoE status 277 | - on: Enable PoE on the port profile 278 | - off: Disable PoE on the port profile 279 | - toggle: toogle PoE on the port profile (i.e. turn it off if 280 | currently enable, turn it on if currently enabled) 281 | -e, --env= define the env file to use (see mistapi env file documentation 282 | here: https://pypi.org/project/mistapi/) 283 | default is "~/.mist_env" 284 | 285 | ------- 286 | Examples: 287 | python3 ./toggle_poe.py 288 | python3 ./toggle_poe.py -o 203d3d02-xxxx-xxxx-xxxx-76896a3330f4 -t 2c57044e-xxxx-xxxx-xxxx-69374b32a070 -p ap -a toggle 289 | ''' 290 | ) 291 | sys.exit(0) 292 | 293 | ##################################################################### 294 | ##### ENTRY POINT #### 295 | if __name__ == "__main__": 296 | try: 297 | opts, args = getopt.getopt(sys.argv[1:], "e:o:t:p:a:", ["env=", "org-id=", "tmpl_id=", "profile=", "action="]) 298 | except getopt.GetoptError as err: 299 | print(err) 300 | usage() 301 | sys.exit(2) 302 | 303 | org_id=None 304 | tmpl_id=None 305 | profile=None 306 | action=None 307 | for o, a in opts: 308 | if o in ["-h", "--help"]: 309 | usage() 310 | sys.exit() 311 | elif o in ["-e", "--env"]: 312 | env_file = a 313 | elif o in ["-o", "--org_id"]: 314 | org_id=a 315 | elif o in ["-t", "--tmpl_id"]: 316 | tmpl_id=a 317 | elif o in ["-p", "--profile"]: 318 | profile=a 319 | elif o in ["-a", "--action"]: 320 | if a not in ["status", "on", "off", "toggle"]: 321 | console.error(f"Unknown action \"{o}\"") 322 | usage() 323 | sys.exit(0) 324 | else: 325 | action=a 326 | else: 327 | assert False, "unhandled option" 328 | 329 | #### LOGS #### 330 | logging.basicConfig(filename=log_file, filemode='w') 331 | logger.setLevel(logging.DEBUG) 332 | ### START ### 333 | apisession = mistapi.APISession(env_file=env_file) 334 | apisession.login() 335 | start(apisession, org_id, tmpl_id, profile, action) 336 | 337 | 338 | -------------------------------------------------------------------------------- /scripts/configuration/config_auto_site_assignment.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to update the org auto assignment rules. The script is displaying 11 | options enable/disable the auto assignment, and to build the rules before 12 | updating the org settings 13 | 14 | ------- 15 | Requirements: 16 | mistapi: https://pypi.org/project/mistapi/ 17 | 18 | ------- 19 | Usage: 20 | This script can be run as is (without parameters), or with the options below. 21 | If no options are defined, or if options are missing, the missing options will 22 | be asked by the script or the default values will be used. 23 | 24 | It is recommended to use an environment file to store the required information 25 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 26 | information about the available parameters). 27 | 28 | ------- 29 | Options: 30 | -h, --help display this help 31 | -o, --org_id= Set the org_id (only one of the org_id or site_id can be defined) 32 | --enable Enable the auto assignment (additional configuration will be asked 33 | by the script). Only one action enable/disable is allowed 34 | --disable Disable the auto assignment Only one action enable/disable is 35 | allowed 36 | -l, --log_file= define the filepath/filename where to write the logs 37 | default is "./script.log" 38 | -e, --env= define the env file to use (see mistapi env file documentation 39 | here: https://pypi.org/project/mistapi/) 40 | default is "~/.mist_env" 41 | 42 | ------- 43 | Examples: 44 | python3 ./config_auto_site_assignment.py 45 | python3 ./config_auto_site_assignment.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --disable 46 | 47 | ''' 48 | 49 | #### IMPORTS ##### 50 | import sys 51 | import logging 52 | import getopt 53 | 54 | MISTAPI_MIN_VERSION = "0.44.1" 55 | 56 | try: 57 | import mistapi 58 | from mistapi.__logger import console 59 | except: 60 | print(""" 61 | Critical: 62 | \"mistapi\" package is missing. Please use the pip command to install it. 63 | 64 | # Linux/macOS 65 | python3 -m pip install mistapi 66 | 67 | # Windows 68 | py -m pip install mistapi 69 | """) 70 | sys.exit(2) 71 | 72 | 73 | #### LOGS #### 74 | LOGGER = logging.getLogger(__name__) 75 | 76 | #### PARAMETERS ##### 77 | auto_site_assignment = { 78 | "enable": True, 79 | "rules": [] 80 | } 81 | 82 | LOG_FILE = "./script.log" 83 | ENV_FILE = "~/.mist_env" 84 | #### GLOBAL VARIABLES #### 85 | AUTO_ASSIGNMENT_RULES = ["name", "subnet", "lldp_system_name", "dns_suffix", "model" ] 86 | 87 | #### FUNCTIONS #### 88 | def create_rule(apisession, org_id): 89 | rules = [] 90 | while True: 91 | rule_conf = {} 92 | rule_conf = select_rule_type(apisession, org_id) 93 | mistapi.cli.pretty_print(rule_conf) 94 | resp = input("Is it correct (Y/n)?") 95 | if resp.lower() == 'y' or resp == "": 96 | rules.append(rule_conf) 97 | while True: 98 | resp = input("Do you want to add a new rule (y/N)?") 99 | if resp.lower() == 'n' or resp == "": 100 | return rules 101 | elif resp.lower() == 'y': 102 | break 103 | 104 | def select_rule_type(apisession, org_id): 105 | rule_conf = {} 106 | while True: 107 | print("Type of auto assignment rule:") 108 | i = -1 109 | for rule in AUTO_ASSIGNMENT_RULES: 110 | i+=1 111 | print(f"{i}) {rule}") 112 | resp = input(f"Please select the type of rule (0-{i})") 113 | try: 114 | resp_num = int(resp) 115 | except: 116 | print(f"Error. The value {resp} is not valid") 117 | if resp_num >= 0 and resp_num <= i: 118 | rule_conf['src'] = AUTO_ASSIGNMENT_RULES[resp_num] 119 | return configure_rule(apisession, org_id, rule_conf) 120 | else: 121 | print(f"Error: {resp_num} is not authorized") 122 | 123 | 124 | def configure_rule(apisession, org_id, rule_conf): 125 | if rule_conf['src'] == "name": 126 | # // use device name (via Installer APIs) 127 | #{ 128 | # "src": "name", 129 | # "expression": "[0:3]", // "abcdef" -> "abc" 130 | # "split(.)[1]", // "a.b.c" -> "b" 131 | # "split(-)[1][0:3], // "a1234-b5678-c90" -> "b56" 132 | # "prefix": "XX-", 133 | # "suffix": "-YY" 134 | # }, 135 | print("Expression to extract the site name from the LLDP system name") 136 | print("Example: \"[0:3]\", // \"abcdef\" -> \"abc\"") 137 | print(" \"split(.)[1]\", // \"a.b.c\" -> \"b\"") 138 | print(" \"split(-)[1][0:3]\", // \"a1234-b5678-c90\" -> \"b56\"") 139 | rule_conf['expression'] = input("Expression: ") 140 | rule_conf['prefix'] = input("Prefix (XX-): " ) 141 | rule_conf['suffix'] = input("Suffix (-XX): ") 142 | elif rule_conf['src'] == "subnet": 143 | # // use subnet 144 | # { 145 | # "src": "subnet", 146 | # "subnet": "10.1.2.0/18", 147 | # "value": "s1351" 148 | # }, 149 | rule_conf['subnet'] = input("Please enter the subnet value (ex: 10.1.2.0/18): ") 150 | site_id = mistapi.cli.select_site(apisession, org_id=org_id)[0] 151 | rule_conf['value'] = mistapi.api.v1.sites.stats.getSiteStats(apisession, site_id).data['name'] 152 | elif rule_conf['src'] == "lldp_system_name": 153 | # // use LLDP System Name 154 | # { 155 | # "src": "lldp_system_name", 156 | # "expression": "..." // same as above 157 | # }, 158 | print("Expression to extract the site name from the LLDP system name") 159 | print("Example: \"[0:3]\", // \"abcdef\" -> \"abc\"") 160 | print(" \"split(.)[1]\", // \"a.b.c\" -> \"b\"") 161 | print(" \"split(-)[1][0:3]\", // \"a1234-b5678-c90\" -> \"b56\"") 162 | rule_conf['expression'] = input("Expression: ") 163 | elif rule_conf['src'] == "dns_suffix": 164 | # // use DNS Suffix 165 | # { 166 | # "src": "dns_suffix", 167 | # "expression": "..." // same as above 168 | # }, 169 | print("Expression to extract the site name from the DNS suffix name") 170 | print("Example: \"[0:3]\", // \"abcdef\" -> \"abc\"") 171 | print(" \"split(.)[1]\", // \"a.b.c\" -> \"b\"") 172 | print(" \"split(-)[1][0:3]\", // \"a1234-b5678-c90\" -> \"b56\"") 173 | rule_conf['expression'] = input("Expression: ") 174 | elif rule_conf['src'] == "model": 175 | # { 176 | # "src": "model", 177 | # "model": "AP41", 178 | # "value": "s1351" 179 | # } 180 | rule_conf['model'] = input("Please enter the model of AP: ") 181 | site_id = mistapi.cli.select_site(apisession, org_id=org_id)[0] 182 | rule_conf['value'] = mistapi.api.v1.sites.stats.getSiteStats(apisession, site_id).data['name'] 183 | return rule_conf 184 | 185 | 186 | ############################################################################### 187 | ### USAGE 188 | def usage(): 189 | print(''' 190 | ------------------------------------------------------------------------------- 191 | 192 | Written by Thomas Munzer (tmunzer@juniper.net) 193 | Github repository: https://github.com/tmunzer/Mist_library/ 194 | 195 | This script is licensed under the MIT License. 196 | 197 | ------------------------------------------------------------------------------- 198 | Python script to update the org auto assignment rules. The script is displaying 199 | options enable/disable the auto assignment, and to build the rules before 200 | updating the org settings 201 | 202 | ------- 203 | Requirements: 204 | mistapi: https://pypi.org/project/mistapi/ 205 | 206 | ------- 207 | Usage: 208 | This script can be run as is (without parameters), or with the options below. 209 | If no options are defined, or if options are missing, the missing options will 210 | be asked by the script or the default values will be used. 211 | 212 | It is recommended to use an environment file to store the required information 213 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 214 | information about the available parameters). 215 | 216 | ------- 217 | Options: 218 | -h, --help display this help 219 | -o, --org_id= Set the org_id (only one of the org_id or site_id can be defined) 220 | --enable Enable the auto assignment (additional configuration will be asked 221 | by the script). Only one action enable/disable is allowed 222 | --disable Disable the auto assignment Only one action enable/disable is 223 | allowed 224 | -l, --log_file= define the filepath/filename where to write the logs 225 | default is "./script.log" 226 | -e, --env= define the env file to use (see mistapi env file documentation 227 | here: https://pypi.org/project/mistapi/) 228 | default is "~/.mist_env" 229 | 230 | ------- 231 | Examples: 232 | python3 ./config_auto_site_assignment.py 233 | python3 ./config_auto_site_assignment.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 --disable 234 | 235 | ''') 236 | sys.exit(0) 237 | 238 | def check_mistapi_version(): 239 | """Check if the installed mistapi version meets the minimum requirement.""" 240 | 241 | current_version = mistapi.__version__.split(".") 242 | required_version = MISTAPI_MIN_VERSION.split(".") 243 | 244 | try: 245 | for i, req in enumerate(required_version): 246 | if current_version[int(i)] > req: 247 | break 248 | if current_version[int(i)] < req: 249 | raise ImportError( 250 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 251 | f"but version {mistapi.__version__} is installed." 252 | ) 253 | except ImportError as e: 254 | LOGGER.critical(str(e)) 255 | LOGGER.critical("Please use the pip command to update it.") 256 | LOGGER.critical("") 257 | LOGGER.critical(" # Linux/macOS") 258 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 259 | LOGGER.critical("") 260 | LOGGER.critical(" # Windows") 261 | LOGGER.critical(" py -m pip install --upgrade mistapi") 262 | print( 263 | f""" 264 | Critical:\r\n 265 | {e}\r\n 266 | Please use the pip command to update it. 267 | # Linux/macOS 268 | python3 -m pip install --upgrade mistapi 269 | # Windows 270 | py -m pip install --upgrade mistapi 271 | """ 272 | ) 273 | sys.exit(2) 274 | finally: 275 | LOGGER.info( 276 | '"mistapi" package version %s is required, ' 277 | "you are currently using version %s.", 278 | MISTAPI_MIN_VERSION, 279 | mistapi.__version__ 280 | ) 281 | 282 | 283 | ############################################################################### 284 | ### ENTRY POINT 285 | if __name__ == "__main__": 286 | try: 287 | opts, args = getopt.getopt(sys.argv[1:], "ho:e:l:", ["help", "org_id=", "env=", "log_file=", "enable", "disable"]) 288 | except getopt.GetoptError as err: 289 | console.error(err) 290 | usage() 291 | 292 | ORG_ID=None 293 | ACTION=None 294 | for o, a in opts: 295 | if o in ["-h", "--help"]: 296 | usage() 297 | elif o in ["-o", "--org_id"]: 298 | ORG_ID = a 299 | elif o in ["-e", "--env"]: 300 | env_file=a 301 | elif o in ["-l", "--log_file"]: 302 | LOG_FILE = a 303 | elif o in ["-a", "--enable"]: 304 | if ACTION: 305 | console.error("Only one action \"enable\" or \"disable\" is allowed") 306 | sys.exit(0) 307 | ACTION ="enable" 308 | auto_site_assignment["enable"] = True 309 | elif o in ["-b", "--disable"]: 310 | if ACTION: 311 | console.error("Only one action \"enable\" or \"disable\" is allowed") 312 | sys.exit(0) 313 | ACTION ="disable" 314 | auto_site_assignment["enable"] = False 315 | else: 316 | assert False, "unhandled option" 317 | 318 | #### LOGS #### 319 | logging.basicConfig(filename=LOG_FILE, filemode='w') 320 | LOGGER.setLevel(logging.DEBUG) 321 | check_mistapi_version() 322 | ### MIST SESSION ### 323 | APISESSION = mistapi.APISession(env_file=ENV_FILE) 324 | APISESSION.login() 325 | 326 | if not ORG_ID: 327 | ORG_ID = mistapi.cli.select_org(APISESSION) 328 | 329 | while not ACTION: 330 | resp = input("Do you want to (E)nable of (D)isable auto site assignment (e/d)?") 331 | if resp.lower() == 'e': 332 | auto_site_assignment["enable"] = True 333 | ACTION = "enable" 334 | elif resp.lower() == 'd': 335 | auto_site_assignment["enable"] = False 336 | ACTION = "disable" 337 | else: 338 | console.error("Only \"e\" and \"d\" are allowed, to Enable or Disable auto site assignment") 339 | 340 | if auto_site_assignment["enable"] == True: 341 | auto_site_assignment["rules"] = create_rule(APISESSION, ORG_ID) 342 | 343 | 344 | print("Configuration to upload".center(80, "-")) 345 | mistapi.cli.pretty_print({"auto_site_assignment": auto_site_assignment}) 346 | mistapi.api.v1.orgs.setting.updateOrgSettings(APISESSION, ORG_ID, {"auto_site_assignment": auto_site_assignment}) 347 | print("Configuration from Mist".center(80, "-")) 348 | conf_from_mist = mistapi.api.v1.orgs.setting.getOrgSettings(APISESSION, ORG_ID).data["auto_site_assignment"] 349 | mistapi.cli.pretty_print({"auto_site_assignment": conf_from_mist}) 350 | -------------------------------------------------------------------------------- /scripts/devices/aps/configure_ap_mgmt_vlan.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script reconfigure Management VLAN on all the Mist APs from one or 11 | multiple sites. 12 | 13 | ------- 14 | Requirements: 15 | mistapi: https://pypi.org/project/mistapi/ 16 | 17 | ------- 18 | Usage: 19 | This script can be run as is (without parameters), or with the options below. 20 | If no options are defined, or if options are missing, the missing options will 21 | be asked by the script or the default values will be used. 22 | 23 | It is recommended to use an environment file to store the required information 24 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 25 | information about the available parameters). 26 | 27 | ------- 28 | Script Parameters: 29 | -h, --help display this help 30 | -s, --site_ids= list of sites to use, comma separated 31 | -v, --vlan_id= Set the mgmt VLAN ID, 0 for untagged 32 | -l, --log_file= define the filepath/filename where to write the logs 33 | default is "./script.log" 34 | -e, --env= define the env file to use (see mistapi env file 35 | documentation here: https://pypi.org/project/mistapi/) 36 | default is "~/.mist_env" 37 | 38 | ------- 39 | Examples: 40 | python3 ./configure_ap_mgmt_vlan.py 41 | python3 ./configure_ap_mgmt_vlan.py -s 203d3d02-xxxx-xxxx-xxxx-76896a3330f4,203d3d02-xxxx-xxxx-xxxx-76896a3330f4 -v 31 42 | 43 | ''' 44 | 45 | ##################################################################### 46 | #### IMPORTS #### 47 | import logging 48 | import sys 49 | import getopt 50 | 51 | MISTAPI_MIN_VERSION = "0.44.1" 52 | 53 | try: 54 | import mistapi 55 | from mistapi.__logger import console 56 | except: 57 | print(""" 58 | Critical: 59 | \"mistapi\" package is missing. Please use the pip command to install it. 60 | 61 | # Linux/macOS 62 | python3 -m pip install mistapi 63 | 64 | # Windows 65 | py -m pip install mistapi 66 | """) 67 | sys.exit(2) 68 | 69 | 70 | ##################################################################### 71 | #### PARAMETERS ##### 72 | log_file = "./script.log" 73 | env_file = "~/.mist_env" 74 | 75 | ##################################################################### 76 | #### LOGS #### 77 | LOGGER = logging.getLogger(__name__) 78 | 79 | ##################################################################### 80 | # PROGRESS BAR AND DISPLAY 81 | class ProgressBar(): 82 | def __init__(self): 83 | self.steps_total = 0 84 | self.steps_count = 0 85 | 86 | def _pb_update(self, size:int=80): 87 | if self.steps_count > self.steps_total: 88 | self.steps_count = self.steps_total 89 | 90 | percent = self.steps_count/self.steps_total 91 | delta = 17 92 | x = int((size-delta)*percent) 93 | print(f"Progress: ", end="") 94 | print(f"[{'█'*x}{'.'*(size-delta-x)}]", end="") 95 | print(f"{int(percent*100)}%".rjust(5), end="") 96 | 97 | def _pb_new_step(self, message:str, result:str, inc:bool=False, size:int=80, display_pbar:bool=True): 98 | if inc: self.steps_count += 1 99 | text = f"\033[A\033[F{message}" 100 | print(f"{text} ".ljust(size + 4, "."), result) 101 | print("".ljust(80)) 102 | if display_pbar: self._pb_update(size) 103 | 104 | def _pb_title(self, text:str, size:int=80, end:bool=False, display_pbar:bool=True): 105 | print("\033[A") 106 | print(f" {text} ".center(size, "-"),"\n\n") 107 | if not end and display_pbar: 108 | print("".ljust(80)) 109 | self._pb_update(size) 110 | 111 | def set_steps_total(self, steps_total:int): 112 | self.steps_total = steps_total 113 | 114 | def log_message(self, message, display_pbar:bool=True): 115 | self._pb_new_step(message, " ", display_pbar=display_pbar) 116 | 117 | def log_success(self, message, inc:bool=False, display_pbar:bool=True): 118 | LOGGER.info(f"{message}: Success") 119 | self._pb_new_step(message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar) 120 | 121 | def log_failure(self, message, inc:bool=False, display_pbar:bool=True): 122 | LOGGER.error(f"{message}: Failure") 123 | self._pb_new_step(message, '\033[31m\u2716\033[0m\n', inc=inc, display_pbar=display_pbar) 124 | 125 | def log_title(self, message, end:bool=False, display_pbar:bool=True): 126 | LOGGER.info(message) 127 | self._pb_title(message, end=end, display_pbar=display_pbar) 128 | 129 | pb = ProgressBar() 130 | ##################################################################### 131 | #### FUNCTIONS #### 132 | 133 | def _get_device_ids(apisession:mistapi.APISession, site_name:str, site_id:str): 134 | LOGGER.info(f"{site_id}: Retrieving devices list") 135 | devices = [] 136 | try: 137 | response = mistapi.api.v1.sites.devices.listSiteDevices(apisession, site_id=site_id, type="ap") 138 | devices = mistapi.get_all(apisession, response) 139 | except: 140 | LOGGER.error(f"{site_id}: Unable to retrieve devices list") 141 | print(f"Unable to retrieve devices list from site {site_name}") 142 | finally: 143 | LOGGER.info(f"{site_id}: {len(devices)} devices will be updated") 144 | return devices 145 | 146 | def _update_vlan_id(ip_config:dict, vlan_id:int): 147 | # set "vlan_id" in AP settings if vlan_id > 0 148 | if vlan_id > 0: 149 | ip_config["vlan_id"] = vlan_id 150 | # or delete the "vlan_id" key from the AP settings 151 | elif ip_config.get("vlan_id", None) is not None: 152 | del ip_config["vlan_id"] 153 | return ip_config 154 | 155 | 156 | def _update_devices(apisession:mistapi.APISession, site_name:str, site_id:str, vlan_id:int): 157 | try: 158 | message="Retrieving devices list" 159 | pb.log_message(message, display_pbar=False) 160 | devices = _get_device_ids(apisession, site_name, site_id) 161 | pb.log_success(message, display_pbar=False) 162 | except: 163 | pb.log_failure(message, display_pbar=False) 164 | 165 | if len(devices) == 0: 166 | pb.log_success("No APs assigned to this site") 167 | LOGGER.info(f"{site_id}: no devices to process") 168 | else: 169 | count = len(devices) 170 | pb.set_steps_total(count) 171 | for device in devices: 172 | device_id = device["id"] 173 | device_name = device.get("name", "No Name") 174 | device_mac = device.get("mac") 175 | if not device_name: device_name = device_mac 176 | message=f"Updating device {device_name} ({device_mac})" 177 | pb.log_message(message) 178 | try: 179 | device_settings = mistapi.api.v1.sites.devices.getSiteDevice(apisession, site_id=site_id, device_id=device_id).data 180 | LOGGER.debug(device_settings) 181 | ip_config = device_settings.get("ip_config", {}) 182 | LOGGER.debug(f"ip_config before change: {ip_config}") 183 | ip_config = _update_vlan_id(ip_config, vlan_id) 184 | LOGGER.debug(f"ip_config after change: {ip_config}") 185 | device_settings["ip_config"] = ip_config 186 | mistapi.api.v1.sites.devices.updateSiteDevice(apisession, site_id, device_id, device_settings) 187 | pb.log_success(message, inc=True) 188 | except: 189 | pb.log_failure(message, inc=True) 190 | 191 | ##################################################################### 192 | ##### MENU #### 193 | def _enter_vlan_id(): 194 | vid = -1 195 | while vid < 0 or vid > 4095: 196 | print("") 197 | resp = input("Management VLAN ID (0 for untagged): ") 198 | try: 199 | resp = int(resp) 200 | if resp < 0 or resp > 4095: 201 | print("Please enter a number between 0 and 4095") 202 | else: 203 | vid = resp 204 | except: 205 | print("Please enter a number between 0 and 4095") 206 | return vid 207 | 208 | 209 | def process_sites(apisession:mistapi.APISession, site_ids:list, vlan_id:int): 210 | print() 211 | for site_id in site_ids: 212 | LOGGER.info(f"{site_id}: Processing site") 213 | site_info = mistapi.api.v1.sites.sites.getSiteInfo(apisession, site_id).data 214 | site_name = site_info["name"] 215 | LOGGER.info(f"{site_id}: name is {site_name}") 216 | pb.log_title(f"Processing Site {site_name}", display_pbar=False) 217 | _update_devices(apisession, site_name, site_id, vlan_id) 218 | pb.log_title("VLAN configuration done", end=True) 219 | 220 | 221 | def start(apisession:mistapi.APISession, site_ids:list=None, vlan_id:int=-1): 222 | if not site_ids: 223 | site_ids = mistapi.cli.select_site(apisession, allow_many=True) 224 | if not type(vlan_id) == int or vlan_id < 0: 225 | vlan_id = _enter_vlan_id() 226 | LOGGER.info(f"Site IDs: {site_ids}") 227 | LOGGER.info(f"VLAN ID : {vlan_id}") 228 | process_sites(apisession, site_ids, vlan_id) 229 | 230 | 231 | ##################################################################### 232 | ##### USAGE #### 233 | def usage(): 234 | print(''' 235 | ------------------------------------------------------------------------------- 236 | 237 | Written by Thomas Munzer (tmunzer@juniper.net) 238 | Github repository: https://github.com/tmunzer/Mist_library/ 239 | 240 | This script is licensed under the MIT License. 241 | 242 | ------------------------------------------------------------------------------- 243 | Python script reconfigure Mist APs with a tagged managed VLAN 244 | 245 | ------- 246 | Requirements: 247 | mistapi: https://pypi.org/project/mistapi/ 248 | 249 | ------- 250 | Usage: 251 | This script can be run as is (without parameters), or with the options below. 252 | If no options are defined, or if options are missing, the missing options will 253 | be asked by the script or the default values will be used. 254 | 255 | It is recommended to use an environment file to store the required information 256 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 257 | information about the available parameters). 258 | 259 | ------- 260 | Script Parameters: 261 | -h, --help display this help 262 | -s, --site_ids= list of sites to use, comma separated 263 | -v, --vlan_id= Set the mgmt VLAN ID, 0 for untagged 264 | -l, --log_file= define the filepath/filename where to write the logs 265 | default is "./script.log" 266 | -e, --env= define the env file to use (see mistapi env file 267 | documentation here: https://pypi.org/project/mistapi/) 268 | default is "~/.mist_env" 269 | 270 | ------- 271 | Examples: 272 | python3 ./configure_ap_mgmt_vlan.py 273 | python3 ./configure_ap_mgmt_vlan.py -s 203d3d02-xxxx-xxxx-xxxx-76896a3330f4,203d3d02-xxxx-xxxx-xxxx-76896a3330f4 -v 31 274 | 275 | ''' 276 | ) 277 | sys.exit(0) 278 | 279 | def check_mistapi_version(): 280 | """Check if the installed mistapi version meets the minimum requirement.""" 281 | 282 | current_version = mistapi.__version__.split(".") 283 | required_version = MISTAPI_MIN_VERSION.split(".") 284 | 285 | try: 286 | for i, req in enumerate(required_version): 287 | if current_version[int(i)] > req: 288 | break 289 | if current_version[int(i)] < req: 290 | raise ImportError( 291 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 292 | f"but version {mistapi.__version__} is installed." 293 | ) 294 | except ImportError as e: 295 | LOGGER.critical(str(e)) 296 | LOGGER.critical("Please use the pip command to update it.") 297 | LOGGER.critical("") 298 | LOGGER.critical(" # Linux/macOS") 299 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 300 | LOGGER.critical("") 301 | LOGGER.critical(" # Windows") 302 | LOGGER.critical(" py -m pip install --upgrade mistapi") 303 | print( 304 | f""" 305 | Critical:\r\n 306 | {e}\r\n 307 | Please use the pip command to update it. 308 | # Linux/macOS 309 | python3 -m pip install --upgrade mistapi 310 | # Windows 311 | py -m pip install --upgrade mistapi 312 | """ 313 | ) 314 | sys.exit(2) 315 | finally: 316 | LOGGER.info( 317 | '"mistapi" package version %s is required, ' 318 | "you are currently using version %s.", 319 | MISTAPI_MIN_VERSION, 320 | mistapi.__version__ 321 | ) 322 | 323 | ##################################################################### 324 | ##### ENTRY POINT #### 325 | if __name__ == "__main__": 326 | try: 327 | opts, args = getopt.getopt(sys.argv[1:], "hs:v:e:l:", [ 328 | "help", "site_ids=", "vlan_id=", "env=", "log_file="]) 329 | except getopt.GetoptError as err: 330 | console.error(err) 331 | usage() 332 | 333 | site_ids = [] 334 | vlan_id = -1 335 | for o, a in opts: 336 | if o in ["-h", "--help"]: 337 | usage() 338 | elif o in ["-s", "--site_ids"]: 339 | try: 340 | for site_id in a.split(","): 341 | site_ids.append(site_id.strip()) 342 | except: 343 | console.critical("Unable to parse the Site IDs from the parameters") 344 | sys.exit(1) 345 | elif o in ["-v", "--vlan_id"]: 346 | try: 347 | vlan_id = int(a) 348 | except: 349 | console.critical("Unable to parse the VLAN ID from the parameters") 350 | sys.exit(1) 351 | elif o in ["-e", "--env"]: 352 | env_file = a 353 | elif o in ["-l", "--log_file"]: 354 | log_file = a 355 | else: 356 | assert False, "unhandled option" 357 | 358 | #### LOGS #### 359 | logging.basicConfig(filename=log_file, filemode='w') 360 | LOGGER.setLevel(logging.DEBUG) 361 | check_mistapi_version() 362 | ### START ### 363 | apisession = mistapi.APISession(env_file=env_file) 364 | apisession.login() 365 | start(apisession, site_ids, vlan_id) 366 | 367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /scripts/devices/gateways/fix_gateway_backup_firmware.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script trigger a snapshot/firmware backup on SRX devices. 11 | This script is using the CSV report for the report_gateway_firmware.py script to 12 | identify the SRX on which the command must be triggered. 13 | 14 | ------- 15 | Requirements: 16 | mistapi: https://pypi.org/project/mistapi/ 17 | 18 | ------- 19 | Usage: 20 | This script can be run as is (without parameters), or with the options below. 21 | If no options are defined, or if options are missing, the missing options will 22 | be asked by the script or the default values will be used. 23 | 24 | It is recommended to use an environment file to store the required information 25 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 26 | information about the available parameters). 27 | 28 | ------- 29 | Options: 30 | -h, --help display this help 31 | 32 | -s, --site_id= Set the site_id if only devices from a specific site must be 33 | processed. If not set, the script will process all the devices 34 | from the report that need a firmware backup 35 | -f, --in_file= path to the report generated by the report_gateway_firmware script 36 | default is "./report_gateway_firmware.csv" 37 | --auto-approve Does not ask for user confirmation before triggering the 38 | firmware backup API Calls 39 | 40 | -l, --log_file= define the filepath/filename where to write the logs 41 | default is "./script.log" 42 | -e, --env= define the env file to use (see mistapi env file documentation 43 | here: https://pypi.org/project/mistapi/) 44 | default is "~/.mist_env" 45 | 46 | ------- 47 | Examples: 48 | python3 ./fix_gateway_backup_firmware.py 49 | python3 ./fix_gateway_backup_firmware.py --site_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 50 | 51 | ''' 52 | 53 | #### IMPORTS ##### 54 | import sys 55 | import csv 56 | import getopt 57 | import logging 58 | 59 | MISTAPI_MIN_VERSION = "0.53.0" 60 | 61 | try: 62 | import mistapi 63 | from mistapi.__logger import console as CONSOLE 64 | except: 65 | print(""" 66 | Critical: 67 | \"mistapi\" package is missing. Please use the pip command to install it. 68 | 69 | # Linux/macOS 70 | python3 -m pip install mistapi 71 | 72 | # Windows 73 | py -m pip install mistapi 74 | """) 75 | sys.exit(2) 76 | 77 | 78 | 79 | #### LOGS #### 80 | LOGGER = logging.getLogger(__name__) 81 | 82 | #### PARAMETERS ##### 83 | CSV_FILE = "./report_gateway_firmware.csv" 84 | LOG_FILE = "./script.log" 85 | ENV_FILE = "~/.mist_env" 86 | 87 | 88 | 89 | ##################################################################### 90 | # PROGRESS BAR AND DISPLAY 91 | class ProgressBar: 92 | def __init__(self): 93 | self.steps_total = 0 94 | self.steps_count = 0 95 | 96 | def _pb_update(self, size: int = 80): 97 | if self.steps_count > self.steps_total: 98 | self.steps_count = self.steps_total 99 | 100 | percent = self.steps_count / self.steps_total 101 | delta = 17 102 | x = int((size - delta) * percent) 103 | print(f"Progress: ", end="") 104 | print(f"[{'█'*x}{'.'*(size-delta-x)}]", end="") 105 | print(f"{int(percent*100)}%".rjust(5), end="") 106 | 107 | def _pb_new_step( 108 | self, 109 | message: str, 110 | result: str, 111 | inc: bool = False, 112 | size: int = 80, 113 | display_pbar: bool = True, 114 | ): 115 | if inc: 116 | self.steps_count += 1 117 | text = f"\033[A\033[F{message}" 118 | print(f"{text} ".ljust(size + 4, "."), result) 119 | print("".ljust(80)) 120 | if display_pbar: 121 | self._pb_update(size) 122 | 123 | def _pb_title( 124 | self, text: str, size: int = 80, end: bool = False, display_pbar: bool = True 125 | ): 126 | print("\033[A") 127 | print(f" {text} ".center(size, "-"), "\n\n") 128 | if not end and display_pbar: 129 | print("".ljust(80)) 130 | self._pb_update(size) 131 | 132 | def set_steps_total(self, steps_total: int): 133 | self.steps_total = steps_total 134 | 135 | def log_message(self, message, display_pbar: bool = True): 136 | self._pb_new_step(message, " ", display_pbar=display_pbar) 137 | 138 | def log_success(self, message, inc: bool = False, display_pbar: bool = True): 139 | LOGGER.info(f"{message}: Success") 140 | self._pb_new_step( 141 | message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar 142 | ) 143 | 144 | def log_warning(self, message, inc: bool = False, display_pbar: bool = True): 145 | LOGGER.warning(f"{message}: Warning") 146 | self._pb_new_step( 147 | message, "\033[93m\u2B58\033[0m\n", inc=inc, display_pbar=display_pbar 148 | ) 149 | 150 | def log_failure(self, message, inc: bool = False, display_pbar: bool = True): 151 | LOGGER.error(f"{message}: Failure") 152 | self._pb_new_step( 153 | message, "\033[31m\u2716\033[0m\n", inc=inc, display_pbar=display_pbar 154 | ) 155 | 156 | def log_title(self, message, end: bool = False, display_pbar: bool = True): 157 | LOGGER.info(message) 158 | self._pb_title(message, end=end, display_pbar=display_pbar) 159 | 160 | 161 | PB = ProgressBar() 162 | 163 | ############################################################################### 164 | #### FUNCTIONS #### 165 | def _process_gateways(apisession:mistapi.APISession, gateways:list) -> list: 166 | i=0 167 | PB.set_steps_total(len(gateways)) 168 | for gateway in gateways: 169 | site_id = gateway.get("cluster_site_id") 170 | device_id = gateway.get("cluster_device_id") 171 | device_mac = gateway.get("module_mac") 172 | message = f"Processing device {device_id}" 173 | PB.log_message(message) 174 | if not site_id: 175 | PB.log_failure(message, inc=True) 176 | CONSOLE.error(f"Missing site_id for device {device_mac}") 177 | continue 178 | if not device_id: 179 | PB.log_failure(message, inc=True) 180 | CONSOLE.error(f"Missing device_id for device {device_mac}") 181 | continue 182 | try: 183 | resp = mistapi.api.v1.sites.devices.createSiteDeviceSnapshot(apisession, site_id, device_id) 184 | if resp.status_code == 200: 185 | PB.log_success(message, inc=True) 186 | else: 187 | PB.log_failure(message, inc=True) 188 | except: 189 | PB.log_failure(message, inc=True) 190 | LOGGER.error("Exception occurred", exc_info=True) 191 | 192 | ### READ REPORT 193 | def _read_csv(csv_file:str, site_id:str) -> list: 194 | data = [] 195 | device_ids = [] 196 | message="Reading CSV Report" 197 | PB.log_message(message, display_pbar=False) 198 | try: 199 | with open(csv_file, newline='') as f: 200 | reader = csv.DictReader(filter(lambda row: row[0]!='#', f)) 201 | for row in reader: 202 | if not "SRX" in row.get("module_model"): 203 | continue 204 | if site_id and row.get("cluster_site_id") != site_id: 205 | continue 206 | if row.get("module_need_snapshot") != "True": 207 | continue 208 | if row.get("cluster_device_id") in device_ids: 209 | continue 210 | data.append(row) 211 | device_ids.append(row.get("cluster_device_id")) 212 | PB.log_success(message, inc=False, display_pbar=False) 213 | except: 214 | PB.log_failure(message, inc=False, display_pbar=False) 215 | LOGGER.error("Exception occurred", exc_info=True) 216 | sys.exit(1) 217 | return data 218 | 219 | def _request_approval(data:dict): 220 | print("".center(80, "-")) 221 | print("List of gateways to process:") 222 | print() 223 | mistapi.cli.pretty_print(data) 224 | r = input("Do you want to continue (y/N)? ") 225 | if r.lower() == "y": 226 | print("".center(80, "-")) 227 | return 228 | else: 229 | CONSOLE.info("process stopped by the user. Exiting...") 230 | sys.exit(0) 231 | 232 | 233 | ############################################################################### 234 | ### START 235 | def _start(apisession: mistapi.APISession, site_id: str, csv_file:str=CSV_FILE, auto_approve:bool=False) -> None: 236 | 237 | data = _read_csv(csv_file, site_id) 238 | if not data: 239 | print("All the gateways are compliant... Exiting...") 240 | sys.exit(0) 241 | if auto_approve: 242 | CONSOLE.info("auto-approve parameter has been set to True. Starting the process") 243 | else: 244 | _request_approval(data) 245 | _process_gateways(apisession, data) 246 | 247 | 248 | ############################################################################### 249 | ### USAGE 250 | def usage(): 251 | print(''' 252 | ------------------------------------------------------------------------------- 253 | 254 | Written by Thomas Munzer (tmunzer@juniper.net) 255 | Github repository: https://github.com/tmunzer/Mist_library/ 256 | 257 | This script is licensed under the MIT License. 258 | 259 | ------------------------------------------------------------------------------- 260 | Python script trigger a snapshot/firmware backup on SRX devices. 261 | This script is using the CSV report for the report_gateway_firmware.py script to 262 | identify the SRX on which the command must be triggered. 263 | 264 | ------- 265 | Requirements: 266 | mistapi: https://pypi.org/project/mistapi/ 267 | 268 | ------- 269 | Usage: 270 | This script can be run as is (without parameters), or with the options below. 271 | If no options are defined, or if options are missing, the missing options will 272 | be asked by the script or the default values will be used. 273 | 274 | It is recommended to use an environment file to store the required information 275 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 276 | information about the available parameters). 277 | 278 | ------- 279 | Options: 280 | -h, --help display this help 281 | 282 | -s, --site_id= Set the site_id if only devices from a specific site must be 283 | processed. If not set, the script will process all the devices 284 | from the report that need a firmware backup 285 | -f, --in_file= path to the report generated by the report_gateway_firmware script 286 | default is "./report_gateway_firmware.csv" 287 | --auto-approve Does not ask for user confirmation before triggering the 288 | firmware backup API Calls 289 | 290 | -l, --log_file= define the filepath/filename where to write the logs 291 | default is "./script.log" 292 | -e, --env= define the env file to use (see mistapi env file documentation 293 | here: https://pypi.org/project/mistapi/) 294 | default is "~/.mist_env" 295 | 296 | ------- 297 | Examples: 298 | python3 ./fix_gateway_backup_firmware.py 299 | python3 ./fix_gateway_backup_firmware.py --site_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 300 | 301 | ''') 302 | sys.exit(0) 303 | 304 | def check_mistapi_version(): 305 | """Check if the installed mistapi version meets the minimum requirement.""" 306 | 307 | current_version = mistapi.__version__.split(".") 308 | required_version = MISTAPI_MIN_VERSION.split(".") 309 | 310 | try: 311 | for i, req in enumerate(required_version): 312 | if current_version[int(i)] > req: 313 | break 314 | if current_version[int(i)] < req: 315 | raise ImportError( 316 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 317 | f"but version {mistapi.__version__} is installed." 318 | ) 319 | except ImportError as e: 320 | LOGGER.critical(str(e)) 321 | LOGGER.critical("Please use the pip command to update it.") 322 | LOGGER.critical("") 323 | LOGGER.critical(" # Linux/macOS") 324 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 325 | LOGGER.critical("") 326 | LOGGER.critical(" # Windows") 327 | LOGGER.critical(" py -m pip install --upgrade mistapi") 328 | print( 329 | f""" 330 | Critical:\r\n 331 | {e}\r\n 332 | Please use the pip command to update it. 333 | # Linux/macOS 334 | python3 -m pip install --upgrade mistapi 335 | # Windows 336 | py -m pip install --upgrade mistapi 337 | """ 338 | ) 339 | sys.exit(2) 340 | finally: 341 | LOGGER.info( 342 | '"mistapi" package version %s is required, ' 343 | "you are currently using version %s.", 344 | MISTAPI_MIN_VERSION, 345 | mistapi.__version__ 346 | ) 347 | 348 | 349 | 350 | ############################################################################### 351 | ### ENTRY POINT 352 | if __name__ == "__main__": 353 | try: 354 | opts, args = getopt.getopt(sys.argv[1:], "hs:f:e:l:", ["help", "site_id", "in_file=", "auto-approve", "env=", "log_file="]) 355 | except getopt.GetoptError as err: 356 | CONSOLE.error(err) 357 | usage() 358 | 359 | SITE_ID=None 360 | AUTO_APPROVE=False 361 | for o, a in opts: 362 | if o in ["-h", "--help"]: 363 | usage() 364 | elif o in ["-s", "--site_id"]: 365 | SITE_ID = a 366 | elif o in ["-f", "--in_file"]: 367 | CSV_FILE=a 368 | elif o in ["--auto-approve"]: 369 | AUTO_APPROVE=True 370 | elif o in ["-e", "--env"]: 371 | ENV_FILE=a 372 | elif o in ["-l", "--log_file"]: 373 | LOG_FILE = a 374 | 375 | else: 376 | assert False, "unhandled option" 377 | 378 | #### LOGS #### 379 | logging.basicConfig(filename=LOG_FILE, filemode='w') 380 | LOGGER.setLevel(logging.DEBUG) 381 | check_mistapi_version() 382 | ### MIST SESSION ### 383 | APISESSION = mistapi.APISession(env_file=ENV_FILE) 384 | APISESSION.login() 385 | ### START ### 386 | _start(APISESSION, SITE_ID, CSV_FILE, AUTO_APPROVE) 387 | -------------------------------------------------------------------------------- /scripts/exports/export_inventory.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | Python script to export the inventory from an organization. The export will 12 | include all the information available from the org inventory, including the 13 | claim codes. 14 | The current version is not exporting Mist Edges inventory. 15 | 16 | ------- 17 | Requirements: 18 | mistapi: https://pypi.org/project/mistapi/ 19 | 20 | ------- 21 | Usage: 22 | This script can be run as is (without parameters), or with the options below. 23 | If no options are defined, or if options are missing, the missing options will 24 | be asked by the script. 25 | 26 | It is recommended to use an environment file to store the required information 27 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 28 | information about the available parameters). 29 | 30 | ------- 31 | Options: 32 | -h, --help display this help 33 | -o, --org_id= required for Org reports. Set the org_id 34 | -a, --all add the "virtual" devices to the export (e.g. add the 35 | virtual device used by Mist to identify a virtual chassis) 36 | 37 | -l, --log_file= define the filepath/filename where to write the logs 38 | default is "./script.log" 39 | -f, --out_file= define the filepath/filename where to save the data 40 | default is "./export.csv" 41 | --out_format= define the output format (csv or json) 42 | default is csv 43 | -e, --env= define the env file to use (see mistapi env file documentation 44 | here: https://pypi.org/project/mistapi/) 45 | default is "~/.mist_env" 46 | 47 | ------- 48 | Examples: 49 | python3 ./export_inventory.py 50 | python3 ./export_inventory.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 51 | ''' 52 | 53 | #### IMPORTS #### 54 | 55 | import sys 56 | import json 57 | import csv 58 | import os 59 | import logging 60 | import datetime 61 | import getopt 62 | 63 | MISTAPI_MIN_VERSION = "0.44.1" 64 | 65 | try: 66 | import mistapi 67 | from mistapi.__logger import console 68 | except: 69 | print(""" 70 | Critical: 71 | \"mistapi\" package is missing. Please use the pip command to install it. 72 | 73 | # Linux/macOS 74 | python3 -m pip install mistapi 75 | 76 | # Windows 77 | py -m pip install mistapi 78 | """) 79 | sys.exit(2) 80 | 81 | #### PARAMETERS ##### 82 | 83 | log_file = "./script.log" 84 | env_file = os.path.join(os.path.expanduser('~'), ".mist_env") 85 | out_file_format="csv" 86 | out_file_path="./export.csv" 87 | 88 | #### LOGS #### 89 | LOGGER = logging.getLogger(__name__) 90 | 91 | 92 | ##################################################################### 93 | # PROGRESS BAR AND DISPLAY 94 | class ProgressBar(): 95 | def __init__(self): 96 | self.steps_total = 0 97 | self.steps_count = 0 98 | 99 | def _pb_update(self, size:int=80): 100 | if self.steps_count > self.steps_total: 101 | self.steps_count = self.steps_total 102 | if self.steps_total > 0: 103 | percent = self.steps_count/self.steps_total 104 | else: 105 | percent = 0 106 | delta = 17 107 | x = int((size-delta)*percent) 108 | print(f"Progress: ", end="") 109 | print(f"[{'█'*x}{'.'*(size-delta-x)}]", end="") 110 | print(f"{int(percent*100)}%".rjust(5), end="") 111 | 112 | def _pb_new_step(self, message:str, result:str, inc:bool=False, size:int=80, display_pbar:bool=True): 113 | if inc: self.steps_count += 1 114 | text = f"\033[A\033[F{message}" 115 | print(f"{text} ".ljust(size + 4, "."), result) 116 | print("".ljust(80)) 117 | if display_pbar: self._pb_update(size) 118 | 119 | def _pb_title(self, text:str, size:int=80, end:bool=False, display_pbar:bool=True): 120 | print("\033[A") 121 | print(f" {text} ".center(size, "-"),"\n\n") 122 | if not end and display_pbar: 123 | print("".ljust(80)) 124 | self._pb_update(size) 125 | 126 | def set_steps_total(self, steps_total:int): 127 | self.steps_total = steps_total 128 | 129 | def log_message(self, message, display_pbar:bool=True): 130 | self._pb_new_step(message, " ", display_pbar=display_pbar) 131 | 132 | def log_success(self, message, inc:bool=False, display_pbar:bool=True): 133 | LOGGER.info(f"{message}: Success") 134 | self._pb_new_step(message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar) 135 | 136 | def log_failure(self, message, inc:bool=False, display_pbar:bool=True): 137 | LOGGER.error(f"{message}: Failure") 138 | self._pb_new_step(message, '\033[31m\u2716\033[0m\n', inc=inc, display_pbar=display_pbar) 139 | 140 | def log_title(self, message, end:bool=False, display_pbar:bool=True): 141 | LOGGER.info(message) 142 | self._pb_title(message, end=end, display_pbar=display_pbar) 143 | 144 | def inc_only(self, size:int=80): 145 | print("\033[F") 146 | self.steps_count += 1 147 | self._pb_update(size) 148 | 149 | def reinit(self): 150 | self.steps_count = 0 151 | 152 | 153 | pb = ProgressBar() 154 | 155 | #################### 156 | ## REQUEST 157 | def _resolve_vcmembers_site_id(switches:list, all:bool) -> list: 158 | mac_to_site_id = {} 159 | data = [] 160 | for switch in switches: 161 | if switch.get("vc_mac") == switch.get("mac"): 162 | mac_to_site_id[switch["mac"]] = switch["site_id"] 163 | for switch in switches: 164 | if switch.get("vc_mac") and switch.get("vc_mac") != switch.get("mac"): 165 | switch["site_id"] = mac_to_site_id.get(switch["vc_mac"]) 166 | if all or not switch.get("mac").startswith("020003"): 167 | data.append(switch) 168 | return data 169 | 170 | def _process_export(apisession:mistapi.APISession, org_id:str, all:bool): 171 | data = [] 172 | 173 | print() 174 | pb.log_title("Retrieving Data from Mist", display_pbar=False) 175 | 176 | for device_type in ["ap", "switch", "gateway"]: 177 | try: 178 | message = f"Retrieving {device_type.title()} Inventory" 179 | pb.log_message(message, display_pbar=False) 180 | response = mistapi.api.v1.orgs.inventory.getOrgInventory(apisession, org_id, type=device_type, vc=True) 181 | devices = mistapi.get_all(apisession, response) 182 | if device_type == "switch": 183 | devices = _resolve_vcmembers_site_id(devices, all) 184 | data+=devices 185 | pb.log_success(message, display_pbar=False) 186 | except: 187 | pb.log_failure(message, display_pbar=False) 188 | LOGGER.error("Exception occurred", exc_info=True) 189 | return data 190 | #################### 191 | ## SAVE TO FILE 192 | def _save_as_csv(data:list, org_name:str, org_id:str): 193 | headers=[] 194 | total = len(data) 195 | pb.log_title("Saving Data", display_pbar=False) 196 | pb.set_steps_total(total) 197 | message = "Generating CSV Headers" 198 | pb.log_message(message) 199 | i = 0 200 | for entry in data: 201 | for key in entry: 202 | if not key in headers: 203 | headers.append(key) 204 | i += 1 205 | pb.inc_only() 206 | pb.log_success(message, inc=True) 207 | 208 | message="Saving to file" 209 | pb.set_steps_total(total) 210 | pb.reinit() 211 | pb.log_message(message) 212 | i = 0 213 | with open(out_file_path, "w", encoding='UTF8', newline='') as f: 214 | csv_writer = csv.writer(f) 215 | csv_writer.writerow([f"#Report: Device Inventory Export", f"Date: {datetime.datetime.now()}", f"Org Name: {org_name}", f"Org ID: {org_id}"]) 216 | csv_writer.writerow(headers) 217 | for entry in data: 218 | tmp=[] 219 | for header in headers: 220 | tmp.append(entry.get(header, "")) 221 | csv_writer.writerow(tmp) 222 | i += 1 223 | pb.inc_only() 224 | pb.log_success(message, inc=True) 225 | print("\033[FDone.".ljust(80)) 226 | 227 | 228 | def _save_as_json(data:list, org_name:str, org_id:str): 229 | print(" Saving Data ".center(80, "-")) 230 | print() 231 | json_data = { 232 | 'report': "Device Inventory Export", 233 | 'Date': f'{datetime.datetime.now()}', 234 | 'Org Name': org_name, 235 | 'Org ID': org_id, 236 | 'data': data 237 | } 238 | with open(os.path.abspath(out_file_path), 'w') as f: 239 | json.dump(json_data, f) 240 | print("Done.") 241 | 242 | #################### 243 | ## MENU 244 | 245 | def start(apisession, org_id:str=None, all:bool=False): 246 | if not org_id: 247 | org_id = mistapi.cli.select_org(apisession)[0] 248 | org_name = mistapi.api.v1.orgs.orgs.getOrg(apisession, org_id).data["name"] 249 | data=_process_export(apisession, org_id, all) 250 | if out_file_format == "csv": 251 | _save_as_csv(data, org_name, org_id) 252 | elif out_file_format == "json": 253 | _save_as_json(data, org_name, org_id) 254 | else: 255 | console.error(f"file format {out_file_format} not supported") 256 | 257 | def usage(): 258 | print(f""" 259 | ------------------------------------------------------------------------------- 260 | 261 | Written by Thomas Munzer (tmunzer@juniper.net) 262 | Github repository: https://github.com/tmunzer/Mist_library/ 263 | 264 | This script is licensed under the MIT License. 265 | 266 | ------------------------------------------------------------------------------- 267 | 268 | Python script to export the inventory from an organization. The export will 269 | include all the information available from the org inventory, including the 270 | claim codes. 271 | The current version is not exporting Mist Edges inventory. 272 | 273 | ------- 274 | Requirements: 275 | mistapi: https://pypi.org/project/mistapi/ 276 | 277 | ------- 278 | Usage: 279 | This script can be run as is (without parameters), or with the options below. 280 | If no options are defined, or if options are missing, the missing options will 281 | be asked by the script. 282 | 283 | It is recommended to use an environment file to store the required information 284 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 285 | information about the available parameters). 286 | 287 | ------- 288 | Options: 289 | -h, --help display this help 290 | -o, --org_id= required for Org reports. Set the org_id 291 | -a, --all add the "virtual" devices to the export (e.g. add the 292 | virtual device used by Mist to identify a virtual chassis) 293 | 294 | -l, --log_file= define the filepath/filename where to write the logs 295 | default is {log_file} 296 | -f, --out_file= define the filepath/filename where to save the data 297 | default is {out_file_path} 298 | --out_format= define the output format (csv or json) 299 | default is csv 300 | -e, --env= define the env file to use (see mistapi env file documentation 301 | here: https://pypi.org/project/mistapi/) 302 | default is {env_file} 303 | 304 | ------- 305 | Examples: 306 | python3 ./export_inventory.py 307 | python3 ./export_inventory.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 308 | """) 309 | sys.exit(0) 310 | 311 | def check_mistapi_version(): 312 | """Check if the installed mistapi version meets the minimum requirement.""" 313 | 314 | current_version = mistapi.__version__.split(".") 315 | required_version = MISTAPI_MIN_VERSION.split(".") 316 | 317 | try: 318 | for i, req in enumerate(required_version): 319 | if current_version[int(i)] > req: 320 | break 321 | if current_version[int(i)] < req: 322 | raise ImportError( 323 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 324 | f"but version {mistapi.__version__} is installed." 325 | ) 326 | except ImportError as e: 327 | LOGGER.critical(str(e)) 328 | LOGGER.critical("Please use the pip command to update it.") 329 | LOGGER.critical("") 330 | LOGGER.critical(" # Linux/macOS") 331 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 332 | LOGGER.critical("") 333 | LOGGER.critical(" # Windows") 334 | LOGGER.critical(" py -m pip install --upgrade mistapi") 335 | print( 336 | f""" 337 | Critical:\r\n 338 | {e}\r\n 339 | Please use the pip command to update it. 340 | # Linux/macOS 341 | python3 -m pip install --upgrade mistapi 342 | # Windows 343 | py -m pip install --upgrade mistapi 344 | """ 345 | ) 346 | sys.exit(2) 347 | finally: 348 | LOGGER.info( 349 | '"mistapi" package version %s is required, ' 350 | "you are currently using version %s.", 351 | MISTAPI_MIN_VERSION, 352 | mistapi.__version__ 353 | ) 354 | 355 | #### SCRIPT ENTRYPOINT #### 356 | 357 | if __name__ == "__main__": 358 | try: 359 | opts, args = getopt.getopt( 360 | sys.argv[1:], "ho:f:e:l:a", 361 | [ 362 | "help", 363 | "org_id=", 364 | "out_format=", 365 | "out_file=", 366 | "env=", 367 | "log_file=", 368 | "all" 369 | ] 370 | ) 371 | except getopt.GetoptError as err: 372 | console.error(err) 373 | usage() 374 | 375 | org_id=None 376 | all=False 377 | for o, a in opts: 378 | if o in ["-h", "--help"]: 379 | usage() 380 | elif o in ["-o", "--org_id"]: 381 | org_id = a 382 | elif o in ["-a", "--all"]: 383 | all=True 384 | elif o in ["--out_format"]: 385 | if a in ["csv", "json"]: 386 | out_file_format=a 387 | else: 388 | console.error(f"Out format {a} not supported") 389 | usage() 390 | elif o in ["-f", "--out_file"]: 391 | out_file_path=a 392 | elif o in ["-e", "--env"]: 393 | env_file=a 394 | elif o in ["-l", "--log_file"]: 395 | log_file = a 396 | else: 397 | assert False, "unhandled option" 398 | 399 | #### LOGS #### 400 | logging.basicConfig(filename=log_file, filemode='w') 401 | LOGGER.setLevel(logging.DEBUG) 402 | check_mistapi_version() 403 | ### START ### 404 | apisession = mistapi.APISession(env_file=env_file) 405 | apisession.login() 406 | start(apisession, org_id, all) 407 | -------------------------------------------------------------------------------- /scripts/orgs/org_complete_backup.py: -------------------------------------------------------------------------------- 1 | """ 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to backup a whole organization configuration and devices. 11 | 12 | This script will not change/create/delete/touch any existing objects. It will 13 | just retrieve every single object from the organization. 14 | 15 | ------- 16 | Requirements: 17 | mistapi: https://pypi.org/project/mistapi/ 18 | 19 | This script requires the following scripts to be in the same folder: 20 | - org_conf_backup.py 21 | - org_inventory_backup.py 22 | 23 | ------- 24 | Usage: 25 | This script can be run as is (without parameters), or with the options below. 26 | If no options are defined, or if options are missing, the missing options will 27 | be asked by the script or the default values will be used. 28 | 29 | It is recommended to use an environment file to store the required information 30 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 31 | information about the available parameters). 32 | 33 | ------- 34 | Script Parameters: 35 | -h, --help display this help 36 | 37 | -o, --org_id= Optional, org_id of the org to clone 38 | 39 | --src_env= Optional, env file to use to access the src org (see 40 | mistapi env file documentation here: 41 | https://pypi.org/project/mistapi/) 42 | default is "~/.mist_env" 43 | 44 | -l, --log_file= define the filepath/filename where to write the logs 45 | default is "./script.log" 46 | -b, --backup_folder= Path to the folder where to save the org backup (a 47 | subfolder will be created with the org name) 48 | default is "./org_backup" 49 | 50 | -d, --datetime append the current date and time (ISO format) to the 51 | backup name 52 | -t, --timestamp append the current timestamp to the backup 53 | 54 | ------- 55 | Examples: 56 | python3 ./org_complete_backup.py 57 | python3 ./org_complete_backup.py \ 58 | --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 59 | 60 | """ 61 | 62 | ##################################################################### 63 | #### IMPORTS #### 64 | import sys 65 | import logging 66 | import argparse 67 | import datetime 68 | 69 | MISTAPI_MIN_VERSION = "0.52.0" 70 | try: 71 | import mistapi 72 | from mistapi.__logger import console 73 | except ImportError: 74 | print( 75 | """ 76 | Critical: 77 | \"mistapi\" package is missing. Please use the pip command to install it. 78 | 79 | # Linux/macOS 80 | python3 -m pip install mistapi 81 | 82 | # Windows 83 | py -m pip install mistapi 84 | """ 85 | ) 86 | sys.exit(2) 87 | 88 | try: 89 | import org_conf_backup 90 | import org_inventory_backup 91 | except ImportError: 92 | print( 93 | """ 94 | Critical: 95 | This script is using other scripts from the mist_library to perform all the 96 | action. Please make sure the following python files are in the same folder 97 | as the org_clone.py file: 98 | - org_conf_backup.py 99 | - org_inventory_backup.py 100 | """ 101 | ) 102 | sys.exit(2) 103 | 104 | ##################################################################### 105 | #### PARAMETERS ##### 106 | DEFAULT_BACKUP_FOLDER = "./org_backup" 107 | LOG_FILE = "./script.log" 108 | SRC_ENV_FILE = "~/.mist_env" 109 | 110 | ##################################################################### 111 | #### LOGS #### 112 | LOGGER = logging.getLogger(__name__) 113 | 114 | 115 | ##################################################################### 116 | #### ORG FUNCTIONS #### 117 | def _backup_org( 118 | source_mist_session: mistapi.APISession, 119 | org_id: str, 120 | backup_folder_param: str, 121 | backup_name: str, 122 | ) -> None: 123 | LOGGER.debug("org_complete_backup:_backup_org") 124 | LOGGER.debug("org_complete_backup:_backup_org:parameter:org_id: %s", org_id) 125 | LOGGER.debug( 126 | "org_complete_backup:_backup_org:parameter:backup_folder_param: %s", 127 | backup_folder_param, 128 | ) 129 | LOGGER.debug( 130 | "org_complete_backup:_backup_org:parameter:backup_name: %s", backup_name 131 | ) 132 | 133 | try: 134 | _print_new_step("Backing up SOURCE Org Configuration") 135 | org_conf_backup.start( 136 | mist_session=source_mist_session, 137 | org_id=org_id, 138 | backup_folder_param=backup_folder_param, 139 | backup_name=backup_name, 140 | ) 141 | except Exception: 142 | sys.exit(255) 143 | 144 | 145 | ####### 146 | ####### 147 | 148 | 149 | def _backup_inventory( 150 | source_mist_session: mistapi.APISession, 151 | org_id: str, 152 | backup_folder_param: str, 153 | backup_name: str, 154 | ) -> None: 155 | LOGGER.debug("org_complete_backup:_backup_inventory") 156 | LOGGER.debug("org_complete_backup:_backup_inventory:parameter:org_id: %s", org_id) 157 | LOGGER.debug( 158 | "org_complete_backup:_backup_inventory:parameter:backup_folder_param: %s", 159 | backup_folder_param, 160 | ) 161 | LOGGER.debug( 162 | "org_complete_backup:_backup_inventory:parameter:backup_name: %s", backup_name 163 | ) 164 | 165 | _print_new_step("Backing up SOURCE Org Inventory") 166 | org_inventory_backup.start( 167 | mist_session=source_mist_session, 168 | org_id=org_id, 169 | backup_folder=backup_folder_param, 170 | backup_name=backup_name, 171 | ) 172 | 173 | 174 | ####### 175 | ####### 176 | 177 | 178 | def _print_new_step(message) -> None: 179 | print() 180 | print("".center(80, "*")) 181 | print(f" {message} ".center(80, "*")) 182 | print("".center(80, "*")) 183 | print() 184 | LOGGER.info(message) 185 | 186 | 187 | ####### 188 | ####### 189 | def start( 190 | apisession: mistapi.APISession, 191 | org_id: str = "", 192 | backup_folder_param: str = "", 193 | backup_name: str = "", 194 | backup_name_date: bool = False, 195 | backup_name_ts: bool = False, 196 | ) -> None: 197 | """ 198 | Start the process to clone the src org to the dst org 199 | 200 | PARAMS 201 | ------- 202 | apisession : mistapi.APISession 203 | mistapi session with `Super User` access the source Org, already logged in 204 | org_id : str 205 | Optional, org_id of the org to clone 206 | backup_folder_param : str 207 | Path to the folder where to save the org backup (a subfolder will be created 208 | with the org name). default is "./org_backup" 209 | backup_name : str 210 | Name of the subfolder where the the backup files will be saved 211 | default is the org name 212 | backup_name_date : bool, default = False 213 | if `backup_name_date`==`True`, append the current date and time (ISO 214 | format) to the backup name 215 | backup_name_ts : bool, default = False 216 | if `backup_name_ts`==`True`, append the current timestamp to the backup 217 | name 218 | """ 219 | LOGGER.debug("org_complete_backup:start") 220 | LOGGER.debug("org_complete_backup:start:parameter:org_id: %s", org_id) 221 | LOGGER.debug( 222 | "org_complete_backup:start:parameter:backup_folder_param: %s", 223 | backup_folder_param, 224 | ) 225 | LOGGER.debug("org_complete_backup:start:parameter:backup_name: %s", backup_name) 226 | LOGGER.debug( 227 | "org_complete_backup:start:parameter:backup_name_date: %s", backup_name_date 228 | ) 229 | LOGGER.debug( 230 | "org_complete_backup:start:parameter:backup_name_ts: %s", backup_name_ts 231 | ) 232 | 233 | if not backup_folder_param: 234 | backup_folder_param = DEFAULT_BACKUP_FOLDER 235 | if not org_id: 236 | org_id = mistapi.cli.select_org(apisession)[0] 237 | org_name = mistapi.api.v1.orgs.orgs.getOrg(apisession, org_id).data["name"] 238 | 239 | if not backup_name: 240 | backup_name = org_name 241 | if backup_name_date: 242 | backup_name = f"{backup_name}_{datetime.datetime.isoformat(datetime.datetime.now()).split('.')[0].replace(':', '.')}" 243 | elif backup_name_ts: 244 | backup_name = f"{backup_name}_{round(datetime.datetime.timestamp(datetime.datetime.now()))}" 245 | 246 | _backup_org(apisession, org_id, backup_folder_param, backup_name) 247 | _backup_inventory(apisession, org_id, backup_folder_param, backup_name) 248 | _print_new_step("Process finished") 249 | 250 | 251 | ############################################################################### 252 | #### USAGE #### 253 | def usage(error_message: str = "") -> None: 254 | """ 255 | display script usage 256 | """ 257 | print( 258 | """ 259 | ------------------------------------------------------------------------------- 260 | 261 | Written by Thomas Munzer (tmunzer@juniper.net) 262 | Github repository: https://github.com/tmunzer/Mist_library/ 263 | 264 | This script is licensed under the MIT License. 265 | 266 | ------------------------------------------------------------------------------- 267 | Python script to backup a whole organization configuration and devices. 268 | 269 | This script will not change/create/delete/touch any existing objects. It will 270 | just retrieve every single object from the organization. 271 | 272 | ------- 273 | Requirements: 274 | mistapi: https://pypi.org/project/mistapi/ 275 | 276 | This script requires the following scripts to be in the same folder: 277 | - org_conf_backup.py 278 | - org_inventory_backup.py 279 | 280 | ------- 281 | Usage: 282 | This script can be run as is (without parameters), or with the options below. 283 | If no options are defined, or if options are missing, the missing options will 284 | be asked by the script or the default values will be used. 285 | 286 | It is recommended to use an environment file to store the required information 287 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 288 | information about the available parameters). 289 | 290 | ------- 291 | Script Parameters: 292 | -h, --help display this help 293 | 294 | --org_id= Optional, org_id of the org to clone 295 | 296 | -e, --env= Optional, env file to use to access the src org (see 297 | mistapi env file documentation here: 298 | https://pypi.org/project/mistapi/) 299 | default is "~/.mist_env" 300 | 301 | -l, --log_file= define the filepath/filename where to write the logs 302 | default is "./script.log" 303 | -b, --backup_folder= Path to the folder where to save the org backup (a 304 | subfolder will be created with the org name) 305 | default is "./org_backup" 306 | 307 | -d, --datetime append the current date and time (ISO format) to the 308 | backup name 309 | -t, --timestamp append the current timestamp to the backup 310 | 311 | ------- 312 | Examples: 313 | python3 ./org_complete_backup.py 314 | python3 ./org_complete_backup.py \ 315 | --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 316 | 317 | """ 318 | ) 319 | if error_message: 320 | console.critical(error_message) 321 | sys.exit(0) 322 | 323 | 324 | def check_mistapi_version(): 325 | """Check if the installed mistapi version meets the minimum requirement.""" 326 | 327 | current_version = mistapi.__version__.split(".") 328 | required_version = MISTAPI_MIN_VERSION.split(".") 329 | 330 | try: 331 | for i, req in enumerate(required_version): 332 | if current_version[int(i)] > req: 333 | break 334 | if current_version[int(i)] < req: 335 | raise ImportError( 336 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 337 | f"but version {mistapi.__version__} is installed." 338 | ) 339 | except ImportError as e: 340 | LOGGER.critical(str(e)) 341 | LOGGER.critical("Please use the pip command to update it.") 342 | LOGGER.critical("") 343 | LOGGER.critical(" # Linux/macOS") 344 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 345 | LOGGER.critical("") 346 | LOGGER.critical(" # Windows") 347 | LOGGER.critical(" py -m pip install --upgrade mistapi") 348 | print( 349 | f""" 350 | Critical:\r\n 351 | {e}\r\n 352 | Please use the pip command to update it. 353 | # Linux/macOS 354 | python3 -m pip install --upgrade mistapi 355 | # Windows 356 | py -m pip install --upgrade mistapi 357 | """ 358 | ) 359 | sys.exit(2) 360 | finally: 361 | LOGGER.info( 362 | '"mistapi" package version %s is required, ' 363 | "you are currently using version %s.", 364 | MISTAPI_MIN_VERSION, 365 | mistapi.__version__, 366 | ) 367 | 368 | 369 | ############################################################################### 370 | #### SCRIPT ENTRYPOINT #### 371 | if __name__ == "__main__": 372 | parser = argparse.ArgumentParser( 373 | description="Backup a whole organization configuration and devices" 374 | ) 375 | parser.add_argument("-o", "--org_id", help="Optional, org_id of the org to backup") 376 | parser.add_argument( 377 | "-e", 378 | "--env", 379 | "--src_env", 380 | dest="env", 381 | default=SRC_ENV_FILE, 382 | help="Optional, env file to use to access the src org", 383 | ) 384 | parser.add_argument( 385 | "-l", 386 | "--log_file", 387 | default=LOG_FILE, 388 | help="define the filepath/filename where to write the logs", 389 | ) 390 | parser.add_argument( 391 | "-b", 392 | "--backup_folder", 393 | default=DEFAULT_BACKUP_FOLDER, 394 | help="Path to the folder where to save the org backup", 395 | ) 396 | 397 | time_group = parser.add_mutually_exclusive_group() 398 | time_group.add_argument( 399 | "-d", 400 | "--datetime", 401 | action="store_true", 402 | help="append the current date and time (ISO format) to the backup name", 403 | ) 404 | time_group.add_argument( 405 | "-t", 406 | "--timestamp", 407 | action="store_true", 408 | help="append the current timestamp to the backup name", 409 | ) 410 | 411 | args = parser.parse_args() 412 | 413 | ORG_ID = args.org_id 414 | BACKUP_FOLDER = args.backup_folder 415 | BACKUP_NAME = "" 416 | BACKUP_NAME_DATE = args.datetime 417 | BACKUP_NAME_TS = args.timestamp 418 | LOG_FILE = args.log_file 419 | SRC_ENV_FILE = args.env 420 | 421 | #### LOGS #### 422 | logging.basicConfig(filename=LOG_FILE, filemode="w") 423 | LOGGER.setLevel(logging.DEBUG) 424 | check_mistapi_version() 425 | ### MIST SESSION ### 426 | print(" API Session to access the Source Org ".center(80, "_")) 427 | APISESSION = mistapi.APISession(env_file=SRC_ENV_FILE) 428 | APISESSION.login() 429 | 430 | ### START ### 431 | start( 432 | APISESSION, 433 | org_id=ORG_ID, 434 | backup_folder_param=BACKUP_FOLDER, 435 | backup_name=BACKUP_NAME, 436 | backup_name_date=BACKUP_NAME_DATE, 437 | backup_name_ts=BACKUP_NAME_TS, 438 | ) 439 | -------------------------------------------------------------------------------- /scripts/reports/report_sites.py: -------------------------------------------------------------------------------- 1 | """ 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to generate a report of the Mist Sites and resolving the site 11 | group names. 12 | The script will display the report in the console and save it to a CSV file. 13 | It is possible to customize the columns to include in the report by modifying 14 | the `REPORT_HEADERS` list. 15 | 16 | ------- 17 | Requirements: 18 | mistapi: https://pypi.org/project/mistapi/ 19 | 20 | ------- 21 | Usage: 22 | This script can be run as is (without parameters), or with the options below. 23 | If no options are defined, or if options are missing, the missing options will 24 | be asked by the script or the default values will be used. 25 | 26 | It is recommended to use an environment file to store the required information 27 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 28 | information about the available parameters). 29 | 30 | ------- 31 | Options: 32 | -h, --help display this help 33 | -o, --org_id= Set the org_id 34 | -f, --out_file= define the filepath/filename where to save the data 35 | default is "./report_rogues.csv" 36 | -l, --log_file= define the filepath/filename where to write the logs 37 | default is "./script.log" 38 | -e, --env= define the env file to use (see mistapi env file documentation 39 | here: https://pypi.org/project/mistapi/) 40 | default is "~/.mist_env" 41 | 42 | ------- 43 | Examples: 44 | python3 ./report_sites.py 45 | python3 ./report_sites.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 46 | 47 | """ 48 | 49 | #### IMPORTS ##### 50 | import sys 51 | import csv 52 | import argparse 53 | import logging 54 | 55 | MISTAPI_MIN_VERSION = "0.56.4" 56 | 57 | try: 58 | import mistapi 59 | from mistapi.__logger import console as CONSOLE 60 | except ImportError: 61 | print(""" 62 | Critical: 63 | \"mistapi\" package is missing. Please use the pip command to install it. 64 | 65 | # Linux/macOS 66 | python3 -m pip install mistapi 67 | 68 | # Windows 69 | py -m pip install mistapi 70 | """) 71 | sys.exit(2) 72 | 73 | 74 | #### PARAMETERS ##### 75 | ENV_FILE = "~/.mist_env" 76 | CSV_FILE = "./report_sites.csv" 77 | LOG_FILE = "./script.log" 78 | CSV_DELIMITER = "," 79 | REPORT_HEADERS = [ 80 | "country_code", 81 | "timezone", 82 | "address", 83 | "sitegroup_names", 84 | "sitegroup_ids", 85 | "notes", 86 | "latlng", 87 | "id", 88 | "name", 89 | "org_id", 90 | "created_time", 91 | "modified_time", 92 | "rftemplate_id", 93 | "aptemplate_id", 94 | "secpolicy_id", 95 | "alarmtemplate_id", 96 | "networktemplate_id", 97 | "gatewaytemplate_id", 98 | "sitetemplate_id", 99 | "tzoffset", 100 | "engagementEnabled", 101 | "analyticEnabled", 102 | "lat", 103 | "lng", 104 | ] 105 | #### LOGS #### 106 | LOGGER = logging.getLogger(__name__) 107 | out = sys.stdout 108 | 109 | 110 | ##################################################################### 111 | # PROGRESS BAR AND DISPLAY 112 | class ProgressBar: 113 | """ 114 | PROGRESS BAR AND DISPLAY 115 | """ 116 | 117 | def __init__(self): 118 | self.steps_total = 0 119 | self.steps_count = 0 120 | 121 | def _pb_update(self, size: int = 80): 122 | if self.steps_count > self.steps_total: 123 | self.steps_count = self.steps_total 124 | 125 | percent = self.steps_count / self.steps_total 126 | delta = 17 127 | x = int((size - delta) * percent) 128 | print("Progress: ", end="") 129 | print(f"[{'█' * x}{'.' * (size - delta - x)}]", end="") 130 | print(f"{int(percent * 100)}%".rjust(5), end="") 131 | 132 | def _pb_new_step( 133 | self, 134 | message: str, 135 | result: str, 136 | inc: bool = False, 137 | size: int = 80, 138 | display_pbar: bool = True, 139 | ): 140 | if inc: 141 | self.steps_count += 1 142 | text = f"\033[A\033[F{message}" 143 | print(f"{text} ".ljust(size + 4, "."), result) 144 | print("".ljust(80)) 145 | if display_pbar: 146 | self._pb_update(size) 147 | 148 | def _pb_title( 149 | self, text: str, size: int = 80, end: bool = False, display_pbar: bool = True 150 | ): 151 | print("\033[A") 152 | print(f" {text} ".center(size, "-"), "\n") 153 | if not end and display_pbar: 154 | print("".ljust(80)) 155 | self._pb_update(size) 156 | 157 | def set_steps_total(self, steps_total: int): 158 | self.steps_total = steps_total 159 | 160 | def log_message(self, message, display_pbar: bool = True): 161 | self._pb_new_step(message, " ", display_pbar=display_pbar) 162 | 163 | def log_success(self, message, inc: bool = False, display_pbar: bool = True): 164 | LOGGER.info("%s: Success", message) 165 | self._pb_new_step( 166 | message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar 167 | ) 168 | 169 | def log_warning(self, message, inc: bool = False, display_pbar: bool = True): 170 | LOGGER.warning("%s: Warning", message) 171 | self._pb_new_step( 172 | message, "\033[93m\u2b58\033[0m\n", inc=inc, display_pbar=display_pbar 173 | ) 174 | 175 | def log_failure(self, message, inc: bool = False, display_pbar: bool = True): 176 | LOGGER.error("%s: Failure", message) 177 | self._pb_new_step( 178 | message, "\033[31m\u2716\033[0m\n", inc=inc, display_pbar=display_pbar 179 | ) 180 | 181 | def log_title(self, message, end: bool = False, display_pbar: bool = True): 182 | LOGGER.info("%s", message) 183 | self._pb_title(message, end=end, display_pbar=display_pbar) 184 | 185 | 186 | PB = ProgressBar() 187 | 188 | ############################################################################### 189 | #### FUNCTIONS #### 190 | 191 | 192 | def _get_sitegroups(apisession: mistapi.APISession, org_id: str) -> dict: 193 | sitegroups = {} 194 | message = "Retrieving site groups..." 195 | PB.log_message(message, display_pbar=False) 196 | try: 197 | response = mistapi.api.v1.orgs.sitegroups.listOrgSiteGroups( 198 | apisession, org_id, limit=1000 199 | ) 200 | if response.status_code == 200: 201 | data = mistapi.get_all(apisession, response) 202 | for site in data: 203 | sitegroups[site["id"]] = site["name"] 204 | PB.log_success(message, inc=False, display_pbar=False) 205 | else: 206 | PB.log_failure(message, inc=False, display_pbar=False) 207 | except Exception: 208 | PB.log_failure(message, inc=False, display_pbar=False) 209 | LOGGER.error("Exception occurred", exc_info=True) 210 | sys.exit(2) 211 | return sitegroups 212 | 213 | 214 | def _get_sites( 215 | apisession: mistapi.APISession, 216 | org_id: str, 217 | ) -> list: 218 | sites = [] 219 | message = "Retrieving sites..." 220 | PB.log_message(message, display_pbar=False) 221 | try: 222 | response = mistapi.api.v1.orgs.sites.listOrgSites( 223 | apisession, org_id, limit=1000 224 | ) 225 | if response.status_code == 200: 226 | sites = mistapi.get_all(apisession, response) 227 | PB.log_success(message, inc=False, display_pbar=False) 228 | else: 229 | PB.log_failure(message, inc=False, display_pbar=False) 230 | except Exception: 231 | PB.log_failure(message, inc=False, display_pbar=False) 232 | LOGGER.error("Exception occurred", exc_info=True) 233 | sys.exit(2) 234 | return sites 235 | 236 | 237 | def _process_sites( 238 | api_session: mistapi.APISession, 239 | org_id: str, 240 | sitegroups: dict, 241 | ) -> list: 242 | sites = [] 243 | results = _get_sites(api_session, org_id) 244 | for r in results: 245 | sitegroup_ids = r.get("sitegroup_ids", []) 246 | sitegroup_names = [] 247 | for sg_id in sitegroup_ids: 248 | sitegroup_names.append(sitegroups.get(sg_id, "")) 249 | r["sitegroup_names"] = sitegroup_names 250 | sites.append(r) 251 | return sites 252 | 253 | 254 | ### SAVE REPORT 255 | def _format_data(data: list) -> list: 256 | formatted = [] 257 | message = "Formatting results..." 258 | PB.log_message(message, display_pbar=False) 259 | formatted.append(REPORT_HEADERS) 260 | for site_data in data: 261 | tmp = [] 262 | for header in REPORT_HEADERS: 263 | tmp.append(site_data.get(header, "")) 264 | formatted.append(tmp) 265 | return formatted 266 | 267 | 268 | def _save_as_csv(data: list, csv_file: str): 269 | PB.log_title(f"Saving report to {csv_file}", display_pbar=False) 270 | with open(csv_file, "w", encoding="UTF8", newline="") as f: 271 | csv_writer = csv.writer(f, delimiter=CSV_DELIMITER) 272 | csv_writer.writerows(data) 273 | PB.log_success("Saving to file ", display_pbar=False) 274 | 275 | 276 | def _display_report(data: list): 277 | PB.log_title("Displaying report", display_pbar=False) 278 | print(mistapi.cli.tabulate(data[1:], headers=data[0], tablefmt="rounded_grid")) 279 | 280 | 281 | def start( 282 | api_session: mistapi.APISession, 283 | org_id: str, 284 | csv_file: str = CSV_FILE, 285 | ): 286 | """ 287 | Start the report generation process. 288 | 289 | PARAMS 290 | ------ 291 | apisession : mistapi.APISession 292 | mistapi session with `Observer` access the Org, already logged in 293 | org_id : str 294 | Mist Organization ID 295 | csv_file : str 296 | path to the CSV file to save the report 297 | """ 298 | if not org_id: 299 | org_id = mistapi.cli.select_org(api_session)[0] 300 | 301 | sitegroups_map = _get_sitegroups(api_session, org_id) 302 | sites = _process_sites(api_session, org_id, sitegroups_map) 303 | formatted_data = _format_data(sites) 304 | _save_as_csv(formatted_data, csv_file) 305 | _display_report(formatted_data) 306 | 307 | 308 | ############################################################################### 309 | ### USAGE 310 | 311 | 312 | def usage(error_message: str | None = None): 313 | """ 314 | show script usage 315 | """ 316 | print( 317 | """ 318 | ------------------------------------------------------------------------------- 319 | 320 | Written by Thomas Munzer (tmunzer@juniper.net) 321 | Github repository: https://github.com/tmunzer/Mist_library/ 322 | 323 | This script is licensed under the MIT License. 324 | 325 | ------------------------------------------------------------------------------- 326 | Python script to generate a report of the Mist Sites and resolving the site 327 | group names. 328 | The script will display the report in the console and save it to a CSV file. 329 | It is possible to customize the columns to include in the report by modifying 330 | the `REPORT_HEADERS` list. 331 | 332 | ------- 333 | Requirements: 334 | mistapi: https://pypi.org/project/mistapi/ 335 | 336 | ------- 337 | Usage: 338 | This script can be run as is (without parameters), or with the options below. 339 | If no options are defined, or if options are missing, the missing options will 340 | be asked by the script or the default values will be used. 341 | 342 | It is recommended to use an environment file to store the required information 343 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 344 | information about the available parameters). 345 | 346 | ------- 347 | Options: 348 | -h, --help display this help 349 | -o, --org_id= Set the org_id 350 | -f, --out_file= define the filepath/filename where to save the data 351 | default is "./report_rogues.csv" 352 | -l, --log_file= define the filepath/filename where to write the logs 353 | default is "./script.log" 354 | -e, --env= define the env file to use (see mistapi env file documentation 355 | here: https://pypi.org/project/mistapi/) 356 | default is "~/.mist_env" 357 | 358 | ------- 359 | Examples: 360 | python3 ./report_sites.py 361 | python3 ./report_sites.py --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 362 | """ 363 | ) 364 | if error_message: 365 | CONSOLE.critical(error_message) 366 | sys.exit(0) 367 | 368 | 369 | def check_mistapi_version(): 370 | """Check if the installed mistapi version meets the minimum requirement.""" 371 | 372 | current_version = mistapi.__version__.split(".") 373 | required_version = MISTAPI_MIN_VERSION.split(".") 374 | 375 | try: 376 | for i, req in enumerate(required_version): 377 | if current_version[int(i)] > req: 378 | break 379 | if current_version[int(i)] < req: 380 | raise ImportError( 381 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 382 | f"but version {mistapi.__version__} is installed." 383 | ) 384 | except ImportError as e: 385 | LOGGER.critical(str(e)) 386 | LOGGER.critical("Please use the pip command to update it.") 387 | LOGGER.critical("") 388 | LOGGER.critical(" # Linux/macOS") 389 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 390 | LOGGER.critical("") 391 | LOGGER.critical(" # Windows") 392 | LOGGER.critical(" py -m pip install --upgrade mistapi") 393 | print( 394 | f""" 395 | Critical:\r\n 396 | {e}\r\n 397 | Please use the pip command to update it. 398 | # Linux/macOS 399 | python3 -m pip install --upgrade mistapi 400 | # Windows 401 | py -m pip install --upgrade mistapi 402 | """ 403 | ) 404 | sys.exit(2) 405 | finally: 406 | LOGGER.info( 407 | '"mistapi" package version %s is required, ' 408 | "you are currently using version %s.", 409 | MISTAPI_MIN_VERSION, 410 | mistapi.__version__, 411 | ) 412 | 413 | 414 | ############################################################################### 415 | ### ENTRY POINT 416 | if __name__ == "__main__": 417 | parser = argparse.ArgumentParser( 418 | description="Display list of open events/alarms that are not cleared" 419 | ) 420 | parser.add_argument( 421 | "-e", "--env_file", help="define the env file to use", default=None 422 | ) 423 | parser.add_argument( 424 | "-o", 425 | "--org_id", 426 | help="ID of the Mist organization", 427 | default="", 428 | ) 429 | parser.add_argument( 430 | "-l", 431 | "--log_file", 432 | help="define the filepath/filename where to write the logs", 433 | default=LOG_FILE, 434 | ) 435 | parser.add_argument( 436 | "-c", 437 | "--csv_file", 438 | help="Path to the CSV file where to save the result", 439 | default=CSV_FILE, 440 | ) 441 | 442 | args = parser.parse_args() 443 | 444 | ENV_FILE = args.env_file 445 | ORG_ID = args.org_id 446 | LOG_FILE = args.log_file 447 | CSV_FILE = args.csv_file 448 | 449 | #### LOGS #### 450 | logging.basicConfig(filename=LOG_FILE, filemode="w") 451 | LOGGER.setLevel(logging.DEBUG) 452 | check_mistapi_version() 453 | ### MIST SESSION ### 454 | API_SESSION = mistapi.APISession(env_file=ENV_FILE) 455 | API_SESSION.login() 456 | ### START ### 457 | start(API_SESSION, ORG_ID, CSV_FILE) 458 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mist_library 2 | 3 | Utilities and example Python scripts that use the Mist Cloud APIs (via the [`mistapi`](https://pypi.org/project/mistapi/) package) to automate configuration, backups, imports and reporting for Mist organizations and sites. 4 | 5 | This repository collects small, focused scripts that are ready to run and easy to adapt for your environment. 6 | 7 | 8 | ## License 9 | 10 | This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details. 11 | 12 | ## Highlights 13 | 14 | - Collection of scripts for: configuration, EVPN, device operations, org/site backups & restores, imports, and reports. 15 | - Uses the [`mistapi`](https://pypi.org/project/mistapi/) Python package for authentication and API calls. 16 | - Scripts accept an environment file or interactive credentials and support dry-run modes where applicable. 17 | 18 | ## Quick start 19 | 20 | Prerequisites: 21 | 22 | - Python 3.8+ (recommend 3.9+) 23 | - A Mist API token or username/password with appropriate privileges 24 | - Repository checked out locally 25 | 26 | Install dependencies (recommended in a virtualenv): 27 | 28 | ```bash 29 | python -m pip install -r requirements.txt 30 | ``` 31 | 32 | Run a script example (prints help): 33 | 34 | ```bash 35 | python scripts/orgs/org_conf_deploy.py -h 36 | ``` 37 | 38 | Or run with a specific environment file: 39 | 40 | ```bash 41 | python scripts/orgs/org_conf_deploy.py -e my_env_file 42 | ``` 43 | 44 | ## Environment file 45 | 46 | You can store credentials and common options in an environment file. By default the scripts look for `~/.mist_env` (scripts commonly use `ENV_FILE = "~/.mist_env"`) but you can pass `-e /path/to/env` to use a different file. 47 | 48 | Authoritative env vars: 49 | 50 | - `MIST_HOST` — Mist API host (for example `api.mist.com`). 51 | - `MIST_APITOKEN` — API token (preferred authentication). 52 | - `MIST_USER` / `MIST_PASSWORD` — username / password fallback when no token is provided. 53 | - `MIST_VAULT_MOUNT_POINT`, `MIST_VAULT_PATH`, `MIST_VAULT_TOKEN`, `MIST_VAULT_URL` — Vault integration variables used by `mistapi` when retrieving secrets from a HashiCorp Vault instance. 54 | - `CONSOLE_LOG_LEVEL`, `LOGGING_LOG_LEVEL` — optional numeric logging levels used in several scripts (not required by `mistapi` itself, but useful to control verbosity). 55 | 56 | If no credentials are present in the env file, most scripts will prompt interactively for missing values or fall back to the `mistapi` login flow. 57 | 58 | Example `~/.mist_env` (minimal): 59 | 60 | ```ini 61 | # Required: set either MIST_APITOKEN or the user/password pair 62 | MIST_HOST=api.mist.com 63 | MIST_APITOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 64 | # Or, alternatively: 65 | # MIST_USER=your.username@example.com 66 | # MIST_PASSWORD=yourpassword 67 | 68 | # Optional logging controls used by some scripts 69 | CONSOLE_LOG_LEVEL=20 70 | LOGGING_LOG_LEVEL=10 71 | 72 | # Optional: Vault integration (only if you use HashiCorp Vault with mistapi) 73 | # MIST_VAULT_URL=https://vault.example.com 74 | # MIST_VAULT_TOKEN=xxxxx 75 | # MIST_VAULT_MOUNT_POINT=secret 76 | # MIST_VAULT_PATH=secret/data/mist 77 | ``` 78 | 79 | Notes: 80 | 81 | - The list above for [`mistapi`](https://pypi.org/project/mistapi/) env vars was verified against the installed `mistapi` package (v0.57.x). If you pin or use an older `mistapi` version, the available behavior may differ; several scripts in this repo include a `MISTAPI_MIN_VERSION` constant — check the script header for compatibility notes. 82 | 83 | ## Repository layout 84 | 85 | - `scripts/` — main scripts grouped by topic (configuration, orgs, sites, devices, exports, reports, nac, clients) 86 | - `utils/` — helper utilities (e.g. encryption) 87 | - `v-tool/` — additional tooling 88 | - `requirements.txt` — Python dependencies 89 | 90 | Each script contains a short header describing purpose, options and examples. Run any script with `-h` for usage details. 91 | 92 | ## Example workflows 93 | 94 | - Backup an organization configuration: 95 | 96 | ```bash 97 | python scripts/orgs/org_conf_backup.py -e ~/.mist_env --org-id 98 | ``` 99 | 100 | - Deploy a saved org configuration (dry-run first): 101 | 102 | ```bash 103 | python scripts/orgs/org_conf_deploy.py -e ~/.mist_env --file backup.json --dry-run 104 | ``` 105 | 106 | - Import guests from CSV into a site: 107 | 108 | ```bash 109 | python scripts/clients/import_guests.py -e ~/.mist_env --site --csv guests.csv 110 | ``` 111 | 112 | ## Finding scripts 113 | 114 | There are many scripts. Here are a few high-level categories (see `scripts/` for the full list): 115 | 116 | - Configuration: `scripts/configuration/` (webhooks, auto-assignment, AP auto-upgrade) 117 | - EVPN helpers: `scripts/configuration/evpn_topology/` 118 | - Orgs / Backups: `scripts/orgs/` (backup, deploy, clone, inventory) 119 | - Devices: `scripts/devices/` (AP, gateway, switch helpers) 120 | - Imports: `scripts/clients/`, `scripts/nac/`, `scripts/sites/` (CSV-driven imports) 121 | - Reports / Exports: `scripts/reports/`, `scripts/exports/` 122 | 123 | 124 | ## Scripts index 125 | 126 | Below is a full index of scripts included in this repository. Each entry contains a short description and a relative link to the script. 127 | 128 | ### Organization / backup / deploy 129 | 130 | - [scripts/orgs/org_complete_backup.py](scripts/orgs/org_complete_backup.py) — Backup an entire organization configuration and devices (combines conf + inventory backups). 131 | - [scripts/orgs/org_complete_backup_encrypted.py](scripts/orgs/org_complete_backup_encrypted.py) — Same as `org_complete_backup.py` but writes an AES-encrypted backup. 132 | - [scripts/orgs/org_conf_backup.py](scripts/orgs/org_conf_backup.py) — Backup organization configuration objects (sites, templates, policies, etc.). 133 | - [scripts/orgs/org_conf_backup_encrypted.py](scripts/orgs/org_conf_backup_encrypted.py) — AES-encrypted organization configuration backup. 134 | - [scripts/orgs/org_conf_deploy.py](scripts/orgs/org_conf_deploy.py) — Deploy an organization configuration backup/template to a destination org (supports dry-run). 135 | - [scripts/orgs/org_conf_deploy_only.py](scripts/orgs/org_conf_deploy_only.py) — Deploy configuration objects only (helper used during deploy flows). 136 | - [scripts/orgs/org_conf_zeroize.py](scripts/orgs/org_conf_zeroize.py) — Zeroize an organization (remove config, sites and objects). 137 | - [scripts/orgs/org_inventory_backup.py](scripts/orgs/org_inventory_backup.py) — Backup all devices from an organization (claims, config, map positions, pictures). 138 | - [scripts/orgs/org_inventory_backup_encrypted.py](scripts/orgs/org_inventory_backup_encrypted.py) — AES-encrypted inventory backup. 139 | - [scripts/orgs/org_inventory_deploy.py](scripts/orgs/org_inventory_deploy.py) — Deploy inventory backup files to an organization (supports dry-run). 140 | - [scripts/orgs/org_inventory_restore_pictures.py](scripts/orgs/org_inventory_restore_pictures.py) — Restore device images from an inventory backup. 141 | - [scripts/orgs/org_clone.py](scripts/orgs/org_clone.py) — Clone a full organization to another org (configuration + optional creations). 142 | - [scripts/orgs/org_migration.py](scripts/orgs/org_migration.py) — Migrate an organization and optionally its devices to another org (supports unclaim options). 143 | - [scripts/orgs/inventory_claim.py](scripts/orgs/inventory_claim.py) — Claim devices to an org from a CSV list of claim codes. 144 | - [scripts/orgs/inventory_assign.py](scripts/orgs/inventory_assign.py) — Assign claimed devices to sites from a CSV. 145 | - [scripts/orgs/clone_template.py](scripts/orgs/clone_template.py) — Clone a single template (WLAN/LAN/WAN/HUB) between orgs. 146 | - [scripts/orgs/validate_site_variables.py](scripts/orgs/validate_site_variables.py) — Validate that variables used by templates are configured at the site level. 147 | 148 | #### Org admins 149 | 150 | - [scripts/orgs/admins/import_admins.py](scripts/orgs/admins/import_admins.py) — Invite/add administrators from a CSV. 151 | - [scripts/orgs/admins/delete_admins.py](scripts/orgs/admins/delete_admins.py) — Python script to revoke (remove) administrator access from a Mist organization using a CSV file containing administrator information. This script provides a safe and efficient way to bulk remove multiple administrators from an organization. 152 | - [scripts/orgs/admins/delete_expired_invites.py](scripts/orgs/admins/delete_expired_invites.py) — Python script to automatically identify and remove expired administrator invitations from a Mist organization. This script helps maintain clean administrator lists by removing stale invitations that were never accepted. 153 | 154 | 155 | ### Configuration 156 | 157 | - [scripts/configuration/config_webhook.py](scripts/configuration/config_webhook.py) — List/add/delete org/site webhooks. 158 | - [scripts/configuration/config_webhook_settings.json](scripts/configuration/config_webhook_settings.json) — Example/default webhook settings used by the webhook scripts. 159 | - [scripts/configuration/config_ap_auto_upgrade.py](scripts/configuration/config_ap_auto_upgrade.py) — Update AP auto-upgrade parameters in site settings. 160 | - [scripts/configuration/config_auto_site_assignment.py](scripts/configuration/config_auto_site_assignment.py) — Update org auto-assignment rules (IP subnet based site assignment). 161 | 162 | #### EVPN helpers 163 | 164 | - [scripts/configuration/evpn_topology/provision_evpntoplogy_vlans.py](scripts/configuration/evpn_topology/provision_evpntoplogy_vlans.py) — Generate VLANs and VRFs for EVPN topologies. 165 | - [scripts/configuration/evpn_topology/update_evpn_switch_ip.py](scripts/configuration/evpn_topology/update_evpn_switch_ip.py) — Update switch IP addresses inside an EVPN topology. 166 | 167 | 168 | ### Devices 169 | 170 | - [scripts/devices/rename_devices.py](scripts/devices/rename_devices.py) — Rename devices (AP, Switch, Router) from a CSV; finds site automatically. 171 | - [scripts/devices/update_managed_status.py](scripts/devices/update_managed_status.py) — Enable/disable managed mode on devices from a CSV; finds site automatically. 172 | 173 | #### AP helpers 174 | 175 | - [scripts/devices/aps/configure_ap_mgmt_vlan.py](scripts/devices/aps/configure_ap_mgmt_vlan.py) — Reconfigure management VLAN on APs across sites. 176 | - [scripts/devices/aps/report_power_constrained_aps.py](scripts/devices/aps/report_power_constrained_aps.py) — Report APs with power constraints; output CSV. 177 | - [scripts/devices/aps/report_bssids.py](scripts/devices/aps/report_bssids.py) — List APs and their BSSIDs for orgs/sites. 178 | 179 | #### Gateway helpers 180 | 181 | - [scripts/devices/gateways/report_gateway_firmware.py](scripts/devices/gateways/report_gateway_firmware.py) — Report SRX firmware versions and snapshot/backup status. 182 | - [scripts/devices/gateways/cluster_node_check.py](scripts/devices/gateways/cluster_node_check.py) — Report cluster node roles across an org. 183 | - [scripts/devices/gateways/fix_gateway_backup_firmware.py](scripts/devices/gateways/fix_gateway_backup_firmware.py) — Trigger snapshot/firmware backup on SRX devices. 184 | - [scripts/devices/gateways/bgp_peers_peak_values.py](scripts/devices/gateways/bgp_peers_peak_values.py) — Retrieve VPN peer statistics for gateways. 185 | 186 | #### Switch helpers 187 | 188 | - [scripts/devices/switches/check_local_commit_events.py](scripts/devices/switches/check_local_commit_events.py) — Retrieve CLI commit events from switches in an org. 189 | - [scripts/devices/switches/fix_switch_backup_firmware.py](scripts/devices/switches/fix_switch_backup_firmware.py) — Python script to automate firmware snapshot/backup operations on Juniper EX. 190 | - [scripts/devices/switches/update_port_config.py](scripts/devices/switches/update_port_config.py) — Reconfigure switch interfaces from a CSV by creating/updating device overrides. 191 | - [scripts/devices/switches/toggle_poe.py](scripts/devices/switches/toggle_poe.py) — Enable/disable/toggle PoE for a port profile in a switch template. 192 | - [scripts/devices/switches/report_switch_firmware.py](scripts/devices/switches/report_switch_firmware.py) — Generate report of switch snapshot/backup status. 193 | 194 | 195 | ### Clients / NAC / Imports 196 | 197 | - [scripts/clients/import_guests.py](scripts/clients/import_guests.py) — Import or update Guests from a CSV into an org or site. 198 | - [scripts/nac/import_client_macs.py](scripts/nac/import_client_macs.py) — Import client MAC addresses into NAC labels from CSV. 199 | - [scripts/nac/import_user_macs.py](scripts/nac/import_user_macs.py) — Import MAC addresses as NAC endpoints from CSV. 200 | 201 | 202 | ### Sites 203 | 204 | - [scripts/sites/import_sites.py](scripts/sites/import_sites.py) — Create sites in an org from a CSV. 205 | - [scripts/sites/import_floorplans.py](scripts/sites/import_floorplans.py) — Import Ekahau / iBwave floorplans into Mist. 206 | - [scripts/sites/import_psk.py](scripts/sites/import_psk.py) — Import PSKs for sites from a CSV. 207 | - [scripts/sites/site_conf_backup.py](scripts/sites/site_conf_backup.py) — Backup a single site's configuration. 208 | - [scripts/sites/site_conf_psk.py](scripts/sites/site_conf_psk.py) — Site PSK configuration helper. 209 | - [scripts/sites/site_conf_wlan.py](scripts/sites/site_conf_wlan.py) — Manage SSIDs (list/add/delete) at org/site level. 210 | - [scripts/sites/site_conf_wlan_settings.json](scripts/sites/site_conf_wlan_settings.json) — Example WLAN settings used by scripts. 211 | - [scripts/sites/update_sitegroups.py](scripts/sites/update_sitegroups.py) — Update the sitegroups assigned to sites from a CSV (append or replace). 212 | - [scripts/sites/update_sites_templates.py](scripts/sites/update_sites_templates.py) — Update templates assigned to sites and auto-assignment rules from a CSV. 213 | - [scripts/sites/fix_sites_geocoding.py](scripts/sites/fix_sites_geocoding.py) — Ensure sites have geo info (lat/lng, country_code, timezone) and fix missing data. 214 | - [scripts/sites/not_migrated_to_mistapi/site_conf_restore.py](scripts/sites/not_migrated_to_mistapi/site_conf_restore.py) — Legacy/site-specific restore helper (not migrated to mistapi). 215 | 216 | 217 | ### Exports & Reports 218 | 219 | - [scripts/exports/export_inventory.py](scripts/exports/export_inventory.py) — Export org inventory to CSV/JSON (includes claim codes). 220 | - [scripts/exports/export_search.py](scripts/exports/export_search.py) — Export historical data from Mist API to CSV/JSON. 221 | - [scripts/exports/export_org_events.py](scripts/exports/export_org_events.py) — Export organization events. 222 | - [scripts/exports/compare_org_events_summary.py](scripts/exports/compare_org_events_summary.py) — Helper to compare/aggregate exported org event summaries. 223 | 224 | #### Reports 225 | 226 | - [scripts/reports/report_admins_last_login.py](scripts/reports/report_admins_last_login.py) — Python script to generate a CSV report of organization admins and their last login activity 227 | - [scripts/reports/report_app_usage.py](scripts/reports/report_app_usage.py) — Application usage report for a site (hours/duration based). 228 | - [scripts/reports/list_open_events.py](scripts/reports/list_open_events.py) — Display events/alarms that are still open (tries to correlate open/close events). 229 | - [scripts/reports/report_sites.py](scripts/reports/report_sites.py) — Generate a report of sites (resolves site group names); CSV output. 230 | - [scripts/reports/report_sites_sles.py](scripts/reports/report_sites_sles.py) — Generate Site SLE (Service Level Expectations) reports. 231 | - [scripts/reports/report_wlans.py](scripts/reports/report_wlans.py) — List all WLANs and parameters for orgs/sites; CSV output. 232 | - [scripts/reports/report_inventory_site_notes.py](scripts/reports/report_inventory_site_notes.py) — Inventory report augmented with site notes and metadata. 233 | - [scripts/reports/report_rogues.py](scripts/reports/report_rogues.py) — Generate Rogue AP reports. 234 | - [scripts/reports/report_wan_services_usage.py](scripts/reports/report_wan_services_usage.py) — Report WAN Services usage across Gateway Templates, Hub Profiles, Service Policies and Gateways. 235 | - [scripts/reports/list_webhook_deliveries.py](scripts/reports/list_webhook_deliveries.py) — Extract and filter webhook deliveries; CSV export. 236 | - [scripts/reports/report_wlans.py](scripts/reports/report_wlans.py) — WLANs report (duplicate listing for quick access). 237 | 238 | 239 | ### Utilities 240 | 241 | - [scripts/utils/encryption.py](scripts/utils/encryption.py) — Utility functions for AES encryption/decryption used by encrypted backups. 242 | 243 | 244 | 245 | ## Contributing 246 | 247 | Contributions are welcome. Good ways to help: 248 | 249 | - Open issues for bugs or feature requests 250 | - Send small, focused pull requests that include a short description and example usage 251 | - Add or update script documentation at the top of the script file 252 | 253 | When editing scripts, keep backwards compatibility where possible and include tests or a smoke-check if you add complex logic. -------------------------------------------------------------------------------- /scripts/nac/import_user_macs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script import import a list of MAC Address as "NAC Endpoints" from a 11 | CSV File. 12 | 13 | ------- 14 | Requirements: 15 | mistapi: https://pypi.org/project/mistapi/ 16 | 17 | ------- 18 | Usage: 19 | This script can be run as is (without parameters), or with the options below. 20 | If no options are defined, or if options are missing, the missing options will 21 | be asked by the script or the default values will be used. 22 | 23 | It is recommended to use an environment file to store the required information 24 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 25 | information about the available parameters). 26 | 27 | ------- 28 | CSV Example: 29 | Example 1: 30 | #mac,labels,vlan,notes,name,radius_group 31 | 921b638445cd,"bldg1,floor1",vlan-100 32 | 721b638445ef,"bldg2,floor2",vlan-101,Canon Printers 33 | 721b638445ee,"bldg3,floor3",vlan-102,Printer2,VIP 34 | 921b638445ce,"bldg4,floor4",vlan-103 35 | 921b638445cf,"bldg5,floor5",vlan-104 36 | 37 | 38 | ------ 39 | CSV Parameters 40 | Required: 41 | - mac MAC Address of the Wi-Fi client 42 | 43 | Optional: 44 | - labels Name of the Label. If not provided, a default label value can be 45 | passed in the script parameter with the -d option or will be asked 46 | by the script 47 | - vlan Name of the VLAN to assign to the endpoint 48 | - name Name to assign to the endpoint 49 | - radius_group 50 | 51 | ------- 52 | Script Parameters: 53 | -h, --help display this help 54 | -f, --file= OPTIONAL: path to the CSV file 55 | 56 | -o, --org_id= Set the org_id where the webhook must be create/delete/retrieved 57 | This parameter cannot be used if -s/--site_id is used. 58 | If no org_id and not site_id are defined, the script will show 59 | a menu to select the org/the site. 60 | 61 | -f, --file path the to csv file to load 62 | default is ./import_user_macs.csv 63 | 64 | -l, --log_file= define the filepath/filename where to write the logs 65 | default is "./script.log" 66 | -e, --env= define the env file to use (see mistapi env file documentation 67 | here: https://pypi.org/project/mistapi/) 68 | default is "~/.mist_env" 69 | 70 | ------- 71 | Examples: 72 | python3 ./import_user_macs.py 73 | python3 ./import_user_macs.py \ 74 | -f ./import_user_macs.csv \ 75 | --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 76 | 77 | ''' 78 | #### IMPORTS #### 79 | import sys 80 | import csv 81 | import getopt 82 | import logging 83 | 84 | MISTAPI_MIN_VERSION = "0.52.4" 85 | 86 | try: 87 | import mistapi 88 | from mistapi.__logger import console as CONSOLE 89 | except: 90 | print(""" 91 | Critical: 92 | \"mistapi\" package is missing. Please use the pip command to install it. 93 | 94 | # Linux/macOS 95 | python3 -m pip install mistapi 96 | 97 | # Windows 98 | py -m pip install mistapi 99 | """) 100 | sys.exit(2) 101 | 102 | #### PARAMETERS ##### 103 | ENV_FILE="~/.mist_env" 104 | CSV_FILE="./import_user_macs.csv" 105 | LOG_FILE = "./script.log" 106 | 107 | ##################################################################### 108 | #### LOGS #### 109 | LOGGER = logging.getLogger(__name__) 110 | 111 | ##################################################################### 112 | # PROGRESS BAR AND DISPLAY 113 | class ProgressBar: 114 | """ 115 | PROGRESS BAR AND DISPLAY 116 | """ 117 | def __init__(self): 118 | self.steps_total = 0 119 | self.steps_count = 0 120 | 121 | def _pb_update(self, size: int = 80): 122 | if self.steps_count > self.steps_total: 123 | self.steps_count = self.steps_total 124 | 125 | percent = self.steps_count / self.steps_total 126 | delta = 17 127 | x = int((size - delta) * percent) 128 | print(f"Progress: ", end="") 129 | print(f"[{'█'*x}{'.'*(size-delta-x)}]", end="") 130 | print(f"{int(percent*100)}%".rjust(5), end="") 131 | 132 | def _pb_new_step( 133 | self, 134 | message: str, 135 | result: str, 136 | inc: bool = False, 137 | size: int = 80, 138 | display_pbar: bool = True, 139 | ): 140 | if inc: 141 | self.steps_count += 1 142 | text = f"\033[A\033[F{message}" 143 | print(f"{text} ".ljust(size + 4, "."), result) 144 | print("".ljust(80)) 145 | if display_pbar: 146 | self._pb_update(size) 147 | 148 | def _pb_title( 149 | self, text: str, size: int = 80, end: bool = False, display_pbar: bool = True 150 | ): 151 | print("\033[A") 152 | print(f" {text} ".center(size, "-"), "\n") 153 | if not end and display_pbar: 154 | print("".ljust(80)) 155 | self._pb_update(size) 156 | 157 | def set_steps_total(self, steps_total: int): 158 | self.steps_total = steps_total 159 | 160 | def log_message(self, message, display_pbar: bool = True): 161 | self._pb_new_step(message, " ", display_pbar=display_pbar) 162 | 163 | def log_success(self, message, inc: bool = False, display_pbar: bool = True): 164 | LOGGER.info(f"{message}: Success") 165 | self._pb_new_step( 166 | message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar 167 | ) 168 | 169 | def log_warning(self, message, inc: bool = False, display_pbar: bool = True): 170 | LOGGER.warning(f"{message}: Warning") 171 | self._pb_new_step( 172 | message, "\033[93m\u2B58\033[0m\n", inc=inc, display_pbar=display_pbar) 173 | 174 | def log_failure(self, message, inc: bool = False, display_pbar: bool = True): 175 | LOGGER.error(f"{message}: Failure") 176 | self._pb_new_step( 177 | message, "\033[31m\u2716\033[0m\n", inc=inc, display_pbar=display_pbar 178 | ) 179 | 180 | def log_title(self, message, end: bool = False, display_pbar: bool = True): 181 | LOGGER.info(f"{message}") 182 | self._pb_title(message, end=end, display_pbar=display_pbar) 183 | 184 | PB = ProgressBar() 185 | 186 | ##################################################################### 187 | # FUNCTIONS 188 | 189 | 190 | def import_usermacs( 191 | apisession:mistapi.APISession, 192 | org_id:str, 193 | usermacs:dict 194 | ): 195 | try: 196 | message = "Importing user macs" 197 | PB.log_message(message, display_pbar=False) 198 | resp = mistapi.api.v1.orgs.usermacs.importOrgUserMacs(apisession, org_id, usermacs) 199 | if resp.status_code == 200: 200 | PB.log_success(message, display_pbar=False) 201 | return 202 | else: 203 | PB.log_failure(message, display_pbar=False) 204 | except: 205 | PB.log_failure(message, display_pbar=False) 206 | LOGGER.error("Exception occurred", exc_info=True) 207 | 208 | 209 | def _read_csv(csv_file:str): 210 | message = "Processing CSV File" 211 | try: 212 | PB.log_message(message, display_pbar=False) 213 | LOGGER.debug(f"_read_csv:opening CSV file {csv_file}") 214 | with open(csv_file, "r") as f: 215 | data = csv.reader(f, skipinitialspace=True, quotechar='"') 216 | data = [[c.replace("\ufeff", "") for c in row] for row in data] 217 | fields = [] 218 | entries = [] 219 | for line in data: 220 | LOGGER.debug(f"_read_csv:new csv line:{line}") 221 | if not fields: 222 | for column in line: 223 | fields.append(column.replace("#", "").strip()) 224 | LOGGER.debug(f"_read_csv:detected CSV fields: {fields}") 225 | if "mac" not in fields: 226 | LOGGER.critical(f"_read_csv:mac address not in CSV file... Exiting...") 227 | PB.log_failure(message, display_pbar=False) 228 | CONSOLE.critical( 229 | "CSV format invalid (MAC Address not found). " 230 | "Please double check it... Exiting..." 231 | ) 232 | sys.exit(255) 233 | else: 234 | entry = {} 235 | i = 0 236 | for column in line: 237 | field = fields[i] 238 | if field == "labels": 239 | entry[field] = [] 240 | for label in column.split(","): 241 | entry[field].append(label.strip()) 242 | else: 243 | entry[field] = column.strip() 244 | i += 1 245 | entries.append(entry) 246 | LOGGER.debug(f"_read_csv:new entry processed: {entry['mac']} with label {entry.get('labels')}") 247 | PB.log_success(message, display_pbar=False) 248 | return entries 249 | except Exception as e: 250 | PB.log_failure(message, display_pbar=False) 251 | LOGGER.error("Exception occurred", exc_info=True) 252 | 253 | def start(apisession:mistapi.APISession, org_id:str=None, csv_file:str=CSV_FILE): 254 | """ 255 | Start the process 256 | 257 | PARAMS 258 | ------- 259 | apisession : mistapi.APISession 260 | mistapi session with `Super User` access the Org, already logged in 261 | org_id : str 262 | org_id where the webhook guests be added. This parameter cannot be used if "site_id" 263 | is used. If no org_id and not site_id are defined, the script will show a menu to 264 | select the org/the site. 265 | csv_file : str 266 | Path to the CSV file where the guests information are stored. 267 | default is "./import_guests.csv" 268 | """ 269 | if not org_id: 270 | org_id = mistapi.cli.select_org(apisession)[0] 271 | 272 | PB.log_title("Reading data", display_pbar=False) 273 | print() 274 | entries = _read_csv(csv_file) 275 | 276 | import_usermacs(apisession, org_id, entries) 277 | 278 | 279 | ############################################################################### 280 | # USAGE 281 | def usage(error_message:str=None): 282 | """ 283 | show script usage 284 | """ 285 | print(""" 286 | ------------------------------------------------------------------------------- 287 | 288 | Written by Thomas Munzer (tmunzer@juniper.net) 289 | Github repository: https://github.com/tmunzer/Mist_library/ 290 | 291 | This script is licensed under the MIT License. 292 | 293 | ------------------------------------------------------------------------------- 294 | Python script import import a list of MAC Address as "NAC Endpoints" from a 295 | CSV File. 296 | 297 | ------- 298 | Requirements: 299 | mistapi: https://pypi.org/project/mistapi/ 300 | 301 | ------- 302 | Usage: 303 | This script can be run as is (without parameters), or with the options below. 304 | If no options are defined, or if options are missing, the missing options will 305 | be asked by the script or the default values will be used. 306 | 307 | It is recommended to use an environment file to store the required information 308 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 309 | information about the available parameters). 310 | 311 | ------- 312 | CSV Example: 313 | Example 1: 314 | mac,labels,vlan,notes,name,radius_group 315 | 921b638445cd,"bldg1,floor1",vlan-100 316 | 721b638445ef,"bldg2,floor2",vlan-101,Canon Printers 317 | 721b638445ee,"bldg3,floor3",vlan-102,Printer2,VIP 318 | 921b638445ce,"bldg4,floor4",vlan-103 319 | 921b638445cf,"bldg5,floor5",vlan-104 320 | 321 | 322 | ------ 323 | CSV Parameters 324 | Required: 325 | - mac MAC Address of the Wi-Fi client 326 | 327 | Optional: 328 | - labels Name of the Label. If not provided, a default label value can be 329 | passed in the script parameter with the -d option or will be asked 330 | by the script 331 | - vlan Name of the VLAN to assign to the endpoint 332 | - name Name to assign to the endpoint 333 | - radius_group 334 | 335 | ------- 336 | Script Parameters: 337 | -h, --help display this help 338 | -f, --file= OPTIONAL: path to the CSV file 339 | 340 | -o, --org_id= Set the org_id where the webhook must be create/delete/retrieved 341 | This parameter cannot be used if -s/--site_id is used. 342 | If no org_id and not site_id are defined, the script will show 343 | a menu to select the org/the site. 344 | 345 | -f, --file path the to csv file to load 346 | default is ./import_user_macs.csv 347 | 348 | -l, --log_file= define the filepath/filename where to write the logs 349 | default is "./script.log" 350 | -e, --env= define the env file to use (see mistapi env file documentation 351 | here: https://pypi.org/project/mistapi/) 352 | default is "~/.mist_env" 353 | 354 | ------- 355 | Examples: 356 | python3 ./import_user_macs.py 357 | python3 ./import_user_macs.py \ 358 | -f ./import_user_macs.csv \ 359 | --org_id=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 360 | 361 | """) 362 | if error_message: 363 | CONSOLE.critical(error_message) 364 | sys.exit(0) 365 | 366 | def check_mistapi_version(): 367 | """Check if the installed mistapi version meets the minimum requirement.""" 368 | 369 | current_version = mistapi.__version__.split(".") 370 | required_version = MISTAPI_MIN_VERSION.split(".") 371 | 372 | try: 373 | for i, req in enumerate(required_version): 374 | if current_version[int(i)] > req: 375 | break 376 | if current_version[int(i)] < req: 377 | raise ImportError( 378 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 379 | f"but version {mistapi.__version__} is installed." 380 | ) 381 | except ImportError as e: 382 | LOGGER.critical(str(e)) 383 | LOGGER.critical("Please use the pip command to update it.") 384 | LOGGER.critical("") 385 | LOGGER.critical(" # Linux/macOS") 386 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 387 | LOGGER.critical("") 388 | LOGGER.critical(" # Windows") 389 | LOGGER.critical(" py -m pip install --upgrade mistapi") 390 | print( 391 | f""" 392 | Critical:\r\n 393 | {e}\r\n 394 | Please use the pip command to update it. 395 | # Linux/macOS 396 | python3 -m pip install --upgrade mistapi 397 | # Windows 398 | py -m pip install --upgrade mistapi 399 | """ 400 | ) 401 | sys.exit(2) 402 | finally: 403 | LOGGER.info( 404 | '"mistapi" package version %s is required, ' 405 | "you are currently using version %s.", 406 | MISTAPI_MIN_VERSION, 407 | mistapi.__version__ 408 | ) 409 | 410 | ##################################################################### 411 | ##### ENTRY POINT #### 412 | 413 | if __name__ == "__main__": 414 | try: 415 | opts, args = getopt.getopt(sys.argv[1:], "ho:f:e:l:", [ 416 | "help", "org_id=", "file=", "env=", "log_file="]) 417 | except getopt.GetoptError as err: 418 | CONSOLE.error(err) 419 | usage() 420 | 421 | ORG_ID = None 422 | for o, a in opts: 423 | if o in ["-h", "--help"]: 424 | usage() 425 | elif o in ["-o", "--org_id"]: 426 | ORG_ID = a 427 | elif o in ["-f", "--file"]: 428 | CSV_FILE = a 429 | elif o in ["-e", "--env"]: 430 | ENV_FILE = a 431 | elif o in ["-l", "--log_file"]: 432 | LOG_FILE = a 433 | else: 434 | assert False, "unhandled option" 435 | #### LOGS #### 436 | logging.basicConfig(filename=LOG_FILE, filemode='w') 437 | LOGGER.setLevel(logging.DEBUG) 438 | check_mistapi_version() 439 | ### START ### 440 | apisession = mistapi.APISession(env_file=ENV_FILE) 441 | apisession.login() 442 | start(apisession, ORG_ID, CSV_FILE) 443 | 444 | -------------------------------------------------------------------------------- /scripts/reports/report_inventory_site_notes.py: -------------------------------------------------------------------------------- 1 | """ 2 | ------------------------------------------------------------------------------- 3 | 4 | Written by Thomas Munzer (tmunzer@juniper.net) 5 | Github repository: https://github.com/tmunzer/Mist_library/ 6 | 7 | This script is licensed under the MIT License. 8 | 9 | ------------------------------------------------------------------------------- 10 | Python script to generate a report of the Mist Inventory and add information 11 | related to the Site the devices are assigned to. 12 | The script will display the report in the console and save it to a CSV file. 13 | It is possible to customize the columns to include in the report by modifying 14 | the `REPORT_HEADERS` list. 15 | 16 | ------- 17 | Requirements: 18 | mistapi: https://pypi.org/project/mistapi/ 19 | 20 | ------- 21 | Usage: 22 | This script can be run as is (without parameters), or with the options below. 23 | If no options are defined, or if options are missing, the missing options will 24 | be asked by the script or the default values will be used. 25 | 26 | It is recommended to use an environment file to store the required information 27 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 28 | information about the available parameters). 29 | 30 | ------- 31 | Options: 32 | -h, --help display this help 33 | -s, --org_id= Set the org_id 34 | -f, --out_file= define the filepath/filename where to save the data 35 | default is "./report_rogues.csv" 36 | -l, --log_file= define the filepath/filename where to write the logs 37 | default is "./script.log" 38 | -e, --env= define the env file to use (see mistapi env file documentation 39 | here: https://pypi.org/project/mistapi/) 40 | default is "~/.mist_env" 41 | 42 | ------- 43 | Examples: 44 | python3 ./report_inventory_site_notes.py 45 | python3 ./report_inventory_site_notes.py --site_ids=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 46 | 47 | """ 48 | 49 | #### IMPORTS ##### 50 | import sys 51 | import csv 52 | import argparse 53 | import logging 54 | 55 | MISTAPI_MIN_VERSION = "0.56.4" 56 | 57 | try: 58 | import mistapi 59 | from mistapi.__logger import console as CONSOLE 60 | except ImportError: 61 | print(""" 62 | Critical: 63 | \"mistapi\" package is missing. Please use the pip command to install it. 64 | 65 | # Linux/macOS 66 | python3 -m pip install mistapi 67 | 68 | # Windows 69 | py -m pip install mistapi 70 | """) 71 | sys.exit(2) 72 | 73 | 74 | #### PARAMETERS ##### 75 | ENV_FILE = "~/.mist_env" 76 | CSV_FILE = "./report_inventory_site_notes.csv" 77 | LOG_FILE = "./script.log" 78 | CSV_DELIMITER = "," 79 | REPORT_HEADERS = [ 80 | # "org_id", 81 | "site_name", 82 | "site_notes", 83 | "site_id", 84 | "type", 85 | "hostname", 86 | # "name", 87 | "mac", 88 | "serial", 89 | "model", 90 | # "magic", 91 | # "id", 92 | # "adopted", 93 | "version", 94 | "deviceprofile_id", 95 | "connected", 96 | # "site_country_code", 97 | # "site_timezone", 98 | # "site_address", 99 | # "site_latlng", 100 | # "site_rftemplate_id", 101 | # "site_aptemplate_id", 102 | # "site_secpolicy_id", 103 | # "site_alarmtemplate_id", 104 | # "site_networktemplate_id", 105 | # "site_gatewaytemplate_id", 106 | # "site_sitetemplate_id", 107 | ] 108 | #### LOGS #### 109 | LOGGER = logging.getLogger(__name__) 110 | out = sys.stdout 111 | 112 | 113 | ##################################################################### 114 | # PROGRESS BAR AND DISPLAY 115 | class ProgressBar: 116 | """ 117 | PROGRESS BAR AND DISPLAY 118 | """ 119 | 120 | def __init__(self): 121 | self.steps_total = 0 122 | self.steps_count = 0 123 | 124 | def _pb_update(self, size: int = 80): 125 | if self.steps_count > self.steps_total: 126 | self.steps_count = self.steps_total 127 | 128 | percent = self.steps_count / self.steps_total 129 | delta = 17 130 | x = int((size - delta) * percent) 131 | print("Progress: ", end="") 132 | print(f"[{'█' * x}{'.' * (size - delta - x)}]", end="") 133 | print(f"{int(percent * 100)}%".rjust(5), end="") 134 | 135 | def _pb_new_step( 136 | self, 137 | message: str, 138 | result: str, 139 | inc: bool = False, 140 | size: int = 80, 141 | display_pbar: bool = True, 142 | ): 143 | if inc: 144 | self.steps_count += 1 145 | text = f"\033[A\033[F{message}" 146 | print(f"{text} ".ljust(size + 4, "."), result) 147 | print("".ljust(80)) 148 | if display_pbar: 149 | self._pb_update(size) 150 | 151 | def _pb_title( 152 | self, text: str, size: int = 80, end: bool = False, display_pbar: bool = True 153 | ): 154 | print("\033[A") 155 | print(f" {text} ".center(size, "-"), "\n") 156 | if not end and display_pbar: 157 | print("".ljust(80)) 158 | self._pb_update(size) 159 | 160 | def set_steps_total(self, steps_total: int): 161 | self.steps_total = steps_total 162 | 163 | def log_message(self, message, display_pbar: bool = True): 164 | self._pb_new_step(message, " ", display_pbar=display_pbar) 165 | 166 | def log_success(self, message, inc: bool = False, display_pbar: bool = True): 167 | LOGGER.info("%s: Success", message) 168 | self._pb_new_step( 169 | message, "\033[92m\u2714\033[0m\n", inc=inc, display_pbar=display_pbar 170 | ) 171 | 172 | def log_warning(self, message, inc: bool = False, display_pbar: bool = True): 173 | LOGGER.warning("%s: Warning", message) 174 | self._pb_new_step( 175 | message, "\033[93m\u2b58\033[0m\n", inc=inc, display_pbar=display_pbar 176 | ) 177 | 178 | def log_failure(self, message, inc: bool = False, display_pbar: bool = True): 179 | LOGGER.error("%s: Failure", message) 180 | self._pb_new_step( 181 | message, "\033[31m\u2716\033[0m\n", inc=inc, display_pbar=display_pbar 182 | ) 183 | 184 | def log_title(self, message, end: bool = False, display_pbar: bool = True): 185 | LOGGER.info("%s", message) 186 | self._pb_title(message, end=end, display_pbar=display_pbar) 187 | 188 | 189 | PB = ProgressBar() 190 | 191 | ############################################################################### 192 | #### FUNCTIONS #### 193 | 194 | 195 | def _get_inventory(apisession: mistapi.APISession, org_id: str) -> list: 196 | inventory = [] 197 | message = "Retrieving inventory..." 198 | PB.log_message(message, display_pbar=False) 199 | try: 200 | response = mistapi.api.v1.orgs.inventory.getOrgInventory( 201 | apisession, org_id, limit=1000 202 | ) 203 | if response.status_code == 200: 204 | inventory = mistapi.get_all(apisession, response) 205 | PB.log_success(message, inc=False, display_pbar=False) 206 | else: 207 | PB.log_failure(message, inc=False, display_pbar=False) 208 | except Exception: 209 | PB.log_failure(message, inc=False, display_pbar=False) 210 | LOGGER.error("Exception occurred", exc_info=True) 211 | sys.exit(2) 212 | return inventory 213 | 214 | 215 | def _get_sites( 216 | apisession: mistapi.APISession, 217 | org_id: str, 218 | ) -> list: 219 | sites = [] 220 | message = "Retrieving sites..." 221 | PB.log_message(message, display_pbar=False) 222 | try: 223 | response = mistapi.api.v1.orgs.sites.listOrgSites( 224 | apisession, org_id, limit=1000 225 | ) 226 | if response.status_code == 200: 227 | sites = mistapi.get_all(apisession, response) 228 | PB.log_success(message, inc=False, display_pbar=False) 229 | else: 230 | PB.log_failure(message, inc=False, display_pbar=False) 231 | except Exception: 232 | PB.log_failure(message, inc=False, display_pbar=False) 233 | LOGGER.error("Exception occurred", exc_info=True) 234 | sys.exit(2) 235 | return sites 236 | 237 | 238 | def _process_inventory( 239 | inventory: list, 240 | sites: list, 241 | ) -> list: 242 | results = [] 243 | for device in inventory: 244 | if ( 245 | device.get("site_id") 246 | and device["site_id"] != "00000000-0000-0000-0000-000000000000" 247 | ): 248 | site = next((s for s in sites if s["id"] == device["site_id"]), {}) 249 | device["site_notes"] = site.get("notes", "") 250 | device["site_name"] = site.get("name", "") 251 | device["site_country_code"] = site.get("country_code", "") 252 | device["site_timezone"] = site.get("timezone", "") 253 | device["site_latlng"] = site.get("latlng", "") 254 | device["site_rftemplate_id"] = site.get("rftemplate_id", "") 255 | device["site_aptemplate_id"] = site.get("aptemplate_id", "") 256 | device["site_secpolicy_id"] = site.get("secpolicy_id", "") 257 | device["site_alarmtemplate_id"] = site.get("alarmtemplate_id", "") 258 | device["site_networktemplate_id"] = site.get("networktemplate_id", "") 259 | device["site_gatewaytemplate_id"] = site.get("gatewaytemplate_id", "") 260 | device["site_sitetemplate_id"] = site.get("sitetemplate_id", "") 261 | 262 | results.append(device) 263 | return results 264 | 265 | 266 | ### SAVE REPORT 267 | def _format_data(data: list) -> list: 268 | formatted = [] 269 | message = "Formatting results..." 270 | PB.log_message(message, display_pbar=False) 271 | formatted.append(REPORT_HEADERS) 272 | for site_data in data: 273 | tmp = [] 274 | for header in REPORT_HEADERS: 275 | tmp.append(site_data.get(header, "")) 276 | formatted.append(tmp) 277 | return formatted 278 | 279 | 280 | def _save_as_csv(data: list, csv_file: str): 281 | PB.log_title(f"Saving report to {csv_file}", display_pbar=False) 282 | with open(csv_file, "w", encoding="UTF8", newline="") as f: 283 | csv_writer = csv.writer(f, delimiter=CSV_DELIMITER) 284 | csv_writer.writerows(data) 285 | PB.log_success("Saving to file ", display_pbar=False) 286 | 287 | 288 | def _display_report(data: list): 289 | PB.log_title("Displaying report", display_pbar=False) 290 | print(mistapi.cli.tabulate(data[1:], headers=data[0], tablefmt="rounded_grid")) 291 | 292 | 293 | def start( 294 | api_session: mistapi.APISession, 295 | org_id: str, 296 | csv_file: str = CSV_FILE, 297 | ): 298 | """ 299 | Start the report generation process. 300 | 301 | PARAMS 302 | ------ 303 | apisession : mistapi.APISession 304 | mistapi session with `Observer` access the Org, already logged in 305 | org_id : str 306 | Mist Organization ID 307 | csv_file : str 308 | path to the CSV file to save the report 309 | """ 310 | if not org_id: 311 | org_id = mistapi.cli.select_org(api_session)[0] 312 | 313 | inventory = _get_inventory(api_session, org_id) 314 | sites = _get_sites(api_session, org_id) 315 | results = _process_inventory(inventory, sites) 316 | formatted_data = _format_data(results) 317 | _save_as_csv(formatted_data, csv_file) 318 | _display_report(formatted_data) 319 | 320 | 321 | ############################################################################### 322 | ### USAGE 323 | 324 | 325 | def usage(error_message: str | None = None): 326 | """ 327 | show script usage 328 | """ 329 | print( 330 | """ 331 | ------------------------------------------------------------------------------- 332 | 333 | Written by Thomas Munzer (tmunzer@juniper.net) 334 | Github repository: https://github.com/tmunzer/Mist_library/ 335 | 336 | This script is licensed under the MIT License. 337 | 338 | ------------------------------------------------------------------------------- 339 | Python script to generate a report of the Mist Inventory and add information 340 | related to the Site the devices are assigned to. 341 | The script will display the report in the console and save it to a CSV file. 342 | It is possible to customize the columns to include in the report by modifying 343 | the `REPORT_HEADERS` list. 344 | 345 | ------- 346 | Requirements: 347 | mistapi: https://pypi.org/project/mistapi/ 348 | 349 | ------- 350 | Usage: 351 | This script can be run as is (without parameters), or with the options below. 352 | If no options are defined, or if options are missing, the missing options will 353 | be asked by the script or the default values will be used. 354 | 355 | It is recommended to use an environment file to store the required information 356 | to request the Mist Cloud (see https://pypi.org/project/mistapi/ for more 357 | information about the available parameters). 358 | 359 | ------- 360 | Options: 361 | -h, --help display this help 362 | -s, --org_id= Set the org_id 363 | -f, --out_file= define the filepath/filename where to save the data 364 | default is "./report_rogues.csv" 365 | -l, --log_file= define the filepath/filename where to write the logs 366 | default is "./script.log" 367 | -e, --env= define the env file to use (see mistapi env file documentation 368 | here: https://pypi.org/project/mistapi/) 369 | default is "~/.mist_env" 370 | 371 | ------- 372 | Examples: 373 | python3 ./report_inventory_site_notes.py 374 | python3 ./report_inventory_site_notes.py --site_ids=203d3d02-xxxx-xxxx-xxxx-76896a3330f4 375 | 376 | """ 377 | ) 378 | if error_message: 379 | CONSOLE.critical(error_message) 380 | sys.exit(0) 381 | 382 | 383 | def check_mistapi_version(): 384 | """Check if the installed mistapi version meets the minimum requirement.""" 385 | 386 | current_version = mistapi.__version__.split(".") 387 | required_version = MISTAPI_MIN_VERSION.split(".") 388 | 389 | try: 390 | for i, req in enumerate(required_version): 391 | if current_version[int(i)] > req: 392 | break 393 | if current_version[int(i)] < req: 394 | raise ImportError( 395 | f'"mistapi" package version {MISTAPI_MIN_VERSION} is required ' 396 | f"but version {mistapi.__version__} is installed." 397 | ) 398 | except ImportError as e: 399 | LOGGER.critical(str(e)) 400 | LOGGER.critical("Please use the pip command to update it.") 401 | LOGGER.critical("") 402 | LOGGER.critical(" # Linux/macOS") 403 | LOGGER.critical(" python3 -m pip install --upgrade mistapi") 404 | LOGGER.critical("") 405 | LOGGER.critical(" # Windows") 406 | LOGGER.critical(" py -m pip install --upgrade mistapi") 407 | print( 408 | f""" 409 | Critical:\r\n 410 | {e}\r\n 411 | Please use the pip command to update it. 412 | # Linux/macOS 413 | python3 -m pip install --upgrade mistapi 414 | # Windows 415 | py -m pip install --upgrade mistapi 416 | """ 417 | ) 418 | sys.exit(2) 419 | finally: 420 | LOGGER.info( 421 | '"mistapi" package version %s is required, ' 422 | "you are currently using version %s.", 423 | MISTAPI_MIN_VERSION, 424 | mistapi.__version__, 425 | ) 426 | 427 | 428 | ############################################################################### 429 | ### ENTRY POINT 430 | if __name__ == "__main__": 431 | parser = argparse.ArgumentParser( 432 | description="Display list of open events/alarms that are not cleared" 433 | ) 434 | parser.add_argument( 435 | "-e", "--env_file", help="define the env file to use", default=None 436 | ) 437 | parser.add_argument( 438 | "-o", 439 | "--org_id", 440 | help="Set the org_id where the webhook must be create/delete/retrieved", 441 | default="", 442 | ) 443 | parser.add_argument( 444 | "-l", 445 | "--log_file", 446 | help="define the filepath/filename where to write the logs", 447 | default=LOG_FILE, 448 | ) 449 | parser.add_argument( 450 | "-c", 451 | "--csv_file", 452 | help="Path to the CSV file where to save the result", 453 | default=CSV_FILE, 454 | ) 455 | 456 | args = parser.parse_args() 457 | 458 | ENV_FILE = args.env_file 459 | ORG_ID = args.org_id 460 | 461 | #### LOGS #### 462 | logging.basicConfig(filename=LOG_FILE, filemode="w") 463 | LOGGER.setLevel(logging.DEBUG) 464 | check_mistapi_version() 465 | ### MIST SESSION ### 466 | API_SESSION = mistapi.APISession(env_file=ENV_FILE) 467 | API_SESSION.login() 468 | ### START ### 469 | start(API_SESSION, ORG_ID, CSV_FILE) 470 | --------------------------------------------------------------------------------