Authentication using zCompute APIs

Symp CLI commands can be run by authenticating, without requiring a token.

However, it is advisable to authenticate and then generate a token for use in any automated Symp CLI or API process. This will prevent possible connection reset by peer errors when rate limits are exceeded.

Generating a token at a bash prompt

Since some commands are limited to the scope of a project, while others have the scope of an entire account (domain), it’s necessary to generate a token for the account (domain) scope, and a separate token for the project scope.

Using curl on the /api/v2/identity/auth endpoint, to generate authentication tokens involves two steps:

First, generating an account (domain) token, and then using the domain token to generate a project token.

  1. Generating a domain token:

    The /api/v2/identity/auth endpoint’s authentication token request data structure that defines the scope of an entire account (domain):

    {
      "auth": {
        "scope": {
          "domain": {
            "name": "<ACCOUNT_NAME>"
          }
        },
        "identity": {
          "password": {
            "user": {
              "domain": {
                "name": "<ACCOUNT_NAME>"
              },
              "password": "<PASSWORD>",
              "name": "<USER_NAME>"
            }
          },
          "methods": [
            "password"
          ]
        }
      }
    }
    
    1. The curl command at the bash prompt for generating an account (domain) authentication token:

      curl -D - -k -H "Content-Type: application/json" -X POST https://<cluster IP address>/api/v2/identity/auth -d \
      '{"auth": {"scope": {"domain": {"name": "<account>"}}, "identity": {"password": {"user": {"domain": {"name": "<account>"}, "password": "<password>", "name": "<user>"}}, "methods": ["password"]}}}'
      

      For example:

      curl -D - -k -H "Content-Type: application/json" -X POST https://10.11.12.13/api/v2/identity/auth -d \
      '{"auth": {"scope": {"domain": {"name": "acc1"}}, "identity": {"password": {"user": {"domain": {"name": "acc1"}, "password": "Mypass1!", "name": "user1"}}, "methods": ["password"]}}}'
      
    2. The domain token is returned in the x-subject-token header, and can be copy/pasted to assign it to an environment variable for later use.

      For example:

      export ZC_DOMAIN_TOKEN=MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
      
  2. Using the previously generated domain token to generate a project token:

    The /api/v2/identity/auth endpoint’s authentication token request data structure that defines the scope as limited to a project, and the identity method as a token:

    {
      "auth": {
        "scope": {
          "project": {
            "domain": {
              "name": "<ACCOUNT_NAME>"
            },
            "name": "<PROJECT_NAME>"
          }
        },
        "identity": {
          "token": {
            "id": "'<DOMAIN_TOKEN>'"
          },
          "methods": [
            "token"
          ]
        }
      }
    }
    
    1. The curl command at the bash prompt for generating a project authentication token:

      curl -D - -k -H "Content-Type: application/json" -X POST https:<cluster IP address>/api/v2/identity/auth -d \
      '{"auth": {"scope": {"project": {"domain": {"name": "<account>"}, "name": "<project>"}}, "identity": {"token": {"id": "'<domain token string>'"}, "methods": ["token"]}}}'
      

      For example:

      curl -D - -k -H "Content-Type: application/json" -X POST https://10.11.12.13/api/v2/identity/auth -d \
      '{"auth": {"scope": {"project": {"domain": {"name": "acc1"}, "name": "vpcproj1"}}, "identity": {"token": {"id": "'$ZC_DOMAIN_TOKEN'"}, "methods": ["token"]}}}'
      
    2. The project token is returned in the x-subject-token header, and can be copy/pasted to assign it to an environment variable for later use.

      For example:

      export ZC_PROJECT_TOKEN=MIIScwYJKoZIhvcNAQcCoIISZDCC...<2K+ character string>...VpADxR3RDrScUbgSwMAaZ8zCSrow=
      
    3. To use curl to call a REST method on an endpoint, provide the token string in the X-Auth-Token header:

      curl -k -L -X <REST method> "https://<cluster IP address>/api-explorer/api/v2/<endpoint>/" -H "accept: application/json" -H "X-Auth-Token: <token string>"
      

      For example, to retrieve the project’s tags, use the GET method on the /api-explorer/api/v2/tags/ endpoint, and the project authentication token (assigned in the previous step to the environment variable $ZC_PROJECT_TOKEN). This example uses sed and awk to format the output:

      curl -k -L -X GET "https://10.11.12.13/api-explorer/api/v2/tags/" -H "accept: application/json" -H "X-Auth-Token: $ZC_PROJECT_TOKEN" | awk 'BEGIN{FS=",";OFS="\n"} FNR==1{$1=$1;print;exit}' | sed '/{/{x;p;x;}'
        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                       Dload  Upload   Total   Spent    Left  Speed
      100   116  100   116    0     0  11756      0 --:--:-- --:--:-- --:--:-- 12888
      100  2302  100  2302    0     0  59137      0 --:--:-- --:--:-- --:--:--     0
      
      [{"description": "Tag2"
       "updated-at": "2023-10-31T08:44:01Z"
        "created-at": "2023-10-31T08:44:01Z"
        "value": ""
        "scope": "public"
        "project-id": "7ff34832b6754f7d91a918302af3e77f"
        "name": "vpctag2"}
      
        {"description": "Tag 1"
        "updated-at": "2023-10-31T08:10:49Z"
        "created-at": "2023-10-31T08:10:49Z"
        "value": ""
        "scope": "public"
        "project-id": "7ff34832b6754f7d91a918302af3e77f"
        "name": "vpctag1"}]
      

