Example script

Python example for TS7785 REST API authentication and clusters endpoint queries.

The following sample program written in Python can do the following:
  • Access the token with the provided MI username and password.
  • Interact with necessary endpoints to configure a virtual library.

Sample Python script

To use this script:
  • RESTDriver.py provides a helper class and functions to interact with RESTful API endpoints.
  • Update server variable in an instance of RESTDriver class.
  • Update username and password to match the valid Management Interface user ID and password on the target TS7785.
  • In order to try other endpoints, refer to create_ and list_ functions how to interact with endpoints.
Figure 1. Sample 1-VTL Configuration
#!/usr/bin/python3

'''
IBM Doc :
TS7785 -> Managing -> Management interfaces: GUI and REST API -> IBM TS7785 REST API

Demonstrates:
  --- VTL Lifecycle (from sample1_VTL.py) ---
  - List virtual libraries               GET  /virtualLibraries
  - Create a virtual library             POST /virtualLibraries

  --- Host / ACL / Mapping ---
  - List cluster ports                  GET  /clusterPorts
  - List hosts                          GET  /hosts
  - List / create host groups           GET  /hostGroups  POST /hostGroups
  - List / create virtual ports         GET  /virtualPorts  POST /virtualPorts
  - List / create host ports            GET  /hostPorts  POST /hostPorts
  - Create an ACL entry                 POST /accessControlLists
  - Map a host group ↔ host port        POST /hostGroups/hostPortMappings
  - Map a host group ↔ virtual port     POST /hostGroups/virtualPortMappings

Prerequisite:
  - A valid virtual library exists in the system, OR
  - Run the VTL creation section in main() to create one automatically.
'''

from RESTDriver import RESTDriver
from typing import Optional
import sys
import time

# ---------------------------------------------------------------------------
# Sample payloads — replace placeholder UUIDs before enabling POST calls
#
# Where to get UUID values:
#  - VIRTUAL_LIBRARY_UUID  : list_virtual_libraries() or create_virtual_library()
#  - clusterPortUUID       : list_cluster_ports()
#  - HOST_GROUP_UUID       : create_host_group()
#  - hostPortUUID          : create_host_port()
#  - virtualPortUUID       : create_virtual_port()
#  - accessControlListUUID : create_acl()
#
#  - SAMPLE_HOST_PORT_PAYLOAD['wwpn'] : get from your host :)
# ---------------------------------------------------------------------------

# Used only if you skip Section 2 (VTL creation) and supply a pre-existing VTL.
# If you run Section 2, vtl_uuid is set at runtime and the payloads are updated
# in Section 4 — this constant is not used for those fields.
VIRTUAL_LIBRARY_UUID = "00000000-0000-0000-0000-000000000001"   # REPLACE if skipping Section 2

# Example for a 2-way Grid: cluster 1 (3 drives) and cluster 3 (5 drives)
SAMPLE_VTL_CREATE_PAYLOAD = {
    "name": "testvtl",
    "description": "test vtl",
    "storageSlots": 100,
    "ioSlots": 100,
    "virtualDrives": [
        {"cluster": 0, "count": 3},
        {"cluster": 1, "count": 5},
    ],
}

SAMPLE_VTL_UPDATE_PAYLOAD = {
    "description": "description updated",
    "storageSlots": 200,
}

SAMPLE_HOST_GROUP_PAYLOAD = {"name": "sample-host-group-01"}

# Virtual port on cluster 0 — clusterPortUUID must belong to cluster 0
SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER0 = {
    "name": "sample-virtual-port-c0-01",
    "owningVirtualLibraryUUID": "REPLACE",  # updated at runtime in Section 4
    "clusterPortUUID": "00000000-0000-0000-0000-000000000009",  # REPLACE: cluster 0 port
}
# Virtual port on cluster 1 — clusterPortUUID must belong to cluster 1
SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER1 = {
    "name": "sample-virtual-port-c1-01",
    "owningVirtualLibraryUUID": "REPLACE",  # updated at runtime in Section 4
    "clusterPortUUID": "00000000-0000-0000-0000-000000000002",  # REPLACE: cluster 1 port
}

SAMPLE_HOST_PORT_PAYLOAD = {
    "hostName": "sample-host-01",
    "wwpn": "00:00:00:00:00:00:00:FF",                          # REPLACE
}

SAMPLE_ACL_PAYLOAD_CLUSTER0 = {
    "name": "sample-acl-c0-01",
    "description": "Sample ACL cluster 0",
    "virtualDriveStartIndex": 0,    # REPLACE: 0-based index of the first virtual drive
                                    #   to expose via this ACL (0 = first drive in the VTL)
    "virtualDriveCount": 3,         # REPLACE: number of consecutive drives to expose,
                                    #   starting from virtualDriveStartIndex
    "clusterID": 0,                 # cluster 0
    "virtualLibraryUUID": "REPLACE",  # updated at runtime in Section 4
}

