├── sample_scripts ├── __init__.py ├── input_token_only.yaml ├── input_token_only_cluster_name.yaml ├── input_token_only.json ├── input_token_only_cluster_name.json ├── input_credentials.yaml ├── input_credentials_cluster_name.yaml ├── input_credentials.json ├── input_credentials_cluster_name.json ├── pycentral_module_sample.py └── pycentral_base_sample.py ├── pycentral ├── classic │ ├── workflows │ │ ├── __init__.py │ │ └── workflows_utils.py │ ├── __init__.py │ ├── constants.py │ ├── refresh_api_token.py │ ├── topology.py │ ├── audit_logs.py │ ├── url_utils.py │ └── base_utils.py ├── troubleshooting │ └── __init__.py ├── profiles │ └── __init__.py ├── new_monitoring │ ├── __init__.py │ ├── sites.py │ └── devices.py ├── glp │ ├── __init__.py │ ├── user_management.py │ ├── service_manager.py │ └── subscriptions.py ├── exceptions │ ├── __init__.py │ ├── parameter_error.py │ ├── pycentral_error.py │ ├── login_error.py │ ├── response_error.py │ └── verification_error.py ├── utils │ ├── __init__.py │ ├── profile_utils.py │ ├── troubleshooting_utils.py │ ├── common_utils.py │ ├── constants.py │ ├── url_utils.py │ ├── glp_utils.py │ ├── monitoring_utils.py │ ├── base_utils.py │ └── scope_utils.py ├── scopes │ ├── __init__.py │ ├── device_group.py │ ├── scope_base.py │ └── scope_maps.py └── __init__.py ├── docs ├── stylesheets │ └── extra.css ├── requirements.txt ├── modules │ ├── profiles.md │ ├── base.md │ ├── glp.md │ ├── exceptions.md │ ├── new_monitoring.md │ ├── scopes.md │ ├── utils.md │ ├── classic.md │ └── troubleshooting.md ├── getting-started │ ├── installation.md │ ├── quickstart.md │ └── authentication.md └── index.md ├── .readthedocs.yml ├── LICENSE ├── setup.py ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── mkdocs.yml ├── RELEASE-NOTES.md ├── README.md └── CONTRIBUTIONS.md /sample_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycentral/classic/workflows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycentral/troubleshooting/__init__.py: -------------------------------------------------------------------------------- 1 | from .troubleshooting import Troubleshooting 2 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root > * { 2 | --md-primary-fg-color: #01A982; 3 | } -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.6.1 2 | mkdocs-material==9.7.0 3 | mkdocstrings==0.30.1 4 | mkdocstrings-python==1.19.0 -------------------------------------------------------------------------------- /pycentral/classic/__init__.py: -------------------------------------------------------------------------------- 1 | # This file can be empty or contain initialization code for the classic package. 2 | -------------------------------------------------------------------------------- /docs/modules/profiles.md: -------------------------------------------------------------------------------- 1 | # Profiles 2 | 3 | The Profiles module manages network configuration profiles. 4 | 5 | ::: pycentral.profiles.profiles 6 | -------------------------------------------------------------------------------- /pycentral/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .profiles import Profiles 5 | -------------------------------------------------------------------------------- /sample_scripts/input_token_only.yaml: -------------------------------------------------------------------------------- 1 | central_info: 2 | base_url: "" 3 | token: 4 | access_token: "" 5 | ssl_verify: true -------------------------------------------------------------------------------- /docs/modules/base.md: -------------------------------------------------------------------------------- 1 | # Base Module 2 | 3 | The base module provides the core connection functionality for PyCentral. 4 | 5 | ## NewCentralBase 6 | 7 | ::: pycentral.base.NewCentralBase 8 | -------------------------------------------------------------------------------- /sample_scripts/input_token_only_cluster_name.yaml: -------------------------------------------------------------------------------- 1 | central_info: 2 | cluster-name: "" 3 | token: 4 | access_token: "" 5 | ssl_verify: true -------------------------------------------------------------------------------- /pycentral/new_monitoring/__init__.py: -------------------------------------------------------------------------------- 1 | from .aps import MonitoringAPs 2 | from .devices import MonitoringDevices 3 | from .gateways import MonitoringGateways 4 | from .sites import MonitoringSites 5 | from .clients import Clients 6 | -------------------------------------------------------------------------------- /sample_scripts/input_token_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "central_info": { 3 | "base_url": "", 4 | "token": { 5 | "access_token": "" 6 | } 7 | }, 8 | "ssl_verify": true 9 | } -------------------------------------------------------------------------------- /sample_scripts/input_token_only_cluster_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "central_info": { 3 | "cluster_name": "", 4 | "token": { 5 | "access_token": "" 6 | } 7 | }, 8 | "ssl_verify": true 9 | } -------------------------------------------------------------------------------- /pycentral/glp/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .devices import Devices 5 | from .subscriptions import Subscriptions 6 | from .user_management import UserMgmt 7 | from .service_manager import ServiceManager 8 | -------------------------------------------------------------------------------- /pycentral/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .login_error import LoginError 5 | from .parameter_error import ParameterError 6 | from .pycentral_error import PycentralError 7 | from .response_error import ResponseError 8 | from .verification_error import VerificationError 9 | -------------------------------------------------------------------------------- /sample_scripts/input_credentials.yaml: -------------------------------------------------------------------------------- 1 | central_info: 2 | username: "" 3 | password: "" 4 | client_id: "" 5 | client_secret: "" 6 | customer_id: "" 7 | base_url: "" 8 | token_store: 9 | type: "local" 10 | path: "temp" 11 | ssl_verify: true 12 | 13 | 14 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-24.04" 5 | tools: 6 | python: "3" 7 | # We recommend using a requirements file for reproducible builds. 8 | # This is just a quick example to get started. 9 | # https://docs.readthedocs.io/page/guides/reproducible-builds.html 10 | jobs: 11 | pre_install: 12 | - pip install -r docs/requirements.txt 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | -------------------------------------------------------------------------------- /pycentral/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from . import constants 5 | from .url_utils import generate_url 6 | 7 | for name in constants.__all__: 8 | globals()[name] = getattr(constants, name) 9 | 10 | # Define what gets exported when someone does "from pycentral.utils import *" 11 | __all__ = [ 12 | "generate_url", 13 | *constants.__all__, 14 | ] 15 | -------------------------------------------------------------------------------- /docs/modules/glp.md: -------------------------------------------------------------------------------- 1 | # GLP 2 | 3 | The GLP module provides interfaces for device, subscription, and user management through the HPE GreenLake Platform 4 | 5 | --- 6 | 7 | ## Devices 8 | ::: pycentral.glp.devices 9 | 10 | --- 11 | 12 | ## Service Manager 13 | ::: pycentral.glp.service_manager 14 | 15 | --- 16 | 17 | ## Subscriptions 18 | ::: pycentral.glp.subscriptions 19 | 20 | --- 21 | 22 | ## User Management 23 | ::: pycentral.glp.user_management -------------------------------------------------------------------------------- /sample_scripts/input_credentials_cluster_name.yaml: -------------------------------------------------------------------------------- 1 | central_info: 2 | username: "" 3 | password: "" 4 | client_id: "" 5 | client_secret: "" 6 | customer_id: "" 7 | cluster_name: "" 8 | token_store: 9 | type: "local" 10 | path: "temp" 11 | ssl_verify: true 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/modules/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | Custom exception classes for error handling. 4 | 5 | ## Base Exception 6 | 7 | ::: pycentral.exceptions.pycentral_error 8 | 9 | ## Login Error 10 | 11 | ::: pycentral.exceptions.login_error 12 | 13 | ## Response Error 14 | 15 | ::: pycentral.exceptions.response_error 16 | 17 | ## Parameter Error 18 | 19 | ::: pycentral.exceptions.parameter_error 20 | 21 | ## Verification Error 22 | 23 | ::: pycentral.exceptions.verification_error 24 | -------------------------------------------------------------------------------- /pycentral/scopes/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .site import Site 5 | from .site_collection import Site_Collection 6 | from .scopes import Scopes 7 | from .scope_maps import ScopeMaps 8 | from .device import Device 9 | from .device_group import Device_Group 10 | 11 | __all__ = [ 12 | "Site", 13 | "Site_Collection", 14 | "Scopes", 15 | "ScopeMaps", 16 | "Device", 17 | "Device_Group", 18 | ] 19 | -------------------------------------------------------------------------------- /sample_scripts/input_credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "central_info": { 3 | "username": "", 4 | "password": "", 5 | "client_id": "", 6 | "client_secret": "", 7 | "customer_id": "", 8 | "base_url": "" 9 | }, 10 | "token_store": { 11 | "type": "local", 12 | "path": "temp" 13 | }, 14 | "ssl_verify": true 15 | } -------------------------------------------------------------------------------- /sample_scripts/input_credentials_cluster_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "central_info": { 3 | "username": "", 4 | "password": "", 5 | "client_id": "", 6 | "client_secret": "", 7 | "customer_id": "", 8 | "cluster_name": "" 9 | }, 10 | "token_store": { 11 | "type": "local", 12 | "path": "temp" 13 | }, 14 | "ssl_verify": true 15 | } -------------------------------------------------------------------------------- /docs/modules/new_monitoring.md: -------------------------------------------------------------------------------- 1 | # Monitoring 2 | 3 | Monitoring modules available in the PyCentral SDK are separated by type APs, Clients, Devices, Gateways, and Sites. 4 | 5 | --- 6 | 7 | ## APs 8 | ::: pycentral.new_monitoring.aps 9 | 10 | --- 11 | 12 | ## Clients 13 | ::: pycentral.new_monitoring.clients 14 | 15 | --- 16 | 17 | ## Devices 18 | ::: pycentral.new_monitoring.devices 19 | 20 | --- 21 | 22 | ## Gateways 23 | ::: pycentral.new_monitoring.gateways 24 | 25 | --- 26 | 27 | ## Sites 28 | ::: pycentral.new_monitoring.sites 29 | -------------------------------------------------------------------------------- /docs/modules/scopes.md: -------------------------------------------------------------------------------- 1 | # Scopes 2 | 3 | The Scopes module handles hierachical structures such as scopes, sites, collections, and groups. 4 | 5 | --- 6 | 7 | ## Device 8 | ::: pycentral.scopes.device 9 | 10 | --- 11 | 12 | ## Device Group 13 | ::: pycentral.scopes.device_group 14 | 15 | --- 16 | 17 | ## Scope Base 18 | ::: pycentral.scopes.scope_base 19 | 20 | --- 21 | 22 | ## Scope Maps 23 | ::: pycentral.scopes.scope_maps 24 | 25 | --- 26 | 27 | ## Scopes 28 | ::: pycentral.scopes.scopes 29 | 30 | --- 31 | 32 | ## Site 33 | ::: pycentral.scopes.site 34 | 35 | --- 36 | 37 | ## Site Collection 38 | ::: pycentral.scopes.site_collection -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To install the latest pre-release version of PyCentral, use the following command: 4 | 5 | ``` 6 | pip3 install --pre pycentral 7 | ``` 8 | 9 | ## Upgrading from PyCentral V1 to V2 10 | 11 | If you are currently using PyCentral-V1 and want to upgrade to the latest pre-release version of PyCentral-V2, run: 12 | 13 | ``` 14 | pip3 install --upgrade --pre pycentral 15 | ``` 16 | 17 | Upgrading to V2 will not break existing Classic Central workflows - V2 remains fully backward-compatible with V1 while enabling access to New Central & GLP APIs. 18 | 19 | ## Next Steps 20 | 21 | - [Authentication](authentication.md) - Configure your credentials 22 | - [Quick Start](quickstart.md) - Begin using PyCentral 23 | -------------------------------------------------------------------------------- /docs/modules/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | Utility modules providing helper functions and common operations. 4 | 5 | --- 6 | 7 | ## Base Utils 8 | ::: pycentral.utils.base_utils 9 | 10 | --- 11 | 12 | ## Common Utils 13 | ::: pycentral.utils.common_utils 14 | 15 | --- 16 | 17 | ## Constants 18 | ::: pycentral.utils.constants 19 | 20 | --- 21 | 22 | ## GLP Utils 23 | ::: pycentral.utils.glp_utils 24 | 25 | --- 26 | 27 | ## Monitoring Utils 28 | ::: pycentral.utils.monitoring_utils 29 | 30 | --- 31 | 32 | ## Profile Utils 33 | ::: pycentral.utils.profile_utils 34 | 35 | --- 36 | 37 | ## Scope Utils 38 | ::: pycentral.utils.scope_utils 39 | 40 | --- 41 | 42 | ## Troubleshooting Utils 43 | ::: pycentral.utils.troubleshooting_utils 44 | 45 | --- 46 | 47 | ## URL utils 48 | ::: pycentral.utils.url_utils 49 | -------------------------------------------------------------------------------- /pycentral/exceptions/parameter_error.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from pycentral.exceptions.verification_error import VerificationError 5 | 6 | 7 | class ParameterError(VerificationError): 8 | """Exception raised when invalid parameters are passed to functions. 9 | 10 | This exception is a subclass of VerificationError and is used to indicate 11 | that one or more parameters provided to a function are invalid, missing, 12 | or do not meet the required constraints. 13 | 14 | Attributes: 15 | base_msg (str): The base error message for this exception type. 16 | 17 | Example: 18 | ```python 19 | >>> raise ParameterError(f"name must be a valid string found {type(name)}") 20 | ParameterError: "PARAMETER ERROR: name must be a valid string found " 21 | ``` 22 | """ 23 | 24 | base_msg = "PARAMETER ERROR" 25 | -------------------------------------------------------------------------------- /pycentral/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .base import NewCentralBase 5 | 6 | # Manually import each module in the legacy Central folder 7 | import importlib 8 | import sys 9 | 10 | CLASSIC_MODULES = [ 11 | "audit_logs", 12 | "base", 13 | "base_utils", 14 | "configuration", 15 | "constants", 16 | "device_inventory", 17 | "firmware_management", 18 | "licensing", 19 | "monitoring", 20 | "msp", 21 | "rapids", 22 | "refresh_api_token", 23 | "topology", 24 | "url_utils", 25 | "user_management", 26 | "visualrf", 27 | "workflows", 28 | ] 29 | 30 | for module in CLASSIC_MODULES: 31 | full_module_name = f"pycentral.classic.{module}" 32 | imported_module = importlib.import_module(full_module_name) 33 | 34 | sys.modules[f"pycentral.{module}"] = imported_module 35 | 36 | # Delete importlib and sys to clean up the namespace after their use 37 | del importlib, sys 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hewlett Packard Enterprise Development LP. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/modules/classic.md: -------------------------------------------------------------------------------- 1 | # Classic API 2 | 3 | Legacy PyCentral reference modules for device management, monitoring, and configuration 4 | with classic Central. 5 | 6 | --- 7 | 8 | ## Audit Logs 9 | ::: pycentral.classic.audit_logs 10 | 11 | --- 12 | 13 | ## Base Utils 14 | ::: pycentral.classic.base_utils 15 | 16 | --- 17 | 18 | ## Configuration 19 | ::: pycentral.classic.configuration 20 | 21 | --- 22 | 23 | ## Constants 24 | ::: pycentral.classic.constants 25 | 26 | --- 27 | 28 | ## Device Inventory 29 | ::: pycentral.classic.device_inventory 30 | 31 | --- 32 | 33 | ## Firmware Management 34 | ::: pycentral.classic.firmware_management 35 | 36 | --- 37 | 38 | ## Licensing 39 | ::: pycentral.classic.licensing 40 | 41 | --- 42 | 43 | ## Monitoring 44 | ::: pycentral.classic.monitoring 45 | 46 | --- 47 | 48 | ## MSP 49 | ::: pycentral.classic.msp 50 | 51 | --- 52 | 53 | ## RAPIDS 54 | ::: pycentral.classic.rapids 55 | 56 | --- 57 | 58 | ## Refresh API Token 59 | ::: pycentral.classic.refresh_api_token 60 | 61 | --- 62 | 63 | ## Topology 64 | ::: pycentral.classic.topology 65 | 66 | --- 67 | 68 | ## URL Utility 69 | ::: pycentral.classic.url_utils 70 | 71 | --- 72 | 73 | ## User Management 74 | ::: pycentral.classic.user_management 75 | 76 | --- 77 | 78 | ## VisualRF 79 | ::: pycentral.classic.visualrf -------------------------------------------------------------------------------- /pycentral/utils/profile_utils.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | 5 | from pycentral.exceptions import ParameterError 6 | 7 | 8 | def validate_local(local): 9 | """Validate local profile attributes and prepare them for API requests. 10 | 11 | Args: 12 | local (dict or None): Local profile attributes dictionary containing 13 | scope_id (int) and persona (str). 14 | 15 | Returns: 16 | (dict): Validated local attributes dictionary with object_type set to "LOCAL". 17 | 18 | Raises: 19 | ParameterError: If local is not a dictionary or missing required keys 20 | with correct types. 21 | """ 22 | required_keys = {"scope_id": int, "persona": str} 23 | local_attributes = dict() 24 | if local: 25 | if not isinstance(local, dict): 26 | raise ParameterError( 27 | "Invalid local profile attributes. Please provide a valid dictionary." 28 | ) 29 | for key, expected_type in required_keys.items(): 30 | if key not in local or not isinstance(local[key], expected_type): 31 | raise ParameterError( 32 | f"Invalid local profile attributes. Key '{key}' must be of type {expected_type.__name__}." 33 | ) 34 | local_attributes = {"object_type": "LOCAL"} 35 | local_attributes.update(local) 36 | return local_attributes 37 | -------------------------------------------------------------------------------- /pycentral/exceptions/pycentral_error.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from typing import Any 5 | 6 | 7 | class PycentralError(Exception): 8 | """Base exception class for all pycentral-specific errors. 9 | 10 | This exception serves as the base class for all custom exceptions in the pycentral library. 11 | It provides common functionality for error handling and message formatting. 12 | 13 | Attributes: 14 | base_msg (str): The base error message for this exception type. 15 | message (str): The complete formatted error message. 16 | response (dict): The API response associated with the error, if applicable. 17 | 18 | Example: 19 | ```python 20 | >>> raise PycentralError("An unexpected error occurred") 21 | PycentralError: 'PYCENTRAL ERROR, An unexpected error occurred' 22 | ``` 23 | """ 24 | 25 | base_msg = "PYCENTRAL ERROR" 26 | 27 | def __init__(self, *args): 28 | self.message = ", ".join( 29 | ( 30 | self.base_msg, 31 | *(str(a) for a in args), 32 | ) 33 | ) 34 | self.response = None 35 | 36 | def __setattr__(self, name: str, value: Any) -> None: 37 | return super().__setattr__(name, value) 38 | 39 | def __str__(self): 40 | return repr(self.message) 41 | 42 | def set_response(self, response): 43 | self.response = response 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from os import path 3 | 4 | this_directory = path.abspath(path.dirname(__file__)) 5 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | VERSION = "2.0a12" 9 | 10 | setuptools.setup( 11 | name="pycentral", 12 | version=VERSION, 13 | author="aruba-automation", 14 | author_email="aruba-automation@hpe.com", 15 | description="HPE Aruba Networking Central Python Package", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | project_urls={ 19 | "Documentation": "https://pycentral.readthedocs.io/en/v2/", 20 | "Repository": "https://github.com/aruba/pycentral/", 21 | "Issues": "https://github.com/aruba/pycentral/issues", 22 | }, 23 | packages=setuptools.find_packages(exclude=["docs", "tests", "sample_scripts"]), 24 | classifiers=[ 25 | "Programming Language :: Python :: 3 :: Only", 26 | "License :: OSI Approved :: MIT License", 27 | "Operating System :: OS Independent", 28 | "Intended Audience :: System Administrators", 29 | "Topic :: System :: Networking", 30 | "Development Status :: 4 - Beta", 31 | ], 32 | python_requires=">=3.8", 33 | install_requires=[ 34 | "requests==2.32.4", 35 | "PyYAML==6.0.2", 36 | "oauthlib==3.2.2", 37 | "requests_oauthlib==2.0.0", 38 | "pytz==2025.2", 39 | ], 40 | extras_require={"colorLog": ["colorlog"]}, 41 | ) 42 | -------------------------------------------------------------------------------- /pycentral/utils/troubleshooting_utils.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | """Utilities for troubleshooting operations 5 | 6 | This module provides constants related to supported device types and mappings 7 | for troubleshooting methods to supported devices. 8 | 9 | Attributes: 10 | SUPPORTED_DEVICE_TYPES (list[str]): List of supported device types for 11 | troubleshooting operations. 12 | 13 | TROUBLESHOOTING_METHOD_DEVICE_MAPPING (dict[str, list[str]]): Mapping of 14 | troubleshooting method names to lists of compatible device types. 15 | Each key represents a supported troubleshooting test, and the value is a list of 16 | device types that support it. 17 | """ 18 | 19 | SUPPORTED_DEVICE_TYPES = ["aos-s", "cx", "aps", "gateways"] 20 | 21 | TROUBLESHOOTING_METHOD_DEVICE_MAPPING = { 22 | "retrieve_arp_table_test": ["aos-s", "aps", "gateways"], 23 | "locate_test": ["cx", "aps", "gateways"], 24 | "http_test": ["cx", "aps", "gateways"], 25 | "poe_bounce_test": ["cx", "aos-s", "gateways"], 26 | "port_bounce_test": ["cx", "aos-s", "gateways"], 27 | "speedtest_test": ["aps"], 28 | "aaa_test": ["cx", "aps"], 29 | "tcp_test": ["aps"], 30 | "iperf_test": ["gateways"], 31 | "cable_test": ["cx", "aos-s"], 32 | "nslookup_test": ["aps"], 33 | "disconnect_user_mac_addr": ["aps"], 34 | "disconnect_all_users": ["aps"], 35 | "disconnect_all_users_ssid": ["aps"], 36 | "disconnect_all_clients": ["gateways"], 37 | "disconnect_client_mac_addr": ["gateways"], 38 | } 39 | -------------------------------------------------------------------------------- /pycentral/exceptions/login_error.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from pycentral.exceptions.pycentral_error import PycentralError 5 | 6 | 7 | class LoginError(PycentralError): 8 | """Exception raised when login or authentication fails. 9 | 10 | This exception is raised when authentication to HPE Aruba Networking Central fails, 11 | typically due to invalid credentials, expired tokens, or network issues. 12 | 13 | Attributes: 14 | base_msg (str): The base error message for this exception type. 15 | message (str): Detailed error message describing the login failure. 16 | status_code (int): HTTP status code associated with the login failure, if available. 17 | 18 | Example: 19 | ```python 20 | >>> raise LoginError(msg, status_code) 21 | LoginError: LOGIN ERROR - Invalid client or client credentials. for 22 | new_central. Provide valid client_id and client_secret to create an 23 | access token. (status_code=401) 24 | ``` 25 | """ 26 | 27 | base_msg = "LOGIN ERROR" 28 | 29 | def __init__(self, message, status_code=None, *details): 30 | self.status_code = status_code 31 | 32 | parts = [self.base_msg] 33 | if message: 34 | parts.append(str(message)) 35 | if details: 36 | parts.extend(str(d) for d in details) 37 | 38 | self.message = " - ".join(parts) 39 | 40 | def __str__(self): 41 | if self.status_code is not None: 42 | return f"{self.message} (status_code={self.status_code})" 43 | return self.message 44 | -------------------------------------------------------------------------------- /pycentral/exceptions/response_error.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from pycentral.exceptions.pycentral_error import PycentralError 5 | 6 | 7 | class ResponseError(PycentralError): 8 | """Exception raised when an API response indicates an error. 9 | 10 | This exception is raised when the API returns an error response, such as 11 | HTTP error codes (4xx, 5xx) or when the response content indicates a failure. 12 | 13 | Attributes: 14 | base_msg (str): The base error message for this exception type. 15 | message (str): Detailed error message describing the response failure. 16 | response (dict): The API response object containing error details. 17 | 18 | Example: 19 | ```python 20 | >>> raise ResponseError({"code": 404, "msg": "Not found"}, "Resource does not exist") 21 | ResponseError: 'RESPONSE ERROR: Resource does not exist: Response: {"code": 404, "msg": "Not found"}' 22 | ``` 23 | """ 24 | 25 | base_msg = "RESPONSE ERROR" 26 | 27 | def __init__(self, *args): 28 | self.message = None 29 | self.response = None 30 | if args: 31 | self.response = args[0] 32 | if len(args) > 1: 33 | self.message = ", ".join(str(a) for a in args[1:]) 34 | else: 35 | self.message = None 36 | 37 | def __str__(self): 38 | msg_parts = [self.base_msg] 39 | if self.message: 40 | msg_parts.append(str(self.message)) 41 | if self.response: 42 | msg_parts.append("Response") 43 | msg_parts.append(str(self.response)) 44 | msg = ": ".join(msg_parts) 45 | return repr(msg) 46 | -------------------------------------------------------------------------------- /pycentral/exceptions/verification_error.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from pycentral.exceptions.pycentral_error import PycentralError 5 | 6 | 7 | class VerificationError(PycentralError): 8 | """Exception raised when verification checks fail during pycentral operations. 9 | 10 | This exception is raised when verification checks of values fail prior to API execution. 11 | It serves as a base class for more specific verification-related exceptions. 12 | 13 | Attributes: 14 | base_msg (str): The base error message for this exception type. 15 | message (str): Detailed error message describing the verification failure. 16 | module (str): The module or context where the verification error occurred. 17 | 18 | Example: 19 | ```python 20 | >>> raise VerificationError(err_str, " get_resource_str() failed") 21 | VerificationError: "VERIFICATION ERROR: Missing self.object_data['resource'] attribute DETAIL: get_resource_str() failed" 22 | ``` 23 | """ 24 | 25 | base_msg = "VERIFICATION ERROR" 26 | 27 | def __init__(self, *args): 28 | self.message = None 29 | self.module = None 30 | if args: 31 | self.module = args[0] 32 | if len(args) > 1: 33 | self.message = ", ".join(str(a) for a in args[1:]) 34 | 35 | def __str__(self): 36 | msg_parts = [self.base_msg] 37 | if self.module: 38 | if self.message: 39 | msg_parts.append("{0} DETAIL".format(self.module)) 40 | msg_parts.append(self.message) 41 | else: 42 | msg_parts.append(self.module) 43 | msg = ": ".join(msg_parts) 44 | return repr(msg) 45 | -------------------------------------------------------------------------------- /pycentral/classic/constants.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | DEVICE_TYPES = ["IAP", "ArubaSwitch", "CX", "MobilityController"] 24 | 25 | # Dictionary of Public Aruba Cluster Names with their corresponding API Base 26 | # URLs. You can update this dictionary, if you want to add your own private 27 | # Aruba cluster details 28 | CLUSTER_API_BASE_URL_LIST = { 29 | "US-1": "app1-apigw.central.arubanetworks.com", 30 | "US-2": "apigw-prod2.central.arubanetworks.com", 31 | "US-East1": "apigw-us-east-1.central.arubanetworks.com", 32 | "US-West4": "apigw-uswest4.central.arubanetworks.com", 33 | "EU-1": "eu-apigw.central.arubanetworks.com", 34 | "EU-Central2": "apigw-eucentral2.central.arubanetworks.com", 35 | "EU-Central3": "apigw-eucentral3.central.arubanetworks.com", 36 | "Canada-1": "apigw-ca.central.arubanetworks.com", 37 | "China-1": "apigw.central.arubanetworks.com.cn", 38 | "APAC-1": "api-ap.central.arubanetworks.com", 39 | "APAC-EAST1": "apigw-apaceast.central.arubanetworks.com", 40 | "APAC-SOUTH1": "apigw-apacsouth.central.arubanetworks.com", 41 | "UAE-NORTH1": "apigw-uaenorth1.central.arubanetworks.com" 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build, Stage, Approve, Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | name: Build distribution 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v6 14 | with: 15 | persist-credentials: false 16 | - name: Set up Python 17 | uses: actions/setup-python@v6 18 | with: 19 | python-version: "3.x" 20 | - name: Install pypa/build 21 | run: >- 22 | python3 -m 23 | pip install 24 | build 25 | --user 26 | - name: Build a binary wheel and a source tarball 27 | run: python3 -m build 28 | - name: Store the distribution packages 29 | uses: actions/upload-artifact@v5 30 | with: 31 | name: python-package-distributions 32 | path: dist/ 33 | 34 | publish-to-testpypi: 35 | name: Publish PyCentral to TestPyPI 36 | needs: 37 | - build 38 | runs-on: ubuntu-latest 39 | 40 | environment: 41 | name: testpypi 42 | url: https://test.pypi.org/p/pycentral/ 43 | 44 | permissions: 45 | id-token: write # IMPORTANT: mandatory for trusted publishing 46 | 47 | steps: 48 | - name: Download all the dists 49 | uses: actions/download-artifact@v6 50 | with: 51 | name: python-package-distributions 52 | path: dist/ 53 | - name: Publish distribution to TestPyPI 54 | uses: pypa/gh-action-pypi-publish@release/v1 55 | with: 56 | repository-url: https://test.pypi.org/legacy/ 57 | 58 | publish-to-pypi: 59 | name: Publish PyCentral to PyPI 60 | needs: publish-to-testpypi # only run after TestPyPI succeeds 61 | runs-on: ubuntu-latest 62 | if: startsWith(github.ref, 'refs/tags/') # only run on tags 63 | 64 | environment: 65 | name: pypi 66 | url: https://pypi.org/project/pycentral/ 67 | 68 | permissions: 69 | id-token: write # required for trusted publishing 70 | contents: read 71 | 72 | steps: 73 | - name: Download all the dists 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: python-package-distributions 77 | path: dist/ 78 | 79 | - name: Publish distribution to PyPI 80 | uses: pypa/gh-action-pypi-publish@release/v1 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyCentral 2 | site_description: Documentation for PyCentral - Central Python SDK 3 | site_author: Hewlett Packard Enterprise 4 | repo_url: https://github.com/aruba/pycentral 5 | repo_name: pycentral 6 | 7 | theme: 8 | name: material 9 | palette: 10 | - media: "(prefers-color-scheme)" 11 | toggle: 12 | icon: material/brightness-auto 13 | name: Switch to light mode 14 | 15 | # Palette toggle for light mode 16 | - media: "(prefers-color-scheme: light)" 17 | scheme: default 18 | toggle: 19 | icon: material/weather-night 20 | name: Switch to dark mode 21 | 22 | # Palette toggle for dark mode 23 | - media: "(prefers-color-scheme: dark)" 24 | scheme: slate 25 | toggle: 26 | icon: material/weather-sunny 27 | name: Switch to light mode 28 | 29 | features: 30 | - navigation.tabs 31 | - navigation.sections 32 | - navigation.expand 33 | - toc.integrate 34 | - search.suggest 35 | - search.highlight 36 | - content.code.copy 37 | extra_css: 38 | - stylesheets/extra.css 39 | 40 | nav: 41 | - Home: index.md 42 | - Getting Started: 43 | - Installation: getting-started/installation.md 44 | - Authentication: getting-started/authentication.md 45 | - Quick Start: getting-started/quickstart.md 46 | - Module Reference: 47 | - Base Module: modules/base.md 48 | - Exceptions: modules/exceptions.md 49 | - GLP: modules/glp.md 50 | - Profiles: modules/profiles.md 51 | - New Monitoring: modules/new_monitoring.md 52 | - Scopes: modules/scopes.md 53 | - Troubleshooting: modules/troubleshooting.md 54 | - Utils: modules/utils.md 55 | - Classic Modules: modules/classic.md 56 | 57 | plugins: 58 | - search 59 | - mkdocstrings: 60 | handlers: 61 | python: 62 | options: 63 | docstring_style: google 64 | show_source: true 65 | show_root_heading: true 66 | show_root_full_path: false 67 | show_symbol_type_heading: true 68 | show_symbol_type_toc: true 69 | members_order: source 70 | show_signature_annotations: true 71 | merge_init_into_class: true 72 | 73 | markdown_extensions: 74 | - admonition 75 | - pymdownx.details 76 | - pymdownx.superfences 77 | - pymdownx.highlight: 78 | anchor_linenums: true 79 | - pymdownx.inlinehilite 80 | - pymdownx.snippets 81 | - pymdownx.tabbed: 82 | alternate_style: true 83 | - toc: 84 | permalink: true 85 | - attr_list 86 | - md_in_html 87 | -------------------------------------------------------------------------------- /pycentral/scopes/device_group.py: -------------------------------------------------------------------------------- 1 | from .scope_base import ScopeBase 2 | 3 | 4 | API_ATTRIBUTE_MAPPING = { 5 | "deviceCount": "device_count", 6 | "id": "id", 7 | "scopeName": "name", 8 | "description": "description", 9 | } 10 | 11 | REQUIRED_ATTRIBUTES = ["name", "id"] 12 | 13 | 14 | class Device_Group(ScopeBase): 15 | """This class holds device groups and all of its attributes & related methods.""" 16 | 17 | def __init__( 18 | self, device_group_attributes=None, central_conn=None, from_api=False 19 | ): 20 | """Constructor for Device Group object. 21 | 22 | Args: 23 | device_group_attributes (dict, optional): Attributes of the Device Group 24 | central_conn (NewCentralBase, optional): Instance of NewCentralBase 25 | to establish connection to Central 26 | from_api (bool, optional): Boolean indicates if the device_group_attributes is from the 27 | Central API response 28 | 29 | Raises: 30 | Exception: If from_api is False (currently not supported) 31 | """ 32 | self.materialized = from_api 33 | self.central_conn = central_conn 34 | self.type = "device_group" 35 | if from_api: 36 | # Rename keys if attributes are from API 37 | device_group_attributes = self.__rename_keys( 38 | device_group_attributes, API_ATTRIBUTE_MAPPING 39 | ) 40 | device_group_attributes["assigned_profiles"] = [] 41 | device_group_attributes["devices"] = [] 42 | for key, value in device_group_attributes.items(): 43 | setattr(self, key, value) 44 | else: 45 | raise Exception( 46 | "Currently, Device Group requires attributes from API response to be created." 47 | ) 48 | 49 | def __rename_keys(self, api_dict, api_attribute_mapping): 50 | """Renames the keys of the attributes from the API response. 51 | 52 | Args: 53 | api_dict (dict): Dict from Central API Response 54 | api_attribute_mapping (dict): Dict mapping API keys to object attributes 55 | 56 | Returns: 57 | (dict): Renamed dictionary of object attributes 58 | """ 59 | integer_attributes = {"id"} 60 | renamed_dict = {} 61 | 62 | for key, value in api_dict.items(): 63 | new_key = api_attribute_mapping.get(key) 64 | if not new_key: 65 | continue # Skip unknown keys 66 | if key in integer_attributes and value is not None: 67 | value = int(value) 68 | renamed_dict[new_key] = value 69 | return renamed_dict 70 | -------------------------------------------------------------------------------- /pycentral/utils/common_utils.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from copy import deepcopy 5 | import os 6 | import yaml 7 | import json 8 | 9 | 10 | def __setattrs__(self, config_attrs): 11 | """Dynamically set attributes of an object based on the provided dictionary. 12 | 13 | Args: 14 | config_attrs (dict): Dictionary whose keys will be added as attributes 15 | to the object with corresponding values. 16 | 17 | Returns: 18 | (dict): Dictionary of attribute names and their values. 19 | """ 20 | attr_data_dict = dict() 21 | for key, value in config_attrs.items(): 22 | if hasattr(self, key): 23 | attr_data_dict[key] = getattr(self, key) 24 | else: 25 | attr_data_dict[key] = value 26 | 27 | return attr_data_dict 28 | 29 | 30 | def create_attrs(obj, data_dictionary): 31 | """Create class attributes from a dictionary. 32 | 33 | Uses setattr() to set the value of attributes on the specified object. 34 | If an attribute already exists and its current value is not None, 35 | it keeps the previous value. 36 | 37 | Args: 38 | obj (object): Object instance to create/set attributes on. 39 | data_dictionary (dict): Dictionary containing keys that will become attributes. 40 | """ 41 | # Used to create a deep copy of the dictionary 42 | dictionary_var = deepcopy(data_dictionary) 43 | 44 | # K is the argument and V is the value of the given argument 45 | for k, v in dictionary_var.items(): 46 | # In case a key has '-' inside it's name. 47 | k = k.replace("-", "_") 48 | 49 | obj.__dict__[k] = v 50 | 51 | 52 | def parse_input_file(file_path): 53 | """Parse data from a YAML or JSON file. 54 | 55 | Args: 56 | file_path (str): Path to the file. 57 | 58 | Returns: 59 | (dict): Parsed data from the file. 60 | 61 | Raises: 62 | FileNotFoundError: If the specified file does not exist. 63 | ValueError: If the file format is unsupported or file cannot be loaded. 64 | """ 65 | if not os.path.isfile(file_path): 66 | raise FileNotFoundError(f"File not found: {file_path}") 67 | 68 | try: 69 | with open(file_path, "r") as file: 70 | if file_path.endswith(".yaml") or file_path.endswith(".yml"): 71 | return yaml.safe_load(file) 72 | elif file_path.endswith(".json"): 73 | return json.load(file) 74 | else: 75 | raise ValueError("Unsupported file format. Use YAML or JSON.") 76 | except Exception as e: 77 | raise ValueError(f"Failed to parse data from file: {e}") 78 | -------------------------------------------------------------------------------- /sample_scripts/pycentral_module_sample.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | 5 | """ 6 | Sample script shows making a REST API call to Aruba Central using module 7 | `pycentral.pycentral.configuration` and class:`Groups`. In this sample script 8 | an API call is made to obtain list of existing groups. 9 | 10 | 1. central_info: 11 | Either provide the following Aruba Central credentials [or] 12 | use API Gateway Access Token 13 | 14 | central_info = { 15 | "username": "", 16 | "password": "", 17 | "client_id": "", 18 | "client_secret": "", 19 | "customer_id": "", 20 | "base_url": "" 21 | } 22 | 23 | [OR] 24 | 25 | central_info = { 26 | "base_url": "", 27 | "token": { 28 | "access_token": "" 29 | } 30 | } 31 | 32 | 2. token_store: 33 | When (username, password, client_id, client_secret and customer_id) are provided in central_info, 34 | a new access token will be generated and by default cached locally under `temp` dir. To prevent 35 | caching directly provide `token` in `central_info` (instead of username, password, client_id, 36 | client_secret and customer_id). 37 | 38 | To modify where the token is stored, use the following variable. 39 | 40 | token_store = { 41 | "type": "local", 42 | "path": "temp" 43 | } 44 | 45 | 3. ssl_verify: 46 | To disable Python Client to validate SSL certs, set ssl_verify to False. By default, set to True. 47 | 48 | ssl_verify = True 49 | """ 50 | 51 | # Import Aruba Central Base 52 | from pycentral.base import ArubaCentralBase 53 | from pprint import pprint 54 | 55 | # Create an instance of ArubaCentralBase using API access token 56 | # or API Gateway credentials. 57 | central_info = { 58 | "base_url": "", 59 | "token": { 60 | "access_token": "" 61 | } 62 | } 63 | ssl_verify = True 64 | central = ArubaCentralBase(central_info=central_info, 65 | token_store=None, 66 | ssl_verify=ssl_verify) 67 | 68 | # Sample API call using Configuration module `pycentral.configuration` 69 | from pycentral.configuration import Groups 70 | 71 | # Get groups max limit 20, apply offset and fetch other groups in loop 72 | g = Groups() 73 | 74 | module_resp = g.get_groups(central) 75 | pprint(module_resp) 76 | -------------------------------------------------------------------------------- /sample_scripts/pycentral_base_sample.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | 5 | """ 6 | Sample script shows making a REST API call to Aruba Central using base module 7 | `pycentral.pycentral.base` and function `command()`. In this sample script an 8 | API call is made to obtain list of existing groups. 9 | 10 | 1. central_info: 11 | Either provide the following Aruba Central credentials [or] 12 | use API Gateway Access Token 13 | 14 | central_info = { 15 | "username": "", 16 | "password": "", 17 | "client_id": "", 18 | "client_secret": "", 19 | "customer_id": "", 20 | "base_url": "" 21 | } 22 | 23 | [OR] 24 | 25 | central_info = { 26 | "base_url": "", 27 | "token": { 28 | "access_token": "" 29 | } 30 | } 31 | 32 | 2. token_store: 33 | When (username, password, client_id, client_secret and customer_id) are provided in central_info, 34 | a new access token will be generated and by default cached locally under `temp` dir. To prevent 35 | caching directly provide `token` in `central_info` (instead of username, password, client_id, 36 | client_secret and customer_id). 37 | 38 | To modify where the token is stored, use the following variable. 39 | 40 | token_store = { 41 | "type": "local", 42 | "path": "temp" 43 | } 44 | 45 | 3. ssl_verify: 46 | To disable Python Client to validate SSL certs, set ssl_verify to False. By default, set to True. 47 | 48 | ssl_verify = True 49 | """ 50 | 51 | # Import Aruba Central Base 52 | from pycentral.base import ArubaCentralBase 53 | from pprint import pprint 54 | 55 | # Create an instance of ArubaCentralBase using API access token 56 | # or API Gateway credentials. 57 | central_info = { 58 | "base_url": "", 59 | "token": { 60 | "access_token": "" 61 | } 62 | } 63 | ssl_verify = True 64 | central = ArubaCentralBase(central_info=central_info, 65 | ssl_verify=ssl_verify) 66 | 67 | # Sample API call using 'ArubaCentralBase.command()' 68 | # GET groups from Aruba Central 69 | apiPath = "/configuration/v2/groups" 70 | apiMethod = "GET" 71 | apiParams = { 72 | "limit": 20, 73 | "offset": 0 74 | } 75 | base_resp = central.command(apiMethod=apiMethod, 76 | apiPath=apiPath, 77 | apiParams=apiParams) 78 | pprint(base_resp) 79 | -------------------------------------------------------------------------------- /pycentral/classic/refresh_api_token.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .url_utils import RefreshUrl 24 | from .base_utils import console_logger 25 | 26 | urls = RefreshUrl() 27 | 28 | 29 | class RefreshApiToken(object): 30 | """Refresh the API access token in API Gateway using OAUTH API 31 | """ 32 | 33 | def refresh_token(self, conn, apigw_client_id, apigw_client_secret, 34 | old_refresh_token): 35 | """This function refreshes the existing access token and replaces old\ 36 | token with new token. The returned token dict will contain both access\ 37 | and refresh token. Use refresh token provided in the return dict for\ 38 | next refresh. 39 | 40 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 41 | API call. 42 | :type conn: class:`pycentral.ArubaCentralBase` 43 | :param apigw_client_id: Client ID from API Gateway page 44 | :type apigw_client_id: str 45 | :param apigw_client_secret: Client Secret from API Gateway page 46 | :type apigw_client_secret: str 47 | :param old_refresh_token: Refresh token value from the current/expired\ 48 | API token. 49 | :type old_refresh_token: str 50 | :return: Refrehed token dict consisting of access_token and\ 51 | refresh_token. 52 | :rtype: dict 53 | """ 54 | path = urls.REFRESH_TOKEN["REFRESH"] 55 | resp = None 56 | params = { 57 | "client_id": apigw_client_id, 58 | "client_secret": apigw_client_secret, 59 | "grant_type": "refresh_token", 60 | "refresh_token": old_refresh_token 61 | } 62 | resp = conn.command( 63 | apiMethod="POST", 64 | apiPath=path, 65 | apiParams=params, 66 | retry_api_call=False) 67 | return resp 68 | -------------------------------------------------------------------------------- /docs/modules/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | This documentation provides an overview of the troubleshooting capabilities available in the PyCentral SDK. It lists the supported troubleshooting tests for different device types (APs, AOS-CX, AOS-S, Gateway), and maps each test to the corresponding method in the `Troubleshooting` class. Use this as a reference to identify which troubleshooting functions are available for your device and how to invoke them programmatically. 4 | 5 | | Test Name | Method Name | APs | AOS-CX | AOS-S | Gateway | 6 | |----------------------------------|-------------------------------|-----|--------|-------|---------| 7 | | **AAA Authentication Test** | - | `aaa_aps_test` | `aaa_cx_test` | ❌ | ❌ | 8 | | **Cable Test** | `cable_test` | ❌ | ✅ | ✅ | ❌ | 9 | | **Disconnect All Clients** | `disconnect_all_clients` | ❌ | ❌ | ❌ | ✅ | 10 | | **Disconnect All Users** | `disconnect_all_users` | ✅ | ❌ | ❌ | ❌ | 11 | | **Disconnect Users by SSID** | `disconnect_all_users_ssid` | ✅ | ❌ | ❌ | ❌ | 12 | | **Disconnect Client by MAC** | `disconnect_client_mac_addr` | ❌ | ❌ | ❌ | ✅ | 13 | | **Disconnect User by MAC** | `disconnect_user_mac_addr` | ✅ | ❌ | ❌ | ❌ | 14 | | **HTTP Test** | `http_test` | ✅ | ✅ | ✅ | ✅ | 15 | | **HTTPS Test** | - | `https_aps_test` | `https_cx_test` | ❌ | `https_gateways_test` | 16 | | **iPerf Performance Test** | `iperf_test` | ❌ | ❌ | ❌ | ✅ | 17 | | **Locate Device (LED Blink)** | `locate_device` | ✅ | ✅ | ✅ | ✅ | 18 | | **NSLookup Test** | `nslookup_test` | ✅ | ❌ | ❌ | ❌ | 19 | | **Ping Test** | - | `ping_aps_test` | `ping_cx_test` | `ping_aoss_test` | `ping_gateways_test` | 20 | | **PoE Bounce Test** | `poe_bounce_test` | ❌ | ✅ | ✅ | ✅ | 21 | | **Port Bounce Test** | `port_bounce_test` | ❌ | ✅ | ✅ | ✅ | 22 | | **Reboot Device** | `reboot_device` | ✅ | ✅ | ✅ | ✅ | 23 | | **ARP Table Retrieval** | `retrieve_arp_table_test` | ✅ | ❌ | ✅ | ✅ | 24 | | **Speed Test** | `speedtest_test` | ✅ | ❌ | ❌ | ❌ | 25 | | **TCP Test** | `tcp_test` | ✅ | ❌ | ❌ | ❌ | 26 | | **Traceroute Test** | - | `traceroute_aps_test` | `traceroute_cx_test` | `traceroute_aoss_test` | `traceroute_gateways_test` | 27 | | **List Show Commands** | `list_show_commands` | ✅ | ✅ | ✅ | ✅ | 28 | | **Run Show Command** | `run_show_command` | ✅ | ✅ | ✅ | ✅ | 29 | | **List Active Tasks** | `list_active_tasks` | ✅ | ✅ | ✅ | ✅ | 30 | 31 | **Legend:** 32 | 33 | - ✅ = Supported with the method named in the "Method Name" column 34 | - ❌ = Not supported 35 | 36 | For device-specific implementations (HTTPS Test, Ping Test), the specific method name is shown in the device column in the table above. 37 | 38 | ::: pycentral.troubleshooting.troubleshooting 39 | -------------------------------------------------------------------------------- /pycentral/utils/constants.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | """Constants used across the pycentral package. 5 | 6 | This module contains constant values for API endpoints, cluster URLs, 7 | and configuration mappings used throughout the pycentral SDK. 8 | 9 | Attributes: 10 | CLUSTER_BASE_URLS (dict[str, str]): Public HPE Aruba Networking Central cluster 11 | names with their corresponding API Base URLs. You can update this 12 | dictionary to add your own private cluster details. You can learn more about Base URLs here. 13 | 14 | SUPPORTED_CONFIG_PERSONAS (dict[str, str]): Supported New Central Device Personas 15 | and their corresponding API values. You can learn more about Device Personas here. 16 | 17 | AUTHENTICATION (dict[str, str]): Authentication endpoints for OAuth flows for token creation. 18 | 19 | GLP_URLS (dict[str, str]): GreenLake Platform (GLP) API URLs and endpoints. 20 | 21 | SCOPE_URLS (dict[str, str]): Scope-related API URLs for site and device 22 | organization. 23 | """ 24 | 25 | CLUSTER_BASE_URLS = { 26 | "EU-1": "https://ge1.api.central.arubanetworks.com", 27 | "EU-Central2": "https://ge2.api.central.arubanetworks.com", 28 | "EU-Central3": "https://ge3.api.central.arubanetworks.com", 29 | "US-1": "https://us1.api.central.arubanetworks.com", 30 | "US-2": "https://us2.api.central.arubanetworks.com", 31 | "US-WEST-4": "https://us4.api.central.arubanetworks.com", 32 | "US-WEST-5": "https://us5.api.central.arubanetworks.com", 33 | "US-East1": "https://us6.api.central.arubanetworks.com", 34 | "Canada-1": "https://cn1.api.central.arubanetworks.com", 35 | "APAC-1": "https://in.api.central.arubanetworks.com", 36 | "APAC-EAST1": "https://jp1.api.central.arubanetworks.com", 37 | "APAC-SOUTH1": "https://au1.api.central.arubanetworks.com", 38 | "Internal": "https://internal.api.central.arubanetworks.com", 39 | } 40 | 41 | SUPPORTED_CONFIG_PERSONAS = { 42 | "Campus AP": "CAMPUS_AP", 43 | "Micro Branch AP": "MICROBRANCH_AP", 44 | "Access Switch": "ACCESS_SWITCH", 45 | "Core Switch": "CORE_SWITCH", 46 | "Aggregation Switch": "AGG_SWITCH", 47 | "Mobility Gateway": "MOBILITY_GW", 48 | "Branch GW": "BRANCH_GW", 49 | "Bridge": "BRIDGE", 50 | "Hybrid NAC": "HYBRID_NAC", 51 | } 52 | 53 | AUTHENTICATION = {"OAUTH": "https://sso.common.cloud.hpe.com/as/token.oauth2"} 54 | 55 | GLP_URLS = { 56 | "BaseURL": "https://global.api.greenlake.hpe.com", 57 | "DEVICE": "devices", 58 | "SUBSCRIPTION": "subscriptions", 59 | "USER_MANAGEMENT": "users", 60 | "ASYNC": "async-operations", 61 | "SERVICE_MANAGER": "service-managers", 62 | "SERVICE_MANAGER_PROVISIONS": "service-manager-provisions", 63 | "SERVICE_MANAGER_BY_REGION": "per-region-service-managers", 64 | } 65 | 66 | SCOPE_URLS = { 67 | "SITE": "sites", 68 | "SITE_COLLECTION": "site-collections", 69 | "DEVICE": "devices", 70 | "DEVICE_GROUP": "device-collections", 71 | "ADD_SITE_TO_COLLECTION": "site-collection-add-sites", 72 | "REMOVE_SITE_FROM_COLLECTION": "site-collection-remove-sites", 73 | "HIERARCHY": "hierarchy", 74 | "SCOPE-MAPS": "scope-maps", 75 | } 76 | 77 | __all__ = [ 78 | "CLUSTER_BASE_URLS", 79 | "SUPPORTED_CONFIG_PERSONAS", 80 | "AUTHENTICATION", 81 | "GLP_URLS", 82 | "SCOPE_URLS", 83 | ] 84 | -------------------------------------------------------------------------------- /pycentral/utils/url_utils.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | versions = ["v1alpha1", "v1"] 5 | latest = "v1alpha1" 6 | glp_latest = "v1" 7 | 8 | CATEGORIES = { 9 | "configuration": { 10 | "value": "network-config", 11 | "type": "configuration", 12 | "latest": "v1alpha1", 13 | }, 14 | "monitoring": { 15 | "value": "network-monitoring", 16 | "type": "monitoring", 17 | "latest": "v1alpha1", 18 | }, 19 | "troubleshooting": { 20 | "value": "network-troubleshooting", 21 | "type": "troubleshooting", 22 | "latest": "v1alpha1", 23 | }, 24 | "subscriptions": {"value": "subscriptions", "type": "glp", "latest": "v1"}, 25 | "user_management": {"value": "identity", "type": "glp", "latest": "v1"}, 26 | "devices": {"value": "devices", "type": "glp", "latest": "v1"}, 27 | "service_catalog": { 28 | "value": "service-catalog", 29 | "type": "glp", 30 | "latest": "v1", 31 | }, 32 | } 33 | 34 | 35 | def get_prefix(category="configuration", version="latest"): 36 | """Generate URL prefix for a given category and version. 37 | 38 | Args: 39 | category (str, optional): API category name. 40 | version (str, optional): API version. 41 | 42 | Returns: 43 | (str): URL prefix in the format "category_value/version/". 44 | 45 | Raises: 46 | ValueError: If category is not supported or version is invalid. 47 | """ 48 | if category not in CATEGORIES: 49 | raise ValueError( 50 | f"Invalid category: {category}, Supported categories: {list(CATEGORIES.keys())}" 51 | ) 52 | category_value = CATEGORIES[category]["value"] 53 | if version == "latest": 54 | version = ( 55 | latest 56 | if not (CATEGORIES[category]["type"] == "glp") 57 | else glp_latest 58 | ) 59 | else: 60 | if version not in versions: 61 | raise ValueError( 62 | f"Invalid version: {version}. Allowed versions: {versions}" 63 | ) 64 | return f"{category_value}/{version}/" 65 | 66 | 67 | def generate_url(api_endpoint, category="configuration", version="latest"): 68 | """Generate complete API URL for a given endpoint, category, and version. 69 | 70 | Args: 71 | api_endpoint (str): The API endpoint path to append to the URL. 72 | category (str, optional): API category name. 73 | version (str, optional): API version. 74 | 75 | Returns: 76 | (str): Complete API URL in the format "category[value]/version/api_endpoint". 77 | 78 | Raises: 79 | ValueError: If category is not supported or version is invalid. 80 | TypeError: If api_endpoint is not a string. 81 | """ 82 | if category not in CATEGORIES: 83 | raise ValueError( 84 | f"Invalid category: {category}, Supported categories: {list(CATEGORIES.keys())}" 85 | ) 86 | if api_endpoint is not None and not isinstance(api_endpoint, str): 87 | raise TypeError( 88 | f"Invalid type: {type(api_endpoint)} for api_endpoint, expected str" 89 | ) 90 | category_value = CATEGORIES[category]["value"] 91 | if version == "latest": 92 | version = ( 93 | latest 94 | if not (CATEGORIES[category]["type"] == "glp") 95 | else glp_latest 96 | ) 97 | else: 98 | if version not in versions: 99 | raise ValueError( 100 | f"Invalid version: {version}. Allowed versions: {versions}" 101 | ) 102 | return f"{category_value}/{version}/{api_endpoint}" 103 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyCentral Documentation 2 | 3 | PyCentral is a Python SDK that makes it easier to interact with HPE Aruba Networking Central and the HPE GreenLake Platform (GLP) through REST APIs. Instead of building and managing low-level HTTP requests, developers can: 4 | 5 | - Authenticate securely 6 | - Configure and manage devices and subscriptions 7 | - Monitor performance and collect analytics 8 | - Run troubleshooting workflows 9 | 10 | PyCentral handles authentication, request formatting, and error handling, while exposing simple Python functions. This lets you configure, monitor, and troubleshoot your network without dealing with raw REST API calls. 11 | 12 | ## Overview 13 | 14 | PyCentral(v2) is the latest version of the SDK, designed for compatibility and simplicity: 15 | 16 | - Backwards Compatible → Works with PyCentral v1 scripts, with no breaking changes. 17 | - Multi-Platform Support → Works across Classic Central, New Central, and GLP. 18 | - Simplified Token Management → Built-in OAuth2.0 support (no manual refresh needed). 19 | - Simplified Automation → Modules for configuration, monitoring, devices, subscriptions, and troubleshooting. 20 | 21 | ## Versions 22 | 23 | PyCentral-v2 is currently in pre-release, and we welcome feedback [here](https://github.com/aruba/pycentral/issues) as we continue improving it. 24 | Today, there are two versions of PyCentral, each designed for different versions of HPE Aruba Networking Central 25 | 26 | | Version | Supports | Notes | 27 | | :------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- | :--------------------------- | 28 | | [v1](https://pypi.org/project/pycentral/) | HPE Aruba Networking Central (Classic Central) | Legacy Version | 29 | | [v2(pre-release)](https://pypi.org/project/pycentral/2.0a12/) | HPE Aruba Networking Central(new Central), GLP, HPE Aruba Networking Central (Classic Central) | Backwards compatible with v1 | 30 | 31 | ## Quick Example 32 | 33 | ```python 34 | from pycentral import NewCentralBase 35 | 36 | central_credential = { 37 | "new_central": { 38 | "base_url": "https://us5.api.central.arubanetworks.com", 39 | "client_id": "client_id", 40 | "client_secret": "client_secret", 41 | } 42 | } 43 | 44 | # Initialize NewCentralBase class with the token credentials for Central/GLP 45 | central_conn = NewCentralBase(token_info=central_credential) 46 | 47 | # Central API Call 48 | central_resp = central_conn.command( 49 | api_method="GET", api_path="network-monitoring/v1alpha1/devices" 50 | ) 51 | 52 | # Check response status and print results or error message 53 | if central_resp["code"] == 200: 54 | print(central_resp["msg"]) 55 | else: 56 | print(f"Error - Response code {central_resp['code']}") 57 | print(central_resp["msg"]) 58 | 59 | ``` 60 | 61 | ## Getting Started 62 | 63 | - [Installation](getting-started/installation.md) - Install PyCentral 64 | - [Authentication](getting-started/authentication.md) - Configure credentials 65 | - [Quick Start](getting-started/quickstart.md) - Basic usage examples 66 | 67 | ## Module Documentation 68 | 69 | Browse the complete module documentation: 70 | 71 | - [Base Module](modules/base.md) - Core connection classes 72 | - [Exceptions](modules/scopes.md) - Exceptions used throughout PyCentral 73 | - [GLP](modules/glp.md) - HPE GreenLake Platform management 74 | - [Monitoring](modules/new_monitoring.md) - Monitoring modules 75 | - [Profiles](modules/profiles.md) - Configuration profile management 76 | - [Scopes](modules/scopes.md) - Scope management modules 77 | - [Troubleshooting](modules/troubleshooting.md) - Troubleshooting Modules 78 | - [Utils](modules/utils.md) - Utilities used throughout PyCentral 79 | - [Classic](modules/classic.md) - Legacy PyCentral module reference 80 | 81 | ## License 82 | 83 | MIT License - Copyright © 2025 Hewlett Packard Enterprise Development LP 84 | -------------------------------------------------------------------------------- /pycentral/utils/glp_utils.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .base_utils import console_logger 5 | 6 | import time 7 | 8 | DEVICE_LIMIT = 20 9 | SUB_LIMIT = 5 10 | 11 | logger = console_logger("RATE LIMIT CHECK") 12 | 13 | 14 | def rate_limit_check(input_array, input_size_limit, rate_per_minute): 15 | """Check and handle rate limiting for API requests. 16 | 17 | Splits the input array into smaller chunks and calculates wait time 18 | to prevent rate limit errors. 19 | 20 | Args: 21 | input_array (list): Array of items to process. 22 | input_size_limit (int): Maximum size of each chunk. 23 | rate_per_minute (int): Maximum number of requests allowed per minute. 24 | 25 | Returns: 26 | (tuple): A tuple containing: 27 | 28 | - queue (list): List of sub-arrays split by input_size_limit. 29 | - wait_time (float): Seconds to wait between requests (0 if no wait needed). 30 | """ 31 | print("Attempting to bypass rate limit") 32 | queue = [] 33 | wait_time = [] 34 | 35 | for i in range(0, len(input_array), input_size_limit): 36 | sub_array = input_array[i : i + input_size_limit] 37 | queue.append(sub_array) 38 | 39 | if len(queue) > rate_per_minute: 40 | wait_time = 60 / rate_per_minute 41 | print( 42 | "Array size exceeded,", 43 | wait_time, 44 | "second wait timer implemented per request to prevent errors", 45 | ) 46 | print("Loading ...") 47 | else: 48 | wait_time = 0 49 | 50 | return queue, wait_time 51 | 52 | 53 | def check_progress(conn, id, module_instance, limit=None): 54 | """Check progress of an async GLP API operation. 55 | 56 | Polls the status of an asynchronous operation until it completes, 57 | times out, or fails. 58 | 59 | Args: 60 | conn (NewCentralBase): PyCentral base connection object. 61 | id (str): Async transaction ID. 62 | module_instance (object): Instance of the module class (Devices or Subscriptions). 63 | limit (int, optional): Rate limit for the module. If None, uses default 64 | based on module type (20 for Devices, 5 for Subscriptions). 65 | 66 | Returns: 67 | (tuple): A tuple containing: 68 | 69 | - success (bool): True if operation succeeded, False otherwise. 70 | - status (dict): API response with operation status details. 71 | 72 | Raises: 73 | ValueError: If module_instance is not an instance of Devices or Subscriptions. 74 | """ 75 | if limit is None: 76 | if module_instance.__class__.__name__ == "Devices": 77 | limit = DEVICE_LIMIT 78 | elif module_instance.__class__.__name__ == "Subscriptions": 79 | limit = SUB_LIMIT 80 | else: 81 | raise ValueError( 82 | "module_instance must be an instance of Devices or Subscription" 83 | ) 84 | 85 | updated = False 86 | while not updated: 87 | status = module_instance.get_status(conn, id) 88 | if status["code"] != 200: 89 | conn.logger.error( 90 | f"Bad request for get async status with transaction {id}!" 91 | ) 92 | return (False, status) 93 | elif status["msg"]["status"] == "SUCCEEDED": 94 | updated = True 95 | return (True, status) 96 | elif status["msg"]["status"] == "TIMEOUT": 97 | updated = True 98 | conn.logger.error( 99 | f"Async operation timed out for transaction {id}!" 100 | ) 101 | return (False, status) 102 | elif status["msg"]["status"] == "FAILED": 103 | updated = True 104 | conn.logger.error(f"Async operation failed for transaction {id}!") 105 | return (False, status) 106 | else: 107 | # Sleep time calculated by async rate limit. 108 | sleep_time = 60 / limit 109 | time.sleep(sleep_time) 110 | -------------------------------------------------------------------------------- /docs/getting-started/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | With authentication set-up, let's see how to use PyCentral to interact with New Central. 3 | 4 | ## Basic Setup 5 | Before running a script, create a `token.yaml`. This file provides the authentication credentials required by the SDK to make API calls to New Central. 6 | You will need to populate it with the required credentials as follows: 7 | 8 | ```yaml title="token.yaml" 9 | new_central: 10 | base_url: 11 | client_id: 12 | client_secret: 13 | ``` 14 | 15 | Find your base URL: 16 | 17 | | Cluster | Base URL | 18 | | :----------------------- | :------------------------------------- | 19 | | EU-1 (eu) | de1.api.central.arubanetworks.com | 20 | | EU-Central2 (eucentral2) | de2.api.central.arubanetworks.com | 21 | | EU-Central3 (eucentral3) | de3.api.central.arubanetworks.com | 22 | | US-1 (prod) | us1.api.central.arubanetworks.com | 23 | | US-2 (central-prod2) | us2.api.central.arubanetworks.com | 24 | | US-WEST-4 (uswest4) | us4.api.central.arubanetworks.com | 25 | | US-WEST-5 (uswest5) | us5.api.central.arubanetworks.com | 26 | | US-East1 (us-east-1) | us6.api.central.arubanetworks.com | 27 | | Canada-1 (starman) | ca1.api.central.arubanetworks.com | 28 | | APAC-1 (apac) | in.api.central.arubanetworks.com | 29 | | APAC-EAST1 (apaceast) | jp1.api.central.arubanetworks.com | 30 | | APAC-SOUTH1 (apacsouth) | au1.api.central.arubanetworks.com | 31 | | Internal (internal) | internal.api.central.arubanetworks.com | 32 | 33 | In the above `token.yaml` file, only New Central API credentials are provided as the quickstart example script will only be making API calls to New Central. If your script needs to make API calls to GLP, you will need to include your GLP API client credentials or token. 34 | 35 | ### Example: Python Script to Get Devices 36 | 37 | The python script below demonstrates: 38 | 39 | 1. Loading API Client Credentials from [`token.yaml`](doc:pycentral-quickstart-guide#prerequisites) file. 40 | 2. Initialize the PyCentral SDK's connection to New Central 41 | 3. Fetch a list of devices from New Central 42 | 43 | Once you have the `token.yaml` file ready, you can run the following Python script: 44 | 45 | ```python title="get_devices.py" 46 | import os 47 | from pycentral import NewCentralBase 48 | 49 | # Validate token file exists 50 | token_file = "token.yaml" 51 | if not os.path.exists(token_file): 52 | raise FileNotFoundError( 53 | f"Token file '{token_file}' not found. Please provide a valid token file." 54 | ) 55 | 56 | # Initialize NewCentralBase class with the token credentials for New Central/GLP 57 | new_central_conn = NewCentralBase(token_info=token_file) 58 | 59 | # New Central API Call 60 | new_central_resp = new_central_conn.command( 61 | api_method="GET", api_path="network-monitoring/v1alpha1/devices" 62 | ) 63 | 64 | # Check response status and print results or error message 65 | if new_central_resp["code"] == 200: 66 | print(new_central_resp["msg"]) 67 | else: 68 | print(f"Error - Response code {new_central_resp['code']}") 69 | print(new_central_resp["msg"]) 70 | 71 | ``` 72 | 73 | ### Running the Script 74 | 75 | Save the code above as `get_devices.py` then execute it using: 76 | 77 | ``` 78 | python get_devices.py 79 | ``` 80 | 81 | If successful, you'll see a list of devices retrieved from New Central. 82 | 83 | *** 84 | 85 | The above example serves as a foundation for writing your own custom scripts. As you explore [more API endpoints](https://developer.arubanetworks.com/new-central/reference/) , you can extend this approach to automate tasks, gather monitoring statistics, or integrate with your existing tools. The PyCentral SDK is designed to help you script confidently and build workflows that fit your specific needs. 86 | 87 | ## Next Steps 88 | 89 | - Explore the [Modules Reference](../modules/base.md) 90 | - Explore our full catalog of Central guides on our [Developer Hub](https://developer.arubanetworks.com/new-central/docs/about) 91 | - Explore other python workflows on our [GitHub](https://github.com/aruba/central-python-workflows) 92 | -------------------------------------------------------------------------------- /docs/getting-started/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | PyCentral provides a flexible authentication system allowing users to make API calls to New Central, GLP, and/or Classic Central. You only need to provide credentials for the platform(s) you want PyCentral to interact with. 4 | 5 | ## Choosing an Authentication Method 6 | 7 | PyCentral supports two authentication methods for New Central & GLP: 8 | 9 | | Method | Best For | Token Expiry | 10 | | :------------------------------------------ | :-------------------------------- | :---------------------------------------------------------------------- | 11 | | **Client ID & Client Secret** (Recommended) | Automation & long-running scripts | Auto-renews - New tokens are generated automatically by SDK upon expiry | 12 | | **Access Tokens** | Quick tests & one-time API calls | Manually need to update token upon expiry | 13 | 14 | If you want a hassle-free setup use **Client ID & Secret**, the SDK will automatically generate a new token whenever required. 15 | If you're just testing an API, using **Access Token** is fine 16 | 17 | ## New Central 18 | 19 | **Base URL** 20 | You can find your Central account's base URL from the table here: 21 | 22 | | Cluster | Base URL | 23 | | :----------------------- | :------------------------------------- | 24 | | EU-1 (eu) | de1.api.central.arubanetworks.com | 25 | | EU-Central2 (eucentral2) | de2.api.central.arubanetworks.com | 26 | | EU-Central3 (eucentral3) | de3.api.central.arubanetworks.com | 27 | | US-1 (prod) | us1.api.central.arubanetworks.com | 28 | | US-2 (central-prod2) | us2.api.central.arubanetworks.com | 29 | | US-WEST-4 (uswest4) | us4.api.central.arubanetworks.com | 30 | | US-WEST-5 (uswest5) | us5.api.central.arubanetworks.com | 31 | | US-East1 (us-east-1) | us6.api.central.arubanetworks.com | 32 | | Canada-1 (starman) | ca1.api.central.arubanetworks.com | 33 | | APAC-1 (apac) | in.api.central.arubanetworks.com | 34 | | APAC-EAST1 (apaceast) | jp1.api.central.arubanetworks.com | 35 | | APAC-SOUTH1 (apacsouth) | au1.api.central.arubanetworks.com | 36 | | Internal (internal) | internal.api.central.arubanetworks.com | 37 | 38 | Ensure you use the correct Base URL in your API calls. Using the wrong Base URL will result in failed requests. 39 | 40 | **API Credentials** (Choose one): 41 | 42 | - Client ID & Client Secret _(Recommended)_ 43 | The SDK automatically generates new tokens when they expire, so you don't have to manage them manually. Learn how to create your credentials [here](https://developer.arubanetworks.com/new-central/docs/generating-and-managing-access-tokens#create-client-credentials). 44 | - Access Token 45 | Manually, retrieve an access token. Learn how to retreive an access token [here](https://developer.arubanetworks.com/new-central/docs/generating-and-managing-access-tokens#generate-access-token). **(Tokens expire in 2 hours)** 46 | 47 | ## GLP 48 | 49 | **No Base URL Required** 50 | **API Credentials** (Choose one): 51 | 52 | - Client ID & Client Secret _(Recommended)_ 53 | The SDK automatically generates new tokens when they expire, so you don't have to manage them manually. Get your credentials [here](https://developer.greenlake.hpe.com/docs/greenlake/guides/public/authentication/authentication/#creating-a-personal-api-client). 54 | - Access Token 55 | Manually, retrieve an access token [here](https://developer.greenlake.hpe.com/docs/greenlake/guides/public/authentication/authentication/#generating-an-access-token)** (Tokens expire in 15 mins)** 56 | 57 | ## Classic Central 58 | 59 | Classic Central suppports authentication methods for access tokens or generating through OAUTH APIs 60 | 61 | **Base URL** 62 | You can find your Classic Central account's base URL from the table provided within this guide [here](https://developer.arubanetworks.com/central/docs/api-oauth-access-token#table-domain-urls-for-api-gateway-access). 63 | 64 | **API Credentials** (Choose one): 65 | 66 | - OAUTH 67 | 68 | Manually, use the pycentral.workflows_utils.get_conn_from_file() function to generate new tokens when they expire. View our Classic Central guide for OAUTH Authentication with PyCentral [here](https://developer.arubanetworks.com/central/docs/python-using-api-sdk#arubacentralbase-class-requirements). 69 | 70 | - Access Token 71 | Manually, retrieve an access token. Learn how to retreive an access token [here](https://developer.arubanetworks.com/central/docs/api-gateway-creating-application-token). **(Tokens expire in 2 hours)** 72 | 73 | ## Next Steps 74 | 75 | - [Quick Start](quickstart.md) - Start using PyCentral 76 | - [Module Reference](../modules/base.md) - Explore modules 77 | -------------------------------------------------------------------------------- /pycentral/new_monitoring/sites.py: -------------------------------------------------------------------------------- 1 | from ..utils.monitoring_utils import execute_get, simplified_site_resp 2 | from ..exceptions import ParameterError 3 | 4 | # Sites doesn't really abide by the same pattern as other monitor types 5 | # Should we keep? 6 | MONITOR_TYPE = "sites" 7 | SITE_LIMIT = 100 8 | 9 | 10 | class MonitoringSites: 11 | @staticmethod 12 | def get_all_sites(central_conn, return_raw_response=False): 13 | """ 14 | Retrieve all sites information including health details, handling pagination. 15 | 16 | Args: 17 | central_conn (NewCentralBase): Central connection object. 18 | return_raw_response (bool, optional): If True, return the raw API payloads. Defaults to False. 19 | 20 | Returns: 21 | (list[dict]): List of site records. If return_raw_response is False, each site response is simplified via simplified_site_resp. 22 | """ 23 | sites = [] 24 | total_sites = None 25 | limit = SITE_LIMIT 26 | offset = 0 27 | while True: 28 | response = MonitoringSites.get_sites( 29 | central_conn, limit=limit, offset=offset 30 | ) 31 | if total_sites is None: 32 | total_sites = response.get("total", 0) 33 | sites.extend(response["items"]) 34 | if len(sites) == total_sites: 35 | break 36 | offset += limit 37 | if not return_raw_response: 38 | sites = [simplified_site_resp(site) for site in sites] 39 | return sites 40 | 41 | @staticmethod 42 | def get_sites(central_conn, limit=SITE_LIMIT, offset=0): 43 | """ 44 | Retrieve a single page of site health information. It returns details such as devices, clients, critical alerts with count, along with their respective health and health reasons for each site. 45 | 46 | This method makes an API call to the following endpoint - `GET network-monitoring/v1alpha1/sites-health` 47 | 48 | Args: 49 | central_conn (NewCentralBase): Central connection object. 50 | limit (int, optional): Number of entries to return (default is 100). 51 | offset (int, optional): Number of entries to skip for pagination (default is 0). 52 | 53 | Returns: 54 | (dict): Raw API response for the requested page (typically contains 'items' and 'total'). 55 | """ 56 | params = {"limit": limit, "offset": offset} 57 | path = "sites-health" 58 | return execute_get(central_conn, endpoint=path, params=params) 59 | 60 | @staticmethod 61 | def list_sites_device_health(central_conn, limit=100, offset=0): 62 | """ 63 | Retrieve per-site device health statistics. It returns the number of poor, fair, and good performing devices for each site. 64 | 65 | This method makes an API call to the following endpoint - `GET network-monitoring/v1alpha1/sites-device-health` 66 | 67 | Args: 68 | central_conn (NewCentralBase): Central connection object. 69 | limit (int, optional): Number of entries to return (default is 100). 70 | offset (int, optional): Number of entries to skip for pagination (default is 0). 71 | 72 | Returns: 73 | (dict): Raw API response containing device health counts per site. 74 | """ 75 | params = {"limit": limit, "offset": offset} 76 | path = "sites-device-health" 77 | return execute_get(central_conn, endpoint=path, params=params) 78 | 79 | # need to include logic to handle params/filters/sorting 80 | @staticmethod 81 | def list_site_information(central_conn, site_id, limit=100, offset=0): 82 | """ 83 | Retrieve detailed health information for a specific site. It returns details such as devices, clients, critical alerts with count, along with their respective health and health reasons. 84 | 85 | This method makes an API call to the following endpoint - `GET network-monitoring/v1alpha1/site-health/{site_id}` 86 | 87 | Args: 88 | central_conn (NewCentralBase): Central connection object. 89 | site_id (int): Identifier of the site to query. 90 | limit (int, optional): Number of entries to return (default is 100). 91 | offset (int, optional): Number of entries to skip for pagination (default is 0). 92 | 93 | Returns: 94 | (dict): Raw API response with site health details. 95 | 96 | Raises: 97 | ParameterError: If site_id is missing or not an integer. 98 | """ 99 | if not site_id or not isinstance(site_id, int): 100 | raise ParameterError("site_id is required and must be an integer") 101 | params = {"limit": limit, "offset": offset} 102 | path = f"site-health/{site_id}" 103 | return execute_get(central_conn, endpoint=path, params=params) 104 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # 1.4.1 2 | 3 | ## Notable Changes 4 | * Support Preserve Configuration Overrides in Device Movement by @fiveghz in #43 5 | * Version fixes by @KarthikSKumar98 in #45 6 | * Packages that are installed along with PyCentral will have version number associated with. 7 | * requests - v2.31.0 8 | * PyYAML - v6.0.1 9 | * urllib3 - 2.2.2 10 | * certifi - 2024.7.4 11 | ## New Contributors 12 | * @fiveghz made their first contribution in [#43](https://github.com/aruba/pycentral/pull/43) 13 | 14 | Full Changelog: [v1.4...v1.4.1](https://github.com/aruba/pycentral/compare/v1.4...v1.4.1) 15 | ## Known Issues 16 | * No known issues. 17 | 18 | 19 | 20 | 21 | 22 | # 1.4 23 | 24 | ## Notable Changes 25 | * Updated minimum required version of python from 3.6 to 3.8 26 | * Add the ability to consume cluster_name as a parameter instead of base_url 27 | * Added ability to enable and disable WLANs in Central UI Group 28 | * enable_wlan 29 | * disable_wlan 30 | * Error handling when invalid base_url is passed 31 | * Resolved minor bugs 32 | 33 | ## Known Issues 34 | * No known issues. 35 | 36 | Full Changelog: [v1.3...v1.4](https://github.com/aruba/pycentral/compare/v1.3...v1.4) 37 | 38 | # 1.3 39 | 40 | ## Notable Changes 41 | * Added MSP Module. These are some of the functions that are implemented in this module - 42 | * Customer Management Functions 43 | * get_customers 44 | * get_all_customers 45 | * create_customer 46 | * update_customer 47 | * delete_customer 48 | * get_customer_details 49 | * get_customer_id 50 | * Device Management Functions 51 | * get_customer_devices_and_subscriptions 52 | * assign_devices_to_customers 53 | * unassign_devices_from_customers 54 | * unassign_all_customer_device 55 | * get_msp_devices_and_subscriptions 56 | * get_msp_all_devices_and_subscriptions 57 | * Other Functions 58 | * get_msp_id 59 | * get_msp_users 60 | * get_customer_users 61 | * get_country_code 62 | * get_country_codes_list 63 | * get_msp_resources 64 | * edit_msp_resources 65 | * get_customers_per_group 66 | 67 | Full Changelog: [v1.2.1...v1.3](https://github.com/aruba/pycentral/compare/v1.2.1...v1.3) 68 | 69 | ## Known Issues 70 | * No known issues. 71 | 72 | # 1.2.1 73 | 74 | ## Notable Changes 75 | * Fixed bug with add_device function 76 | 77 | ## Known Issues 78 | * No known issues. 79 | 80 | # 1.2 81 | 82 | ## Notable Changes 83 | * Added new Device Inventory functions - Archive Devices, Unarchive Devices, Add Devices 84 | 85 | ## Known Issues 86 | * No known issues. 87 | 88 | ## PRs in this release 89 | * Added device inventory functions by [@KarthikSKumar98](https://github.com/KarthikSKumar98) in [#35](https://github.com/aruba/pycentral/pull/35) 90 | * Style linting pycentral modules by[@KarthikSKumar98](https://github.com/KarthikSKumar98) in [#36](https://github.com/aruba/pycentral/pull/36) 91 | 92 | Full Changelog: [v1.1.1...v1.2](https://github.com/aruba/pycentral/compare/v1.1.1...v1.2) 93 | 94 | # 1.1.1 95 | 96 | ## Notable Changes 97 | * Updated README links 98 | 99 | ## Known Issues 100 | * No known issues. 101 | 102 | # 1.1.0 103 | 104 | ## Notable Changes 105 | * Added APConfiguration Class to Configuration Module 106 | 107 | ## Known Issues 108 | * No known issues. 109 | 110 | # 1.0.0 111 | 112 | ## Notable Changes 113 | * Added wait & retry logic when Aruba Central's Per-Second API rate-limit is hit 114 | * Added log messages when Aruba Central's Per-Day API Rate-limit is exhausted 115 | * Added new module for device_inventory APIs 116 | * Added WLAN class to configuration module 117 | * Merged PRs and resolved GitHub issues: 118 | - PR #16 : Fixing multiple devices to site bug 119 | - PR #19 : Added ability to associate/unassociate multiple devices to a site 120 | - PR #20 : Added New Device Inventory Module 121 | - PR #22 : Added Rate Limit Log Messages 122 | - PR #23 : Fixed Rate Limit Bugs 123 | - PR #25 : Licensing API Bug Fixes 124 | - PR #28, #29 : Added WLAN class to Configuration module 125 | 126 | ## Known Issues 127 | * No known issues. 128 | 129 | # 0.0.3 130 | 131 | ## Notable Changes 132 | * Quick fix on deprecated pip module usage. 133 | * Merged PRs and resolved GitHub issues: 134 | - PR #10 : remove dep on _internal function from pip module 135 | - Issue #9: pip 21.3 just posted 2 days ago breaks pycentral in the config file and passed to pycentral 136 | 137 | ## Known Issues 138 | * No known issues. 139 | 140 | # 0.0.2 141 | 142 | ## Notable Changes 143 | * Added modules for firmware_management, rapids, topology and user_management 144 | * Major update to existing config_apsettings_from_csv.py workflow. It accepts CSV file downloaded from the Central UI group. This workflow also generates a new CSV file with failed APs. CSV file format from the previous version is not backward compatible. 145 | * Fixes and improvement to existing modules, utilities and the documentation 146 | * Merged PRs and resolved GitHub issues: 147 | - PR #6: example AP rename code always terminates with an error 148 | - PR #2: fix url concat in command() 149 | - PR #1: Added the ability for multiple Aruba Central account's in the config file and passed to pycentral 150 | 151 | ## Known Issues 152 | * No known issues. 153 | 154 | # 0.0.1 155 | 156 | ## Notable Changes 157 | * This is the initial release for the Aruba Central Python SDK, sample scripts, and workflows. 158 | 159 | ## Known Issues 160 | * No known issues. 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyCentral (Python SDK for HPE Aruba Networking Central) 2 | 3 | > **⚠️ Pre-release Notice** 4 | > This is a **pre-release version** of PyCentral-v2, and the features are constantly being updated as the APIs evolve. This version of the SDK allows you to make API calls to New Central, GLP, and Classic Central. 5 | > If you are looking for the stable version of PyCentral (v1), it is still available and fully supported. PyCentral-v1, which only supports Classic Central, can be found [here](https://pypi.org/project/pycentral/). 6 | 7 | A Python SDK for interacting with **HPE Aruba Networking Central** via REST APIs. 8 | Automate onboarding, configuration, monitoring, and management for: 9 | - **New Central** 10 | - **HPE GreenLake Platform (GLP)** 11 | - **Classic Central** (via the `classic` module for backward compatibility) 12 | 13 | Upgrading to this pre-release version will not break PyCentral-v1 code. All the PyCentral-v1 code has been moved to the `classic` folder within the PyCentral directory, ensuring backward compatibility. You can find Classic Central PyCentral Documentation [here](#classic-central). 14 | 15 | --- 16 | 17 | ## Install 18 | 19 | To install the latest pre-release version of PyCentral, use the following command: 20 | 21 | ```bash 22 | pip3 install --pre pycentral 23 | ``` 24 | 25 | If you already have PyCentral-v1 and would like to upgrade to the pre-release version, use the following command: 26 | 27 | ```bash 28 | pip3 install --upgrade --pre pycentral 29 | ``` 30 | 31 | --- 32 | 33 | 34 | ## Authentication 35 | 36 | ### New Central 37 | You will need: 38 | - **Base URL or Cluster Name**: Base URL is the API Gateway URL for your New Central account based on the geographical cluster of your account on the HPE GreenLake Platform. You can find the base URL or cluster name of your New Central account's API Gateway from the table [here](https://developer.arubanetworks.com/new-central/docs/getting-started-with-rest-apis#base-urls). 39 | - **Client ID and Client Secret**: These credentials are required to generate an access token to authenticate API requests. You can obtain them by creating a Personal API Client for your New Central Account. Follow the detailed steps in the [Create Client Credentials documentation](https://developer.arubanetworks.com/new-central/docs/generating-and-managing-access-tokens#create-client-credentials). 40 | 41 | ```yaml 42 | new_central: 43 | base_url: 44 | client_id: 45 | client_secret: 46 | ``` 47 | 48 | ### HPE GreenLake Platform (GLP) 49 | If you are working with HPE GreenLake APIs, authentication is slightly different: 50 | - GLP does not require a Base URL. 51 | - You only need the **Client ID & Client Secret** for the HPE GreenLake Platform. 52 | ```yaml 53 | glp: 54 | client_id: 55 | client_secret: 56 | ``` 57 | 58 | --- 59 | 60 | ## Example 61 | 62 | Before running the script, create a `token.yaml` file in the same directory and populate it with the required credentials as follows: 63 | 64 | ```yaml 65 | new_central: 66 | base_url: 67 | client_id: 68 | client_secret: 69 | glp: 70 | client_id: 71 | client_secret: 72 | ``` 73 | 74 | Once you have the `token.yaml` file ready, you can run the following Python script: 75 | 76 | ```python 77 | import os 78 | from pycentral import NewCentralBase 79 | 80 | # Validate token file exists 81 | token_file = "token.yaml" 82 | if not os.path.exists(token_file): 83 | raise FileNotFoundError( 84 | f"Token file '{token_file}' not found. Please provide a valid token file." 85 | ) 86 | 87 | # Initialize NewCentralBase class with the token credentials for New Central/GLP 88 | new_central_conn = NewCentralBase( 89 | token_info=token_file, 90 | ) 91 | 92 | # New Central API Call 93 | new_central_resp = new_central_conn.command( 94 | api_method="GET", api_path="network-monitoring/v1alpha1/aps" 95 | ) 96 | 97 | print(new_central_resp) 98 | print() 99 | # GLP API Call 100 | glp_resp = new_central_conn.command( 101 | api_method="GET", api_path="devices/v1/devices", app_name="glp" 102 | ) 103 | 104 | print(glp_resp) 105 | ``` 106 | 107 | Run the script using the following command: 108 | 109 | ```bash 110 | python3 demo.py 111 | ``` 112 | ## Compatibility 113 | 114 | - **v2** supports **New Central** and **GLP**. 115 | - **Classic Central (v1)** remains in the `classic` module for backward compatibility. 116 | - Existing v1 code will continue to work without changes. 117 | 118 | --- 119 | 120 | ## Documentation 121 | - [Documentation - Getting Started](https://developer.arubanetworks.com/new-central/docs/getting-started-with-python) 122 | - [Documentation - Quickstart Guide](https://developer.arubanetworks.com/new-central/docs/pycentral-quickstart-guide) 123 | 124 | --- 125 | 126 | ## Classic Central 127 | 128 | The Classic Central functionality is still fully supported by the SDK and has been moved to a dedicated documentation page. For information on using the SDK with Classic Central, including authentication methods, API calls, and workflow examples, please see the [Classic Central Documentation](https://github.com/aruba/pycentral/blob/master/README.md). 129 | 130 | ### Documentation 131 | 132 | - Python package documentation 133 | 134 | ### **Use-Cases and Workflows** 135 | - HPE Aruba Networking Developer Hub 136 | - central-python-workflows 137 | -------------------------------------------------------------------------------- /pycentral/glp/user_management.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from ..utils import GLP_URLS, generate_url 5 | 6 | 7 | class UserMgmt(object): 8 | def get_users(self, conn, filter=None, limit=300, offset=0): 9 | """Retrieve users that match given filters. 10 | 11 | All users are returned when no filters are provided. The Get users API can be filtered by: 12 | id, username, userStatus, createdAt, updatedAt, lastLogin. 13 | 14 | Args: 15 | conn (NewCentralBase): pycentral base connection object. 16 | filter (str, optional): Filter data using a subset of OData 4.0 and return only the subset of 17 | resources that match the filter. Examples: 18 | 19 | - Filter with id: filter=id eq '7600415a-8876-5722-9f3c-b0fd11112283' 20 | - Filter with username: filter=username eq 'user@example.com' 21 | - Filter with userStatus: filter=userStatus neq 'UNVERIFIED' 22 | - Filter with createdAt: filter=createdAt gt '2020-09-21T14:19:09.769747' 23 | - Filter with updatedAt: filter=updatedAt gt '2020-09-21T14:19:09.769747' 24 | - Filter with lastLogin: filter=lastLogin lt '2020-09-21T14:19:09.769747' 25 | limit (int, optional): Specify the maximum number of entries per page. Maximum value accepted is 300. Range: 1-300. 26 | offset (int, optional): Specify pagination offset. Defines how many pages to skip before returning results. 27 | 28 | Returns: 29 | (dict): API response containing users. 30 | """ 31 | conn.logger.info("Getting users in GLP workspace") 32 | path = generate_url( 33 | GLP_URLS["USER_MANAGEMENT"], category="user_management" 34 | ) 35 | 36 | params = { 37 | "limit": limit, 38 | "offset": offset, 39 | } 40 | if filter: 41 | params["filter"] = filter 42 | 43 | resp = conn.command( 44 | api_method="GET", api_path=path, api_params=params, app_name="glp" 45 | ) 46 | return resp 47 | 48 | def get_user(self, conn, email=None, id=None): 49 | """Get a user from a workspace. 50 | 51 | Args: 52 | conn (NewCentralBase): pycentral base connection object. 53 | email (str, optional): Account username (email address). 54 | id (str, optional): Target user ID. 55 | 56 | Returns: 57 | (dict): Response as provided by 'command' function in NewCentralBase. 58 | """ 59 | conn.logger.info("Getting a user in GLP workspace") 60 | if email: 61 | id = self.get_user_id(conn, email)[1] 62 | 63 | path = generate_url( 64 | f"{GLP_URLS['USER_MANAGEMENT']}/{id}", category="user_management" 65 | ) 66 | 67 | resp = conn.command("GET", path, "glp") 68 | if resp["code"] == 200: 69 | conn.logger.info("Get user successful!") 70 | else: 71 | conn.logger.error("Get user failed!") 72 | return resp 73 | 74 | def get_user_id(self, conn, email): 75 | """Get user ID in a GLP workspace by email. 76 | 77 | Args: 78 | conn (NewCentralBase): pycentral base connection object. 79 | email (str): Account username (email address). 80 | 81 | Returns: 82 | (tuple(bool, str)): Tuple of two elements. First element returns True if user ID is found, 83 | else False. The second element is a GLP user ID if found, else an error message. 84 | """ 85 | 86 | filter = f"username eq '{email}'" 87 | resp = self.get_users(conn, filter=filter) 88 | if resp["code"] != 200: 89 | return (resp, (False, "Bad request for get_id")) 90 | elif resp["msg"]["count"] == 0: 91 | return (False, "Email not found") 92 | else: 93 | return (True, resp["msg"]["items"][0]["id"]) 94 | 95 | def delete_user(self, conn, email=None, user_id=None): 96 | """Delete a user from a workspace. 97 | 98 | Args: 99 | conn (NewCentralBase): pycentral base connection object. 100 | email (str, optional): Account username (email address). 101 | user_id (str, optional): Target user ID. 102 | 103 | Returns: 104 | (dict): API response from the delete operation. 105 | """ 106 | conn.logger.info("Deleting a user in GLP workspace") 107 | if email: 108 | user_id = self.get_user_id(conn, email)[1] 109 | 110 | path = generate_url( 111 | f"{GLP_URLS['USER_MANAGEMENT']}/{user_id}", 112 | category="user_management", 113 | ) 114 | resp = conn.command(api_method="DELETE", api_path=path, app_name="glp") 115 | return resp 116 | 117 | def inv_user(self, conn, email, send_link): 118 | """Invite a user to a GLP workspace. 119 | 120 | Args: 121 | conn (NewCentralBase): pycentral base connection object. 122 | email (str): Email address of the user to invite. 123 | send_link (bool): Set to True to send welcome email. 124 | 125 | Returns: 126 | (dict): Response as provided by 'command' function in NewCentralBase. 127 | """ 128 | conn.logger.info("Inviting a user to GLP workspace") 129 | 130 | path = generate_url( 131 | GLP_URLS["USER_MANAGEMENT"], category="user_management" 132 | ) 133 | body = {"email": email, "sendWelcomeEmail": send_link} 134 | 135 | resp = conn.command("POST", path, "glp", api_data=body) 136 | if resp["code"] == 201: 137 | conn.logger.info("Invite user successful!") 138 | else: 139 | conn.logger.error("Invite user failed!") 140 | return resp 141 | -------------------------------------------------------------------------------- /pycentral/classic/topology.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .url_utils import TopoUrl, urlJoin 24 | from .base_utils import console_logger 25 | urls = TopoUrl() 26 | 27 | 28 | class Topology(): 29 | """A python class to obtain Aruba Central Site's topology details via REST\ 30 | APIs. 31 | """ 32 | 33 | def get_topology(self, conn, site_id): 34 | """Get topology details of a site. The input is the id corresponding\ 35 | to a label or a site. 36 | 37 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 38 | API call. 39 | :type conn: class:`pycentral.ArubaCentralBase` 40 | :param site_id: Site ID 41 | :type site_id: int 42 | :return: HTTP Response as provided by 'command' function in\ 43 | class:`pycentral.ArubaCentralBase` 44 | :rtype: dict 45 | """ 46 | path = urlJoin(urls.TOPOLOGY["GET_TOPO_SITE"], str(site_id)) 47 | print(path) 48 | resp = conn.command(apiMethod="GET", apiPath=path) 49 | return resp 50 | 51 | def get_device_details(self, conn, device_serial): 52 | """Provides details of a device when serial number is passed as input. 53 | 54 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 55 | API call. 56 | :type conn: class:`pycentral.ArubaCentralBase` 57 | :param device_serial: Device Serial Number 58 | :type device_serial: str 59 | :return: HTTP Response as provided by 'command' function in\ 60 | class:`pycentral.ArubaCentralBase` 61 | :rtype: dict 62 | """ 63 | path = urlJoin(urls.TOPOLOGY["GET_DEVICES"], device_serial) 64 | resp = conn.command(apiMethod="GET", apiPath=path) 65 | return resp 66 | 67 | def get_edge_details(self, conn, source_serial, dest_serial): 68 | """Get details of an edge grouped by lagname. The serials of\ 69 | nodes/devices on both sides of the edge should passed as input. 70 | 71 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 72 | API call. 73 | :type conn: class:`pycentral.ArubaCentralBase` 74 | :param source_serial: Device serial number. 75 | :type source_serial: str 76 | :param dest_serial: Device serial number. 77 | :type dest_serial: str 78 | :return: HTTP Response as provided by 'command' function in\ 79 | class:`pycentral.ArubaCentralBase` 80 | :rtype: dict 81 | """ 82 | path = urlJoin(urls.TOPOLOGY["GET_EDGES"], source_serial, dest_serial) 83 | resp = conn.command(apiMethod="GET", apiPath=path) 84 | return resp 85 | 86 | def get_uplink_details(self, conn, source_serial, uplink_id): 87 | """Get details of an uplink. The serials of node/device on one side of\ 88 | the uplink and the uplink id of the uplink should passed as input.\ 89 | Desired uplink id can be found in get 90 | topology details api. 91 | 92 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 93 | API call. 94 | :type conn: class:`pycentral.ArubaCentralBase` 95 | :param source_serial: Device serial number.xx 96 | :type source_serial: str 97 | :param uplink_id: Uplink id. 98 | :type uplink_id: str 99 | :return: HTTP Response as provided by 'command' function in\ 100 | class:`pycentral.ArubaCentralBase` 101 | :rtype: dict 102 | """ 103 | path = urlJoin( 104 | urls.TOPOLOGY["GET_UPLINK"], 105 | source_serial, 106 | str(uplink_id)) 107 | resp = conn.command(apiMethod="GET", apiPath=path) 108 | return resp 109 | 110 | def tunnel_details(self, conn, site_id, tunnel_map_names): 111 | """Get tunnel details. 112 | 113 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 114 | API call. 115 | :type conn: class:`pycentral.ArubaCentralBase` 116 | :param site_id: Site ID 117 | :type site_id: int 118 | :param tunnel_map_names: Comma separated list of tunnel map names. 119 | :type tunnel_map_names: list 120 | :return: HTTP Response as provided by 'command' function in\ 121 | class:`pycentral.ArubaCentralBase` 122 | :rtype: dict 123 | """ 124 | path = urlJoin(urls.TOPOLOGY["GET_TUNNEL"], str(site_id)) 125 | params = {} 126 | params["tunnel_map_names"] = tunnel_map_names 127 | resp = conn.command(apiMethod="GET", apiPath=path, apiParams=params) 128 | return resp 129 | 130 | def ap_lldp_neighbors(self, conn, device_serial): 131 | """Get neighbor details reported by AP via LLDP. 132 | 133 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 134 | API call. 135 | :type conn: class:`pycentral.ArubaCentralBase` 136 | :param device_serial: Device serial number. 137 | :type device_serial: str 138 | :return: HTTP Response as provided by 'command' function in\ 139 | class:`pycentral.ArubaCentralBase` 140 | :rtype: dict 141 | """ 142 | path = urlJoin(urls.TOPOLOGY["GET_AP_LLDP"], device_serial) 143 | 144 | resp = conn.command(apiMethod="GET", apiPath=path) 145 | return resp 146 | -------------------------------------------------------------------------------- /pycentral/classic/workflows/workflows_utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import sys 24 | import os 25 | import json 26 | import yaml 27 | import csv 28 | 29 | from pycentral.classic.base import ArubaCentralBase 30 | 31 | 32 | def get_file_contents(filename, logger=None): 33 | """Function to open a JSON/YAML/CSV file and return the contents of the\ 34 | file in dict format. (A list of dict is returned for a CSV file.) 35 | 36 | :param filename: Name of an existing JSON/YAML/CSV file. 37 | :type filename: str 38 | :param logger: Provide an instance of class:`logging.logger`. 39 | :type logger: class:`logging.logger`, optional 40 | :raises UserWarning: Raises warning when supported filetypes are not\ 41 | provided. 42 | :return: Data loaded from JSON/YAML/CSV file 43 | :rtype: dict (a list of dict for CSV) 44 | """ 45 | read_data = {} 46 | try: 47 | with open(filename, "r") as fp: 48 | file_dummy, file_ext = os.path.splitext(filename) 49 | if ".json" in file_ext: 50 | read_data = json.loads(fp.read()) 51 | elif file_ext in ['.yaml', '.yml']: 52 | read_data = yaml.safe_load(fp.read()) 53 | elif ".csv" in file_ext: 54 | read_data = list(csv.DictReader(open(filename))) 55 | else: 56 | raise UserWarning("Provide valid file with" 57 | "format/extension [.json/.yaml/.yml/.csv]!") 58 | return read_data 59 | except FileNotFoundError: 60 | if logger: 61 | logger.error("File %s not found.." % filename) 62 | else: 63 | print("File %s Not Found!" % filename) 64 | except Exception as err: 65 | if logger: 66 | logger.error("Error reading file %s: %s" % (filename, str(err))) 67 | else: 68 | print(str(err)) 69 | 70 | 71 | def dict_list_to_csv(filename, csv_data_list, logger=None): 72 | """Write list of dictionaries into a CSV File via csv.DictWriter() 73 | 74 | :param filename: Name of the file to be created or overwritten 75 | :type filename: str 76 | :param csv_data_list: A list of dictionaries, where each dict is a row in\ 77 | CSV file 78 | :type csv_data_list: list 79 | :param logger: Provide an instance of class:`logging.logger`. 80 | :type logger: class:`logging.logger`, optional 81 | """ 82 | csv_columns = [] 83 | if csv_data_list and csv_data_list[0]: 84 | csv_columns = list(csv_data_list[0].keys()) 85 | else: 86 | if logger: 87 | logger.warning("No data to write to a CSV file...") 88 | return 89 | try: 90 | with open(filename, 'w') as csvfile: 91 | writer = csv.DictWriter(csvfile, fieldnames=csv_columns) 92 | writer.writeheader() 93 | for data in csv_data_list: 94 | writer.writerow(data) 95 | if logger: 96 | logger.info( 97 | "Creating a new csv file '%s' with failed APs..." % 98 | filename) 99 | except IOError: 100 | print("I/O error") 101 | except Exception as err: 102 | if logger: 103 | logger.error("Error writing to file %s: %s" % (filename, str(err))) 104 | else: 105 | print(str(err)) 106 | 107 | 108 | def get_conn_from_file(filename, account=None, logger=None): 109 | """Creates an instance of class`pycentral.ArubaCentralBase` based on the\ 110 | information 111 | provided in the YAML/JSON file. \n 112 | * keyword central_info: A dict containing arguments as accepted by\ 113 | class`pycentral.ArubaCentralBase` \n 114 | * keyword ssl_verify: A boolean when set to True, the python client\ 115 | validates Aruba Central's SSL certs. \n 116 | * keyword token_store: Optional. Defaults to None. \n 117 | 118 | :param filename: Name of a JSON/YAML file containing the keywords required\ 119 | for class:`pycentral.ArubaCentralBase` 120 | :type filename: str 121 | :param logger: Provide an instance of class:`logging.logger`, defaults to\ 122 | logger class with name "ARUBA_BASE". 123 | :type logger: class:`logging.logger`, optional 124 | :return: An instance of class:`pycentral.ArubaCentralBase` to make API\ 125 | calls and manage access tokens. 126 | :rtype: class:`pycentral.ArubaCentralBase` 127 | """ 128 | conn = None 129 | token_store = None 130 | ssl_verify = True 131 | 132 | input_args = get_file_contents(filename=filename, logger=logger) 133 | if not input_args: 134 | sys.exit("Unable to get the file content... exiting!") 135 | # if "central_info" not in input_args: 136 | # sys.exit("exiting... Provide central_info in the file %s" % filename) 137 | # central_info = input_args["central_info"] 138 | 139 | if account is None: 140 | account = "central_info" 141 | if account not in input_args: 142 | sys.exit("exiting... Provide %s in the file %s" % (account, filename)) 143 | central_info = input_args[account] 144 | 145 | if "token_store" in input_args: 146 | token_store = input_args["token_store"] 147 | if "ssl_verify" in input_args: 148 | ssl_verify = input_args["ssl_verify"] 149 | 150 | conn = ArubaCentralBase(central_info=central_info, 151 | token_store=token_store, 152 | ssl_verify=ssl_verify, 153 | logger=logger) 154 | return conn 155 | -------------------------------------------------------------------------------- /pycentral/scopes/scope_base.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from .scope_maps import ScopeMaps 5 | from ..utils.scope_utils import ( 6 | fetch_attribute, 7 | ) 8 | 9 | scope_maps = ScopeMaps() 10 | 11 | 12 | class ScopeBase: 13 | """Base class for all scope elements, such as Site, Site_Collection, and Device. 14 | 15 | Provides common functionality like: 16 | - Returning the object's ID or name. 17 | - Assigning and unassigning profiles. 18 | """ 19 | 20 | def get_id(self): 21 | """Fetches the ID of the scope element. 22 | 23 | Returns: 24 | (int): ID of the scope element 25 | """ 26 | return fetch_attribute(self, "id") 27 | 28 | def get_name(self): 29 | """Fetches the name of the scope element. 30 | 31 | Returns: 32 | (str): Name of the scope element 33 | """ 34 | return fetch_attribute(self, "name") 35 | 36 | def get_type(self): 37 | """Fetches the type of the scope element. 38 | 39 | Returns: 40 | (str): Type of the scope element (e.g., 'site', 'site_collection', 'device') 41 | """ 42 | return fetch_attribute(self, "type") 43 | 44 | def assign_profile(self, profile_name, profile_persona=None): 45 | """Assigns a profile with the provided name and persona to the scope. 46 | 47 | Args: 48 | profile_name (str): Name of the profile to assign 49 | profile_persona (str, optional): Device Persona of the profile to assign. 50 | Optional if assigning a profile to a device 51 | 52 | Returns: 53 | (bool): True if the profile assignment was successful, False otherwise 54 | """ 55 | profile_persona = self._resolve_profile_persona(profile_persona) 56 | if profile_persona is None: 57 | return False 58 | 59 | resp = scope_maps.associate_profile_to_scope( 60 | central_conn=self.central_conn, 61 | scope_id=self.get_id(), 62 | profile_name=profile_name, 63 | persona=profile_persona, 64 | ) 65 | if resp["code"] == 200: 66 | self.add_profile(name=profile_name, persona=profile_persona) 67 | return True 68 | else: 69 | self.central_conn.logger.error( 70 | "Unable to assign profile " 71 | + profile_name 72 | + " to " 73 | + self.get_name() 74 | ) 75 | return False 76 | 77 | def unassign_profile(self, profile_name, profile_persona=None): 78 | """Unassigns a profile with the provided name and persona from the scope. 79 | 80 | Args: 81 | profile_name (str): Name of the profile to unassign 82 | profile_persona (str, optional): Persona of the profile to unassign. 83 | Optional if unassigning a profile from a device 84 | 85 | Returns: 86 | (bool): True if the profile unassignment was successful, False otherwise 87 | """ 88 | profile_persona = self._resolve_profile_persona(profile_persona) 89 | if profile_persona is None: 90 | return False 91 | 92 | resp = scope_maps.unassociate_profile_from_scope( 93 | central_conn=self.central_conn, 94 | scope_id=self.get_id(), 95 | profile_name=profile_name, 96 | persona=profile_persona, 97 | ) 98 | if resp["code"] == 200: 99 | self.remove_profile(name=profile_name, persona=profile_persona) 100 | return True 101 | else: 102 | self.central_conn.logger.error( 103 | "Unable to unassign profile " 104 | + profile_name 105 | + " to " 106 | + self.get_name() 107 | ) 108 | return False 109 | 110 | def _resolve_profile_persona(self, profile_persona): 111 | """Internal helper to validate and resolve the correct profile_persona for the scope. 112 | 113 | Args: 114 | profile_persona (str or None): Profile persona to validate and resolve 115 | 116 | Returns: 117 | (str or None): Resolved persona or None if invalid 118 | """ 119 | if self.get_type() == "device": 120 | if not self.provisioned_status: 121 | self.central_conn.logger.error( 122 | "Device is currently configured via Classic Central only. Please provision the device to new Central before assigning/unassigning profile to device." 123 | ) 124 | return None 125 | if profile_persona is not None: 126 | if profile_persona != self.config_persona: 127 | self.central_conn.logger.error( 128 | f"Invalid profile persona(device function) '{profile_persona}' for device. Device's current persona is {self.persona} ({self.config_persona}). If you would like the profile to take the device's current persona, you can leave the profile_persona attribute empty." 129 | ) 130 | return None 131 | return profile_persona 132 | else: 133 | return self.config_persona 134 | else: 135 | if profile_persona is None: 136 | self.central_conn.logger.error( 137 | "Profile persona is required when assigning a profile to a scope other than device." 138 | ) 139 | return None 140 | return profile_persona 141 | 142 | def add_profile(self, name, persona): 143 | """Helper function that adds a profile to the assigned profiles of the scope in the SDK. 144 | 145 | Args: 146 | name (str): Name of the profile to add 147 | persona (str): Device Persona of the profile to add 148 | """ 149 | self.assigned_profiles.append({"persona": persona, "resource": name}) 150 | 151 | def remove_profile(self, name, persona): 152 | """Helper function that removes a profile from the assigned profiles of the scope in the SDK. 153 | 154 | Args: 155 | name (str): Name of the profile to remove 156 | persona (str): Device Persona of the profile to remove 157 | 158 | Returns: 159 | (bool): True if the profile was successfully removed, False otherwise 160 | """ 161 | remove_status = False 162 | index = None 163 | for id_element, element in enumerate(self.assigned_profiles): 164 | if element["persona"] == persona and element["resource"] == name: 165 | index = id_element 166 | break 167 | if index is not None: 168 | self.assigned_profiles.pop(index) 169 | remove_status = True 170 | return remove_status 171 | -------------------------------------------------------------------------------- /pycentral/glp/service_manager.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from ..utils import GLP_URLS, generate_url 5 | 6 | 7 | class ServiceManager(object): 8 | def get_application_id_and_region(self, conn, application_name, region): 9 | """Retrieve the application ID and API region name for a specified application and region. 10 | 11 | 12 | Args: 13 | conn (NewCentralBase): pycentral base connection object 14 | application_name (str): name of the application to search for. 15 | region (str): The region (UI name) where the application is deployed. 16 | 17 | Returns: 18 | (dict): Dictionary containing the application ID and API region name if found, otherwise None. 19 | """ 20 | if not application_name or not region: 21 | conn.logger.error("Application name or region cannot be empty.") 22 | return None 23 | 24 | resp = self.get_service_manager_by_region(conn) 25 | if resp["code"] != 200: 26 | conn.logger.error( 27 | f"Error fetching list of service manager(applications) by region: {resp['code']} - {resp['msg']}" 28 | ) 29 | return None 30 | 31 | region_service_manager_mapping = ( 32 | self._generate_application_region_mapping(resp["msg"]["items"]) 33 | ) 34 | if region not in region_service_manager_mapping.keys(): 35 | conn.logger.error( 36 | f"Unable to find region with name {region}. \nValid regions are {', '.join(region_service_manager_mapping.keys())}" 37 | ) 38 | return None 39 | api_region_name = region_service_manager_mapping[region]["id"] 40 | 41 | region_service_managers = list( 42 | region_service_manager_mapping[region]["serviceManagers"].keys() 43 | ) 44 | if application_name not in region_service_managers: 45 | conn.logger.error( 46 | f"Unable to find service manager with name {application_name}. \nValid service managers(applications) in region {region} are {', '.join(region_service_managers)}" 47 | ) 48 | return None 49 | service_manager_id = region_service_manager_mapping[region][ 50 | "serviceManagers" 51 | ][application_name] 52 | 53 | resp = self.get_service_manager_provisions(conn) 54 | if resp["code"] != 200: 55 | conn.logger.error( 56 | f"Error fetching list of service manager provisions (installed applications): {resp['code']} - {resp['msg']}" 57 | ) 58 | return None 59 | for provisioned_service in resp["msg"]["items"]: 60 | if ( 61 | service_manager_id 62 | == provisioned_service["serviceManager"]["id"] 63 | and provisioned_service["region"] == api_region_name 64 | ): 65 | conn.logger.info( 66 | f"Successfully verified installation of service manager {application_name} in region {region}." 67 | ) 68 | return { 69 | "id": service_manager_id, 70 | "region": api_region_name, 71 | } 72 | 73 | conn.logger.error( 74 | f"Unable to find service manager(application) with name {application_name} in region {region}." 75 | ) 76 | return None 77 | 78 | def get_service_manager_provisions(self, conn, limit=2000, offset=0): 79 | """Retrieve all provisioned services in GLP workspace. 80 | 81 | Args: 82 | conn (NewCentralBase): pycentral base connection object 83 | limit (int, optional): Specify the maximum number of entries per page. Maximum value accepted is 2000. 84 | offset (int, optional): Specify pagination offset. Defines how many pages to skip before returning results. Default is 0. 85 | 86 | Returns: 87 | (dict): API response containing provisioned services. 88 | """ 89 | conn.logger.info("Getting provisioned services in GLP workspace") 90 | path = generate_url( 91 | GLP_URLS["SERVICE_MANAGER_PROVISIONS"], category="service_catalog" 92 | ) 93 | 94 | params = { 95 | "limit": limit, 96 | "offset": offset, 97 | } 98 | 99 | resp = conn.command( 100 | api_method="GET", api_path=path, api_params=params, app_name="glp" 101 | ) 102 | return resp 103 | 104 | def get_service_manager_by_region(self, conn): 105 | """Get the region mapping for the service manager. 106 | 107 | Args: 108 | conn (NewCentralBase): pycentral base connection object 109 | 110 | Returns: 111 | (dict): API response containing service managers by region. 112 | """ 113 | conn.logger.info("Getting services managers by region in GLP") 114 | path = generate_url( 115 | GLP_URLS["SERVICE_MANAGER_BY_REGION"], category="service_catalog" 116 | ) 117 | 118 | resp = conn.command(api_method="GET", api_path=path, app_name="glp") 119 | return resp 120 | 121 | def _generate_application_region_mapping(self, service_manager_list): 122 | """Generate mappings for service managers and regions. 123 | 124 | Args: 125 | service_manager_list (list): List of dictionaries where each dictionary represents a region 126 | and contains its name, ID, and associated service managers. 127 | 128 | Returns: 129 | (dict): Dictionary mapping region names to their IDs and service managers. 130 | """ 131 | region_map = {} 132 | for region in service_manager_list: 133 | region_map[region["regionName"]] = {"id": region["id"]} 134 | region_map[region["regionName"]]["serviceManagers"] = { 135 | serviceManager["name"]: serviceManager["id"] 136 | for serviceManager in region["serviceManagers"] 137 | } 138 | 139 | return region_map 140 | 141 | def get_service_managers(self, conn, limit=2000, offset=0): 142 | """Retrieve all available service managers in GLP. 143 | 144 | Args: 145 | conn (NewCentralBase): pycentral base connection object 146 | limit (int, optional): Specify the maximum number of entries per page. Maximum value accepted is 2000. 147 | offset (int, optional): Specify pagination offset. Defines how many pages to skip before returning results. 148 | 149 | Returns: 150 | (dict): API response containing service managers. 151 | """ 152 | conn.logger.info("Getting service managers in GLP") 153 | path = generate_url( 154 | GLP_URLS["SERVICE_MANAGER"], category="service_catalog" 155 | ) 156 | 157 | params = { 158 | "limit": limit, 159 | "offset": offset, 160 | } 161 | 162 | resp = conn.command( 163 | api_method="GET", api_path=path, api_params=params, app_name="glp" 164 | ) 165 | return resp 166 | -------------------------------------------------------------------------------- /pycentral/new_monitoring/devices.py: -------------------------------------------------------------------------------- 1 | from ..utils.monitoring_utils import execute_get 2 | from ..exceptions import ParameterError 3 | 4 | MONITOR_TYPE = "devices" 5 | DEVICE_LIMIT = 100 6 | 7 | 8 | class MonitoringDevices: 9 | @staticmethod 10 | def get_all_devices(central_conn, filter_str=None, sort=None): 11 | """ 12 | Retrieve all devices that are onboarded and currently being monitored in new Central. 13 | 14 | Args: 15 | central_conn (NewCentralBase): Central connection object. 16 | filter_str (str, optional): Optional filter expression (supported fields documented in API Reference Guide). 17 | sort (str, optional): Optional sort parameter (supported fields documented in API Reference Guide). 18 | 19 | Returns: 20 | (list[dict]): Processed list of all devices. 21 | """ 22 | devices = [] 23 | total_devices = None 24 | limit = DEVICE_LIMIT 25 | next = 1 26 | while True: 27 | response = MonitoringDevices.get_devices( 28 | central_conn, filter_str=filter_str, limit=limit, next=next 29 | ) 30 | if total_devices is None: 31 | total_devices = response.get("total", 0) 32 | devices.extend(response.get("items", [])) 33 | if len(devices) >= total_devices: 34 | break 35 | next += 1 36 | 37 | return devices 38 | 39 | @staticmethod 40 | def get_devices( 41 | central_conn, filter_str=None, sort=None, limit=DEVICE_LIMIT, next=1 42 | ): 43 | """ 44 | Retrieve a single page of devices with optional filtering and sorting. This response retrieves a list of network devices that are onboarded and currently being monitored. 45 | 46 | This method makes an API call to the following endpoint - `GET network-monitoring/v1alpha1/devices` 47 | 48 | Args: 49 | central_conn (NewCentralBase): Central connection object. 50 | filter_str (str, optional): Optional filter expression (supported fields documented in API Reference Guide). 51 | sort (str, optional): Optional sort parameter (supported fields documented in API Reference Guide). 52 | limit (int, optional): Number of entries to return (default is 100). 53 | next (int, optional): Pagination cursor for next page of resources (default is 1). 54 | 55 | Returns: 56 | (dict): API response from the device endpoint (typically contains 'items', 'total', and 'next'). 57 | """ 58 | params = { 59 | "limit": limit, 60 | "next": next, 61 | "filter": filter_str, 62 | "sort": sort, 63 | } 64 | 65 | path = MONITOR_TYPE 66 | return execute_get(central_conn, endpoint=path, params=params) 67 | 68 | @staticmethod 69 | def get_all_device_inventory( 70 | central_conn, 71 | filter_str=None, 72 | sort=None, 73 | search=None, 74 | site_assigned=None, 75 | ): 76 | """ 77 | Retrieve all devices from the account, including devices that are not yet onboarded to new Central. 78 | 79 | Args: 80 | central_conn (NewCentralBase): Central connection object. 81 | filter_str (str, optional): Optional filter expression (supported fields documented in API Reference Guide). 82 | sort (str, optional): Optional sort parameter (supported fields documented in API Reference Guide). 83 | search (str, optional): Search string to filter results. 84 | site_assigned (str|None, optional): Filter by site-assigned status. 85 | Returns: 86 | (list[dict]): Processed list of all devices from inventory. 87 | """ 88 | devices = [] 89 | total_devices = None 90 | next_int = 1 91 | while True: 92 | response = MonitoringDevices.get_device_inventory( 93 | central_conn, 94 | filter_str=filter_str, 95 | sort=sort, 96 | search=search, 97 | site_assigned=site_assigned, 98 | limit=DEVICE_LIMIT, 99 | next=next_int, 100 | ) 101 | if total_devices is None: 102 | total_devices = response.get("total", 0) 103 | devices.extend(response.get("items", [])) 104 | if len(devices) == total_devices: 105 | break 106 | next_int += 1 107 | 108 | return devices 109 | 110 | @staticmethod 111 | def get_device_inventory( 112 | central_conn, 113 | filter_str=None, 114 | sort=None, 115 | search=None, 116 | site_assigned=None, 117 | limit=DEVICE_LIMIT, 118 | next=1, 119 | ): 120 | """ 121 | Retrieve device data from device inventory API response. This API includes devices yet to be onboarded, as well as those already onboarded and currently being monitored. 122 | 123 | This method makes an API call to the following endpoint - `GET network-monitoring/v1alpha1/device-inventory` 124 | 125 | Args: 126 | central_conn (NewCentralBase): Central connection object. 127 | filter_str (str, optional): Optional filter expression (supported fields documented in API Reference Guide). 128 | sort (str, optional): Optional sort parameter (supported fields documented in API Reference Guide). 129 | search (str, optional): Search string to filter results. 130 | site_assigned (str|None, optional): Filter by site-assigned status. Supported values are "ASSIGNED", "UNASSIGNED" 131 | limit (int, optional): Number of entries to return (default is 100). 132 | next (int, optional): Pagination cursor for next page of resources (default is 1). 133 | 134 | Returns: 135 | (dict): Raw API response containing device inventory per site. 136 | 137 | """ 138 | params = { 139 | "limit": limit, 140 | "next": next, 141 | "filter": filter_str, 142 | "sort": sort, 143 | "search": search, 144 | "site-assigned": site_assigned, 145 | } 146 | path = "device-inventory" 147 | return execute_get(central_conn, endpoint=path, params=params) 148 | 149 | @staticmethod 150 | def delete_device(central_conn, serial_number): 151 | """ 152 | Delete a device from Central Monitoring (device must be OFFLINE). 153 | 154 | Args: 155 | central_conn (NewCentralBase): Central connection object. 156 | serial_number (str): Serial number of the device to delete. 157 | 158 | Returns: 159 | (tuple(bool, dict)): (True, API response) on success. 160 | 161 | Raises: 162 | ParameterError: If serial_number is missing or not a string. 163 | Exception: If delete API returns a non-200 response. 164 | 165 | """ 166 | if not serial_number or not isinstance(serial_number, str): 167 | raise ParameterError( 168 | "serial_number is required and must be a string" 169 | ) 170 | 171 | path = f"{MONITOR_TYPE}/{serial_number}" 172 | 173 | resp = central_conn.command("DELETE", path) 174 | 175 | if resp["code"] != 200: 176 | raise Exception( 177 | f"Error deleting device from {path}: {resp['code']} - {resp['msg']}" 178 | ) 179 | 180 | return True, resp 181 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | If you're reading this, you're probably thinking about contributing to this repository. We really appreciate that--thank you! 4 | 5 | This document provides guidelines on contributing to this repository. Please follow these guidelines when creating issues, making commits, and submitting pull requests. The repository maintainers review all pull requests and verify that they conform to these guidelines before approving and merging. 6 | 7 | #### Table Of Contents 8 | [How Can I Contribute?](#how-can-i-contribute) 9 | * [Contribution Ideas](#contribution-ideas) 10 | * [What should I know before I get started?](#what-should-i-know-before-i-get-started) 11 | 12 | [Licensing](#licensing) 13 | * [Developer's Certificate of Origin](#developers-certificate-of-origin) 14 | * [Sign Your Work](#sign-your-work) 15 | 16 | [Coding Conventions](#coding-conventions) 17 | 18 | [Additional Notes](#additional-notes) 19 | * [Resources](#resources) 20 | 21 | ## How Can I Contribute? 22 | 23 | ### Contribution Ideas 24 | 25 | 1. Raise issues for bugs, features, and enhancements. 26 | 1. Submit updates and improvements to the documentation. 27 | 1. Submit articles and guides, which are also part of the documentation. 28 | 1. Help out repo maintainers by answering questions in [Airheads Developer Community][airheads-link]. 29 | 1. Share feedback and let us know about interesting use cases in [Airheads Developer Community][airheads-link]. 30 | 31 | ### What should I know before I get started? 32 | 33 | The best way to directly collaborate with the project contributors is through GitHub. 34 | 35 | * If you want to raise an issue such as a defect, an enhancement request, feature request, or a general issue, please open a GitHub issue. 36 | * If you want to contribute to our code by either fixing a problem, enhancing some code, or creating a new feature, please open a GitHub pull request against the development branch. 37 | > **Note:** All pull requests require an associated issue number, must be made against the **development** branch, and require acknowledgement of the DCO. See the [Licensing](#licensing) section below. 38 | 39 | Before you start to code, we recommend discussing your plans through a GitHub issue, especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. 40 | 41 | It is your responsibility to test and verify, prior to submitting a pull request, that your updated code doesn't introduce any bugs. Please write a clear commit message for each commit. Brief messages are fine for small changes, but bigger changes warrant a little more detail (at least a few sentences). 42 | Note that all patches from all contributors get reviewed. 43 | After a pull request is made, other contributors will offer feedback. If the patch passes review, a maintainer will accept it with a comment. 44 | When a pull request fails review, the author is expected to update the pull request to address the issue until it passes review and the pull request merges successfully. 45 | 46 | At least one review from a maintainer is required for all patches. 47 | 48 | ### Contribution Guidelines 49 | This repo is maintained on a best-effort basis. The burden is on the submitter and not the repo maintainers to ensure the following criteria are met when code is submitted. 50 | 1. All code submissions must adhere to the structure of the repo: 51 | * Directory /pycentral holds python libraries and /workflows directory. 52 | * The library files are base/low-level API calls with each file relating to a category in the API Documentation page within the Aruba Central API Gateway. 53 | * High-level process/use-case focused scripts must be placed in the /workflows directory. 54 | * Sample Scripts should be placed in /sample_scripts directory. 55 | * Do not create new separate directory for submitted projects. 56 | * Do not make copies of existing files to be saved in different folders. 57 | 2. All Python code should conform to liberal PEP-8 standards. 58 | 3. All functions should have explanatory docstrings using the reStructuredText format. 59 | 4. All workflows should have a comment at the top explaining the configuration steps the workflow performs, and any preconditions that need to be met before running the script. 60 | 5. All git commits should have clear, concise messages which explain the changes made in the commit. All Pull Requests (PRs) should contain a title and comments that explain the impact of the PR. 61 | 6. All code submitted for merge consideration must be tested by the submitter. 62 | 63 | ## Licensing 64 | 65 | All contributions must include acceptance of the DCO: 66 | 67 | ### Developer’s Certificate of Origin 68 | 69 | > Developer Certificate of Origin Version 1.1 70 | > 71 | > Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 72 | > York Street, Suite 102, San Francisco, CA 94110 USA 73 | > 74 | > Everyone is permitted to copy and distribute verbatim copies of this 75 | > license document, but changing it is not allowed. 76 | > 77 | > Developer's Certificate of Origin 1.1 78 | > 79 | > By making a contribution to this project, I certify that: 80 | > 81 | > \(a) The contribution was created in whole or in part by me and I have 82 | > the right to submit it under the open source license indicated in the 83 | > file; or 84 | > 85 | > \(b) The contribution is based upon previous work that, to the best of my 86 | > knowledge, is covered under an appropriate open source license and I 87 | > have the right under that license to submit that work with 88 | > modifications, whether created in whole or in part by me, under the same 89 | > open source license (unless I am permitted to submit under a different 90 | > license), as indicated in the file; or 91 | > 92 | > \(c) The contribution was provided directly to me by some other person 93 | > who certified (a), (b) or (c) and I have not modified it. 94 | > 95 | > \(d) I understand and agree that this project and the contribution are 96 | > public and that a record of the contribution (including all personal 97 | > information I submit with it, including my sign-off) is maintained 98 | > indefinitely and may be redistributed consistent with this project or 99 | > the open source license(s) involved. 100 | 101 | ### Sign Your Work 102 | 103 | To accept the DCO, simply add this line to each commit message with your 104 | name and email address (`git commit -s` will do this for you): 105 | 106 | Signed-off-by: Jane Example 107 | 108 | For legal reasons, no anonymous or pseudonymous contributions are 109 | accepted. 110 | 111 | ## Coding Conventions 112 | 113 | 1. Python code should conform to PEP-8. 114 | 1. Since this is a collaborative project, document your code with comments that will help other contributors understand the code you write. 115 | 1. When in doubt, follow conventions you see used in the source already. 116 | 117 | ## Additional Notes 118 | 119 | > **Note:** Please don't file an issue to ask a question. Please reach out to us via email or disucssion forums. 120 | 121 | ### Resources 122 | 123 | | Resource | Description | 124 | | --- | --- | 125 | | [Airheads Developer Community][airheads-link] | Aruba Airheads forum to discuss all things network automation. | 126 | | [Aruba Bots Automate Videos][aruba-bots-playlist-link]| YouTube playlist containing instructional videos for Ansible and Python automation repositories. | 127 | | [aruba-automation@hpe.com][email-link] | Distribution list email to contact the switching automation technical marketing engineering team. | 128 | 129 | 130 | [airheads-link]: https://community.arubanetworks.com/community-home/digestviewer?communitykey=ea467413-8db4-4c49-b5f8-1a12f193e959 131 | [aruba-bots-playlist-link]: https://youtube.com/playlist?list=PLsYGHuNuBZcY02FUh95ZpOB5VFkPurVaX 132 | [email-link]: mailto:aruba-automation@hpe.com 133 | -------------------------------------------------------------------------------- /pycentral/classic/audit_logs.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .url_utils import AuditUrl, urlJoin 24 | from .base_utils import console_logger 25 | 26 | urls = AuditUrl() 27 | 28 | 29 | class Audit: 30 | """Get the audit logs and event logs with the functions in this class.""" 31 | 32 | def get_traillogs(self, conn, limit=100, offset=0, username=None, 33 | start_time=None, end_time=None, description=None, 34 | target=None, classification=None, customer_name=None, 35 | ip_address=None, app_id=None): 36 | """Get audit logs, sort by time in in reverse chronological order. 37 | This API returns the first 10,000 results only. Please use filter 38 | in the API for more relevant results. MSP Customer Would see logs 39 | of MSP's and tenants as well. 40 | 41 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 42 | API call. 43 | :type conn: class:`pycentral.ArubaCentralBase` 44 | :param limit: Maximum number of audit events to be returned, defaults\ 45 | to 100 46 | :type limit: int, optional 47 | :param offset: Number of items to be skipped before returning the \ 48 | data, useful for pagination, defaults to 0 49 | :type offset: int, optional 50 | :param username: Filter audit logs by User Name, defaults to None 51 | :type username: str, optional 52 | :param start_time: Filter audit logs by Time Range. Start time of the\ 53 | audit logs should be provided in epoch seconds, defaults to None 54 | :type start_time: int, optional 55 | :param end_time: Filter audit logs by Time Range. End time of the \ 56 | audit logs should be provided in epoch seconds, defaults to None 57 | :type end_time: int, optional 58 | :param description: Filter audit logs by Description, defaults to \ 59 | None 60 | :type description: str, optional 61 | :param target: Filter audit logs by target, defaults to None 62 | :type target: str, optional 63 | :param classification: Filter audit logs by Classification, defaults \ 64 | to None 65 | :type classification: str, optional 66 | :param customer_name: Filter audit logs by Customer NameFilter audit \ 67 | logs by Customer Name, defaults to None 68 | :type customer_name: str, optional 69 | :param ip_address: Filter audit logs by IP Address, defaults to None 70 | :type ip_address: str, optional 71 | :param app_id: Filter audit logs by app_id, defaults to None 72 | :type app_id: str, optional 73 | :return: HTTP Response as provided by 'command' function in \ 74 | class:`pycentral.ArubaCentralBase` 75 | :rtype: dict 76 | """ 77 | path = urls.TRAIL_LOG["GET_ALL"] 78 | params = {"limit": limit, "offset": offset} 79 | if username: 80 | params["username"] = username 81 | if start_time: 82 | params["start_time"] = start_time 83 | if end_time: 84 | params["end_time"] = end_time 85 | if description: 86 | params["description"] = description 87 | if target: 88 | params["target"] = target 89 | if classification: 90 | params["classification"] = classification 91 | if customer_name: 92 | params["customer_name"] = customer_name 93 | if ip_address: 94 | params["ip_address"] = ip_address 95 | if app_id: 96 | params["app_id"] = app_id 97 | resp = conn.command(apiMethod="GET", apiPath=path, apiParams=params) 98 | return resp 99 | 100 | def get_traillogs_detail(self, conn, id): 101 | """Get details of an audit log 102 | 103 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 104 | API call. 105 | :type conn: class:`pycentral.ArubaCentralBase` 106 | :param id: ID of audit event 107 | :type id: str 108 | :return: HTTP Response as provided by 'command' function in \ 109 | class:`pycentral.ArubaCentralBase` 110 | :rtype: dict 111 | """ 112 | path = urlJoin(urls.TRAIL_LOG["GET"], id) 113 | resp = conn.command(apiMethod="GET", apiPath=path) 114 | return resp 115 | 116 | def get_eventlogs(self, conn, limit=100, offset=0, group_name=None, 117 | device_id=None, classification=None, start_time=None, 118 | end_time=None): 119 | """Get audit events for all groups, sort by time in in reverse\ 120 | chronological order.This API returns the first 10,000 results\ 121 | only. Please use filter in the API for more relevant results. 122 | 123 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 124 | API call. 125 | :type conn: class:`pycentral.ArubaCentralBase` 126 | :param limit: Maximum number of audit events to be returned, defaults\ 127 | to 100 128 | :type limit: int, optional 129 | :param offset: Number of items to be skipped before returning the\ 130 | data, useful for pagination, defaults to 0 131 | :type offset: int, optional 132 | :param group_name: Filter audit events by Group Name, defaults to None 133 | :type group_name: str, optional 134 | :param device_id: Filter audit events by Target / Device ID. Device ID\ 135 | for AP is VC Name and Serial Number 136 | for Switches, defaults to None 137 | :type device_id: str, optional 138 | :param classification: Filter audit events by classification, defaults\ 139 | to None 140 | :type classification: str, optional 141 | :param start_time: Filter audit logs by Time Range. Start time of the\ 142 | audit logs should be provided 143 | in epoch seconds, defaults to None 144 | :type start_time: int, optional 145 | :param end_time: Filter audit logs by Time Range. End time of the\ 146 | audit logs should be provided in epoch 147 | seconds, defaults to None 148 | :type end_time: int, optional 149 | :return: HTTP Response as provided by 'command' function in\ 150 | class:`pycentral.ArubaCentralBase` 151 | :rtype: dict 152 | """ 153 | path = urls.EVENT_LOG["GET_ALL"] 154 | params = {"limit": limit, "offset": offset} 155 | if group_name: 156 | params["group_name"] = group_name 157 | if device_id: 158 | params["device_id"] = device_id 159 | if classification: 160 | params["classification"] = classification 161 | if start_time: 162 | params["start_time"] = start_time 163 | if end_time: 164 | params["end_time"] = end_time 165 | resp = conn.command(apiMethod="GET", apiPath=path, apiParams=params) 166 | return resp 167 | 168 | def get_eventlogs_detail(self, conn, id): 169 | """Get details of an audit event/log 170 | 171 | :param conn: Instance of class:`pycentral.ArubaCentralBase` to make an\ 172 | API call. 173 | :type conn: class:`pycentral.ArubaCentralBase` 174 | :param id: ID of audit event 175 | :type id: str 176 | :return: HTTP Response as provided by 'command' function in\ 177 | class:`pycentral.ArubaCentralBase` 178 | :rtype: dict 179 | """ 180 | path = urlJoin(urls.EVENT_LOG["GET"], id) 181 | resp = conn.command(apiMethod="GET", apiPath=path) 182 | return resp 183 | -------------------------------------------------------------------------------- /pycentral/scopes/scope_maps.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from ..utils import SCOPE_URLS, generate_url 5 | 6 | 7 | class ScopeMaps: 8 | def __init__(self): 9 | pass 10 | 11 | # ? Should central_conn be stored in self 12 | def get(self, central_conn): 13 | """Perform a GET call to retrieve data for the Global Scope Map. 14 | 15 | Args: 16 | central_conn (NewCentralBase): Established Central connection object 17 | 18 | Returns: 19 | (list): List of scope map dictionaries if success, empty list otherwise 20 | """ 21 | scope_maps_list = [] 22 | api_method = "GET" 23 | api_path = generate_url(SCOPE_URLS["SCOPE-MAPS"]) 24 | resp = central_conn.command(api_method=api_method, api_path=api_path) 25 | if resp["code"] == 200: 26 | for mapping in resp["msg"]["scope-map"]: 27 | mapping["scope-name"] = int(mapping["scope-name"]) 28 | scope_maps_list = resp["msg"]["scope-map"] 29 | else: 30 | central_conn.logger.error( 31 | f"Unable to fetch scope maps data. Error code - {resp['code']}.\n Error Description - {resp['msg']}" 32 | ) 33 | return scope_maps_list 34 | 35 | # ? should this return a value or set a value to self? 36 | def get_scope_assigned_profiles(self, central_conn, scope_id): 37 | """Performs a GET call to retrieve Global Scope Map then finds matching scope. 38 | 39 | Args: 40 | central_conn (NewCentralBase): Established Central connection object 41 | scope_id (int): ID of the scope to be matched on 42 | 43 | Returns: 44 | (list): List of assigned profile dictionaries for the scope 45 | """ 46 | assigned_profiles = [] 47 | mappings = self.get(central_conn=central_conn) 48 | if mappings: 49 | for mapping in mappings: 50 | if mapping["scope-name"] == scope_id: 51 | assigned_profiles.append(mapping) 52 | for profile in assigned_profiles: 53 | profile.pop("scope-name") 54 | 55 | def associate_profile_to_scope( 56 | self, central_conn, scope_id, profile_name, persona 57 | ): 58 | """Performs a POST call to associate a profile with device persona to the provided scope. 59 | 60 | Args: 61 | central_conn (NewCentralBase): Established Central connection object 62 | scope_id (int or str): ID of the scope to associate the profile 63 | profile_name (str): Name of the profile to be assigned 64 | persona (str or list): Device persona(s) to be associated with the profile. 65 | Valid values: SERVICE_PERSONA, HYBRID_NAC, CORE_SWITCH, BRIDGE, 66 | CAMPUS_AP, IOT, MOBILITY_GW, AGG_SWITCH, BRANCH_GW, VPNC, 67 | ACCESS_SWITCH, MICROBRANCH_AP, or "ALL" 68 | 69 | Returns: 70 | (dict): JSON Data of returned response from POST call 71 | """ 72 | api_method = "POST" 73 | api_path = generate_url(SCOPE_URLS["SCOPE-MAPS"]) 74 | valid_personas = [ 75 | "SERVICE_PERSONA", 76 | "HYBRID_NAC", 77 | "CORE_SWITCH", 78 | "BRIDGE", 79 | "CAMPUS_AP", 80 | "IOT", 81 | "MOBILITY_GW", 82 | "AGG_SWITCH", 83 | "BRANCH_GW", 84 | "VPNC", 85 | "ACCESS_SWITCH", 86 | "MICROBRANCH_AP", 87 | ] 88 | if isinstance(persona, list) or "ALL" in persona: 89 | if "ALL" in persona: 90 | persona = valid_personas 91 | for p in persona: 92 | if p not in valid_personas: 93 | central_conn.logger.error( 94 | f"{p} is not a valid device persona" 95 | f"Unable to assign profile {profile_name} to " 96 | f"{scope_id}" 97 | ) 98 | api_data = { 99 | "scope-map": [ 100 | { 101 | "scope-name": str(scope_id), 102 | "persona": p, 103 | "resource": profile_name, 104 | } 105 | ] 106 | } 107 | resp = central_conn.command( 108 | api_method=api_method, api_path=api_path, api_data=api_data 109 | ) 110 | if resp["code"] == 200: 111 | central_conn.logger.info( 112 | f"Successfully assigned profile {profile_name} to" 113 | f" {scope_id} with {p} device persona" 114 | ) 115 | else: 116 | api_data = { 117 | "scope-map": [ 118 | { 119 | "scope-name": str(scope_id), 120 | "persona": persona, 121 | "resource": profile_name, 122 | } 123 | ] 124 | } 125 | 126 | resp = central_conn.command( 127 | api_method=api_method, api_path=api_path, api_data=api_data 128 | ) 129 | 130 | if resp["code"] == 200: 131 | central_conn.logger.info( 132 | f"Successfully assigned profile {profile_name} to " 133 | f"{scope_id} with {persona} device persona" 134 | ) 135 | return resp 136 | 137 | def unassociate_profile_from_scope( 138 | self, central_conn, scope_id, profile_name, persona 139 | ): 140 | """Performs a DELETE call to unassign a profile with device persona from the provided scope. 141 | 142 | Args: 143 | central_conn (NewCentralBase): Established Central connection object 144 | scope_id (int or str): ID of the scope to unassociate the profile from 145 | profile_name (str): Name of the profile to be unassigned 146 | persona (str or list): Device persona(s) to be unassociated from the profile. 147 | Valid values: SERVICE_PERSONA, HYBRID_NAC, CORE_SWITCH, BRIDGE, 148 | CAMPUS_AP, IOT, MOBILITY_GW, AGG_SWITCH, BRANCH_GW, VPNC, 149 | ACCESS_SWITCH, MICROBRANCH_AP, or "ALL" 150 | 151 | Returns: 152 | (dict): JSON Data of returned response from DELETE call 153 | """ 154 | api_method = "DELETE" 155 | api_path = generate_url(SCOPE_URLS["SCOPE-MAPS"]) 156 | valid_personas = [ 157 | "SERVICE_PERSONA", 158 | "HYBRID_NAC", 159 | "CORE_SWITCH", 160 | "BRIDGE", 161 | "CAMPUS_AP", 162 | "IOT", 163 | "MOBILITY_GW", 164 | "AGG_SWITCH", 165 | "BRANCH_GW", 166 | "VPNC", 167 | "ACCESS_SWITCH", 168 | "MICROBRANCH_AP", 169 | ] 170 | if isinstance(persona, list) or "ALL" in persona: 171 | if "ALL" in persona: 172 | persona = valid_personas 173 | for p in persona: 174 | if p not in valid_personas: 175 | central_conn.logger.error( 176 | f"{p} is not a valid device persona" 177 | f"Unable to unassign profile {profile_name} from" 178 | f" {scope_id}" 179 | ) 180 | api_data = { 181 | "scope-map": [ 182 | { 183 | "scope-name": str(scope_id), 184 | "persona": p, 185 | "resource": profile_name, 186 | } 187 | ] 188 | } 189 | resp = central_conn.command( 190 | api_method=api_method, api_path=api_path, api_data=api_data 191 | ) 192 | 193 | if resp["code"] == 200: 194 | central_conn.logger.info( 195 | f"Successfully unassigned profile {profile_name} from" 196 | f" {scope_id} with {p} device persona" 197 | ) 198 | else: 199 | api_data = { 200 | "scope-map": [ 201 | { 202 | "scope-name": str(scope_id), 203 | "persona": persona, 204 | "resource": profile_name, 205 | } 206 | ] 207 | } 208 | 209 | resp = central_conn.command( 210 | api_method=api_method, api_path=api_path, api_data=api_data 211 | ) 212 | 213 | if resp["code"] == 200: 214 | central_conn.logger.info( 215 | f"Successfully unassigned profile {profile_name} from" 216 | f" {scope_id} with {persona} device persona" 217 | ) 218 | return resp 219 | -------------------------------------------------------------------------------- /pycentral/utils/monitoring_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | from pycentral.utils.url_utils import generate_url 4 | 5 | from ..exceptions import ParameterError 6 | 7 | 8 | def build_timestamp_filter( 9 | start_time=None, 10 | end_time=None, 11 | duration=None, 12 | fmt="rfc3339", 13 | ): 14 | """Build a formatted timestamp filter for API queries. 15 | 16 | Returns timestamps for filtering based on the provided parameters. 17 | 18 | Behavior: 19 | - If start_time and end_time are given, passes through directly. 20 | - If duration is given, computes timestamps relative to now. 21 | - Max supported duration is 3 months (90 days). 22 | 23 | Args: 24 | start_time (str, optional): RFC3339 or Unix timestamp for start. 25 | end_time (str, optional): RFC3339 or Unix timestamp for end. 26 | duration (str, optional): Duration string like '3h', '2d', '1w', '1m' 27 | (hours, days, weeks, minutes). 28 | fmt (str, optional): Output format, either 'rfc3339' or 'unix'. 29 | 30 | Returns: 31 | (tuple): A tuple of (start_time, end_time) formatted strings. 32 | 33 | Raises: 34 | ValueError: If invalid parameter combinations are provided or duration exceeds maximum. 35 | """ 36 | now = datetime.now(timezone.utc) 37 | 38 | # --- Validation --- 39 | if (start_time or end_time) and duration: 40 | raise ValueError( 41 | "Cannot specify start/end timestamps together with duration." 42 | ) 43 | if (start_time and not end_time) or (end_time and not start_time): 44 | raise ValueError( 45 | "Both start_time and end_time must be provided together." 46 | ) 47 | if not duration and not (start_time and end_time): 48 | raise ValueError( 49 | "Provide either both start_time and end_time or a duration." 50 | ) 51 | 52 | # --- Case 1: Start + End (pass-through) --- 53 | if start_time and end_time: 54 | return f"timestamp gt {start_time} and timestamp lt {end_time}" 55 | 56 | # --- Case 2: Duration only --- 57 | unit = duration[-1].lower() 58 | value = int(duration[:-1]) 59 | 60 | if unit not in {"w", "h", "d", "m"}: 61 | raise ValueError( 62 | "Duration must end with w, h, d, or m (weeks, hours, days, mins)." 63 | ) 64 | if unit == "w": 65 | delta = timedelta(weeks=value) 66 | elif unit == "d": 67 | delta = timedelta(days=value) 68 | elif unit == "h": 69 | delta = timedelta(hours=value) 70 | else: 71 | delta = timedelta(minutes=value) 72 | 73 | max_period = timedelta(days=90) 74 | if delta > max_period: 75 | raise ValueError("Maximum supported duration is 3 months (90 days).") 76 | 77 | start_dt = now - delta 78 | end_dt = now 79 | 80 | if fmt == "unix": 81 | start_val = str(int(start_dt.timestamp() * 1000)) 82 | end_val = str(int(end_dt.timestamp() * 1000)) 83 | else: 84 | start_val = start_dt.isoformat().replace("+00:00", "Z") 85 | end_val = end_dt.isoformat().replace("+00:00", "Z") 86 | return start_val, end_val 87 | 88 | 89 | def generate_timestamp_str(start_time, end_time, duration): 90 | """Generate a timestamp filter string for API queries. 91 | 92 | Args: 93 | start_time (str): Start timestamp. 94 | end_time (str): End timestamp. 95 | duration (str): Duration string. 96 | 97 | Returns: 98 | (str): Formatted filter string "timestamp gt and timestamp lt ". 99 | """ 100 | start_time, end_time = build_timestamp_filter( 101 | start_time=start_time, end_time=end_time, duration=duration 102 | ) 103 | return f"timestamp gt {start_time} and timestamp lt {end_time}" 104 | 105 | 106 | def execute_get(central_conn, endpoint, params={}): 107 | """Execute a GET request to the monitoring API. 108 | 109 | Args: 110 | central_conn (NewCentralBase): Central connection object. 111 | endpoint (str): API endpoint path. 112 | params (dict, optional): Query parameters for the request. 113 | 114 | Returns: 115 | (dict): The message portion of the API response. 116 | 117 | Raises: 118 | ParameterError: If central_conn is None or endpoint is invalid. 119 | Exception: If the API call returns a non-200 status code. 120 | """ 121 | if not central_conn: 122 | raise ParameterError("central_conn(Central connection) is required") 123 | 124 | if not endpoint or not isinstance(endpoint, str) and len(endpoint) == 0: 125 | raise ParameterError("endpoint is required and must be a string") 126 | 127 | if endpoint.startswith("/"): 128 | # remove leading slash if present 129 | endpoint = endpoint.lstrip("/") 130 | 131 | path = generate_url(endpoint, "monitoring") 132 | resp = central_conn.command("GET", path, api_params=params) 133 | 134 | if resp["code"] != 200: 135 | raise Exception( 136 | f"Error retrieving data from {path}: {resp['code']} - {resp['msg']}" 137 | ) 138 | return resp["msg"] 139 | 140 | 141 | def simplified_site_resp(site): 142 | """Simplify the site response structure for easier consumption. 143 | 144 | Args: 145 | site (dict): Raw site data from API response. 146 | 147 | Returns: 148 | (dict): Simplified site data with restructured health, devices, clients, and alerts. 149 | """ 150 | site["health"] = _groups_to_dict(site.get("health", {}).get("groups", [])) 151 | site["devices"] = { 152 | "count": site.get("devices", {}).get("count", 0), 153 | "health": _groups_to_dict( 154 | site.get("devices", {}).get("health", {}).get("groups", []) 155 | ), 156 | } 157 | site["clients"] = { 158 | "count": site.get("clients", {}).get("count", 0), 159 | "health": _groups_to_dict( 160 | site.get("clients", {}).get("health", {}).get("groups", []) 161 | ), 162 | } 163 | site["alerts"] = { 164 | "critical": site.get("alerts", {}) 165 | .get("groups", [{}])[0] 166 | .get("count", 0) 167 | if site.get("alerts", {}).get("groups") 168 | else 0, 169 | "total": site.get("alerts", {}).get("totalCount", 0), 170 | } 171 | site.pop("type", None) 172 | return site 173 | 174 | 175 | def _groups_to_dict(groups_list): 176 | """Convert a list of group objects to a dictionary. 177 | 178 | Args: 179 | groups_list (list): List of group dictionaries with 'name' and 'value' keys. 180 | 181 | Returns: 182 | (dict): Dictionary with group names as keys and values as values. 183 | Defaults to {"Poor": 0, "Fair": 0, "Good": 0}. 184 | """ 185 | result = {"Poor": 0, "Fair": 0, "Good": 0} 186 | if isinstance(groups_list, list): 187 | for group in groups_list: 188 | if ( 189 | isinstance(group, dict) 190 | and "name" in group 191 | and "value" in group 192 | ): 193 | result[group["name"]] = group["value"] 194 | return result 195 | 196 | 197 | def clean_raw_trend_data(raw_results, data=None): 198 | """Clean and restructure raw trend data from API response. 199 | 200 | Args: 201 | raw_results (dict): Raw trend data containing 'graph' with 'keys' and 'samples'. 202 | data (dict, optional): Existing data dictionary to append to. 203 | 204 | Returns: 205 | (dict): Dictionary with timestamps as keys and metric values as nested dictionaries. 206 | """ 207 | if data is None: 208 | data = {} 209 | graph = raw_results.get("graph", {}) or {} 210 | keys = graph.get("keys", []) or [] 211 | samples = graph.get("samples", []) or [] 212 | 213 | for s in samples: 214 | ts = s.get("timestamp") 215 | if not ts: 216 | continue 217 | vals = s.get("data") 218 | if isinstance(vals, (list, tuple)): 219 | for k, v in zip(keys, vals): 220 | data.setdefault(ts, {})[k] = v 221 | else: 222 | target_key = keys[0] if keys else None 223 | if target_key: 224 | data.setdefault(ts, {})[target_key] = vals 225 | else: 226 | # fallback to a generic key if none provided 227 | data.setdefault(ts, {})["value"] = vals 228 | return data 229 | 230 | 231 | def merged_dict_to_sorted_list(merged): 232 | """Convert a merged dictionary to a sorted list of timestamped entries. 233 | 234 | Args: 235 | merged (dict): Dictionary with timestamps as keys. 236 | 237 | Returns: 238 | (list): Sorted list of dictionaries with 'timestamp' key and all associated values. 239 | """ 240 | # try strict RFC3339 parsing (Z -> +00:00), fallback to lexicographic 241 | try: 242 | keys = sorted( 243 | merged.keys(), 244 | key=lambda t: datetime.fromisoformat(t.replace("Z", "+00:00")), 245 | ) 246 | except Exception: 247 | keys = sorted(merged.keys()) 248 | return [{"timestamp": ts, **merged[ts]} for ts in keys] 249 | -------------------------------------------------------------------------------- /pycentral/glp/subscriptions.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2025 Hewlett Packard Enterprise Development LP. 2 | # MIT License 3 | 4 | from ..utils import GLP_URLS, generate_url 5 | from ..utils.glp_utils import rate_limit_check, check_progress 6 | import time 7 | 8 | # This is the single input size limit for using the POST request endpoint for adding subscriptions. 9 | INPUT_SIZE = 5 10 | # This is the rate limit per minute of requests that can be processed by the POST request endpoint for adding subscriptions. 11 | POST_RPM = 4 12 | 13 | SUB_GET_LIMIT = 50 14 | 15 | SUB_LIMIT = 5 16 | 17 | 18 | class Subscriptions(object): 19 | def get_all_subscriptions(self, conn, select=None): 20 | """Get all subscriptions managed in a workspace. 21 | 22 | API will result in 429 if this threshold is breached. 23 | 24 | Args: 25 | conn (NewCentralBase): pycentral base connection object. 26 | select (list, optional): A comma separated list of select properties to return in the response. 27 | By default, all properties are returned. Example: select=id,key 28 | 29 | Returns: 30 | (list[dict]): A list of all subscriptions in the workspace, or an empty list if an error occurs. 31 | """ 32 | conn.logger.info("Getting all subscriptions in GLP workspace") 33 | 34 | limit = SUB_GET_LIMIT 35 | offset = 0 36 | count = 0 37 | result = [] 38 | 39 | while True: 40 | resp = self.get_subscription( 41 | conn, limit=limit, offset=offset, select=select 42 | ) 43 | if resp["code"] == 200: 44 | resp_message = resp["msg"] 45 | count += resp_message["count"] 46 | for subscription in resp_message["items"]: 47 | result.append(subscription) 48 | if count == resp_message["total"]: 49 | break 50 | else: 51 | offset += limit 52 | else: 53 | conn.logger.error( 54 | f"Error fetching list of subscriptions: {resp['code']} - {resp['msg']}" 55 | ) 56 | result = [] 57 | break 58 | 59 | return result 60 | 61 | def get_subscription( 62 | self, 63 | conn, 64 | filter=None, 65 | select=None, 66 | sort=None, 67 | limit=SUB_GET_LIMIT, 68 | offset=0, 69 | ): 70 | """Get a subscription managed in a workspace. 71 | 72 | Rate limits are enforced on this API. 60 requests per minute is supported per workspace. 73 | API will result in 429 if this threshold is breached. 74 | 75 | Args: 76 | conn (NewCentralBase): pycentral base connection object. 77 | filter (str, optional): Filter expressions consisting of simple comparison operations joined by logical operators. 78 | Example: filter=key eq 'MHNBAP0001' and key in 'PAYHAH3YJE6THY, E91A7FDFE04D44C339' 79 | select (list, optional): A comma separated list of select properties to return in the response. 80 | By default, all properties are returned. Example: select=id,key 81 | sort (str, optional): A comma separated list of sort expressions. A sort expression is a property name 82 | optionally followed by a direction indicator asc or desc. Default is ascending order. 83 | Example: sort=key, quote desc 84 | limit (int, optional): Specifies the number of results to be returned. Default is 50. Range: 1-50. 85 | offset (int, optional): Specifies the zero-based resource offset to start the response from. Default is 0. 86 | 87 | Returns: 88 | (dict): API response containing subscriptions. 89 | """ 90 | conn.logger.info("Getting subscriptions in GLP workspace") 91 | path = generate_url(GLP_URLS["SUBSCRIPTION"], category="subscriptions") 92 | 93 | params = {"limit": limit, "offset": offset} 94 | if filter: 95 | params["filter"] = filter 96 | if select: 97 | params["select"] = select 98 | if sort: 99 | params["sort"] = sort 100 | 101 | resp = conn.command( 102 | api_method="GET", api_path=path, api_params=params, app_name="glp" 103 | ) 104 | return resp 105 | 106 | def get_sub_id(self, conn, key): 107 | """Get subscription ID in a GLP workspace based on subscription key. 108 | 109 | Args: 110 | conn (NewCentralBase): pycentral base connection object. 111 | key (str): Subscription key. 112 | 113 | Returns: 114 | (tuple(bool, str)): Tuple of two elements. First element returns True if subscription ID is found, 115 | else False. The second element is a GLP subscription ID if found, else an error message. 116 | """ 117 | 118 | filter = f"key eq '{key}'" 119 | resp = self.get_subscription(conn, filter=filter) 120 | if resp["code"] != 200: 121 | return (resp, (False, "Bad request for get_sub")) 122 | elif resp["msg"]["count"] == 0: 123 | return (False, "Key not found") 124 | else: 125 | return (True, resp["msg"]["items"][0]["id"]) 126 | 127 | def get_status(self, conn, id): 128 | """Get status of an async GLP subscription request. 129 | 130 | Args: 131 | conn (NewCentralBase): pycentral base connection object. 132 | id (str): Transaction ID from async API request. 133 | 134 | Returns: 135 | (dict): Response as provided by 'command' function in NewCentralBase. 136 | """ 137 | 138 | path = generate_url( 139 | f"{GLP_URLS['ASYNC']}/{id}", category="subscriptions" 140 | ) 141 | resp = conn.command("GET", path, "glp") 142 | return resp 143 | 144 | def add_subscription(self, conn, subscriptions=None, limit=0, offset=0): 145 | """Add one or more subscriptions to a workspace. 146 | 147 | This API provides an asynchronous response and will always return "202 Accepted" if basic input 148 | validations are successful. The location header in the response provides the URI to be invoked 149 | for fetching progress of the subscription addition task. For details about the status fetch URL, 150 | refer to the API Get progress or status of async operations in subscriptions. 151 | Rate limits are enforced on this API. 4 requests per minute is supported per workspace. 152 | API will result in 429 if this threshold is breached. 153 | 154 | Args: 155 | conn (NewCentralBase): pycentral base connection object. 156 | subscriptions (list, optional): List of subscription key objects. 157 | Example: [{"key": "string"}] 158 | limit (int, optional): Specifies the number of results to be returned. Default is 0. Range: 1-50. 159 | offset (int, optional): Specifies the zero-based resource offset to start the response from. 160 | 161 | Returns: 162 | (dict): API response from the add subscription operation. 163 | """ 164 | conn.logger.info("Adding subscription(s) to GLP workspace") 165 | path = generate_url(GLP_URLS["SUBSCRIPTION"], category="subscriptions") 166 | 167 | if len(subscriptions) > INPUT_SIZE: 168 | resp = [] 169 | rate_check = rate_limit_check(subscriptions, INPUT_SIZE, POST_RPM) 170 | queue, wait_time = rate_check 171 | 172 | for i in range(len(queue)): 173 | params = { 174 | "offset": offset, 175 | } 176 | 177 | data = {"subscriptions": queue[i]} 178 | 179 | time.sleep(wait_time) 180 | 181 | resp.append( 182 | conn.command( 183 | api_method="POST", 184 | api_path=path, 185 | api_params=params, 186 | api_data=data, 187 | app_name="glp", 188 | ) 189 | ) 190 | 191 | else: 192 | params = { 193 | "offset": offset, 194 | } 195 | 196 | data = {"subscriptions": subscriptions} 197 | 198 | resp = conn.command( 199 | api_method="POST", 200 | api_path=path, 201 | api_params=params, 202 | api_data=data, 203 | app_name="glp", 204 | ) 205 | 206 | if resp["code"] == 202: 207 | conn.logger.info( 208 | "Add subscription(s) to workspace request accepted..." 209 | ) 210 | id = resp["msg"]["transactionId"] 211 | status = check_progress(conn, id, self, limit=SUB_LIMIT) 212 | if status[0]: 213 | conn.logger.info( 214 | "Sucessfully added subscription(s) to workspace!" 215 | ) 216 | return status[1] 217 | else: 218 | conn.logger.error("Add subscription(s) to workspace failed!") 219 | return status[1] 220 | conn.logger.error("Bad request for add subscription(s) to workspace!") 221 | return resp 222 | -------------------------------------------------------------------------------- /pycentral/classic/url_utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | def urlJoin(*args): 24 | trailing_slash = '/' if args[-1].endswith('/') else '' 25 | return "/" + "/".join(map(lambda x: str(x).strip('/'), 26 | args)) + trailing_slash 27 | 28 | 29 | class RefreshUrl(object): 30 | REFRESH_TOKEN = { 31 | "REFRESH": "/oauth2/token" 32 | } 33 | 34 | 35 | class ConfigurationUrl(): 36 | AP_SETTINGS = { 37 | "GET": "/configuration/v2/ap_settings", 38 | "UPDATE": "/configuration/v2/ap_settings" 39 | } 40 | 41 | AP_CONFIGURATION = { 42 | "GET": "/configuration/v1/ap_cli", 43 | "REPLACE": "/configuration/v1/ap_cli" 44 | } 45 | 46 | GROUPS = { 47 | "DELETE": "/configuration/v1/groups", 48 | "UPDATE": "/configuration/v1/groups", 49 | "GET_ALL": "/configuration/v2/groups", 50 | "GET_TEMPLATE_INFO": "/configuration/v2/groups/template_info", 51 | "CREATE": "/configuration/v2/groups", 52 | "CREATE_CLONE": "/configuration/v2/groups/clone" 53 | } 54 | 55 | TEMPLATES = { 56 | "GET": "/configuration/v1/groups", 57 | "CREATE": "/configuration/v1/groups", 58 | "UPDATE": "/configuration/v1/groups", 59 | "DELETE": "/configuration/v1/groups" 60 | } 61 | 62 | DEVICES = { 63 | "GET": "/configuration/v1/devices", 64 | "GET_TEMPLATES": "/configuration/v1/devices/template", 65 | "GET_GRP_TEMPLATES": "/configuration/v1/devices/groups/template", 66 | "SET_SWITCH_CRED": "/configuration/v1/devices", 67 | "MOVE_DEVICES": "/configuration/v1/devices/move" 68 | } 69 | 70 | VARIABLES = { 71 | "GET": "/configuration/v1/devices", 72 | "DELETE": "/configuration/v1/devices", 73 | "CREATE": "/configuration/v1/devices", 74 | "UPDATE": "/configuration/v1/devices", 75 | "REPLACE": "/configuration/v1/devices", 76 | "GET_ALL": "/configuration/v1/devices/template_variables", 77 | "CREATE_ALL": "/configuration/v1/devices/template_variables", 78 | "UPDATE_ALL": "/configuration/v1/devices/template_variables", 79 | "REPLACE_ALL": "/configuration/v1/devices/template_variables" 80 | } 81 | 82 | WLAN = { 83 | "GET": "/configuration/full_wlan", 84 | "GET_ALL": "/configuration/v1/wlan", 85 | "CREATE": "/configuration/v2/wlan", 86 | "CREATE_FULL": "/configuration/full_wlan", 87 | "DELETE": "/configuration/v1/wlan", 88 | "UPDATE": "/configuration/v2/wlan", 89 | "UPDATE_FULL": "/configuration/full_wlan" 90 | } 91 | 92 | 93 | class LicensingUrl(): 94 | SUBSCRIPTIONS = { 95 | "GET_KEYS": "/platform/licensing/v1/subscriptions", 96 | "GET_ENABLED_SVC": "/platform/licensing/v1/services/enabled", 97 | "ASSIGN": "/platform/licensing/v1/subscriptions/assign", 98 | "UNASSIGN": "/platform/licensing/v1/subscriptions/unassign", 99 | "GET_STATS": "/platform/licensing/v1/subscriptions/stats", 100 | "GET_LIC_SVC": "/platform/licensing/v1/services/config", 101 | "UNASSIGN_LIC": "/platform/licensing/v1/subscriptions/devices/all", 102 | "ASSIGN_LIC": "/platform/licensing/v1/subscriptions/devices/all", 103 | "UNASSIGN_LIC_MSP": 104 | "/platform/licensing/v1/msp/subscriptions/devices/all", 105 | "ASSIGN_LIC_MSP": 106 | "/platform/licensing/v1/msp/subscriptions/devices/all"} 107 | 108 | AUTO_LICENSE = { 109 | "GET_SVC": "/platform/licensing/v1/customer/settings/autolicense", 110 | "DISABLE_SVC": "/platform/licensing/v1/customer/settings/autolicense", 111 | "ASSIGN_LIC_SVC": 112 | "/platform/licensing/v1/customer/settings/autolicense", 113 | "DISABLE_LIC_SVC_MSP": 114 | "/platform/licensing/v1/msp/customer/settings/autolicense", 115 | "GET_LIC_SVC_MSP": 116 | "/platform/licensing/v1/msp/customer/settings/autolicense", 117 | "ASSIGN_LIC_SVC_MSP": 118 | "/platform/licensing/v1/msp/customer/settings/autolicense", 119 | "GET_SVC_LIC_TOK": "/platform/licensing/v1/autolicensing/services"} 120 | 121 | 122 | class UserManagementUrl(): 123 | USERS = { 124 | "LIST": "/accounts/v2/users", 125 | "GET_USERS": "/platform/rbac/v1/users", 126 | "GET": "/platform/rbac/v1/users", 127 | "CREATE": "/platform/rbac/v1/users", 128 | "UPDATE": "/platform/rbac/v1/users", 129 | "DELETE": "/platform/rbac/v1/users" 130 | } 131 | 132 | ROLES = { 133 | "GET_ROLES": "/platform/rbac/v1/roles", 134 | "GET": "/platform/rbac/v1/apps", 135 | "CREATE": "/platform/rbac/v1/apps", 136 | "UPDATE": "/platform/rbac/v1/apps", 137 | "DELETE": "/platform/rbac/v1/apps" 138 | } 139 | 140 | 141 | class FirmwareManagementUrl(): 142 | FIRMWARE = { 143 | "GET_ALL_SWARMS": "/firmware/v1/swarms", 144 | "GET_SWARM": "/firmware/v1/swarms", 145 | "GET_VERSIONS_IAP": "/firmware/v1/versions", 146 | "CHECK_VERSION_SUPPORT": "/firmware/v1/versions", 147 | "GET_STATUS": "/firmware/v1/status", 148 | "UPGRADE": "/firmware/v1/upgrade", 149 | "CANCEL": "/firmware/v1/upgrade/cancel" 150 | } 151 | 152 | 153 | class TopoUrl(): 154 | TOPOLOGY = { 155 | "GET_TOPO_SITE": "/topology_external_api", 156 | "GET_DEVICES": "/topology_external_api/devices", 157 | "GET_EDGES": "/topology_external_api/v2/edges", 158 | "GET_UPLINK": "/topology_external_api/uplinks", 159 | "GET_TUNNEL": "/topology_external_api/tunnels", 160 | "GET_AP_LLDP": "/topology_external_api/apNeighbors" 161 | } 162 | 163 | 164 | class RapidsUrl(): 165 | ROGUES = { 166 | "GET_ROGUE_AP": "/rapids/v1/rogue_aps", 167 | "GET_INTERFERING_AP": "/rapids/v1/interfering_aps", 168 | "GET_SUSPECT_AP": "/rapids/v1/suspect_aps", 169 | "GET_NEIGHBOR_AP": "/rapids/v1/neighbor_aps" 170 | } 171 | 172 | WIDS = { 173 | "GET_INFRA_ATTACKS": "/rapids/v1/wids/infrastructure_attacks", 174 | "GET_CLIENT_ATTACKS": "/rapids/v1/wids/client_attacks", 175 | "GET_WIDS_EVENTS": "/rapids/v1/wids/events" 176 | } 177 | 178 | 179 | class AuditUrl(): 180 | TRAIL_LOG = { 181 | "GET_ALL": "/platform/auditlogs/v1/logs", 182 | "GET": "/platform/auditlogs/v1/logs" 183 | } 184 | 185 | EVENT_LOG = { 186 | "GET_ALL": "/auditlogs/v1/events", 187 | "GET": "/auditlogs/v1/event_details" 188 | } 189 | 190 | 191 | class VisualrfUrl(): 192 | CLIENT_LOCATION = { 193 | "GET_CLIENT_LOC": "/visualrf_api/v1/client_location/", 194 | "GET_FLOOR_CLIENTS": "/visualrf_api/v1/floor" 195 | } 196 | 197 | ROGUE_LOCATION = { 198 | "GET_AP_LOC": "/visualrf_api/v1/rogue_location", 199 | "GET_FLOOR_APS": "/visualrf_api/v1/floor" 200 | } 201 | 202 | FLOOR_PLAN = { 203 | "GET_CAMPUS_LIST": "/visualrf_api/v1/campus", 204 | "GET_CAMPUS_INFO": "/visualrf_api/v1/campus", 205 | "GET_BUILDING_INFO": "/visualrf_api/v1/building", 206 | "GET_FLOOR_INFO": "/visualrf_api/v1/floor", 207 | "GET_FLOOR_IMG": "/visualrf_api/v1/floor", 208 | "GET_FLOOR_APS": "/visualrf_api/v1/floor", 209 | "GET_AP_LOC": "/visualrf_api/v1/access_point_location" 210 | } 211 | 212 | 213 | class MonitoringUrl(): 214 | SITES = { 215 | "GET_ALL": "/central/v2/sites", 216 | "CREATE": "/central/v2/sites", 217 | "DELETE": "/central/v2/sites", 218 | "UPDATE": "/central/v2/sites", 219 | "ADD_DEVICE": "/central/v2/sites/associate", 220 | "DELETE_DEVICE": "/central/v2/sites/associate", 221 | "ADD_DEVICES": "/central/v2/sites/associations", 222 | "DELETE_DEVICES": "/central/v2/sites/associations" 223 | } 224 | 225 | 226 | class InventoryUrl(): 227 | DEVICES = { 228 | "GET_DEVICES": "/platform/device_inventory/v1/devices", 229 | "ARCHIVE_DEVICES": "/platform/device_inventory/v1/devices/archive", 230 | "UNARCHIVE_DEVICES": "/platform/device_inventory/v1/devices/unarchive", 231 | "ADD_DEVICE": "/platform/device_inventory/v1/devices" 232 | } 233 | 234 | 235 | class MspURL(): 236 | MSP = { 237 | "V1_CUSTOMER": "/msp_api/v1/customers", 238 | "V2_CUSTOMER": "/msp_api/v2/customers", 239 | "COUNTRY_CODE": "/msp_api/v2/get_country_code", 240 | "USERS": "/msp_api/v1/customers/users", 241 | "RESOURCES": "/msp_api/v1/resource", 242 | "DEVICES": "/msp_api/v1/devices", 243 | "GROUPS": "/msp_api/v1/groups" 244 | } 245 | -------------------------------------------------------------------------------- /pycentral/classic/base_utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2020 Aruba, a Hewlett Packard Enterprise company 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import os 25 | from urllib.parse import urlencode, urlparse, urlunparse 26 | from .constants import CLUSTER_API_BASE_URL_LIST 27 | try: 28 | import colorlog # type: ignore 29 | COLOR = True 30 | except (ImportError, ModuleNotFoundError): 31 | COLOR = False 32 | 33 | C_LOG_LEVEL = { 34 | "CRITICAL": 50, 35 | "ERROR": 40, 36 | "WARNING": 30, 37 | "INFO": 20, 38 | "DEBUG": 10, 39 | "NOTSET": 0 40 | } 41 | 42 | C_DEFAULT_ARGS = { 43 | "base_url": None, 44 | "client_id": None, 45 | "client_secret": None, 46 | "customer_id": None, 47 | "username": None, 48 | "password": None, 49 | "token": None 50 | } 51 | 52 | URL_BASE_ERR_MESSAGE = "Please provide either the base_url of API Gateway or a valid cluster_name of cluster where Central account is provisioned!" 53 | 54 | 55 | class clusterBaseURL(object): 56 | """This class helps to fetch the API Base URL when Aruba Central cluster 57 | name is provided. 58 | """ 59 | 60 | def getBaseURL(self, cluster_name): 61 | """This method returns the API Base URL of the provided Aruba Central\ 62 | cluster. 63 | 64 | :param cluster_name: Name of the Aruba Central cluster whose base_url\ 65 | needs to be returned 66 | :type cluster_name: str 67 | :return: Base URL of provided cluster 68 | :rtype: str 69 | """ 70 | if cluster_name in CLUSTER_API_BASE_URL_LIST: 71 | return f'https://{CLUSTER_API_BASE_URL_LIST[cluster_name]}' 72 | else: 73 | errorMessage = (f"Unable to find cluster_name - {cluster_name}.\n" + 74 | URL_BASE_ERR_MESSAGE) 75 | raise ValueError(errorMessage) 76 | 77 | def getAllBaseURLs(self): 78 | """This method returns the list of base URLs of all the clusters of\ 79 | Aruba Central 80 | 81 | :return: List of all valid base URLs of Aruba Central 82 | :rtype: list 83 | """ 84 | return list(CLUSTER_API_BASE_URL_LIST.values()) 85 | 86 | 87 | c = clusterBaseURL() 88 | 89 | 90 | def parseInputArgs(central_info): 91 | """This method parses user input, checks for the availability of mandatory\ 92 | arguments. If the user opts to provide a cluster_name instead of\ 93 | base_url, the method will use the cluster_name to fetch the base_url.\ 94 | Optional missing parameters in central_info variable is\ 95 | initialized as defined in C_DEFAULT_ARGS. 96 | 97 | :param central_info: central_info dictionary as read from user's input\ 98 | file. 99 | :type central_info: dict 100 | :return: parsed central_info dict with missing optional params set to\ 101 | default values. 102 | :rtype: dict 103 | """ 104 | if not central_info: 105 | exit("Error: Invalid Input!") 106 | 107 | valid_url_input_keys = ["cluster_name", "base_url"] 108 | url_conditional = [cluster_var in central_info.keys() 109 | for cluster_var in valid_url_input_keys] 110 | if all(url_conditional): 111 | errorMessage = ("Cannot provide both base_url & cluster_name in token information. " + 112 | URL_BASE_ERR_MESSAGE) 113 | raise KeyError(errorMessage) 114 | elif any(url_conditional): 115 | if "cluster_name" in central_info: 116 | central_info['base_url'] = c.getBaseURL( 117 | central_info['cluster_name']) 118 | elif "base_url" in central_info: 119 | central_info['base_url'] = valid_url(central_info['base_url']) 120 | else: 121 | raise KeyError(URL_BASE_ERR_MESSAGE) 122 | 123 | default_dict = dict(C_DEFAULT_ARGS) 124 | for key in default_dict.keys(): 125 | if key in central_info: 126 | default_dict[key] = central_info[key] 127 | 128 | return default_dict 129 | 130 | 131 | def tokenLocalStoreUtil(token_store, customer_id="customer", 132 | client_id="client"): 133 | """Utility function for storeToken and loadToken default access token\ 134 | storage/cache method. This function generates unique file name for a\ 135 | customer and API gateway client to store and load access token in the\ 136 | local machine for reuse. The format of the file name is\ 137 | tok__.json. If customer_id or client_id is not\ 138 | provided, default values mentioned in args will be used. 139 | 140 | :param token_store: Placeholder to support different token storage\ 141 | mechanism. \n 142 | * keyword type: Place holder for different token storage mechanism.\ 143 | Defaults to local storage. \n 144 | * keyword path: path where temp folder is created to store token JSON\ 145 | file. \n 146 | :type token_store: dict 147 | :param customer_id: Aruba Central customer id, defaults to "customer" 148 | :type customer_id: str, optional 149 | :param client_id: API Gateway client id, defaults to "client" 150 | :type client_id: str, optional 151 | :return: Filename for access token storage. 152 | :rtype: str 153 | """ 154 | fileName = "tok_" 155 | if customer_id is not None: 156 | fileName += str(customer_id) 157 | fileName += "_" + str(client_id) + ".json" 158 | filePath = os.path.join(os.getcwd(), "temp") 159 | if token_store and "path" in token_store: 160 | filePath = os.path.join(token_store["path"]) 161 | fullName = os.path.join(filePath, fileName) 162 | return fullName 163 | 164 | 165 | def get_url(base_url, path='', params='', query={}, fragment=''): 166 | """This method constructs complete URL based on multiple parts of URL. 167 | 168 | :param base_url: base url for a HTTP request 169 | :type base_url: str 170 | :param path: API endpoint path, defaults to '' 171 | :type path: str, optional 172 | :param params: API endpoint path parameters, defaults to '' 173 | :type params: str, optional 174 | :param query: HTTP request url query parameters, defaults to {} 175 | :type query: dict, optional 176 | :param fragment: URL fragment identifier, defaults to '' 177 | :type fragment: str, optional 178 | :return: Parsed URL 179 | :rtype: class:`urllib.parse.ParseResult` 180 | """ 181 | base_url = valid_url(base_url) 182 | parsed_baseurl = urlparse(base_url) 183 | scheme = parsed_baseurl.scheme 184 | netloc = parsed_baseurl.netloc 185 | query = urlencode(query) 186 | url = urlunparse((scheme, netloc, path, params, query, fragment)) 187 | return url 188 | 189 | 190 | def valid_url(url): 191 | """This method verifies & returns the URL in a valid format. If the URL is\ 192 | missing the https prefix, the function will prepend the prefix after\ 193 | verifiying that its a valid base URL of an Aruba Central cluster. 194 | 195 | :param base_url: base url for a HTTP request 196 | :type base_url: str 197 | :return: Valid Base URL 198 | :rtype: str 199 | """ 200 | parsed_url = urlparse(url) 201 | if all([parsed_url.scheme, parsed_url.netloc]): 202 | return parsed_url.geturl() 203 | elif bool(parsed_url.scheme) is False and bool(parsed_url.path): 204 | parsed_url = parsed_url._replace(**{"scheme": "https", 205 | "netloc": parsed_url.path, 206 | "path": ''}) 207 | return parsed_url.geturl() 208 | else: 209 | errorMessage = ('Invalid Base URL - ' + f'{url}\n' + 210 | URL_BASE_ERR_MESSAGE) 211 | raise ValueError(errorMessage) 212 | 213 | 214 | def console_logger(name, level="DEBUG"): 215 | """This method create an instance of python logging and sets the following\ 216 | format for log messages.\n