Move openstack actions to mistral-extra

Implements: blueprint mistral-actions-api-separate-openstack-actions
Change-Id: Iaf1cca7a29df35409fea641fdb60191afb509b5a
This commit is contained in:
Eyal 2020-01-19 17:04:19 +02:00
parent 57afe0a3f4
commit bb2af2e2be
39 changed files with 4825 additions and 3 deletions

View File

@ -3,3 +3,5 @@
- openstack-python3-ussuri-jobs
- check-requirements
- openstack-lower-constraints-jobs
- publish-openstack-docs-pti
- release-notes-jobs-python3

View File

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

View File

@ -0,0 +1 @@
import mistral_extra.config # noqa

View File

View File

@ -0,0 +1,31 @@
# Copyright 2014 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
class ActionGenerator(object):
"""Action generator.
Action generator uses some data to build Action classes
dynamically.
"""
@abc.abstractmethod
def create_actions(self, *args, **kwargs):
"""Constructs classes of needed action.
return: list of actions dicts containing name, class,
description and parameter info.
"""
pass

View File

@ -0,0 +1,44 @@
# Copyright 2014 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_utils import importutils
from mistral_extra.actions.openstack.action_generator import base
SUPPORTED_MODULES = [
'Nova', 'Glance', 'Keystone', 'Heat', 'Neutron', 'Cinder',
'Trove', 'Ironic', 'Baremetal Introspection', 'Swift', 'SwiftService',
'Zaqar', 'Barbican', 'Mistral', 'Designate', 'Magnum', 'Murano', 'Tacker',
'Aodh', 'Gnocchi', 'Glare', 'Vitrage', 'Senlin', 'Zun', 'Qinling', 'Manila'
]
def all_generators():
for mod_name in SUPPORTED_MODULES:
prefix = mod_name.replace(' ', '')
mod_namespace = mod_name.lower().replace(' ', '_')
mod_cls_name = 'mistral_extra.actions.openstack.actions.%sAction' \
% prefix
mod_action_cls = importutils.import_class(mod_cls_name)
generator_cls_name = '%sActionGenerator' % prefix
yield type(
generator_cls_name,
(base.OpenStackActionGenerator,),
{
'action_namespace': mod_namespace,
'base_action_class': mod_action_cls
}
)

View File