SAMPLE_ACL_PAYLOAD_CLUSTER1 = {
    "name": "sample-acl-c1-01",
    "description": "Sample ACL cluster 1",
    "virtualDriveStartIndex": 0,    # REPLACE: 0-based index of the first virtual drive
                                    #   to expose via this ACL (0 = first drive in the VTL)
    "virtualDriveCount": 5,         # REPLACE: number of consecutive drives to expose,
                                    #   starting from virtualDriveStartIndex
    "clusterID": 1,                 # cluster 1
    "virtualLibraryUUID": "REPLACE",  # updated at runtime in Section 4
}

SAMPLE_HOST_PORT_MAPPING = {
    "hostGroupUUID": "00000000-0000-0000-0000-000000000003",
    "hostPortUUID":  "00000000-0000-0000-0000-000000000004",
}

# POST /hostGroups/virtualPortMappings accepts one mapping dict per call.
SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER0 = {
    "hostGroupUUID":         "00000000-0000-0000-0000-000000000003",
    "virtualPortUUID":       "00000000-0000-0000-0000-000000000005",  # filled at runtime (cluster 0)
    "accessControlListUUID": "00000000-0000-0000-0000-000000000006",  # filled at runtime (cluster 0)
}

SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER1 = {
    "hostGroupUUID":         "00000000-0000-0000-0000-000000000003",
    "virtualPortUUID":       "00000000-0000-0000-0000-000000000007",  # filled at runtime (cluster 1)
    "accessControlListUUID": "00000000-0000-0000-0000-000000000008",  # filled at runtime (cluster 1)
}


# ---------------------------------------------------------------------------
# VTL Lifecycle functions
# ---------------------------------------------------------------------------

def list_virtual_libraries(client: RESTDriver) -> list:
    """GET /virtualLibraries — return list of all configured VTLs."""
    rc, response = client.get_method("virtualLibraries")
    if rc != 0:
        print("  Failed to GET virtualLibraries")
        return []
    vtls = response.json()
    vtls = vtls if isinstance(vtls, list) else []
    print(f"  Virtual Libraries ({len(vtls)}):")
    for vtl in vtls:
        print(f"    uuid={vtl.get('uuid')}  name={vtl.get('name')}")
    return vtls


def create_virtual_library(client: RESTDriver, payload: dict) -> str:
    """POST /virtualLibraries — create a VTL and return its UUID.

    After a successful POST the list is re-queried to obtain the UUID
    assigned to the newly created VTL (identified by name).
    """
    rc, _ = client.post_method("virtualLibraries", payload)
    if rc != 0:
        print("  Failed to POST virtualLibraries")
        sys.exit(1)

    name = payload.get("name", "")
    # Re-query to get the assigned UUID
    rc, response = client.get_method("virtualLibraries")
    if rc != 0:
        print("  Failed to GET virtualLibraries after POST")
        sys.exit(1)

    for vtl in response.json():
        if vtl.get("name") == name:
            uuid = vtl["uuid"]
            desc = vtl.get("description", "")
            slots = vtl.get("storageSlots", "")
            print(f"  Created VTL: uuid={uuid}  name={name}  "
                  f"desc={desc}  storageSlots={slots}")
            return uuid

    print(f"  VTL '{name}' not found after POST")
    sys.exit(1)


def update_virtual_library(client: RESTDriver, uuid: str, payload: dict) -> None:
    """PATCH /virtualLibraries/{uuid} — update attributes of an existing VTL."""
    endpoint = f"virtualLibraries/{uuid}"
    rc, _ = client.patch_method(endpoint, payload)
    if rc != 0:
        print(f"  Failed to PATCH virtualLibraries/{uuid}")
        sys.exit(1)

    # Verify the update by re-reading the resource
    rc, response = client.get_method(endpoint)
    if rc != 0:
        print(f"  Failed to GET virtualLibraries/{uuid} after PATCH")
        sys.exit(1)

    vtl = response.json()
    print(f"  Updated VTL: uuid={vtl.get('uuid')}  name={vtl.get('name')}  "
          f"desc={vtl.get('description')}  storageSlots={vtl.get('storageSlots')}")


# ---------------------------------------------------------------------------
# Host / ACL / Mapping functions
# ---------------------------------------------------------------------------

