Skip to content

MSP

The MSP module provides MSPBase, an extension of NewCentralBase designed for Managed Service Providers (MSPs). It handles MSP-level authentication and provides isolated, tenant-scoped API connections through a token-exchange flow — all without managing tokens manually.

Unified Credentials Only

MSP features are available starting in PyCentral SDK v2.0a19. MSPBase exclusively uses "unified" credentials. Standalone "glp" or "new_central" entries are not supported.

Overview

MSPBase builds on NewCentralBase and adds:

  • MSP-level GLP and New Central API calls using a single "unified" credential
  • Per-tenant connections via get_tenant_connection(), which exchanges the MSP token for a tenant-scoped token
  • Automatic token renewal — when a token expires (MSP or tenant), the SDK refreshes it transparently
  • Tenant connection caching — repeated calls with the same tenant_workspace_id return the same connection without re-running the token exchange

Authentication

All MSP connections use a single set of "unified" credentials. Provide a workspace_id for your MSP workspace, along with your client_id and client_secret.

To also enable New Central API calls (e.g. for monitoring or configuration), include a cluster_name or a base_url for the Central cluster your MSP account is provisioned on.

msp_token.yaml
unified:
  client_id: <client-id>
  client_secret: <client-secret>
  workspace_id: <workspace-id>
  cluster_name: US-WEST-5   # Optional — include to enable New Central API calls

Basic Setup

MSP-Level API Calls

Once you have the msp_token.yaml file ready, you can make MSP-level API calls to both New Central and GLP.

msp_api_calls.py
from pycentral import MSPBase

with MSPBase(token_info="msp_token.yaml") as msp:
    # New Central API call — list MSP tenants
    response = msp.command(
            api_method="GET",
            api_path="network-msp/v1/list-tenants",
            api_params={"limit": 100, "next": 1},
    )

    if response["code"] == 200:
        tenants = response["msg"].get("items", [])
        print(f"Total tenants: {len(tenants)}")
        for tenant in tenants:
            print(f'Central Tenant ID - {tenant.get("tenantId")}, Tenant Name - {tenant.get("tenantName")}')
    else:
        print(f"Error {response['code']}: {response['msg']}")

GLP-Only Mode

If you only need GLP API calls (e.g. listing tenants, managing subscriptions), omit cluster_name / base_url from your credentials. Only the GLP endpoint will be configured.


Tenant Connections

get_tenant_connection() exchanges the MSP token for a tenant-scoped token and returns a connection that behaves exactly like NewCentralBase. Use it for all tenant-scoped API calls.

The connection is cached — calling get_tenant_connection() again with the same tenant_workspace_id returns the same object without repeating the token exchange.

Tenant Workspace ID

tenant_workspace_id is the GLP workspace ID for the tenant. This is an ID from GLP for the tenant similar to the workspace_id in your MSP credentials, but scoped to the individual tenant. It can be found in the tenantId field of the GLP tenant list response. The SDK automatically strips dashes from the value (e.g. "abc-def-123" becomes "abcdef123") to match GLP API requirements. This normalization is applied consistently across get_tenant_connection() and close_tenant_connection().

Create a Tenant Connection

You can create a tenant connection by providing either a tenant_workspace_id or a tenant_name. When using tenant_name, the SDK resolves it to a tenant_workspace_id via a GLP API call.

tenant_connection.py
from pycentral import MSPBase

with MSPBase(token_info="msp_token.yaml") as msp:
    # Option 1: Connect by tenant workspace ID (from the GLP tenant list)
    tenant_conn = msp.get_tenant_connection(tenant_workspace_id="<tenant-workspace-id>")

    # Option 2: Connect by tenant name (resolves to workspace ID via GLP API)
    # tenant_conn = msp.get_tenant_connection(tenant_name="<tenant-name>")

Tenant — New Central API Call

The tenant connection inherits the same Central base URL from the MSP credentials. Provide cluster_name or base_url in your MSP credentials to enable New Central calls.

tenant_new_central.py
from pycentral import MSPBase

with MSPBase(token_info="account_credentials_new.yaml") as msp:
        tenant_conn = msp.get_tenant_connection(tenant_workspace_id="<tenant-workspace-id>")

        # List APs in the tenant's Central environment
        response = tenant_conn.command(
            api_method="GET",
            api_path="network-monitoring/v1/aps",
        )
        if response["code"] == 200:
            aps = response["msg"].get("items", [])
            print(f"Tenant APs: {len(aps)}")
            for ap in aps:
                print(ap.get("serialNumber"), ap.get("deviceName"), ap.get("status"))
        else:
            print(f"Error {response['code']}: {response['msg']}")

