Move openstack actions to mistral-extra
Implements: blueprint mistral-actions-api-separate-openstack-actions Change-Id: Iaf1cca7a29df35409fea641fdb60191afb509b5a
This commit is contained in:
parent
57afe0a3f4
commit
bb2af2e2be
@ -3,3 +3,5 @@
|
||||
- openstack-python3-ussuri-jobs
|
||||
- check-requirements
|
||||
- openstack-lower-constraints-jobs
|
||||
- publish-openstack-docs-pti
|
||||
- release-notes-jobs-python3
|
||||
|
@ -1,9 +1,11 @@
|
||||
aodhclient==0.9.0
|
||||
Babel==2.3.4
|
||||
coverage==4.0
|
||||
doc8==0.6.0
|
||||
fixtures==3.0.0
|
||||
hacking==1.1.0
|
||||
mistral-lib==1.2.0
|
||||
keystoneauth1===3.18.0
|
||||
mistral-lib==1.4.0
|
||||
mock==2.0.0
|
||||
openstackdocstheme==1.30.0
|
||||
oslo.log==3.36.0
|
||||
@ -16,3 +18,29 @@ sphinxcontrib-httpdomain==1.3.0
|
||||
sphinxcontrib-pecanwsme==0.10.0
|
||||
stestr==2.0.0
|
||||
unittest2==1.1.0
|
||||
gnocchiclient==3.3.1
|
||||
oauthlib==0.6.2
|
||||
python-barbicanclient==4.5.2
|
||||
python-cinderclient==3.3.0
|
||||
python-zaqarclient==1.0.0
|
||||
python-designateclient==2.7.0
|
||||
python-glanceclient==2.8.0
|
||||
python-glareclient==0.3.0
|
||||
python-heatclient==1.10.0
|
||||
python-keystoneclient==3.8.0
|
||||
python-mistralclient==3.1.0
|
||||
python-manilaclient==1.23.0
|
||||
python-magnumclient==2.15.0
|
||||
python-muranoclient==1.3.0
|
||||
python-neutronclient==6.7.0
|
||||
python-novaclient==9.1.0
|
||||
python-senlinclient==1.11.0
|
||||
python-swiftclient==3.2.0
|
||||
python-tackerclient==0.8.0
|
||||
python-troveclient==2.2.0
|
||||
python-ironicclient==2.7.0
|
||||
python-ironic-inspector-client==1.5.0
|
||||
python-vitrageclient==2.0.0
|
||||
python-zunclient==3.4.0
|
||||
python-qinlingclient==1.0.0
|
||||
yaql==1.1.3
|
||||
|
@ -0,0 +1 @@
|
||||
import mistral_extra.config # noqa
|
0
mistral_extra/actions/__init__.py
Normal file
0
mistral_extra/actions/__init__.py
Normal file
31
mistral_extra/actions/action_generator.py
Normal file
31
mistral_extra/actions/action_generator.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class ActionGenerator(object):
|
||||
"""Action generator.
|
||||
|
||||
Action generator uses some data to build Action classes
|
||||
dynamically.
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def create_actions(self, *args, **kwargs):
|
||||
"""Constructs classes of needed action.
|
||||
|
||||
return: list of actions dicts containing name, class,
|
||||
description and parameter info.
|
||||
"""
|
||||
pass
|
44
mistral_extra/actions/generator_factory.py
Normal file
44
mistral_extra/actions/generator_factory.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from mistral_extra.actions.openstack.action_generator import base
|
||||
|
||||
|
||||
SUPPORTED_MODULES = [
|
||||
'Nova', 'Glance', 'Keystone', 'Heat', 'Neutron', 'Cinder',
|
||||
'Trove', 'Ironic', 'Baremetal Introspection', 'Swift', 'SwiftService',
|
||||
'Zaqar', 'Barbican', 'Mistral', 'Designate', 'Magnum', 'Murano', 'Tacker',
|
||||
'Aodh', 'Gnocchi', 'Glare', 'Vitrage', 'Senlin', 'Zun', 'Qinling', 'Manila'
|
||||
]
|
||||
|
||||
|
||||
def all_generators():
|
||||
for mod_name in SUPPORTED_MODULES:
|
||||
prefix = mod_name.replace(' ', '')
|
||||
mod_namespace = mod_name.lower().replace(' ', '_')
|
||||
mod_cls_name = 'mistral_extra.actions.openstack.actions.%sAction' \
|
||||
% prefix
|
||||
mod_action_cls = importutils.import_class(mod_cls_name)
|
||||
generator_cls_name = '%sActionGenerator' % prefix
|
||||
|
||||
yield type(
|
||||
generator_cls_name,
|
||||
(base.OpenStackActionGenerator,),
|
||||
{
|
||||
'action_namespace': mod_namespace,
|
||||
'base_action_class': mod_action_cls
|
||||
}
|
||||
)
|
0
mistral_extra/actions/openstack/__init__.py
Normal file
0
mistral_extra/actions/openstack/__init__.py
Normal file
172
mistral_extra/actions/openstack/action_generator/base.py
Normal file
172
mistral_extra/actions/openstack/action_generator/base.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import pkg_resources as pkg
|
||||
|
||||
from mistral_extra.actions import action_generator
|
||||
from mistral_extra import version
|
||||
from mistral_lib.utils import inspect_utils as i_u
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_mapping():
|
||||
def delete_comment(map_part):
|
||||
for key, value in map_part.items():
|
||||
if isinstance(value, dict):
|
||||
delete_comment(value)
|
||||
if '_comment' in map_part:
|
||||
del map_part['_comment']
|
||||
package = version.version_info.package
|
||||
|
||||
if os.path.isabs(CONF.openstack_actions_mapping_path):
|
||||
mapping_file_path = CONF.openstack_actions_mapping_path
|
||||
else:
|
||||
path = CONF.openstack_actions_mapping_path
|
||||
mapping_file_path = pkg.resource_filename(package, path)
|
||||
|
||||
LOG.info(
|
||||
"Processing OpenStack action mapping from file: %s",
|
||||
mapping_file_path
|
||||
)
|
||||
|
||||
with open(mapping_file_path) as fh:
|
||||
mapping = json.load(fh)
|
||||
|
||||
for k, v in mapping.items():
|
||||
if isinstance(v, dict):
|
||||
delete_comment(v)
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
class OpenStackActionGenerator(action_generator.ActionGenerator):
|
||||
"""OpenStackActionGenerator.
|
||||
|
||||
Base generator for all OpenStack actions,
|
||||
creates a client method declaration using
|
||||
specific python-client and sets needed arguments
|
||||
to actions.
|
||||
"""
|
||||
action_namespace = None
|
||||
base_action_class = None
|
||||
|
||||
@classmethod
|
||||
def prepare_action_inputs(cls, origin_inputs, added=()):
|
||||
"""Modify action input string.
|
||||
|
||||
Sometimes we need to change the default action input definition for
|
||||
OpenStack actions in order to make the workflow more powerful.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> prepare_action_inputs('a,b,c', added=['region=RegionOne'])
|
||||
a, b, c, region=RegionOne
|
||||
>>> prepare_action_inputs('a,b,c=1', added=['region=RegionOne'])
|
||||
a, b, region=RegionOne, c=1
|
||||
>>> prepare_action_inputs('a,b,c=1,**kwargs',
|
||||
added=['region=RegionOne'])
|
||||
a, b, region=RegionOne, c=1, **kwargs
|
||||
>>> prepare_action_inputs('**kwargs', added=['region=RegionOne'])
|
||||
region=RegionOne, **kwargs
|
||||
>>> prepare_action_inputs('', added=['region=RegionOne'])
|
||||
region=RegionOne
|
||||
|
||||
:param origin_inputs: A string consists of action inputs, separated by
|
||||
comma.
|
||||
:param added: (Optional) A list of params to add to input string.
|
||||
:return: The new action input string.
|
||||
"""
|
||||
if not origin_inputs:
|
||||
return ", ".join(added)
|
||||
|
||||
inputs = [i.strip() for i in origin_inputs.split(',')]
|
||||
kwarg_index = None
|
||||
|
||||
for index, input in enumerate(inputs):
|
||||
if "=" in input:
|
||||
kwarg_index = index
|
||||
if "**" in input:
|
||||
kwarg_index = index - 1
|
||||
|
||||
kwarg_index = len(inputs) if kwarg_index is None else kwarg_index
|
||||
kwarg_index = kwarg_index + 1 if kwarg_index < 0 else kwarg_index
|
||||
|
||||
for a in added:
|
||||
if "=" not in a:
|
||||
inputs.insert(0, a)
|
||||
kwarg_index += 1
|
||||
else:
|
||||
inputs.insert(kwarg_index, a)
|
||||
|
||||
return ", ".join(inputs)
|
||||
|
||||
@classmethod
|
||||
def create_action_class(cls, method_name):
|
||||
if not method_name:
|
||||
return None
|
||||
|
||||
action_class = type(str(method_name), (cls.base_action_class,),
|
||||
{'client_method_name': method_name})
|
||||
|
||||
return action_class
|
||||
|
||||
@classmethod
|
||||
def create_actions(cls):
|
||||
mapping = get_mapping()
|
||||
method_dict = mapping.get(cls.action_namespace, {})
|
||||
|
||||
action_classes = []
|
||||
|
||||
for action_name, method_name in method_dict.items():
|
||||
class_ = cls.create_action_class(method_name)
|
||||
|
||||
try:
|
||||
client_method = class_.get_fake_client_method()
|
||||
except Exception:
|
||||
LOG.exception(
|
||||
"Failed to create action: %s.%s",
|
||||
cls.action_namespace, action_name
|
||||
)
|
||||
continue
|
||||
|
||||
arg_list = i_u.get_arg_list_as_str(client_method)
|
||||
|
||||
# Support specifying region for OpenStack actions.
|
||||
modules = CONF.openstack_actions.modules_support_region
|
||||
if cls.action_namespace in modules:
|
||||
arg_list = cls.prepare_action_inputs(
|
||||
arg_list,
|
||||
added=['action_region=""']
|
||||
)
|
||||
|
||||
description = i_u.get_docstring(client_method)
|
||||
|
||||
action_classes.append(
|
||||
{
|
||||
'class': class_,
|
||||
'name': "%s.%s" % (cls.action_namespace, action_name),
|
||||
'description': description,
|
||||
'arg_list': arg_list,
|
||||
}
|
||||
)
|
||||
|
||||
return action_classes
|
1081
mistral_extra/actions/openstack/actions.py
Normal file
1081
mistral_extra/actions/openstack/actions.py
Normal file
File diff suppressed because it is too large
Load Diff
137
mistral_extra/actions/openstack/base.py
Normal file
137
mistral_extra/actions/openstack/base.py
Normal file
@ -0,0 +1,137 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from mistral_extra.actions.openstack.utils import exceptions as exc
|
||||
from mistral_extra.actions.openstack.utils import keystone as \
|
||||
keystone_utils
|
||||
from mistral_lib import actions
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenStackAction(actions.Action):
|
||||
"""OpenStack Action.
|
||||
|
||||
OpenStack Action is the basis of all OpenStack-specific actions,
|
||||
which are constructed via OpenStack Action generators.
|
||||
"""
|
||||
_kwargs_for_run = {}
|
||||
client_method_name = None
|
||||
_service_name = None
|
||||
_service_type = None
|
||||
_client_class = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._kwargs_for_run = kwargs
|
||||
self.action_region = self._kwargs_for_run.pop('action_region', None)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _create_client(self, context):
|
||||
"""Creates client required for action operation."""
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_client_class(cls):
|
||||
return cls._client_class
|
||||
|
||||
@classmethod
|
||||
def _get_client_method(cls, client):
|
||||
hierarchy_list = cls.client_method_name.split('.')
|
||||
attribute = client
|
||||
|
||||
for attr in hierarchy_list:
|
||||
attribute = getattr(attribute, attr)
|
||||
|
||||
return attribute
|
||||
|
||||
@classmethod
|
||||
def _get_fake_client(cls):
|
||||
"""Returns python-client instance which initiated via wrong args.
|
||||
|
||||
It is needed for getting client-method args and description for
|
||||
saving into DB.
|
||||
"""
|
||||
# Default is simple _get_client_class instance
|
||||
return cls._get_client_class()()
|
||||
|
||||
@classmethod
|
||||
def get_fake_client_method(cls):
|
||||
return cls._get_client_method(cls._get_fake_client())
|
||||
|
||||
def _get_client(self, context):
|
||||
"""Returns python-client instance via cache or creation
|
||||
|
||||
Gets client instance according to specific OpenStack Service
|
||||
(e.g. Nova, Glance, Heat, Keystone etc)
|
||||
|
||||
"""
|
||||
return self._create_client(context)
|
||||
|
||||
def get_session_and_auth(self, context):
|
||||
"""Get keystone session and auth parameters.
|
||||
|
||||
:param context: the action context
|
||||
:return: dict that can be used to initialize service clients
|
||||
"""
|
||||
|
||||
return keystone_utils.get_session_and_auth(
|
||||
service_name=self._service_name,
|
||||
service_type=self._service_type,
|
||||
region_name=self.action_region,
|
||||
ctx=context)
|
||||
|
||||
def get_service_endpoint(self):
|
||||
"""Get OpenStack service endpoint.
|
||||
|
||||
'service_name' and 'service_type' are defined in specific OpenStack
|
||||
service action.
|
||||
"""
|
||||
endpoint = keystone_utils.get_endpoint_for_project(
|
||||
service_name=self._service_name,
|
||||
service_type=self._service_type,
|
||||
region_name=self.action_region
|
||||
)
|
||||
|
||||
return endpoint
|
||||
|
||||
def run(self, context):
|
||||
try:
|
||||
method = self._get_client_method(self._get_client(context))
|
||||
|
||||
result = method(**self._kwargs_for_run)
|
||||
|
||||
if inspect.isgenerator(result):
|
||||
return [v for v in result]
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
# Print the traceback for the last exception so that we can see
|
||||
# where the issue comes from.
|
||||
LOG.warning(traceback.format_exc())
|
||||
|
||||
raise exc.ActionException(
|
||||
"%s.%s failed: %s" %
|
||||
(self.__class__.__name__, self.client_method_name, str(e))
|
||||
)
|
||||
|
||||
def test(self, context):
|
||||
return dict(
|
||||
zip(self._kwargs_for_run, ['test'] * len(self._kwargs_for_run))
|
||||
)
|
1505
mistral_extra/actions/openstack/mapping.json
Normal file
1505
mistral_extra/actions/openstack/mapping.json
Normal file
File diff suppressed because it is too large
Load Diff
0
mistral_extra/actions/openstack/utils/__init__.py
Normal file
0
mistral_extra/actions/openstack/utils/__init__.py
Normal file
42
mistral_extra/actions/openstack/utils/context.py
Normal file
42
mistral_extra/actions/openstack/utils/context.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from mistral_extra.actions.openstack.utils import exceptions as exc
|
||||
from mistral_lib import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
_CTX_THREAD_LOCAL_NAME = "MISTRAL_APP_CTX_THREAD_LOCAL"
|
||||
|
||||
|
||||
def has_ctx():
|
||||
return utils.has_thread_local(_CTX_THREAD_LOCAL_NAME)
|
||||
|
||||
|
||||
def ctx():
|
||||
if not has_ctx():
|
||||
raise exc.ApplicationContextNotFoundException()
|
||||
|
||||
return utils.get_thread_local(_CTX_THREAD_LOCAL_NAME)
|
||||
|
||||
|
||||
def set_ctx(new_ctx):
|
||||
utils.set_thread_local(_CTX_THREAD_LOCAL_NAME, new_ctx)
|
29
mistral_extra/actions/openstack/utils/exceptions.py
Normal file
29
mistral_extra/actions/openstack/utils/exceptions.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2020 - Nokia Corporation
|
||||
#
|
||||
# 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 mistral_lib.exceptions import MistralException
|
||||
|
||||
|
||||
class UnauthorizedException(MistralException):
|
||||
http_code = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class ApplicationContextNotFoundException(MistralException):
|
||||
http_code = 400
|
||||
message = "Application context not found"
|
||||
|
||||
|
||||
class ActionException(MistralException):
|
||||
http_code = 400
|
297
mistral_extra/actions/openstack/utils/keystone.py
Normal file
297
mistral_extra/actions/openstack/utils/keystone.py
Normal file
@ -0,0 +1,297 @@
|
||||
# Copyright (c) 2013 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import keystoneauth1.identity.generic as auth_plugins
|
||||
from keystoneauth1 import loading
|
||||
from keystoneauth1 import session as ks_session
|
||||
from keystoneauth1.token_endpoint import Token
|
||||
from keystoneclient import service_catalog as ks_service_catalog
|
||||
from keystoneclient.v3 import client as ks_client
|
||||
from keystoneclient.v3 import endpoints as ks_endpoints
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
|
||||
from mistral_extra.actions.openstack.utils import context
|
||||
from mistral_extra.actions.openstack.utils import exceptions
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def client():
|
||||
ctx = context.ctx()
|
||||
auth_url = ctx.auth_uri or CONF.keystone_authtoken.www_authenticate_uri
|
||||
|
||||
cl = ks_client.Client(
|
||||
user_id=ctx.user_id,
|
||||
token=ctx.auth_token,
|
||||
tenant_id=ctx.project_id,
|
||||
auth_url=auth_url
|
||||
)
|
||||
|
||||
cl.management_url = auth_url
|
||||
|
||||
return cl
|
||||
|
||||
|
||||
def _determine_verify(ctx):
|
||||
if ctx.insecure:
|
||||
return False
|
||||
elif ctx.auth_cacert:
|
||||
return ctx.auth_cacert
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_session_and_auth(ctx, **kwargs):
|
||||
"""Get session and auth parameters.
|
||||
|
||||
:param ctx: action context
|
||||
:return: dict to be used as kwargs for client service initialization
|
||||
"""
|
||||
|
||||
if not ctx:
|
||||
raise AssertionError('context is mandatory')
|
||||
|
||||
project_endpoint = get_endpoint_for_project(**kwargs)
|
||||
endpoint = format_url(
|
||||
project_endpoint.url,
|
||||
{
|
||||
'tenant_id': ctx.project_id,
|
||||
'project_id': ctx.project_id
|
||||
}
|
||||
)
|
||||
|
||||
auth = Token(endpoint=endpoint, token=ctx.auth_token)
|
||||
|
||||
auth_uri = ctx.auth_uri or CONF.keystone_authtoken.www_authenticate_uri
|
||||
ks_auth = Token(
|
||||
endpoint=auth_uri,
|
||||
token=ctx.auth_token
|
||||
)
|
||||
session = ks_session.Session(
|
||||
auth=ks_auth,
|
||||
verify=_determine_verify(ctx)
|
||||
)
|
||||
|
||||
return {
|
||||
"session": session,
|
||||
"auth": auth
|
||||
}
|
||||
|
||||
|
||||
def _admin_client(trust_id=None):
|
||||
if CONF.keystone_authtoken.auth_type is None:
|
||||
auth_url = CONF.keystone_authtoken.www_authenticate_uri
|
||||
project_name = CONF.keystone_authtoken.admin_tenant_name
|
||||
|
||||
# You can't use trust and project together
|
||||
|
||||
if trust_id:
|
||||
project_name = None
|
||||
|
||||
cl = ks_client.Client(
|
||||
username=CONF.keystone_authtoken.admin_user,
|
||||
password=CONF.keystone_authtoken.admin_password,
|
||||
project_name=project_name,
|
||||
auth_url=auth_url,
|
||||
trusts=trust_id
|
||||
)
|
||||
|
||||
cl.management_url = auth_url
|
||||
|
||||
return cl
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
if trust_id:
|
||||
# Remove domain_id, domain_name, project_name and project_id,
|
||||
# since we need a trust scoped auth object
|
||||
kwargs['domain_id'] = None
|
||||
kwargs['domain_name'] = None
|
||||
kwargs['project_name'] = None
|
||||
kwargs['project_domain_name'] = None
|
||||
kwargs['project_id'] = None
|
||||
kwargs['trust_id'] = trust_id
|
||||
|
||||
auth = loading.load_auth_from_conf_options(
|
||||
CONF,
|
||||
'keystone_authtoken',
|
||||
**kwargs
|
||||
)
|
||||
sess = loading.load_session_from_conf_options(
|
||||
CONF,
|
||||
'keystone',
|
||||
auth=auth
|
||||
)
|
||||
|
||||
return ks_client.Client(session=sess)
|
||||
|
||||
|
||||
def client_for_admin():
|
||||
return _admin_client()
|
||||
|
||||
|
||||
def client_for_trusts(trust_id):
|
||||
return _admin_client(trust_id=trust_id)
|
||||
|
||||
|
||||
def get_endpoint_for_project(service_name=None, service_type=None,
|
||||
region_name=None):
|
||||
if service_name is None and service_type is None:
|
||||
raise exceptions.MistralException(
|
||||
"Either 'service_name' or 'service_type' must be provided."
|
||||
)
|
||||
|
||||
ctx = context.ctx()
|
||||
|
||||
service_catalog = obtain_service_catalog(ctx)
|
||||
|
||||
# When region_name is not passed, first get from context as region_name
|
||||
# could be passed to rest api in http header ('X-Region-Name'). Otherwise,
|
||||
# just get region from mistral configuration.
|
||||
region = (region_name or ctx.region_name)
|
||||
if service_name == 'keystone':
|
||||
# Determining keystone endpoint should be done using
|
||||
# keystone_authtoken section as this option is special for keystone.
|
||||
region = region or CONF.keystone_authtoken.region_name
|
||||
else:
|
||||
region = region or CONF.openstack_actions.default_region
|
||||
|
||||
service_endpoints = service_catalog.get_endpoints(
|
||||
service_name=service_name,
|
||||
service_type=service_type,
|
||||
region_name=region
|
||||
)
|
||||
|
||||
endpoint = None
|
||||
os_actions_endpoint_type = CONF.openstack_actions.os_actions_endpoint_type
|
||||
|
||||
for endpoints in six.itervalues(service_endpoints):
|
||||
for ep in endpoints:
|
||||
# is V3 interface?
|
||||
if 'interface' in ep:
|
||||
interface_type = ep['interface']
|
||||
if os_actions_endpoint_type in interface_type:
|
||||
endpoint = ks_endpoints.Endpoint(
|
||||
None,
|
||||
ep,
|
||||
loaded=True
|
||||
)
|
||||
break
|
||||
# is V2 interface?
|
||||
if 'publicURL' in ep:
|
||||
endpoint_data = {
|
||||
'url': ep['publicURL'],
|
||||
'region': ep['region']
|
||||
}
|
||||
endpoint = ks_endpoints.Endpoint(
|
||||
None,
|
||||
endpoint_data,
|
||||
loaded=True
|
||||
)
|
||||
break
|
||||
|
||||
if not endpoint:
|
||||
raise exceptions.MistralException(
|
||||
"No endpoints found [service_name=%s, service_type=%s,"
|
||||
" region_name=%s]"
|
||||
% (service_name, service_type, region)
|
||||
)
|
||||
else:
|
||||
return endpoint
|
||||
|
||||
|
||||
def obtain_service_catalog(ctx):
|
||||
token = ctx.auth_token
|
||||
|
||||
if ctx.is_trust_scoped and is_token_trust_scoped(token):
|
||||
if ctx.trust_id is None:
|
||||
raise Exception(
|
||||
"'trust_id' must be provided in the admin context."
|
||||
)
|
||||
|
||||
# trust_client = client_for_trusts(ctx.trust_id)
|
||||
# Using trust client, it can't validate token
|
||||
# when cron trigger running because keystone policy
|
||||
# don't allow do this. So we need use admin client to
|
||||
# get token data
|
||||
token_data = _admin_client().tokens.get_token_data(
|
||||
token,
|
||||
include_catalog=True
|
||||
)
|
||||
response = token_data['token']
|
||||
else:
|
||||
response = ctx.service_catalog
|
||||
|
||||
# Target service catalog may not be passed via API.
|
||||
# If we don't have the catalog yet, it should be requested.
|
||||
if not response:
|
||||
response = client().tokens.get_token_data(
|
||||
token,
|
||||
include_catalog=True
|
||||
)['token']
|
||||
|
||||
if not response:
|
||||
raise exceptions.UnauthorizedException()
|
||||
|
||||
service_catalog = ks_service_catalog.ServiceCatalog.factory(response)
|
||||
|
||||
return service_catalog
|
||||
|
||||
|
||||
def get_keystone_endpoint():
|
||||
return get_endpoint_for_project('keystone', service_type='identity')
|
||||
|
||||
|
||||
def get_keystone_url():
|
||||
return get_endpoint_for_project('keystone', service_type='identity').url
|
||||
|
||||
|
||||
def format_url(url_template, values):
|
||||
# Since we can't use keystone module, we can do similar thing:
|
||||
# see https://github.com/openstack/keystone/blob/master/keystone/
|
||||
# catalog/core.py#L42-L60
|
||||
return url_template.replace('$(', '%(') % values
|
||||
|
||||
|
||||
def is_token_trust_scoped(auth_token):
|
||||
return 'OS-TRUST:trust' in client_for_admin().tokens.validate(auth_token)
|
||||
|
||||
|
||||
def get_admin_session():
|
||||
"""Returns a keystone session from Mistral's service credentials."""
|
||||
if CONF.keystone_authtoken.auth_type is None:
|
||||
auth = auth_plugins.Password(
|
||||
CONF.keystone_authtoken.www_authenticate_uri,
|
||||
username=CONF.keystone_authtoken.admin_user,
|
||||
password=CONF.keystone_authtoken.admin_password,
|
||||
project_name=CONF.keystone_authtoken.admin_tenant_name,
|
||||
# NOTE(jaosorior): Once mistral supports keystone v3 properly, we
|
||||
# can fetch the following values from the configuration.
|
||||
user_domain_name='Default',
|
||||
project_domain_name='Default')
|
||||
|
||||
return ks_session.Session(auth=auth)
|
||||
else:
|
||||
auth = loading.load_auth_from_conf_options(
|
||||
CONF,
|
||||
'keystone_authtoken'
|
||||
)
|
||||
|
||||
return loading.load_session_from_conf_options(
|
||||
CONF,
|
||||
'keystone',
|
||||
auth=auth
|
||||
)
|
56
mistral_extra/config.py
Normal file
56
mistral_extra/config.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Copyright 2020 - Nokia Corporation
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
os_actions_mapping_path = cfg.StrOpt(
|
||||
'openstack_actions_mapping_path',
|
||||
short='m',
|
||||
metavar='MAPPING_PATH',
|
||||
default='actions/openstack/mapping.json',
|
||||
help='Path to openstack action mapping json file.'
|
||||
'It could be relative to mistral package '
|
||||
'directory or absolute.'
|
||||
)
|
||||
|
||||
openstack_actions_opts = [
|
||||
cfg.StrOpt(
|
||||
'os-actions-endpoint-type',
|
||||
default=os.environ.get('OS_ACTIONS_ENDPOINT_TYPE', 'public'),
|
||||
choices=['public', 'admin', 'internal'],
|
||||
deprecated_group='DEFAULT',
|
||||
help='Type of endpoint in identity service catalog to use for'
|
||||
' communication with OpenStack services.'
|
||||
),
|
||||
cfg.ListOpt(
|
||||
'modules-support-region',
|
||||
default=['nova', 'glance', 'heat', 'neutron', 'cinder',
|
||||
'trove', 'ironic', 'designate', 'murano', 'tacker', 'senlin',
|
||||
'aodh', 'gnocchi'],
|
||||
help='List of module names that support region in actions.'
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'default_region',
|
||||
help='Default region name for openstack actions supporting region.'
|
||||
),
|
||||
]
|
||||
|
||||
OPENSTACK_ACTIONS_GROUP = 'openstack_actions'
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
CONF.register_opts(openstack_actions_opts, group=OPENSTACK_ACTIONS_GROUP)
|
||||
CONF.register_opt(os_actions_mapping_path)
|
16
mistral_extra/tests/resources/openstack/test_mapping.json
Normal file
16
mistral_extra/tests/resources/openstack/test_mapping.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"_comment": "Mapping OpenStack action namespaces to all its actions. Each action name is mapped to python-client method name in this namespace.",
|
||||
"nova": {
|
||||
"servers_get": "servers.get",
|
||||
"servers_find": "servers.find",
|
||||
"volumes_delete_server_volume": "volumes.delete_server_volume"
|
||||
},
|
||||
"keystone": {
|
||||
"users_list": "users.list",
|
||||
"trusts_create": "trusts.create"
|
||||
},
|
||||
"glance": {
|
||||
"images_list": "images.list",
|
||||
"images_delete": "images.delete"
|
||||
}
|
||||
}
|
0
mistral_extra/tests/unit/actions/__init__.py
Normal file
0
mistral_extra/tests/unit/actions/__init__.py
Normal file
194
mistral_extra/tests/unit/actions/openstack/test_generator.py
Normal file
194
mistral_extra/tests/unit/actions/openstack/test_generator.py
Normal file
@ -0,0 +1,194 @@
|
||||
#
|
||||
# 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 contextlib
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
import mock
|
||||
|
||||
from mistral_extra.actions import generator_factory
|
||||
from mistral_extra.actions.openstack.action_generator import base as \
|
||||
generator_base
|
||||
from mistral_extra.actions.openstack import actions
|
||||
from mistral_extra import config
|
||||
|
||||
from mistral_extra.tests.unit import base
|
||||
|
||||
ABSOLUTE_TEST_MAPPING_PATH = os.path.realpath(
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
"../../../resources/openstack/test_mapping.json")
|
||||
)
|
||||
|
||||
RELATIVE_TEST_MAPPING_PATH = "tests/resources/openstack/test_mapping.json"
|
||||
|
||||
MODULE_MAPPING = {
|
||||
'nova': ['nova.servers_get', actions.NovaAction],
|
||||
'glance': ['glance.images_list', actions.GlanceAction],
|
||||
'keystone': ['keystone.users_create', actions.KeystoneAction],
|
||||
'heat': ['heat.stacks_list', actions.HeatAction],
|
||||
'neutron': ['neutron.show_network', actions.NeutronAction],
|
||||
'cinder': ['cinder.volumes_list', actions.CinderAction],
|
||||
'trove': ['trove.instances_list', actions.TroveAction],
|
||||
'ironic': ['ironic.node_list', actions.IronicAction],
|
||||
'baremetal_introspection': ['baremetal_introspection.introspect',
|
||||
actions.BaremetalIntrospectionAction],
|
||||
'swift': ['swift.head_account', actions.SwiftAction],
|
||||
'swiftservice': ['swiftservice.delete', actions.SwiftServiceAction],
|
||||
'zaqar': ['zaqar.queue_messages', actions.ZaqarAction],
|
||||
'barbican': ['barbican.orders_list', actions.BarbicanAction],
|
||||
'mistral': ['mistral.workflows_get', actions.MistralAction],
|
||||
'designate': ['designate.quotas_list', actions.DesignateAction],
|
||||
'manila': ['manila.shares_list', actions.ManilaAction],
|
||||
'magnum': ['magnum.bays_list', actions.MagnumAction],
|
||||
'murano': ['murano.deployments_list', actions.MuranoAction],
|
||||
'tacker': ['tacker.list_vims', actions.TackerAction],
|
||||
'senlin': ['senlin.get_profile', actions.SenlinAction],
|
||||
'aodh': ['aodh.alarm_list', actions.AodhAction],
|
||||
'gnocchi': ['gnocchi.metric_list', actions.GnocchiAction],
|
||||
'glare': ['glare.artifacts_list', actions.GlareAction],
|
||||
'vitrage': ['vitrage.alarm_get', actions.VitrageAction],
|
||||
'zun': ['zun.containers_list', actions.ZunAction],
|
||||
'qinling': ['qinling.runtimes_list', actions.QinlingAction]
|
||||
}
|
||||
|
||||
EXTRA_MODULES = ['neutron', 'swift', 'zaqar', 'tacker', 'senlin']
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(config.os_actions_mapping_path)
|
||||
|
||||
|
||||
class GeneratorTest(base.BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
super(GeneratorTest, self).setUp()
|
||||
|
||||
# The baremetal inspector client expects the service to be running
|
||||
# when it is initialised and attempts to connect. This mocks out this
|
||||
# service only and returns a simple function that can be used by the
|
||||
# inspection utils.
|
||||
self.baremetal_patch = mock.patch.object(
|
||||
actions.BaremetalIntrospectionAction,
|
||||
"get_fake_client_method",
|
||||
return_value=lambda x: None)
|
||||
|
||||
self.baremetal_patch.start()
|
||||
self.addCleanup(self.baremetal_patch.stop)
|
||||
|
||||
# Do the same for the Designate client.
|
||||
self.designate_patch = mock.patch.object(
|
||||
actions.DesignateAction,
|
||||
"get_fake_client_method",
|
||||
return_value=lambda x: None)
|
||||
|
||||
self.designate_patch.start()
|
||||
self.addCleanup(self.designate_patch.stop)
|
||||
|
||||
def test_generator(self):
|
||||
for generator_cls in generator_factory.all_generators():
|
||||
action_classes = generator_cls.create_actions()
|
||||
|
||||
action_name = MODULE_MAPPING[generator_cls.action_namespace][0]
|
||||
action_cls = MODULE_MAPPING[generator_cls.action_namespace][1]
|
||||
method_name_pre = action_name.split('.')[1]
|
||||
method_name = (
|
||||
method_name_pre
|
||||
if generator_cls.action_namespace in EXTRA_MODULES
|
||||
else method_name_pre.replace('_', '.')
|
||||
)
|
||||
|
||||
action = self._assert_single_item(
|
||||
action_classes,
|
||||
name=action_name
|
||||
)
|
||||
|
||||
self.assertTrue(issubclass(action['class'], action_cls))
|
||||
self.assertEqual(method_name, action['class'].client_method_name)
|
||||
|
||||
modules = CONF.openstack_actions.modules_support_region
|
||||
if generator_cls.action_namespace in modules:
|
||||
self.assertIn('action_region', action['arg_list'])
|
||||
|
||||
def test_missing_module_from_mapping(self):
|
||||
with _patch_openstack_action_mapping_path(RELATIVE_TEST_MAPPING_PATH):
|
||||
for generator_cls in generator_factory.all_generators():
|
||||
action_classes = generator_cls.create_actions()
|
||||
action_names = [action['name'] for action in action_classes]
|
||||
|
||||
cls = MODULE_MAPPING.get(generator_cls.action_namespace)[1]
|
||||
if cls == actions.NovaAction:
|
||||
self.assertIn('nova.servers_get', action_names)
|
||||
self.assertEqual(3, len(action_names))
|
||||
elif cls not in (actions.GlanceAction, actions.KeystoneAction):
|
||||
self.assertEqual([], action_names)
|
||||
|
||||
def test_absolute_mapping_path(self):
|
||||
with _patch_openstack_action_mapping_path(ABSOLUTE_TEST_MAPPING_PATH):
|
||||
self.assertTrue(os.path.isabs(ABSOLUTE_TEST_MAPPING_PATH),
|
||||
"Mapping path is relative: %s" %
|
||||
ABSOLUTE_TEST_MAPPING_PATH)
|
||||
for generator_cls in generator_factory.all_generators():
|
||||
action_classes = generator_cls.create_actions()
|
||||
action_names = [action['name'] for action in action_classes]
|
||||
|
||||
cls = MODULE_MAPPING.get(generator_cls.action_namespace)[1]
|
||||
if cls == actions.NovaAction:
|
||||
self.assertIn('nova.servers_get', action_names)
|
||||
self.assertEqual(3, len(action_names))
|
||||
elif cls not in (actions.GlanceAction, actions.KeystoneAction):
|
||||
self.assertEqual([], action_names)
|
||||
|
||||
def test_prepare_action_inputs(self):
|
||||
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
|
||||
'a,b,c',
|
||||
added=['region=RegionOne']
|
||||
)
|
||||
|
||||
self.assertEqual('a, b, c, region=RegionOne', inputs)
|
||||
|
||||
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
|
||||
'a,b,c=1',
|
||||
added=['region=RegionOne']
|
||||
)
|
||||
|
||||
self.assertEqual('a, b, region=RegionOne, c=1', inputs)
|
||||
|
||||
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
|
||||
'a,b,c=1,**kwargs',
|
||||
added=['region=RegionOne']
|
||||
)
|
||||
|
||||
self.assertEqual('a, b, region=RegionOne, c=1, **kwargs', inputs)
|
||||
|
||||
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
|
||||
'**kwargs',
|
||||
added=['region=RegionOne']
|
||||
)
|
||||
|
||||
self.assertEqual('region=RegionOne, **kwargs', inputs)
|
||||
|
||||
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
|
||||
'',
|
||||
added=['region=RegionOne']
|
||||
)
|
||||
|
||||
self.assertEqual('region=RegionOne', inputs)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_openstack_action_mapping_path(path):
|
||||
original_path = CONF.openstack_actions_mapping_path
|
||||
CONF.set_default("openstack_actions_mapping_path", path)
|
||||
yield
|
||||
CONF.set_default("openstack_actions_mapping_path", original_path)
|
@ -0,0 +1,414 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from mistral_extra.actions.openstack import actions
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
from oslotest import base
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(cfg.BoolOpt('auth_enable'), group='pecan')
|
||||
CONF.register_opt(cfg.HostAddressOpt('host'), group='api')
|
||||
CONF.register_opt(cfg.PortOpt('port'), group='api')
|
||||
|
||||
|
||||
class FakeEndpoint(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
class OpenStackActionTest(base.BaseTestCase):
|
||||
def tearDown(self):
|
||||
super(OpenStackActionTest, self).tearDown()
|
||||
cfg.CONF.set_default('auth_enable', False, group='pecan')
|
||||
|
||||
@mock.patch.object(actions.NovaAction, '_get_client')
|
||||
def test_nova_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "servers.get"
|
||||
action_class = actions.NovaAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'server': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().servers.get.called)
|
||||
mocked().servers.get.assert_called_once_with(server="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.GlanceAction, '_get_client')
|
||||
def test_glance_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "images.delete"
|
||||
action_class = actions.GlanceAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'image': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().images.delete.called)
|
||||
mocked().images.delete.assert_called_once_with(image="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.KeystoneAction, '_get_client')
|
||||
def test_keystone_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "users.get"
|
||||
action_class = actions.KeystoneAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'user': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().users.get.called)
|
||||
mocked().users.get.assert_called_once_with(user="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.HeatAction, '_get_client')
|
||||
def test_heat_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "stacks.get"
|
||||
action_class = actions.HeatAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().stacks.get.called)
|
||||
mocked().stacks.get.assert_called_once_with(id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.NeutronAction, '_get_client')
|
||||
def test_neutron_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "show_network"
|
||||
action_class = actions.NeutronAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().show_network.called)
|
||||
mocked().show_network.assert_called_once_with(id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.CinderAction, '_get_client')
|
||||
def test_cinder_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "volumes.get"
|
||||
action_class = actions.CinderAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'volume': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().volumes.get.called)
|
||||
mocked().volumes.get.assert_called_once_with(volume="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.TroveAction, '_get_client')
|
||||
def test_trove_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "instances.get"
|
||||
action_class = actions.TroveAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'instance': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().instances.get.called)
|
||||
mocked().instances.get.assert_called_once_with(instance="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.IronicAction, '_get_client')
|
||||
def test_ironic_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "node.get"
|
||||
action_class = actions.IronicAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'node': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().node.get.called)
|
||||
mocked().node.get.assert_called_once_with(node="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.BaremetalIntrospectionAction, '_get_client')
|
||||
def test_baremetal_introspector_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "get_status"
|
||||
action_class = actions.BaremetalIntrospectionAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'uuid': '1234'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().get_status.called)
|
||||
mocked().get_status.assert_called_once_with(uuid="1234")
|
||||
|
||||
@mock.patch.object(actions.MistralAction, '_get_client')
|
||||
def test_mistral_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "workflows.get"
|
||||
action_class = actions.MistralAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'name': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().workflows.get.called)
|
||||
mocked().workflows.get.assert_called_once_with(name="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.MistralAction, 'get_session_and_auth')
|
||||
def test_integrated_mistral_action(self, mocked):
|
||||
CONF.set_default('auth_enable', True, group='pecan')
|
||||
mock_endpoint = mock.Mock()
|
||||
mock_endpoint.endpoint = 'http://testendpoint.com:8989/v2'
|
||||
mocked.return_value = {'auth': mock_endpoint, 'session': None}
|
||||
mock_ctx = mock.Mock()
|
||||
action_class = actions.MistralAction
|
||||
params = {'identifier': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
client = action._get_client(mock_ctx)
|
||||
self.assertEqual(client.workbooks.http_client.base_url,
|
||||
mock_endpoint.endpoint)
|
||||
|
||||
def test_standalone_mistral_action(self):
|
||||
CONF.set_default('auth_enable', False, group='pecan')
|
||||
mock_ctx = mock.Mock()
|
||||
action_class = actions.MistralAction
|
||||
params = {'identifier': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
client = action._get_client(mock_ctx)
|
||||
base_url = 'http://{}:{}/v2'.format(CONF.api.host, CONF.api.port)
|
||||
self.assertEqual(client.workbooks.http_client.base_url, base_url)
|
||||
|
||||
@mock.patch.object(actions.SwiftAction, '_get_client')
|
||||
def test_swift_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "get_object"
|
||||
action_class = actions.SwiftAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'container': 'foo', 'object': 'bar'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().get_object.called)
|
||||
mocked().get_object.assert_called_once_with(container='foo',
|
||||
object='bar')
|
||||
|
||||
@mock.patch.object(actions.SwiftServiceAction, '_get_client')
|
||||
def test_swift_service_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "list"
|
||||
action_class = actions.SwiftServiceAction
|
||||
action_class.client_method_name = method_name
|
||||
action = action_class()
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().list.called)
|
||||
mocked().list.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(actions.ZaqarAction, '_get_client')
|
||||
def test_zaqar_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "queue_messages"
|
||||
action_class = actions.ZaqarAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'queue_name': 'foo'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
mocked().queue.assert_called_once_with('foo')
|
||||
mocked().queue().messages.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(actions.BarbicanAction, '_get_client')
|
||||
def test_barbican_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "orders_list"
|
||||
action_class = actions.BarbicanAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'limit': 5}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().orders_list.called)
|
||||
mocked().orders_list.assert_called_once_with(limit=5)
|
||||
|
||||
@mock.patch.object(actions.DesignateAction, '_get_client')
|
||||
def test_designate_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "domain.get"
|
||||
action_class = actions.DesignateAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'domain': 'example.com'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().domain.get.called)
|
||||
mocked().domain.get.assert_called_once_with(domain="example.com")
|
||||
|
||||
@mock.patch.object(actions.MagnumAction, '_get_client')
|
||||
def test_magnum_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "baymodels.get"
|
||||
action_class = actions.MagnumAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().baymodels.get.called)
|
||||
mocked().baymodels.get.assert_called_once_with(id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.MuranoAction, '_get_client')
|
||||
def test_murano_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "categories.get"
|
||||
action_class = actions.MuranoAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'category_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().categories.get.called)
|
||||
mocked().categories.get.assert_called_once_with(
|
||||
category_id="1234-abcd"
|
||||
)
|
||||
|
||||
@mock.patch.object(actions.TackerAction, '_get_client')
|
||||
def test_tacker_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "show_vim"
|
||||
action_class = actions.TackerAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'vim_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().show_vim.called)
|
||||
mocked().show_vim.assert_called_once_with(
|
||||
vim_id="1234-abcd"
|
||||
)
|
||||
|
||||
@mock.patch.object(actions.SenlinAction, '_get_client')
|
||||
def test_senlin_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
action_class = actions.SenlinAction
|
||||
action_class.client_method_name = "get_cluster"
|
||||
action = action_class(cluster_id='1234-abcd')
|
||||
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().get_cluster.called)
|
||||
|
||||
mocked().get_cluster.assert_called_once_with(
|
||||
cluster_id="1234-abcd"
|
||||
)
|
||||
|
||||
@mock.patch.object(actions.AodhAction, '_get_client')
|
||||
def test_aodh_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "alarm.get"
|
||||
action_class = actions.AodhAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'alarm_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().alarm.get.called)
|
||||
mocked().alarm.get.assert_called_once_with(alarm_id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.GnocchiAction, '_get_client')
|
||||
def test_gnocchi_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "metric.get"
|
||||
action_class = actions.GnocchiAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'metric_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().metric.get.called)
|
||||
mocked().metric.get.assert_called_once_with(metric_id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.GlareAction, '_get_client')
|
||||
def test_glare_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "artifacts.get"
|
||||
action_class = actions.GlareAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'artifact_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().artifacts.get.called)
|
||||
mocked().artifacts.get.assert_called_once_with(artifact_id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.VitrageAction, '_get_client')
|
||||
def test_vitrage_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "alarm.get"
|
||||
action_class = actions.VitrageAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'vitrage_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().alarm.get.called)
|
||||
mocked().alarm.get.assert_called_once_with(vitrage_id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.ZunAction, '_get_client')
|
||||
def test_zun_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "containers.get"
|
||||
action_class = actions.ZunAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'container_id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().containers.get.called)
|
||||
mocked().containers.get.assert_called_once_with(
|
||||
container_id="1234-abcd"
|
||||
)
|
||||
|
||||
@mock.patch.object(actions.QinlingAction, '_get_client')
|
||||
def test_qinling_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "runtimes.get"
|
||||
action_class = actions.QinlingAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'id': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().runtimes.get.called)
|
||||
mocked().runtimes.get.assert_called_once_with(id="1234-abcd")
|
||||
|
||||
@mock.patch.object(actions.ManilaAction, '_get_client')
|
||||
def test_manila_action(self, mocked):
|
||||
mock_ctx = mock.Mock()
|
||||
method_name = "shares.get"
|
||||
action_class = actions.ManilaAction
|
||||
action_class.client_method_name = method_name
|
||||
params = {'share': '1234-abcd'}
|
||||
action = action_class(**params)
|
||||
action.run(mock_ctx)
|
||||
|
||||
self.assertTrue(mocked().shares.get.called)
|
||||
mocked().shares.get.assert_called_once_with(share="1234-abcd")
|
||||
|
||||
|
||||
class TestImport(base.BaseTestCase):
|
||||
@mock.patch.object(importutils, 'try_import')
|
||||
def test_try_import_fails(self, mocked):
|
||||
mocked.side_effect = Exception('Exception when importing module')
|
||||
bad_module = actions._try_import('raiser')
|
||||
self.assertIsNone(bad_module)
|
@ -22,3 +22,31 @@ LOG = logging.getLogger(__name__)
|
||||
class BaseTest(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
|
||||
def _assert_single_item(self, items, **props):
|
||||
return self._assert_multiple_items(items, 1, **props)[0]
|
||||
|
||||
def _assert_multiple_items(self, items, count, **props):
|
||||
def _matches(item, **props):
|
||||
for prop_name, prop_val in props.items():
|
||||
v = item[prop_name] if isinstance(
|
||||
item, dict) else getattr(item, prop_name)
|
||||
|
||||
if v != prop_val:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
filtered_items = list(
|
||||
[item for item in items if _matches(item, **props)]
|
||||
)
|
||||
|
||||
found = len(filtered_items)
|
||||
|
||||
if found != count:
|
||||
LOG.info("[failed test ctx] items=%s, expected_props=%s", str(
|
||||
items), props)
|
||||
self.fail("Wrong number of items found [props=%s, "
|
||||
"expected=%s, found=%s]" % (props, count, found))
|
||||
|
||||
return filtered_items
|
||||
|
18
mistral_extra/version.py
Normal file
18
mistral_extra/version.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pbr import version
|
||||
|
||||
version_info = version.VersionInfo('mistral_extra')
|
||||
version_string = version_info.version_string()
|
@ -0,0 +1,3 @@
|
||||
upgrade:
|
||||
- |
|
||||
Move all OpenStack actions to mistral-extra
|
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
276
releasenotes/source/conf.py
Normal file
276
releasenotes/source/conf.py
Normal file
@ -0,0 +1,276 @@
|
||||
# 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.
|
||||
|
||||
# Mistral-lib Release Notes documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
# templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'mistral-extra Release Notes'
|
||||
copyright = u'2016, OpenStack Foundation'
|
||||
|
||||
# Release notes are version independent
|
||||
|
||||
release = ''
|
||||
version = ''
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# Must set this variable to include year, month, day, hours, and minutes.
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
html_use_index = False
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'MistralextraReleaseNotesdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'MistralExtraReleaseNotes.tex',
|
||||
u'Mistral Extra Release Notes Documentation', u'Mistral '
|
||||
u'Extra Developers', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'mistralextrareleasenotes', u'Mistral Extra Release Notes '
|
||||
u'Documentation', [u'Mistral Extra Developers'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'MistralExtraReleaseNotes', u'Mistral Library Release Notes '
|
||||
u'Documentation', u'Mistral Extra Developers',
|
||||
'MistralExtraReleaseNotes', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# -- Options for Internationalization output ------------------------------
|
||||
locale_dirs = ['locale/']
|
||||
|
||||
# -- Options for openstackdocstheme -------------------------------------------
|
||||
repository_name = 'openstack/mistral-extra'
|
||||
bug_project = 'mistral-extra'
|
||||
bug_tag = ''
|
13
releasenotes/source/index.rst
Normal file
13
releasenotes/source/index.rst
Normal file
@ -0,0 +1,13 @@
|
||||
===========================
|
||||
mistral-extra Release Notes
|
||||
===========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
||||
train
|
||||
stein
|
||||
rocky
|
||||
queens
|
||||
pike
|
6
releasenotes/source/pike.rst
Normal file
6
releasenotes/source/pike.rst
Normal file
@ -0,0 +1,6 @@
|
||||
=========================
|
||||
Pike Series Release Notes
|
||||
=========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/pike
|
6
releasenotes/source/queens.rst
Normal file
6
releasenotes/source/queens.rst
Normal file
@ -0,0 +1,6 @@
|
||||
===========================
|
||||
Queens Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/queens
|
6
releasenotes/source/rocky.rst
Normal file
6
releasenotes/source/rocky.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==========================
|
||||
Rocky Series Release Notes
|
||||
==========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/rocky
|
6
releasenotes/source/stein.rst
Normal file
6
releasenotes/source/stein.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==========================
|
||||
Stein Series Release Notes
|
||||
==========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/stein
|
6
releasenotes/source/train.rst
Normal file
6
releasenotes/source/train.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==========================
|
||||
Train Series Release Notes
|
||||
==========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/train
|
5
releasenotes/source/unreleased.rst
Normal file
5
releasenotes/source/unreleased.rst
Normal file
@ -0,0 +1,5 @@
|
||||
============================
|
||||
Current Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
@ -5,4 +5,32 @@
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
Babel!=2.4.0,>=2.3.4 # BSD
|
||||
oslo.log>=3.36.0 # Apache-2.0
|
||||
mistral-lib>=1.2.0 # Apache-2.0
|
||||
mistral-lib>=1.4.0 # Apache-2.0
|
||||
aodhclient>=0.9.0 # Apache-2.0
|
||||
gnocchiclient>=3.3.1 # Apache-2.0
|
||||
python-barbicanclient>=4.5.2 # Apache-2.0
|
||||
python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0
|
||||
python-zaqarclient>=1.0.0 # Apache-2.0
|
||||
python-designateclient>=2.7.0 # Apache-2.0
|
||||
python-glanceclient>=2.8.0 # Apache-2.0
|
||||
python-glareclient>=0.3.0 # Apache-2.0
|
||||
python-heatclient>=1.10.0 # Apache-2.0
|
||||
python-keystoneclient>=3.8.0 # Apache-2.0
|
||||
python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0
|
||||
python-manilaclient>=1.23.0 # Apache-2.0
|
||||
python-magnumclient>=2.15.0 # Apache-2.0
|
||||
python-muranoclient>=1.3.0 # Apache-2.0
|
||||
python-neutronclient>=6.7.0 # Apache-2.0
|
||||
python-novaclient>=9.1.0 # Apache-2.0
|
||||
python-senlinclient>=1.11.0 # Apache-2.0
|
||||
python-swiftclient>=3.2.0 # Apache-2.0
|
||||
python-tackerclient>=0.8.0 # Apache-2.0
|
||||
python-troveclient>=2.2.0 # Apache-2.0
|
||||
python-ironicclient!=2.7.1,!=3.0.0,>=2.7.0 # Apache-2.0
|
||||
python-ironic-inspector-client>=1.5.0 # Apache-2.0
|
||||
python-vitrageclient>=2.0.0 # Apache-2.0
|
||||
python-zunclient>=3.4.0 # Apache-2.0
|
||||
python-qinlingclient>=1.0.0 # Apache-2.0
|
||||
oauthlib>=0.6.2 # BSD
|
||||
yaql>=1.1.3 # Apache-2.0
|
||||
keystoneauth1>=3.18.0 # Apache-2.0
|
@ -22,6 +22,10 @@ classifier =
|
||||
packages =
|
||||
mistral-extra
|
||||
|
||||
[entry_points]
|
||||
mistral.generators =
|
||||
generators = mistral_extra.actions.generator_factory:all_generators
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
|
368
tools/get_action_list.py
Normal file
368
tools/get_action_list.py
Normal file
@ -0,0 +1,368 @@
|
||||
# Copyright 2020 - Nokia Corporation
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
import collections
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
|
||||
from aodhclient.v2 import base as aodh_base
|
||||
from aodhclient.v2 import client as aodhclient
|
||||
from barbicanclient import base as barbican_base
|
||||
from barbicanclient import client as barbicanclient
|
||||
from cinderclient.apiclient import base as cinder_base
|
||||
from cinderclient.v2 import client as cinderclient
|
||||
from designateclient import client as designateclient
|
||||
from glanceclient.v2 import client as glanceclient
|
||||
from glareclient.v1 import client as glareclient
|
||||
from gnocchiclient.v1 import base as gnocchi_base
|
||||
from gnocchiclient.v1 import client as gnocchiclient
|
||||
from heatclient.common import base as heat_base
|
||||
from heatclient.v1 import client as heatclient
|
||||
from ironicclient.common import base as ironic_base
|
||||
from ironicclient.v1 import client as ironicclient
|
||||
from keystoneclient import base as keystone_base
|
||||
from keystoneclient.v3 import client as keystoneclient
|
||||
from magnumclient.common import base as magnum_base
|
||||
from magnumclient.v1 import client as magnumclient
|
||||
from manilaclient import base as manila_base
|
||||
from manilaclient.v2 import client as manilaclient
|
||||
from mistralclient.api import base as mistral_base
|
||||
from mistralclient.api.v2 import client as mistralclient
|
||||
from muranoclient.common import base as murano_base
|
||||
from muranoclient.v1 import client as muranoclient
|
||||
from novaclient import base as nova_base
|
||||
from novaclient import client as novaclient
|
||||
from troveclient import base as trove_base
|
||||
from troveclient.v1 import client as troveclient
|
||||
|
||||
# TODO(nmakhotkin): Find a rational way to do it for neutron.
|
||||
# TODO(nmakhotkin): Implement recursive way of searching for managers
|
||||
# TODO(nmakhotkin): (e.g. keystone).
|
||||
# TODO(dprince): Need to update ironic_inspector_client before we can
|
||||
# plug it in cleanly here.
|
||||
# TODO(dprince): Swiftclient doesn't currently support discovery
|
||||
# like we do in this class.
|
||||
# TODO(therve): Zaqarclient doesn't currently support discovery
|
||||
# like we do in this class.
|
||||
# TODO(sa709c): Tackerclient doesn't currently support discovery
|
||||
# like we do in this class.
|
||||
|
||||
"""It is simple CLI tool which allows to see and update mapping.json file
|
||||
if needed. mapping.json contains all allowing OpenStack actions sorted by
|
||||
service name. Usage example:
|
||||
|
||||
python tools/get_action_list.py nova
|
||||
|
||||
The result will be simple JSON containing action name as a key and method
|
||||
path as a value. For updating mapping.json it is need to copy all keys and
|
||||
values of the result to corresponding section of mapping.json:
|
||||
|
||||
...mapping.json...
|
||||
"nova": {
|
||||
<put it here>
|
||||
},
|
||||
...mapping.json...
|
||||
|
||||
|
||||
Note: in case of Keystone service, correct OS_AUTH_URL v3 and the rest auth
|
||||
info must be provided. It can be provided either via environment variables
|
||||
or CLI arguments. See --help for details.
|
||||
"""
|
||||
|
||||
BASE_HEAT_MANAGER = heat_base.HookableMixin
|
||||
BASE_NOVA_MANAGER = nova_base.HookableMixin
|
||||
BASE_KEYSTONE_MANAGER = keystone_base.Manager
|
||||
BASE_CINDER_MANAGER = cinder_base.HookableMixin
|
||||
BASE_MISTRAL_MANAGER = mistral_base.ResourceManager
|
||||
BASE_TROVE_MANAGER = trove_base.Manager
|
||||
BASE_IRONIC_MANAGER = ironic_base.Manager
|
||||
BASE_BARBICAN_MANAGER = barbican_base.BaseEntityManager
|
||||
BASE_MANILA_MANAGER = manila_base.Manager
|
||||
BASE_MAGNUM_MANAGER = magnum_base.Manager
|
||||
BASE_MURANO_MANAGER = murano_base.Manager
|
||||
BASE_AODH_MANAGER = aodh_base.Manager
|
||||
BASE_GNOCCHI_MANAGER = gnocchi_base.Manager
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Gets All needed methods of OpenStack clients.',
|
||||
usage="python get_action_list.py <service_name>"
|
||||
)
|
||||
parser.add_argument(
|
||||
'service',
|
||||
choices=CLIENTS.keys(),
|
||||
help='Service name which methods need to be found.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-username',
|
||||
dest='username',
|
||||
default=os.environ.get('OS_USERNAME', 'admin'),
|
||||
help='Authentication username (Env: OS_USERNAME)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-password',
|
||||
dest='password',
|
||||
default=os.environ.get('OS_PASSWORD', 'openstack'),
|
||||
help='Authentication password (Env: OS_PASSWORD)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-tenant-name',
|
||||
dest='tenant_name',
|
||||
default=os.environ.get('OS_TENANT_NAME', 'Default'),
|
||||
help='Authentication tenant name (Env: OS_TENANT_NAME)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--os-auth-url',
|
||||
dest='auth_url',
|
||||
default=os.environ.get('OS_AUTH_URL'),
|
||||
help='Authentication URL (Env: OS_AUTH_URL)'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
GLANCE_NAMESPACE_LIST = [
|
||||
'image_members', 'image_tags', 'images', 'schemas', 'tasks',
|
||||
'metadefs_resource_type', 'metadefs_property', 'metadefs_object',
|
||||
'metadefs_tag', 'metadefs_namespace', 'versions'
|
||||
]
|
||||
|
||||
|
||||
DESIGNATE_NAMESPACE_LIST = [
|
||||
'diagnostics', 'domains', 'quotas', 'records', 'reports', 'servers',
|
||||
'sync', 'touch'
|
||||
]
|
||||
|
||||
|
||||
GLARE_NAMESPACE_LIST = ['artifacts', 'versions']
|
||||
|
||||
|
||||
def get_nova_client(**kwargs):
|
||||
return novaclient.Client(2)
|
||||
|
||||
|
||||
def get_keystone_client(**kwargs):
|
||||
return keystoneclient.Client(**kwargs)
|
||||
|
||||
|
||||
def get_glance_client(**kwargs):
|
||||
return glanceclient.Client(kwargs.get('auth_url'))
|
||||
|
||||
|
||||
def get_heat_client(**kwargs):
|
||||
return heatclient.Client('')
|
||||
|
||||
|
||||
def get_cinder_client(**kwargs):
|
||||
return cinderclient.Client()
|
||||
|
||||
|
||||
def get_mistral_client(**kwargs):
|
||||
return mistralclient.Client()
|
||||
|
||||
|
||||
def get_trove_client(**kwargs):
|
||||
return troveclient.Client('username', 'password')
|
||||
|
||||
|
||||
def get_ironic_client(**kwargs):
|
||||
return ironicclient.Client("http://127.0.0.1:6385/")
|
||||
|
||||
|
||||
def get_barbican_client(**kwargs):
|
||||
return barbicanclient.Client(
|
||||
project_id="1",
|
||||
endpoint="http://127.0.0.1:9311"
|
||||
)
|
||||
|
||||
|
||||
def get_designate_client(**kwargs):
|
||||
return designateclient.Client('2')
|
||||
|
||||
|
||||
def get_magnum_client(**kwargs):
|
||||
return magnumclient.Client()
|
||||
|
||||
|
||||
def get_murano_client(**kwargs):
|
||||
return muranoclient.Client('')
|
||||
|
||||
|
||||
def get_aodh_client(**kwargs):
|
||||
return aodhclient.Client('')
|
||||
|
||||
|
||||
def get_gnocchi_client(**kwargs):
|
||||
return gnocchiclient.Client()
|
||||
|
||||
|
||||
def get_glare_client(**kwargs):
|
||||
return glareclient.Client('')
|
||||
|
||||
|
||||
def get_manila_client(**kwargs):
|
||||
return manilaclient.Client(
|
||||
input_auth_token='token',
|
||||
service_catalog_url='http://127.0.0.1:8786'
|
||||
)
|
||||
|
||||
|
||||
CLIENTS = {
|
||||
'nova': get_nova_client,
|
||||
'heat': get_heat_client,
|
||||
'cinder': get_cinder_client,
|
||||
'keystone': get_keystone_client,
|
||||
'glance': get_glance_client,
|
||||
'trove': get_trove_client,
|
||||
'ironic': get_ironic_client,
|
||||
'barbican': get_barbican_client,
|
||||
'mistral': get_mistral_client,
|
||||
'designate': get_designate_client,
|
||||
'magnum': get_magnum_client,
|
||||
'murano': get_murano_client,
|
||||
'aodh': get_aodh_client,
|
||||
'gnocchi': get_gnocchi_client,
|
||||
'glare': get_glare_client,
|
||||
'manila': get_manila_client,
|
||||
# 'neutron': get_nova_client
|
||||
# 'baremetal_introspection': ...
|
||||
# 'swift': ...
|
||||
# 'zaqar': ...
|
||||
}
|
||||
BASE_MANAGERS = {
|
||||
'nova': BASE_NOVA_MANAGER,
|
||||
'heat': BASE_HEAT_MANAGER,
|
||||
'cinder': BASE_CINDER_MANAGER,
|
||||
'keystone': BASE_KEYSTONE_MANAGER,
|
||||
'glance': None,
|
||||
'trove': BASE_TROVE_MANAGER,
|
||||
'ironic': BASE_IRONIC_MANAGER,
|
||||
'barbican': BASE_BARBICAN_MANAGER,
|
||||
'mistral': BASE_MISTRAL_MANAGER,
|
||||
'designate': None,
|
||||
'magnum': BASE_MAGNUM_MANAGER,
|
||||
'murano': BASE_MURANO_MANAGER,
|
||||
'aodh': BASE_AODH_MANAGER,
|
||||
'gnocchi': BASE_GNOCCHI_MANAGER,
|
||||
'glare': None,
|
||||
'manila': BASE_MANILA_MANAGER,
|
||||
# 'neutron': BASE_NOVA_MANAGER
|
||||
# 'baremetal_introspection': ...
|
||||
# 'swift': ...
|
||||
# 'zaqar': ...
|
||||
}
|
||||
NAMESPACES = {
|
||||
'glance': GLANCE_NAMESPACE_LIST,
|
||||
'designate': DESIGNATE_NAMESPACE_LIST,
|
||||
'glare': GLARE_NAMESPACE_LIST
|
||||
}
|
||||
ALLOWED_ATTRS = ['service_catalog', 'catalog']
|
||||
FORBIDDEN_METHODS = [
|
||||
'add_hook', 'alternate_service_type', 'completion_cache', 'run_hooks',
|
||||
'write_to_completion_cache', 'model', 'build_key_only_query', 'build_url',
|
||||
'head', 'put', 'unvalidated_model'
|
||||
]
|
||||
|
||||
|
||||
def get_public_attrs(obj):
|
||||
all_attrs = dir(obj)
|
||||
|
||||
return [a for a in all_attrs if not a.startswith('_')]
|
||||
|
||||
|
||||
def get_public_methods(attr, client):
|
||||
hierarchy_list = attr.split('.')
|
||||
attribute = client
|
||||
|
||||
for attr in hierarchy_list:
|
||||
attribute = getattr(attribute, attr)
|
||||
all_attributes_list = get_public_attrs(attribute)
|
||||
|
||||
methods = []
|
||||
for a in all_attributes_list:
|
||||
allowed = a in ALLOWED_ATTRS
|
||||
forbidden = a in FORBIDDEN_METHODS
|
||||
|
||||
if (not forbidden and
|
||||
(allowed or inspect.ismethod(getattr(attribute, a)))):
|
||||
methods.append(a)
|
||||
|
||||
return methods
|
||||
|
||||
|
||||
def get_manager_list(service_name, client):
|
||||
base_manager = BASE_MANAGERS[service_name]
|
||||
|
||||
if not base_manager:
|
||||
return NAMESPACES[service_name]
|
||||
|
||||
public_attrs = get_public_attrs(client)
|
||||
|
||||
manager_list = []
|
||||
|
||||
for attr in public_attrs:
|
||||
if (isinstance(getattr(client, attr), base_manager) or
|
||||
attr in ALLOWED_ATTRS):
|
||||
manager_list.append(attr)
|
||||
|
||||
return manager_list
|
||||
|
||||
|
||||
def get_mapping_for_service(service, client):
|
||||
mapping = collections.OrderedDict()
|
||||
for man in get_manager_list(service, client):
|
||||
public_methods = get_public_methods(man, client)
|
||||
for method in public_methods:
|
||||
key = "%s_%s" % (man, method)
|
||||
value = "%s.%s" % (man, method)
|
||||
mapping[key] = value
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def print_mapping(mapping):
|
||||
print(json.dumps(mapping, indent=8, separators=(',', ': ')))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_parser().parse_args()
|
||||
|
||||
auth_info = {
|
||||
'username': args.username,
|
||||
'tenant_name': args.tenant_name,
|
||||
'password': args.password,
|
||||
'auth_url': args.auth_url
|
||||
}
|
||||
|
||||
service = args.service
|
||||
client = CLIENTS.get(service)(**auth_info)
|
||||
|
||||
print("Find methods for service: %s..." % service)
|
||||
|
||||
print_mapping(get_mapping_for_service(service, client))
|
Loading…
x
Reference in New Issue
Block a user