def list_cluster_ports(client: RESTDriver) -> list:
    """GET /clusterPorts."""
    rc, response = client.get_method("clusterPorts")
    if rc != 0:
        print("  Failed to GET clusterPorts")
        return []
    ports = response.json()
    ports = ports if isinstance(ports, list) else []
    print(f"  Cluster Ports ({len(ports)}):")
    for p in ports:
        print(f"    {p.get('uuid', '')}  cluster={p.get('clusterID')}  bay={p.get('bay', '')}"
              f"  slot={p.get('slot', '')}  port={p.get('port', '')}  wwpn={p.get('wwpn', '')}")
    return ports


def list_hosts(client: RESTDriver) -> list:
    """GET /hosts — discovered host initiators."""
    rc, response = client.get_method("hosts")
    if rc != 0:
        print("  Failed to GET hosts")
        return []
    hosts = response.json()
    hosts = hosts if isinstance(hosts, list) else []
    print(f"  Hosts ({len(hosts)}):")
    for h in hosts:
        print(f"    name={h.get('name')}")
    return hosts


def list_host_groups(client: RESTDriver) -> list:
    """GET /hostGroups."""
    rc, response = client.get_method("hostGroups")
    if rc != 0:
        print("  Failed to GET hostGroups")
        return []
    groups = response.json()
    groups = groups if isinstance(groups, list) else []
    print(f"  Host Groups ({len(groups)}):")
    for g in groups:
        print(f"    {g.get('uuid')}  name={g.get('name')}")
    return groups


def list_virtual_ports(client: RESTDriver) -> list:
    """GET /virtualPorts."""
    rc, response = client.get_method("virtualPorts")
    if rc != 0:
        print("  Failed to GET virtualPorts")
        return []
    ports = response.json()
    ports = ports if isinstance(ports, list) else []
    print(f"  Virtual Ports ({len(ports)}):")
    for p in ports:
        print(f"    {p.get('uuid')}  name={p.get('name', '')}")
    return ports


def list_host_ports(client: RESTDriver) -> list:
    """GET /hostPorts."""
    rc, response = client.get_method("hostPorts")
    if rc != 0:
        print("  Failed to GET hostPorts")
        return []
    ports = response.json()
    ports = ports if isinstance(ports, list) else []
    print(f"  Host Ports ({len(ports)}):")
    for p in ports:
        print(f"    {p.get('uuid')}  name={p.get('hostName')}")
    return ports


def create_virtual_port(client: RESTDriver, payload: dict) -> str:
    """POST /virtualPorts — return new UUID (polled until available)."""
    rc, _ = client.post_method("virtualPorts", payload)
    if rc != 0:
        print("  Failed to POST virtualPorts")
        sys.exit(1)
    name = payload.get("name", "")
    uuid = client.wait_for_uuid("virtualPorts", "name", name)
    if not uuid:
        sys.exit(1)
    print(f"  Created virtual port: uuid={uuid}  name={name}")
    return uuid


def create_host_group(client: RESTDriver, payload: dict) -> str:
    """POST /hostGroups — return new UUID (polled until available)."""
    rc, _ = client.post_method("hostGroups", payload)
    if rc != 0:
        print("  Failed to POST hostGroups")
        sys.exit(1)
    name = payload.get("name", "")
    uuid = client.wait_for_uuid("hostGroups", "name", name)
    if not uuid:
        sys.exit(1)
    print(f"  Created host group: uuid={uuid}  name={name}")
    return uuid


def create_host_port(client: RESTDriver, payload: dict) -> str:
    """POST /hostPorts — return new UUID (polled until available)."""
    rc, _ = client.post_method("hostPorts", payload)
    if rc != 0:
        print("  Failed to POST hostPorts")
        sys.exit(1)
    name = payload.get("hostName", "")
    uuid = client.wait_for_uuid("hostPorts", "hostName", name)
    if not uuid:
        sys.exit(1)
    print(f"  Created host port: uuid={uuid}  name={name}")
    return uuid


def create_acl(client: RESTDriver, payload: dict) -> str:
    """POST /accessControlLists — return new UUID (polled until available)."""
    rc, _ = client.post_method("accessControlLists", payload)
    if rc != 0:
        print("  Failed to POST accessControlLists")
        sys.exit(1)
    name = payload.get("name", "")
    uuid = client.wait_for_uuid("accessControlLists", "name", name)
    if not uuid:
        sys.exit(1)
    print(f"  Created ACL: uuid={uuid}  name={name}")
    return uuid


def map_host_port(client: RESTDriver, payload: dict) -> None:
    """POST /hostGroups/hostPortMappings."""
    rc, _ = client.post_method("hostGroups/hostPortMappings", payload)
    if rc != 0:
        print("  Failed to POST hostGroups/hostPortMappings")
        return
    print(f"  Mapped host port {payload.get('hostPortUUID')} "
          f"to group {payload.get('hostGroupUUID')}")


