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 extras==1.0.0
fasteners==0.7.0 fasteners==0.7.0
fixtures==3.0.0 fixtures==3.0.0
flake8==2.5.5
future==0.16.0 future==0.16.0
futurist==1.2.0 futurist==1.2.0
greenlet==0.4.15 greenlet==0.4.15
hacking==0.12.0
idna==2.6 idna==2.6
imagesize==0.7.1 imagesize==0.7.1
iso8601==0.1.11 iso8601==0.1.11
@ -37,7 +35,6 @@ keystoneauth1==3.4.0
kombu==4.0.0 kombu==4.0.0
linecache2==1.0.0 linecache2==1.0.0
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mccabe==0.2.1
monotonic==0.6 monotonic==0.6
mox3==0.20.0 mox3==0.20.0
msgpack-python==0.4.0 msgpack-python==0.4.0
@ -72,7 +69,6 @@ positional==1.2.1
prettytable==0.7.2 prettytable==0.7.2
pyasn1==0.1.8 pyasn1==0.1.8
pycparser==2.18 pycparser==2.18
pyflakes==0.8.1
Pygments==2.2.0 Pygments==2.2.0
pyinotify==0.9.6 pyinotify==0.9.6
pyOpenSSL==17.1.0 pyOpenSSL==17.1.0

View File

