diff --git a/.zuul.yaml b/.zuul.yaml index ff2b269..3d4f6fb 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -3,3 +3,5 @@ - openstack-python3-ussuri-jobs - check-requirements - openstack-lower-constraints-jobs + - publish-openstack-docs-pti + - release-notes-jobs-python3 diff --git a/lower-constraints.txt b/lower-constraints.txt index 78aad50..8c3f2f8 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,9 +1,11 @@ +aodhclient==0.9.0 Babel==2.3.4 coverage==4.0 doc8==0.6.0 fixtures==3.0.0 hacking==1.1.0 -mistral-lib==1.2.0 +keystoneauth1===3.18.0 +mistral-lib==1.4.0 mock==2.0.0 openstackdocstheme==1.30.0 oslo.log==3.36.0 @@ -16,3 +18,29 @@ sphinxcontrib-httpdomain==1.3.0 sphinxcontrib-pecanwsme==0.10.0 stestr==2.0.0 unittest2==1.1.0 +gnocchiclient==3.3.1 +oauthlib==0.6.2 +python-barbicanclient==4.5.2 +python-cinderclient==3.3.0 +python-zaqarclient==1.0.0 +python-designateclient==2.7.0 +python-glanceclient==2.8.0 +python-glareclient==0.3.0 +python-heatclient==1.10.0 +python-keystoneclient==3.8.0 +python-mistralclient==3.1.0 +python-manilaclient==1.23.0 +python-magnumclient==2.15.0 +python-muranoclient==1.3.0 +python-neutronclient==6.7.0 +python-novaclient==9.1.0 +python-senlinclient==1.11.0 +python-swiftclient==3.2.0 +python-tackerclient==0.8.0 +python-troveclient==2.2.0 +python-ironicclient==2.7.0 +python-ironic-inspector-client==1.5.0 +python-vitrageclient==2.0.0 +python-zunclient==3.4.0 +python-qinlingclient==1.0.0 +yaql==1.1.3 diff --git a/mistral_extra/__init__.py b/mistral_extra/__init__.py index e69de29..c2b74be 100644 --- a/mistral_extra/__init__.py +++ b/mistral_extra/__init__.py @@ -0,0 +1 @@ +import mistral_extra.config # noqa \ No newline at end of file diff --git a/mistral_extra/actions/__init__.py b/mistral_extra/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/actions/action_generator.py b/mistral_extra/actions/action_generator.py new file mode 100644 index 0000000..e38eb43 --- /dev/null +++ b/mistral_extra/actions/action_generator.py @@ -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 diff --git a/mistral_extra/actions/generator_factory.py b/mistral_extra/actions/generator_factory.py new file mode 100644 index 0000000..c741bac --- /dev/null +++ b/mistral_extra/actions/generator_factory.py @@ -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 + } + ) diff --git a/mistral_extra/actions/openstack/__init__.py b/mistral_extra/actions/openstack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/actions/openstack/action_generator/__init__.py b/mistral_extra/actions/openstack/action_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/actions/openstack/action_generator/base.py b/mistral_extra/actions/openstack/action_generator/base.py new file mode 100644 index 0000000..758e55e --- /dev/null +++ b/mistral_extra/actions/openstack/action_generator/base.py @@ -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 diff --git a/mistral_extra/actions/openstack/actions.py b/mistral_extra/actions/openstack/actions.py new file mode 100644 index 0000000..9091875 --- /dev/null +++ b/mistral_extra/actions/openstack/actions.py @@ -0,0 +1,1081 @@ +# 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 functools + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import importutils + +from keystoneauth1.identity import v3 as ks_identity_v3 +from keystoneauth1 import session as ks_session +from keystoneauth1.token_endpoint import Token +from keystoneclient import httpclient + +from mistral_extra.actions.openstack import base +from mistral_extra.actions.openstack.utils import keystone as keystone_utils +from mistral_lib.utils import inspect_utils + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +IRONIC_API_VERSION = '1.34' +"""The default microversion to pass to Ironic API. + +1.34 corresponds to Pike final. +""" + + +def _try_import(module_name): + try: + return importutils.try_import(module_name) + except Exception as e: + msg = 'Unable to load module "%s". %s' % (module_name, str(e)) + LOG.error(msg) + return None + + +aodhclient = _try_import('aodhclient.v2.client') +barbicanclient = _try_import('barbicanclient.client') +cinderclient = _try_import('cinderclient.client') +cinder_api_versions = _try_import('cinderclient.api_versions') +designateclient = _try_import('designateclient.v2.client') +glanceclient = _try_import('glanceclient') +glareclient = _try_import('glareclient.v1.client') +gnocchiclient = _try_import('gnocchiclient.v1.client') +heatclient = _try_import('heatclient.client') +ironic_inspector_client = _try_import('ironic_inspector_client.v1') +ironicclient = _try_import('ironicclient.v1.client') +keystoneclient = _try_import('keystoneclient.v3.client') +manila = _try_import('manilaclient') +manilaclient = _try_import('manilaclient.client') +manila_api_versions = _try_import('manilaclient.api_versions') +magnumclient = _try_import('magnumclient.v1.client') +mistralclient = _try_import('mistralclient.api.v2.client') +muranoclient = _try_import('muranoclient.v1.client') +neutronclient = _try_import('neutronclient.v2_0.client') +nova = _try_import('novaclient') +novaclient = _try_import('novaclient.client') +nova_api_versions = _try_import('novaclient.api_versions') +qinlingclient = _try_import('qinlingclient.v1.client') +senlinclient = _try_import('senlinclient.v1.client') +swift_client = _try_import('swiftclient.client') +swiftservice = _try_import('swiftclient.service') +tackerclient = _try_import('tackerclient.v1_0.client') +troveclient = _try_import('troveclient.v1.client') +vitrageclient = _try_import('vitrageclient.v1.client') +zaqarclient = _try_import('zaqarclient.queues.client') +zunclient = _try_import('zunclient.v1.client') +zun_api_versions = _try_import('zunclient.api_versions') + + +class NovaAction(base.OpenStackAction): + _service_type = 'compute' + + @classmethod + def _get_client_class(cls): + return novaclient.Client + + def _create_client(self, context): + LOG.debug("Nova action security context: %s", context) + + nova_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + temp_client = self._get_client_class()( + nova.API_MAX_VERSION, + endpoint_override=nova_endpoint.url, + session=session_and_auth['session'] + ) + + discovered_version = nova_api_versions.discover_version( + temp_client, + nova.API_MAX_VERSION + ) + + return self._get_client_class()( + discovered_version, + endpoint_override=nova_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(2) + + +class GlanceAction(base.OpenStackAction): + _service_type = 'image' + + @classmethod + def _get_client_class(cls): + return glanceclient.Client + + def _create_client(self, context): + + LOG.debug("Glance action security context: %s", context) + + glance_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + '2', + endpoint=glance_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(endpoint="http://127.0.0.1:9292/v2") + + +class KeystoneAction(base.OpenStackAction): + + _service_type = 'identity' + + @classmethod + def _get_client_class(cls): + return keystoneclient.Client + + def _create_client(self, context): + + LOG.debug("Keystone action security context: %s", context) + + keystone_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + endpoint=keystone_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + # Here we need to replace httpclient authenticate method temporarily + authenticate = httpclient.HTTPClient.authenticate + + httpclient.HTTPClient.authenticate = lambda x: True + fake_client = cls._get_client_class()() + + # Once we get fake client, return back authenticate method + httpclient.HTTPClient.authenticate = authenticate + + return fake_client + + +class HeatAction(base.OpenStackAction): + _service_type = 'orchestration' + + @classmethod + def _get_client_class(cls): + return heatclient.Client + + def _create_client(self, context): + + LOG.debug("Heat action security context: %s", context) + + heat_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + '1', + endpoint_override=heat_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + '1', + endpoint="http://127.0.0.1:8004/v1/fake" + ) + + +class NeutronAction(base.OpenStackAction): + _service_type = 'network' + + @classmethod + def _get_client_class(cls): + return neutronclient.Client + + def _create_client(self, context): + + LOG.debug("Neutron action security context: %s", context) + + neutron_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + endpoint_override=neutron_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(endpoint="http://127.0.0.1") + + +class CinderAction(base.OpenStackAction): + _service_type = 'volumev3' + + @classmethod + def _get_client_class(cls): + return cinderclient.Client + + def _create_client(self, context): + + LOG.debug("Cinder action security context: %s", context) + + cinder_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + temp_client = self._get_client_class()( + cinder_api_versions.MAX_VERSION, + endpoint_override=cinder_endpoint.url, + session=session_and_auth['session'] + ) + + discovered_version = cinder_api_versions.discover_version( + temp_client, + cinder_api_versions.APIVersion(cinder_api_versions.MAX_VERSION), + ) + + return self._get_client_class()( + discovered_version, + endpoint_override=cinder_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()('3') + + +class MistralAction(base.OpenStackAction): + _service_type = 'workflowv2' + + @classmethod + def _get_client_class(cls): + return mistralclient.Client + + def _create_client(self, context): + + LOG.debug("Mistral action security context: %s", context) + + if CONF.pecan.auth_enable: + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + mistral_url=session_and_auth['auth'].endpoint, + **session_and_auth) + else: + mistral_url = 'http://{}:{}/v2'.format(CONF.api.host, + CONF.api.port) + return self._get_client_class()(mistral_url=mistral_url) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()() + + +class TroveAction(base.OpenStackAction): + _service_type = 'database' + + @classmethod + def _get_client_class(cls): + return troveclient.Client + + def _create_client(self, context): + + LOG.debug("Trove action security context: %s", context) + + trove_endpoint = self.get_service_endpoint() + + trove_url = keystone_utils.format_url( + trove_endpoint.url, + {'tenant_id': context.project_id} + ) + + client = self._get_client_class()( + context.user_name, + context.auth_token, + project_id=context.project_id, + auth_url=trove_url, + region_name=trove_endpoint.region, + insecure=context.insecure + ) + + client.client.auth_token = context.auth_token + client.client.management_url = trove_url + + return client + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()("fake_user", "fake_passwd") + + +class IronicAction(base.OpenStackAction): + _service_name = 'ironic' + + @classmethod + def _get_client_class(cls): + return ironicclient.Client + + def _create_client(self, context): + + LOG.debug("Ironic action security context: %s", context) + + ironic_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + client = self._get_client_class()( + os_ironic_api_version=IRONIC_API_VERSION, + endpoint_override=ironic_endpoint.url, + session=session_and_auth['session'] + ) + + return client + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + endpoint_override="http://127.0.0.1:6385/", + session=ks_session.Session(), + ) + + +class BaremetalIntrospectionAction(base.OpenStackAction): + + @classmethod + def _get_client_class(cls): + return ironic_inspector_client.ClientV1 + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(inspector_url='http://127.0.0.1') + + def _create_client(self, context): + LOG.debug( + "Baremetal introspection action security context: %s", context) + + inspector_endpoint = keystone_utils.get_endpoint_for_project( + service_type='baremetal-introspection' + ) + auth = Token(endpoint=inspector_endpoint.url, + token=context.auth_token) + + return self._get_client_class()( + api_version=1, + session=ks_session.Session(auth) + ) + + +class SwiftAction(base.OpenStackAction): + _service_name = 'swift' + + @classmethod + def _get_client_class(cls): + return swift_client.Connection + + def _create_client(self, context): + + LOG.debug("Swift action security context: %s", context) + + swift_endpoint = self.get_service_endpoint() + + swift_url = keystone_utils.format_url( + swift_endpoint.url, + {'tenant_id': context.project_id} + ) + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + session=session_and_auth['session'], + preauthurl=swift_url + ) + + +class SwiftServiceAction(base.OpenStackAction): + _service_name = 'swift' + + @classmethod + def _get_client_class(cls): + return swiftservice.SwiftService + + def _create_client(self, context): + + LOG.debug("Swift action security context: %s", context) + + swift_endpoint = self.get_service_endpoint() + + swift_opts = { + 'os_storage_url': swift_endpoint.url % { + 'tenant_id': context.project_id + }, + 'os_auth_token': context.auth_token, + 'os_region_name': swift_endpoint.region, + 'os_project_id': context.security.project_id, + } + + return swiftservice.SwiftService(options=swift_opts) + + @classmethod + def _get_client_method(cls, client): + return getattr(client, cls.client_method_name) + + +class ZaqarAction(base.OpenStackAction): + _service_type = 'messaging' + + @classmethod + def _get_client_class(cls): + return zaqarclient.Client + + def _create_client(self, context): + LOG.debug("Zaqar action security context: %s", context) + + zaqar_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + version=2, + url=zaqar_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(version=2) + + @classmethod + def _get_client_method(cls, client): + method = getattr(cls, cls.client_method_name) + + # We can't use partial as it's not supported by getargspec + @functools.wraps(method) + def wrap(*args, **kwargs): + return method(client, *args, **kwargs) + + arguments = inspect_utils.get_arg_list_as_str(method) + # Remove client + wrap.__arguments__ = arguments.split(', ', 1)[1] + + return wrap + + @staticmethod + def queue_messages(client, queue_name, **params): + """Gets a list of messages from the queue. + + :param client: the Zaqar client + :type client: zaqarclient.queues.client + + :param queue_name: Name of the target queue. + :type queue_name: `six.string_type` + + :param params: Filters to use for getting messages. + :type params: **kwargs dict + + :returns: List of messages. + :rtype: `list` + """ + queue = client.queue(queue_name) + + return queue.messages(**params) + + @staticmethod + def queue_post(client, queue_name, messages): + """Posts one or more messages to a queue. + + :param client: the Zaqar client + :type client: zaqarclient.queues.client + + :param queue_name: Name of the target queue. + :type queue_name: `six.string_type` + + :param messages: One or more messages to post. + :type messages: `list` or `dict` + + :returns: A dict with the result of this operation. + :rtype: `dict` + """ + queue = client.queue(queue_name) + + return queue.post(messages) + + @staticmethod + def queue_pop(client, queue_name, count=1): + """Pop `count` messages from the queue. + + :param client: the Zaqar client + :type client: zaqarclient.queues.client + + :param queue_name: Name of the target queue. + :type queue_name: `six.string_type` + + :param count: Number of messages to pop. + :type count: int + + :returns: List of messages. + :rtype: `list` + """ + queue = client.queue(queue_name) + + return queue.pop(count) + + @staticmethod + def claim_messages(client, queue_name, **params): + """Claim messages from the queue + + :param client: the Zaqar client + :type client: zaqarclient.queues.client + + :param queue_name: Name of the target queue. + :type queue_name: `six.string_type` + + :returns: List of claims + :rtype: `list` + """ + queue = client.queue(queue_name) + return queue.claim(**params) + + @staticmethod + def delete_messages(client, queue_name, messages): + """Delete messages from the queue + + :param client: the Zaqar client + :type client: zaqarclient.queues.client + + :param queue_name: Name of the target queue. + :type queue_name: `six.string_type` + + :param messages: List of messages' ids to delete. + :type messages: *args of `six.string_type` + + :returns: List of messages' ids that have been deleted + :rtype: `list` + """ + queue = client.queue(queue_name) + return queue.delete_messages(*messages) + + +class BarbicanAction(base.OpenStackAction): + _service_type = 'key-manager' + + @classmethod + def _get_client_class(cls): + return barbicanclient.Client + + def _create_client(self, context): + LOG.debug("Barbican action security context: %s", context) + + barbican_endpoint = self.get_service_endpoint() + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()( + endpoint=barbican_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + project_id="1", + endpoint="http://127.0.0.1:9311" + ) + + @classmethod + def _get_client_method(cls, client): + if cls.client_method_name not in ["secrets_store", + "secrets_retrieve"]: + return super(BarbicanAction, cls)._get_client_method(client) + + method = getattr(cls, cls.client_method_name) + + @functools.wraps(method) + def wrap(*args, **kwargs): + return method(client, *args, **kwargs) + + arguments = inspect_utils.get_arg_list_as_str(method) + + # Remove client. + wrap.__arguments__ = arguments.split(', ', 1)[1] + + return wrap + + @staticmethod + def secrets_store(client, + name=None, + payload=None, + algorithm=None, + bit_length=None, + secret_type=None, + mode=None, expiration=None): + """Create and Store a secret in Barbican. + + :param client: the Barbican client + :type client: barbicanclient.client + + :param name: A friendly name for the Secret + :type name: string + + :param payload: The unencrypted secret data + :type payload: string + + :param algorithm: The algorithm associated with this secret key + :type algorithm: string + + :param bit_length: The bit length of this secret key + :type bit_length: int + + :param secret_type: The secret type for this secret key + :type secret_type: string + + :param mode: The algorithm mode used with this secret keybit_length: + :type mode: string + + :param expiration: The expiration time of the secret in ISO 8601 format + :type expiration: string + + :returns: A new Secret object + :rtype: class:`barbicanclient.secrets.Secret' + """ + + entity = client.secrets.create( + name, + payload, + algorithm, + bit_length, + secret_type, + mode, + expiration + ) + + entity.store() + + return entity._get_formatted_entity() + + @staticmethod + def secrets_retrieve(client, secret_ref): + """Retrieve the payload from a secret in Barbican. + + :param client: the Barbican client + :type client: barbicanclient.client + + :param secret_ref: Full HATEOAS reference to a Secret + :type secret_ref: string + """ + + return client.secrets.get(secret_ref).payload + + +class DesignateAction(base.OpenStackAction): + _service_type = 'dns' + + @classmethod + def _get_client_class(cls): + return designateclient.Client + + def _create_client(self, context): + LOG.debug("Designate action security context: %s", context) + + session_and_auth = self.get_session_and_auth(context) + + client = self._get_client_class()( + session=session_and_auth['session'] + ) + + return client + + @classmethod + def _get_fake_client(cls): + session = keystone_utils.get_admin_session() + return cls._get_client_class()( + endpoint_override="http://127.0.0.1:9001/", + session=session + ) + + +class MagnumAction(base.OpenStackAction): + + @classmethod + def _get_client_class(cls): + return magnumclient.Client + + def _create_client(self, context): + + LOG.debug("Magnum action security context: %s", context) + + keystone_endpoint = keystone_utils.get_keystone_endpoint() + auth_url = keystone_endpoint.url + magnum_url = keystone_utils.get_endpoint_for_project('magnum').url + + return self._get_client_class()( + magnum_url=magnum_url, + auth_token=context.auth_token, + project_id=context.project_id, + auth_url=auth_url, + insecure=context.insecure + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()(auth_url='X', magnum_url='X') + + +class MuranoAction(base.OpenStackAction): + _service_name = 'murano' + + @classmethod + def _get_client_class(cls): + return muranoclient.Client + + def _create_client(self, context): + + LOG.debug("Murano action security context: %s", context) + + keystone_endpoint = keystone_utils.get_keystone_endpoint() + murano_endpoint = self.get_service_endpoint() + + return self._get_client_class()( + endpoint=murano_endpoint.url, + token=context.auth_token, + tenant=context.project_id, + region_name=murano_endpoint.region, + auth_url=keystone_endpoint.url, + insecure=context.insecure + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()("http://127.0.0.1:8082/") + + +class TackerAction(base.OpenStackAction): + _service_name = 'tacker' + + @classmethod + def _get_client_class(cls): + return tackerclient.Client + + def _create_client(self, context): + + LOG.debug("Tacker action security context: %s", context) + + keystone_endpoint = keystone_utils.get_keystone_endpoint() + tacker_endpoint = self.get_service_endpoint() + + return self._get_client_class()( + endpoint_url=tacker_endpoint.url, + token=context.auth_token, + tenant_id=context.project_id, + region_name=tacker_endpoint.region, + auth_url=keystone_endpoint.url, + insecure=context.insecure + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()() + + +class SenlinAction(base.OpenStackAction): + _service_name = 'senlin' + + @classmethod + def _get_client_class(cls): + return senlinclient.Client + + def _create_client(self, context): + + LOG.debug("Senlin action security context: %s", context) + + keystone_endpoint = keystone_utils.get_keystone_endpoint() + senlin_endpoint = self.get_service_endpoint() + + if context.is_trust_scoped and keystone_utils.is_token_trust_scoped( + context.auth_token): + if context.trust_id is None: + raise Exception( + "'trust_id' must be provided in the admin context." + ) + + auth = ks_identity_v3.Password( + auth_url=keystone_endpoint.url, + trust_id=context.trust_id, + username=CONF.keystone_authtoken.username, + password=CONF.keystone_authtoken.password, + user_domain_name=CONF.keystone_authtoken.user_domain_name + ) + else: + auth = ks_identity_v3.Token( + auth_url=keystone_endpoint.url, + token=context.auth_token, + project_id=context.project_id + ) + + return self._get_client_class()( + endpoint_url=senlin_endpoint.url, + session=ks_session.Session(auth=auth), + tenant_id=context.project_id, + region_name=senlin_endpoint.region, + auth_url=keystone_endpoint.url, + insecure=context.insecure + ) + + @classmethod + def _get_fake_client(cls): + # Senlin client changed interface a bit, let's skip __init__ altogether + class_ = cls._get_client_class() + return class_.__new__(class_) + + +class AodhAction(base.OpenStackAction): + _service_type = 'alarming' + + @classmethod + def _get_client_class(cls): + return aodhclient.Client + + def _create_client(self, context): + + LOG.debug("Aodh action security context: %s", context) + + aodh_endpoint = self.get_service_endpoint() + + endpoint_url = keystone_utils.format_url( + aodh_endpoint.url, + {'tenant_id': context.project_id} + ) + + return self._get_client_class()( + endpoint_url, + region_name=aodh_endpoint.region, + token=context.auth_token, + username=context.user_name, + insecure=context.insecure + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()() + + +class GnocchiAction(base.OpenStackAction): + _service_type = 'metric' + + @classmethod + def _get_client_class(cls): + return gnocchiclient.Client + + def _create_client(self, context): + LOG.debug("Gnocchi action security context: %s", context) + + gnocchi_endpoint = keystone_utils.get_endpoint_for_project( + service_type="metric") + keystone_endpoint = keystone_utils.get_keystone_endpoint() + + auth = ks_identity_v3.Token( + auth_url=keystone_endpoint.url, + tenant_name=context.user_name, + token=context.auth_token, + tenant_id=context.project_id + ) + + return self._get_client_class()( + adapter_options={"region_name": gnocchi_endpoint.region, + "project_id": context.project_id, + "endpoint": gnocchi_endpoint.url, + "insecure": context.insecure}, + session_options={"auth": auth} + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()() + + +class GlareAction(base.OpenStackAction): + _service_name = 'glare' + + @classmethod + def _get_client_class(cls): + return glareclient.Client + + def _create_client(self, context): + + LOG.debug("Glare action security context: %s", context) + + glare_endpoint = self.get_service_endpoint() + + endpoint_url = keystone_utils.format_url( + glare_endpoint.url, + {'tenant_id': context.project_id} + ) + + return self._get_client_class()( + endpoint_url, + **self.get_session_and_auth(context) + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()("http://127.0.0.1:9494/") + + +class VitrageAction(base.OpenStackAction): + _service_type = 'rca' + + @classmethod + def _get_client_class(cls): + return vitrageclient.Client + + def _create_client(self, context): + + LOG.debug("Vitrage action security context: %s", context) + + vitrage_endpoint = self.get_service_endpoint() + + endpoint_url = keystone_utils.format_url( + vitrage_endpoint.url, + {'tenant_id': context.project_id} + ) + + session_and_auth = self.get_session_and_auth(context) + + return vitrageclient.Client( + session=session_and_auth['session'], + endpoint_override=endpoint_url + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()() + + +class ZunAction(base.OpenStackAction): + _service_name = 'appcontainer' + + @classmethod + def _get_client_class(cls): + return zunclient.Client + + def _create_client(self, context): + + LOG.debug("Zun action security context: %s", context) + + keystone_endpoint = keystone_utils.get_keystone_endpoint() + zun_endpoint = self.get_service_endpoint() + session_and_auth = self.get_session_and_auth(context) + + temp_client = self._get_client_class()( + zun_api_versions.MAX_API_VERSION, + endpoint_override=zun_endpoint.url, + auth_url=keystone_endpoint.url, + session=session_and_auth['auth'] + ) + + discovered_version = zun_api_versions.discover_version( + temp_client, + zun_api_versions.APIVersion(zun_api_versions.MAX_API_VERSION) + ) + + return self._get_client_class()( + discovered_version, + endpoint_override=zun_endpoint.url, + auth_url=keystone_endpoint.url, + session=session_and_auth['session'] + ) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + endpoint_override="http://127.0.0.1:9517/", + session=ks_session.Session() + ) + + +class QinlingAction(base.OpenStackAction): + _service_type = 'function-engine' + + @classmethod + def _get_client_class(cls): + return qinlingclient.Client + + def _create_client(self, context): + qinling_endpoint = self.get_service_endpoint() + session_and_auth = self.get_session_and_auth(context) + + return self._get_client_class()(endpoint_override=qinling_endpoint.url, + session=session_and_auth['session']) + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + endpoint_override="http://127.0.0.1:7070/", + session=ks_session.Session() + ) + + +class ManilaAction(base.OpenStackAction): + _service_type = 'sharev2' + + @classmethod + def _get_client_class(cls): + return manilaclient.Client + + def _create_client(self, context): + + LOG.debug("Manila action security context: %s", context) + + manila_endpoint = self.get_service_endpoint() + + session_and_auth = self.get_session_and_auth(context) + + temp_client = self._get_client_class()( + manila.API_MAX_VERSION, + service_catalog_url=manila_endpoint.url, + session=session_and_auth['auth'] + ) + + discovered_version = manila_api_versions.discover_version( + temp_client, + manila.API_MAX_VERSION + ) + + client = self._get_client_class()( + discovered_version, + service_catalog_url=manila_endpoint.url, + session=session_and_auth['session'] + ) + + return client + + @classmethod + def _get_fake_client(cls): + return cls._get_client_class()( + manila.API_MAX_VERSION, + input_auth_token='token', + service_catalog_url='http://127.0.0.1:8786') diff --git a/mistral_extra/actions/openstack/base.py b/mistral_extra/actions/openstack/base.py new file mode 100644 index 0000000..bee79dc --- /dev/null +++ b/mistral_extra/actions/openstack/base.py @@ -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)) + ) diff --git a/mistral_extra/actions/openstack/mapping.json b/mistral_extra/actions/openstack/mapping.json new file mode 100644 index 0000000..f5028d3 --- /dev/null +++ b/mistral_extra/actions/openstack/mapping.json @@ -0,0 +1,1505 @@ +{ + "_comment": "Mapping OpenStack action namespaces to all its actions. Each action name is mapped to python-client method name in this namespace.", + "nova": { + "_comment": "It uses novaclient.v2.", + "agents_convert_into_with_meta": "agents.convert_into_with_meta", + "agents_create": "agents.create", + "agents_delete": "agents.delete", + "agents_find": "agents.find", + "agents_findall": "agents.findall", + "agents_list": "agents.list", + "agents_update": "agents.update", + "aggregates_add_host": "aggregates.add_host", + "aggregates_convert_into_with_meta": "aggregates.convert_into_with_meta", + "aggregates_create": "aggregates.create", + "aggregates_delete": "aggregates.delete", + "aggregates_find": "aggregates.find", + "aggregates_findall": "aggregates.findall", + "aggregates_get": "aggregates.get", + "aggregates_get_details": "aggregates.get_details", + "aggregates_list": "aggregates.list", + "aggregates_remove_host": "aggregates.remove_host", + "aggregates_set_metadata": "aggregates.set_metadata", + "aggregates_update": "aggregates.update", + "availability_zones_convert_into_with_meta": "availability_zones.convert_into_with_meta", + "availability_zones_find": "availability_zones.find", + "availability_zones_findall": "availability_zones.findall", + "availability_zones_list": "availability_zones.list", + "flavor_access_add_tenant_access": "flavor_access.add_tenant_access", + "flavor_access_convert_into_with_meta": "flavor_access.convert_into_with_meta", + "flavor_access_find": "flavor_access.find", + "flavor_access_findall": "flavor_access.findall", + "flavor_access_list": "flavor_access.list", + "flavor_access_remove_tenant_access": "flavor_access.remove_tenant_access", + "flavors_convert_into_with_meta": "flavors.convert_into_with_meta", + "flavors_create": "flavors.create", + "flavors_delete": "flavors.delete", + "flavors_find": "flavors.find", + "flavors_findall": "flavors.findall", + "flavors_get": "flavors.get", + "flavors_list": "flavors.list", + "hypervisor_stats_convert_into_with_meta": "hypervisor_stats.convert_into_with_meta", + "hypervisor_stats_statistics": "hypervisor_stats.statistics", + "hypervisors_convert_into_with_meta": "hypervisors.convert_into_with_meta", + "hypervisors_find": "hypervisors.find", + "hypervisors_findall": "hypervisors.findall", + "hypervisors_get": "hypervisors.get", + "hypervisors_list": "hypervisors.list", + "hypervisors_search": "hypervisors.search", + "hypervisors_statistics": "hypervisors.statistics", + "hypervisors_uptime": "hypervisors.uptime", + "glance_find_image": "glance.find_image", + "glance_list": "glance.list", + "keypairs_convert_into_with_meta": "keypairs.convert_into_with_meta", + "keypairs_create": "keypairs.create", + "keypairs_delete": "keypairs.delete", + "keypairs_find": "keypairs.find", + "keypairs_findall": "keypairs.findall", + "keypairs_get": "keypairs.get", + "keypairs_list": "keypairs.list", + "limits_convert_into_with_meta": "limits.convert_into_with_meta", + "limits_get": "limits.get", + "neutron_find_network": "neutron.find_network", + "quota_classes_convert_into_with_meta": "quota_classes.convert_into_with_meta", + "quota_classes_get": "quota_classes.get", + "quota_classes_update": "quota_classes.update", + "quotas_convert_into_with_meta": "quotas.convert_into_with_meta", + "quotas_defaults": "quotas.defaults", + "quotas_delete": "quotas.delete", + "quotas_get": "quotas.get", + "quotas_update": "quotas.update", + "server_groups_convert_into_with_meta": "server_groups.convert_into_with_meta", + "server_groups_create": "server_groups.create", + "server_groups_delete": "server_groups.delete", + "server_groups_find": "server_groups.find", + "server_groups_findall": "server_groups.findall", + "server_groups_get": "server_groups.get", + "server_groups_list": "server_groups.list", + "server_migrations_convert_into_with_meta": "server_migrations.convert_into_with_meta", + "server_migrations_find": "server_migrations.find", + "server_migrations_findall": "server_migrations.findall", + "server_migrations_get": "server_migrations.get", + "server_migrations_list": "server_migrations.list", + "server_migrations_live_migrate_force_complete": "server_migrations.live_migrate_force_complete", + "server_migrations_live_migration_abort": "server_migrations.live_migration_abort", + "servers_add_security_group": "servers.add_security_group", + "servers_backup": "servers.backup", + "servers_change_password": "servers.change_password", + "servers_clear_password": "servers.clear_password", + "servers_confirm_resize": "servers.confirm_resize", + "servers_convert_into_with_meta": "servers.convert_into_with_meta", + "servers_create": "servers.create", + "servers_create_image": "servers.create_image", + "servers_delete": "servers.delete", + "servers_delete_meta": "servers.delete_meta", + "servers_diagnostics": "servers.diagnostics", + "servers_evacuate": "servers.evacuate", + "servers_find": "servers.find", + "servers_findall": "servers.findall", + "servers_force_delete": "servers.force_delete", + "servers_get": "servers.get", + "servers_get_console_output": "servers.get_console_output", + "servers_get_mks_console": "servers.get_mks_console", + "servers_get_password": "servers.get_password", + "servers_get_rdp_console": "servers.get_rdp_console", + "servers_get_serial_console": "servers.get_serial_console", + "servers_get_spice_console": "servers.get_spice_console", + "servers_get_vnc_console": "servers.get_vnc_console", + "servers_interface_attach": "servers.interface_attach", + "servers_interface_detach": "servers.interface_detach", + "servers_interface_list": "servers.interface_list", + "servers_ips": "servers.ips", + "servers_list": "servers.list", + "servers_list_security_group": "servers.list_security_group", + "servers_live_migrate": "servers.live_migrate", + "servers_lock": "servers.lock", + "servers_migrate": "servers.migrate", + "servers_pause": "servers.pause", + "servers_reboot": "servers.reboot", + "servers_rebuild": "servers.rebuild", + "servers_remove_security_group": "servers.remove_security_group", + "servers_rescue": "servers.rescue", + "servers_reset_network": "servers.reset_network", + "servers_reset_state": "servers.reset_state", + "servers_resize": "servers.resize", + "servers_restore": "servers.restore", + "servers_resume": "servers.resume", + "servers_revert_resize": "servers.revert_resize", + "servers_set_meta": "servers.set_meta", + "servers_set_meta_item": "servers.set_meta_item", + "servers_shelve": "servers.shelve", + "servers_shelve_offload": "servers.shelve_offload", + "servers_start": "servers.start", + "servers_stop": "servers.stop", + "servers_suspend": "servers.suspend", + "servers_trigger_crash_dump": "servers.trigger_crash_dump", + "servers_unlock": "servers.unlock", + "servers_unpause": "servers.unpause", + "servers_unrescue": "servers.unrescue", + "servers_unshelve": "servers.unshelve", + "servers_update": "servers.update", + "services_convert_into_with_meta": "services.convert_into_with_meta", + "services_delete": "services.delete", + "services_disable": "services.disable", + "services_disable_log_reason": "services.disable_log_reason", + "services_enable": "services.enable", + "services_find": "services.find", + "services_findall": "services.findall", + "services_force_down": "services.force_down", + "services_list": "services.list", + "usage_convert_into_with_meta": "usage.convert_into_with_meta", + "usage_find": "usage.find", + "usage_findall": "usage.findall", + "usage_get": "usage.get", + "usage_list": "usage.list", + "versions_convert_into_with_meta": "versions.convert_into_with_meta", + "versions_find": "versions.find", + "versions_findall": "versions.findall", + "versions_get_current": "versions.get_current", + "versions_list": "versions.list", + "volumes_convert_into_with_meta": "volumes.convert_into_with_meta", + "volumes_create_server_volume": "volumes.create_server_volume", + "volumes_delete_server_volume": "volumes.delete_server_volume", + "volumes_get_server_volume": "volumes.get_server_volume", + "volumes_get_server_volumes": "volumes.get_server_volumes", + "volumes_update_server_volume": "volumes.update_server_volume" + }, + "glance": { + "_comment": "It uses glanceclient.v2.", + "image_members_create": "image_members.create", + "image_members_delete": "image_members.delete", + "image_members_list": "image_members.list", + "image_members_update": "image_members.update", + "image_tags_delete": "image_tags.delete", + "image_tags_update": "image_tags.update", + "images_add_location": "images.add_location", + "images_create": "images.create", + "images_data": "images.data", + "images_deactivate": "images.deactivate", + "images_delete": "images.delete", + "images_delete_locations": "images.delete_locations", + "images_get": "images.get", + "images_list": "images.list", + "images_reactivate": "images.reactivate", + "images_update": "images.update", + "images_update_location": "images.update_location", + "images_upload": "images.upload", + "schemas_get": "schemas.get", + "tasks_create": "tasks.create", + "tasks_get": "tasks.get", + "tasks_list": "tasks.list", + "metadefs_resource_type_associate": "metadefs_resource_type.associate", + "metadefs_resource_type_deassociate": "metadefs_resource_type.deassociate", + "metadefs_resource_type_get": "metadefs_resource_type.get", + "metadefs_resource_type_list": "metadefs_resource_type.list", + "metadefs_property_create": "metadefs_property.create", + "metadefs_property_delete": "metadefs_property.delete", + "metadefs_property_delete_all": "metadefs_property.delete_all", + "metadefs_property_get": "metadefs_property.get", + "metadefs_property_list": "metadefs_property.list", + "metadefs_property_update": "metadefs_property.update", + "metadefs_object_create": "metadefs_object.create", + "metadefs_object_delete": "metadefs_object.delete", + "metadefs_object_delete_all": "metadefs_object.delete_all", + "metadefs_object_get": "metadefs_object.get", + "metadefs_object_list": "metadefs_object.list", + "metadefs_object_update": "metadefs_object.update", + "metadefs_tag_create": "metadefs_tag.create", + "metadefs_tag_create_multiple": "metadefs_tag.create_multiple", + "metadefs_tag_delete": "metadefs_tag.delete", + "metadefs_tag_delete_all": "metadefs_tag.delete_all", + "metadefs_tag_get": "metadefs_tag.get", + "metadefs_tag_list": "metadefs_tag.list", + "metadefs_tag_update": "metadefs_tag.update", + "metadefs_namespace_create": "metadefs_namespace.create", + "metadefs_namespace_delete": "metadefs_namespace.delete", + "metadefs_namespace_get": "metadefs_namespace.get", + "metadefs_namespace_list": "metadefs_namespace.list", + "metadefs_namespace_update": "metadefs_namespace.update", + "versions_list": "versions.list" + }, + "keystone": { + "_comment": "It uses keystoneclient.v3.", + "credentials_create": "credentials.create", + "credentials_delete": "credentials.delete", + "credentials_find": "credentials.find", + "credentials_get": "credentials.get", + "credentials_list": "credentials.list", + "credentials_update": "credentials.update", + "domains_create": "domains.create", + "domains_delete": "domains.delete", + "domains_find": "domains.find", + "domains_get": "domains.get", + "domains_list": "domains.list", + "domains_update": "domains.update", + "endpoint_filter_add_endpoint_to_project": "endpoint_filter.add_endpoint_to_project", + "endpoint_filter_check_endpoint_in_project": "endpoint_filter.check_endpoint_in_project", + "endpoint_filter_delete_endpoint_from_project": "endpoint_filter.delete_endpoint_from_project", + "endpoint_filter_list_endpoints_for_project": "endpoint_filter.list_endpoints_for_project", + "endpoint_filter_list_projects_for_endpoint": "endpoint_filter.list_projects_for_endpoint", + "endpoint_policy_check_policy_association_for_endpoint": "endpoint_policy.check_policy_association_for_endpoint", + "endpoint_policy_check_policy_association_for_region_and_service": "endpoint_policy.check_policy_association_for_region_and_service", + "endpoint_policy_check_policy_association_for_service": "endpoint_policy.check_policy_association_for_service", + "endpoint_policy_create_policy_association_for_endpoint": "endpoint_policy.create_policy_association_for_endpoint", + "endpoint_policy_create_policy_association_for_region_and_service": "endpoint_policy.create_policy_association_for_region_and_service", + "endpoint_policy_create_policy_association_for_service": "endpoint_policy.create_policy_association_for_service", + "endpoint_policy_delete_policy_association_for_endpoint": "endpoint_policy.delete_policy_association_for_endpoint", + "endpoint_policy_delete_policy_association_for_region_and_service": "endpoint_policy.delete_policy_association_for_region_and_service", + "endpoint_policy_delete_policy_association_for_service": "endpoint_policy.delete_policy_association_for_service", + "endpoint_policy_get_policy_for_endpoint": "endpoint_policy.get_policy_for_endpoint", + "endpoint_policy_list_endpoints_for_policy": "endpoint_policy.list_endpoints_for_policy", + "endpoints_create": "endpoints.create", + "endpoints_delete": "endpoints.delete", + "endpoints_find": "endpoints.find", + "endpoints_get": "endpoints.get", + "endpoints_list": "endpoints.list", + "endpoints_update": "endpoints.update", + "groups_create": "groups.create", + "groups_delete": "groups.delete", + "groups_find": "groups.find", + "groups_get": "groups.get", + "groups_list": "groups.list", + "groups_update": "groups.update", + "oauth1.consumers_build_url": "oauth1.consumers.build_url", + "oauth1.consumers_create": "oauth1.consumers.create", + "oauth1.consumers_delete": "oauth1.consumers.delete", + "oauth1.consumers_find": "oauth1.consumers.find", + "oauth1.consumers_get": "oauth1.consumers.get", + "oauth1.consumers_list": "oauth1.consumers.list", + "oauth1.consumers_put": "oauth1.consumers.put", + "oauth1.consumers_update": "oauth1.consumers.update", + "oauth1.request_tokens_authorize": "oauth1.request_tokens.authorize", + "oauth1.request_tokens_build_url": "oauth1.request_tokens.build_url", + "oauth1.request_tokens_create": "oauth1.request_tokens.create", + "oauth1.request_tokens_delete": "oauth1.request_tokens.delete", + "oauth1.request_tokens_find": "oauth1.request_tokens.find", + "oauth1.request_tokens_get": "oauth1.request_tokens.get", + "oauth1.request_tokens_list": "oauth1.request_tokens.list", + "oauth1.request_tokens_put": "oauth1.request_tokens.put", + "oauth1.request_tokens_update": "oauth1.request_tokens.update", + "oauth1.access_tokens_build_url": "oauth1.access_tokens.build_url", + "oauth1.access_tokens_create": "oauth1.access_tokens.create", + "oauth1.access_tokens_delete": "oauth1.access_tokens.delete", + "oauth1.access_tokens_find": "oauth1.access_tokens.find", + "oauth1.access_tokens_get": "oauth1.access_tokens.get", + "oauth1.access_tokens_list": "oauth1.access_tokens.list", + "oauth1.access_tokens_put": "oauth1.access_tokens.put", + "oauth1.access_tokens_update": "oauth1.access_tokens.update", + "policies_create": "policies.create", + "policies_delete": "policies.delete", + "policies_find": "policies.find", + "policies_get": "policies.get", + "policies_list": "policies.list", + "policies_update": "policies.update", + "projects_create": "projects.create", + "projects_delete": "projects.delete", + "projects_find": "projects.find", + "projects_get": "projects.get", + "projects_list": "projects.list", + "projects_update": "projects.update", + "regions_create": "regions.create", + "regions_delete": "regions.delete", + "regions_find": "regions.find", + "regions_get": "regions.get", + "regions_list": "regions.list", + "regions_update": "regions.update", + "role_assignments_create": "role_assignments.create", + "role_assignments_delete": "role_assignments.delete", + "role_assignments_find": "role_assignments.find", + "role_assignments_get": "role_assignments.get", + "role_assignments_list": "role_assignments.list", + "role_assignments_update": "role_assignments.update", + "roles_check": "roles.check", + "roles_create": "roles.create", + "roles_delete": "roles.delete", + "roles_find": "roles.find", + "roles_get": "roles.get", + "roles_grant": "roles.grant", + "roles_list": "roles.list", + "roles_revoke": "roles.revoke", + "roles_update": "roles.update", + "services_create": "services.create", + "services_delete": "services.delete", + "services_find": "services.find", + "services_get": "services.get", + "services_list": "services.list", + "services_update": "services.update", + "trusts_create": "trusts.create", + "trusts_delete": "trusts.delete", + "trusts_find": "trusts.find", + "trusts_get": "trusts.get", + "trusts_list": "trusts.list", + "trusts_update": "trusts.update", + "users_add_to_group": "users.add_to_group", + "users_check_in_group": "users.check_in_group", + "users_create": "users.create", + "users_delete": "users.delete", + "users_find": "users.find", + "users_get": "users.get", + "users_list": "users.list", + "users_remove_from_group": "users.remove_from_group", + "users_update": "users.update", + "users_update_password": "users.update_password" + }, + "heat": { + "_comment": "It uses heatclient.v1.", + "actions_cancel_update": "actions.cancel_update", + "actions_check": "actions.check", + "actions_resume": "actions.resume", + "actions_suspend": "actions.suspend", + "build_info_build_info": "build_info.build_info", + "events_get": "events.get", + "events_list": "events.list", + "resource_types_generate_template": "resource_types.generate_template", + "resource_types_get": "resource_types.get", + "resource_types_list": "resource_types.list", + "resources_generate_template": "resources.generate_template", + "resources_get": "resources.get", + "resources_list": "resources.list", + "resources_mark_unhealthy": "resources.mark_unhealthy", + "resources_metadata": "resources.metadata", + "resources_signal": "resources.signal", + "services_list": "services.list", + "software_configs_create": "software_configs.create", + "software_configs_delete": "software_configs.delete", + "software_configs_get": "software_configs.get", + "software_configs_list": "software_configs.list", + "software_deployments_create": "software_deployments.create", + "software_deployments_delete": "software_deployments.delete", + "software_deployments_get": "software_deployments.get", + "software_deployments_list": "software_deployments.list", + "software_deployments_metadata": "software_deployments.metadata", + "software_deployments_update": "software_deployments.update", + "stacks_abandon": "stacks.abandon", + "stacks_create": "stacks.create", + "stacks_delete": "stacks.delete", + "stacks_environment": "stacks.environment", + "stacks_get": "stacks.get", + "stacks_list": "stacks.list", + "stacks_output_list": "stacks.output_list", + "stacks_output_show": "stacks.output_show", + "stacks_preview": "stacks.preview", + "stacks_preview_update": "stacks.preview_update", + "stacks_restore": "stacks.restore", + "stacks_snapshot": "stacks.snapshot", + "stacks_snapshot_delete": "stacks.snapshot_delete", + "stacks_snapshot_list": "stacks.snapshot_list", + "stacks_snapshot_show": "stacks.snapshot_show", + "stacks_template": "stacks.template", + "stacks_update": "stacks.update", + "stacks_validate": "stacks.validate", + "template_versions_get": "template_versions.get", + "template_versions_list": "template_versions.list" + }, + "aodh": { + "_comment": "It uses aodhclient.v2.", + "capabilities_list": "capabilities.list", + "alarm_create": "alarm.create", + "alarm_delete": "alarm.delete", + "alarm_get": "alarm.get", + "alarm_get_state": "alarm.get_state", + "alarm_list": "alarm.list", + "alarm_set_state": "alarm.set_state", + "alarm_update": "alarm.update", + "alarm_query": "alarm.query", + "alarm_history_get": "alarm_history.get", + "alarm_history_search": "alarm_history.search" + }, + "gnocchi":{ + "_comment": "It uses gnocchiclient.v1.", + "archive_policy_create": "archive_policy.create", + "archive_policy_delete": "archive_policy.delete", + "archive_policy_get": "archive_policy.get", + "archive_policy_list": "archive_policy.list", + "archive_policy_update": "archive_policy.update", + "archive_policy_rule_create": "archive_policy_rule.create", + "archive_policy_rule_delete": "archive_policy_rule.delete", + "archive_policy_rule_get": "archive_policy_rule.get", + "archive_policy_rule_list": "archive_policy_rule.list", + "capabilities_list": "capabilities.list", + "measures_add": "metric.add_measures", + "metric_batch_metrics_measures": "metric.batch_metrics_measures", + "metric_batch_resources_metrics_measures": "metric.batch_resources_metrics_measures", + "metric_create": "metric.create", + "metric_delete": "metric.delete", + "metric_get": "metric.get", + "measures_get": "metric.get_measures", + "metric_list": "metric.list", + "resource_batch_delete_resource": "resource.batch_delete", + "resource_create": "resource.create", + "resource_delete": "resource.delete", + "resource_get": "resource.get", + "resource_history": "resource.history", + "resource_list": "resource.list", + "resource_search": "resource.search", + "resource_update": "resource.update", + "resource_type_create": "resource_type.create", + "resource_type_delete": "resource_type.delete", + "resource_type_get": "resource_type.get", + "resource_type_list": "resource_type.list", + "resource_type_update": "resource_type.update", + "measures_aggregation": "metric.aggregation", + "status": "status.get" + }, + "neutron": { + "_comment": "It uses neutronclient.v2_0.", + "add_gateway_router": "add_gateway_router", + "add_interface_router": "add_interface_router", + "add_network_to_dhcp_agent": "add_network_to_dhcp_agent", + "add_router_to_l3_agent": "add_router_to_l3_agent", + "associate_health_monitor": "associate_health_monitor", + "connect_network_gateway": "connect_network_gateway", + "create_ext": "create_ext", + "create_firewall": "create_firewall", + "create_firewall_policy": "create_firewall_policy", + "create_firewall_rule": "create_firewall_rule", + "create_floatingip": "create_floatingip", + "create_gateway_device": "create_gateway_device", + "create_health_monitor": "create_health_monitor", + "create_ikepolicy": "create_ikepolicy", + "create_ipsec_site_connection": "create_ipsec_site_connection", + "create_ipsecpolicy": "create_ipsecpolicy", + "create_lbaas_healthmonitor": "create_lbaas_healthmonitor", + "create_lbaas_member": "create_lbaas_member", + "create_lbaas_pool": "create_lbaas_pool", + "create_listener": "create_listener", + "create_loadbalancer": "create_loadbalancer", + "create_member": "create_member", + "create_metering_label": "create_metering_label", + "create_metering_label_rule": "create_metering_label_rule", + "create_network": "create_network", + "create_network_gateway": "create_network_gateway", + "create_pool": "create_pool", + "create_port": "create_port", + "create_qos_queue": "create_qos_queue", + "create_router": "create_router", + "create_security_group": "create_security_group", + "create_security_group_rule": "create_security_group_rule", + "create_subnet": "create_subnet", + "create_subnetpool": "create_subnetpool", + "create_vip": "create_vip", + "create_vpnservice": "create_vpnservice", + "delete_agent": "delete_agent", + "delete_ext": "delete_ext", + "delete_firewall": "delete_firewall", + "delete_firewall_policy": "delete_firewall_policy", + "delete_firewall_rule": "delete_firewall_rule", + "delete_floatingip": "delete_floatingip", + "delete_gateway_device": "delete_gateway_device", + "delete_health_monitor": "delete_health_monitor", + "delete_ikepolicy": "delete_ikepolicy", + "delete_ipsec_site_connection": "delete_ipsec_site_connection", + "delete_ipsecpolicy": "delete_ipsecpolicy", + "delete_lbaas_healthmonitor": "delete_lbaas_healthmonitor", + "delete_lbaas_member": "delete_lbaas_member", + "delete_lbaas_pool": "delete_lbaas_pool", + "delete_listener": "delete_listener", + "delete_loadbalancer": "delete_loadbalancer", + "delete_member": "delete_member", + "delete_metering_label": "delete_metering_label", + "delete_metering_label_rule": "delete_metering_label_rule", + "delete_network": "delete_network", + "delete_network_gateway": "delete_network_gateway", + "delete_pool": "delete_pool", + "delete_port": "delete_port", + "delete_qos_queue": "delete_qos_queue", + "delete_quota": "delete_quota", + "delete_router": "delete_router", + "delete_security_group": "delete_security_group", + "delete_security_group_rule": "delete_security_group_rule", + "delete_subnet": "delete_subnet", + "delete_subnetpool": "delete_subnetpool", + "delete_vip": "delete_vip", + "delete_vpnservice": "delete_vpnservice", + "disassociate_health_monitor": "disassociate_health_monitor", + "disconnect_network_gateway": "disconnect_network_gateway", + "extend_create": "extend_create", + "extend_delete": "extend_delete", + "extend_list": "extend_list", + "extend_show": "extend_show", + "extend_update": "extend_update", + "firewall_policy_insert_rule": "firewall_policy_insert_rule", + "firewall_policy_remove_rule": "firewall_policy_remove_rule", + "get_lbaas_agent_hosting_loadbalancer": "get_lbaas_agent_hosting_loadbalancer", + "get_lbaas_agent_hosting_pool": "get_lbaas_agent_hosting_pool", + "get_quotas_tenant": "get_quotas_tenant", + "list_agents": "list_agents", + "list_dhcp_agent_hosting_networks": "list_dhcp_agent_hosting_networks", + "list_ext": "list_ext", + "list_extensions": "list_extensions", + "list_firewall_policies": "list_firewall_policies", + "list_firewall_rules": "list_firewall_rules", + "list_firewalls": "list_firewalls", + "list_floatingips": "list_floatingips", + "list_gateway_devices": "list_gateway_devices", + "list_health_monitors": "list_health_monitors", + "list_ikepolicies": "list_ikepolicies", + "list_ipsec_site_connections": "list_ipsec_site_connections", + "list_ipsecpolicies": "list_ipsecpolicies", + "list_l3_agent_hosting_routers": "list_l3_agent_hosting_routers", + "list_lbaas_healthmonitors": "list_lbaas_healthmonitors", + "list_lbaas_loadbalancers": "list_lbaas_loadbalancers", + "list_lbaas_members": "list_lbaas_members", + "list_lbaas_pools": "list_lbaas_pools", + "list_listeners": "list_listeners", + "list_loadbalancers": "list_loadbalancers", + "list_loadbalancers_on_lbaas_agent": "list_loadbalancers_on_lbaas_agent", + "list_members": "list_members", + "list_metering_label_rules": "list_metering_label_rules", + "list_metering_labels": "list_metering_labels", + "list_network_gateways": "list_network_gateways", + "list_networks": "list_networks", + "list_networks_on_dhcp_agent": "list_networks_on_dhcp_agent", + "list_pools": "list_pools", + "list_pools_on_lbaas_agent": "list_pools_on_lbaas_agent", + "list_ports": "list_ports", + "list_qos_queues": "list_qos_queues", + "list_quotas": "list_quotas", + "list_routers": "list_routers", + "list_routers_on_l3_agent": "list_routers_on_l3_agent", + "list_security_group_rules": "list_security_group_rules", + "list_security_groups": "list_security_groups", + "list_service_providers": "list_service_providers", + "list_subnetpools": "list_subnetpools", + "list_subnets": "list_subnets", + "list_vips": "list_vips", + "list_vpnservices": "list_vpnservices", + "remove_gateway_router": "remove_gateway_router", + "remove_interface_router": "remove_interface_router", + "remove_network_from_dhcp_agent": "remove_network_from_dhcp_agent", + "remove_router_from_l3_agent": "remove_router_from_l3_agent", + "retrieve_pool_stats": "retrieve_pool_stats", + "show_agent": "show_agent", + "show_ext": "show_ext", + "show_extension": "show_extension", + "show_firewall": "show_firewall", + "show_firewall_policy": "show_firewall_policy", + "show_firewall_rule": "show_firewall_rule", + "show_floatingip": "show_floatingip", + "show_gateway_device": "show_gateway_device", + "show_health_monitor": "show_health_monitor", + "show_ikepolicy": "show_ikepolicy", + "show_ipsec_site_connection": "show_ipsec_site_connection", + "show_ipsecpolicy": "show_ipsecpolicy", + "show_lbaas_healthmonitor": "show_lbaas_healthmonitor", + "show_lbaas_member": "show_lbaas_member", + "show_lbaas_pool": "show_lbaas_pool", + "show_listener": "show_listener", + "show_loadbalancer": "show_loadbalancer", + "show_member": "show_member", + "show_metering_label": "show_metering_label", + "show_metering_label_rule": "show_metering_label_rule", + "show_network": "show_network", + "show_network_gateway": "show_network_gateway", + "show_pool": "show_pool", + "show_port": "show_port", + "show_qos_queue": "show_qos_queue", + "show_quota": "show_quota", + "show_router": "show_router", + "show_security_group": "show_security_group", + "show_security_group_rule": "show_security_group_rule", + "show_subnet": "show_subnet", + "show_subnetpool": "show_subnetpool", + "show_vip": "show_vip", + "show_vpnservice": "show_vpnservice", + "update_agent": "update_agent", + "update_ext": "update_ext", + "update_firewall": "update_firewall", + "update_firewall_policy": "update_firewall_policy", + "update_firewall_rule": "update_firewall_rule", + "update_floatingip": "update_floatingip", + "update_gateway_device": "update_gateway_device", + "update_health_monitor": "update_health_monitor", + "update_ikepolicy": "update_ikepolicy", + "update_ipsec_site_connection": "update_ipsec_site_connection", + "update_ipsecpolicy": "update_ipsecpolicy", + "update_lbaas_healthmonitor": "update_lbaas_healthmonitor", + "update_lbaas_member": "update_lbaas_member", + "update_lbaas_pool": "update_lbaas_pool", + "update_listener": "update_listener", + "update_loadbalancer": "update_loadbalancer", + "update_member": "update_member", + "update_network": "update_network", + "update_network_gateway": "update_network_gateway", + "update_pool": "update_pool", + "update_port": "update_port", + "update_quota": "update_quota", + "update_router": "update_router", + "update_security_group": "update_security_group", + "update_subnet": "update_subnet", + "update_subnetpool": "update_subnetpool", + "update_vip": "update_vip", + "update_vpnservice": "update_vpnservice" + }, + "cinder": { + "_comment": "It uses cinderclient.v3.", + "availability_zones_find": "availability_zones.find", + "availability_zones_findall": "availability_zones.findall", + "availability_zones_list": "availability_zones.list", + "backups_create": "backups.create", + "backups_delete": "backups.delete", + "backups_export_record": "backups.export_record", + "backups_find": "backups.find", + "backups_findall": "backups.findall", + "backups_get": "backups.get", + "backups_import_record": "backups.import_record", + "backups_list": "backups.list", + "backups_reset_state": "backups.reset_state", + "capabilities_get": "capabilities.get", + "cgsnapshots_create": "cgsnapshots.create", + "cgsnapshots_delete": "cgsnapshots.delete", + "cgsnapshots_find": "cgsnapshots.find", + "cgsnapshots_findall": "cgsnapshots.findall", + "cgsnapshots_get": "cgsnapshots.get", + "cgsnapshots_list": "cgsnapshots.list", + "cgsnapshots_update": "cgsnapshots.update", + "consistencygroups_create": "consistencygroups.create", + "consistencygroups_create_from_src": "consistencygroups.create_from_src", + "consistencygroups_delete": "consistencygroups.delete", + "consistencygroups_find": "consistencygroups.find", + "consistencygroups_findall": "consistencygroups.findall", + "consistencygroups_get": "consistencygroups.get", + "consistencygroups_list": "consistencygroups.list", + "consistencygroups_update": "consistencygroups.update", + "limits_get": "limits.get", + "pools_list": "pools.list", + "qos_specs_associate": "qos_specs.associate", + "qos_specs_create": "qos_specs.create", + "qos_specs_delete": "qos_specs.delete", + "qos_specs_disassociate": "qos_specs.disassociate", + "qos_specs_disassociate_all": "qos_specs.disassociate_all", + "qos_specs_find": "qos_specs.find", + "qos_specs_findall": "qos_specs.findall", + "qos_specs_get": "qos_specs.get", + "qos_specs_get_associations": "qos_specs.get_associations", + "qos_specs_list": "qos_specs.list", + "qos_specs_set_keys": "qos_specs.set_keys", + "qos_specs_unset_keys": "qos_specs.unset_keys", + "quota_classes_get": "quota_classes.get", + "quota_classes_update": "quota_classes.update", + "quotas_defaults": "quotas.defaults", + "quotas_delete": "quotas.delete", + "quotas_get": "quotas.get", + "quotas_update": "quotas.update", + "restores_restore": "restores.restore", + "services_disable": "services.disable", + "services_disable_log_reason": "services.disable_log_reason", + "services_enable": "services.enable", + "services_find": "services.find", + "services_findall": "services.findall", + "services_list": "services.list", + "transfers_accept": "transfers.accept", + "transfers_create": "transfers.create", + "transfers_delete": "transfers.delete", + "transfers_find": "transfers.find", + "transfers_findall": "transfers.findall", + "transfers_get": "transfers.get", + "transfers_list": "transfers.list", + "volume_encryption_types_create": "volume_encryption_types.create", + "volume_encryption_types_delete": "volume_encryption_types.delete", + "volume_encryption_types_find": "volume_encryption_types.find", + "volume_encryption_types_findall": "volume_encryption_types.findall", + "volume_encryption_types_get": "volume_encryption_types.get", + "volume_encryption_types_list": "volume_encryption_types.list", + "volume_encryption_types_update": "volume_encryption_types.update", + "volume_snapshots_create": "volume_snapshots.create", + "volume_snapshots_delete": "volume_snapshots.delete", + "volume_snapshots_delete_metadata": "volume_snapshots.delete_metadata", + "volume_snapshots_find": "volume_snapshots.find", + "volume_snapshots_findall": "volume_snapshots.findall", + "volume_snapshots_get": "volume_snapshots.get", + "volume_snapshots_list": "volume_snapshots.list", + "volume_snapshots_reset_state": "volume_snapshots.reset_state", + "volume_snapshots_set_metadata": "volume_snapshots.set_metadata", + "volume_snapshots_update": "volume_snapshots.update", + "volume_snapshots_update_all_metadata": "volume_snapshots.update_all_metadata", + "volume_snapshots_update_snapshot_status": "volume_snapshots.update_snapshot_status", + "volume_type_access_add_project_access": "volume_type_access.add_project_access", + "volume_type_access_find": "volume_type_access.find", + "volume_type_access_findall": "volume_type_access.findall", + "volume_type_access_list": "volume_type_access.list", + "volume_type_access_remove_project_access": "volume_type_access.remove_project_access", + "volume_types_create": "volume_types.create", + "volume_types_default": "volume_types.default", + "volume_types_delete": "volume_types.delete", + "volume_types_find": "volume_types.find", + "volume_types_findall": "volume_types.findall", + "volume_types_get": "volume_types.get", + "volume_types_list": "volume_types.list", + "volume_types_update": "volume_types.update", + "volumes_attach": "volumes.attach", + "volumes_begin_detaching": "volumes.begin_detaching", + "volumes_create": "volumes.create", + "volumes_delete": "volumes.delete", + "volumes_delete_image_metadata": "volumes.delete_image_metadata", + "volumes_delete_metadata": "volumes.delete_metadata", + "volumes_detach": "volumes.detach", + "volumes_extend": "volumes.extend", + "volumes_find": "volumes.find", + "volumes_findall": "volumes.findall", + "volumes_force_delete": "volumes.force_delete", + "volumes_get": "volumes.get", + "volumes_get_encryption_metadata": "volumes.get_encryption_metadata", + "volumes_get_pools": "volumes.get_pools", + "volumes_initialize_connection": "volumes.initialize_connection", + "volumes_list": "volumes.list", + "volumes_manage": "volumes.manage", + "volumes_migrate_volume": "volumes.migrate_volume", + "volumes_migrate_volume_completion": "volumes.migrate_volume_completion", + "volumes_reserve": "volumes.reserve", + "volumes_reset_state": "volumes.reset_state", + "volumes_retype": "volumes.retype", + "volumes_roll_detaching": "volumes.roll_detaching", + "volumes_set_bootable": "volumes.set_bootable", + "volumes_set_image_metadata": "volumes.set_image_metadata", + "volumes_set_metadata": "volumes.set_metadata", + "volumes_show_image_metadata": "volumes.show_image_metadata", + "volumes_terminate_connection": "volumes.terminate_connection", + "volumes_unmanage": "volumes.unmanage", + "volumes_unreserve": "volumes.unreserve", + "volumes_update": "volumes.update", + "volumes_update_all_metadata": "volumes.update_all_metadata", + "volumes_update_readonly_flag": "volumes.update_readonly_flag", + "volumes_upload_to_image": "volumes.upload_to_image" + }, + "trove": { + "_comment": "It uses troveclient.v1.", + "backups_create": "backups.create", + "backups_delete": "backups.delete", + "backups_find": "backups.find", + "backups_findall": "backups.findall", + "backups_get": "backups.get", + "backups_list": "backups.list", + "clusters_add_shard": "clusters.add_shard", + "clusters_create": "clusters.create", + "clusters_delete": "clusters.delete", + "clusters_find": "clusters.find", + "clusters_findall": "clusters.findall", + "clusters_get": "clusters.get", + "clusters_grow": "clusters.grow", + "clusters_list": "clusters.list", + "clusters_shrink": "clusters.shrink", + "configuration_parameters_find": "configuration_parameters.find", + "configuration_parameters_findall": "configuration_parameters.findall", + "configuration_parameters_get_parameter": "configuration_parameters.get_parameter", + "configuration_parameters_get_parameter_by_version": "configuration_parameters.get_parameter_by_version", + "configuration_parameters_list": "configuration_parameters.list", + "configuration_parameters_parameters": "configuration_parameters.parameters", + "configuration_parameters_parameters_by_version": "configuration_parameters.parameters_by_version", + "configurations_create": "configurations.create", + "configurations_delete": "configurations.delete", + "configurations_edit": "configurations.edit", + "configurations_find": "configurations.find", + "configurations_findall": "configurations.findall", + "configurations_get": "configurations.get", + "configurations_instances": "configurations.instances", + "configurations_list": "configurations.list", + "configurations_update": "configurations.update", + "databases_create": "databases.create", + "databases_delete": "databases.delete", + "databases_find": "databases.find", + "databases_findall": "databases.findall", + "databases_list": "databases.list", + "datastore_versions_find": "datastore_versions.find", + "datastore_versions_findall": "datastore_versions.findall", + "datastore_versions_get": "datastore_versions.get", + "datastore_versions_get_by_uuid": "datastore_versions.get_by_uuid", + "datastore_versions_list": "datastore_versions.list", + "datastore_versions_update": "datastore_versions.update", + "datastores_find": "datastores.find", + "datastores_findall": "datastores.findall", + "datastores_get": "datastores.get", + "datastores_list": "datastores.list", + "flavors_find": "flavors.find", + "flavors_findall": "flavors.findall", + "flavors_get": "flavors.get", + "flavors_list": "flavors.list", + "flavors_list_datastore_version_associated_flavors": "flavors.list_datastore_version_associated_flavors", + "instances_backups": "instances.backups", + "instances_configuration": "instances.configuration", + "instances_create": "instances.create", + "instances_delete": "instances.delete", + "instances_edit": "instances.edit", + "instances_eject_replica_source": "instances.eject_replica_source", + "instances_find": "instances.find", + "instances_findall": "instances.findall", + "instances_get": "instances.get", + "instances_list": "instances.list", + "instances_modify": "instances.modify", + "instances_promote_to_replica_source": "instances.promote_to_replica_source", + "instances_resize_instance": "instances.resize_instance", + "instances_resize_volume": "instances.resize_volume", + "instances_restart": "instances.restart", + "limits_find": "limits.find", + "limits_findall": "limits.findall", + "limits_list": "limits.list", + "metadata_create": "metadata.create", + "metadata_delete": "metadata.delete", + "metadata_edit": "metadata.edit", + "metadata_list": "metadata.list", + "metadata_show": "metadata.show", + "metadata_update": "metadata.update", + "root_create": "root.create", + "root_create_cluster_root": "root.create_cluster_root", + "root_create_instance_root": "root.create_instance_root", + "root_delete": "root.delete", + "root_disable_instance_root": "root.disable_instance_root", + "root_find": "root.find", + "root_findall": "root.findall", + "root_is_cluster_root_enabled": "root.is_cluster_root_enabled", + "root_is_instance_root_enabled": "root.is_instance_root_enabled", + "root_is_root_enabled": "root.is_root_enabled", + "root_list": "root.list", + "security_group_rules_create": "security_group_rules.create", + "security_group_rules_delete": "security_group_rules.delete", + "security_group_rules_find": "security_group_rules.find", + "security_group_rules_findall": "security_group_rules.findall", + "security_group_rules_list": "security_group_rules.list", + "security_groups_find": "security_groups.find", + "security_groups_findall": "security_groups.findall", + "security_groups_get": "security_groups.get", + "security_groups_list": "security_groups.list", + "users_change_passwords": "users.change_passwords", + "users_create": "users.create", + "users_delete": "users.delete", + "users_find": "users.find", + "users_findall": "users.findall", + "users_get": "users.get", + "users_grant": "users.grant", + "users_list": "users.list", + "users_list_access": "users.list_access", + "users_revoke": "users.revoke", + "users_update_attributes": "users.update_attributes" + }, + "ironic": { + "_comment": "It uses ironicclient.v1.", + "chassis_create": "chassis.create", + "chassis_delete": "chassis.delete", + "chassis_get": "chassis.get", + "chassis_list": "chassis.list", + "chassis_list_nodes": "chassis.list_nodes", + "chassis_update": "chassis.update", + "driver_delete": "driver.delete", + "driver_get": "driver.get", + "driver_get_vendor_passthru_methods": "driver.get_vendor_passthru_methods", + "driver_list": "driver.list", + "driver_properties": "driver.properties", + "driver_raid_logical_disk_properties": "driver.raid_logical_disk_properties", + "driver_update": "driver.update", + "driver_vendor_passthru": "driver.vendor_passthru", + "node_create": "node.create", + "node_delete": "node.delete", + "node_get": "node.get", + "node_get_boot_device": "node.get_boot_device", + "node_get_by_instance_uuid": "node.get_by_instance_uuid", + "node_get_console": "node.get_console", + "node_get_supported_boot_devices": "node.get_supported_boot_devices", + "node_get_vendor_passthru_methods": "node.get_vendor_passthru_methods", + "node_list": "node.list", + "node_list_ports": "node.list_ports", + "node_set_boot_device": "node.set_boot_device", + "node_set_console_mode": "node.set_console_mode", + "node_set_maintenance": "node.set_maintenance", + "node_set_power_state": "node.set_power_state", + "node_set_provision_state": "node.set_provision_state", + "node_set_target_raid_config": "node.set_target_raid_config", + "node_states": "node.states", + "node_update": "node.update", + "node_validate": "node.validate", + "node_vendor_passthru": "node.vendor_passthru", + "node_vif_attach": "node.vif_attach", + "node_vif_detach": "node.vif_detach", + "node_vif_list": "node.vif_list", + "node_wait_for_provision_state": "node.wait_for_provision_state", + "port_create": "port.create", + "port_delete": "port.delete", + "port_get": "port.get", + "port_get_by_address": "port.get_by_address", + "port_list": "port.list", + "port_update": "port.update" + }, + "baremetal_introspection": { + "_comment": "It uses ironic_inspector_client.v1.", + "abort": "abort", + "introspect": "introspect", + "get_status": "get_status", + "get_data": "get_data", + "rules_create": "rules.create", + "rules_delete": "rules.delete", + "rules_delete_all": "rules.delete_all", + "rules_from_json": "rules.from_json", + "rules_get": "rules.get", + "rules_get_all": "rules.get_all", + "wait_for_finish": "wait_for_finish" + }, + "swift": { + "_comment": "It uses swiftclient.v1.", + "head_account": "head_account", + "get_account": "get_account", + "post_account": "post_account", + "head_container": "head_container", + "get_container": "get_container", + "put_container": "put_container", + "post_container": "post_container", + "delete_container": "delete_container", + "head_object": "head_object", + "get_object": "get_object", + "put_object": "put_object", + "post_object": "post_object", + "delete_object": "delete_object", + "copy_object": "copy_object", + "get_capabilities": "get_capabilities" + }, + "swiftservice": { + "_comment": "It uses swiftclient.service.", + "capabilities": "capabilities", + "copy": "copy", + "delete": "delete", + "download": "download", + "list": "list", + "post": "post", + "stat": "stat", + "upload": "upload" + }, + "zaqar": { + "_comment": "It uses zaqarclient.v2.", + "claim_messages": "claim_messages", + "delete_messages": "delete_messages", + "queue_messages": "queue_messages", + "queue_post": "queue_post", + "queue_pop": "queue_pop" + }, + "barbican": { + "_comment": "It uses barbicanclient", + "cas_get": "cas.get", + "cas_list": "cas.list", + "cas_total": "cas.total", + "containers_create": "containers.create", + "containers_create_certificate": "containers.create_certificate", + "containers_create_rsa": "containers.create_rsa", + "containers_delete": "containers.delete", + "containers_get": "containers.get", + "containers_list": "containers.list", + "containers_register_consumer": "containers.register_consumer", + "containers_remove_consumer": "containers.remove_consumer", + "containers_total": "containers.total", + "orders_create": "orders.create", + "orders_create_asymmetric": "orders.create_asymmetric", + "orders_create_certificate": "orders.create_certificate", + "orders_create_key": "orders.create_key", + "orders_delete": "orders.delete", + "orders_get": "orders.get", + "orders_list": "orders.list", + "orders_total": "orders.total", + "secrets_create": "secrets.create", + "secrets_delete": "secrets.delete", + "secrets_get": "secrets.get", + "secrets_list": "secrets.list", + "secrets_total": "secrets.total", + "secrets_retrieve": "secrets_retrieve", + "secrets_store": "secrets_store" + }, + "mistral": { + "_comment": "It uses mistralclient.v2.", + "action_executions_create": "action_executions.create", + "action_executions_delete": "action_executions.delete", + "action_executions_find": "action_executions.find", + "action_executions_get": "action_executions.get", + "action_executions_list": "action_executions.list", + "action_executions_update": "action_executions.update", + "actions_create": "actions.create", + "actions_delete": "actions.delete", + "actions_find": "actions.find", + "actions_get": "actions.get", + "actions_list": "actions.list", + "actions_update": "actions.update", + "cron_triggers_create": "cron_triggers.create", + "cron_triggers_delete": "cron_triggers.delete", + "cron_triggers_find": "cron_triggers.find", + "cron_triggers_get": "cron_triggers.get", + "cron_triggers_list": "cron_triggers.list", + "environments_create": "environments.create", + "environments_delete": "environments.delete", + "environments_find": "environments.find", + "environments_get": "environments.get", + "environments_list": "environments.list", + "environments_update": "environments.update", + "executions_create": "executions.create", + "executions_delete": "executions.delete", + "executions_find": "executions.find", + "executions_get": "executions.get", + "executions_list": "executions.list", + "executions_update": "executions.update", + "members_create": "members.create", + "members_delete": "members.delete", + "members_find": "members.find", + "members_get": "members.get", + "members_list": "members.list", + "members_update": "members.update", + "services_find": "services.find", + "services_list": "services.list", + "tasks_find": "tasks.find", + "tasks_get": "tasks.get", + "tasks_list": "tasks.list", + "tasks_rerun": "tasks.rerun", + "workbooks_create": "workbooks.create", + "workbooks_delete": "workbooks.delete", + "workbooks_find": "workbooks.find", + "workbooks_get": "workbooks.get", + "workbooks_list": "workbooks.list", + "workbooks_update": "workbooks.update", + "workbooks_validate": "workbooks.validate", + "workflows_create": "workflows.create", + "workflows_delete": "workflows.delete", + "workflows_find": "workflows.find", + "workflows_get": "workflows.get", + "workflows_list": "workflows.list", + "workflows_update": "workflows.update", + "workflows_validate": "workflows.validate" + }, + "designate": { + "_comment": "It uses designateclient.v2.", + "quotas_list": "quotas.list", + "quotas_reset": "quotas.reset", + "quotas_update": "quotas.update", + "recordset_create": "recordsets.create", + "recordset_delete": "recordsets.delete", + "recordset_get": "recordsets.get", + "recordset_list": "recordsets.list", + "recordset_list_all_zones": "recordsets.list_all_zones", + "recordset_update": "recordsets.update", + "nameservers_list": "nameservers.list", + "pools_list": "pools.list", + "limits_get": "limits.get", + "blacklists_create": "blacklists.create", + "blacklists_get": "blacklists.get", + "blacklists_list": "blacklists.list", + "blacklists_update": "blacklists.update", + "blacklists_delete": "blacklists.delete" + }, + "magnum": { + "_comment": "It uses magnumclient.v1.", + "baymodels_create": "baymodels.create", + "baymodels_delete": "baymodels.delete", + "baymodels_get": "baymodels.get", + "baymodels_list": "baymodels.list", + "baymodels_update": "baymodels.update", + "bays_create": "bays.create", + "bays_delete": "bays.delete", + "bays_get": "bays.get", + "bays_list": "bays.list", + "bays_update": "bays.update", + "certificates_create": "certificates.create", + "certificates_get": "certificates.get", + "certificates_rotate_ca": "certificates.rotate_ca", + "mservices_list": "mservices.list" + }, + "murano":{ + "_comment": "It uses muranoclient.v1.", + "categories_add": "categories.add", + "categories_delete": "categories.delete", + "categories_get": "categories.get", + "categories_list": "categories.list", + "deployments_list": "deployments.list", + "deployments_reports": "deployments.reports", + "env_templates_clone": "env_templates.clone", + "env_templates_create": "env_templates.create", + "env_templates_create_app": "env_templates.create_app", + "env_templates_create_env": "env_templates.create_env", + "env_templates_delete": "env_templates.delete", + "env_templates_delete_app": "env_templates.delete_app", + "env_templates_get": "env_templates.get", + "env_templates_list": "env_templates.list", + "env_templates_update": "env_templates.update", + "environments_create": "environments.create", + "environments_delete": "environments.delete", + "environments_find": "environments.find", + "environments_findall": "environments.findall", + "environments_get": "environments.get", + "environments_last_status": "environments.last_status", + "environments_list": "environments.list", + "environments_update": "environments.update", + "instance_statistics_get": "instance_statistics.get", + "instance_statistics_get_aggregated": "instance_statistics.get_aggregated", + "packages_create": "packages.create", + "packages_delete": "packages.delete", + "packages_download": "packages.download", + "packages_filter": "packages.filter", + "packages_get": "packages.get", + "packages_get_logo": "packages.get_logo", + "packages_get_supplier_logo": "packages.get_supplier_logo", + "packages_get_ui": "packages.get_ui", + "packages_list": "packages.list", + "packages_toggle_active": "packages.toggle_active", + "packages_toggle_public": "packages.toggle_public", + "packages_update": "packages.update", + "request_statistics_list": "request_statistics.list", + "services_delete": "services.delete", + "services_get": "services.get", + "services_list": "services.list", + "services_post": "services.post", + "sessions_configure": "sessions.configure", + "sessions_delete": "sessions.delete", + "sessions_deploy": "sessions.deploy", + "sessions_get": "sessions.get" + }, + "tacker":{ + "_comment": "It uses tackerclient.v1_0.", + "list_extensions": "list_extensions", + "show_extension": "show_extension", + "create_vnfd": "create_vnfd", + "delete_vnfd": "delete_vnfd", + "list_vnfds": "list_vnfds", + "show_vnfd": "show_vnfd", + "create_vnf": "create_vnf", + "update_vnf": "update_vnf", + "delete_vnf": "delete_vnf", + "list_vnfs": "list_vnfs", + "show_vnf": "show_vnf", + "create_vnffgd": "create_vnffgd", + "delete_vnffgd": "delete_vnffgd", + "list_vnffgds": "list_vnffgds", + "show_vnffgd": "show_vnffgd", + "create_vnffg": "create_vnffg", + "update_vnffg": "update_vnffg", + "delete_vnffg": "delete_vnffg", + "list_vnffgs": "list_vnffgs", + "show_vnffg": "show_vnffg", + "create_nsd": "create_nsd", + "delete_nsd": "delete_nsd", + "list_nsds": "list_nsds", + "show_nsd": "show_nsd", + "create_ns": "create_ns", + "delete_ns": "delete_ns", + "list_nss": "list_nss", + "show_ns": "show_ns", + "create_vim": "create_vim", + "update_vim": "update_vim", + "delete_vim": "delete_vim", + "list_vims": "list_vims", + "show_vim": "show_vim" + }, + "senlin":{ + "_comment": "It uses senlinclient.v1_0.", + "profile_types": "profile_types", + "get_profile_type": "get_profile_type", + "profiles": "profiles", + "create_profile": "create_profile", + "get_profile": "get_profile", + "update_profile": "update_profile", + "delete_profile": "delete_profile", + "validate_profile": "validate_profile", + "policy_types": "policy_types", + "get_policy_type": "get_policy_type", + "policies": "policies", + "create_policy": "create_policy", + "get_policy": "get_policy", + "update_policy": "update_policy", + "delete_policy": "delete_policy", + "validate_policy": "validate_policy", + "clusters": "clusters", + "create_cluster": "create_cluster", + "get_cluster": "get_cluster", + "update_cluster": "update_cluster", + "delete_cluster": "delete_cluster", + "cluster_add_nodes": "cluster_add_nodes", + "cluster_del_nodes": "cluster_del_nodes", + "cluster_resize": "cluster_resize", + "cluster_scale_out": "cluster_scale_out", + "cluster_scale_in": "cluster_scale_in", + "cluster_policies": "cluster_policies", + "get_cluster_policy": "get_cluster_policy", + "cluster_attach_policy": "cluster_attach_policy", + "cluster_detach_policy": "cluster_detach_policy", + "cluster_update_policy": "cluster_update_policy", + "check_cluster": "check_cluster", + "recover_cluster": "recover_cluster", + "nodes": "nodes", + "create_node": "create_node", + "get_node": "get_node", + "update_node": "update_node", + "delete_node": "delete_node", + "check_node": "check_node", + "recover_node": "recover_node", + "receivers": "receivers", + "create_receiver": "create_receiver", + "get_receiver": "get_receiver", + "delete_receiver": "delete_receiver", + "events": "events", + "get_event": "get_event", + "actions": "actions", + "get_action": "get_action" + }, + "glare": { + "_comment": "It uses glareclient.v1.", + "artifacts_create": "artifacts.create", + "artifacts_delete": "artifacts.delete", + "artifacts_get": "artifacts.get", + "artifacts_list": "artifacts.list", + "artifacts_update": "artifacts.update", + "artifacts_activate": "artifacts.activate", + "artifacts_deactivate": "artifacts.deactivate", + "artifacts_reactivate": "artifacts.reactivate", + "artifacts_publish": "artifacts.publish", + "artifacts_add_tag": "artifacts.add_tag", + "artifacts_remove_tag": "artifacts.remove_tag", + "artifacts_get_type_list": "artifacts.get_type_list", + "artifacts_get_type_schema": "artifacts.get_type_schema", + "artifacts_upload_blob": "artifacts.upload_blob", + "artifacts_download_blob": "artifacts.download_blob", + "artifacts_add_external_location": "artifacts.add_external_location" + }, + "vitrage": { + "_comment": "It uses vitrageclient.v1.", + "alarm_list": "alarm.list", + "alarm_get": "alarm.get", + "alarm_count": "alarm.count", + "event_post": "event.post", + "healthcheck_get": "healthcheck.get", + "rca_get": "rca.get", + "resource_list": "resource.list", + "resource_get": "resource.get", + "template_list": "template.list", + "template_show": "template.show", + "template_add": "template.add", + "template_delete": "template.delete", + "template_validate": "template.validate", + "topology_get": "topology.get", + "webhook_list": "webhook.list", + "webhook_show": "webhook.show", + "webhook_add": "webhook.add", + "webhook_delete": "webhook.delete" + }, + "zun":{ + "_comment": "It uses zunclient.v1. ", + "containers_add_security_group": "containers.add_security_group", + "containers_attach": "containers.attach", + "containers_commit": "containers.commit", + "containers_create": "containers.create", + "containers_delete": "containers.delete", + "containers_execute": "containers.execute", + "containers_execute_resize": "containers.execute_resize", + "containers_get": "containers.get", + "containers_get_archive": "containers.get_archive", + "containers_kill": "containers.kill", + "containers_list": "containers.list", + "containers_logs": "containers.logs", + "containers_network_attach": "containers.network_attach", + "containers_network_detach": "containers.network_detach", + "containers_pause": "containers.pause", + "containers_put_archive": "containers.put_archive", + "containers_remove_security_group": "containers.remove_security_group", + "containers_rename": "containers.rename", + "containers_resize": "containers.resize", + "containers_restart": "containers.restart", + "containers_run": "containers.run", + "containers_start": "containers.start", + "containers_stats": "containers.stats", + "containers_stop": "containers.stop", + "containers_top": "containers.top", + "containers_update": "containers.update", + "hosts_list": "hosts.list", + "hosts_get": "hosts.get", + "images_delete": "images.delete", + "images_list": "images.list", + "services_delete": "services.delete", + "services_disable": "services.disable", + "services_enable": "services.enable", + "services_list": "services.list" + }, + "qinling": { + "_comment": "Qinling v1 actions", + "runtimes_create": "runtimes.create", + "runtimes_list": "runtimes.list", + "runtimes_get": "runtimes.get", + "runtimes_delete": "runtimes.delete", + "functions_create": "functions.create", + "functions_list": "functions.list", + "functions_get": "functions.get", + "functions_update": "functions.update", + "functions_delete": "functions.delete", + "function_executions_create": "function_executions.create", + "function_executions_list": "function_executions.list", + "function_executions_get": "function_executions.get", + "function_executions_delete": "function_executions.delete", + "function_executions_get_log": "function_executions.get_log", + "jobs_create": "jobs.create", + "jobs_list": "jobs.list", + "jobs_get": "jobs.get", + "jobs_update": "jobs.update", + "jobs_delete": "jobs.delete", + "webhooks_create": "webhooks.create", + "webhooks_list": "webhooks.list", + "webhooks_get": "webhooks.get", + "webhooks_update": "webhooks.update", + "webhooks_delete": "webhooks.delete" + }, + "manila": { + "_comment": "It uses manilaclient.v2.", + "availability_zones_list": "availability_zones.list", + "limits_get": "limits.get", + "messages_delete": "messages.delete", + "messages_find": "messages.find", + "messages_findall": "messages.findall", + "messages_get": "messages.get", + "messages_list": "messages.list", + "pools_list": "pools.list", + "quota_classes_find": "quota_classes.find", + "quota_classes_findall": "quota_classes.findall", + "quota_classes_get": "quota_classes.get", + "quota_classes_list": "quota_classes.list", + "quota_classes_update": "quota_classes.update", + "quotas_defaults": "quotas.defaults", + "quotas_delete": "quotas.delete", + "quotas_find": "quotas.find", + "quotas_findall": "quotas.findall", + "quotas_get": "quotas.get", + "quotas_list": "quotas.list", + "quotas_update": "quotas.update", + "security_services_create": "security_services.create", + "security_services_delete": "security_services.delete", + "security_services_find": "security_services.find", + "security_services_findall": "security_services.findall", + "security_services_get": "security_services.get", + "security_services_list": "security_services.list", + "security_services_update": "security_services.update", + "services_disable": "services.disable", + "services_enable": "services.enable", + "services_list": "services.list", + "services_server_api_version": "services.server_api_version", + "share_export_locations_find": "share_export_locations.find", + "share_export_locations_findall": "share_export_locations.findall", + "share_export_locations_get": "share_export_locations.get", + "share_export_locations_list": "share_export_locations.list", + "share_group_snapshots_create": "share_group_snapshots.create", + "share_group_snapshots_delete": "share_group_snapshots.delete", + "share_group_snapshots_find": "share_group_snapshots.find", + "share_group_snapshots_findall": "share_group_snapshots.findall", + "share_group_snapshots_get": "share_group_snapshots.get", + "share_group_snapshots_list": "share_group_snapshots.list", + "share_group_snapshots_reset_state": "share_group_snapshots.reset_state", + "share_group_snapshots_update": "share_group_snapshots.update", + "share_group_type_access_add_project_access": "share_group_type_access.add_project_access", + "share_group_type_access_find": "share_group_type_access.find", + "share_group_type_access_findall": "share_group_type_access.findall", + "share_group_type_access_list": "share_group_type_access.list", + "share_group_type_access_remove_project_access": "share_group_type_access.remove_project_access", + "share_group_types_create": "share_group_types.create", + "share_group_types_delete": "share_group_types.delete", + "share_group_types_find": "share_group_types.find", + "share_group_types_findall": "share_group_types.findall", + "share_group_types_get": "share_group_types.get", + "share_group_types_list": "share_group_types.list", + "share_groups_create": "share_groups.create", + "share_groups_delete": "share_groups.delete", + "share_groups_find": "share_groups.find", + "share_groups_findall": "share_groups.findall", + "share_groups_get": "share_groups.get", + "share_groups_list": "share_groups.list", + "share_groups_reset_state": "share_groups.reset_state", + "share_groups_update": "share_groups.update", + "share_instance_export_locations_find": "share_instance_export_locations.find", + "share_instance_export_locations_findall": "share_instance_export_locations.findall", + "share_instance_export_locations_get": "share_instance_export_locations.get", + "share_instance_export_locations_list": "share_instance_export_locations.list", + "share_instances_do_list": "share_instances.do_list", + "share_instances_find": "share_instances.find", + "share_instances_findall": "share_instances.findall", + "share_instances_force_delete": "share_instances.force_delete", + "share_instances_get": "share_instances.get", + "share_instances_list": "share_instances.list", + "share_instances_reset_state": "share_instances.reset_state", + "share_networks_add_security_service": "share_networks.add_security_service", + "share_networks_create": "share_networks.create", + "share_networks_delete": "share_networks.delete", + "share_networks_find": "share_networks.find", + "share_networks_findall": "share_networks.findall", + "share_networks_get": "share_networks.get", + "share_networks_list": "share_networks.list", + "share_networks_remove_security_service": "share_networks.remove_security_service", + "share_networks_update": "share_networks.update", + "share_replicas_create": "share_replicas.create", + "share_replicas_delete": "share_replicas.delete", + "share_replicas_find": "share_replicas.find", + "share_replicas_findall": "share_replicas.findall", + "share_replicas_get": "share_replicas.get", + "share_replicas_list": "share_replicas.list", + "share_replicas_promote": "share_replicas.promote", + "share_replicas_reset_replica_state": "share_replicas.reset_replica_state", + "share_replicas_reset_state": "share_replicas.reset_state", + "share_replicas_resync": "share_replicas.resync", + "share_servers_delete": "share_servers.delete", + "share_servers_details": "share_servers.details", + "share_servers_find": "share_servers.find", + "share_servers_findall": "share_servers.findall", + "share_servers_get": "share_servers.get", + "share_servers_list": "share_servers.list", + "share_snapshot_export_locations_find": "share_snapshot_export_locations.find", + "share_snapshot_export_locations_findall": "share_snapshot_export_locations.findall", + "share_snapshot_export_locations_get": "share_snapshot_export_locations.get", + "share_snapshot_export_locations_list": "share_snapshot_export_locations.list", + "share_snapshot_instance_export_locations_find": "share_snapshot_instance_export_locations.find", + "share_snapshot_instance_export_locations_findall": "share_snapshot_instance_export_locations.findall", + "share_snapshot_instance_export_locations_get": "share_snapshot_instance_export_locations.get", + "share_snapshot_instance_export_locations_list": "share_snapshot_instance_export_locations.list", + "share_snapshot_instances_find": "share_snapshot_instances.find", + "share_snapshot_instances_findall": "share_snapshot_instances.findall", + "share_snapshot_instances_get": "share_snapshot_instances.get", + "share_snapshot_instances_list": "share_snapshot_instances.list", + "share_snapshot_instances_reset_state": "share_snapshot_instances.reset_state", + "share_snapshots_access_list": "share_snapshots.access_list", + "share_snapshots_allow": "share_snapshots.allow", + "share_snapshots_create": "share_snapshots.create", + "share_snapshots_delete": "share_snapshots.delete", + "share_snapshots_deny": "share_snapshots.deny", + "share_snapshots_find": "share_snapshots.find", + "share_snapshots_findall": "share_snapshots.findall", + "share_snapshots_force_delete": "share_snapshots.force_delete", + "share_snapshots_get": "share_snapshots.get", + "share_snapshots_list": "share_snapshots.list", + "share_snapshots_manage": "share_snapshots.manage", + "share_snapshots_reset_state": "share_snapshots.reset_state", + "share_snapshots_unmanage": "share_snapshots.unmanage", + "share_snapshots_update": "share_snapshots.update", + "share_type_access_add_project_access": "share_type_access.add_project_access", + "share_type_access_find": "share_type_access.find", + "share_type_access_findall": "share_type_access.findall", + "share_type_access_list": "share_type_access.list", + "share_type_access_remove_project_access": "share_type_access.remove_project_access", + "share_types_create": "share_types.create", + "share_types_delete": "share_types.delete", + "share_types_find": "share_types.find", + "share_types_findall": "share_types.findall", + "share_types_get": "share_types.get", + "share_types_list": "share_types.list", + "share_types_show": "share_types.show", + "shares_access_list": "shares.access_list", + "shares_allow": "shares.allow", + "shares_create": "shares.create", + "shares_delete": "shares.delete", + "shares_delete_metadata": "shares.delete_metadata", + "shares_deny": "shares.deny", + "shares_do_list": "shares.do_list", + "shares_extend": "shares.extend", + "shares_find": "shares.find", + "shares_findall": "shares.findall", + "shares_force_delete": "shares.force_delete", + "shares_get": "shares.get", + "shares_get_metadata": "shares.get_metadata", + "shares_list": "shares.list", + "shares_list_instances": "shares.list_instances", + "shares_manage": "shares.manage", + "shares_migration_cancel": "shares.migration_cancel", + "shares_migration_complete": "shares.migration_complete", + "shares_migration_get_progress": "shares.migration_get_progress", + "shares_migration_start": "shares.migration_start", + "shares_reset_state": "shares.reset_state", + "shares_reset_task_state": "shares.reset_task_state", + "shares_revert_to_snapshot": "shares.revert_to_snapshot", + "shares_set_metadata": "shares.set_metadata", + "shares_shrink": "shares.shrink", + "shares_unmanage": "shares.unmanage", + "shares_update": "shares.update", + "shares_update_all_metadata": "shares.update_all_metadata" + } +} diff --git a/mistral_extra/actions/openstack/utils/__init__.py b/mistral_extra/actions/openstack/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/actions/openstack/utils/context.py b/mistral_extra/actions/openstack/utils/context.py new file mode 100644 index 0000000..c75f831 --- /dev/null +++ b/mistral_extra/actions/openstack/utils/context.py @@ -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) diff --git a/mistral_extra/actions/openstack/utils/exceptions.py b/mistral_extra/actions/openstack/utils/exceptions.py new file mode 100644 index 0000000..c658433 --- /dev/null +++ b/mistral_extra/actions/openstack/utils/exceptions.py @@ -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 diff --git a/mistral_extra/actions/openstack/utils/keystone.py b/mistral_extra/actions/openstack/utils/keystone.py new file mode 100644 index 0000000..f1c73a7 --- /dev/null +++ b/mistral_extra/actions/openstack/utils/keystone.py @@ -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 + ) diff --git a/mistral_extra/config.py b/mistral_extra/config.py new file mode 100644 index 0000000..0b569a4 --- /dev/null +++ b/mistral_extra/config.py @@ -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) diff --git a/mistral_extra/tests/resources/openstack/test_mapping.json b/mistral_extra/tests/resources/openstack/test_mapping.json new file mode 100644 index 0000000..82d1347 --- /dev/null +++ b/mistral_extra/tests/resources/openstack/test_mapping.json @@ -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" + } +} diff --git a/mistral_extra/tests/unit/actions/__init__.py b/mistral_extra/tests/unit/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/tests/unit/actions/openstack/__init__.py b/mistral_extra/tests/unit/actions/openstack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mistral_extra/tests/unit/actions/openstack/test_generator.py b/mistral_extra/tests/unit/actions/openstack/test_generator.py new file mode 100644 index 0000000..42c10ae --- /dev/null +++ b/mistral_extra/tests/unit/actions/openstack/test_generator.py @@ -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) diff --git a/mistral_extra/tests/unit/actions/openstack/test_openstack_actions.py b/mistral_extra/tests/unit/actions/openstack/test_openstack_actions.py new file mode 100644 index 0000000..27d8b50 --- /dev/null +++ b/mistral_extra/tests/unit/actions/openstack/test_openstack_actions.py @@ -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) diff --git a/mistral_extra/tests/unit/base.py b/mistral_extra/tests/unit/base.py index be58475..febf6c9 100644 --- a/mistral_extra/tests/unit/base.py +++ b/mistral_extra/tests/unit/base.py @@ -22,3 +22,31 @@ LOG = logging.getLogger(__name__) class BaseTest(base.BaseTestCase): def setUp(self): super(BaseTest, self).setUp() + + def _assert_single_item(self, items, **props): + return self._assert_multiple_items(items, 1, **props)[0] + + def _assert_multiple_items(self, items, count, **props): + def _matches(item, **props): + for prop_name, prop_val in props.items(): + v = item[prop_name] if isinstance( + item, dict) else getattr(item, prop_name) + + if v != prop_val: + return False + + return True + + filtered_items = list( + [item for item in items if _matches(item, **props)] + ) + + found = len(filtered_items) + + if found != count: + LOG.info("[failed test ctx] items=%s, expected_props=%s", str( + items), props) + self.fail("Wrong number of items found [props=%s, " + "expected=%s, found=%s]" % (props, count, found)) + + return filtered_items diff --git a/mistral_extra/version.py b/mistral_extra/version.py new file mode 100644 index 0000000..09942b1 --- /dev/null +++ b/mistral_extra/version.py @@ -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() diff --git a/releasenotes/notes/move_openstack_actions_from_mistral_to_mistral_extra-70ad20eb06621f6c.yaml b/releasenotes/notes/move_openstack_actions_from_mistral_to_mistral_extra-70ad20eb06621f6c.yaml new file mode 100644 index 0000000..086384a --- /dev/null +++ b/releasenotes/notes/move_openstack_actions_from_mistral_to_mistral_extra-70ad20eb06621f6c.yaml @@ -0,0 +1,3 @@ +upgrade: + - | + Move all OpenStack actions to mistral-extra \ No newline at end of file diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..df7bc2c --- /dev/null +++ b/releasenotes/source/conf.py @@ -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 +# " v 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 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 = '' diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..1613e6b --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,13 @@ +=========================== +mistral-extra Release Notes +=========================== + +.. toctree:: + :maxdepth: 1 + + unreleased + train + stein + rocky + queens + pike diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 0000000..9184412 --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +========================= +Pike Series Release Notes +========================= + +.. release-notes:: + :branch: stable/pike diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 0000000..3e12438 --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=========================== +Queens Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 0000000..6c5dfad --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +========================== +Rocky Series Release Notes +========================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 0000000..6b7e725 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +========================== +Stein Series Release Notes +========================== + +.. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 0000000..5839003 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..875030f --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt index 8cb50ec..a6ce566 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,32 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD oslo.log>=3.36.0 # Apache-2.0 -mistral-lib>=1.2.0 # Apache-2.0 +mistral-lib>=1.4.0 # Apache-2.0 +aodhclient>=0.9.0 # Apache-2.0 +gnocchiclient>=3.3.1 # Apache-2.0 +python-barbicanclient>=4.5.2 # Apache-2.0 +python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 +python-zaqarclient>=1.0.0 # Apache-2.0 +python-designateclient>=2.7.0 # Apache-2.0 +python-glanceclient>=2.8.0 # Apache-2.0 +python-glareclient>=0.3.0 # Apache-2.0 +python-heatclient>=1.10.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 +python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 +python-manilaclient>=1.23.0 # Apache-2.0 +python-magnumclient>=2.15.0 # Apache-2.0 +python-muranoclient>=1.3.0 # Apache-2.0 +python-neutronclient>=6.7.0 # Apache-2.0 +python-novaclient>=9.1.0 # Apache-2.0 +python-senlinclient>=1.11.0 # Apache-2.0 +python-swiftclient>=3.2.0 # Apache-2.0 +python-tackerclient>=0.8.0 # Apache-2.0 +python-troveclient>=2.2.0 # Apache-2.0 +python-ironicclient!=2.7.1,!=3.0.0,>=2.7.0 # Apache-2.0 +python-ironic-inspector-client>=1.5.0 # Apache-2.0 +python-vitrageclient>=2.0.0 # Apache-2.0 +python-zunclient>=3.4.0 # Apache-2.0 +python-qinlingclient>=1.0.0 # Apache-2.0 +oauthlib>=0.6.2 # BSD +yaql>=1.1.3 # Apache-2.0 +keystoneauth1>=3.18.0 # Apache-2.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 31b6d3b..e8ec7bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,10 @@ classifier = packages = mistral_extra +[entry_points] +mistral.generators = + generators = mistral_extra.actions.generator_factory:all_generators + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/tools/get_action_list.py b/tools/get_action_list.py new file mode 100644 index 0000000..e4ea5b9 --- /dev/null +++ b/tools/get_action_list.py @@ -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": { + + }, + ...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 " + ) + 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)) diff --git a/tox.ini b/tox.ini index eaec768..683419b 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ max-line-length = 80 # E123, E125 skipped as they are invalid PEP-8. show-source = True -ignore = E123,E125 +ignore = E123,E125,W504 builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build