def map_virtual_port(client: RESTDriver, payload: dict) -> None:
    """POST /hostGroups/virtualPortMappings — one mapping dict per call."""
    rc, _ = client.post_method("hostGroups/virtualPortMappings", payload)
    if rc != 0:
        print("  Failed to POST hostGroups/virtualPortMappings")
        return
    print(f"  Mapped virtual port {payload.get('virtualPortUUID')} "
          f"to group {payload.get('hostGroupUUID')}")


def enable_virtual_library(client: RESTDriver, uuid: str,
                            target_clusters: Optional[list] = None) -> None:
    """POST /virtualLibraries/{uuid}/enable — bring a VTL online.

    Args:
        client:          RESTDriver instance.
        uuid:            UUID of the virtual library to enable.
        target_clusters: List of integer cluster IDs to enable, e.g. [1, 3].
                         Pass None (default) to enable on all clusters.
    """
    endpoint = f"virtualLibraries/{uuid}/enable"
    payload = {"targetClusters": target_clusters}
    rc, _ = client.post_method(endpoint, payload)
    if rc != 0:
        print(f"  Failed to POST {endpoint}")
        sys.exit(1)
    clusters = target_clusters if target_clusters is not None else "all"
    print(f"  Enabled VTL uuid={uuid}  targetClusters={clusters}")


def wait_for_vtl_online(client: RESTDriver, uuid: str,
                        interval: int = 30, timeout: int = 300) -> bool:
    """Poll GET /virtualLibraries/{uuid}/state until the VTL is online.

    Args:
        client:   RESTDriver instance.
        uuid:     UUID of the virtual library to monitor.
        interval: Seconds between polls (default 30).
        timeout:  Total seconds to wait before giving up (default 300 = 5 min).

    Returns:
        True if the VTL reached the 'online' state within the timeout,
        False otherwise (a warning is printed for the user to check manually).
    """
    endpoint = f"virtualLibraries/{uuid}/state"
    attempts = max(1, timeout // interval)
    print(f"  Monitoring VTL state (every {interval}s, up to {timeout}s) ...")

    for attempt in range(1, attempts + 1):
        time.sleep(interval)
        rc, response = client.get_method(endpoint)
        if rc != 0:
            print(f"  [{attempt}/{attempts}] Failed to GET {endpoint} — skipping poll")
            continue

        state = response.json()
        # The state endpoint may return a plain string or a dict with a 'state' key.
        if isinstance(state, dict):
            state = state.get("state", "")
        state = str(state).lower()
        print(f"  [{attempt}/{attempts}] VTL uuid={uuid}  state={state}")

        if state == "online":
            print(f"  VTL uuid={uuid} is online.")
            return True

    print(
        f"\n  WARNING: VTL uuid={uuid} did not reach 'online' state within {timeout}s.\n"
        f"  Please check the VTL state manually via GET /{endpoint}"
    )
    return False


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

if __name__ == '__main__':
    rest = RESTDriver(server="xxx.xxx.xxx.xxx",
                      username="xxx",
                      password="xxx")

    print(f"Connecting to {rest.base_url}\n")

    # -----------------------------------------------------------------------
    # Section 1 — VTL read operations
    # -----------------------------------------------------------------------
    print("--- VTL read operations ---")
    vtls = list_virtual_libraries(rest)

    # Comment out the exit() below once you have confirmed the VTL list and
    # are ready to run create / update / delete operations.
    print("\nDry run (VTL list) completed. Remove or comment the exit() to continue."); exit(0)

    # -----------------------------------------------------------------------
    # Section 2 — VTL lifecycle (create → update → delete)
    # Optional: skip this section if a valid VTL already exists and set
    # VIRTUAL_LIBRARY_UUID manually at the top of the script.
    # -----------------------------------------------------------------------
    print("\n--- VTL lifecycle operations ---")

    # Create a new VTL
    vtl_uuid = create_virtual_library(rest, SAMPLE_VTL_CREATE_PAYLOAD)

    # -----------------------------------------------------------------------
    # Section 3 — Mapping read operations
    # -----------------------------------------------------------------------
    print("\n--- Mapping read operations ---")
    list_cluster_ports(rest)
    list_virtual_ports(rest)
    list_host_groups(rest)
    list_host_ports(rest)
    list_hosts(rest)

    # Comment out the line below, once you have enough data to continue.
    print("\nDry run completed. Make sure to set valid data for those marked REPLACE in the script."); exit(0)

    # -----------------------------------------------------------------------
    # Section 4 — Mapping POST operations
    # -----------------------------------------------------------------------
    print("\n--- POST operations ---")

    # Propagate the VTL UUID into the payloads that need it.
    # - If Section 2 ran:   use vtl_uuid (the UUID returned by create_virtual_library)
    # - If Section 2 was skipped: assign VIRTUAL_LIBRARY_UUID to vtl_uuid manually, e.g.:
    #     vtl_uuid = VIRTUAL_LIBRARY_UUID
    SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER0['owningVirtualLibraryUUID'] = vtl_uuid
    SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER1['owningVirtualLibraryUUID'] = vtl_uuid
    SAMPLE_ACL_PAYLOAD_CLUSTER0['virtualLibraryUUID']                = vtl_uuid
    SAMPLE_ACL_PAYLOAD_CLUSTER1['virtualLibraryUUID']                = vtl_uuid

    # 1a. Create virtual port for cluster 0
    new_vp_c0_uuid = create_virtual_port(rest, SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER0)
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER0['virtualPortUUID'] = new_vp_c0_uuid

    # 1b. Create virtual port for cluster 1
    new_vp_c1_uuid = create_virtual_port(rest, SAMPLE_VIRTUAL_PORT_PAYLOAD_CLUSTER1)
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER1['virtualPortUUID'] = new_vp_c1_uuid

    # 2. Create a host group (shared across both clusters)
    new_hg_uuid = create_host_group(rest, SAMPLE_HOST_GROUP_PAYLOAD)
    SAMPLE_HOST_PORT_MAPPING['hostGroupUUID']                    = new_hg_uuid
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER0['hostGroupUUID']        = new_hg_uuid
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER1['hostGroupUUID']        = new_hg_uuid

    # 3. Create a host port (shared across both clusters)
    new_hp_uuid = create_host_port(rest, SAMPLE_HOST_PORT_PAYLOAD)
    SAMPLE_HOST_PORT_MAPPING['hostPortUUID'] = new_hp_uuid

    # 4. Map the host port to the host group
    map_host_port(rest, SAMPLE_HOST_PORT_MAPPING)

    # 5a. Create ACL for cluster 0
    new_acl_c0_uuid = create_acl(rest, SAMPLE_ACL_PAYLOAD_CLUSTER0)
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER0['accessControlListUUID'] = new_acl_c0_uuid

    # 5b. Create ACL for cluster 1
    new_acl_c1_uuid = create_acl(rest, SAMPLE_ACL_PAYLOAD_CLUSTER1)
    SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER1['accessControlListUUID'] = new_acl_c1_uuid

    # 6a. Map cluster 0 virtual port to the host group (with ACL)
    map_virtual_port(rest, SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER0)

    # 6b. Map cluster 1 virtual port to the host group (with ACL)
    map_virtual_port(rest, SAMPLE_VIRTUAL_PORT_MAPPING_CLUSTER1)

    # 7. Enable the virtual library on all clusters
    enable_virtual_library(rest, vtl_uuid)

    # 8. Wait for the VTL to come online (polls every 30 s, up to 5 min)
    wait_for_vtl_online(rest, vtl_uuid)
Figure 2. RestDriver.py
#!/usr/bin/python3

import pprint
import requests
import json
import time
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)

