Move openstack actions to mistral-extra
Implements: blueprint mistral-actions-api-separate-openstack-actions Change-Id: Iaf1cca7a29df35409fea641fdb60191afb509b5a
This commit is contained in:
@@ -3,3 +3,5 @@
|
|||||||
- openstack-python3-ussuri-jobs
|
- openstack-python3-ussuri-jobs
|
||||||
- check-requirements
|
- check-requirements
|
||||||
- openstack-lower-constraints-jobs
|
- openstack-lower-constraints-jobs
|
||||||
|
- publish-openstack-docs-pti
|
||||||
|
- release-notes-jobs-python3
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
|
aodhclient==0.9.0
|
||||||
Babel==2.3.4
|
Babel==2.3.4
|
||||||
coverage==4.0
|
coverage==4.0
|
||||||
doc8==0.6.0
|
doc8==0.6.0
|
||||||
fixtures==3.0.0
|
fixtures==3.0.0
|
||||||
hacking==1.1.0
|
hacking==1.1.0
|
||||||
mistral-lib==1.2.0
|
keystoneauth1===3.18.0
|
||||||
|
mistral-lib==1.4.0
|
||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
openstackdocstheme==1.30.0
|
openstackdocstheme==1.30.0
|
||||||
oslo.log==3.36.0
|
oslo.log==3.36.0
|
||||||
@@ -16,3 +18,29 @@ sphinxcontrib-httpdomain==1.3.0
|
|||||||
sphinxcontrib-pecanwsme==0.10.0
|
sphinxcontrib-pecanwsme==0.10.0
|
||||||
stestr==2.0.0
|
stestr==2.0.0
|
||||||
unittest2==1.1.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):
|
class BaseTest(base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseTest, self).setUp()
|
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
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
Babel!=2.4.0,>=2.3.4 # BSD
|
Babel!=2.4.0,>=2.3.4 # BSD
|
||||||
oslo.log>=3.36.0 # Apache-2.0
|
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 =
|
packages =
|
||||||
mistral-extra
|
mistral-extra
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
mistral.generators =
|
||||||
|
generators = mistral_extra.actions.generator_factory:all_generators
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
build-dir = doc/build
|
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))
|
2
tox.ini
2
tox.ini
@@ -69,7 +69,7 @@ max-line-length = 80
|
|||||||
# E123, E125 skipped as they are invalid PEP-8.
|
# E123, E125 skipped as they are invalid PEP-8.
|
||||||
|
|
||||||
show-source = True
|
show-source = True
|
||||||
ignore = E123,E125
|
ignore = E123,E125,W504
|
||||||
builtins = _
|
builtins = _
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user