Tenant — GLP API Call

Use the tenant connection to make GLP calls scoped to that tenant's workspace.

tenant_glp.py
from pycentral import MSPBase

 with MSPBase(token_info="account_credentials_new.yaml") as msp:
    tenant_conn = msp.get_tenant_connection(tenant_name="The Wandwright Academy")

    # Specify app_name="glp" to make API call to GreenLake.
    response = tenant_conn.command(
        api_method="GET",
        api_path="audit-log/v1/logs",
        app_name="glp",
    )

    if response["code"] == 200:
        logs = response["msg"].get("items", [])
        print(f"Tenant audit logs: {len(logs)}")
        for log in logs:
            print(log.get("createdAt"), log.get("category"), log.get("user"))
    else:
        print(f"Error {response['code']}: {response['msg']}")

Token Renewal

Token renewal is handled automatically by the SDK. No manual intervention is required.

Scenario What Happens
MSP token expires during an API call SDK refreshes the MSP token using client credentials and retries
Tenant token expires during an API call SDK refreshes the MSP parent token first, then re-exchanges it for a fresh tenant-scoped token, and retries
MSP token expires during tenant token exchange SDK renews the MSP token and retries the exchange (up to one retry)

Managing Connections

Both MSPBase and tenant connections support Python's with statement, which ensures all HTTP clients are closed cleanly when the block exits.

manage_connections.py
from pycentral import MSPBase

with MSPBase(token_info="msp_token.yaml") as msp:
    tenant_conn = msp.get_tenant_connection(tenant_workspace_id="<tenant-workspace-id>")

    # Use tenant_conn for API calls...

    # Optionally close a single tenant connection mid-session
    msp.close_tenant_connection(tenant_workspace_id="<tenant-workspace-id>")

# msp.close() is called automatically — closes MSP and all remaining tenant connections

Manual Cleanup

If you're not using with, call msp.close() when done to release HTTP client resources for both the MSP connection and all cached tenant connections.


Next Steps


API Reference

MSPBase(token_info, logger=None, log_level='INFO', enable_scope=False)

Bases: NewCentralBase

NewCentralBase subclass with MSP tenant token-exchange support.

All constructor arguments are identical to NewCentralBase. MSP mode exclusively uses "unified" credentials so that both GLP and Central API calls share a single token, while per-tenant access is obtained via get_tenant_connection().

Tenant connections are cached internally: calling get_tenant_connection() with the same tenant_workspace_id multiple times always returns the same TenantBase instance without re-running the token exchange. Use close_tenant_connection() to explicitly close a single tenant connection, or close() (inherited) to close the MSP connection and all cached tenant connections.

Parameters:

Name Type Description Default
token_info dict or str

Token information dict or path to a YAML/JSON credentials file. See NewCentralBase for full details.

required
logger Logger

External logger. Defaults to None.

None
log_level str

Log level string. Defaults to "INFO".

'INFO'
enable_scope bool

Whether to initialise scope management. Defaults to False.

False
Source code in pycentral/msp/msp_base.py
def __init__(self, token_info, logger=None, log_level="INFO", enable_scope=False):
    # Tenant connection cache: {tenant_workspace_id: TenantBase}
    # Initialised before super().__init__ so it exists if any subclass hook
    # were to call get_tenant_connection during initialisation.
    self._tenant_connections: dict = {}
    super().__init__(
        token_info=token_info,
        logger=logger,
        log_level=log_level,
        enable_scope=enable_scope,
    )

get_tenant_connection(tenant_name=None, tenant_workspace_id=None) -> TenantBase

Obtain a tenant-scoped connection for the given tenant.

Returns a cached TenantBase if one already exists for tenant_workspace_id. Otherwise performs the token exchange, creates a new connection, caches it, and returns it.

Token expiry is handled automatically: when a cached connection receives a 401, it renews the MSP parent token and re-exchanges it for a fresh tenant token without any additional caller action.

Parameters:

Name Type Description Default
tenant_name str

The display name of the MSP tenant.

None
tenant_workspace_id str

The tenant's GLP workspace ID, used as the subject in the token exchange request. Find this in the GLP tenant list response (tenantId field).

None

Returns:

Type Description
TenantBase

A connection instance scoped to the specified tenant's Central environment. Use its command() method for all tenant-scoped API calls. The same object is returned on subsequent calls with the same tenant_workspace_id.