class RESTDriver:
    '''
    This class includes the
    '''
    # class variables
    server = ''              # VTL server IP address/DNS
    username = ''            # username to access VTL
    password = ''            # password of the username
    base_url = ''            # Base RESTful API URL
    auth_dict = {}           # authenticate data payload
    bearer_token_dict = {}   # bearer token header
    content_type_dict = {}   # content type header
    token = ''               # retrieved bearer token

    def __init__(self, server, username='admin', password='admin'):
        '''
        constructor to set the class variables.
        Input:
            server: target VTL server IP address or DNS
            username: username to access VTL
            password: password of the username
        '''
        self.server = server
        self.username = username
        self.password = password

        self.base_url = 'https://' + server + '/api/'                             # base URL (https://<VTL address>/api/)
        self.content_type_dict = {'Content-Type': 'application/json'}             # content type header (always application/json)
        self.auth_dict = {'username': self.username, 'password': self.password}   # authenticate data payload (for POST /token)
        self.token = self.post_token()                                            # POST to get bearer token
        self.bearer_token_dict = {'Authorization': 'Bearer ' + self.token}        # bearer token header (Need for GET/POST/PATCH/DELETE)

    # =============================================================================
    # Getter/Setter of class variables
    def get_server(self):
        return   self.server

    def get_credentials(self):
        return   self.username,self.password

    def get_current_token(self):
        return   self.token

    def set_token(self, token):
        self.token = token

    # =============================================================================
    # Base RESTful API methods
    def get_method(self, endpoint, data_dict={}):
        '''
        GET method
        Input:
            endpoint: target endpoint name such as token, virtualLibraries
            data_dict: data payload in dictionary. Default is no data payload.
        '''
        rc = 1  # default rc
        # URL
        this_url = self.base_url + endpoint
        # data payload
        data_json = json.dumps(data_dict)
        # headers
        headers = self.content_type_dict | self.bearer_token_dict  # content type + bearer token header

        try:
            if not data_dict:
                response = requests.get(
                    this_url,
                    headers=headers,
                    verify=False
                )
            else:
                response = requests.get(
                    this_url,
                    data=data_json,
                    headers=headers,
                    verify=False
                )
            status = response.status_code
            if status == 200 or status == 206:
                rc = 0
            else:
                print(f"GET:{this_url} with {data_json} failed. RC:{response.status_code}")

        except requests.exceptions.RequestException as e:
            print(f"Failed to GET:{this_url} with {data_json}: {e}")

        return rc, response

    def post_method(self, endpoint, data_dict={}):
        '''
        POST method
        Input:
            endpoint: target endpoint name such as token, virtualLibraries
            data_dict: data payload in dictionary. Default is no data payload.
        '''
        rc = 1  # default rc
        # URL
        this_url = self.base_url + endpoint
        # data payload
        data_json = json.dumps(data_dict)
        # headers
        # If this is POST /token with authentication, no more header is required
        if endpoint == 'token' and data_dict:
            headers = self.content_type_dict                           # content type header only
        else:
            headers = self.content_type_dict | self.bearer_token_dict  # content type + bearer token header

        try:
            if not data_dict:
                response = requests.post(
                    this_url,
                    headers=headers,
                    verify=False
                )
            else:
                response = requests.post(
                    this_url,
                    data=data_json,
                    headers=headers,
                    verify=False
                )
            status = response.status_code
            if status == 200 or status == 201 or status == 202 or status == 204:
                rc = 0
            else:
                print(f"POST:{this_url} with {data_json} failed. RC:{response.status_code}")

        except requests.exceptions.RequestException as e:
            print(f"Failed to POST:{this_url} with {data_json}: {e}")

        return rc, response

    def patch_method(self, endpoint, data_dict={}):
        '''
        PATCH method
        Input:
            endpoint: target endpoint name such as token, virtualLibraries
            data_dict: data payload in dictionary. Default is no data payload.
        '''
        rc = 1  # default rc
        # URL
        this_url = self.base_url + endpoint
        # data payload
        data_json = json.dumps(data_dict)
        # headers
        headers = self.content_type_dict | self.bearer_token_dict  # content type + bearer token header

        try:
            if not data_dict:
                response = requests.patch(
                    this_url,
                    headers=headers,
                    verify=False
                )
            else:
                response = requests.patch(
                    this_url,
                    data=data_json,
                    headers=headers,
                    verify=False
                )
            status = response.status_code
            if status == 200 or status == 202 or status == 204:
                rc = 0
            else:
                print(f"PATCH:{this_url} with {data_json} failed. RC:{response.status_code}")

        except requests.exceptions.RequestException as e:
                print(f"Failed to PATCH:{this_url} with {data_json}: {e}")

        return rc, response

    def delete_method(self, endpoint, data_dict={}):
        '''
        DELETE method
        Input:
            endpoint: target endpoint name such as token, virtualLibraries
            data_dict: data payload in dictionary. Default is no data payload.
        '''
        rc = 1  # default rc
        # URL
        this_url = self.base_url + endpoint
        # data payload
        data_json = json.dumps(data_dict)
        # headers
        headers = self.content_type_dict | self.bearer_token_dict  # content type + bearer token header

        try:
            if not data_dict:
                response = requests.delete(
                    this_url,
                    headers=headers,
                    verify=False
                )
            else:
                response = requests.delete(
                    this_url,
                    data=data_json,
                    headers=headers,
                    verify=False
                )
            status = response.status_code
            if status == 200 or status == 202 or status == 204:
                rc = 0
            else:
                print(f"DELETE:{this_url} with {data_json} failed. RC:{response.status_code}")

        except requests.exceptions.RequestException as e:
                print(f"Failed to DELETE:{this_url} with {data_json}: {e}")

        return rc, response

    # =============================================================================
    # Base token function
    def post_token(self):
        '''
        This method gets token by POST with the username and password.
        This is called from the constructor then token is saved as a class variable.
        '''
        endpoint = 'token'
        rc, response = self.post_method(endpoint, self.auth_dict)
        if rc == 0:
            token = response.headers.get('X-Auth-Token')
        else:
            raise ConnectionError(f"Faild to connect the server {self.server} to POST {endpoint}")

        return token

    def renew_token(self):
        '''
        This method renews token by POST with the non-expired token.
        '''
        endpoint = 'token'
        rc, response = self.post_method(endpoint)
        if rc == 0:
            token = response.headers.get('X-Auth-Token')
        else:
            raise ConnectionError(f"Faild to connect the server {self.server} to renew {endpoint}")

        self.set_token(token)   # Renew token of the class variable

    # =============================================================================
    # Wait for async operations.
    def wait_for_uuid(self, endpoint, name_key, name_value, retries=30, interval=5, id_key="uuid"):
        '''
        Poll GET <endpoint> until an object matching name_key==name_value appears
        with a non-empty identifier. Useful after async POST operations.
        Input:
            endpoint:    target endpoint, e.g. "virtualPorts", "hostGroups"
            name_key:    field name to match on, e.g. "name"
            name_value:  expected value of that field
            retries:     maximum number of polling attempts (default 30)
            interval:    seconds to wait between attempts (default 5)
            id_key:      field name for the object identifier (default "uuid";
                         use "id" for endpoints such as cloudRepositories)
        Returns the identifier string, or "" if not found within the retry limit.
        '''
        for attempt in range(retries):
            rc, response = self.get_method(endpoint)
            if rc == 0:
                items = response.json()
                if isinstance(items, list):
                    identifier = next(
                        (item.get(id_key, "") for item in items
                         if item.get(name_key) == name_value),
                        ""
                    )
                    if identifier:
                        return identifier
            print(f"  [{attempt+1}/{retries}] Waiting for {endpoint} '{name_value}'...")
            time.sleep(interval)
        print(f"  Timed out waiting for {endpoint} '{name_value}'")
        return ""