Generating a token in Symp CLI

  1. To generate an authentication token via the Symp CLI shell, use the Symp login command:

    login <user> <password> <domain> <project> -f value
    

    For example:

    login user1 Mypass1! acc1 vpcproj1 -f value
    
    MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
    

    Alternatively, an example of a single command at the bash prompt to generate an authentication token and assign it to an environment variable:

    export ZC_PROJECT_TOKEN=`symp -k --url https://10.11.12.13 -d acc1 -u user1 --project vpcproj1 -p Mypass1! login user1 Mypass1! acc1 vpcproj1 -f value`
    
    echo $ZC_PROJECT_TOKEN
    
    MIISUgYJKoZIhvcNAQcCoIISQzCC...<2K+ character string>...ngpXjEqQtxlRGuCEIvo46sGMfedc=
    
  2. Using the token to sign on to Symp from the bash prompt:

    symp -k --url https://<cluster IP address> --project-token <token string>
    

    For example, generating the token (as in the previous step), and using it to sign on to Symp:

    export ZC_PROJECT_TOKEN=`symp -k --url https://10.11.12.13 -d acc1 -u user1 --project vpcproj1 -p Mypass1! login user1 Mypass1! acc1 vpcproj1 -f value`
    
    symp -k --url https://10.11.12.13 --project-token $ZC_PROJECT_TOKEN
    
    Starting new HTTPS connection (1): 10.16.145.114
    Connecting in insecure mode!
    Starting new HTTPS connection (1): 10.16.145.114
    Starting new HTTPS connection (1): 10.16.145.114
    
     d88888b  dP    dP 8888ba.88ba   888888ba  dP     dP   .88888.  888888ba  dP    dP
    88.     "' Y8.  .8P 88  `8b  `8b  88    `8b 88     88  d8'   `8b 88    `8b Y8.  .8P
    `Y88888b.  Y8aa8P  88   88   88 a88aaaa8P' 88aaaaa88a 88     88 88     88  Y8aa8P
          `8b    88    88   88   88  88        88     88  88     88 88     88    88
    d8'   .8P    88    88   88   88  88        88     88  Y8.   .8P 88     88    88
     Y88888P     dP    dP   dP   dP  dP        dP     dP   `8888P'  dP     dP    dP
    
    Tap <TAB> twice to get list of available commands.
    Type --help to get help with any command
    Symphony >
    

Python authentication example

When using a zCompute API, the user is required to pass a valid token in an X-Auth-Token HTTP header.

Users can generate a valid token by logging into the system using the identity/auth API.

The following Python script is an example of this process:

#!/usr/bin/python
from __future__ import print_function
import copy
import getpass
import os
import sys
import argparse
import requests
try:
    input = raw_input
except NameError:
    pass

IDENTITY_API_SUFFIX = "identity"
VPC_API_SUFFIX = "vpc"
DEFAULT_HEADERS = {
    "Accept": "application/json",
    "Content-Type": "application/json"
}
_debug = False


def _debug_msg(msg, *args, **kwargs):
    global _debug
    if _debug:
        print(msg.format(*args, **kwargs), file=sys.stderr)


def generate_totp_passcode(secret):
    """Generate TOTP passcode.
    :param bytes secret: A base32 encoded secret for the TOTP authentication
    :returns: totp passcode as bytes
    """
    try:
        import mintotp
    except:
        sys.exit("TOTP code generation library is not present. Please install mintotp using 'pip install mintotp'")
    return mintotp.totp(secret)


class ZComputeApi(object):

    def __init__(self, api_base_url, account_name, project_name, user, password,
                 insecure=False, totp_code=None, mfa_secret=None, debug=False):
        self._api_base_url = '{}/api/v2'.format(api_base_url)
        self._account_name = account_name
        self._project_name = project_name
        self._user = user
        self._password = password
        self._insecure = insecure
        self._token = None
        self._totp_code = totp_code
        self._mfa_secret = mfa_secret
        self._debug = debug

    def reset_api_params(self, token=None, project_name=None):
        if token:
            self._token = token
        if project_name:
            self._project_name = project_name

    def send_request(self, method='GET', api_path='', api_body=None, headers=None, raise_on_status=True):
        api_url = '{}/{}'.format(self._api_base_url, api_path)
        if headers:
            headers = copy.deepcopy(headers)
            headers.update(DEFAULT_HEADERS)
        else:
            headers = copy.deepcopy(DEFAULT_HEADERS)
        if self._token:
            headers['X-Auth-Token'] = self._token
        try:
            response = requests.request(
                method=method,
                url=api_url,
                headers=headers,
                json=api_body,
                verify=not self._insecure
            )
            if raise_on_status:
                response.raise_for_status()
        except Exception:
            _debug_msg("Failed sending {} {} reuquest".format(method, api_url))
            raise
        return response

    def send_api_request(self, method='GET', api_path='', api_body=None):
        return self.send_request(method=method, api_path=api_path, api_body=api_body).json()

    def _get_project_scope(self):
        return {"project": {"name": self._project_name, "domain": {"name": self._account_name}}}

    def _get_domain_scope(self):
        return {"domain": {"name": self._account_name}}

    def _get_password_auth(self):
        return {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "name": self._user,
                    "password": self._password,
                    "domain": {
                        "name": self._account_name
                    }
                }
            }
        }

    def _add_totp_code(self, identity_auth, totp_code):
        if identity_auth.get('methods'):
            identity_auth['methods'].append('totp')
        else:
            identity_auth['methods'] = ['totp']
        identity_auth['totp'] = {
            "user": {
                "name": self._user,
                "passcode": totp_code,
                "domain": {
                    "name": self._account_name
                }
            }
        }
        return identity_auth

    def get_project_token(self):
        identity_auth = self._get_password_auth()
        if self._mfa_secret:
            self._totp_code = generate_totp_passcode(self._mfa_secret)
        if self._totp_code:
            self._add_totp_code(identity_auth, self._totp_code)
        auth_json = {
            "auth": {
                "identity": identity_auth,
                "scope": self._get_project_scope()
            }
        }
        response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX), auth_json, raise_on_status=False)
        if response.status_code == requests.codes.UNAUTHORIZED and response.json().get('receipt'):
            os_receipt = response.headers.get('openstack-auth-receipt')
            if self._mfa_secret:
                _debug_msg("Generating MFA secret")
                totp_code = generate_totp_passcode(self._mfa_secret)
            else:
                totp_code = str(input('MFA Code: ')).lower().strip()
            identity_auth = self._add_totp_code({}, totp_code)
            auth_json = {
                "auth": {
                    "identity": identity_auth,
                    "scope": self._get_project_scope()
                }
            }
            response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX),
                                         auth_json,
                                         headers={"openstack-auth-receipt": os_receipt})
        else:
            response.raise_for_status()

        return response.headers['x-subject-token']

    def get_account_token(self):
        auth_json = {
            "auth": {
                "identity": self._get_password_auth(),
                "scope": self._get_domain_scope()
            }
        }
        auth_response = self.send_request('POST', '{}/auth'.format(IDENTITY_API_SUFFIX), auth_json)
        return auth_response.headers['x-subject-token']

    def get_user_default_project(self):
        details_api_uri = '{}/users/myself/projects'.format(IDENTITY_API_SUFFIX)
        projects = self.send_api_request('GET', details_api_uri)
        if len(projects) > 1:
            sys.exit("There are {} projects: {}\n"
                     "please select one using --project flag".format(
                          len(projects), ', '.join([p['name'] for p in projects])))
        return projects[0]['name']


def ask_for_value(parameter, current, hide=False):
    if hide:
        value = getpass.getpass("{} []: ".format(parameter))
    else:
        value = input("{} [{}]: ".format(parameter, current or '')).strip()
    if value:
        return value
    return current


def main():
    global _debug
    parser = argparse.ArgumentParser(description="Obtain zCompute API Token using user credentials")
    parser.add_argument("cluster", help="Cluster API endpoint IP or host name")
    parser.add_argument("--interactive",
                        help="Will get login credentials interactively",
                        action='store_true',
                        default=False)
    parser.add_argument("--account",
                        help="Account name (will use environment variable ZCOMPUTE_ACCOUNT if not provided)",
                        default=os.environ.get('ZCOMPUTE_ACCOUNT', None),
                        required=False)
    parser.add_argument("--username",
                        help="User name (will use environment variable ZCOMPUTE_USER if not provided)",
                        default=os.environ.get('ZCOMPUTE_USER', None),
                        required=False)
    parser.add_argument("--mfa-secret",
                        help="MFA secret to generate totp code MFA enabled login "
                             "(will use environment variable ZCOMPUTE_MFASECRET if not provided)",
                        default=os.environ.get('ZCOMPUTE_MFASECRET', None),
                        required=False)
    parser.add_argument("--totp-code",
                        help="TOTP code to use for MFA enabled login "
                             "(will use environment variable ZCOMPUTE_TOTP if not provided)",
                        default=os.environ.get('ZCOMPUTE_TOTP', None),
                        required=False)
    parser.add_argument("--project",
                        dest='project_name',
                        help="Project name (will use environment variable ZCOMPUTE_PROJECT if not provided)"
                             " if not provided and only one exists in the account will it",
                        default=os.environ.get('ZCOMPUTE_PROJECT', None),
                        required=False)
    parser.add_argument("--password",
                        help="Password - will use environment variable ZCOMPUTE_PASSWORD if not provided",
                        default=os.environ.get('ZCOMPUTE_PASSWORD', None))
    parser.add_argument("--debug", help="Print debug messages", default=False, action='store_true')
    parser.add_argument("-k", "--insecure", action='store_true', help="Do not verify cluster certificate", default=False)
    args = parser.parse_args()
    if args.debug:
        _debug = True
    if args.interactive:
        args.account = ask_for_value('account', args.account)
        args.project_name = ask_for_value('project name', args.project_name)
        args.username = ask_for_value('User name', args.username)
        args.mfa_secret = ask_for_value('MFA secret', args.mfa_secret, hide=True)
        args.totp_code = ask_for_value('TOTP code', args.totp_code)
        args.password = ask_for_value('Password', args.password, hide=True)

    url = 'https://{}'.format(args.cluster)
    if args.cluster.startswith('https://'):
        url = args.cluster
    api = ZComputeApi(url, args.account, args.project_name, args.username, args.password,
                      insecure=args.insecure, totp_code=args.totp_code, mfa_secret=args.mfa_secret, debug=args.debug)
    if not args.password:
        sys.exit("Please provide a password")
    if not args.project_name:
        if args.account is None:
            sys.exit("Please provide the account name to login into")
        if args.username is None:
            sys.exit("Please provide the user name to login with")
        api.reset_api_params(token=api.get_account_token())
        project_name = api.get_user_default_project()
    else:
        if args.project_name is None:
            sys.exit("Please provide the project name to login into")
        project_name = args.project_name
    api.reset_api_params(project_name=project_name)
    print(api.get_project_token())


if __name__ == '__main__':
    main()