IBM Storage Protect Operations Center REST API + Python

As a TSM server administrator, you may sometimes find it useful to fetch and process data from the servers programmatically. For example, when you wish to automate certain administrative routines. This article will cover how this can be done reliably.

The CLI Client Method

You could use the dsmadmc administrative client. For example, like this:

dsmadmc -id=admin -pa=secret -displaymode=table \
  -commadelimited -outfile=nodes.csv -dataonly=yes q node f=d

You may invoke such a command manually, or within a script. The output of the invocation will be a CSV-file at a desired location, with details on all the nodes managed by the server. The content of this file can in turn be processed as needed.

By omitting the -outfile=nodes.csv flag, the data will be written to the standard output stream rather than a file called "nodes.csv", making it more convenient to run the command in a script.

This method has three weaknesses:

  1. You need the Storage Protect Client installed and pre-configured on your system.
  2. There is no easy way to guarantee that the output will be "clean," even with the -dataonly=yes flag. If you prepend q node with server_name: to specify which server to execute the command against, the output will, apart from the data itself, also contain information text from the client. Thus requiring additional processing by you manually, or by a script to clean it up. In general, it is not clear how the client will react with different kinds of invocations and server states. This unpredictability is a big negative from a programming perspective.
  3. The output above will not include any headers. You will need a different way to figure out what the columns are.

The Operations Center REST API Method

This brings us to the second method. Storage Protect (a.k.a. TSM) includes as part of its software suite the Operations Center (OC). It can connect all the TSM servers of an organization to one web interface, where they can easily be managed by an administrator. In other words, through the OC you have access to all these servers the same way you do with dsmadmc.

Apart from the Web GUI, a lesser known feature of the OC is the built-in REST API. This API allows the administrator to programmatically issue commands against any of these servers, through the OC, and receive the output in JSON format.

Although it does have its own quirks, as we will see later on, it will provide predictable and machine-readable output. You also get the headers, unlike the comma- or tab-delimited output in the CLI. Furthermore, because it is a regular REST API, all communication happens through the HTTP protocol. No TSM client is needed on your system!

Requirements

To utilize the REST API, you need:

  1. An Operations Center (OC) instance with at least one TSM server connected to it.
  2. To have the REST API enabled in the OC settings. It is not enabled by default.
  3. An admin account. The account must be registered on every TSM server instance that you wish to access through the API, and also have the same passwords.

Python Scripting & TSM

Now to the fun part. Here is a complete Python script that executes a command against a TSM server using the OC REST API.

import json
import ssl
import sys
import httpx


###########################
# Change these:
ADMIN_USERNAME = "you_admin_username"
ADMIN_PASSWORD = "hopefully_with_good_entropy"
OC_HOSTNAME = "oc.company.com"
SERVER_NAME = "your_tsm_server"
COMMAND_TO_ISSUE = "q node f=d"

# If the OC certificate is issued by a custom CA (and potentially an
# intermediate CA), include their CA certificates in a file called
# tsm_oc_chain.pem, and set this option to True:
NEEDS_CUSTOM_CA = False
###########################

# These headers should always be included in an HTTP request to the OC REST API.
OC_STANDARD_HEADERS = {
    "OC-API-Version": "1.0",
    "Accept": "application/json",
    "Content-Type": "text/plain"
}

if NEEDS_CUSTOM_CA:
    # Use custom CA certificate(s).
    ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
    ssl_context.load_verify_locations("tsm_oc_chain.pem")
else:
    # Rely on the default CA certificates on the system for TLS
    # server authentication.
    ssl_context = True

client = httpx.Client(
    base_url=f"https://{OC_HOSTNAME}:11090/oc/api/",
    # Only if you have custom CA certificates:
    verify=ssl_context,
    # No timeout, as opposed to the default 5 seconds.
    timeout=None,
    # Basic HTTP authentication:
    auth=(ADMIN_USERNAME, ADMIN_PASSWORD),
    # The extra HTTP headers:
    headers=OC_STANDARD_HEADERS
)

response = client.post(
    url=f"cli/issueConfirmedCommand/{SERVER_NAME}/",
    content=COMMAND_TO_ISSUE
)

if response.status_code == 500:
    print(f"The command is invalid, or your account does not have access to a "
          f"server with the name '{SERVER_NAME}'.", file=sys.stderr)
elif response.status_code == 401:
    print("You are unauthorized to make this request. Make sure that the "
          "account name and password are correct.", file=sys.stderr)
elif response.status_code == 200:
    # Success! Here is the output:
    print(json.dumps(response.json(), indent=2))
else:
    print(f"An unknown error occurred with HTTP status code "
          f"{response.status_code}.", file=sys.stderr)

It uses the httpx module to make the HTTP request to the API. Install it using python3 -m pip install httpx. This Python program is almost equivalent to the following bash script:

export ADMIN_USERNAME="you_admin_username"
export ADMIN_PASSWORD="hopefully_with_good_entropy"
export OC_HOSTNAME="oc.company.com"
export SERVER_NAME="your_tsm_server"
export COMMAND_TO_ISSUE="q node f=d"
curl --request POST \
  --url "https://$OC_HOSTNAME:11090/oc/api/cli/issueConfirmedCommand/$SERVER_NAME/" \
  --header 'Accept: application/json' \
  --header 'Content-Type: text/plain' \
  --header 'OC-API-VERSION: 1.0' \
  --cacert 'tsm_oc_chain.pem' \
  -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
  --data "$COMMAND_TO_ISSUE" \
  | python3 -m json.tool