Response

- 1st run
Connecting to https://x.x.x.x/api/

--- VTL read operations ---
  Virtual Libraries (2):
    uuid=72232a25-0921-46bf-847b-3c1d772927a6  name=vtl1
    uuid=302aa022-7753-4fb6-9209-4f9f50bd5f09  name=vtl_ibmi4

Dry run (VTL list) completed. Remove or comment the exit() to continue.

- 2nd run
Connecting to https://x.x.x.x/api/

--- VTL read operations ---
  Virtual Libraries (2):
    uuid=72232a25-0921-46bf-847b-3c1d772927a6  name=vtl1
    uuid=302aa022-7753-4fb6-9209-4f9f50bd5f09  name=vtl_ibmi4

--- VTL lifecycle operations ---
  Created VTL: uuid=71e84036-96ca-453e-aaef-449fbea1947d  name=testvtl  desc=test vtl  storageSlots=100

--- Mapping read operations ---
  Cluster Ports (8):
    7cb82860-0825-4f37-a0d9-857fdc11cadb  cluster=0  bay=0  slot=3  port=0  wwpn=50:05:07:60:7D:00:40:01
    036cb0ae-d7cf-4fb6-af11-5982245017f7  cluster=0  bay=0  slot=9  port=0  wwpn=50:05:07:60:7D:00:40:02
    2a03f8bc-6c67-4501-94ed-af4b1dec0567  cluster=0  bay=0  slot=3  port=1  wwpn=50:05:07:60:7D:00:40:03
    653fd00f-fb09-4132-91b4-1ca26bee1c21  cluster=0  bay=0  slot=9  port=1  wwpn=50:05:07:60:7D:00:40:04
    090cfae3-3f17-48e8-bc7b-5613e1e93fa9  cluster=0  bay=0  slot=3  port=2  wwpn=50:05:07:60:7D:00:40:05
    750aa13b-6dc3-43cc-aa9e-bfadc4b37320  cluster=0  bay=0  slot=9  port=2  wwpn=50:05:07:60:7D:00:40:06
    cd892111-5819-4581-bdc0-3e20cdcd70fd  cluster=0  bay=0  slot=3  port=3  wwpn=50:05:07:60:7D:00:40:07
    e95418b1-81cc-4392-8152-b740b5556e05  cluster=0  bay=0  slot=9  port=3  wwpn=50:05:07:60:7D:00:40:08
  Virtual Ports (2):
    da0b802c-b521-4362-824d-29f47f3c3d1c  name=EP_Defaul_vtl_ib_C0_S3P0-10
    6aaa3613-bd36-4e16-8d77-81d0ddecfb28  name=EP_Defaul_vtl1_C0_S9P0-9
  Host Groups (2):
    763f7da3-1dec-4bda-8712-6cceecfca59d  name=VTL1_HG1
    699970fa-6be5-464d-8fff-2134122db800  name=vtl_ibmi4_hg
  Host Ports (3):
    f33f5e01-fa76-4e24-adc8-4cb3c79904de  name=vtl1_hp1
    2362ab03-1540-443a-bd09-a992b398efb3  name=vtl_ibmi4_hp1
    48b0ee5c-00f3-45ae-a25e-534f2a10c47f  name=vtl_ibmi4_hp2
  Hosts (3):
    name=vtl1_hp1
    name=vtl_ibmi4_hp1
    name=vtl_ibmi4_hp2