@ -0,0 +1,172 @@
# Copyright 2014 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
from oslo_config import cfg
from oslo_log import log as logging
import pkg_resources as pkg
from mistral_extra.actions import action_generator
from mistral_extra import version
from mistral_lib.utils import inspect_utils as i_u
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def get_mapping():
def delete_comment(map_part):
for key, value in map_part.items():
if isinstance(value, dict):
delete_comment(value)
if '_comment' in map_part:
del map_part['_comment']
package = version.version_info.package
if os.path.isabs(CONF.openstack_actions_mapping_path):
mapping_file_path = CONF.openstack_actions_mapping_path
else:
path = CONF.openstack_actions_mapping_path
mapping_file_path = pkg.resource_filename(package, path)
LOG.info(
"Processing OpenStack action mapping from file: %s",
mapping_file_path
)
with open(mapping_file_path) as fh:
mapping = json.load(fh)
for k, v in mapping.items():
if isinstance(v, dict):
delete_comment(v)
return mapping
class OpenStackActionGenerator(action_generator.ActionGenerator):
"""OpenStackActionGenerator.
Base generator for all OpenStack actions,
creates a client method declaration using
specific python-client and sets needed arguments
to actions.
"""
action_namespace = None
base_action_class = None
@classmethod
def prepare_action_inputs(cls, origin_inputs, added=()):
"""Modify action input string.
Sometimes we need to change the default action input definition for
OpenStack actions in order to make the workflow more powerful.
Examples::
>>> prepare_action_inputs('a,b,c', added=['region=RegionOne'])
a, b, c, region=RegionOne
>>> prepare_action_inputs('a,b,c=1', added=['region=RegionOne'])
a, b, region=RegionOne, c=1
>>> prepare_action_inputs('a,b,c=1,**kwargs',
added=['region=RegionOne'])
a, b, region=RegionOne, c=1, **kwargs
>>> prepare_action_inputs('**kwargs', added=['region=RegionOne'])
region=RegionOne, **kwargs
>>> prepare_action_inputs('', added=['region=RegionOne'])
region=RegionOne
:param origin_inputs: A string consists of action inputs, separated by
comma.
:param added: (Optional) A list of params to add to input string.
:return: The new action input string.
"""
if not origin_inputs:
return ", ".join(added)
inputs = [i.strip() for i in origin_inputs.split(',')]
kwarg_index = None
for index, input in enumerate(inputs):
if "=" in input:
kwarg_index = index
if "**" in input:
kwarg_index = index - 1
kwarg_index = len(inputs) if kwarg_index is None else kwarg_index
kwarg_index = kwarg_index + 1 if kwarg_index < 0 else kwarg_index
for a in added:
if "=" not in a:
inputs.insert(0, a)
kwarg_index += 1
else:
inputs.insert(kwarg_index, a)
return ", ".join(inputs)
@classmethod
def create_action_class(cls, method_name):
if not method_name:
return None
action_class = type(str(method_name), (cls.base_action_class,),
{'client_method_name': method_name})
return action_class
@classmethod
def create_actions(cls):
mapping = get_mapping()
method_dict = mapping.get(cls.action_namespace, {})
action_classes = []
for action_name, method_name in method_dict.items():
class_ = cls.create_action_class(method_name)
try:
client_method = class_.get_fake_client_method()
except Exception:
LOG.exception(
"Failed to create action: %s.%s",
cls.action_namespace, action_name
)
continue
arg_list = i_u.get_arg_list_as_str(client_method)
# Support specifying region for OpenStack actions.
modules = CONF.openstack_actions.modules_support_region
if cls.action_namespace in modules:
arg_list = cls.prepare_action_inputs(
arg_list,
added=['action_region=""']
)
description = i_u.get_docstring(client_method)
action_classes.append(
{
'class': class_,
'name': "%s.%s" % (cls.action_namespace, action_name),
'description': description,
'arg_list': arg_list,
}
)
return action_classes

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,137 @@
# Copyright 2014 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import inspect
import traceback
from oslo_log import log
from mistral_extra.actions.openstack.utils import exceptions as exc
from mistral_extra.actions.openstack.utils import keystone as \
keystone_utils
from mistral_lib import actions
LOG = log.getLogger(__name__)
class OpenStackAction(actions.Action):
"""OpenStack Action.
OpenStack Action is the basis of all OpenStack-specific actions,
which are constructed via OpenStack Action generators.
"""
_kwargs_for_run = {}
client_method_name = None
_service_name = None
_service_type = None
_client_class = None
def __init__(self, **kwargs):
self._kwargs_for_run = kwargs
self.action_region = self._kwargs_for_run.pop('action_region', None)
@abc.abstractmethod
def _create_client(self, context):
"""Creates client required for action operation."""
return None
@classmethod
def _get_client_class(cls):
return cls._client_class
@classmethod
def _get_client_method(cls, client):
hierarchy_list = cls.client_method_name.split('.')
attribute = client
for attr in hierarchy_list:
attribute = getattr(attribute, attr)
return attribute
@classmethod
def _get_fake_client(cls):
"""Returns python-client instance which initiated via wrong args.
It is needed for getting client-method args and description for
saving into DB.
"""
# Default is simple _get_client_class instance
return cls._get_client_class()()
@classmethod
def get_fake_client_method(cls):
return cls._get_client_method(cls._get_fake_client())
def _get_client(self, context):
"""Returns python-client instance via cache or creation
Gets client instance according to specific OpenStack Service
(e.g. Nova, Glance, Heat, Keystone etc)
"""
return self._create_client(context)
def get_session_and_auth(self, context):
"""Get keystone session and auth parameters.
:param context: the action context
:return: dict that can be used to initialize service clients
"""
return keystone_utils.get_session_and_auth(
service_name=self._service_name,
service_type=self._service_type,
region_name=self.action_region,
ctx=context)
def get_service_endpoint(self):
"""Get OpenStack service endpoint.
'service_name' and 'service_type' are defined in specific OpenStack
service action.
"""
endpoint = keystone_utils.get_endpoint_for_project(
service_name=self._service_name,
service_type=self._service_type,
region_name=self.action_region
)
return endpoint
def run(self, context):
try:
method = self._get_client_method(self._get_client(context))
result = method(**self._kwargs_for_run)
if inspect.isgenerator(result):
return [v for v in result]
return result
except Exception as e:
# Print the traceback for the last exception so that we can see
# where the issue comes from.
LOG.warning(traceback.format_exc())
raise exc.ActionException(
"%s.%s failed: %s" %
(self.__class__.__name__, self.client_method_name, str(e))
)
def test(self, context):
return dict(
zip(self._kwargs_for_run, ['test'] * len(self._kwargs_for_run))
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
# Copyright 2013 - Mirantis, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from oslo_log import log as logging
from mistral_extra.actions.openstack.utils import exceptions as exc
from mistral_lib import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
_CTX_THREAD_LOCAL_NAME = "MISTRAL_APP_CTX_THREAD_LOCAL"
def has_ctx():
return utils.has_thread_local(_CTX_THREAD_LOCAL_NAME)
def ctx():
if not has_ctx():
raise exc.ApplicationContextNotFoundException()
return utils.get_thread_local(_CTX_THREAD_LOCAL_NAME)
def set_ctx(new_ctx):
utils.set_thread_local(_CTX_THREAD_LOCAL_NAME, new_ctx)

View File

@ -0,0 +1,29 @@
# Copyright 2020 - Nokia Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mistral_lib.exceptions import MistralException
class UnauthorizedException(MistralException):
http_code = 401
message = "Unauthorized"
class ApplicationContextNotFoundException(MistralException):
http_code = 400
message = "Application context not found"
class ActionException(MistralException):
http_code = 400

View File

@ -0,0 +1,297 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import keystoneauth1.identity.generic as auth_plugins
from keystoneauth1 import loading
from keystoneauth1 import session as ks_session
from keystoneauth1.token_endpoint import Token
from keystoneclient import service_catalog as ks_service_catalog
from keystoneclient.v3 import client as ks_client
from keystoneclient.v3 import endpoints as ks_endpoints
from oslo_config import cfg
import six
from mistral_extra.actions.openstack.utils import context
from mistral_extra.actions.openstack.utils import exceptions
CONF = cfg.CONF
def client():
ctx = context.ctx()
auth_url = ctx.auth_uri or CONF.keystone_authtoken.www_authenticate_uri
cl = ks_client.Client(
user_id=ctx.user_id,
token=ctx.auth_token,
tenant_id=ctx.project_id,
auth_url=auth_url
)
cl.management_url = auth_url
return cl
def _determine_verify(ctx):
if ctx.insecure:
return False
elif ctx.auth_cacert:
return ctx.auth_cacert
else:
return True
def get_session_and_auth(ctx, **kwargs):
"""Get session and auth parameters.
:param ctx: action context
:return: dict to be used as kwargs for client service initialization
"""
if not ctx:
raise AssertionError('context is mandatory')
project_endpoint = get_endpoint_for_project(**kwargs)
endpoint = format_url(
project_endpoint.url,
{
'tenant_id': ctx.project_id,
'project_id': ctx.project_id
}
)
auth = Token(endpoint=endpoint, token=ctx.auth_token)
auth_uri = ctx.auth_uri or CONF.keystone_authtoken.www_authenticate_uri
ks_auth = Token(
endpoint=auth_uri,
token=ctx.auth_token
)
session = ks_session.Session(
auth=ks_auth,
verify=_determine_verify(ctx)
)
return {
"session": session,
"auth": auth
}
def _admin_client(trust_id=None):
if CONF.keystone_authtoken.auth_type is None:
auth_url = CONF.keystone_authtoken.www_authenticate_uri
project_name = CONF.keystone_authtoken.admin_tenant_name
# You can't use trust and project together
if trust_id:
project_name = None
cl = ks_client.Client(
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_name=project_name,
auth_url=auth_url,
trusts=trust_id
)
cl.management_url = auth_url
return cl
else:
kwargs = {}
if trust_id:
# Remove domain_id, domain_name, project_name and project_id,
# since we need a trust scoped auth object
kwargs['domain_id'] = None
kwargs['domain_name'] = None
kwargs['project_name'] = None
kwargs['project_domain_name'] = None
kwargs['project_id'] = None
kwargs['trust_id'] = trust_id
auth = loading.load_auth_from_conf_options(
CONF,
'keystone_authtoken',
**kwargs
)
sess = loading.load_session_from_conf_options(
CONF,
'keystone',
auth=auth
)
return ks_client.Client(session=sess)
def client_for_admin():
return _admin_client()
def client_for_trusts(trust_id):
return _admin_client(trust_id=trust_id)
def get_endpoint_for_project(service_name=None, service_type=None,
region_name=None):
if service_name is None and service_type is None:
raise exceptions.MistralException(
"Either 'service_name' or 'service_type' must be provided."
)
ctx = context.ctx()
service_catalog = obtain_service_catalog(ctx)
# When region_name is not passed, first get from context as region_name
# could be passed to rest api in http header ('X-Region-Name'). Otherwise,
# just get region from mistral configuration.
region = (region_name or ctx.region_name)
if service_name == 'keystone':
# Determining keystone endpoint should be done using
# keystone_authtoken section as this option is special for keystone.
region = region or CONF.keystone_authtoken.region_name
else:
region = region or CONF.openstack_actions.default_region
service_endpoints = service_catalog.get_endpoints(
service_name=service_name,
service_type=service_type,
region_name=region
)
endpoint = None
os_actions_endpoint_type = CONF.openstack_actions.os_actions_endpoint_type
for endpoints in six.itervalues(service_endpoints):
for ep in endpoints:
# is V3 interface?
if 'interface' in ep:
interface_type = ep['interface']
if os_actions_endpoint_type in interface_type:
endpoint = ks_endpoints.Endpoint(
None,
ep,
loaded=True
)
break
# is V2 interface?
if 'publicURL' in ep:
endpoint_data = {
'url': ep['publicURL'],
'region': ep['region']
}
endpoint = ks_endpoints.Endpoint(
None,
endpoint_data,
loaded=True
)
break
if not endpoint:
raise exceptions.MistralException(
"No endpoints found [service_name=%s, service_type=%s,"
" region_name=%s]"
% (service_name, service_type, region)
)
else:
return endpoint
def obtain_service_catalog(ctx):
token = ctx.auth_token
if ctx.is_trust_scoped and is_token_trust_scoped(token):
if ctx.trust_id is None:
raise Exception(
"'trust_id' must be provided in the admin context."
)
# trust_client = client_for_trusts(ctx.trust_id)
# Using trust client, it can't validate token
# when cron trigger running because keystone policy
# don't allow do this. So we need use admin client to
# get token data
token_data = _admin_client().tokens.get_token_data(
token,
include_catalog=True
)
response = token_data['token']
else:
response = ctx.service_catalog
# Target service catalog may not be passed via API.
# If we don't have the catalog yet, it should be requested.
if not response:
response = client().tokens.get_token_data(
token,
include_catalog=True
)['token']
if not response:
raise exceptions.UnauthorizedException()
service_catalog = ks_service_catalog.ServiceCatalog.factory(response)
return service_catalog
def get_keystone_endpoint():
return get_endpoint_for_project('keystone', service_type='identity')
def get_keystone_url():
return get_endpoint_for_project('keystone', service_type='identity').url
def format_url(url_template, values):
# Since we can't use keystone module, we can do similar thing:
# see https://github.com/openstack/keystone/blob/master/keystone/
# catalog/core.py#L42-L60
return url_template.replace('$(', '%(') % values
def is_token_trust_scoped(auth_token):
return 'OS-TRUST:trust' in client_for_admin().tokens.validate(auth_token)
def get_admin_session():
"""Returns a keystone session from Mistral's service credentials."""
if CONF.keystone_authtoken.auth_type is None:
auth = auth_plugins.Password(
CONF.keystone_authtoken.www_authenticate_uri,
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_name=CONF.keystone_authtoken.admin_tenant_name,
# NOTE(jaosorior): Once mistral supports keystone v3 properly, we
# can fetch the following values from the configuration.
user_domain_name='Default',
project_domain_name='Default')
return ks_session.Session(auth=auth)
else:
auth = loading.load_auth_from_conf_options(
CONF,
'keystone_authtoken'
)
return loading.load_session_from_conf_options(
CONF,
'keystone',
auth=auth
)

56
mistral_extra/config.py Normal file
View File

@ -0,0 +1,56 @@
# Copyright 2020 - Nokia Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from oslo_config import cfg
os_actions_mapping_path = cfg.StrOpt(
'openstack_actions_mapping_path',
short='m',
metavar='MAPPING_PATH',
default='actions/openstack/mapping.json',
help='Path to openstack action mapping json file.'
'It could be relative to mistral package '
'directory or absolute.'
)
openstack_actions_opts = [
cfg.StrOpt(
'os-actions-endpoint-type',
default=os.environ.get('OS_ACTIONS_ENDPOINT_TYPE', 'public'),
choices=['public', 'admin', 'internal'],
deprecated_group='DEFAULT',
help='Type of endpoint in identity service catalog to use for'
' communication with OpenStack services.'
),
cfg.ListOpt(
'modules-support-region',
default=['nova', 'glance', 'heat', 'neutron', 'cinder',
'trove', 'ironic', 'designate', 'murano', 'tacker', 'senlin',
'aodh', 'gnocchi'],
help='List of module names that support region in actions.'
),
cfg.StrOpt(
'default_region',
help='Default region name for openstack actions supporting region.'
),
]
OPENSTACK_ACTIONS_GROUP = 'openstack_actions'
CONF = cfg.CONF
CONF.register_opts(openstack_actions_opts, group=OPENSTACK_ACTIONS_GROUP)
CONF.register_opt(os_actions_mapping_path)

View File

@ -0,0 +1,16 @@
{
"_comment": "Mapping OpenStack action namespaces to all its actions. Each action name is mapped to python-client method name in this namespace.",
"nova": {
"servers_get": "servers.get",
"servers_find": "servers.find",
"volumes_delete_server_volume": "volumes.delete_server_volume"
},
"keystone": {
"users_list": "users.list",
"trusts_create": "trusts.create"
},
"glance": {
"images_list": "images.list",
"images_delete": "images.delete"
}
}

View File

@ -0,0 +1,194 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import os
from oslo_config import cfg
import mock
from mistral_extra.actions import generator_factory
from mistral_extra.actions.openstack.action_generator import base as \
generator_base
from mistral_extra.actions.openstack import actions
from mistral_extra import config
from mistral_extra.tests.unit import base
ABSOLUTE_TEST_MAPPING_PATH = os.path.realpath(
os.path.join(os.path.dirname(__file__),
"../../../resources/openstack/test_mapping.json")
)
RELATIVE_TEST_MAPPING_PATH = "tests/resources/openstack/test_mapping.json"
MODULE_MAPPING = {
'nova': ['nova.servers_get', actions.NovaAction],
'glance': ['glance.images_list', actions.GlanceAction],
'keystone': ['keystone.users_create', actions.KeystoneAction],
'heat': ['heat.stacks_list', actions.HeatAction],
'neutron': ['neutron.show_network', actions.NeutronAction],
'cinder': ['cinder.volumes_list', actions.CinderAction],
'trove': ['trove.instances_list', actions.TroveAction],
'ironic': ['ironic.node_list', actions.IronicAction],
'baremetal_introspection': ['baremetal_introspection.introspect',
actions.BaremetalIntrospectionAction],
'swift': ['swift.head_account', actions.SwiftAction],
'swiftservice': ['swiftservice.delete', actions.SwiftServiceAction],
'zaqar': ['zaqar.queue_messages', actions.ZaqarAction],
'barbican': ['barbican.orders_list', actions.BarbicanAction],
'mistral': ['mistral.workflows_get', actions.MistralAction],
'designate': ['designate.quotas_list', actions.DesignateAction],
'manila': ['manila.shares_list', actions.ManilaAction],
'magnum': ['magnum.bays_list', actions.MagnumAction],
'murano': ['murano.deployments_list', actions.MuranoAction],
'tacker': ['tacker.list_vims', actions.TackerAction],
'senlin': ['senlin.get_profile', actions.SenlinAction],
'aodh': ['aodh.alarm_list', actions.AodhAction],
'gnocchi': ['gnocchi.metric_list', actions.GnocchiAction],
'glare': ['glare.artifacts_list', actions.GlareAction],
'vitrage': ['vitrage.alarm_get', actions.VitrageAction],
'zun': ['zun.containers_list', actions.ZunAction],
'qinling': ['qinling.runtimes_list', actions.QinlingAction]
}
EXTRA_MODULES = ['neutron', 'swift', 'zaqar', 'tacker', 'senlin']
CONF = cfg.CONF
CONF.register_opt(config.os_actions_mapping_path)
class GeneratorTest(base.BaseTest):
def setUp(self):
super(GeneratorTest, self).setUp()
# The baremetal inspector client expects the service to be running
# when it is initialised and attempts to connect. This mocks out this
# service only and returns a simple function that can be used by the
# inspection utils.
self.baremetal_patch = mock.patch.object(
actions.BaremetalIntrospectionAction,
"get_fake_client_method",
return_value=lambda x: None)
self.baremetal_patch.start()
self.addCleanup(self.baremetal_patch.stop)
# Do the same for the Designate client.
self.designate_patch = mock.patch.object(
actions.DesignateAction,
"get_fake_client_method",
return_value=lambda x: None)
self.designate_patch.start()
self.addCleanup(self.designate_patch.stop)
def test_generator(self):
for generator_cls in generator_factory.all_generators():
action_classes = generator_cls.create_actions()
action_name = MODULE_MAPPING[generator_cls.action_namespace][0]
action_cls = MODULE_MAPPING[generator_cls.action_namespace][1]
method_name_pre = action_name.split('.')[1]
method_name = (
method_name_pre
if generator_cls.action_namespace in EXTRA_MODULES
else method_name_pre.replace('_', '.')
)
action = self._assert_single_item(
action_classes,
name=action_name
)
self.assertTrue(issubclass(action['class'], action_cls))
self.assertEqual(method_name, action['class'].client_method_name)
modules = CONF.openstack_actions.modules_support_region
if generator_cls.action_namespace in modules:
self.assertIn('action_region', action['arg_list'])
def test_missing_module_from_mapping(self):
with _patch_openstack_action_mapping_path(RELATIVE_TEST_MAPPING_PATH):
for generator_cls in generator_factory.all_generators():
action_classes = generator_cls.create_actions()
action_names = [action['name'] for action in action_classes]
cls = MODULE_MAPPING.get(generator_cls.action_namespace)[1]
if cls == actions.NovaAction:
self.assertIn('nova.servers_get', action_names)
self.assertEqual(3, len(action_names))
elif cls not in (actions.GlanceAction, actions.KeystoneAction):
self.assertEqual([], action_names)
def test_absolute_mapping_path(self):
with _patch_openstack_action_mapping_path(ABSOLUTE_TEST_MAPPING_PATH):
self.assertTrue(os.path.isabs(ABSOLUTE_TEST_MAPPING_PATH),
"Mapping path is relative: %s" %
ABSOLUTE_TEST_MAPPING_PATH)
for generator_cls in generator_factory.all_generators():
action_classes = generator_cls.create_actions()
action_names = [action['name'] for action in action_classes]
cls = MODULE_MAPPING.get(generator_cls.action_namespace)[1]
if cls == actions.NovaAction:
self.assertIn('nova.servers_get', action_names)
self.assertEqual(3, len(action_names))
elif cls not in (actions.GlanceAction, actions.KeystoneAction):
self.assertEqual([], action_names)
def test_prepare_action_inputs(self):
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
'a,b,c',
added=['region=RegionOne']
)
self.assertEqual('a, b, c, region=RegionOne', inputs)
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
'a,b,c=1',
added=['region=RegionOne']
)
self.assertEqual('a, b, region=RegionOne, c=1', inputs)
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
'a,b,c=1,**kwargs',
added=['region=RegionOne']
)
self.assertEqual('a, b, region=RegionOne, c=1, **kwargs', inputs)
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
'**kwargs',
added=['region=RegionOne']
)
self.assertEqual('region=RegionOne, **kwargs', inputs)
inputs = generator_base.OpenStackActionGenerator.prepare_action_inputs(
'',
added=['region=RegionOne']
)
self.assertEqual('region=RegionOne', inputs)
@contextlib.contextmanager
def _patch_openstack_action_mapping_path(path):
original_path = CONF.openstack_actions_mapping_path
CONF.set_default("openstack_actions_mapping_path", path)
yield
CONF.set_default("openstack_actions_mapping_path", original_path)

