Add support for code sources and dynamic actions

* Adjust low-constraints.txt to fix more rigorous dependency check
  introduced in pip 20.3.0
  (http://lists.openstack.org/pipermail/openstack-discuss/2020-December/019285.html)

Depends-On: Ib5a53f1f1a185f0395ffae1ab0c401633fcdd0fc

Change-Id: I28f5e2e201a0f1a2090ed6aff450f22a4fe846fe
This commit is contained in:
Renat Akhmerov 2020-11-18 14:15:36 +07:00
parent 668712eba2
commit fc6f779b26
21 changed files with 914 additions and 97 deletions

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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
)

View File

@ -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)

View File

@ -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)
)

View File

@ -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})

View File

@ -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)

View File

@ -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 '<none>'
)
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:

View File

@ -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)

View File

@ -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]

View File

@ -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")

View File

@ -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)

View File

@ -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',

View File

@ -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',

View File

@ -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()
)

View File

@ -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,
}

View File

@ -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,

View File

@ -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')

View File

@ -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

View File

@ -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