Dry run completed. Make sure to set valid data for those marked REPLACE in the script.
Make a note of virtual library uuid just created, and comment out create_virtual_library() also.

- 3rd run
Connecting to https://x.x.x.x/api/

--- VTL read operations ---
  Virtual Libraries (3):
    uuid=72232a25-0921-46bf-847b-3c1d772927a6  name=vtl1
    uuid=302aa022-7753-4fb6-9209-4f9f50bd5f09  name=vtl_ibmi4
    uuid=71e84036-96ca-453e-aaef-449fbea1947d  name=testvtl

--- VTL lifecycle operations ---

--- Mapping read operations ---
  Cluster Ports (8):
    7cb82860-0825-4f37-a0d9-857fdc11cadb  cluster=0  bay=0  slot=3  port=0  wwpn=50:05:07:60:7D:00:40:01
    036cb0ae-d7cf-4fb6-af11-5982245017f7  cluster=0  bay=0  slot=9  port=0  wwpn=50:05:07:60:7D:00:40:02
    2a03f8bc-6c67-4501-94ed-af4b1dec0567  cluster=0  bay=0  slot=3  port=1  wwpn=50:05:07:60:7D:00:40:03
    653fd00f-fb09-4132-91b4-1ca26bee1c21  cluster=0  bay=0  slot=9  port=1  wwpn=50:05:07:60:7D:00:40:04
    090cfae3-3f17-48e8-bc7b-5613e1e93fa9  cluster=0  bay=0  slot=3  port=2  wwpn=50:05:07:60:7D:00:40:05
    750aa13b-6dc3-43cc-aa9e-bfadc4b37320  cluster=0  bay=0  slot=9  port=2  wwpn=50:05:07:60:7D:00:40:06
    cd892111-5819-4581-bdc0-3e20cdcd70fd  cluster=0  bay=0  slot=3  port=3  wwpn=50:05:07:60:7D:00:40:07
    e95418b1-81cc-4392-8152-b740b5556e05  cluster=0  bay=0  slot=9  port=3  wwpn=50:05:07:60:7D:00:40:08
  Virtual Ports (2):
    da0b802c-b521-4362-824d-29f47f3c3d1c  name=EP_Defaul_vtl_ib_C0_S3P0-10
    6aaa3613-bd36-4e16-8d77-81d0ddecfb28  name=EP_Defaul_vtl1_C0_S9P0-9
  Host Groups (2):
    763f7da3-1dec-4bda-8712-6cceecfca59d  name=VTL1_HG1
    699970fa-6be5-464d-8fff-2134122db800  name=vtl_ibmi4_hg
  Host Ports (3):
    f33f5e01-fa76-4e24-adc8-4cb3c79904de  name=vtl1_hp1
    2362ab03-1540-443a-bd09-a992b398efb3  name=vtl_ibmi4_hp1
    48b0ee5c-00f3-45ae-a25e-534f2a10c47f  name=vtl_ibmi4_hp2
  Hosts (3):
    name=vtl1_hp1
    name=vtl_ibmi4_hp1
    name=vtl_ibmi4_hp2

--- POST operations ---
  Created virtual port: uuid=4bdc3064-de71-41f2-b220-0e57ff07d14c  name=sample-virtual-port-c0-01
  Created host group: uuid=d3c87101-b5a8-43ef-93d9-544b1844e345  name=sample-host-group-01
  Created host port: uuid=8e4a6c05-4eb3-4d26-b8cc-c64e07046feb  name=sample-host-01
  Mapped host port 8e4a6c05-4eb3-4d26-b8cc-c64e07046feb to group d3c87101-b5a8-43ef-93d9-544b1844e345
  Created ACL: uuid=18567b30-4b84-4779-adcb-288fe87f9517  name=sample-acl-c0-01
  Mapped virtual port 4bdc3064-de71-41f2-b220-0e57ff07d14c to group d3c87101-b5a8-43ef-93d9-544b1844e345
  Enabled VTL uuid=71e84036-96ca-453e-aaef-449fbea1947d  targetClusters=all
  Monitoring VTL state (every 30s, up to 300s) ...
  [1/10] VTL uuid=71e84036-96ca-453e-aaef-449fbea1947d  state=online
  VTL uuid=71e84036-96ca-453e-aaef-449fbea1947d is online.