Raises:

Type Description
ValueError

If neither tenant_name nor tenant_workspace_id is provided, or if Central is not configured on this MSP instance (no cluster_name or base_url under "unified", and no standalone "glp" entry).

LoginError

If MSP token renewal or the token exchange fails.

Source code in pycentral/msp/msp_base.py
def get_tenant_connection(
    self, tenant_name=None, tenant_workspace_id=None
) -> TenantBase:
    """
    Obtain a tenant-scoped connection for the given tenant.

    Returns a cached TenantBase if one already exists for
    tenant_workspace_id. Otherwise performs the token exchange, creates a
    new connection, caches it, and returns it.

    Token expiry is handled automatically: when a cached connection receives
    a 401, it renews the MSP parent token and re-exchanges it for a fresh
    tenant token without any additional caller action.

    Args:
        tenant_name (str, optional): The display name of the MSP tenant.
        tenant_workspace_id (str, optional): The tenant's GLP workspace ID,
            used as the subject in the token exchange request. Find this in
            the GLP tenant list response (``tenantId`` field).

    Returns:
        (TenantBase): A connection instance scoped to the specified
            tenant's Central environment. Use its command() method for all
            tenant-scoped API calls. The same object is returned on subsequent
            calls with the same tenant_workspace_id.

    Raises:
        ValueError: If neither tenant_name nor tenant_workspace_id is provided,
            or if Central is not configured on this MSP instance (no cluster_name
            or base_url under "unified", and no standalone "glp" entry).
        LoginError: If MSP token renewal or the token exchange fails.
    """
    if tenant_workspace_id is None and tenant_name is None:
        raise ValueError("Either 'tenant_workspace_id' or 'tenant_name' must be provided.")

    if tenant_name:
        tenant_workspace_id = self.get_tenant_id(tenant_name)

    if type(tenant_workspace_id) is not str:
        raise ValueError("tenant_workspace_id must be a string.")
    # Normalize tenant_workspace_id by removing dashes as per GLP API requirements
    tenant_workspace_id = tenant_workspace_id.replace("-", "")
    # Return cached connection if it already exists
    if tenant_workspace_id in self._tenant_connections:
        self.logger.debug(
            f"Returning cached tenant connection for tenant_workspace_id='{tenant_workspace_id}'"
        )
        return self._tenant_connections[tenant_workspace_id]

    self.logger.info(f"Creating new tenant connection for tenant_workspace_id='{tenant_workspace_id}'")

    # Exchange the MSP token for a tenant-scoped token
    tenant_access_token = self._exchange_tenant_token(tenant_workspace_id)

    # Build a minimal token_info for the tenant connection using unified mode
    # (no client credentials needed — renewal delegates back to this MSP instance).
    tenant_token_info = {
        "unified": {
            "glp_base_url": self.token_info["unified"].get("glp_base_url"),
            "base_url": self.token_info["unified"].get("base_url"),
            "access_token": tenant_access_token,
        },
    }

    conn = TenantBase(
        token_info=tenant_token_info,
        msp_parent=self,
        tenant_workspace_id=tenant_workspace_id,
        logger=self.logger,
    )
    self._tenant_connections[tenant_workspace_id] = conn
    self.logger.info(f"Tenant connection for '{tenant_workspace_id}' established and cached.")
    return conn

close_tenant_connection(tenant_workspace_id: str) -> None

Close and evict the cached connection for a specific tenant.

Releases the underlying HTTP client resources for the given tenant and removes it from the cache. The next call to get_tenant_connection() with the same tenant_workspace_id will perform a fresh token exchange.

Parameters:

Name Type Description Default
tenant_workspace_id str

The tenant's GLP workspace ID whose cached connection should be closed.

required

Raises:

Type Description
KeyError

If no cached connection exists for tenant_workspace_id.

Source code in pycentral/msp/msp_base.py
def close_tenant_connection(self, tenant_workspace_id: str) -> None:
    """
    Close and evict the cached connection for a specific tenant.

    Releases the underlying HTTP client resources for the given tenant and
    removes it from the cache. The next call to get_tenant_connection() with
    the same tenant_workspace_id will perform a fresh token exchange.

    Args:
        tenant_workspace_id (str): The tenant's GLP workspace ID whose
            cached connection should be closed.

    Raises:
        KeyError: If no cached connection exists for tenant_workspace_id.
    """
    tenant_workspace_id = tenant_workspace_id.replace("-", "")
    if tenant_workspace_id not in self._tenant_connections:
        raise KeyError(
            f"No cached tenant connection found for tenant_workspace_id='{tenant_workspace_id}'. "
            "Use get_tenant_connection() to create one first."
        )
    conn = self._tenant_connections.pop(tenant_workspace_id)
    try:
        conn.close()
    except Exception as err:
        self.logger.debug(
            f"Error closing HTTP clients for tenant '{tenant_workspace_id}': {err}"
        )
    self.logger.info(f"Tenant connection for '{tenant_workspace_id}' closed.")

