diff --git a/lower-constraints.txt b/lower-constraints.txt index f03c6977..bdafcd0d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -20,11 +20,9 @@ eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 -flake8==2.5.5 future==0.16.0 futurist==1.2.0 greenlet==0.4.15 -hacking==0.12.0 idna==2.6 imagesize==0.7.1 iso8601==0.1.11 @@ -37,7 +35,6 @@ keystoneauth1==3.4.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.1 -mccabe==0.2.1 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 @@ -72,7 +69,6 @@ positional==1.2.1 prettytable==0.7.2 pyasn1==0.1.8 pycparser==2.18 -pyflakes==0.8.1 Pygments==2.2.0 pyinotify==0.9.6 pyOpenSSL==17.1.0 diff --git a/mistralclient/api/base.py b/mistralclient/api/base.py index ea3ba299..41b7250b 100644 --- a/mistralclient/api/base.py +++ b/mistralclient/api/base.py @@ -49,9 +49,11 @@ class Resource(object): return copy.deepcopy(self._data) def __str__(self): - vals = ", ".join(["%s='%s'" % (n, v) - for n, v in self._data.items()]) - return "%s [%s]" % (self.resource_name, vals) + values = ", ".join( + ["%s='%s'" % (n, v) for n, v in self._data.items()] + ) + + return "%s [%s]" % (self.resource_name, values) def _check_items(obj, searches): @@ -130,9 +132,16 @@ class ResourceManager(object): def _validate(self, url, data, response_key=None, dump_json=True, headers=None, is_iter_resp=False): - return self._create(url, data, response_key, dump_json, - headers, is_iter_resp, resp_status_ok=200, - as_class=False) + return self._create( + url, + data, + response_key, + dump_json, + headers, + is_iter_resp, + resp_status_ok=200, + as_class=False + ) def _create(self, url, data, response_key=None, dump_json=True, headers=None, is_iter_resp=False, resp_status_ok=201, @@ -149,9 +158,13 @@ class ResourceManager(object): self._raise_api_exception(resp) resource = extract_json(resp, response_key) + if is_iter_resp: - return [self.resource_class(self, resource_data) - for resource_data in resource] + return [ + self.resource_class(self, resource_data) + for resource_data in resource + ] + return self.resource_class(self, resource) if as_class else resource def _update(self, url, data, response_key=None, dump_json=True, @@ -168,9 +181,13 @@ class ResourceManager(object): self._raise_api_exception(resp) resource = extract_json(resp, response_key) + if is_iter_resp: - return [self.resource_class(self, resource_data) - for resource_data in resource] + return [ + self.resource_class(self, resource_data) + for resource_data in resource + ] + return self.resource_class(self, resource) def _list(self, url, response_key=None, headers=None, @@ -186,8 +203,10 @@ class ResourceManager(object): resource_class = returned_res_cls or self.resource_class - return [resource_class(self, resource_data) - for resource_data in extract_json(resp, response_key)] + return [ + resource_class(self, resource_data) + for resource_data in extract_json(resp, response_key) + ] def _get(self, url, response_key=None, headers=None): try: @@ -212,12 +231,17 @@ class ResourceManager(object): @staticmethod def _raise_api_exception(resp): try: - error_data = (resp.headers.get("Server-Error-Message", None) or - get_json(resp).get("faultstring")) + error_data = ( + resp.headers.get("Server-Error-Message", None) + or get_json(resp).get("faultstring") + ) except ValueError: error_data = resp.content - raise APIException(error_code=resp.status_code, - error_message=error_data) + + raise APIException( + error_code=resp.status_code, + error_message=error_data + ) def get_json(response): diff --git a/mistralclient/api/v2/actions.py b/mistralclient/api/v2/actions.py index a5f85661..277253c0 100644 --- a/mistralclient/api/v2/actions.py +++ b/mistralclient/api/v2/actions.py @@ -31,6 +31,7 @@ class ActionManager(base.ResourceManager): # definition file definition = utils.get_contents_if_file(definition) url = '/actions?scope=%s' % scope + if namespace: url += '&namespace=%s' % namespace @@ -45,14 +46,18 @@ class ActionManager(base.ResourceManager): def update(self, definition, scope='private', id=None, namespace=''): self._ensure_not_empty(definition=definition) + params = '?scope=%s' % scope + if namespace: params += '&namespace=%s' % namespace url = ('/actions/%s' % id if id else '/actions') + params + # If the specified definition is actually a file, read in the # definition file definition = utils.get_contents_if_file(definition) + return self._update( url, definition, diff --git a/mistralclient/api/v2/client.py b/mistralclient/api/v2/client.py index d3e52e40..07512cf6 100644 --- a/mistralclient/api/v2/client.py +++ b/mistralclient/api/v2/client.py @@ -20,7 +20,9 @@ from oslo_utils import importutils from mistralclient.api import httpclient from mistralclient.api.v2 import action_executions from mistralclient.api.v2 import actions +from mistralclient.api.v2 import code_sources from mistralclient.api.v2 import cron_triggers +from mistralclient.api.v2 import dynamic_actions from mistralclient.api.v2 import environments from mistralclient.api.v2 import event_triggers from mistralclient.api.v2 import executions @@ -86,6 +88,11 @@ class Client(object): self.event_triggers = event_triggers.EventTriggerManager(http_client) self.environments = environments.EnvironmentManager(http_client) self.action_executions = action_executions.ActionExecutionManager( - http_client) + http_client + ) self.services = services.ServiceManager(http_client) self.members = members.MemberManager(http_client) + self.code_sources = code_sources.CodeSourceManager(http_client) + self.dynamic_actions = dynamic_actions.DynamicActionManager( + http_client + ) diff --git a/mistralclient/api/v2/code_sources.py b/mistralclient/api/v2/code_sources.py new file mode 100644 index 00000000..dc1f73ba --- /dev/null +++ b/mistralclient/api/v2/code_sources.py @@ -0,0 +1,88 @@ +# Copyright 2020 Nokia Software. +# +# 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. + +from mistralclient.api import base +from mistralclient import utils + + +class CodeSource(base.Resource): + resource_name = 'CodeSource' + + +class CodeSourceManager(base.ResourceManager): + resource_class = CodeSource + + def create(self, name, content, namespace='', scope='private'): + self._ensure_not_empty(name=name, content=content) + + # If the specified content is actually a file, read from it. + content = utils.get_contents_if_file(content) + + return self._create( + '/code_sources?name=%s&scope=%s&namespace=%s' % + (name, scope, namespace), + content, + dump_json=False, + headers={'content-type': 'text/plain'} + ) + + def update(self, identifier, content, namespace='', scope='private'): + self._ensure_not_empty(identifier=identifier, content=content) + + # If the specified content is actually a file, read from it. + content = utils.get_contents_if_file(content) + + return self._update( + '/code_sources?identifier=%s&scope=%s&namespace=%s' % + (identifier, scope, namespace), + content, + dump_json=False, + headers={'content-type': 'text/plain'}, + ) + + def list(self, namespace='', marker='', limit=None, sort_keys='', + sort_dirs='', fields='', **filters): + if namespace: + filters['namespace'] = namespace + + query_string = self._build_query_params( + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + filters=filters + ) + + return self._list( + '/code_sources%s' % query_string, + response_key='code_sources', + ) + + def get(self, identifier, namespace=''): + self._ensure_not_empty(identifier=identifier) + + return self._get( + '/code_sources/%s?namespace=%s' % (identifier, namespace) + ) + + def delete(self, identifier, namespace=None): + self._ensure_not_empty(identifier=identifier) + + url = '/code_sources/%s' % identifier + + if namespace: + url = url + '?namespace=%s' % namespace + + self._delete(url) diff --git a/mistralclient/api/v2/dynamic_actions.py b/mistralclient/api/v2/dynamic_actions.py new file mode 100644 index 00000000..11bfad1c --- /dev/null +++ b/mistralclient/api/v2/dynamic_actions.py @@ -0,0 +1,105 @@ +# Copyright 2020 Nokia Software. +# +# 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. + +from oslo_utils import uuidutils + +from mistralclient.api import base + + +class DynamicAction(base.Resource): + resource_name = 'DynamicAction' + + +class DynamicActionManager(base.ResourceManager): + resource_class = DynamicAction + + def get(self, identifier, namespace=''): + self._ensure_not_empty(identifier=identifier) + + return self._get( + '/dynamic_actions/%s?namespace=%s' % (identifier, namespace) + ) + + def create(self, name, class_name, code_source, scope='private', + namespace=''): + self._ensure_not_empty( + name=name, + class_name=class_name, + code_source=code_source + ) + + data = { + "name": name, + "class_name": class_name, + "scope": scope, + "namespace": namespace + } + + if uuidutils.is_uuid_like(code_source): + data['code_source_id'] = code_source + else: + data['code_source_name'] = code_source + + return self._create('/dynamic_actions', data) + + def update(self, identifier, class_name=None, code_source=None, + scope='private', namespace=''): + self._ensure_not_empty(identifier=identifier) + + data = { + 'scope': scope, + 'namespace': namespace + } + + if uuidutils.is_uuid_like(identifier): + data['id'] = identifier + else: + data['name'] = identifier + + if class_name: + data['class_name'] = class_name + + if code_source: + if uuidutils.is_uuid_like(code_source): + data['code_source_id'] = code_source + else: + data['code_source_name'] = code_source + + return self._update('/dynamic_actions', data) + + def list(self, marker='', limit=None, sort_keys='', sort_dirs='', + fields='', namespace='', **filters): + if namespace: + filters['namespace'] = namespace + + query_string = self._build_query_params( + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + filters=filters + ) + + return self._list( + '/dynamic_actions%s' % query_string, + response_key='dynamic_actions', + ) + + def delete(self, identifier, namespace=''): + self._ensure_not_empty(identifier=identifier) + + self._delete( + '/dynamic_actions/%s?namespace=%s' % (identifier, namespace) + ) diff --git a/mistralclient/api/v2/executions.py b/mistralclient/api/v2/executions.py index 71b5ccc1..535b34c3 100644 --- a/mistralclient/api/v2/executions.py +++ b/mistralclient/api/v2/executions.py @@ -28,11 +28,11 @@ class Execution(base.Resource): class ExecutionManager(base.ResourceManager): resource_class = Execution - def create(self, workflow_identifier='', namespace='', + def create(self, wf_identifier='', namespace='', workflow_input=None, description='', source_execution_id=None, **params): self._ensure_not_empty( - workflow_identifier=workflow_identifier or source_execution_id + workflow_identifier=wf_identifier or source_execution_id ) data = {'description': description} @@ -40,11 +40,11 @@ class ExecutionManager(base.ResourceManager): if uuidutils.is_uuid_like(source_execution_id): data.update({'source_execution_id': source_execution_id}) - if workflow_identifier: - if uuidutils.is_uuid_like(workflow_identifier): - data.update({'workflow_id': workflow_identifier}) + if wf_identifier: + if uuidutils.is_uuid_like(wf_identifier): + data.update({'workflow_id': wf_identifier}) else: - data.update({'workflow_name': workflow_identifier}) + data.update({'workflow_name': wf_identifier}) if namespace: data.update({'workflow_namespace': namespace}) diff --git a/mistralclient/api/v2/workbooks.py b/mistralclient/api/v2/workbooks.py index 64cd3647..4626bb3f 100644 --- a/mistralclient/api/v2/workbooks.py +++ b/mistralclient/api/v2/workbooks.py @@ -25,19 +25,19 @@ class WorkbookManager(base.ResourceManager): resource_class = Workbook def _get_workbooks_url(self, resource=None, namespace=None, scope=None): - path = '/workbooks' + url = '/workbooks' if resource: - path += '/%s' % resource + url += '/%s' % resource if scope and namespace: - path += '?scope=%s&namespace=%s' % (scope, namespace) + url += '?scope=%s&namespace=%s' % (scope, namespace) elif scope: - path += '?scope=%s' % scope + url += '?scope=%s' % scope elif namespace: - path += '?namespace=%s' % namespace + url += '?namespace=%s' % namespace - return path + return url def create(self, definition, namespace='', scope='private'): self._ensure_not_empty(definition=definition) @@ -78,8 +78,10 @@ class WorkbookManager(base.ResourceManager): namespace=namespace ) - return self._list('/workbooks{}'.format(query_string), - response_key='workbooks') + return self._list( + '/workbooks{}'.format(query_string), + response_key='workbooks' + ) def get(self, name, namespace=''): self._ensure_not_empty(name=name) diff --git a/mistralclient/commands/v2/action_executions.py b/mistralclient/commands/v2/action_executions.py index 166329bd..b73f5171 100644 --- a/mistralclient/commands/v2/action_executions.py +++ b/mistralclient/commands/v2/action_executions.py @@ -51,11 +51,13 @@ class ActionExecutionFormatter(base.MistralFormatter): columns = ActionExecutionFormatter.LIST_COLUMN_HEADING_NAMES else: columns = ActionExecutionFormatter.headings() + if action_ex: if hasattr(action_ex, 'task_name'): task_name = action_ex.task_name else: task_name = None + data = ( action_ex.id, action_ex.name, @@ -63,16 +65,19 @@ class ActionExecutionFormatter(base.MistralFormatter): action_ex.workflow_namespace, task_name, action_ex.task_execution_id, - action_ex.state,) + action_ex.state, + ) + if not lister: data += (action_ex.state_info,) + data += ( action_ex.accepted, action_ex.created_at, action_ex.updated_at or '' ) else: - data = (tuple('' for _ in range(len(columns))),) + data = (('',) * len(columns),) return columns, data @@ -224,25 +229,30 @@ class Update(command.ShowOne): parser.add_argument( 'id', - help='Action execution ID.') + help='Action execution ID.' + ) parser.add_argument( '--state', dest='state', choices=['PAUSED', 'RUNNING', 'SUCCESS', 'ERROR', 'CANCELLED'], - help='Action execution state') + help='Action execution state' + ) parser.add_argument( '--output', dest='output', - help='Action execution output') + help='Action execution output' + ) return parser def take_action(self, parsed_args): output = None + if parsed_args.output: output = utils.load_json(parsed_args.output) mistral_client = self.app.client_manager.workflow_engine + execution = mistral_client.action_executions.update( parsed_args.id, parsed_args.state, @@ -257,14 +267,17 @@ class GetOutput(command.Command): def get_parser(self, prog_name): parser = super(GetOutput, self).get_parser(prog_name) + parser.add_argument( 'id', - help='Action execution ID.') + help='Action execution ID.' + ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + output = mistral_client.action_executions.get(parsed_args.id).output try: @@ -281,6 +294,7 @@ class GetInput(command.Command): def get_parser(self, prog_name): parser = super(GetInput, self).get_parser(prog_name) + parser.add_argument( 'id', help='Action execution ID.' @@ -290,6 +304,7 @@ class GetInput(command.Command): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + result = mistral_client.action_executions.get(parsed_args.id).input try: diff --git a/mistralclient/commands/v2/actions.py b/mistralclient/commands/v2/actions.py index 188b6f15..f47ab020 100644 --- a/mistralclient/commands/v2/actions.py +++ b/mistralclient/commands/v2/actions.py @@ -113,9 +113,11 @@ class Get(command.ShowOne): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + action = mistral_client.actions.get( parsed_args.action, - parsed_args.namespace) + parsed_args.namespace + ) return ActionFormatter.format(action) diff --git a/mistralclient/commands/v2/base.py b/mistralclient/commands/v2/base.py index 0357934e..2200af4c 100644 --- a/mistralclient/commands/v2/base.py +++ b/mistralclient/commands/v2/base.py @@ -111,6 +111,7 @@ class MistralLister(command.Lister, metaclass=abc.ABCMeta): f = self._get_format_function() ret = self._get_resources(parsed_args) + if not isinstance(ret, list): ret = [ret] diff --git a/mistralclient/commands/v2/code_sources.py b/mistralclient/commands/v2/code_sources.py new file mode 100644 index 00000000..a618c140 --- /dev/null +++ b/mistralclient/commands/v2/code_sources.py @@ -0,0 +1,251 @@ +# Copyright 2020 Nokia Software. +# +# 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. + +import argparse + +from cliff import show +from osc_lib.command import command + +from mistralclient.commands.v2 import base +from mistralclient import utils + + +class CodeSourceFormatter(base.MistralFormatter): + COLUMNS = [ + ('id', 'ID'), + ('name', 'Name'), + ('namespace', 'Namespace'), + ('project_id', 'Project ID'), + ('scope', 'Scope'), + ('created_at', 'Created at'), + ('updated_at', 'Updated at') + ] + + @staticmethod + def format(code_src=None, lister=False): + if code_src: + data = ( + code_src.id, + code_src.name, + code_src.namespace, + code_src.project_id, + code_src.scope, + code_src.created_at + ) + + if hasattr(code_src, 'updated_at'): + data += (code_src.updated_at,) + else: + data += (None,) + else: + data = (('',) * len(CodeSourceFormatter.COLUMNS),) + + return CodeSourceFormatter.headings(), data + + +class List(base.MistralLister): + """List all workflows.""" + + def _get_format_function(self): + return CodeSourceFormatter.format_list + + def get_parser(self, prog_name): + parser = super(List, self).get_parser(prog_name) + + return parser + + def _get_resources(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + return mistral_client.code_sources.list( + marker=parsed_args.marker, + limit=parsed_args.limit, + sort_keys=parsed_args.sort_keys, + sort_dirs=parsed_args.sort_dirs, + fields=CodeSourceFormatter.fields(), + **base.get_filters(parsed_args) + ) + + +class Get(show.ShowOne): + """Show specific code source.""" + + def get_parser(self, prog_name): + parser = super(Get, self).get_parser(prog_name) + + parser.add_argument('identifier', help='Code source ID or name.') + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to get the code source from.", + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + wf = mistral_client.code_sources.get( + parsed_args.identifier, + parsed_args.namespace + ) + + return CodeSourceFormatter.format(wf) + + +class Create(command.ShowOne): + """Create new code source.""" + + def get_parser(self, prog_name): + parser = super(Create, self).get_parser(prog_name) + + parser.add_argument('name', help='Code source name.') + parser.add_argument( + 'content', + type=argparse.FileType('r'), + help='Code source content file.' + ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to create the code source within.", + ) + parser.add_argument( + '--public', + action='store_true', + help='With this flag the code source will be marked as "public".' + ) + + return parser + + def take_action(self, parsed_args): + scope = 'public' if parsed_args.public else 'private' + + mistral_client = self.app.client_manager.workflow_engine + + code_source = mistral_client.code_sources.create( + parsed_args.name, + parsed_args.content.read(), + namespace=parsed_args.namespace, + scope=scope + ) + + return CodeSourceFormatter.format(code_source) + + +class Delete(command.Command): + """Delete workflow.""" + + def get_parser(self, prog_name): + parser = super(Delete, self).get_parser(prog_name) + + parser.add_argument( + 'identifier', + nargs='+', + help='Code source name or ID (can be repeated multiple times).' + ) + + parser.add_argument( + '--namespace', + nargs='?', + default=None, + help="Namespace to delete the code source(s) from.", + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + utils.do_action_on_many( + lambda s: + mistral_client.code_sources.delete(s, parsed_args.namespace), + parsed_args.identifier, + "Request to delete code source '%s' has been accepted.", + "Unable to delete the specified code source(s)." + ) + + +class Update(command.ShowOne): + """Update workflow.""" + + def get_parser(self, prog_name): + parser = super(Update, self).get_parser(prog_name) + + parser.add_argument( + 'identifier', + help='Code source identifier (name or ID).' + ) + parser.add_argument( + 'content', + type=argparse.FileType('r'), + help='Code source content' + ) + parser.add_argument('--id', help='Workflow ID.') + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the workflow.", + ) + parser.add_argument( + '--public', + action='store_true', + help='With this flag workflow will be marked as "public".' + ) + + return parser + + def take_action(self, parsed_args): + scope = 'public' if parsed_args.public else 'private' + + mistral_client = self.app.client_manager.workflow_engine + + code_src = mistral_client.code_sources.update( + parsed_args.identifier, + parsed_args.content.read(), + namespace=parsed_args.namespace, + scope=scope + ) + + return CodeSourceFormatter.format(code_src) + + +class GetContent(command.Command): + """Show workflow definition.""" + + def get_parser(self, prog_name): + parser = super(GetContent, self).get_parser(prog_name) + + parser.add_argument('identifier', help='Code source ID or name.') + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to get the code source from.", + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + code_src = mistral_client.code_sources.get( + parsed_args.identifier, + parsed_args.namespace + ) + + self.app.stdout.write(code_src.content or "\n") diff --git a/mistralclient/commands/v2/dynamic_actions.py b/mistralclient/commands/v2/dynamic_actions.py new file mode 100644 index 00000000..5a73be3b --- /dev/null +++ b/mistralclient/commands/v2/dynamic_actions.py @@ -0,0 +1,244 @@ +# Copyright 2020 Nokia Software. +# +# 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. +# + +from osc_lib.command import command + +from mistralclient.commands.v2 import base +from mistralclient import utils + + +class DynamicActionFormatter(base.MistralFormatter): + COLUMNS = [ + ('id', 'ID'), + ('name', 'Name'), + ('class_name', 'Class'), + ('code_source_id', 'Code source ID'), + ('code_source_name', 'Code source name'), + ('project_id', 'Project ID'), + ('scope', 'Scope'), + ('created_at', 'Created at'), + ('updated_at', 'Updated at'), + ] + + @staticmethod + def format(action=None, lister=False): + if action: + data = ( + action.id, + action.name, + action.class_name, + action.code_source_id, + action.code_source_name, + action.project_id, + action.scope, + action.created_at + ) + + if hasattr(action, 'updated_at'): + data += (action.updated_at,) + else: + data += (None,) + + else: + data = (('',)*len(DynamicActionFormatter.COLUMNS),) + + return DynamicActionFormatter.headings(), data + + +class List(base.MistralLister): + """List all dynamic actions.""" + + def _get_format_function(self): + return DynamicActionFormatter.format_list + + def get_parser(self, prog_name): + parser = super(List, self).get_parser(prog_name) + + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of dynamic actions.", + ) + + return parser + + def _get_resources(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + return mistral_client.dynamic_actions.list( + marker=parsed_args.marker, + limit=parsed_args.limit, + sort_keys=parsed_args.sort_keys, + sort_dirs=parsed_args.sort_dirs, + fields=DynamicActionFormatter.fields(), + namespace=parsed_args.namespace, + **base.get_filters(parsed_args) + ) + + +class Get(command.ShowOne): + """Show specific dynamic action.""" + + def get_parser(self, prog_name): + parser = super(Get, self).get_parser(prog_name) + + parser.add_argument( + 'identifier', + help='Dynamic action identifier (name or ID)' + ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to create the dynamic action within.", + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + action = mistral_client.dynamic_actions.get( + parsed_args.identifier, + parsed_args.namespace + ) + + return DynamicActionFormatter.format(action) + + +class Create(command.ShowOne): + """Create new action.""" + + def get_parser(self, prog_name): + parser = super(Create, self).get_parser(prog_name) + + parser.add_argument('name', help='Dynamic action name') + parser.add_argument('class_name', help='Dynamic action class name') + parser.add_argument('code_source', help='Code source ID or name') + parser.add_argument( + '--public', + action='store_true', + help='With this flag an action will be marked as "public".' + ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to create the action within.", + ) + + return parser + + def take_action(self, parsed_args): + scope = 'public' if parsed_args.public else 'private' + + mistral_client = self.app.client_manager.workflow_engine + + dyn_action = mistral_client.dynamic_actions.create( + parsed_args.name, + parsed_args.class_name, + parsed_args.code_source, + namespace=parsed_args.namespace, + scope=scope + ) + + return DynamicActionFormatter.format(dyn_action) + + +class Delete(command.Command): + """Delete action.""" + + def get_parser(self, prog_name): + parser = super(Delete, self).get_parser(prog_name) + + parser.add_argument( + 'identifier', + nargs='+', + help="Dynamic action name or ID (can be repeated multiple times)." + ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the dynamic action(s).", + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + utils.do_action_on_many( + lambda s: mistral_client.dynamic_actions.delete( + s, + namespace=parsed_args.namespace + ), + parsed_args.identifier, + "Request to delete dynamic action(s) %s has been accepted.", + "Unable to delete the specified dynamic action(s)." + ) + + +class Update(command.ShowOne): + """Update dynamic action.""" + + def get_parser(self, prog_name): + parser = super(Update, self).get_parser(prog_name) + + parser.add_argument( + 'identifier', + help='Dynamic action identifier (ID or name)' + ) + parser.add_argument( + '--class-name', + dest='class_name', + nargs='?', + help='Dynamic action class name.' + ) + parser.add_argument( + '--code-source', + dest='code_source', + nargs='?', + help='Code source identifier (ID or name).' + ) + parser.add_argument( + '--public', + action='store_true', + help='With this flag action will be marked as "public".' + ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the action.", + ) + + return parser + + def take_action(self, parsed_args): + scope = 'public' if parsed_args.public else 'private' + + mistral_client = self.app.client_manager.workflow_engine + + dyn_action = mistral_client.dynamic_actions.update( + parsed_args.identifier, + class_name=parsed_args.class_name, + code_source=parsed_args.code_source, + scope=scope, + namespace=parsed_args.namespace + ) + + return DynamicActionFormatter.format(dyn_action) diff --git a/mistralclient/commands/v2/environments.py b/mistralclient/commands/v2/environments.py index 9c1bbeb9..cdcb4e9b 100644 --- a/mistralclient/commands/v2/environments.py +++ b/mistralclient/commands/v2/environments.py @@ -87,10 +87,7 @@ class Get(command.ShowOne): def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) - parser.add_argument( - 'environment', - help='Environment name' - ) + parser.add_argument('environment', help='Environment name') parser.add_argument( '--export', diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py index c666112b..73b650f2 100644 --- a/mistralclient/commands/v2/executions.py +++ b/mistralclient/commands/v2/executions.py @@ -89,6 +89,7 @@ class List(base.MistralExecutionLister): def get_parser(self, parsed_args): parser = super(List, self).get_parser(parsed_args) + parser.add_argument( '--task', nargs='?', @@ -146,8 +147,7 @@ class Create(command.ShowOne): parser.add_argument( 'workflow_identifier', nargs='?', - help='Workflow ID or name. Workflow name will be deprecated since ' - 'Mitaka.' + help='Workflow ID or name' ) parser.add_argument( '--namespace', @@ -249,10 +249,7 @@ class Update(command.ShowOne): def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) - parser.add_argument( - 'id', - help='Execution identifier' - ) + parser.add_argument('id', help='Execution identifier') parser.add_argument( '-s', diff --git a/mistralclient/commands/v2/workbooks.py b/mistralclient/commands/v2/workbooks.py index faa5e4d1..fce64bbd 100644 --- a/mistralclient/commands/v2/workbooks.py +++ b/mistralclient/commands/v2/workbooks.py @@ -61,6 +61,7 @@ class List(base.MistralLister): def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + return mistral_client.workbooks.list( marker=parsed_args.marker, limit=parsed_args.limit, @@ -90,6 +91,7 @@ class Get(command.ShowOne): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + workbook = mistral_client.workbooks.get( parsed_args.workbook, parsed_args.namespace @@ -128,6 +130,7 @@ class Create(command.ShowOne): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine + workbook = mistral_client.workbooks.create( parsed_args.definition.read(), namespace=parsed_args.namespace, @@ -155,9 +158,10 @@ class Delete(command.Command): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + utils.do_action_on_many( - lambda s: mistral_client.workbooks.delete(s, - parsed_args.namespace), + lambda s: + mistral_client.workbooks.delete(s, parsed_args.namespace), parsed_args.workbook, "Request to delete workbook %s has been accepted.", "Unable to delete the specified workbook(s)." @@ -193,6 +197,7 @@ class Update(command.ShowOne): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine + workbook = mistral_client.workbooks.update( parsed_args.definition.read(), namespace=parsed_args.namespace, @@ -245,6 +250,7 @@ class Validate(command.ShowOne): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + result = mistral_client.workbooks.validate( parsed_args.definition.read() ) diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 0847ba73..9f78fe55 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -29,7 +29,9 @@ 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.code_sources import mistralclient.commands.v2.cron_triggers +import mistralclient.commands.v2.dynamic_actions import mistralclient.commands.v2.environments import mistralclient.commands.v2.event_triggers import mistralclient.commands.v2.executions @@ -581,12 +583,15 @@ class MistralShell(app.App): 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" @@ -603,6 +608,7 @@ class MistralShell(app.App): ("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 @@ -767,6 +773,26 @@ class MistralShell(app.App): 'member-update': mistralclient.commands.v2.members.Update, 'member-list': mistralclient.commands.v2.members.List, 'member-get': mistralclient.commands.v2.members.Get, + 'code-source-create': + mistralclient.commands.v2.code_sources.Create, + 'code-source-get': mistralclient.commands.v2.code_sources.Get, + 'code-source-update': + mistralclient.commands.v2.code_sources.Update, + 'code-source-list': mistralclient.commands.v2.code_sources.List, + 'code-source-delete': + mistralclient.commands.v2.code_sources.Delete, + 'code-source-get-content': + mistralclient.commands.v2.code_sources.GetContent, + 'dynamic-action-create': + mistralclient.commands.v2.dynamic_actions.Create, + 'dynamic-action-get': + mistralclient.commands.v2.dynamic_actions.Get, + 'dynamic-action-update': + mistralclient.commands.v2.dynamic_actions.Update, + 'dynamic-action-list': + mistralclient.commands.v2.dynamic_actions.List, + 'dynamic-action-delete': + mistralclient.commands.v2.dynamic_actions.Delete, } diff --git a/mistralclient/tests/unit/v2/test_workbooks.py b/mistralclient/tests/unit/v2/test_workbooks.py index 6c06f88c..4cbb5b6d 100644 --- a/mistralclient/tests/unit/v2/test_workbooks.py +++ b/mistralclient/tests/unit/v2/test_workbooks.py @@ -71,9 +71,11 @@ URL_TEMPLATE_VALIDATE = '/workbooks/validate' class TestWorkbooksV2(base.BaseClientV2Test): def test_create(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, - json=WORKBOOK, - status_code=201) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE, + json=WORKBOOK, + status_code=201 + ) wb = self.workbooks.create(WB_DEF) @@ -86,9 +88,11 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_create_with_file_uri(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, - json=WORKBOOK, - status_code=201) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE, + json=WORKBOOK, + status_code=201 + ) # The contents of wb_v2.yaml must be identical to WB_DEF path = pkg.resource_filename( @@ -143,8 +147,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_list(self): - self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, - json={'workbooks': [WORKBOOK]}) + self.requests_mock.get( + self.TEST_URL + URL_TEMPLATE, + json={'workbooks': [WORKBOOK]} + ) workbook_list = self.workbooks.list() @@ -158,8 +164,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): ) def test_get(self): - url = self.TEST_URL + URL_TEMPLATE_NAME % 'wb' - self.requests_mock.get(url, json=WORKBOOK) + self.requests_mock.get( + self.TEST_URL + URL_TEMPLATE_NAME % 'wb', + json=WORKBOOK + ) wb = self.workbooks.get('wb') @@ -176,8 +184,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.workbooks.delete('wb') def test_validate(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, - json={'valid': True}) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_VALIDATE, + json={'valid': True} + ) result = self.workbooks.validate(WB_DEF) @@ -191,8 +201,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_with_file(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, - json={'valid': True}) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_VALIDATE, + json={'valid': True} + ) # The contents of wb_v2.yaml must be identical to WB_DEF path = pkg.resource_filename( @@ -218,8 +230,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): "can't be specified both" } - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, - json=mock_result) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_VALIDATE, + json=mock_result + ) result = self.workbooks.validate(INVALID_WB_DEF) @@ -238,8 +252,10 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_api_failed(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, - status_code=500) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_VALIDATE, + status_code=500 + ) self.assertRaises( api_base.APIException, diff --git a/mistralclient/tests/unit/v2/test_workflows.py b/mistralclient/tests/unit/v2/test_workflows.py index 39064b5d..4b28fdcb 100644 --- a/mistralclient/tests/unit/v2/test_workflows.py +++ b/mistralclient/tests/unit/v2/test_workflows.py @@ -47,9 +47,11 @@ URL_TEMPLATE_NAME = '/workflows/%s' class TestWorkflowsV2(base.BaseClientV2Test): def test_create(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_SCOPE, - json={'workflows': [WORKFLOW]}, - status_code=201) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_SCOPE, + json={'workflows': [WORKFLOW]}, + status_code=201 + ) wfs = self.workflows.create(WF_DEF) @@ -62,9 +64,11 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_create_with_file(self): - self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_SCOPE, - json={'workflows': [WORKFLOW]}, - status_code=201) + self.requests_mock.post( + self.TEST_URL + URL_TEMPLATE_SCOPE, + json={'workflows': [WORKFLOW]}, + status_code=201 + ) # The contents of wf_v2.yaml must be identical to WF_DEF path = pkg.resource_filename( @@ -83,8 +87,10 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_update(self): - self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_SCOPE, - json={'workflows': [WORKFLOW]}) + self.requests_mock.put( + self.TEST_URL + URL_TEMPLATE_SCOPE, + json={'workflows': [WORKFLOW]} + ) wfs = self.workflows.update(WF_DEF) @@ -97,8 +103,10 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_update_with_id(self): - self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_NAME % '123', - json=WORKFLOW) + self.requests_mock.put( + self.TEST_URL + URL_TEMPLATE_NAME % '123', + json=WORKFLOW + ) wf = self.workflows.update(WF_DEF, id='123') @@ -112,8 +120,10 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_update_with_file_uri(self): - self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_SCOPE, - json={'workflows': [WORKFLOW]}) + self.requests_mock.put( + self.TEST_URL + URL_TEMPLATE_SCOPE, + json={'workflows': [WORKFLOW]} + ) # The contents of wf_v2.yaml must be identical to WF_DEF path = pkg.resource_filename( @@ -136,8 +146,10 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) def test_list(self): - self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, - json={'workflows': [WORKFLOW]}) + self.requests_mock.get( + self.TEST_URL + URL_TEMPLATE, + json={'workflows': [WORKFLOW]} + ) workflows_list = self.workflows.list() @@ -151,9 +163,13 @@ class TestWorkflowsV2(base.BaseClientV2Test): ) def test_list_with_pagination(self): - self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, - json={'workflows': [WORKFLOW], - 'next': '/workflows?fake'}) + self.requests_mock.get( + self.TEST_URL + URL_TEMPLATE, + json={ + 'workflows': [WORKFLOW], + 'next': '/workflows?fake' + } + ) workflows_list = self.workflows.list( limit=1, @@ -171,8 +187,10 @@ class TestWorkflowsV2(base.BaseClientV2Test): self.assertEqual(['asc'], last_request.qs['sort_dirs']) def test_list_with_no_limit(self): - self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, - json={'workflows': [WORKFLOW]}) + self.requests_mock.get( + self.TEST_URL + URL_TEMPLATE, + json={'workflows': [WORKFLOW]} + ) workflows_list = self.workflows.list(limit=-1) @@ -184,6 +202,7 @@ class TestWorkflowsV2(base.BaseClientV2Test): def test_get(self): url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf' + self.requests_mock.get(url, json=WORKFLOW) wf = self.workflows.get('wf') @@ -195,7 +214,9 @@ class TestWorkflowsV2(base.BaseClientV2Test): ) def test_delete(self): - url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf' - self.requests_mock.delete(url, status_code=204) + self.requests_mock.delete( + self.TEST_URL + URL_TEMPLATE_NAME % 'wf', + status_code=204 + ) self.workflows.delete('wf') diff --git a/mistralclient/utils.py b/mistralclient/utils.py index 737868e8..b8aff3a5 100644 --- a/mistralclient/utils.py +++ b/mistralclient/utils.py @@ -31,6 +31,7 @@ def do_action_on_many(action, resources, success_msg, error_msg): for resource in resources: try: action(resource) + print(success_msg % resource) except Exception as e: failure_flag = True @@ -73,10 +74,9 @@ def get_contents_if_file(contents_or_file_name): definition_url = contents_or_file_name else: path = os.path.abspath(contents_or_file_name) - definition_url = parse.urljoin( - 'file:', - request.pathname2url(path) - ) + + definition_url = parse.urljoin('file:', request.pathname2url(path)) + return request.urlopen(definition_url).read().decode('utf8') except Exception: return contents_or_file_name diff --git a/setup.cfg b/setup.cfg index 9d38d098..fa1942f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -104,6 +104,20 @@ openstack.workflow_engine.v2 = resource_member_delete = mistralclient.commands.v2.members:Delete resource_member_update = mistralclient.commands.v2.members:Update + code_source_list = mistralclient.commands.v2.code_sources:List + code_source_show = mistralclient.commands.v2.code_sources:Get + code_source_create = mistralclient.commands.v2.code_sources:Create + code_source_delete = mistralclient.commands.v2.code_sources:Delete + code_source_update = mistralclient.commands.v2.code_sources:Update + code_source_content_show = mistralclient.commands.v2.code_sources:GetContent + + dynamic_action_list = mistralclient.commands.v2.dynamic_actions:List + dynamic_action_show = mistralclient.commands.v2.dynamic_actions:Get + dynamic_action_create = mistralclient.commands.v2.dynamic_actions:Create + dynamic_action_delete = mistralclient.commands.v2.dynamic_actions:Delete + dynamic_action_update = mistralclient.commands.v2.dynamic_actions:Update + + mistralclient.auth = # Standard Keystone authentication. keystone = mistralclient.auth.keystone:KeystoneAuthHandler