From fe527999aa28b78b69aa449c3da4098fca6210b2 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 18 Oct 2017 14:00:57 -0500 Subject: [PATCH] CLI support of Keystone openrc Add support of the standard openrc env vars to the CLI so that a token will be automatically generated and the Drydock API endpoint will be sourced from the Keystone catalogue. - Use click to allow env or cli based keystone env Change-Id: I10a48d509c0b90670af07870a1ae4d31525b4a3b --- drydock_provisioner/cli/commands.py | 72 ++++++++++++++----- drydock_provisioner/drydock_client/session.py | 45 +++++++++++- 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/drydock_provisioner/cli/commands.py b/drydock_provisioner/cli/commands.py index 75825a58..479b313f 100644 --- a/drydock_provisioner/cli/commands.py +++ b/drydock_provisioner/cli/commands.py @@ -18,6 +18,7 @@ from urllib.parse import urlparse import click from drydock_provisioner.drydock_client.session import DrydockSession +from drydock_provisioner.drydock_client.session import KeystoneClient from drydock_provisioner.drydock_client.client import DrydockClient from .design import commands as design from .part import commands as part @@ -27,32 +28,43 @@ from .node import commands as node @click.group() @click.option( '--debug/--no-debug', help='Enable or disable debugging', default=False) +# Supported Environment Variables @click.option( - '--token', - '-t', - help='The auth token to be used', - default=lambda: os.environ.get('DD_TOKEN', '')) + '--os_project_domain_name', + envvar='OS_PROJECT_DOMAIN_NAME', + required=False) +@click.option( + '--os_user_domain_name', envvar='OS_USER_DOMAIN_NAME', required=False) +@click.option('--os_project_name', envvar='OS_PROJECT_NAME', required=False) +@click.option('--os_username', envvar='OS_USERNAME', required=False) +@click.option('--os_password', envvar='OS_PASSWORD', required=False) +@click.option('--os_auth_url', envvar='OS_AUTH_URL', required=False) +@click.option( + '--os_token', + help='The Keystone token to be used', + default=lambda: os.environ.get('OS_TOKEN', '')) @click.option( '--url', '-u', help='The url of the running drydock instance', default=lambda: os.environ.get('DD_URL', '')) @click.pass_context -def drydock(ctx, debug, token, url): - """ Drydock CLI to invoke the running instance of the drydock API - """ +def drydock(ctx, debug, url, os_project_domain_name, os_user_domain_name, os_project_name, + os_username, os_password, os_auth_url, os_token): + """Drydock CLI to invoke the running instance of the drydock API.""" if not ctx.obj: ctx.obj = {} ctx.obj['DEBUG'] = debug - if not token: - ctx.fail('Error: Token must be specified either by ' - '--token or DD_TOKEN from the environment') - - if not url: - ctx.fail('Error: URL must be specified either by ' - '--url or DD_URL from the environment') + keystone_env = { + 'project_domain_name': os_project_domain_name, + 'user_domain_name': os_user_domain_name, + 'project_name': os_project_name, + 'username': os_username, + 'password': os_password, + 'auth_url': os_auth_url, + } # setup logging for the CLI # Setup root logger @@ -66,18 +78,44 @@ def drydock(ctx, debug, token, url): logger.addHandler(logging_handler) logger.debug('logging for cli initialized') + try: + if not os_token: + logger.debug("Generating Keystone session by env vars: %s" % str(keystone_env)) + ks_sess = KeystoneClient.get_ks_session(**keystone_env) + else: + logger.debug("Generating Keystone session by explicit token: %s" % os_token) + ks_sess = KeystoneClient.get_ks_session(token=os_token) + KeystoneClient.get_token(ks_sess=ks_sess) + except Exception as ex: + logger.debug("Exception getting Keystone session.", exc_info=ex) + ctx.fail('Error: Unable to authenticate with Keystone') + return + + try: + if not url: + url = KeystoneClient.get_endpoint('physicalprovisioner', ks_sess=ks_sess) + except Exception as ex: + logger.debug("Exception getting Drydock endpoint.", exc_info=ex) + ctx.fail('Error: Unable to discover Drydock API URL') + # setup the drydock client using the passed parameters. url_parse_result = urlparse(url) - logger.debug(url_parse_result) + + if not os_token: + token = KeystoneClient.get_token(ks_sess=ks_sess) + logger.debug("Creating Drydock client with token %s." % token) + else: + token = os_token + if not url_parse_result.scheme: ctx.fail('URL must specify a scheme and hostname, optionally a port') ctx.obj['CLIENT'] = DrydockClient( DrydockSession( scheme=url_parse_result.scheme, - host=url_parse_result.netloc, + host=url_parse_result.hostname, + port=url_parse_result.port, token=token)) - drydock.add_command(design.design) drydock.add_command(part.part) drydock.add_command(task.task) diff --git a/drydock_provisioner/drydock_client/session.py b/drydock_provisioner/drydock_client/session.py index a26e54fb..10993f59 100644 --- a/drydock_provisioner/drydock_client/session.py +++ b/drydock_provisioner/drydock_client/session.py @@ -14,6 +14,8 @@ import requests import logging +from keystoneauth1 import session +from keystoneauth1.identity import v3 class DrydockSession(object): """ @@ -40,7 +42,7 @@ class DrydockSession(object): self.base_url = "%s://%s:%s/api/" % (self.scheme, self.host, self.port) else: - #assume default port for scheme + # assume default port for scheme self.base_url = "%s://%s/api/" % (self.scheme, self.host) self.token = token @@ -48,7 +50,6 @@ class DrydockSession(object): self.logger = logging.getLogger(__name__) - # TODO Add keystone authentication to produce a token for this session def get(self, endpoint, query=None): """ Send a GET request to Drydock. @@ -85,3 +86,43 @@ class DrydockSession(object): self.base_url + endpoint, params=query, json=data, timeout=10) return resp + + +class KeystoneClient(object): + + @staticmethod + def get_endpoint(endpoint, ks_sess=None, auth_info=None): + """ + Wraps calls to keystone for lookup of an endpoint by service type + :param endpoint: The endpoint to look up + :param ks_sess: A keystone session to use for accessing endpoint catalogue + :param auth_info: Authentication info to use for building a token if a ``ks_sess`` is not specified + :returns: The url string of the endpoint + :rtype: str + :raises AppError: if the endpoint cannot be resolved + """ + if ks_sess is None: + ks_sess = KeystoneClient.get_ks_session(**auth_info) + + return ks_sess.get_endpoint(interface='internal', service_type=endpoint) + + @staticmethod + def get_token(ks_sess=None, auth_info=None): + """ + Returns the simple token string for a token acquired from keystone + + :param ks_sess: an existing Keystone session to retrieve a token from + :param auth_info: dictionary of information required to generate a keystone token + """ + if ks_sess is None: + ks_sess = KeystoneClient.get_ks_session(**auth_info) + return ks_sess.get_auth_headers().get('X-Auth-Token') + + @staticmethod + def get_ks_session(**kwargs): + # Establishes a keystone session + if 'token' in kwargs: + auth = v3.TokenMethod(token=kwargs.get('token')) + else: + auth = v3.Password(**kwargs) + return session.Session(auth=auth)