close() -> None

Close the MSP connection and all cached tenant connections.

Closes every cached TenantBase first, then closes the MSP-level HTTP clients inherited from NewCentralBase.

Source code in pycentral/msp/msp_base.py
def close(self) -> None:
    """
    Close the MSP connection and all cached tenant connections.

    Closes every cached TenantBase first, then closes the
    MSP-level HTTP clients inherited from NewCentralBase.
    """
    for tenant_workspace_id in list(self._tenant_connections):
        try:
            self._tenant_connections[tenant_workspace_id].close()
        except Exception as err:
            self.logger.debug(
                f"Error closing tenant connection '{tenant_workspace_id}' during MSP close: {err}"
            )
    self._tenant_connections.clear()
    super().close()

get_tenant_id(tenant_name)

Resolve a tenant display name to its GLP workspace ID.

Queries the GLP MSP tenants API and returns the workspace ID for the given tenant name. This workspace ID is required for the token exchange performed by get_tenant_connection().

Parameters:

Name Type Description Default
tenant_name str

The display name of the tenant to resolve.

required

Returns:

Type Description
str

The tenant's GLP workspace ID.

Raises:

Type Description
ValueError

If the tenant_name cannot be resolved to a workspace ID.

Source code in pycentral/msp/msp_base.py
def get_tenant_id(self, tenant_name):
    """
    Resolve a tenant display name to its GLP workspace ID.

    Queries the GLP MSP tenants API and returns the workspace ID for the
    given tenant name. This workspace ID is required for the token exchange
    performed by get_tenant_connection().

    Args:
        tenant_name (str): The display name of the tenant to resolve.

    Returns:
        (str): The tenant's GLP workspace ID.

    Raises:
        ValueError: If the tenant_name cannot be resolved to a workspace ID.
    """
    api_path = "workspaces/v1/msp-tenants"
    api_method = "GET"
    safe_name = tenant_name.replace("'", "''")
    api_params = {"filter": f"workspaceName eq '{safe_name}'"}
    response = self.command(
        api_method=api_method,
        api_path=api_path,
        app_name="glp",
        api_params=api_params,
    )
    if response["code"] != 200:
        raise ValueError(
            f"Failed to retrieve tenants: {response.get('msg', 'No error message provided')}"
        )
    response_msg = response.get("msg", {})
    tenant = response_msg.get("items", [])

    if not tenant:
        raise ValueError(f"No tenant found with name '{tenant_name}'")
    if len(tenant) > 1:
        raise ValueError(f"Multiple tenants found with name '{tenant_name}'")
    tenant_workspace_id = tenant[0].get("id")
    return tenant_workspace_id

TenantBase(token_info, msp_parent, tenant_workspace_id, logger)

Bases: NewCentralBase

A tenant-scoped connection owned by MSPBase.

Not intended to be instantiated directly — obtain instances via MSPBase.get_tenant_connection().

On a 401 response, _renew_token() renews the parent MSP's shared token first, then re-exchanges it for a fresh tenant-scoped token, keeping both connections in sync.

Parameters:

Name Type Description Default
token_info dict

Pre-built token_info dict, already resolved — bypasses new_parse_input_args.

required
msp_parent MSPBase

The parent MSP instance that owns this tenant connection.

required
tenant_workspace_id str

The tenant's GLP workspace ID, used as the subject in the token exchange request.

required
logger Logger

Shared logger inherited from the parent MSP instance.

required
Source code in pycentral/msp/tenant_base.py
def __init__(self, token_info, msp_parent, tenant_workspace_id, logger):
    self._msp_parent = msp_parent
    self._tenant_workspace_id = tenant_workspace_id
    # Bypass new_parse_input_args — token_info is already fully resolved.
    # Directly initialise the minimal NewCentralBase state we need.
    self.token_info = token_info
    self.token_file_path = None
    self.logger = logger
    self._app_routes = self._build_app_routes()
    self.scopes = None
    self._http_clients = {}
    self._initialize_http_clients()