This is essentially just a curl invocation. A Python tool called json.tool from the built-in json module is used to prettify the JSON output.

Unless your OC has a certificate signed by a public certificate authority that your OS trusts by default, you need to provide a file called "tsm_oc_chain.pem" for the above scripts to work. The certificates inside "tsm_oc_chain.pem" should be chained like this:

-----BEGIN CERTIFICATE-----
Intermediate CA cert here (if any)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Root CA cert here
-----END CERTIFICATE-----

Explanation

The following explains how the script works. In the API documentation, we can see that there are plenty of endpoints to choose from. We are primarily interested in one, that is cli/issueConfirmedCommand/{server name}/.

It is used to execute arbitrary commands against a TSM server. Granted that we have our credentials, the server name, and everything we need to connect to the API, this endpoint is all we need! This is what the Python script above is using as well.

The script starts off by including required modules:

import json
import ssl
import sys
import httpx

Following are constants that you need to fill in based on your configuration:

# Change these:
ADMIN_USERNAME = "you_admin_username"
ADMIN_PASSWORD = "hopefully_with_good_entropy"
OC_HOSTNAME = "oc.company.com"
SERVER_NAME = "your_tsm_server"
COMMAND_TO_ISSUE = "q node f=d"

# If the OC certificate is issued by a custom CA (and potentially an
# intermediate CA), include their CA certificates in a file called
# tsm_oc_chain.pem, and set this option to True:
NEEDS_CUSTOM_CA = False

These are HTTP headers that must always be included in every HTTP request to the OC REST API endpoint cli/issueConfirmedCommand/{server name}/, and we expect a JSON response.

OC_STANDARD_HEADERS = {
    "OC-API-Version": "1.0",
    "Accept": "application/json",
    "Content-Type": "text/plain"
}

They basically specify the API version, that the content (the command that we want to issue) is plain text, and that we want a JSON back.

Depending on which setting you chose, we will either use the CA certificates in "tsm_oc_chain.pem," or just rely on the CAs trusted by the system:

if NEEDS_CUSTOM_CA:
    # Use custom CA certificate(s).
    ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
    ssl_context.load_verify_locations("tsm_oc_chain.pem")
else:
    # Rely on the default CA certificates on the system for TLS
    # server authentication.
    ssl_context = True

Here is the httpx Client being initialized:

client = Client(
    base_url=f"https://{OC_HOSTNAME}:11090/oc/api/",
    # Only if you have custom CA certificates:
    verify=ssl_context,
    # No timeout, as opposed to the default 5 seconds.
    timeout=None,
    # Basic HTTP authentication:
    auth=(ADMIN_USERNAME, ADMIN_PASSWORD),
    # The extra HTTP headers:
    headers=OC_STANDARD_HEADERS
)

Warning! Do not try to disable TLS server verification completely by accepting any certificate that is sent to you during the HTTP request. This will make you susceptible to a man-in-the-middle attack. You always need some kind of verification, either using CA certificates that you supply manually, or using the ones included in your system. This is true for HTTP+TLS (HTTPS) in general.

The following is the most central part of the program, the actual request to the API. Notice, we must use the POST HTTP method for this endpoint.

response = client.post(
    url=f"cli/issueConfirmedCommand/{SERVER_NAME}/",
    content=COMMAND_TO_ISSUE
)

Finally, we act upon the response. Status code 200 means success. Anything else for the above endpoint is a failure. Check out the print statements to find out what different status codes indicate:

if response.status_code == 500:
    print(f"The command is invalid, or your account does not have access to a "
          f"server with the name '{SERVER_NAME}'.", file=sys.stderr)
elif response.status_code == 401:
    print("You are unauthorized to make this request. Make sure that the "
          "account name and password are correct.", file=sys.stderr)
elif response.status_code == 200:
    # Success! Here is the output:
    print(json.dumps(response.json(), indent=2))
else:
    print(f"An unknown error occurred with HTTP status code "
          f"{response.status_code}.", file=sys.stderr)

Caveats

  • As stated previously, the REST API is unfortunately not completely documented. For example, status code 500 does not mean anything by itself in general, other than that there was an HTTP server error. My explanation above comes from experience on what can cause this status code on that particular endpoint. Other endpoints may fail with the same status code, but for other reasons.
  • Some endpoints like servers/{server_name}/clients/{node_name}/details may return status code 200 with an empty response when the object does not exist, rather than the standard code 404. This is also something to keep in mind.

Conclusion

From the looks of it, it may seem like we are doing more work by using the REST API than just invoking the admin CLI client. However, for software development and automation, once everything has been set up, we greatly benefit from the reliability and cleanliness of the REST API output. We also benefit from only needing a thin HTTP client as a dependency, which is widely available, rather than having to rely on the TSM client software being installed. This can make deploying software that operates on TSM servers easier.

Föregående
Föregående

Will Immutable Storage solve all Cyber Attacks and be NIS2 compliant?

Nästa
Nästa

Is Retention Lock a good option to implement?