@ -49,9 +49,11 @@ class Resource(object):
return copy.deepcopy(self._data) return copy.deepcopy(self._data)
def __str__(self): def __str__(self):
vals = ", ".join(["%s='%s'" % (n, v) values = ", ".join(
for n, v in self._data.items()]) ["%s='%s'" % (n, v) for n, v in self._data.items()]
return "%s [%s]" % (self.resource_name, vals) )
return "%s [%s]" % (self.resource_name, values)
def _check_items(obj, searches): def _check_items(obj, searches):
@ -130,9 +132,16 @@ class ResourceManager(object):
def _validate(self, url, data, response_key=None, dump_json=True, def _validate(self, url, data, response_key=None, dump_json=True,
headers=None, is_iter_resp=False): headers=None, is_iter_resp=False):
return self._create(url, data, response_key, dump_json, return self._create(
headers, is_iter_resp, resp_status_ok=200, url,
as_class=False) 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, def _create(self, url, data, response_key=None, dump_json=True,
headers=None, is_iter_resp=False, resp_status_ok=201, headers=None, is_iter_resp=False, resp_status_ok=201,
@ -149,9 +158,13 @@ class ResourceManager(object):
self._raise_api_exception(resp) self._raise_api_exception(resp)
resource = extract_json(resp, response_key) resource = extract_json(resp, response_key)
if is_iter_resp: if is_iter_resp:
return [self.resource_class(self, resource_data) return [
for resource_data in resource] self.resource_class(self, resource_data)
for resource_data in resource
]
return self.resource_class(self, resource) if as_class else resource return self.resource_class(self, resource) if as_class else resource
def _update(self, url, data, response_key=None, dump_json=True, def _update(self, url, data, response_key=None, dump_json=True,
@ -168,9 +181,13 @@ class ResourceManager(object):
self._raise_api_exception(resp) self._raise_api_exception(resp)
resource = extract_json(resp, response_key) resource = extract_json(resp, response_key)
if is_iter_resp: if is_iter_resp:
return [self.resource_class(self, resource_data) return [
for resource_data in resource] self.resource_class(self, resource_data)
for resource_data in resource
]
return self.resource_class(self, resource) return self.resource_class(self, resource)
def _list(self, url, response_key=None, headers=None, 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 resource_class = returned_res_cls or self.resource_class
return [resource_class(self, resource_data) return [
for resource_data in extract_json(resp, response_key)] resource_class(self, resource_data)
for resource_data in extract_json(resp, response_key)
]
def _get(self, url, response_key=None, headers=None): def _get(self, url, response_key=None, headers=None):
try: try:
@ -212,12 +231,17 @@ class ResourceManager(object):
@staticmethod @staticmethod
def _raise_api_exception(resp): def _raise_api_exception(resp):
try: try:
error_data = (resp.headers.get("Server-Error-Message", None) or error_data = (
get_json(resp).get("faultstring")) resp.headers.get("Server-Error-Message", None)
or get_json(resp).get("faultstring")
)
except ValueError: except ValueError:
error_data = resp.content 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): def get_json(response):

View File

@ -31,6 +31,7 @@ class ActionManager(base.ResourceManager):
# definition file # definition file
definition = utils.get_contents_if_file(definition) definition = utils.get_contents_if_file(definition)
url = '/actions?scope=%s' % scope url = '/actions?scope=%s' % scope
if namespace: if namespace:
url += '&namespace=%s' % namespace url += '&namespace=%s' % namespace
@ -45,14 +46,18 @@ class ActionManager(base.ResourceManager):
def update(self, definition, scope='private', id=None, namespace=''): def update(self, definition, scope='private', id=None, namespace=''):
self._ensure_not_empty(definition=definition) self._ensure_not_empty(definition=definition)
params = '?scope=%s' % scope params = '?scope=%s' % scope
if namespace: if namespace:
params += '&namespace=%s' % namespace params += '&namespace=%s' % namespace
url = ('/actions/%s' % id if id else '/actions') + params url = ('/actions/%s' % id if id else '/actions') + params
# If the specified definition is actually a file, read in the # If the specified definition is actually a file, read in the
# definition file # definition file
definition = utils.get_contents_if_file(definition) definition = utils.get_contents_if_file(definition)
return self._update( return self._update(
url, url,
definition, definition,

View File

@ -20,7 +20,9 @@ from oslo_utils import importutils
from mistralclient.api import httpclient from mistralclient.api import httpclient
from mistralclient.api.v2 import action_executions from mistralclient.api.v2 import action_executions
from mistralclient.api.v2 import actions 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 cron_triggers
from mistralclient.api.v2 import dynamic_actions
from mistralclient.api.v2 import environments from mistralclient.api.v2 import environments
from mistralclient.api.v2 import event_triggers from mistralclient.api.v2 import event_triggers
from mistralclient.api.v2 import executions from mistralclient.api.v2 import executions
@ -86,6 +88,11 @@ class Client(object):
self.event_triggers = event_triggers.EventTriggerManager(http_client) self.event_triggers = event_triggers.EventTriggerManager(http_client)
self.environments = environments.EnvironmentManager(http_client) self.environments = environments.EnvironmentManager(http_client)
self.action_executions = action_executions.ActionExecutionManager( self.action_executions = action_executions.ActionExecutionManager(
http_client) http_client
)
self.services = services.ServiceManager(http_client) self.services = services.ServiceManager(http_client)
self.members = members.MemberManager(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): class ExecutionManager(base.ResourceManager):
resource_class = Execution resource_class = Execution
def create(self, workflow_identifier='', namespace='', def create(self, wf_identifier='', namespace='',
workflow_input=None, description='', source_execution_id=None, workflow_input=None, description='', source_execution_id=None,
**params): **params):
self._ensure_not_empty( self._ensure_not_empty(
workflow_identifier=workflow_identifier or source_execution_id workflow_identifier=wf_identifier or source_execution_id
) )
data = {'description': description} data = {'description': description}
@ -40,11 +40,11 @@ class ExecutionManager(base.ResourceManager):
if uuidutils.is_uuid_like(source_execution_id): if uuidutils.is_uuid_like(source_execution_id):
data.update({'source_execution_id': source_execution_id}) data.update({'source_execution_id': source_execution_id})
if workflow_identifier: if wf_identifier:
if uuidutils.is_uuid_like(workflow_identifier): if uuidutils.is_uuid_like(wf_identifier):
data.update({'workflow_id': workflow_identifier}) data.update({'workflow_id': wf_identifier})
else: else:
data.update({'workflow_name': workflow_identifier}) data.update({'workflow_name': wf_identifier})
if namespace: if namespace:
data.update({'workflow_namespace': namespace}) data.update({'workflow_namespace': namespace})

View File

@ -25,19 +25,19 @@ class WorkbookManager(base.ResourceManager):
resource_class = Workbook resource_class = Workbook
def _get_workbooks_url(self, resource=None, namespace=None, scope=None): def _get_workbooks_url(self, resource=None, namespace=None, scope=None):
path = '/workbooks' url = '/workbooks'
if resource: if resource:
path += '/%s' % resource url += '/%s' % resource
if scope and namespace: if scope and namespace:
path += '?scope=%s&namespace=%s' % (scope, namespace) url += '?scope=%s&namespace=%s' % (scope, namespace)
elif scope: elif scope:
path += '?scope=%s' % scope url += '?scope=%s' % scope
elif namespace: elif namespace:
path += '?namespace=%s' % namespace url += '?namespace=%s' % namespace
return path return url
def create(self, definition, namespace='', scope='private'): def create(self, definition, namespace='', scope='private'):
self._ensure_not_empty(definition=definition) self._ensure_not_empty(definition=definition)
@ -78,8 +78,10 @@ class WorkbookManager(base.ResourceManager):
namespace=namespace namespace=namespace
) )
return self._list('/workbooks{}'.format(query_string), return self._list(
response_key='workbooks') '/workbooks{}'.format(query_string),
response_key='workbooks'
)
def get(self, name, namespace=''): def get(self, name, namespace=''):
self._ensure_not_empty(name=name) self._ensure_not_empty(name=name)

View File

@ -51,11 +51,13 @@ class ActionExecutionFormatter(base.MistralFormatter):
columns = ActionExecutionFormatter.LIST_COLUMN_HEADING_NAMES columns = ActionExecutionFormatter.LIST_COLUMN_HEADING_NAMES
else: else:
columns = ActionExecutionFormatter.headings() columns = ActionExecutionFormatter.headings()
if action_ex: if action_ex:
if hasattr(action_ex, 'task_name'): if hasattr(action_ex, 'task_name'):
task_name = action_ex.task_name task_name = action_ex.task_name
else: else:
task_name = None task_name = None
data = ( data = (
action_ex.id, action_ex.id,
action_ex.name, action_ex.name,
@ -63,16 +65,19 @@ class ActionExecutionFormatter(base.MistralFormatter):
action_ex.workflow_namespace, action_ex.workflow_namespace,
task_name, task_name,
action_ex.task_execution_id, action_ex.task_execution_id,
action_ex.state,) action_ex.state,
)
if not lister: if not lister:
data += (action_ex.state_info,) data += (action_ex.state_info,)
data += ( data += (
action_ex.accepted, action_ex.accepted,
action_ex.created_at, action_ex.created_at,
action_ex.updated_at or '<none>' action_ex.updated_at or '<none>'
) )
else: else:
data = (tuple('' for _ in range(len(columns))),) data = (('',) * len(columns),)
return columns, data return columns, data
@ -224,25 +229,30 @@ class Update(command.ShowOne):
parser.add_argument( parser.add_argument(
'id', 'id',
help='Action execution ID.') help='Action execution ID.'
)
parser.add_argument( parser.add_argument(
'--state', '--state',
dest='state', dest='state',
choices=['PAUSED', 'RUNNING', 'SUCCESS', 'ERROR', 'CANCELLED'], choices=['PAUSED', 'RUNNING', 'SUCCESS', 'ERROR', 'CANCELLED'],
help='Action execution state') help='Action execution state'
)
parser.add_argument( parser.add_argument(
'--output', '--output',
dest='output', dest='output',
help='Action execution output') help='Action execution output'
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
output = None output = None
if parsed_args.output: if parsed_args.output:
output = utils.load_json(parsed_args.output) output = utils.load_json(parsed_args.output)
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
execution = mistral_client.action_executions.update( execution = mistral_client.action_executions.update(
parsed_args.id, parsed_args.id,
parsed_args.state, parsed_args.state,
@ -257,14 +267,17 @@ class GetOutput(command.Command):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(GetOutput, self).get_parser(prog_name) parser = super(GetOutput, self).get_parser(prog_name)
parser.add_argument( parser.add_argument(
'id', 'id',
help='Action execution ID.') help='Action execution ID.'
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
output = mistral_client.action_executions.get(parsed_args.id).output output = mistral_client.action_executions.get(parsed_args.id).output
try: try:
@ -281,6 +294,7 @@ class GetInput(command.Command):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(GetInput, self).get_parser(prog_name) parser = super(GetInput, self).get_parser(prog_name)
parser.add_argument( parser.add_argument(
'id', 'id',
help='Action execution ID.' help='Action execution ID.'
@ -290,6 +304,7 @@ class GetInput(command.Command):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
result = mistral_client.action_executions.get(parsed_args.id).input result = mistral_client.action_executions.get(parsed_args.id).input
try: try:

View File

@ -113,9 +113,11 @@ class Get(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
action = mistral_client.actions.get( action = mistral_client.actions.get(
parsed_args.action, parsed_args.action,
parsed_args.namespace) parsed_args.namespace
)
return ActionFormatter.format(action) return ActionFormatter.format(action)

View File

@ -111,6 +111,7 @@ class MistralLister(command.Lister, metaclass=abc.ABCMeta):
f = self._get_format_function() f = self._get_format_function()
ret = self._get_resources(parsed_args) ret = self._get_resources(parsed_args)
if not isinstance(ret, list): if not isinstance(ret, list):
ret = [ret] 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): def get_parser(self, prog_name):
parser = super(Get, self).get_parser(prog_name) parser = super(Get, self).get_parser(prog_name)
parser.add_argument( parser.add_argument('environment', help='Environment name')
'environment',
help='Environment name'
)
parser.add_argument( parser.add_argument(
'--export', '--export',

View File

@ -89,6 +89,7 @@ class List(base.MistralExecutionLister):
def get_parser(self, parsed_args): def get_parser(self, parsed_args):
parser = super(List, self).get_parser(parsed_args) parser = super(List, self).get_parser(parsed_args)
parser.add_argument( parser.add_argument(
'--task', '--task',
nargs='?', nargs='?',
@ -146,8 +147,7 @@ class Create(command.ShowOne):
parser.add_argument( parser.add_argument(
'workflow_identifier', 'workflow_identifier',
nargs='?', nargs='?',
help='Workflow ID or name. Workflow name will be deprecated since ' help='Workflow ID or name'
'Mitaka.'
) )
parser.add_argument( parser.add_argument(
'--namespace', '--namespace',
@ -249,10 +249,7 @@ class Update(command.ShowOne):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(Update, self).get_parser(prog_name) parser = super(Update, self).get_parser(prog_name)
parser.add_argument( parser.add_argument('id', help='Execution identifier')
'id',
help='Execution identifier'
)
parser.add_argument( parser.add_argument(
'-s', '-s',

View File

@ -61,6 +61,7 @@ class List(base.MistralLister):
def _get_resources(self, parsed_args): def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
return mistral_client.workbooks.list( return mistral_client.workbooks.list(
marker=parsed_args.marker, marker=parsed_args.marker,
limit=parsed_args.limit, limit=parsed_args.limit,
@ -90,6 +91,7 @@ class Get(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
workbook = mistral_client.workbooks.get( workbook = mistral_client.workbooks.get(
parsed_args.workbook, parsed_args.workbook,
parsed_args.namespace parsed_args.namespace
@ -128,6 +130,7 @@ class Create(command.ShowOne):
scope = 'public' if parsed_args.public else 'private' scope = 'public' if parsed_args.public else 'private'
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
workbook = mistral_client.workbooks.create( workbook = mistral_client.workbooks.create(
parsed_args.definition.read(), parsed_args.definition.read(),
namespace=parsed_args.namespace, namespace=parsed_args.namespace,
@ -155,9 +158,10 @@ class Delete(command.Command):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
utils.do_action_on_many( utils.do_action_on_many(
lambda s: mistral_client.workbooks.delete(s, lambda s:
parsed_args.namespace), mistral_client.workbooks.delete(s, parsed_args.namespace),
parsed_args.workbook, parsed_args.workbook,
"Request to delete workbook %s has been accepted.", "Request to delete workbook %s has been accepted.",
"Unable to delete the specified workbook(s)." "Unable to delete the specified workbook(s)."
@ -193,6 +197,7 @@ class Update(command.ShowOne):
scope = 'public' if parsed_args.public else 'private' scope = 'public' if parsed_args.public else 'private'
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
workbook = mistral_client.workbooks.update( workbook = mistral_client.workbooks.update(
parsed_args.definition.read(), parsed_args.definition.read(),
namespace=parsed_args.namespace, namespace=parsed_args.namespace,
@ -245,6 +250,7 @@ class Validate(command.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
result = mistral_client.workbooks.validate( result = mistral_client.workbooks.validate(
parsed_args.definition.read() parsed_args.definition.read()
) )

View File

@ -29,7 +29,9 @@ from mistralclient.api import client
from mistralclient.auth import auth_types from mistralclient.auth import auth_types
import mistralclient.commands.v2.action_executions import mistralclient.commands.v2.action_executions
import mistralclient.commands.v2.actions import mistralclient.commands.v2.actions
import mistralclient.commands.v2.code_sources
import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.cron_triggers
import mistralclient.commands.v2.dynamic_actions
import mistralclient.commands.v2.environments import mistralclient.commands.v2.environments
import mistralclient.commands.v2.event_triggers import mistralclient.commands.v2.event_triggers
import mistralclient.commands.v2.executions import mistralclient.commands.v2.executions
@ -581,12 +583,15 @@ class MistralShell(app.App):
if (not self.options.project_domain_id and if (not self.options.project_domain_id and
not self.options.project_domain_name): not self.options.project_domain_name):
self.options.project_domain_id = "default" self.options.project_domain_id = "default"
if (not self.options.user_domain_id and if (not self.options.user_domain_id and
not self.options.user_domain_name): not self.options.user_domain_name):
self.options.user_domain_id = "default" self.options.user_domain_id = "default"
if (not self.options.target_project_domain_id and if (not self.options.target_project_domain_id and
not self.options.target_project_domain_name): not self.options.target_project_domain_name):
self.options.target_project_domain_id = "default" self.options.target_project_domain_id = "default"
if (not self.options.target_user_domain_id and if (not self.options.target_user_domain_id and
not self.options.target_user_domain_name): not self.options.target_user_domain_name):
self.options.target_user_domain_id = "default" self.options.target_user_domain_id = "default"
@ -603,6 +608,7 @@ class MistralShell(app.App):
("You must provide a password " ("You must provide a password "
"via --os-password env[OS_PASSWORD]") "via --os-password env[OS_PASSWORD]")
) )
self.client = self._create_client() if need_client else None self.client = self._create_client() if need_client else None
# Adding client_manager variable to make mistral client work with # 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-update': mistralclient.commands.v2.members.Update,
'member-list': mistralclient.commands.v2.members.List, 'member-list': mistralclient.commands.v2.members.List,
'member-get': mistralclient.commands.v2.members.Get, '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): class TestWorkbooksV2(base.BaseClientV2Test):
def test_create(self): def test_create(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, self.requests_mock.post(
json=WORKBOOK, self.TEST_URL + URL_TEMPLATE,
status_code=201) json=WORKBOOK,
status_code=201
)
wb = self.workbooks.create(WB_DEF) wb = self.workbooks.create(WB_DEF)
@ -86,9 +88,11 @@ class TestWorkbooksV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_create_with_file_uri(self): def test_create_with_file_uri(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, self.requests_mock.post(
json=WORKBOOK, self.TEST_URL + URL_TEMPLATE,
status_code=201) json=WORKBOOK,
status_code=201
)
# The contents of wb_v2.yaml must be identical to WB_DEF # The contents of wb_v2.yaml must be identical to WB_DEF
path = pkg.resource_filename( path = pkg.resource_filename(
@ -143,8 +147,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_list(self): def test_list(self):
self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, self.requests_mock.get(
json={'workbooks': [WORKBOOK]}) self.TEST_URL + URL_TEMPLATE,
json={'workbooks': [WORKBOOK]}
)
workbook_list = self.workbooks.list() workbook_list = self.workbooks.list()
@ -158,8 +164,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
) )
def test_get(self): def test_get(self):
url = self.TEST_URL + URL_TEMPLATE_NAME % 'wb' self.requests_mock.get(
self.requests_mock.get(url, json=WORKBOOK) self.TEST_URL + URL_TEMPLATE_NAME % 'wb',
json=WORKBOOK
)
wb = self.workbooks.get('wb') wb = self.workbooks.get('wb')
@ -176,8 +184,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
self.workbooks.delete('wb') self.workbooks.delete('wb')
def test_validate(self): def test_validate(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, self.requests_mock.post(
json={'valid': True}) self.TEST_URL + URL_TEMPLATE_VALIDATE,
json={'valid': True}
)
result = self.workbooks.validate(WB_DEF) result = self.workbooks.validate(WB_DEF)
@ -191,8 +201,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_validate_with_file(self): def test_validate_with_file(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, self.requests_mock.post(
json={'valid': True}) self.TEST_URL + URL_TEMPLATE_VALIDATE,
json={'valid': True}
)
# The contents of wb_v2.yaml must be identical to WB_DEF # The contents of wb_v2.yaml must be identical to WB_DEF
path = pkg.resource_filename( path = pkg.resource_filename(
@ -218,8 +230,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
"can't be specified both" "can't be specified both"
} }
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, self.requests_mock.post(
json=mock_result) self.TEST_URL + URL_TEMPLATE_VALIDATE,
json=mock_result
)
result = self.workbooks.validate(INVALID_WB_DEF) result = self.workbooks.validate(INVALID_WB_DEF)
@ -238,8 +252,10 @@ class TestWorkbooksV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_validate_api_failed(self): def test_validate_api_failed(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, self.requests_mock.post(
status_code=500) self.TEST_URL + URL_TEMPLATE_VALIDATE,
status_code=500
)
self.assertRaises( self.assertRaises(
api_base.APIException, api_base.APIException,

View File

@ -47,9 +47,11 @@ URL_TEMPLATE_NAME = '/workflows/%s'
class TestWorkflowsV2(base.BaseClientV2Test): class TestWorkflowsV2(base.BaseClientV2Test):
def test_create(self): def test_create(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_SCOPE, self.requests_mock.post(
json={'workflows': [WORKFLOW]}, self.TEST_URL + URL_TEMPLATE_SCOPE,
status_code=201) json={'workflows': [WORKFLOW]},
status_code=201
)
wfs = self.workflows.create(WF_DEF) wfs = self.workflows.create(WF_DEF)
@ -62,9 +64,11 @@ class TestWorkflowsV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_create_with_file(self): def test_create_with_file(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_SCOPE, self.requests_mock.post(
json={'workflows': [WORKFLOW]}, self.TEST_URL + URL_TEMPLATE_SCOPE,
status_code=201) json={'workflows': [WORKFLOW]},
status_code=201
)
# The contents of wf_v2.yaml must be identical to WF_DEF # The contents of wf_v2.yaml must be identical to WF_DEF
path = pkg.resource_filename( path = pkg.resource_filename(
@ -83,8 +87,10 @@ class TestWorkflowsV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_update(self): def test_update(self):
self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_SCOPE, self.requests_mock.put(
json={'workflows': [WORKFLOW]}) self.TEST_URL + URL_TEMPLATE_SCOPE,
json={'workflows': [WORKFLOW]}
)
wfs = self.workflows.update(WF_DEF) wfs = self.workflows.update(WF_DEF)
@ -97,8 +103,10 @@ class TestWorkflowsV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_update_with_id(self): def test_update_with_id(self):
self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_NAME % '123', self.requests_mock.put(
json=WORKFLOW) self.TEST_URL + URL_TEMPLATE_NAME % '123',
json=WORKFLOW
)
wf = self.workflows.update(WF_DEF, id='123') 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']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_update_with_file_uri(self): def test_update_with_file_uri(self):
self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_SCOPE, self.requests_mock.put(
json={'workflows': [WORKFLOW]}) self.TEST_URL + URL_TEMPLATE_SCOPE,
json={'workflows': [WORKFLOW]}
)
# The contents of wf_v2.yaml must be identical to WF_DEF # The contents of wf_v2.yaml must be identical to WF_DEF
path = pkg.resource_filename( path = pkg.resource_filename(
@ -136,8 +146,10 @@ class TestWorkflowsV2(base.BaseClientV2Test):
self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual('text/plain', last_request.headers['content-type'])
def test_list(self): def test_list(self):
self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, self.requests_mock.get(
json={'workflows': [WORKFLOW]}) self.TEST_URL + URL_TEMPLATE,
json={'workflows': [WORKFLOW]}
)
workflows_list = self.workflows.list() workflows_list = self.workflows.list()
@ -151,9 +163,13 @@ class TestWorkflowsV2(base.BaseClientV2Test):
) )
def test_list_with_pagination(self): def test_list_with_pagination(self):
self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, self.requests_mock.get(
json={'workflows': [WORKFLOW], self.TEST_URL + URL_TEMPLATE,
'next': '/workflows?fake'}) json={
'workflows': [WORKFLOW],
'next': '/workflows?fake'
}
)
workflows_list = self.workflows.list( workflows_list = self.workflows.list(
limit=1, limit=1,
@ -171,8 +187,10 @@ class TestWorkflowsV2(base.BaseClientV2Test):
self.assertEqual(['asc'], last_request.qs['sort_dirs']) self.assertEqual(['asc'], last_request.qs['sort_dirs'])
def test_list_with_no_limit(self): def test_list_with_no_limit(self):
self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, self.requests_mock.get(
json={'workflows': [WORKFLOW]}) self.TEST_URL + URL_TEMPLATE,
json={'workflows': [WORKFLOW]}
)
workflows_list = self.workflows.list(limit=-1) workflows_list = self.workflows.list(limit=-1)
@ -184,6 +202,7 @@ class TestWorkflowsV2(base.BaseClientV2Test):
def test_get(self): def test_get(self):
url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf' url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf'
self.requests_mock.get(url, json=WORKFLOW) self.requests_mock.get(url, json=WORKFLOW)
wf = self.workflows.get('wf') wf = self.workflows.get('wf')
@ -195,7 +214,9 @@ class TestWorkflowsV2(base.BaseClientV2Test):
) )
def test_delete(self): def test_delete(self):
url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf' self.requests_mock.delete(
self.requests_mock.delete(url, status_code=204) self.TEST_URL + URL_TEMPLATE_NAME % 'wf',
status_code=204
)
self.workflows.delete('wf') 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: for resource in resources:
try: try:
action(resource) action(resource)
print(success_msg % resource) print(success_msg % resource)
except Exception as e: except Exception as e:
failure_flag = True failure_flag = True
@ -73,10 +74,9 @@ def get_contents_if_file(contents_or_file_name):
definition_url = contents_or_file_name definition_url = contents_or_file_name
else: else:
path = os.path.abspath(contents_or_file_name) path = os.path.abspath(contents_or_file_name)
definition_url = parse.urljoin(
'file:', definition_url = parse.urljoin('file:', request.pathname2url(path))
request.pathname2url(path)
)
return request.urlopen(definition_url).read().decode('utf8') return request.urlopen(definition_url).read().decode('utf8')
except Exception: except Exception:
return contents_or_file_name 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_delete = mistralclient.commands.v2.members:Delete
resource_member_update = mistralclient.commands.v2.members:Update 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 = mistralclient.auth =
# Standard Keystone authentication. # Standard Keystone authentication.
keystone = mistralclient.auth.keystone:KeystoneAuthHandler keystone = mistralclient.auth.keystone:KeystoneAuthHandler