View File

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

View File

@ -22,3 +22,31 @@ LOG = logging.getLogger(__name__)
class BaseTest(base.BaseTestCase):
def setUp(self):
super(BaseTest, self).setUp()
def _assert_single_item(self, items, **props):
return self._assert_multiple_items(items, 1, **props)[0]
def _assert_multiple_items(self, items, count, **props):
def _matches(item, **props):
for prop_name, prop_val in props.items():
v = item[prop_name] if isinstance(
item, dict) else getattr(item, prop_name)
if v != prop_val:
return False
return True
filtered_items = list(
[item for item in items if _matches(item, **props)]
)
found = len(filtered_items)
if found != count:
LOG.info("[failed test ctx] items=%s, expected_props=%s", str(
items), props)
self.fail("Wrong number of items found [props=%s, "
"expected=%s, found=%s]" % (props, count, found))
return filtered_items

18
mistral_extra/version.py Normal file
View File

@ -0,0 +1,18 @@
# Copyright 2013 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pbr import version
version_info = version.VersionInfo('mistral_extra')
version_string = version_info.version_string()

View File

@ -0,0 +1,3 @@
upgrade:
- |
Move all OpenStack actions to mistral-extra

View File

276
releasenotes/source/conf.py Normal file
View File

@ -0,0 +1,276 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Mistral-lib Release Notes documentation build configuration file, created by
# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'openstackdocstheme',
'reno.sphinxext',
]
# Add any paths that contain templates here, relative to this directory.
# templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'mistral-extra Release Notes'
copyright = u'2016, OpenStack Foundation'
# Release notes are version independent
release = ''
version = ''
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# Must set this variable to include year, month, day, hours, and minutes.
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
html_use_index = False
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'MistralextraReleaseNotesdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'MistralExtraReleaseNotes.tex',
u'Mistral Extra Release Notes Documentation', u'Mistral '
u'Extra Developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'mistralextrareleasenotes', u'Mistral Extra Release Notes '
u'Documentation', [u'Mistral Extra Developers'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'MistralExtraReleaseNotes', u'Mistral Library Release Notes '
u'Documentation', u'Mistral Extra Developers',
'MistralExtraReleaseNotes', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
# -- Options for Internationalization output ------------------------------
locale_dirs = ['locale/']
# -- Options for openstackdocstheme -------------------------------------------
repository_name = 'openstack/mistral-extra'
bug_project = 'mistral-extra'
bug_tag = ''

View File

@ -0,0 +1,13 @@
===========================
mistral-extra Release Notes
===========================
.. toctree::
:maxdepth: 1
unreleased
train
stein
rocky
queens
pike

View File

@ -0,0 +1,6 @@
=========================
Pike Series Release Notes
=========================
.. release-notes::
:branch: stable/pike

View File

@ -0,0 +1,6 @@
===========================
Queens Series Release Notes
===========================
.. release-notes::
:branch: stable/queens

View File

@ -0,0 +1,6 @@
==========================
Rocky Series Release Notes
==========================
.. release-notes::
:branch: stable/rocky

View File

@ -0,0 +1,6 @@
==========================
Stein Series Release Notes
==========================
.. release-notes::
:branch: stable/stein

View File

@ -0,0 +1,6 @@
==========================
Train Series Release Notes
==========================
.. release-notes::
:branch: stable/train

View File

@ -0,0 +1,5 @@
============================
Current Series Release Notes
============================
.. release-notes::

View File

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

View File

@ -22,6 +22,10 @@ classifier =
packages =
mistral-extra
[entry_points]
mistral.generators =
generators = mistral_extra.actions.generator_factory:all_generators
[build_sphinx]
source-dir = doc/source
build-dir = doc/build

368
tools/get_action_list.py Normal file
View File

@ -0,0 +1,368 @@
# Copyright 2020 - Nokia Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import collections
import inspect
import json
import os
from aodhclient.v2 import base as aodh_base
from aodhclient.v2 import client as aodhclient
from barbicanclient import base as barbican_base
from barbicanclient import client as barbicanclient
from cinderclient.apiclient import base as cinder_base
from cinderclient.v2 import client as cinderclient
from designateclient import client as designateclient
from glanceclient.v2 import client as glanceclient
from glareclient.v1 import client as glareclient
from gnocchiclient.v1 import base as gnocchi_base
from gnocchiclient.v1 import client as gnocchiclient
from heatclient.common import base as heat_base
from heatclient.v1 import client as heatclient
from ironicclient.common import base as ironic_base
from ironicclient.v1 import client as ironicclient
from keystoneclient import base as keystone_base
from keystoneclient.v3 import client as keystoneclient
from magnumclient.common import base as magnum_base
from magnumclient.v1 import client as magnumclient
from manilaclient import base as manila_base
from manilaclient.v2 import client as manilaclient
from mistralclient.api import base as mistral_base
from mistralclient.api.v2 import client as mistralclient
from muranoclient.common import base as murano_base
from muranoclient.v1 import client as muranoclient
from novaclient import base as nova_base
from novaclient import client as novaclient
from troveclient import base as trove_base
from troveclient.v1 import client as troveclient
# TODO(nmakhotkin): Find a rational way to do it for neutron.
# TODO(nmakhotkin): Implement recursive way of searching for managers
# TODO(nmakhotkin): (e.g. keystone).
# TODO(dprince): Need to update ironic_inspector_client before we can
# plug it in cleanly here.
# TODO(dprince): Swiftclient doesn't currently support discovery
# like we do in this class.
# TODO(therve): Zaqarclient doesn't currently support discovery
# like we do in this class.
# TODO(sa709c): Tackerclient doesn't currently support discovery
# like we do in this class.
"""It is simple CLI tool which allows to see and update mapping.json file
if needed. mapping.json contains all allowing OpenStack actions sorted by
service name. Usage example:
python tools/get_action_list.py nova
The result will be simple JSON containing action name as a key and method
path as a value. For updating mapping.json it is need to copy all keys and
values of the result to corresponding section of mapping.json:
...mapping.json...
"nova": {
<put it here>
},
...mapping.json...
Note: in case of Keystone service, correct OS_AUTH_URL v3 and the rest auth
info must be provided. It can be provided either via environment variables
or CLI arguments. See --help for details.
"""
BASE_HEAT_MANAGER = heat_base.HookableMixin
BASE_NOVA_MANAGER = nova_base.HookableMixin
BASE_KEYSTONE_MANAGER = keystone_base.Manager
BASE_CINDER_MANAGER = cinder_base.HookableMixin
BASE_MISTRAL_MANAGER = mistral_base.ResourceManager
BASE_TROVE_MANAGER = trove_base.Manager
BASE_IRONIC_MANAGER = ironic_base.Manager
BASE_BARBICAN_MANAGER = barbican_base.BaseEntityManager
BASE_MANILA_MANAGER = manila_base.Manager
BASE_MAGNUM_MANAGER = magnum_base.Manager
BASE_MURANO_MANAGER = murano_base.Manager
BASE_AODH_MANAGER = aodh_base.Manager
BASE_GNOCCHI_MANAGER = gnocchi_base.Manager
def get_parser():
parser = argparse.ArgumentParser(
description='Gets All needed methods of OpenStack clients.',
usage="python get_action_list.py <service_name>"
)
parser.add_argument(
'service',
choices=CLIENTS.keys(),
help='Service name which methods need to be found.'
)
parser.add_argument(
'--os-username',
dest='username',
default=os.environ.get('OS_USERNAME', 'admin'),
help='Authentication username (Env: OS_USERNAME)'
)
parser.add_argument(
'--os-password',
dest='password',
default=os.environ.get('OS_PASSWORD', 'openstack'),
help='Authentication password (Env: OS_PASSWORD)'
)
parser.add_argument(
'--os-tenant-name',
dest='tenant_name',
default=os.environ.get('OS_TENANT_NAME', 'Default'),
help='Authentication tenant name (Env: OS_TENANT_NAME)'
)
parser.add_argument(
'--os-auth-url',
dest='auth_url',
default=os.environ.get('OS_AUTH_URL'),
help='Authentication URL (Env: OS_AUTH_URL)'
)
return parser
GLANCE_NAMESPACE_LIST = [
'image_members', 'image_tags', 'images', 'schemas', 'tasks',
'metadefs_resource_type', 'metadefs_property', 'metadefs_object',
'metadefs_tag', 'metadefs_namespace', 'versions'
]
DESIGNATE_NAMESPACE_LIST = [
'diagnostics', 'domains', 'quotas', 'records', 'reports', 'servers',
'sync', 'touch'
]
GLARE_NAMESPACE_LIST = ['artifacts', 'versions']
def get_nova_client(**kwargs):
return novaclient.Client(2)
def get_keystone_client(**kwargs):
return keystoneclient.Client(**kwargs)
def get_glance_client(**kwargs):
return glanceclient.Client(kwargs.get('auth_url'))
def get_heat_client(**kwargs):
return heatclient.Client('')
def get_cinder_client(**kwargs):
return cinderclient.Client()
def get_mistral_client(**kwargs):
return mistralclient.Client()
def get_trove_client(**kwargs):
return troveclient.Client('username', 'password')
def get_ironic_client(**kwargs):
return ironicclient.Client("http://127.0.0.1:6385/")
def get_barbican_client(**kwargs):
return barbicanclient.Client(
project_id="1",
endpoint="http://127.0.0.1:9311"
)
def get_designate_client(**kwargs):
return designateclient.Client('2')
def get_magnum_client(**kwargs):
return magnumclient.Client()
def get_murano_client(**kwargs):
return muranoclient.Client('')
def get_aodh_client(**kwargs):
return aodhclient.Client('')
def get_gnocchi_client(**kwargs):
return gnocchiclient.Client()
def get_glare_client(**kwargs):
return glareclient.Client('')
def get_manila_client(**kwargs):
return manilaclient.Client(
input_auth_token='token',
service_catalog_url='http://127.0.0.1:8786'
)
CLIENTS = {
'nova': get_nova_client,
'heat': get_heat_client,
'cinder': get_cinder_client,
'keystone': get_keystone_client,
'glance': get_glance_client,
'trove': get_trove_client,
'ironic': get_ironic_client,
'barbican': get_barbican_client,
'mistral': get_mistral_client,
'designate': get_designate_client,
'magnum': get_magnum_client,
'murano': get_murano_client,
'aodh': get_aodh_client,
'gnocchi': get_gnocchi_client,
'glare': get_glare_client,
'manila': get_manila_client,
# 'neutron': get_nova_client
# 'baremetal_introspection': ...
# 'swift': ...
# 'zaqar': ...
}
BASE_MANAGERS = {
'nova': BASE_NOVA_MANAGER,
'heat': BASE_HEAT_MANAGER,
'cinder': BASE_CINDER_MANAGER,
'keystone': BASE_KEYSTONE_MANAGER,
'glance': None,
'trove': BASE_TROVE_MANAGER,
'ironic': BASE_IRONIC_MANAGER,
'barbican': BASE_BARBICAN_MANAGER,
'mistral': BASE_MISTRAL_MANAGER,
'designate': None,
'magnum': BASE_MAGNUM_MANAGER,
'murano': BASE_MURANO_MANAGER,
'aodh': BASE_AODH_MANAGER,
'gnocchi': BASE_GNOCCHI_MANAGER,
'glare': None,
'manila': BASE_MANILA_MANAGER,
# 'neutron': BASE_NOVA_MANAGER
# 'baremetal_introspection': ...
# 'swift': ...
# 'zaqar': ...
}
NAMESPACES = {
'glance': GLANCE_NAMESPACE_LIST,
'designate': DESIGNATE_NAMESPACE_LIST,
'glare': GLARE_NAMESPACE_LIST
}
ALLOWED_ATTRS = ['service_catalog', 'catalog']
FORBIDDEN_METHODS = [
'add_hook', 'alternate_service_type', 'completion_cache', 'run_hooks',
'write_to_completion_cache', 'model', 'build_key_only_query', 'build_url',
'head', 'put', 'unvalidated_model'
]
def get_public_attrs(obj):
all_attrs = dir(obj)
return [a for a in all_attrs if not a.startswith('_')]
def get_public_methods(attr, client):
hierarchy_list = attr.split('.')
attribute = client
for attr in hierarchy_list:
attribute = getattr(attribute, attr)
all_attributes_list = get_public_attrs(attribute)
methods = []
for a in all_attributes_list:
allowed = a in ALLOWED_ATTRS
forbidden = a in FORBIDDEN_METHODS
if (not forbidden and
(allowed or inspect.ismethod(getattr(attribute, a)))):
methods.append(a)
return methods
def get_manager_list(service_name, client):
base_manager = BASE_MANAGERS[service_name]
if not base_manager:
return NAMESPACES[service_name]
public_attrs = get_public_attrs(client)
manager_list = []
for attr in public_attrs:
if (isinstance(getattr(client, attr), base_manager) or
attr in ALLOWED_ATTRS):
manager_list.append(attr)
return manager_list
def get_mapping_for_service(service, client):
mapping = collections.OrderedDict()
for man in get_manager_list(service, client):
public_methods = get_public_methods(man, client)
for method in public_methods:
key = "%s_%s" % (man, method)
value = "%s.%s" % (man, method)
mapping[key] = value
return mapping
def print_mapping(mapping):
print(json.dumps(mapping, indent=8, separators=(',', ': ')))
if __name__ == "__main__":
args = get_parser().parse_args()
auth_info = {
'username': args.username,
'tenant_name': args.tenant_name,
'password': args.password,
'auth_url': args.auth_url
}
service = args.service
client = CLIENTS.get(service)(**auth_info)
print("Find methods for service: %s..." % service)
print_mapping(get_mapping_for_service(service, client))

View File

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