Files
python-mistralclient/mistralclient/shell.py
Alex Schultz 5171cdd63a Create client for interactive shell
The client allows you to run an interactive mode that doesn't exit
between calls and reuses the client which is handy for end users.
Currently the code doesn't actually create a workflow engine client in
the interactive mode so all the mistral actions fail. We should create a
client if the client doesn't pass any args to the shell because that
enters interactive mode.

Change-Id: I8b4c56277f0f172da712f777c4c856dba6fa0b0b
Closes-Bug: #1861357
2020-01-29 16:53:52 -07:00

779 lines
28 KiB
Python

# Copyright 2015 - StackStorm, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Command-line interface to the Mistral APIs
"""
import argparse
import logging
import os
import sys
from cliff import app
from cliff import commandmanager
from osc_lib.command import command
from mistralclient.api import client
from mistralclient.auth import auth_types
import mistralclient.commands.v2.action_executions
import mistralclient.commands.v2.actions
import mistralclient.commands.v2.cron_triggers
import mistralclient.commands.v2.environments
import mistralclient.commands.v2.event_triggers
import mistralclient.commands.v2.executions
import mistralclient.commands.v2.members
import mistralclient.commands.v2.services
import mistralclient.commands.v2.tasks
import mistralclient.commands.v2.workbooks
import mistralclient.commands.v2.workflows
from mistralclient import exceptions as exe
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
class OpenStackHelpFormatter(argparse.HelpFormatter):
def __init__(self, prog, indent_increment=2, max_help_position=32,
width=None):
super(OpenStackHelpFormatter, self).__init__(
prog,
indent_increment,
max_help_position,
width
)
def start_section(self, heading):
# Title-case the headings.
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(OpenStackHelpFormatter, self).start_section(heading)
class HelpAction(argparse.Action):
"""Custom help action.
Provide a custom action so the -h and --help options
to the main app will print a list of the commands.
The commands are determined by checking the CommandManager
instance, passed in as the "default" value for the action.
"""
def __call__(self, parser, namespace, values, option_string=None):
outputs = []
max_len = 0
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands for API v2 :\n')
for name, ep in sorted(app.command_manager):
factory = ep.load()
cmd = factory(self, None)
one_liner = cmd.get_description().split('\n')[0]
outputs.append((name, one_liner))
max_len = max(len(name), max_len)
for (name, one_liner) in outputs:
app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner))
sys.exit(0)
class BashCompletionCommand(command.Command):
"""Prints all of the commands and options for bash-completion."""
def take_action(self, parsed_args):
commands = set()
options = set()
for option, _action in self.app.parser._option_string_actions.items():
options.add(option)
for command_name, _cmd in self.app.command_manager:
commands.add(command_name)
print(' '.join(commands | options))
class MistralShell(app.App):
def __init__(self):
super(MistralShell, self).__init__(
description=__doc__.strip(),
version=mistralclient.__version__,
command_manager=commandmanager.CommandManager('mistral.cli'),
)
# Set v2 commands by default
self._set_shell_commands(self._get_commands_v2())
def configure_logging(self):
log_lvl = logging.DEBUG if self.options.debug else logging.WARNING
logging.basicConfig(
format="%(levelname)s (%(module)s) %(message)s",
level=log_lvl
)
logging.getLogger('iso8601').setLevel(logging.WARNING)
if self.options.verbose_level <= 1:
logging.getLogger('requests').setLevel(logging.WARNING)
def build_option_parser(self, description, version,
argparse_kwargs=None):
"""Return an argparse option parser for this application.
Subclasses may override this method to extend
the parser with more global options.
:param description: full description of the application
:paramtype description: str
:param version: version number for the application
:paramtype version: str
:param argparse_kwargs: extra keyword argument passed to the
ArgumentParser constructor
:paramtype extra_kwargs: dict
"""
argparse_kwargs = argparse_kwargs or {}
parser = argparse.ArgumentParser(
description=description,
add_help=False,
formatter_class=OpenStackHelpFormatter,
**argparse_kwargs
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {0}'.format(version),
help='Show program\'s version number and exit.'
)
parser.add_argument(
'-v', '--verbose',
action='count',
dest='verbose_level',
default=self.DEFAULT_VERBOSE_LEVEL,
help='Increase verbosity of output. Can be repeated.',
)
parser.add_argument(
'--log-file',
action='store',
default=None,
help='Specify a file to log output. Disabled by default.',
)
parser.add_argument(
'-q', '--quiet',
action='store_const',
dest='verbose_level',
const=0,
help='Suppress output except warnings and errors.',
)
parser.add_argument(
'-h', '--help',
action=HelpAction,
nargs=0,
default=self, # tricky
help="Show this help message and exit.",
)
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='Show tracebacks on errors.',
)
parser.add_argument(
'--os-mistral-url',
action='store',
dest='mistral_url',
default=env('OS_MISTRAL_URL'),
help='Mistral API host (Env: OS_MISTRAL_URL)'
)
parser.add_argument(
'--os-mistral-version',
action='store',
dest='mistral_version',
default=env('OS_MISTRAL_VERSION', default='v2'),
help='Mistral API version (default = v2) (Env: '
'OS_MISTRAL_VERSION)'
)
parser.add_argument(
'--os-mistral-service-type',
action='store',
dest='service_type',
default=env('OS_MISTRAL_SERVICE_TYPE', default='workflowv2'),
help='Mistral service-type (should be the same name as in '
'keystone-endpoint) (default = workflowv2) (Env: '
'OS_MISTRAL_SERVICE_TYPE)'
)
parser.add_argument(
'--os-mistral-endpoint-type',
action='store',
dest='endpoint_type',
default=env('OS_MISTRAL_ENDPOINT_TYPE', default='publicURL'),
help='Mistral endpoint-type (should be the same name as in '
'keystone-endpoint) (default = publicURL) (Env: '
'OS_MISTRAL_ENDPOINT_TYPE)'
)
parser.add_argument(
'--os-username',
action='store',
dest='username',
default=env('OS_USERNAME'),
help='Authentication username (Env: OS_USERNAME)'
)
parser.add_argument(
'--os-password',
action='store',
dest='password',
default=env('OS_PASSWORD'),
help='Authentication password (Env: OS_PASSWORD)'
)
parser.add_argument(
'--os-tenant-id',
action='store',
dest='tenant_id',
default=env('OS_TENANT_ID', 'OS_PROJECT_ID'),
help='Authentication tenant identifier (Env: OS_TENANT_ID'
' or OS_PROJECT_ID)'
)
parser.add_argument(
'--os-project-id',
action='store',
dest='project_id',
default=env('OS_TENANT_ID', 'OS_PROJECT_ID'),
help='Authentication project identifier (Env: OS_TENANT_ID'
' or OS_PROJECT_ID), will use tenant_id if both tenant_id'
' and project_id are set'
)
parser.add_argument(
'--os-tenant-name',
action='store',
dest='tenant_name',
default=env('OS_TENANT_NAME', 'OS_PROJECT_NAME'),
help='Authentication tenant name (Env: OS_TENANT_NAME'
' or OS_PROJECT_NAME)'
)
parser.add_argument(
'--os-project-name',
action='store',
dest='project_name',
default=env('OS_TENANT_NAME', 'OS_PROJECT_NAME'),
help='Authentication project name (Env: OS_TENANT_NAME'
' or OS_PROJECT_NAME), will use tenant_name if both'
' tenant_name and project_name are set'
)
parser.add_argument(
'--os-auth-token',
action='store',
dest='token',
default=env('OS_AUTH_TOKEN'),
help='Authentication token (Env: OS_AUTH_TOKEN)'
)
parser.add_argument(
'--os-project-domain-name',
action='store',
dest='project_domain_name',
default=env('OS_PROJECT_DOMAIN_NAME'),
help='Authentication project domain name or ID'
' (Env: OS_PROJECT_DOMAIN_NAME or OS_PROJECT_DOMAIN_NAME)'
)
parser.add_argument(
'--os-project-domain-id',
action='store',
dest='project_domain_id',
default=env('OS_PROJECT_DOMAIN_ID'),
help='Authentication project domain ID'
' (Env: OS_PROJECT_DOMAIN_ID)'
)
parser.add_argument(
'--os-user-domain-name',
action='store',
dest='user_domain_name',
default=env('OS_USER_DOMAIN_NAME'),
help='Authentication user domain name'
' (Env: OS_USER_DOMAIN_NAME)'
)
parser.add_argument(
'--os-user-domain-id',
action='store',
dest='user_domain_id',
default=env('OS_USER_DOMAIN_ID'),
help='Authentication user domain name'
' (Env: OS_USER_DOMAIN_ID)'
)
parser.add_argument(
'--os-auth-url',
action='store',
dest='auth_url',
default=env('OS_AUTH_URL'),
help='Authentication URL (Env: OS_AUTH_URL)'
)
parser.add_argument(
'--os-cert',
action='store',
dest='os_cert',
default=env('OS_CERT'),
help='Client Certificate (Env: OS_CERT)'
)
parser.add_argument(
'--os-key',
action='store',
dest='os_key',
default=env('OS_KEY'),
help='Client Key (Env: OS_KEY)'
)
parser.add_argument(
'--os-cacert',
action='store',
dest='os_cacert',
default=env('OS_CACERT'),
help='Authentication CA Certificate (Env: OS_CACERT)'
)
parser.add_argument(
'--os-region-name',
action='store',
dest='region_name',
default=env('OS_REGION_NAME'),
help='Region name (Env: OS_REGION_NAME)'
)
parser.add_argument(
'--insecure',
action='store_true',
dest='insecure',
default=env('MISTRALCLIENT_INSECURE', default=False),
help='Disables SSL/TLS certificate verification '
'(Env: MISTRALCLIENT_INSECURE)'
)
parser.add_argument(
'--auth-type',
action='store',
dest='auth_type',
default=env('MISTRAL_AUTH_TYPE', default='keystone'),
help='Authentication type. Valid options are: %s.'
' (Env: MISTRAL_AUTH_TYPE)' % ', '.join(auth_types.ALL)
)
parser.add_argument(
'--openid-client-id',
action='store',
dest='client_id',
default=env('OPENID_CLIENT_ID'),
help='Client ID (according to OpenID Connect).'
' (Env: OPENID_CLIENT_ID)'
)
parser.add_argument(
'--openid-client-secret',
action='store',
dest='client_secret',
default=env('OPENID_CLIENT_SECRET'),
help='Client secret (according to OpenID Connect)'
' (Env: OPENID_CLIENT_SECRET)'
)
parser.add_argument(
'--os-target-username',
action='store',
dest='target_username',
default=env('OS_TARGET_USERNAME', default='admin'),
help='Authentication username for target cloud'
' (Env: OS_TARGET_USERNAME)'
)
parser.add_argument(
'--os-target-password',
action='store',
dest='target_password',
default=env('OS_TARGET_PASSWORD'),
help='Authentication password for target cloud'
' (Env: OS_TARGET_PASSWORD)'
)
parser.add_argument(
'--os-target-tenant-id',
action='store',
dest='target_tenant_id',
default=env('OS_TARGET_TENANT_ID'),
help='Authentication tenant identifier for target cloud'
' (Env: OS_TARGET_TENANT_ID)'
)
parser.add_argument(
'--os-target-tenant-name',
action='store',
dest='target_tenant_name',
default=env('OS_TARGET_TENANT_NAME'),
help='Authentication tenant name for target cloud'
' (Env: OS_TARGET_TENANT_NAME)'
)
parser.add_argument(
'--os-target-auth-token',
action='store',
dest='target_token',
default=env('OS_TARGET_AUTH_TOKEN'),
help='Authentication token for target cloud'
' (Env: OS_TARGET_AUTH_TOKEN)'
)
parser.add_argument(
'--os-target-auth-url',
action='store',
dest='target_auth_url',
default=env('OS_TARGET_AUTH_URL'),
help='Authentication URL for target cloud'
' (Env: OS_TARGET_AUTH_URL)'
)
parser.add_argument(
'--os-target_cacert',
action='store',
dest='target_cacert',
default=env('OS_TARGET_CACERT'),
help='Authentication CA Certificate for target cloud'
' (Env: OS_TARGET_CACERT)'
)
parser.add_argument(
'--os-target-region-name',
action='store',
dest='target_region_name',
default=env('OS_TARGET_REGION_NAME'),
help='Region name for target cloud'
'(Env: OS_TARGET_REGION_NAME)'
)
parser.add_argument(
'--os-target-user-domain-name',
action='store',
dest='target_user_domain_name',
default=env('OS_TARGET_USER_DOMAIN_NAME'),
help='User domain name for target cloud'
'(Env: OS_TARGET_USER_DOMAIN_NAME)'
)
parser.add_argument(
'--os-target-user-domain-id',
action='store',
dest='target_user_domain_id',
default=env('OS_TARGET_USER_DOMAIN_ID'),
help='User domain ID for target cloud'
'(Env: OS_TARGET_USER_DOMAIN_ID)'
)
parser.add_argument(
'--os-target-project-domain-name',
action='store',
dest='target_project_domain_name',
default=env('OS_TARGET_PROJECT_DOMAIN_NAME'),
help='Project domain name for target cloud'
'(Env: OS_TARGET_PROJECT_DOMAIN_NAME)'
)
parser.add_argument(
'--os-target-project-domain-id',
action='store',
dest='target_project_domain_id',
default=env('OS_TARGET_PROJECT_DOMAIN_ID'),
help='Project domain ID for target cloud'
'(Env: OS_TARGET_PROJECT_DOMAIN_ID)'
)
parser.add_argument(
'--target_insecure',
action='store_true',
dest='target_insecure',
default=env('TARGET_MISTRALCLIENT_INSECURE', default=False),
help='Disables SSL/TLS certificate verification for target cloud '
'(Env: TARGET_MISTRALCLIENT_INSECURE)'
)
parser.add_argument(
'--profile',
dest='profile',
metavar='HMAC_KEY',
default=env('OS_PROFILE'),
help='HMAC key to use for encrypting context data for performance '
'profiling of operation. This key should be one of the '
'values configured for the osprofiler middleware in mistral, '
'it is specified in the profiler section of the mistral '
'configuration (i.e. /etc/mistral/mistral.conf). Without the '
'key, profiling will not be triggered even if osprofiler is '
'enabled on the server side.'
)
return parser
def initialize_app(self, argv):
self._clear_shell_commands()
ver = client.determine_client_version(self.options.mistral_version)
self._set_shell_commands(self._get_commands(ver))
# bash-completion and help messages should not require client creation
need_client = not (
('bash-completion' in argv) or
('help' in argv) or
('-h' in argv) or
('--help' in argv))
# Set default for auth_url if not supplied. The default is not
# set at the parser to support use cases where auth is not enabled.
# An example use case would be a developer's environment.
if not self.options.auth_url:
if self.options.password or self.options.token:
self.options.auth_url = 'http://localhost:35357/v3'
if (self.options.auth_type == 'keystone' and
not self.options.auth_url.endswith("/v2.0")):
# Assume that keystone V3 is used and try to be more user-friendly,
# i.e provide default values for domains
if (not self.options.project_domain_id and
not self.options.project_domain_name):
self.options.project_domain_id = "default"
if (not self.options.user_domain_id and
not self.options.user_domain_name):
self.options.user_domain_id = "default"
if (not self.options.target_project_domain_id and
not self.options.target_project_domain_name):
self.options.target_project_domain_id = "default"
if (not self.options.target_user_domain_id and
not self.options.target_user_domain_name):
self.options.target_user_domain_id = "default"
if self.options.auth_url and not self.options.token:
if not self.options.username:
raise exe.IllegalArgumentException(
("You must provide a username "
"via --os-username env[OS_USERNAME]")
)
if not self.options.password:
raise exe.IllegalArgumentException(
("You must provide a password "
"via --os-password env[OS_PASSWORD]")
)
self.client = self._create_client() if need_client else None
# Adding client_manager variable to make mistral client work with
# unified OpenStack client.
ClientManager = type(
'ClientManager',
(object,),
dict(workflow_engine=self.client)
)
self.client_manager = ClientManager()
def _create_client(self):
kwargs = {
'cert': self.options.os_cert,
'key': self.options.os_key,
'user_domain_name': self.options.user_domain_name,
'user_domain_id': self.options.user_domain_id,
'project_domain_name': self.options.project_domain_name,
'project_domain_id': self.options.project_domain_id,
'target_project_domain_name':
self.options.target_project_domain_name,
'target_project_domain_id': self.options.target_project_domain_id,
'target_user_domain_name': self.options.target_user_domain_name,
'target_user_domain_id': self.options.target_user_domain_id
}
return client.client(
mistral_url=self.options.mistral_url,
username=self.options.username,
api_key=self.options.password,
project_name=self.options.tenant_name or self.options.project_name,
auth_url=self.options.auth_url,
project_id=self.options.tenant_id or self.options.project_id,
endpoint_type=self.options.endpoint_type,
service_type=self.options.service_type,
region_name=self.options.region_name,
auth_token=self.options.token,
cacert=self.options.os_cacert,
insecure=self.options.insecure,
profile=self.options.profile,
auth_type=self.options.auth_type,
client_id=self.options.client_id,
client_secret=self.options.client_secret,
target_username=self.options.target_username,
target_api_key=self.options.target_password,
target_project_name=self.options.target_tenant_name,
target_auth_url=self.options.target_auth_url,
target_project_id=self.options.target_tenant_id,
target_auth_token=self.options.target_token,
target_cacert=self.options.target_cacert,
target_region_name=self.options.target_region_name,
target_insecure=self.options.target_insecure,
**kwargs
)
def _set_shell_commands(self, cmds_dict):
for k, v in cmds_dict.items():
self.command_manager.add_command(k, v)
def _clear_shell_commands(self):
exclude_cmds = ['help', 'complete']
cmds = self.command_manager.commands.copy()
for k, v in cmds.items():
if k not in exclude_cmds:
self.command_manager.commands.pop(k)
def _get_commands(self, version):
if version == 2:
return self._get_commands_v2()
return {}
@staticmethod
def _get_commands_v2():
return {
'bash-completion': BashCompletionCommand,
'workbook-list': mistralclient.commands.v2.workbooks.List,
'workbook-get': mistralclient.commands.v2.workbooks.Get,
'workbook-create': mistralclient.commands.v2.workbooks.Create,
'workbook-delete': mistralclient.commands.v2.workbooks.Delete,
'workbook-update': mistralclient.commands.v2.workbooks.Update,
'workbook-get-definition':
mistralclient.commands.v2.workbooks.GetDefinition,
'workbook-validate': mistralclient.commands.v2.workbooks.Validate,
'workflow-list': mistralclient.commands.v2.workflows.List,
'workflow-get': mistralclient.commands.v2.workflows.Get,
'workflow-create': mistralclient.commands.v2.workflows.Create,
'workflow-delete': mistralclient.commands.v2.workflows.Delete,
'workflow-update': mistralclient.commands.v2.workflows.Update,
'workflow-get-definition':
mistralclient.commands.v2.workflows.GetDefinition,
'workflow-validate': mistralclient.commands.v2.workflows.Validate,
'environment-create':
mistralclient.commands.v2.environments.Create,
'environment-delete':
mistralclient.commands.v2.environments.Delete,
'environment-update':
mistralclient.commands.v2.environments.Update,
'environment-list': mistralclient.commands.v2.environments.List,
'environment-get': mistralclient.commands.v2.environments.Get,
'run-action': mistralclient.commands.v2.action_executions.Create,
'action-execution-list':
mistralclient.commands.v2.action_executions.List,
'action-execution-get':
mistralclient.commands.v2.action_executions.Get,
'action-execution-get-input':
mistralclient.commands.v2.action_executions.GetInput,
'action-execution-get-output':
mistralclient.commands.v2.action_executions.GetOutput,
'action-execution-update':
mistralclient.commands.v2.action_executions.Update,
'action-execution-delete':
mistralclient.commands.v2.action_executions.Delete,
'execution-create': mistralclient.commands.v2.executions.Create,
'execution-delete': mistralclient.commands.v2.executions.Delete,
'execution-update': mistralclient.commands.v2.executions.Update,
'execution-list': mistralclient.commands.v2.executions.List,
'execution-get': mistralclient.commands.v2.executions.Get,
'execution-get-input':
mistralclient.commands.v2.executions.GetInput,
'execution-get-output':
mistralclient.commands.v2.executions.GetOutput,
'execution-get-report':
mistralclient.commands.v2.executions.GetReport,
'execution-get-published':
mistralclient.commands.v2.executions.GetPublished,
'execution-get-sub-executions':
mistralclient.commands.v2.executions.SubExecutionsLister,
'task-list': mistralclient.commands.v2.tasks.List,
'task-get': mistralclient.commands.v2.tasks.Get,
'task-get-published': mistralclient.commands.v2.tasks.GetPublished,
'task-get-result': mistralclient.commands.v2.tasks.GetResult,
'task-get-sub-executions':
mistralclient.commands.v2.tasks.SubExecutionsLister,
'task-rerun': mistralclient.commands.v2.tasks.Rerun,
'action-list': mistralclient.commands.v2.actions.List,
'action-get': mistralclient.commands.v2.actions.Get,
'action-create': mistralclient.commands.v2.actions.Create,
'action-delete': mistralclient.commands.v2.actions.Delete,
'action-update': mistralclient.commands.v2.actions.Update,
'action-get-definition':
mistralclient.commands.v2.actions.GetDefinition,
'action-validate': mistralclient.commands.v2.actions.Validate,
'cron-trigger-list': mistralclient.commands.v2.cron_triggers.List,
'cron-trigger-get': mistralclient.commands.v2.cron_triggers.Get,
'cron-trigger-create':
mistralclient.commands.v2.cron_triggers.Create,
'cron-trigger-delete':
mistralclient.commands.v2.cron_triggers.Delete,
'event-trigger-list':
mistralclient.commands.v2.event_triggers.List,
'event-trigger-get': mistralclient.commands.v2.event_triggers.Get,
'event-trigger-create':
mistralclient.commands.v2.event_triggers.Create,
'event-trigger-delete':
mistralclient.commands.v2.event_triggers.Delete,
'service-list': mistralclient.commands.v2.services.List,
'member-create': mistralclient.commands.v2.members.Create,
'member-delete': mistralclient.commands.v2.members.Delete,
'member-update': mistralclient.commands.v2.members.Update,
'member-list': mistralclient.commands.v2.members.List,
'member-get': mistralclient.commands.v2.members.Get,
}
def main(argv=sys.argv[1:]):
return MistralShell().run(argv)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))