Remove unneccesary oslo modules
* It was synchronised with oslo-inclubator using update.sh script Closes-bug: #1325573 Change-Id: Ie55e3e0f21d5019aeac8d5bf815cb0f0e4f887e2
This commit is contained in:
@@ -1,221 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# Copyright 2013 Spanish National Research Council.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
# E0202: An attribute inherited from %s hide this method
|
|
||||||
# pylint: disable=E0202
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
|
|
||||||
import six
|
|
||||||
from stevedore import extension
|
|
||||||
|
|
||||||
from mistral.openstack.common.apiclient import exceptions
|
|
||||||
|
|
||||||
|
|
||||||
_discovered_plugins = {}
|
|
||||||
|
|
||||||
|
|
||||||
def discover_auth_systems():
|
|
||||||
"""Discover the available auth-systems.
|
|
||||||
|
|
||||||
This won't take into account the old style auth-systems.
|
|
||||||
"""
|
|
||||||
global _discovered_plugins
|
|
||||||
_discovered_plugins = {}
|
|
||||||
|
|
||||||
def add_plugin(ext):
|
|
||||||
_discovered_plugins[ext.name] = ext.plugin
|
|
||||||
|
|
||||||
ep_namespace = "mistral.openstack.common.apiclient.auth"
|
|
||||||
mgr = extension.ExtensionManager(ep_namespace)
|
|
||||||
mgr.map(add_plugin)
|
|
||||||
|
|
||||||
|
|
||||||
def load_auth_system_opts(parser):
|
|
||||||
"""Load options needed by the available auth-systems into a parser.
|
|
||||||
|
|
||||||
This function will try to populate the parser with options from the
|
|
||||||
available plugins.
|
|
||||||
"""
|
|
||||||
group = parser.add_argument_group("Common auth options")
|
|
||||||
BaseAuthPlugin.add_common_opts(group)
|
|
||||||
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
|
||||||
group = parser.add_argument_group(
|
|
||||||
"Auth-system '%s' options" % name,
|
|
||||||
conflict_handler="resolve")
|
|
||||||
auth_plugin.add_opts(group)
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(auth_system):
|
|
||||||
try:
|
|
||||||
plugin_class = _discovered_plugins[auth_system]
|
|
||||||
except KeyError:
|
|
||||||
raise exceptions.AuthSystemNotFound(auth_system)
|
|
||||||
return plugin_class(auth_system=auth_system)
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugin_from_args(args):
|
|
||||||
"""Load required plugin and populate it with options.
|
|
||||||
|
|
||||||
Try to guess auth system if it is not specified. Systems are tried in
|
|
||||||
alphabetical order.
|
|
||||||
|
|
||||||
:type args: argparse.Namespace
|
|
||||||
:raises: AuthPluginOptionsMissing
|
|
||||||
"""
|
|
||||||
auth_system = args.os_auth_system
|
|
||||||
if auth_system:
|
|
||||||
plugin = load_plugin(auth_system)
|
|
||||||
plugin.parse_opts(args)
|
|
||||||
plugin.sufficient_options()
|
|
||||||
return plugin
|
|
||||||
|
|
||||||
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
|
|
||||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
|
||||||
plugin = plugin_class()
|
|
||||||
plugin.parse_opts(args)
|
|
||||||
try:
|
|
||||||
plugin.sufficient_options()
|
|
||||||
except exceptions.AuthPluginOptionsMissing:
|
|
||||||
continue
|
|
||||||
return plugin
|
|
||||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class BaseAuthPlugin(object):
|
|
||||||
"""Base class for authentication plugins.
|
|
||||||
|
|
||||||
An authentication plugin needs to override at least the authenticate
|
|
||||||
method to be a valid plugin.
|
|
||||||
"""
|
|
||||||
|
|
||||||
auth_system = None
|
|
||||||
opt_names = []
|
|
||||||
common_opt_names = [
|
|
||||||
"auth_system",
|
|
||||||
"username",
|
|
||||||
"password",
|
|
||||||
"tenant_name",
|
|
||||||
"token",
|
|
||||||
"auth_url",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, auth_system=None, **kwargs):
|
|
||||||
self.auth_system = auth_system or self.auth_system
|
|
||||||
self.opts = dict((name, kwargs.get(name))
|
|
||||||
for name in self.opt_names)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parser_add_opt(parser, opt):
|
|
||||||
"""Add an option to parser in two variants.
|
|
||||||
|
|
||||||
:param opt: option name (with underscores)
|
|
||||||
"""
|
|
||||||
dashed_opt = opt.replace("_", "-")
|
|
||||||
env_var = "OS_%s" % opt.upper()
|
|
||||||
arg_default = os.environ.get(env_var, "")
|
|
||||||
arg_help = "Defaults to env[%s]." % env_var
|
|
||||||
parser.add_argument(
|
|
||||||
"--os-%s" % dashed_opt,
|
|
||||||
metavar="<%s>" % dashed_opt,
|
|
||||||
default=arg_default,
|
|
||||||
help=arg_help)
|
|
||||||
parser.add_argument(
|
|
||||||
"--os_%s" % opt,
|
|
||||||
metavar="<%s>" % dashed_opt,
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_opts(cls, parser):
|
|
||||||
"""Populate the parser with the options for this plugin.
|
|
||||||
"""
|
|
||||||
for opt in cls.opt_names:
|
|
||||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
|
||||||
# changed in child classes
|
|
||||||
if opt not in BaseAuthPlugin.common_opt_names:
|
|
||||||
cls._parser_add_opt(parser, opt)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_common_opts(cls, parser):
|
|
||||||
"""Add options that are common for several plugins.
|
|
||||||
"""
|
|
||||||
for opt in cls.common_opt_names:
|
|
||||||
cls._parser_add_opt(parser, opt)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_opt(opt_name, args):
|
|
||||||
"""Return option name and value.
|
|
||||||
|
|
||||||
:param opt_name: name of the option, e.g., "username"
|
|
||||||
:param args: parsed arguments
|
|
||||||
"""
|
|
||||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
|
||||||
|
|
||||||
def parse_opts(self, args):
|
|
||||||
"""Parse the actual auth-system options if any.
|
|
||||||
|
|
||||||
This method is expected to populate the attribute `self.opts` with a
|
|
||||||
dict containing the options and values needed to make authentication.
|
|
||||||
"""
|
|
||||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
|
||||||
for opt_name in self.opt_names))
|
|
||||||
|
|
||||||
def authenticate(self, http_client):
|
|
||||||
"""Authenticate using plugin defined method.
|
|
||||||
|
|
||||||
The method usually analyses `self.opts` and performs
|
|
||||||
a request to authentication server.
|
|
||||||
|
|
||||||
:param http_client: client object that needs authentication
|
|
||||||
:type http_client: HTTPClient
|
|
||||||
:raises: AuthorizationFailure
|
|
||||||
"""
|
|
||||||
self.sufficient_options()
|
|
||||||
self._do_authenticate(http_client)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _do_authenticate(self, http_client):
|
|
||||||
"""Protected method for authentication.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def sufficient_options(self):
|
|
||||||
"""Check if all required options are present.
|
|
||||||
|
|
||||||
:raises: AuthPluginOptionsMissing
|
|
||||||
"""
|
|
||||||
missing = [opt
|
|
||||||
for opt in self.opt_names
|
|
||||||
if not self.opts.get(opt)]
|
|
||||||
if missing:
|
|
||||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def token_and_endpoint(self, endpoint_type, service_type):
|
|
||||||
"""Return token and endpoint.
|
|
||||||
|
|
||||||
:param service_type: Service type of the endpoint
|
|
||||||
:type service_type: string
|
|
||||||
:param endpoint_type: Type of endpoint.
|
|
||||||
Possible values: public or publicURL,
|
|
||||||
internal or internalURL,
|
|
||||||
admin or adminURL
|
|
||||||
:type endpoint_type: string
|
|
||||||
:returns: tuple of token and endpoint strings
|
|
||||||
:raises: EndpointException
|
|
||||||
"""
|
|
||||||
@@ -1,507 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# Copyright 2012 Grid Dynamics
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Base utilities to build API operation managers and objects on top of.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# E1102: %s is not callable
|
|
||||||
# pylint: disable=E1102
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import copy
|
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves.urllib import parse
|
|
||||||
|
|
||||||
from mistral.openstack.common.apiclient import exceptions
|
|
||||||
from mistral.openstack.common.gettextutils import _
|
|
||||||
from mistral.openstack.common import strutils
|
|
||||||
|
|
||||||
|
|
||||||
def getid(obj):
|
|
||||||
"""Return id if argument is a Resource.
|
|
||||||
|
|
||||||
Abstracts the common pattern of allowing both an object or an object's ID
|
|
||||||
(UUID) as a parameter when dealing with relationships.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if obj.uuid:
|
|
||||||
return obj.uuid
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return obj.id
|
|
||||||
except AttributeError:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
|
||||||
class HookableMixin(object):
|
|
||||||
"""Mixin so classes can register and run hooks."""
|
|
||||||
_hooks_map = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_hook(cls, hook_type, hook_func):
|
|
||||||
"""Add a new hook of specified type.
|
|
||||||
|
|
||||||
:param cls: class that registers hooks
|
|
||||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
|
||||||
:param hook_func: hook function
|
|
||||||
"""
|
|
||||||
if hook_type not in cls._hooks_map:
|
|
||||||
cls._hooks_map[hook_type] = []
|
|
||||||
|
|
||||||
cls._hooks_map[hook_type].append(hook_func)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
|
||||||
"""Run all hooks of specified type.
|
|
||||||
|
|
||||||
:param cls: class that registers hooks
|
|
||||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
|
||||||
:param args: args to be passed to every hook function
|
|
||||||
:param kwargs: kwargs to be passed to every hook function
|
|
||||||
"""
|
|
||||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
|
||||||
for hook_func in hook_funcs:
|
|
||||||
hook_func(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseManager(HookableMixin):
|
|
||||||
"""Basic manager type providing common operations.
|
|
||||||
|
|
||||||
Managers interact with a particular type of API (servers, flavors, images,
|
|
||||||
etc.) and provide CRUD operations for them.
|
|
||||||
"""
|
|
||||||
resource_class = None
|
|
||||||
|
|
||||||
def __init__(self, client):
|
|
||||||
"""Initializes BaseManager with `client`.
|
|
||||||
|
|
||||||
:param client: instance of BaseClient descendant for HTTP requests
|
|
||||||
"""
|
|
||||||
super(BaseManager, self).__init__()
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
def _list(self, url, response_key, obj_class=None, json=None):
|
|
||||||
"""List the collection.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'
|
|
||||||
:param obj_class: class for constructing the returned objects
|
|
||||||
(self.resource_class will be used by default)
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
"""
|
|
||||||
if json:
|
|
||||||
body = self.client.post(url, json=json).json()
|
|
||||||
else:
|
|
||||||
body = self.client.get(url).json()
|
|
||||||
|
|
||||||
if obj_class is None:
|
|
||||||
obj_class = self.resource_class
|
|
||||||
|
|
||||||
data = body[response_key]
|
|
||||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
|
||||||
# unlike other services which just return the list...
|
|
||||||
try:
|
|
||||||
data = data['values']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
|
||||||
|
|
||||||
def _get(self, url, response_key):
|
|
||||||
"""Get an object from collection.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'server'
|
|
||||||
"""
|
|
||||||
body = self.client.get(url).json()
|
|
||||||
return self.resource_class(self, body[response_key], loaded=True)
|
|
||||||
|
|
||||||
def _head(self, url):
|
|
||||||
"""Retrieve request headers for an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
"""
|
|
||||||
resp = self.client.head(url)
|
|
||||||
return resp.status_code == 204
|
|
||||||
|
|
||||||
def _post(self, url, json, response_key, return_raw=False):
|
|
||||||
"""Create an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'
|
|
||||||
:param return_raw: flag to force returning raw JSON instead of
|
|
||||||
Python object of self.resource_class
|
|
||||||
"""
|
|
||||||
body = self.client.post(url, json=json).json()
|
|
||||||
if return_raw:
|
|
||||||
return body[response_key]
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
|
|
||||||
def _put(self, url, json=None, response_key=None):
|
|
||||||
"""Update an object with PUT method.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'
|
|
||||||
"""
|
|
||||||
resp = self.client.put(url, json=json)
|
|
||||||
# PUT requests may not return a body
|
|
||||||
if resp.content:
|
|
||||||
body = resp.json()
|
|
||||||
if response_key is not None:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
else:
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _patch(self, url, json=None, response_key=None):
|
|
||||||
"""Update an object with PATCH method.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'
|
|
||||||
"""
|
|
||||||
body = self.client.patch(url, json=json).json()
|
|
||||||
if response_key is not None:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
else:
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _delete(self, url):
|
|
||||||
"""Delete an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers/my-server'
|
|
||||||
"""
|
|
||||||
return self.client.delete(url)
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class ManagerWithFind(BaseManager):
|
|
||||||
"""Manager with additional `find()`/`findall()` methods."""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def list(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
"""Find a single item with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
matches = self.findall(**kwargs)
|
|
||||||
num_matches = len(matches)
|
|
||||||
if num_matches == 0:
|
|
||||||
msg = _("No %(name)s matching %(args)s.") % {
|
|
||||||
'name': self.resource_class.__name__,
|
|
||||||
'args': kwargs
|
|
||||||
}
|
|
||||||
raise exceptions.NotFound(msg)
|
|
||||||
elif num_matches > 1:
|
|
||||||
raise exceptions.NoUniqueMatch()
|
|
||||||
else:
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
def findall(self, **kwargs):
|
|
||||||
"""Find all items with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
found = []
|
|
||||||
searches = kwargs.items()
|
|
||||||
|
|
||||||
for obj in self.list():
|
|
||||||
try:
|
|
||||||
if all(getattr(obj, attr) == value
|
|
||||||
for (attr, value) in searches):
|
|
||||||
found.append(obj)
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
class CrudManager(BaseManager):
|
|
||||||
"""Base manager class for manipulating entities.
|
|
||||||
|
|
||||||
Children of this class are expected to define a `collection_key` and `key`.
|
|
||||||
|
|
||||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
|
||||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
|
||||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
|
||||||
{}, {}]}`).
|
|
||||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
|
||||||
refer to an individual member of the collection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
collection_key = None
|
|
||||||
key = None
|
|
||||||
|
|
||||||
def build_url(self, base_url=None, **kwargs):
|
|
||||||
"""Builds a resource URL for the given kwargs.
|
|
||||||
|
|
||||||
Given an example collection where `collection_key = 'entities'` and
|
|
||||||
`key = 'entity'`, the following URL's could be generated.
|
|
||||||
|
|
||||||
By default, the URL will represent a collection of entities, e.g.::
|
|
||||||
|
|
||||||
/entities
|
|
||||||
|
|
||||||
If kwargs contains an `entity_id`, then the URL will represent a
|
|
||||||
specific member, e.g.::
|
|
||||||
|
|
||||||
/entities/{entity_id}
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
url = base_url if base_url is not None else ''
|
|
||||||
|
|
||||||
url += '/%s' % self.collection_key
|
|
||||||
|
|
||||||
# do we have a specific entity?
|
|
||||||
entity_id = kwargs.get('%s_id' % self.key)
|
|
||||||
if entity_id is not None:
|
|
||||||
url += '/%s' % entity_id
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
def _filter_kwargs(self, kwargs):
|
|
||||||
"""Drop null values and handle ids."""
|
|
||||||
for key, ref in six.iteritems(kwargs.copy()):
|
|
||||||
if ref is None:
|
|
||||||
kwargs.pop(key)
|
|
||||||
else:
|
|
||||||
if isinstance(ref, Resource):
|
|
||||||
kwargs.pop(key)
|
|
||||||
kwargs['%s_id' % key] = getid(ref)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._post(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
{self.key: kwargs},
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._get(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def head(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._head(self.build_url(**kwargs))
|
|
||||||
|
|
||||||
def list(self, base_url=None, **kwargs):
|
|
||||||
"""List the collection.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._list(
|
|
||||||
'%(base_url)s%(query)s' % {
|
|
||||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
|
||||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
|
||||||
},
|
|
||||||
self.collection_key)
|
|
||||||
|
|
||||||
def put(self, base_url=None, **kwargs):
|
|
||||||
"""Update an element.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
|
||||||
|
|
||||||
def update(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
params = kwargs.copy()
|
|
||||||
params.pop('%s_id' % self.key)
|
|
||||||
|
|
||||||
return self._patch(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
{self.key: params},
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._delete(
|
|
||||||
self.build_url(**kwargs))
|
|
||||||
|
|
||||||
def find(self, base_url=None, **kwargs):
|
|
||||||
"""Find a single item with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
rl = self._list(
|
|
||||||
'%(base_url)s%(query)s' % {
|
|
||||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
|
||||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
|
||||||
},
|
|
||||||
self.collection_key)
|
|
||||||
num = len(rl)
|
|
||||||
|
|
||||||
if num == 0:
|
|
||||||
msg = _("No %(name)s matching %(args)s.") % {
|
|
||||||
'name': self.resource_class.__name__,
|
|
||||||
'args': kwargs
|
|
||||||
}
|
|
||||||
raise exceptions.NotFound(404, msg)
|
|
||||||
elif num > 1:
|
|
||||||
raise exceptions.NoUniqueMatch
|
|
||||||
else:
|
|
||||||
return rl[0]
|
|
||||||
|
|
||||||
|
|
||||||
class Extension(HookableMixin):
|
|
||||||
"""Extension descriptor."""
|
|
||||||
|
|
||||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
|
||||||
manager_class = None
|
|
||||||
|
|
||||||
def __init__(self, name, module):
|
|
||||||
super(Extension, self).__init__()
|
|
||||||
self.name = name
|
|
||||||
self.module = module
|
|
||||||
self._parse_extension_module()
|
|
||||||
|
|
||||||
def _parse_extension_module(self):
|
|
||||||
self.manager_class = None
|
|
||||||
for attr_name, attr_value in self.module.__dict__.items():
|
|
||||||
if attr_name in self.SUPPORTED_HOOKS:
|
|
||||||
self.add_hook(attr_name, attr_value)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if issubclass(attr_value, BaseManager):
|
|
||||||
self.manager_class = attr_value
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Extension '%s'>" % self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
|
||||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
|
||||||
|
|
||||||
This is pretty much just a bag for attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
HUMAN_ID = False
|
|
||||||
NAME_ATTR = 'name'
|
|
||||||
|
|
||||||
def __init__(self, manager, info, loaded=False):
|
|
||||||
"""Populate and bind to a manager.
|
|
||||||
|
|
||||||
:param manager: BaseManager object
|
|
||||||
:param info: dictionary representing resource attributes
|
|
||||||
:param loaded: prevent lazy-loading if set to True
|
|
||||||
"""
|
|
||||||
self.manager = manager
|
|
||||||
self._info = info
|
|
||||||
self._add_details(info)
|
|
||||||
self._loaded = loaded
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
reprkeys = sorted(k
|
|
||||||
for k in self.__dict__.keys()
|
|
||||||
if k[0] != '_' and k != 'manager')
|
|
||||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
|
||||||
return "<%s %s>" % (self.__class__.__name__, info)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def human_id(self):
|
|
||||||
"""Human-readable ID which can be used for bash completion.
|
|
||||||
"""
|
|
||||||
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
|
|
||||||
return strutils.to_slug(getattr(self, self.NAME_ATTR))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _add_details(self, info):
|
|
||||||
for (k, v) in six.iteritems(info):
|
|
||||||
try:
|
|
||||||
setattr(self, k, v)
|
|
||||||
self._info[k] = v
|
|
||||||
except AttributeError:
|
|
||||||
# In this case we already defined the attribute on the class
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getattr__(self, k):
|
|
||||||
if k not in self.__dict__:
|
|
||||||
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
|
||||||
if not self.is_loaded():
|
|
||||||
self.get()
|
|
||||||
return self.__getattr__(k)
|
|
||||||
|
|
||||||
raise AttributeError(k)
|
|
||||||
else:
|
|
||||||
return self.__dict__[k]
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""Support for lazy loading details.
|
|
||||||
|
|
||||||
Some clients, such as novaclient have the option to lazy load the
|
|
||||||
details, details which can be loaded with this function.
|
|
||||||
"""
|
|
||||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
|
||||||
self.set_loaded(True)
|
|
||||||
if not hasattr(self.manager, 'get'):
|
|
||||||
return
|
|
||||||
|
|
||||||
new = self.manager.get(self.id)
|
|
||||||
if new:
|
|
||||||
self._add_details(new._info)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, Resource):
|
|
||||||
return NotImplemented
|
|
||||||
# two resources of different types are not equal
|
|
||||||
if not isinstance(other, self.__class__):
|
|
||||||
return False
|
|
||||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
|
||||||
return self.id == other.id
|
|
||||||
return self._info == other._info
|
|
||||||
|
|
||||||
def is_loaded(self):
|
|
||||||
return self._loaded
|
|
||||||
|
|
||||||
def set_loaded(self, val):
|
|
||||||
self._loaded = val
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return copy.deepcopy(self._info)
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
|
||||||
# Copyright 2013 Alessio Ababilov
|
|
||||||
# Copyright 2013 Grid Dynamics
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
OpenStack Client interface. Handles the REST calls and responses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# E0202: An attribute inherited from %s hide this method
|
|
||||||
# pylint: disable=E0202
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from mistral.openstack.common.apiclient import exceptions
|
|
||||||
from mistral.openstack.common.gettextutils import _
|
|
||||||
from mistral.openstack.common import importutils
|
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
|
||||||
"""This client handles sending HTTP requests to OpenStack servers.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
- share authentication information between several clients to different
|
|
||||||
services (e.g., for compute and image clients);
|
|
||||||
- reissue authentication request for expired tokens;
|
|
||||||
- encode/decode JSON bodies;
|
|
||||||
- raise exceptions on HTTP errors;
|
|
||||||
- pluggable authentication;
|
|
||||||
- store authentication information in a keyring;
|
|
||||||
- store time spent for requests;
|
|
||||||
- register clients for particular services, so one can use
|
|
||||||
`http_client.identity` or `http_client.compute`;
|
|
||||||
- log requests and responses in a format that is easy to copy-and-paste
|
|
||||||
into terminal and send the same request with curl.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user_agent = "mistral.openstack.common.apiclient"
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
auth_plugin,
|
|
||||||
region_name=None,
|
|
||||||
endpoint_type="publicURL",
|
|
||||||
original_ip=None,
|
|
||||||
verify=True,
|
|
||||||
cert=None,
|
|
||||||
timeout=None,
|
|
||||||
timings=False,
|
|
||||||
keyring_saver=None,
|
|
||||||
debug=False,
|
|
||||||
user_agent=None,
|
|
||||||
http=None):
|
|
||||||
self.auth_plugin = auth_plugin
|
|
||||||
|
|
||||||
self.endpoint_type = endpoint_type
|
|
||||||
self.region_name = region_name
|
|
||||||
|
|
||||||
self.original_ip = original_ip
|
|
||||||
self.timeout = timeout
|
|
||||||
self.verify = verify
|
|
||||||
self.cert = cert
|
|
||||||
|
|
||||||
self.keyring_saver = keyring_saver
|
|
||||||
self.debug = debug
|
|
||||||
self.user_agent = user_agent or self.user_agent
|
|
||||||
|
|
||||||
self.times = [] # [("item", starttime, endtime), ...]
|
|
||||||
self.timings = timings
|
|
||||||
|
|
||||||
# requests within the same session can reuse TCP connections from pool
|
|
||||||
self.http = http or requests.Session()
|
|
||||||
|
|
||||||
self.cached_token = None
|
|
||||||
|
|
||||||
def _http_log_req(self, method, url, kwargs):
|
|
||||||
if not self.debug:
|
|
||||||
return
|
|
||||||
|
|
||||||
string_parts = [
|
|
||||||
"curl -i",
|
|
||||||
"-X '%s'" % method,
|
|
||||||
"'%s'" % url,
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in kwargs['headers']:
|
|
||||||
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
|
||||||
string_parts.append(header)
|
|
||||||
|
|
||||||
_logger.debug("REQ: %s" % " ".join(string_parts))
|
|
||||||
if 'data' in kwargs:
|
|
||||||
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
|
||||||
|
|
||||||
def _http_log_resp(self, resp):
|
|
||||||
if not self.debug:
|
|
||||||
return
|
|
||||||
_logger.debug(
|
|
||||||
"RESP: [%s] %s\n",
|
|
||||||
resp.status_code,
|
|
||||||
resp.headers)
|
|
||||||
if resp._content_consumed:
|
|
||||||
_logger.debug(
|
|
||||||
"RESP BODY: %s\n",
|
|
||||||
resp.text)
|
|
||||||
|
|
||||||
def serialize(self, kwargs):
|
|
||||||
if kwargs.get('json') is not None:
|
|
||||||
kwargs['headers']['Content-Type'] = 'application/json'
|
|
||||||
kwargs['data'] = json.dumps(kwargs['json'])
|
|
||||||
try:
|
|
||||||
del kwargs['json']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_timings(self):
|
|
||||||
return self.times
|
|
||||||
|
|
||||||
def reset_timings(self):
|
|
||||||
self.times = []
|
|
||||||
|
|
||||||
def request(self, method, url, **kwargs):
|
|
||||||
"""Send an http request with the specified characteristics.
|
|
||||||
|
|
||||||
Wrapper around `requests.Session.request` to handle tasks such as
|
|
||||||
setting headers, JSON encoding/decoding, and error handling.
|
|
||||||
|
|
||||||
:param method: method of HTTP request
|
|
||||||
:param url: URL of HTTP request
|
|
||||||
:param kwargs: any other parameter that can be passed to
|
|
||||||
requests.Session.request (such as `headers`) or `json`
|
|
||||||
that will be encoded as JSON and used as `data` argument
|
|
||||||
"""
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
|
||||||
if self.original_ip:
|
|
||||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
|
||||||
self.original_ip, self.user_agent)
|
|
||||||
if self.timeout is not None:
|
|
||||||
kwargs.setdefault("timeout", self.timeout)
|
|
||||||
kwargs.setdefault("verify", self.verify)
|
|
||||||
if self.cert is not None:
|
|
||||||
kwargs.setdefault("cert", self.cert)
|
|
||||||
self.serialize(kwargs)
|
|
||||||
|
|
||||||
self._http_log_req(method, url, kwargs)
|
|
||||||
if self.timings:
|
|
||||||
start_time = time.time()
|
|
||||||
resp = self.http.request(method, url, **kwargs)
|
|
||||||
if self.timings:
|
|
||||||
self.times.append(("%s %s" % (method, url),
|
|
||||||
start_time, time.time()))
|
|
||||||
self._http_log_resp(resp)
|
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
_logger.debug(
|
|
||||||
"Request returned failure status: %s",
|
|
||||||
resp.status_code)
|
|
||||||
raise exceptions.from_response(resp, method, url)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def concat_url(endpoint, url):
|
|
||||||
"""Concatenate endpoint and final URL.
|
|
||||||
|
|
||||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
|
||||||
"http://keystone/v2.0/tokens".
|
|
||||||
|
|
||||||
:param endpoint: the base URL
|
|
||||||
:param url: the final URL
|
|
||||||
"""
|
|
||||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
|
||||||
|
|
||||||
def client_request(self, client, method, url, **kwargs):
|
|
||||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
|
||||||
|
|
||||||
If request was rejected as unauthorized (possibly because the token is
|
|
||||||
expired), issue one authorization attempt and send the request once
|
|
||||||
again.
|
|
||||||
|
|
||||||
:param client: instance of BaseClient descendant
|
|
||||||
:param method: method of HTTP request
|
|
||||||
:param url: URL of HTTP request
|
|
||||||
:param kwargs: any other parameter that can be passed to
|
|
||||||
`HTTPClient.request`
|
|
||||||
"""
|
|
||||||
|
|
||||||
filter_args = {
|
|
||||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
|
||||||
"service_type": client.service_type,
|
|
||||||
}
|
|
||||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
|
||||||
just_authenticated = False
|
|
||||||
if not (token and endpoint):
|
|
||||||
try:
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
except exceptions.EndpointException:
|
|
||||||
pass
|
|
||||||
if not (token and endpoint):
|
|
||||||
self.authenticate()
|
|
||||||
just_authenticated = True
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
if not (token and endpoint):
|
|
||||||
raise exceptions.AuthorizationFailure(
|
|
||||||
_("Cannot find endpoint or token for request"))
|
|
||||||
|
|
||||||
old_token_endpoint = (token, endpoint)
|
|
||||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
|
||||||
self.cached_token = token
|
|
||||||
client.cached_endpoint = endpoint
|
|
||||||
# Perform the request once. If we get Unauthorized, then it
|
|
||||||
# might be because the auth token expired, so try to
|
|
||||||
# re-authenticate and try again. If it still fails, bail.
|
|
||||||
try:
|
|
||||||
return self.request(
|
|
||||||
method, self.concat_url(endpoint, url), **kwargs)
|
|
||||||
except exceptions.Unauthorized as unauth_ex:
|
|
||||||
if just_authenticated:
|
|
||||||
raise
|
|
||||||
self.cached_token = None
|
|
||||||
client.cached_endpoint = None
|
|
||||||
self.authenticate()
|
|
||||||
try:
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
except exceptions.EndpointException:
|
|
||||||
raise unauth_ex
|
|
||||||
if (not (token and endpoint) or
|
|
||||||
old_token_endpoint == (token, endpoint)):
|
|
||||||
raise unauth_ex
|
|
||||||
self.cached_token = token
|
|
||||||
client.cached_endpoint = endpoint
|
|
||||||
kwargs["headers"]["X-Auth-Token"] = token
|
|
||||||
return self.request(
|
|
||||||
method, self.concat_url(endpoint, url), **kwargs)
|
|
||||||
|
|
||||||
def add_client(self, base_client_instance):
|
|
||||||
"""Add a new instance of :class:`BaseClient` descendant.
|
|
||||||
|
|
||||||
`self` will store a reference to `base_client_instance`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> def test_clients():
|
|
||||||
... from keystoneclient.auth import keystone
|
|
||||||
... from openstack.common.apiclient import client
|
|
||||||
... auth = keystone.KeystoneAuthPlugin(
|
|
||||||
... username="user", password="pass", tenant_name="tenant",
|
|
||||||
... auth_url="http://auth:5000/v2.0")
|
|
||||||
... openstack_client = client.HTTPClient(auth)
|
|
||||||
... # create nova client
|
|
||||||
... from novaclient.v1_1 import client
|
|
||||||
... client.Client(openstack_client)
|
|
||||||
... # create keystone client
|
|
||||||
... from keystoneclient.v2_0 import client
|
|
||||||
... client.Client(openstack_client)
|
|
||||||
... # use them
|
|
||||||
... openstack_client.identity.tenants.list()
|
|
||||||
... openstack_client.compute.servers.list()
|
|
||||||
"""
|
|
||||||
service_type = base_client_instance.service_type
|
|
||||||
if service_type and not hasattr(self, service_type):
|
|
||||||
setattr(self, service_type, base_client_instance)
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
self.auth_plugin.authenticate(self)
|
|
||||||
# Store the authentication results in the keyring for later requests
|
|
||||||
if self.keyring_saver:
|
|
||||||
self.keyring_saver.save(self)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClient(object):
|
|
||||||
"""Top-level object to access the OpenStack API.
|
|
||||||
|
|
||||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
|
||||||
will handle a bunch of issues such as authentication.
|
|
||||||
"""
|
|
||||||
|
|
||||||
service_type = None
|
|
||||||
endpoint_type = None # "publicURL" will be used
|
|
||||||
cached_endpoint = None
|
|
||||||
|
|
||||||
def __init__(self, http_client, extensions=None):
|
|
||||||
self.http_client = http_client
|
|
||||||
http_client.add_client(self)
|
|
||||||
|
|
||||||
# Add in any extensions...
|
|
||||||
if extensions:
|
|
||||||
for extension in extensions:
|
|
||||||
if extension.manager_class:
|
|
||||||
setattr(self, extension.name,
|
|
||||||
extension.manager_class(self))
|
|
||||||
|
|
||||||
def client_request(self, method, url, **kwargs):
|
|
||||||
return self.http_client.client_request(
|
|
||||||
self, method, url, **kwargs)
|
|
||||||
|
|
||||||
def head(self, url, **kwargs):
|
|
||||||
return self.client_request("HEAD", url, **kwargs)
|
|
||||||
|
|
||||||
def get(self, url, **kwargs):
|
|
||||||
return self.client_request("GET", url, **kwargs)
|
|
||||||
|
|
||||||
def post(self, url, **kwargs):
|
|
||||||
return self.client_request("POST", url, **kwargs)
|
|
||||||
|
|
||||||
def put(self, url, **kwargs):
|
|
||||||
return self.client_request("PUT", url, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, url, **kwargs):
|
|
||||||
return self.client_request("DELETE", url, **kwargs)
|
|
||||||
|
|
||||||
def patch(self, url, **kwargs):
|
|
||||||
return self.client_request("PATCH", url, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_class(api_name, version, version_map):
|
|
||||||
"""Returns the client class for the requested API version
|
|
||||||
|
|
||||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
|
||||||
:param version: the requested API version
|
|
||||||
:param version_map: a dict of client classes keyed by version
|
|
||||||
:rtype: a client class for the requested API version
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client_path = version_map[str(version)]
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
|
||||||
"Must be one of: %(version_map)s") % {
|
|
||||||
'api_name': api_name,
|
|
||||||
'version': version,
|
|
||||||
'version_map': ', '.join(version_map.keys())
|
|
||||||
}
|
|
||||||
raise exceptions.UnsupportedVersion(msg)
|
|
||||||
|
|
||||||
return importutils.import_class(client_path)
|
|
||||||
@@ -1,466 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 Nebula, Inc.
|
|
||||||
# Copyright 2013 Alessio Ababilov
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Exception definitions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from mistral.openstack.common.gettextutils import _
|
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
|
||||||
"""The base exception class for all exceptions this library raises.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingArgs(ClientException):
|
|
||||||
"""Supplied arguments are not sufficient for calling a function."""
|
|
||||||
def __init__(self, missing):
|
|
||||||
self.missing = missing
|
|
||||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
|
||||||
super(MissingArgs, self).__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(ClientException):
|
|
||||||
"""Error in validation on API client side."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedVersion(ClientException):
|
|
||||||
"""User is trying to use an unsupported version of the API."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(ClientException):
|
|
||||||
"""Error in CLI tool."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationFailure(ClientException):
|
|
||||||
"""Cannot authorize API client."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionRefused(ClientException):
|
|
||||||
"""Cannot connect to API service."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
|
||||||
"""Auth plugin misses some options."""
|
|
||||||
def __init__(self, opt_names):
|
|
||||||
super(AuthPluginOptionsMissing, self).__init__(
|
|
||||||
_("Authentication failed. Missing options: %s") %
|
|
||||||
", ".join(opt_names))
|
|
||||||
self.opt_names = opt_names
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSystemNotFound(AuthorizationFailure):
|
|
||||||
"""User has specified a AuthSystem that is not installed."""
|
|
||||||
def __init__(self, auth_system):
|
|
||||||
super(AuthSystemNotFound, self).__init__(
|
|
||||||
_("AuthSystemNotFound: %s") % repr(auth_system))
|
|
||||||
self.auth_system = auth_system
|
|
||||||
|
|
||||||
|
|
||||||
class NoUniqueMatch(ClientException):
|
|
||||||
"""Multiple entities found instead of one."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointException(ClientException):
|
|
||||||
"""Something is rotten in Service Catalog."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(EndpointException):
|
|
||||||
"""Could not find requested endpoint in Service Catalog."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AmbiguousEndpoints(EndpointException):
|
|
||||||
"""Found more than one matching endpoint in Service Catalog."""
|
|
||||||
def __init__(self, endpoints=None):
|
|
||||||
super(AmbiguousEndpoints, self).__init__(
|
|
||||||
_("AmbiguousEndpoints: %s") % repr(endpoints))
|
|
||||||
self.endpoints = endpoints
|
|
||||||
|
|
||||||
|
|
||||||
class HttpError(ClientException):
|
|
||||||
"""The base exception class for all HTTP exceptions.
|
|
||||||
"""
|
|
||||||
http_status = 0
|
|
||||||
message = _("HTTP Error")
|
|
||||||
|
|
||||||
def __init__(self, message=None, details=None,
|
|
||||||
response=None, request_id=None,
|
|
||||||
url=None, method=None, http_status=None):
|
|
||||||
self.http_status = http_status or self.http_status
|
|
||||||
self.message = message or self.message
|
|
||||||
self.details = details
|
|
||||||
self.request_id = request_id
|
|
||||||
self.response = response
|
|
||||||
self.url = url
|
|
||||||
self.method = method
|
|
||||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
|
||||||
if request_id:
|
|
||||||
formatted_string += " (Request-ID: %s)" % request_id
|
|
||||||
super(HttpError, self).__init__(formatted_string)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPRedirection(HttpError):
|
|
||||||
"""HTTP Redirection."""
|
|
||||||
message = _("HTTP Redirection")
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClientError(HttpError):
|
|
||||||
"""Client-side HTTP error.
|
|
||||||
|
|
||||||
Exception for cases in which the client seems to have erred.
|
|
||||||
"""
|
|
||||||
message = _("HTTP Client Error")
|
|
||||||
|
|
||||||
|
|
||||||
class HttpServerError(HttpError):
|
|
||||||
"""Server-side HTTP error.
|
|
||||||
|
|
||||||
Exception for cases in which the server is aware that it has
|
|
||||||
erred or is incapable of performing the request.
|
|
||||||
"""
|
|
||||||
message = _("HTTP Server Error")
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleChoices(HTTPRedirection):
|
|
||||||
"""HTTP 300 - Multiple Choices.
|
|
||||||
|
|
||||||
Indicates multiple options for the resource that the client may follow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
http_status = 300
|
|
||||||
message = _("Multiple Choices")
|
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(HTTPClientError):
|
|
||||||
"""HTTP 400 - Bad Request.
|
|
||||||
|
|
||||||
The request cannot be fulfilled due to bad syntax.
|
|
||||||
"""
|
|
||||||
http_status = 400
|
|
||||||
message = _("Bad Request")
|
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(HTTPClientError):
|
|
||||||
"""HTTP 401 - Unauthorized.
|
|
||||||
|
|
||||||
Similar to 403 Forbidden, but specifically for use when authentication
|
|
||||||
is required and has failed or has not yet been provided.
|
|
||||||
"""
|
|
||||||
http_status = 401
|
|
||||||
message = _("Unauthorized")
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentRequired(HTTPClientError):
|
|
||||||
"""HTTP 402 - Payment Required.
|
|
||||||
|
|
||||||
Reserved for future use.
|
|
||||||
"""
|
|
||||||
http_status = 402
|
|
||||||
message = _("Payment Required")
|
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(HTTPClientError):
|
|
||||||
"""HTTP 403 - Forbidden.
|
|
||||||
|
|
||||||
The request was a valid request, but the server is refusing to respond
|
|
||||||
to it.
|
|
||||||
"""
|
|
||||||
http_status = 403
|
|
||||||
message = _("Forbidden")
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(HTTPClientError):
|
|
||||||
"""HTTP 404 - Not Found.
|
|
||||||
|
|
||||||
The requested resource could not be found but may be available again
|
|
||||||
in the future.
|
|
||||||
"""
|
|
||||||
http_status = 404
|
|
||||||
message = _("Not Found")
|
|
||||||
|
|
||||||
|
|
||||||
class MethodNotAllowed(HTTPClientError):
|
|
||||||
"""HTTP 405 - Method Not Allowed.
|
|
||||||
|
|
||||||
A request was made of a resource using a request method not supported
|
|
||||||
by that resource.
|
|
||||||
"""
|
|
||||||
http_status = 405
|
|
||||||
message = _("Method Not Allowed")
|
|
||||||
|
|
||||||
|
|
||||||
class NotAcceptable(HTTPClientError):
|
|
||||||
"""HTTP 406 - Not Acceptable.
|
|
||||||
|
|
||||||
The requested resource is only capable of generating content not
|
|
||||||
acceptable according to the Accept headers sent in the request.
|
|
||||||
"""
|
|
||||||
http_status = 406
|
|
||||||
message = _("Not Acceptable")
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyAuthenticationRequired(HTTPClientError):
|
|
||||||
"""HTTP 407 - Proxy Authentication Required.
|
|
||||||
|
|
||||||
The client must first authenticate itself with the proxy.
|
|
||||||
"""
|
|
||||||
http_status = 407
|
|
||||||
message = _("Proxy Authentication Required")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestTimeout(HTTPClientError):
|
|
||||||
"""HTTP 408 - Request Timeout.
|
|
||||||
|
|
||||||
The server timed out waiting for the request.
|
|
||||||
"""
|
|
||||||
http_status = 408
|
|
||||||
message = _("Request Timeout")
|
|
||||||
|
|
||||||
|
|
||||||
class Conflict(HTTPClientError):
|
|
||||||
"""HTTP 409 - Conflict.
|
|
||||||
|
|
||||||
Indicates that the request could not be processed because of conflict
|
|
||||||
in the request, such as an edit conflict.
|
|
||||||
"""
|
|
||||||
http_status = 409
|
|
||||||
message = _("Conflict")
|
|
||||||
|
|
||||||
|
|
||||||
class Gone(HTTPClientError):
|
|
||||||
"""HTTP 410 - Gone.
|
|
||||||
|
|
||||||
Indicates that the resource requested is no longer available and will
|
|
||||||
not be available again.
|
|
||||||
"""
|
|
||||||
http_status = 410
|
|
||||||
message = _("Gone")
|
|
||||||
|
|
||||||
|
|
||||||
class LengthRequired(HTTPClientError):
|
|
||||||
"""HTTP 411 - Length Required.
|
|
||||||
|
|
||||||
The request did not specify the length of its content, which is
|
|
||||||
required by the requested resource.
|
|
||||||
"""
|
|
||||||
http_status = 411
|
|
||||||
message = _("Length Required")
|
|
||||||
|
|
||||||
|
|
||||||
class PreconditionFailed(HTTPClientError):
|
|
||||||
"""HTTP 412 - Precondition Failed.
|
|
||||||
|
|
||||||
The server does not meet one of the preconditions that the requester
|
|
||||||
put on the request.
|
|
||||||
"""
|
|
||||||
http_status = 412
|
|
||||||
message = _("Precondition Failed")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestEntityTooLarge(HTTPClientError):
|
|
||||||
"""HTTP 413 - Request Entity Too Large.
|
|
||||||
|
|
||||||
The request is larger than the server is willing or able to process.
|
|
||||||
"""
|
|
||||||
http_status = 413
|
|
||||||
message = _("Request Entity Too Large")
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
self.retry_after = int(kwargs.pop('retry_after'))
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
self.retry_after = 0
|
|
||||||
|
|
||||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestUriTooLong(HTTPClientError):
|
|
||||||
"""HTTP 414 - Request-URI Too Long.
|
|
||||||
|
|
||||||
The URI provided was too long for the server to process.
|
|
||||||
"""
|
|
||||||
http_status = 414
|
|
||||||
message = _("Request-URI Too Long")
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedMediaType(HTTPClientError):
|
|
||||||
"""HTTP 415 - Unsupported Media Type.
|
|
||||||
|
|
||||||
The request entity has a media type which the server or resource does
|
|
||||||
not support.
|
|
||||||
"""
|
|
||||||
http_status = 415
|
|
||||||
message = _("Unsupported Media Type")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
|
||||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
|
||||||
|
|
||||||
The client has asked for a portion of the file, but the server cannot
|
|
||||||
supply that portion.
|
|
||||||
"""
|
|
||||||
http_status = 416
|
|
||||||
message = _("Requested Range Not Satisfiable")
|
|
||||||
|
|
||||||
|
|
||||||
class ExpectationFailed(HTTPClientError):
|
|
||||||
"""HTTP 417 - Expectation Failed.
|
|
||||||
|
|
||||||
The server cannot meet the requirements of the Expect request-header field.
|
|
||||||
"""
|
|
||||||
http_status = 417
|
|
||||||
message = _("Expectation Failed")
|
|
||||||
|
|
||||||
|
|
||||||
class UnprocessableEntity(HTTPClientError):
|
|
||||||
"""HTTP 422 - Unprocessable Entity.
|
|
||||||
|
|
||||||
The request was well-formed but was unable to be followed due to semantic
|
|
||||||
errors.
|
|
||||||
"""
|
|
||||||
http_status = 422
|
|
||||||
message = _("Unprocessable Entity")
|
|
||||||
|
|
||||||
|
|
||||||
class InternalServerError(HttpServerError):
|
|
||||||
"""HTTP 500 - Internal Server Error.
|
|
||||||
|
|
||||||
A generic error message, given when no more specific message is suitable.
|
|
||||||
"""
|
|
||||||
http_status = 500
|
|
||||||
message = _("Internal Server Error")
|
|
||||||
|
|
||||||
|
|
||||||
# NotImplemented is a python keyword.
|
|
||||||
class HttpNotImplemented(HttpServerError):
|
|
||||||
"""HTTP 501 - Not Implemented.
|
|
||||||
|
|
||||||
The server either does not recognize the request method, or it lacks
|
|
||||||
the ability to fulfill the request.
|
|
||||||
"""
|
|
||||||
http_status = 501
|
|
||||||
message = _("Not Implemented")
|
|
||||||
|
|
||||||
|
|
||||||
class BadGateway(HttpServerError):
|
|
||||||
"""HTTP 502 - Bad Gateway.
|
|
||||||
|
|
||||||
The server was acting as a gateway or proxy and received an invalid
|
|
||||||
response from the upstream server.
|
|
||||||
"""
|
|
||||||
http_status = 502
|
|
||||||
message = _("Bad Gateway")
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceUnavailable(HttpServerError):
|
|
||||||
"""HTTP 503 - Service Unavailable.
|
|
||||||
|
|
||||||
The server is currently unavailable.
|
|
||||||
"""
|
|
||||||
http_status = 503
|
|
||||||
message = _("Service Unavailable")
|
|
||||||
|
|
||||||
|
|
||||||
class GatewayTimeout(HttpServerError):
|
|
||||||
"""HTTP 504 - Gateway Timeout.
|
|
||||||
|
|
||||||
The server was acting as a gateway or proxy and did not receive a timely
|
|
||||||
response from the upstream server.
|
|
||||||
"""
|
|
||||||
http_status = 504
|
|
||||||
message = _("Gateway Timeout")
|
|
||||||
|
|
||||||
|
|
||||||
class HttpVersionNotSupported(HttpServerError):
|
|
||||||
"""HTTP 505 - HttpVersion Not Supported.
|
|
||||||
|
|
||||||
The server does not support the HTTP protocol version used in the request.
|
|
||||||
"""
|
|
||||||
http_status = 505
|
|
||||||
message = _("HTTP Version Not Supported")
|
|
||||||
|
|
||||||
|
|
||||||
# _code_map contains all the classes that have http_status attribute.
|
|
||||||
_code_map = dict(
|
|
||||||
(getattr(obj, 'http_status', None), obj)
|
|
||||||
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
|
||||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response, method, url):
|
|
||||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
|
||||||
|
|
||||||
:param response: instance of `requests.Response` class
|
|
||||||
:param method: HTTP method used for request
|
|
||||||
:param url: URL used for request
|
|
||||||
"""
|
|
||||||
|
|
||||||
req_id = response.headers.get("x-openstack-request-id")
|
|
||||||
#NOTE(hdd) true for older versions of nova and cinder
|
|
||||||
if not req_id:
|
|
||||||
req_id = response.headers.get("x-compute-request-id")
|
|
||||||
kwargs = {
|
|
||||||
"http_status": response.status_code,
|
|
||||||
"response": response,
|
|
||||||
"method": method,
|
|
||||||
"url": url,
|
|
||||||
"request_id": req_id,
|
|
||||||
}
|
|
||||||
if "retry-after" in response.headers:
|
|
||||||
kwargs["retry_after"] = response.headers["retry-after"]
|
|
||||||
|
|
||||||
content_type = response.headers.get("Content-Type", "")
|
|
||||||
if content_type.startswith("application/json"):
|
|
||||||
try:
|
|
||||||
body = response.json()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(body, dict):
|
|
||||||
error = list(body.values())[0]
|
|
||||||
kwargs["message"] = error.get("message")
|
|
||||||
kwargs["details"] = error.get("details")
|
|
||||||
elif content_type.startswith("text/"):
|
|
||||||
kwargs["details"] = response.text
|
|
||||||
|
|
||||||
try:
|
|
||||||
cls = _code_map[response.status_code]
|
|
||||||
except KeyError:
|
|
||||||
if 500 <= response.status_code < 600:
|
|
||||||
cls = HttpServerError
|
|
||||||
elif 400 <= response.status_code < 500:
|
|
||||||
cls = HTTPClientError
|
|
||||||
else:
|
|
||||||
cls = HttpError
|
|
||||||
return cls(**kwargs)
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
A fake server that "responds" to API methods with pre-canned responses.
|
|
||||||
|
|
||||||
All of these responses come from the spec, so if for some reason the spec's
|
|
||||||
wrong the tests might raise AssertionError. I've indicated in comments the
|
|
||||||
places where actual behavior differs from the spec.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# W0102: Dangerous default value %s as argument
|
|
||||||
# pylint: disable=W0102
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
from six.moves.urllib import parse
|
|
||||||
|
|
||||||
from mistral.openstack.common.apiclient import client
|
|
||||||
|
|
||||||
|
|
||||||
def assert_has_keys(dct, required=[], optional=[]):
|
|
||||||
for k in required:
|
|
||||||
try:
|
|
||||||
assert k in dct
|
|
||||||
except AssertionError:
|
|
||||||
extra_keys = set(dct.keys()).difference(set(required + optional))
|
|
||||||
raise AssertionError("found unexpected keys: %s" %
|
|
||||||
list(extra_keys))
|
|
||||||
|
|
||||||
|
|
||||||
class TestResponse(requests.Response):
|
|
||||||
"""Wrap requests.Response and provide a convenient initialization.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
super(TestResponse, self).__init__()
|
|
||||||
self._content_consumed = True
|
|
||||||
if isinstance(data, dict):
|
|
||||||
self.status_code = data.get('status_code', 200)
|
|
||||||
# Fake the text attribute to streamline Response creation
|
|
||||||
text = data.get('text', "")
|
|
||||||
if isinstance(text, (dict, list)):
|
|
||||||
self._content = json.dumps(text)
|
|
||||||
default_headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
self._content = text
|
|
||||||
default_headers = {}
|
|
||||||
if six.PY3 and isinstance(self._content, six.string_types):
|
|
||||||
self._content = self._content.encode('utf-8', 'strict')
|
|
||||||
self.headers = data.get('headers') or default_headers
|
|
||||||
else:
|
|
||||||
self.status_code = data
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (self.status_code == other.status_code and
|
|
||||||
self.headers == other.headers and
|
|
||||||
self._content == other._content)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeHTTPClient(client.HTTPClient):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.callstack = []
|
|
||||||
self.fixtures = kwargs.pop("fixtures", None) or {}
|
|
||||||
if not args and not "auth_plugin" in kwargs:
|
|
||||||
args = (None, )
|
|
||||||
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def assert_called(self, method, url, body=None, pos=-1):
|
|
||||||
"""Assert than an API method was just called.
|
|
||||||
"""
|
|
||||||
expected = (method, url)
|
|
||||||
called = self.callstack[pos][0:2]
|
|
||||||
assert self.callstack, \
|
|
||||||
"Expected %s %s but no calls were made." % expected
|
|
||||||
|
|
||||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
|
||||||
(expected + called)
|
|
||||||
|
|
||||||
if body is not None:
|
|
||||||
if self.callstack[pos][3] != body:
|
|
||||||
raise AssertionError('%r != %r' %
|
|
||||||
(self.callstack[pos][3], body))
|
|
||||||
|
|
||||||
def assert_called_anytime(self, method, url, body=None):
|
|
||||||
"""Assert than an API method was called anytime in the test.
|
|
||||||
"""
|
|
||||||
expected = (method, url)
|
|
||||||
|
|
||||||
assert self.callstack, \
|
|
||||||
"Expected %s %s but no calls were made." % expected
|
|
||||||
|
|
||||||
found = False
|
|
||||||
entry = None
|
|
||||||
for entry in self.callstack:
|
|
||||||
if expected == entry[0:2]:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
assert found, 'Expected %s %s; got %s' % \
|
|
||||||
(method, url, self.callstack)
|
|
||||||
if body is not None:
|
|
||||||
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
|
||||||
|
|
||||||
self.callstack = []
|
|
||||||
|
|
||||||
def clear_callstack(self):
|
|
||||||
self.callstack = []
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def client_request(self, client, method, url, **kwargs):
|
|
||||||
# Check that certain things are called correctly
|
|
||||||
if method in ["GET", "DELETE"]:
|
|
||||||
assert "json" not in kwargs
|
|
||||||
|
|
||||||
# Note the call
|
|
||||||
self.callstack.append(
|
|
||||||
(method,
|
|
||||||
url,
|
|
||||||
kwargs.get("headers") or {},
|
|
||||||
kwargs.get("json") or kwargs.get("data")))
|
|
||||||
try:
|
|
||||||
fixture = self.fixtures[url][method]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return TestResponse({"headers": fixture[0],
|
|
||||||
"text": fixture[1]})
|
|
||||||
|
|
||||||
# Call the method
|
|
||||||
args = parse.parse_qsl(parse.urlparse(url)[4])
|
|
||||||
kwargs.update(args)
|
|
||||||
munged_url = url.rsplit('?', 1)[0]
|
|
||||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
|
||||||
munged_url = munged_url.replace('-', '_')
|
|
||||||
|
|
||||||
callback = "%s_%s" % (method.lower(), munged_url)
|
|
||||||
|
|
||||||
if not hasattr(self, callback):
|
|
||||||
raise AssertionError('Called unknown API method: %s %s, '
|
|
||||||
'expected fakes method name: %s' %
|
|
||||||
(method, url, callback))
|
|
||||||
|
|
||||||
resp = getattr(self, callback)(**kwargs)
|
|
||||||
if len(resp) == 3:
|
|
||||||
status, headers, body = resp
|
|
||||||
else:
|
|
||||||
status, body = resp
|
|
||||||
headers = {}
|
|
||||||
return TestResponse({
|
|
||||||
"status_code": status,
|
|
||||||
"text": body,
|
|
||||||
"headers": headers,
|
|
||||||
})
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
# Copyright 2012 Red Hat, 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.
|
|
||||||
|
|
||||||
# W0603: Using the global statement
|
|
||||||
# W0621: Redefining name %s from outer scope
|
|
||||||
# pylint: disable=W0603,W0621
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import getpass
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import prettytable
|
|
||||||
import six
|
|
||||||
from six import moves
|
|
||||||
|
|
||||||
from mistral.openstack.common.apiclient import exceptions
|
|
||||||
from mistral.openstack.common.gettextutils import _
|
|
||||||
from mistral.openstack.common import strutils
|
|
||||||
from mistral.openstack.common import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
def validate_args(fn, *args, **kwargs):
|
|
||||||
"""Check that the supplied args are sufficient for calling a function.
|
|
||||||
|
|
||||||
>>> validate_args(lambda a: None)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
MissingArgs: Missing argument(s): a
|
|
||||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
MissingArgs: Missing argument(s): b, d
|
|
||||||
|
|
||||||
:param fn: the function to check
|
|
||||||
:param arg: the positional arguments supplied
|
|
||||||
:param kwargs: the keyword arguments supplied
|
|
||||||
"""
|
|
||||||
argspec = inspect.getargspec(fn)
|
|
||||||
|
|
||||||
num_defaults = len(argspec.defaults or [])
|
|
||||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
|
||||||
|
|
||||||
def isbound(method):
|
|
||||||
return getattr(method, '__self__', None) is not None
|
|
||||||
|
|
||||||
if isbound(fn):
|
|
||||||
required_args.pop(0)
|
|
||||||
|
|
||||||
missing = [arg for arg in required_args if arg not in kwargs]
|
|
||||||
missing = missing[len(args):]
|
|
||||||
if missing:
|
|
||||||
raise exceptions.MissingArgs(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def arg(*args, **kwargs):
|
|
||||||
"""Decorator for CLI args.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> @arg("name", help="Name of the new entity")
|
|
||||||
... def entity_create(args):
|
|
||||||
... pass
|
|
||||||
"""
|
|
||||||
def _decorator(func):
|
|
||||||
add_arg(func, *args, **kwargs)
|
|
||||||
return func
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
|
|
||||||
def env(*args, **kwargs):
|
|
||||||
"""Returns the first environment variable set.
|
|
||||||
|
|
||||||
If all are empty, defaults to '' or keyword arg `default`.
|
|
||||||
"""
|
|
||||||
for arg in args:
|
|
||||||
value = os.environ.get(arg)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return kwargs.get('default', '')
|
|
||||||
|
|
||||||
|
|
||||||
def add_arg(func, *args, **kwargs):
|
|
||||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
|
||||||
|
|
||||||
if not hasattr(func, 'arguments'):
|
|
||||||
func.arguments = []
|
|
||||||
|
|
||||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
|
||||||
# tests.
|
|
||||||
if (args, kwargs) not in func.arguments:
|
|
||||||
# Because of the semantics of decorator composition if we just append
|
|
||||||
# to the options list positional options will appear to be backwards.
|
|
||||||
func.arguments.insert(0, (args, kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
def unauthenticated(func):
|
|
||||||
"""Adds 'unauthenticated' attribute to decorated function.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
>>> @unauthenticated
|
|
||||||
... def mymethod(f):
|
|
||||||
... pass
|
|
||||||
"""
|
|
||||||
func.unauthenticated = True
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def isunauthenticated(func):
|
|
||||||
"""Checks if the function does not require authentication.
|
|
||||||
|
|
||||||
Mark such functions with the `@unauthenticated` decorator.
|
|
||||||
|
|
||||||
:returns: bool
|
|
||||||
"""
|
|
||||||
return getattr(func, 'unauthenticated', False)
|
|
||||||
|
|
||||||
|
|
||||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
|
||||||
mixed_case_fields=None):
|
|
||||||
"""Print a list or objects as a table, one row per object.
|
|
||||||
|
|
||||||
:param objs: iterable of :class:`Resource`
|
|
||||||
:param fields: attributes that correspond to columns, in order
|
|
||||||
:param formatters: `dict` of callables for field formatting
|
|
||||||
:param sortby_index: index of the field for sorting table rows
|
|
||||||
:param mixed_case_fields: fields corresponding to object attributes that
|
|
||||||
have mixed case names (e.g., 'serverId')
|
|
||||||
"""
|
|
||||||
formatters = formatters or {}
|
|
||||||
mixed_case_fields = mixed_case_fields or []
|
|
||||||
if sortby_index is None:
|
|
||||||
kwargs = {}
|
|
||||||
else:
|
|
||||||
kwargs = {'sortby': fields[sortby_index]}
|
|
||||||
pt = prettytable.PrettyTable(fields, caching=False)
|
|
||||||
pt.align = 'l'
|
|
||||||
|
|
||||||
for o in objs:
|
|
||||||
row = []
|
|
||||||
for field in fields:
|
|
||||||
if field in formatters:
|
|
||||||
row.append(formatters[field](o))
|
|
||||||
else:
|
|
||||||
if field in mixed_case_fields:
|
|
||||||
field_name = field.replace(' ', '_')
|
|
||||||
else:
|
|
||||||
field_name = field.lower().replace(' ', '_')
|
|
||||||
data = getattr(o, field_name, '')
|
|
||||||
row.append(data)
|
|
||||||
pt.add_row(row)
|
|
||||||
|
|
||||||
print(strutils.safe_encode(pt.get_string(**kwargs)))
|
|
||||||
|
|
||||||
|
|
||||||
def print_dict(dct, dict_property="Property", wrap=0):
|
|
||||||
"""Print a `dict` as a table of two columns.
|
|
||||||
|
|
||||||
:param dct: `dict` to print
|
|
||||||
:param dict_property: name of the first column
|
|
||||||
:param wrap: wrapping for the second column
|
|
||||||
"""
|
|
||||||
pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
|
|
||||||
pt.align = 'l'
|
|
||||||
for k, v in six.iteritems(dct):
|
|
||||||
# convert dict to str to check length
|
|
||||||
if isinstance(v, dict):
|
|
||||||
v = six.text_type(v)
|
|
||||||
if wrap > 0:
|
|
||||||
v = textwrap.fill(six.text_type(v), wrap)
|
|
||||||
# if value has a newline, add in multiple rows
|
|
||||||
# e.g. fault with stacktrace
|
|
||||||
if v and isinstance(v, six.string_types) and r'\n' in v:
|
|
||||||
lines = v.strip().split(r'\n')
|
|
||||||
col1 = k
|
|
||||||
for line in lines:
|
|
||||||
pt.add_row([col1, line])
|
|
||||||
col1 = ''
|
|
||||||
else:
|
|
||||||
pt.add_row([k, v])
|
|
||||||
print(strutils.safe_encode(pt.get_string()))
|
|
||||||
|
|
||||||
|
|
||||||
def get_password(max_password_prompts=3):
|
|
||||||
"""Read password from TTY."""
|
|
||||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
|
||||||
pw = None
|
|
||||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
|
||||||
# Check for Ctrl-D
|
|
||||||
try:
|
|
||||||
for __ in moves.range(max_password_prompts):
|
|
||||||
pw1 = getpass.getpass("OS Password: ")
|
|
||||||
if verify:
|
|
||||||
pw2 = getpass.getpass("Please verify: ")
|
|
||||||
else:
|
|
||||||
pw2 = pw1
|
|
||||||
if pw1 == pw2 and pw1:
|
|
||||||
pw = pw1
|
|
||||||
break
|
|
||||||
except EOFError:
|
|
||||||
pass
|
|
||||||
return pw
|
|
||||||
|
|
||||||
|
|
||||||
def find_resource(manager, name_or_id, **find_args):
|
|
||||||
"""Look for resource in a given manager.
|
|
||||||
|
|
||||||
Used as a helper for the _find_* methods.
|
|
||||||
Example:
|
|
||||||
|
|
||||||
def _find_hypervisor(cs, hypervisor):
|
|
||||||
#Get a hypervisor by name or ID.
|
|
||||||
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
|
||||||
"""
|
|
||||||
# first try to get entity as integer id
|
|
||||||
try:
|
|
||||||
return manager.get(int(name_or_id))
|
|
||||||
except (TypeError, ValueError, exceptions.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# now try to get entity as uuid
|
|
||||||
try:
|
|
||||||
if six.PY2:
|
|
||||||
tmp_id = strutils.safe_encode(name_or_id)
|
|
||||||
else:
|
|
||||||
tmp_id = strutils.safe_decode(name_or_id)
|
|
||||||
|
|
||||||
if uuidutils.is_uuid_like(tmp_id):
|
|
||||||
return manager.get(tmp_id)
|
|
||||||
except (TypeError, ValueError, exceptions.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# for str id which is not uuid
|
|
||||||
if getattr(manager, 'is_alphanum_id_allowed', False):
|
|
||||||
try:
|
|
||||||
return manager.get(name_or_id)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
return manager.find(human_id=name_or_id, **find_args)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# finally try to find entity by name
|
|
||||||
try:
|
|
||||||
resource = getattr(manager, 'resource_class', None)
|
|
||||||
name_attr = resource.NAME_ATTR if resource else 'name'
|
|
||||||
kwargs = {name_attr: name_or_id}
|
|
||||||
kwargs.update(find_args)
|
|
||||||
return manager.find(**kwargs)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
msg = _("No %(name)s with a name or "
|
|
||||||
"ID of '%(name_or_id)s' exists.") % \
|
|
||||||
{
|
|
||||||
"name": manager.resource_class.__name__.lower(),
|
|
||||||
"name_or_id": name_or_id
|
|
||||||
}
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
msg = _("Multiple %(name)s matches found for "
|
|
||||||
"'%(name_or_id)s', use an ID to be more specific.") % \
|
|
||||||
{
|
|
||||||
"name": manager.resource_class.__name__.lower(),
|
|
||||||
"name_or_id": name_or_id
|
|
||||||
}
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def service_type(stype):
|
|
||||||
"""Adds 'service_type' attribute to decorated function.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
@service_type('volume')
|
|
||||||
def mymethod(f):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
def inner(f):
|
|
||||||
f.service_type = stype
|
|
||||||
return f
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_type(f):
|
|
||||||
"""Retrieves service type from function."""
|
|
||||||
return getattr(f, 'service_type', None)
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_choice_list(l):
|
|
||||||
return ', '.join("'%s'" % i for i in l)
|
|
||||||
|
|
||||||
|
|
||||||
def exit(msg=''):
|
|
||||||
if msg:
|
|
||||||
print (msg, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# Copyright 2013 Mirantis.inc
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Provision test environment for specific DB backends"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
from six import moves
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from mistral.openstack.common.db import exception as exc
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_engine(uri):
|
|
||||||
"""Engine creation
|
|
||||||
|
|
||||||
Call the function without arguments to get admin connection. Admin
|
|
||||||
connection required to create temporary user and database for each
|
|
||||||
particular test. Otherwise use existing connection to recreate connection
|
|
||||||
to the temporary database.
|
|
||||||
"""
|
|
||||||
return sqlalchemy.create_engine(uri, poolclass=sqlalchemy.pool.NullPool)
|
|
||||||
|
|
||||||
|
|
||||||
def _execute_sql(engine, sql, driver):
|
|
||||||
"""Initialize connection, execute sql query and close it."""
|
|
||||||
try:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
if driver == 'postgresql':
|
|
||||||
conn.connection.set_isolation_level(0)
|
|
||||||
for s in sql:
|
|
||||||
conn.execute(s)
|
|
||||||
except sqlalchemy.exc.OperationalError:
|
|
||||||
msg = ('%s does not match database admin '
|
|
||||||
'credentials or database does not exist.')
|
|
||||||
LOG.exception(msg % engine.url)
|
|
||||||
raise exc.DBConnectionError(msg % engine.url)
|
|
||||||
|
|
||||||
|
|
||||||
def create_database(engine):
|
|
||||||
"""Provide temporary user and database for each particular test."""
|
|
||||||
driver = engine.name
|
|
||||||
|
|
||||||
auth = {
|
|
||||||
'database': ''.join(random.choice(string.ascii_lowercase)
|
|
||||||
for i in moves.range(10)),
|
|
||||||
'user': engine.url.username,
|
|
||||||
'passwd': engine.url.password,
|
|
||||||
}
|
|
||||||
|
|
||||||
sqls = [
|
|
||||||
"drop database if exists %(database)s;",
|
|
||||||
"create database %(database)s;"
|
|
||||||
]
|
|
||||||
|
|
||||||
if driver == 'sqlite':
|
|
||||||
return 'sqlite:////tmp/%s' % auth['database']
|
|
||||||
elif driver in ['mysql', 'postgresql']:
|
|
||||||
sql_query = map(lambda x: x % auth, sqls)
|
|
||||||
_execute_sql(engine, sql_query, driver)
|
|
||||||
else:
|
|
||||||
raise ValueError('Unsupported RDBMS %s' % driver)
|
|
||||||
|
|
||||||
params = auth.copy()
|
|
||||||
params['backend'] = driver
|
|
||||||
return "%(backend)s://%(user)s:%(passwd)s@localhost/%(database)s" % params
|
|
||||||
|
|
||||||
|
|
||||||
def drop_database(admin_engine, current_uri):
|
|
||||||
"""Drop temporary database and user after each particular test."""
|
|
||||||
|
|
||||||
engine = get_engine(current_uri)
|
|
||||||
driver = engine.name
|
|
||||||
auth = {'database': engine.url.database, 'user': engine.url.username}
|
|
||||||
|
|
||||||
if driver == 'sqlite':
|
|
||||||
try:
|
|
||||||
os.remove(auth['database'])
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
elif driver in ['mysql', 'postgresql']:
|
|
||||||
sql = "drop database if exists %(database)s;"
|
|
||||||
_execute_sql(admin_engine, [sql % auth], driver)
|
|
||||||
else:
|
|
||||||
raise ValueError('Unsupported RDBMS %s' % driver)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Controller to handle commands
|
|
||||||
|
|
||||||
::create: Create test user and database with random names.
|
|
||||||
::drop: Drop user and database created by previous command.
|
|
||||||
"""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Controller to handle database creation and dropping'
|
|
||||||
' commands.',
|
|
||||||
epilog='Under normal circumstances is not used directly.'
|
|
||||||
' Used in .testr.conf to automate test database creation'
|
|
||||||
' and dropping processes.')
|
|
||||||
subparsers = parser.add_subparsers(
|
|
||||||
help='Subcommands to manipulate temporary test databases.')
|
|
||||||
|
|
||||||
create = subparsers.add_parser(
|
|
||||||
'create',
|
|
||||||
help='Create temporary test '
|
|
||||||
'databases and users.')
|
|
||||||
create.set_defaults(which='create')
|
|
||||||
create.add_argument(
|
|
||||||
'instances_count',
|
|
||||||
type=int,
|
|
||||||
help='Number of databases to create.')
|
|
||||||
|
|
||||||
drop = subparsers.add_parser(
|
|
||||||
'drop',
|
|
||||||
help='Drop temporary test databases and users.')
|
|
||||||
drop.set_defaults(which='drop')
|
|
||||||
drop.add_argument(
|
|
||||||
'instances',
|
|
||||||
nargs='+',
|
|
||||||
help='List of databases uri to be dropped.')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
connection_string = os.getenv('OS_TEST_DBAPI_ADMIN_CONNECTION',
|
|
||||||
'sqlite://')
|
|
||||||
engine = get_engine(connection_string)
|
|
||||||
which = args.which
|
|
||||||
|
|
||||||
if which == "create":
|
|
||||||
for i in range(int(args.instances_count)):
|
|
||||||
print(create_database(engine))
|
|
||||||
elif which == "drop":
|
|
||||||
for db in args.instances:
|
|
||||||
drop_database(engine, db)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# Copyright (c) 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 functools
|
|
||||||
import os
|
|
||||||
|
|
||||||
import fixtures
|
|
||||||
from oslotest import base as test_base
|
|
||||||
import six
|
|
||||||
|
|
||||||
from mistral.openstack.common.db.sqlalchemy import provision
|
|
||||||
from mistral.openstack.common.db.sqlalchemy import session
|
|
||||||
from mistral.openstack.common.db.sqlalchemy import utils
|
|
||||||
|
|
||||||
|
|
||||||
class DbFixture(fixtures.Fixture):
|
|
||||||
"""Basic database fixture.
|
|
||||||
|
|
||||||
Allows to run tests on various db backends, such as SQLite, MySQL and
|
|
||||||
PostgreSQL. By default use sqlite backend. To override default backend
|
|
||||||
uri set env variable OS_TEST_DBAPI_CONNECTION with database admin
|
|
||||||
credentials for specific backend.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _get_uri(self):
|
|
||||||
return os.getenv('OS_TEST_DBAPI_CONNECTION', 'sqlite://')
|
|
||||||
|
|
||||||
def __init__(self, test):
|
|
||||||
super(DbFixture, self).__init__()
|
|
||||||
|
|
||||||
self.test = test
|
|
||||||
|
|
||||||
def cleanUp(self):
|
|
||||||
self.test.engine.dispose()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DbFixture, self).setUp()
|
|
||||||
|
|
||||||
self.test.engine = session.create_engine(self._get_uri())
|
|
||||||
self.test.sessionmaker = session.get_maker(self.test.engine)
|
|
||||||
|
|
||||||
|
|
||||||
class DbTestCase(test_base.BaseTestCase):
|
|
||||||
"""Base class for testing of DB code.
|
|
||||||
|
|
||||||
Using `DbFixture`. Intended to be the main database test case to use all
|
|
||||||
the tests on a given backend with user defined uri. Backend specific
|
|
||||||
tests should be decorated with `backend_specific` decorator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
FIXTURE = DbFixture
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DbTestCase, self).setUp()
|
|
||||||
self.useFixture(self.FIXTURE(self))
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_DIALECTS = ['sqlite', 'mysql', 'postgresql']
|
|
||||||
|
|
||||||
|
|
||||||
def backend_specific(*dialects):
|
|
||||||
"""Decorator to skip backend specific tests on inappropriate engines.
|
|
||||||
|
|
||||||
::dialects: list of dialects names under which the test will be launched.
|
|
||||||
"""
|
|
||||||
def wrap(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def ins_wrap(self):
|
|
||||||
if not set(dialects).issubset(ALLOWED_DIALECTS):
|
|
||||||
raise ValueError(
|
|
||||||
"Please use allowed dialects: %s" % ALLOWED_DIALECTS)
|
|
||||||
if self.engine.name not in dialects:
|
|
||||||
msg = ('The test "%s" can be run '
|
|
||||||
'only on %s. Current engine is %s.')
|
|
||||||
args = (f.__name__, ' '.join(dialects), self.engine.name)
|
|
||||||
self.skip(msg % args)
|
|
||||||
else:
|
|
||||||
return f(self)
|
|
||||||
return ins_wrap
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class OpportunisticFixture(DbFixture):
|
|
||||||
"""Base fixture to use default CI databases.
|
|
||||||
|
|
||||||
The databases exist in OpenStack CI infrastructure. But for the
|
|
||||||
correct functioning in local environment the databases must be
|
|
||||||
created manually.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DRIVER = abc.abstractproperty(lambda: None)
|
|
||||||
DBNAME = PASSWORD = USERNAME = 'openstack_citest'
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self._provisioning_engine = provision.get_engine(
|
|
||||||
utils.get_connect_string(backend=self.DRIVER,
|
|
||||||
user=self.USERNAME,
|
|
||||||
passwd=self.PASSWORD,
|
|
||||||
database=self.DBNAME)
|
|
||||||
)
|
|
||||||
self._uri = provision.create_database(self._provisioning_engine)
|
|
||||||
|
|
||||||
super(OpportunisticFixture, self).setUp()
|
|
||||||
|
|
||||||
def cleanUp(self):
|
|
||||||
super(OpportunisticFixture, self).cleanUp()
|
|
||||||
|
|
||||||
provision.drop_database(self._provisioning_engine, self._uri)
|
|
||||||
|
|
||||||
def _get_uri(self):
|
|
||||||
return self._uri
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class OpportunisticTestCase(DbTestCase):
|
|
||||||
"""Base test case to use default CI databases.
|
|
||||||
|
|
||||||
The subclasses of the test case are running only when openstack_citest
|
|
||||||
database is available otherwise a tests will be skipped.
|
|
||||||
"""
|
|
||||||
|
|
||||||
FIXTURE = abc.abstractproperty(lambda: None)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
credentials = {
|
|
||||||
'backend': self.FIXTURE.DRIVER,
|
|
||||||
'user': self.FIXTURE.USERNAME,
|
|
||||||
'passwd': self.FIXTURE.PASSWORD,
|
|
||||||
'database': self.FIXTURE.DBNAME}
|
|
||||||
|
|
||||||
if self.FIXTURE.DRIVER and not utils.is_backend_avail(**credentials):
|
|
||||||
msg = '%s backend is not available.' % self.FIXTURE.DRIVER
|
|
||||||
return self.skip(msg)
|
|
||||||
|
|
||||||
super(OpportunisticTestCase, self).setUp()
|
|
||||||
|
|
||||||
|
|
||||||
class MySQLOpportunisticFixture(OpportunisticFixture):
|
|
||||||
DRIVER = 'mysql'
|
|
||||||
|
|
||||||
|
|
||||||
class PostgreSQLOpportunisticFixture(OpportunisticFixture):
|
|
||||||
DRIVER = 'postgresql'
|
|
||||||
|
|
||||||
|
|
||||||
class MySQLOpportunisticTestCase(OpportunisticTestCase):
|
|
||||||
FIXTURE = MySQLOpportunisticFixture
|
|
||||||
|
|
||||||
|
|
||||||
class PostgreSQLOpportunisticTestCase(OpportunisticTestCase):
|
|
||||||
FIXTURE = PostgreSQLOpportunisticFixture
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
# Copyright 2010-2011 OpenStack Foundation
|
|
||||||
# Copyright 2012-2013 IBM Corp.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import lockfile
|
|
||||||
from oslotest import base as test_base
|
|
||||||
from six import moves
|
|
||||||
from six.moves.urllib import parse
|
|
||||||
import sqlalchemy
|
|
||||||
import sqlalchemy.exc
|
|
||||||
|
|
||||||
from mistral.openstack.common.db.sqlalchemy import utils
|
|
||||||
from mistral.openstack.common.gettextutils import _LE
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _have_mysql(user, passwd, database):
|
|
||||||
present = os.environ.get('TEST_MYSQL_PRESENT')
|
|
||||||
if present is None:
|
|
||||||
return utils.is_backend_avail(backend='mysql',
|
|
||||||
user=user,
|
|
||||||
passwd=passwd,
|
|
||||||
database=database)
|
|
||||||
return present.lower() in ('', 'true')
|
|
||||||
|
|
||||||
|
|
||||||
def _have_postgresql(user, passwd, database):
|
|
||||||
present = os.environ.get('TEST_POSTGRESQL_PRESENT')
|
|
||||||
if present is None:
|
|
||||||
return utils.is_backend_avail(backend='postgres',
|
|
||||||
user=user,
|
|
||||||
passwd=passwd,
|
|
||||||
database=database)
|
|
||||||
return present.lower() in ('', 'true')
|
|
||||||
|
|
||||||
|
|
||||||
def _set_db_lock(lock_path=None, lock_prefix=None):
|
|
||||||
def decorator(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
path = lock_path or os.environ.get("MISTRAL_LOCK_PATH")
|
|
||||||
lock = lockfile.FileLock(os.path.join(path, lock_prefix))
|
|
||||||
with lock:
|
|
||||||
LOG.debug('Got lock "%s"' % f.__name__)
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
finally:
|
|
||||||
LOG.debug('Lock released "%s"' % f.__name__)
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMigrationTestCase(test_base.BaseTestCase):
|
|
||||||
"""Base class fort testing of migration utils."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(BaseMigrationTestCase, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__),
|
|
||||||
'test_migrations.conf')
|
|
||||||
# Test machines can set the TEST_MIGRATIONS_CONF variable
|
|
||||||
# to override the location of the config file for migration testing
|
|
||||||
self.CONFIG_FILE_PATH = os.environ.get('TEST_MIGRATIONS_CONF',
|
|
||||||
self.DEFAULT_CONFIG_FILE)
|
|
||||||
self.test_databases = {}
|
|
||||||
self.migration_api = None
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(BaseMigrationTestCase, self).setUp()
|
|
||||||
|
|
||||||
# Load test databases from the config file. Only do this
|
|
||||||
# once. No need to re-run this on each test...
|
|
||||||
LOG.debug('config_path is %s' % self.CONFIG_FILE_PATH)
|
|
||||||
if os.path.exists(self.CONFIG_FILE_PATH):
|
|
||||||
cp = moves.configparser.RawConfigParser()
|
|
||||||
try:
|
|
||||||
cp.read(self.CONFIG_FILE_PATH)
|
|
||||||
defaults = cp.defaults()
|
|
||||||
for key, value in defaults.items():
|
|
||||||
self.test_databases[key] = value
|
|
||||||
except moves.configparser.ParsingError as e:
|
|
||||||
self.fail("Failed to read test_migrations.conf config "
|
|
||||||
"file. Got error: %s" % e)
|
|
||||||
else:
|
|
||||||
self.fail("Failed to find test_migrations.conf config "
|
|
||||||
"file.")
|
|
||||||
|
|
||||||
self.engines = {}
|
|
||||||
for key, value in self.test_databases.items():
|
|
||||||
self.engines[key] = sqlalchemy.create_engine(value)
|
|
||||||
|
|
||||||
# We start each test case with a completely blank slate.
|
|
||||||
self._reset_databases()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
# We destroy the test data store between each test case,
|
|
||||||
# and recreate it, which ensures that we have no side-effects
|
|
||||||
# from the tests
|
|
||||||
self._reset_databases()
|
|
||||||
super(BaseMigrationTestCase, self).tearDown()
|
|
||||||
|
|
||||||
def execute_cmd(self, cmd=None):
|
|
||||||
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
output = process.communicate()[0]
|
|
||||||
LOG.debug(output)
|
|
||||||
self.assertEqual(0, process.returncode,
|
|
||||||
"Failed to run: %s\n%s" % (cmd, output))
|
|
||||||
|
|
||||||
def _reset_pg(self, conn_pieces):
|
|
||||||
(user,
|
|
||||||
password,
|
|
||||||
database,
|
|
||||||
host) = utils.get_db_connection_info(conn_pieces)
|
|
||||||
os.environ['PGPASSWORD'] = password
|
|
||||||
os.environ['PGUSER'] = user
|
|
||||||
# note(boris-42): We must create and drop database, we can't
|
|
||||||
# drop database which we have connected to, so for such
|
|
||||||
# operations there is a special database template1.
|
|
||||||
sqlcmd = ("psql -w -U %(user)s -h %(host)s -c"
|
|
||||||
" '%(sql)s' -d template1")
|
|
||||||
|
|
||||||
sql = ("drop database if exists %s;") % database
|
|
||||||
droptable = sqlcmd % {'user': user, 'host': host, 'sql': sql}
|
|
||||||
self.execute_cmd(droptable)
|
|
||||||
|
|
||||||
sql = ("create database %s;") % database
|
|
||||||
createtable = sqlcmd % {'user': user, 'host': host, 'sql': sql}
|
|
||||||
self.execute_cmd(createtable)
|
|
||||||
|
|
||||||
os.unsetenv('PGPASSWORD')
|
|
||||||
os.unsetenv('PGUSER')
|
|
||||||
|
|
||||||
@_set_db_lock(lock_prefix='migration_tests-')
|
|
||||||
def _reset_databases(self):
|
|
||||||
for key, engine in self.engines.items():
|
|
||||||
conn_string = self.test_databases[key]
|
|
||||||
conn_pieces = parse.urlparse(conn_string)
|
|
||||||
engine.dispose()
|
|
||||||
if conn_string.startswith('sqlite'):
|
|
||||||
# We can just delete the SQLite database, which is
|
|
||||||
# the easiest and cleanest solution
|
|
||||||
db_path = conn_pieces.path.strip('/')
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
# No need to recreate the SQLite DB. SQLite will
|
|
||||||
# create it for us if it's not there...
|
|
||||||
elif conn_string.startswith('mysql'):
|
|
||||||
# We can execute the MySQL client to destroy and re-create
|
|
||||||
# the MYSQL database, which is easier and less error-prone
|
|
||||||
# than using SQLAlchemy to do this via MetaData...trust me.
|
|
||||||
(user, password, database, host) = \
|
|
||||||
utils.get_db_connection_info(conn_pieces)
|
|
||||||
sql = ("drop database if exists %(db)s; "
|
|
||||||
"create database %(db)s;") % {'db': database}
|
|
||||||
cmd = ("mysql -u \"%(user)s\" -p\"%(password)s\" -h %(host)s "
|
|
||||||
"-e \"%(sql)s\"") % {'user': user, 'password': password,
|
|
||||||
'host': host, 'sql': sql}
|
|
||||||
self.execute_cmd(cmd)
|
|
||||||
elif conn_string.startswith('postgresql'):
|
|
||||||
self._reset_pg(conn_pieces)
|
|
||||||
|
|
||||||
|
|
||||||
class WalkVersionsMixin(object):
|
|
||||||
def _walk_versions(self, engine=None, snake_walk=False, downgrade=True):
|
|
||||||
# Determine latest version script from the repo, then
|
|
||||||
# upgrade from 1 through to the latest, with no data
|
|
||||||
# in the databases. This just checks that the schema itself
|
|
||||||
# upgrades successfully.
|
|
||||||
|
|
||||||
# Place the database under version control
|
|
||||||
self.migration_api.version_control(engine, self.REPOSITORY,
|
|
||||||
self.INIT_VERSION)
|
|
||||||
self.assertEqual(self.INIT_VERSION,
|
|
||||||
self.migration_api.db_version(engine,
|
|
||||||
self.REPOSITORY))
|
|
||||||
|
|
||||||
LOG.debug('latest version is %s' % self.REPOSITORY.latest)
|
|
||||||
versions = range(self.INIT_VERSION + 1, self.REPOSITORY.latest + 1)
|
|
||||||
|
|
||||||
for version in versions:
|
|
||||||
# upgrade -> downgrade -> upgrade
|
|
||||||
self._migrate_up(engine, version, with_data=True)
|
|
||||||
if snake_walk:
|
|
||||||
downgraded = self._migrate_down(
|
|
||||||
engine, version - 1, with_data=True)
|
|
||||||
if downgraded:
|
|
||||||
self._migrate_up(engine, version)
|
|
||||||
|
|
||||||
if downgrade:
|
|
||||||
# Now walk it back down to 0 from the latest, testing
|
|
||||||
# the downgrade paths.
|
|
||||||
for version in reversed(versions):
|
|
||||||
# downgrade -> upgrade -> downgrade
|
|
||||||
downgraded = self._migrate_down(engine, version - 1)
|
|
||||||
|
|
||||||
if snake_walk and downgraded:
|
|
||||||
self._migrate_up(engine, version)
|
|
||||||
self._migrate_down(engine, version - 1)
|
|
||||||
|
|
||||||
def _migrate_down(self, engine, version, with_data=False):
|
|
||||||
try:
|
|
||||||
self.migration_api.downgrade(engine, self.REPOSITORY, version)
|
|
||||||
except NotImplementedError:
|
|
||||||
# NOTE(sirp): some migrations, namely release-level
|
|
||||||
# migrations, don't support a downgrade.
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
version, self.migration_api.db_version(engine, self.REPOSITORY))
|
|
||||||
|
|
||||||
# NOTE(sirp): `version` is what we're downgrading to (i.e. the 'target'
|
|
||||||
# version). So if we have any downgrade checks, they need to be run for
|
|
||||||
# the previous (higher numbered) migration.
|
|
||||||
if with_data:
|
|
||||||
post_downgrade = getattr(
|
|
||||||
self, "_post_downgrade_%03d" % (version + 1), None)
|
|
||||||
if post_downgrade:
|
|
||||||
post_downgrade(engine)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _migrate_up(self, engine, version, with_data=False):
|
|
||||||
"""migrate up to a new version of the db.
|
|
||||||
|
|
||||||
We allow for data insertion and post checks at every
|
|
||||||
migration version with special _pre_upgrade_### and
|
|
||||||
_check_### functions in the main test.
|
|
||||||
"""
|
|
||||||
# NOTE(sdague): try block is here because it's impossible to debug
|
|
||||||
# where a failed data migration happens otherwise
|
|
||||||
try:
|
|
||||||
if with_data:
|
|
||||||
data = None
|
|
||||||
pre_upgrade = getattr(
|
|
||||||
self, "_pre_upgrade_%03d" % version, None)
|
|
||||||
if pre_upgrade:
|
|
||||||
data = pre_upgrade(engine)
|
|
||||||
|
|
||||||
self.migration_api.upgrade(engine, self.REPOSITORY, version)
|
|
||||||
self.assertEqual(version,
|
|
||||||
self.migration_api.db_version(engine,
|
|
||||||
self.REPOSITORY))
|
|
||||||
if with_data:
|
|
||||||
check = getattr(self, "_check_%03d" % version, None)
|
|
||||||
if check:
|
|
||||||
check(engine, data)
|
|
||||||
except Exception:
|
|
||||||
LOG.error(_LE("Failed to migrate to version %s on engine %s") %
|
|
||||||
(version, engine))
|
|
||||||
raise
|
|
||||||
@@ -1,655 +0,0 @@
|
|||||||
# Copyright 2010 United States Government as represented by the
|
|
||||||
# Administrator of the National Aeronautics and Space Administration.
|
|
||||||
# Copyright 2010-2011 OpenStack Foundation.
|
|
||||||
# Copyright 2012 Justin Santa Barbara
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
from sqlalchemy import Boolean
|
|
||||||
from sqlalchemy import CheckConstraint
|
|
||||||
from sqlalchemy import Column
|
|
||||||
from sqlalchemy.engine import reflection
|
|
||||||
from sqlalchemy.ext.compiler import compiles
|
|
||||||
from sqlalchemy import func
|
|
||||||
from sqlalchemy import Index
|
|
||||||
from sqlalchemy import Integer
|
|
||||||
from sqlalchemy import MetaData
|
|
||||||
from sqlalchemy import or_
|
|
||||||
from sqlalchemy.sql.expression import literal_column
|
|
||||||
from sqlalchemy.sql.expression import UpdateBase
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy import Table
|
|
||||||
from sqlalchemy.types import NullType
|
|
||||||
|
|
||||||
from mistral.openstack.common import context as request_context
|
|
||||||
from mistral.openstack.common.db.sqlalchemy import models
|
|
||||||
from mistral.openstack.common.gettextutils import _, _LI, _LW
|
|
||||||
from mistral.openstack.common import timeutils
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_DBURL_REGEX = re.compile(r"[^:]+://([^:]+):([^@]+)@.+")
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_db_url(url):
|
|
||||||
match = _DBURL_REGEX.match(url)
|
|
||||||
if match:
|
|
||||||
return '%s****:****%s' % (url[:match.start(1)], url[match.end(2):])
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSortKey(Exception):
|
|
||||||
message = _("Sort key supplied was not valid.")
|
|
||||||
|
|
||||||
|
|
||||||
# copy from glance/db/sqlalchemy/api.py
|
|
||||||
def paginate_query(query, model, limit, sort_keys, marker=None,
|
|
||||||
sort_dir=None, sort_dirs=None):
|
|
||||||
"""Returns a query with sorting / pagination criteria added.
|
|
||||||
|
|
||||||
Pagination works by requiring a unique sort_key, specified by sort_keys.
|
|
||||||
(If sort_keys is not unique, then we risk looping through values.)
|
|
||||||
We use the last row in the previous page as the 'marker' for pagination.
|
|
||||||
So we must return values that follow the passed marker in the order.
|
|
||||||
With a single-valued sort_key, this would be easy: sort_key > X.
|
|
||||||
With a compound-values sort_key, (k1, k2, k3) we must do this to repeat
|
|
||||||
the lexicographical ordering:
|
|
||||||
(k1 > X1) or (k1 == X1 && k2 > X2) or (k1 == X1 && k2 == X2 && k3 > X3)
|
|
||||||
|
|
||||||
We also have to cope with different sort_directions.
|
|
||||||
|
|
||||||
Typically, the id of the last row is used as the client-facing pagination
|
|
||||||
marker, then the actual marker object must be fetched from the db and
|
|
||||||
passed in to us as marker.
|
|
||||||
|
|
||||||
:param query: the query object to which we should add paging/sorting
|
|
||||||
:param model: the ORM model class
|
|
||||||
:param limit: maximum number of items to return
|
|
||||||
:param sort_keys: array of attributes by which results should be sorted
|
|
||||||
:param marker: the last item of the previous page; we returns the next
|
|
||||||
results after this value.
|
|
||||||
:param sort_dir: direction in which results should be sorted (asc, desc)
|
|
||||||
:param sort_dirs: per-column array of sort_dirs, corresponding to sort_keys
|
|
||||||
|
|
||||||
:rtype: sqlalchemy.orm.query.Query
|
|
||||||
:return: The query with sorting/pagination added.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if 'id' not in sort_keys:
|
|
||||||
# TODO(justinsb): If this ever gives a false-positive, check
|
|
||||||
# the actual primary key, rather than assuming its id
|
|
||||||
LOG.warning(_LW('Id not in sort_keys; is sort_keys unique?'))
|
|
||||||
|
|
||||||
assert(not (sort_dir and sort_dirs))
|
|
||||||
|
|
||||||
# Default the sort direction to ascending
|
|
||||||
if sort_dirs is None and sort_dir is None:
|
|
||||||
sort_dir = 'asc'
|
|
||||||
|
|
||||||
# Ensure a per-column sort direction
|
|
||||||
if sort_dirs is None:
|
|
||||||
sort_dirs = [sort_dir for _sort_key in sort_keys]
|
|
||||||
|
|
||||||
assert(len(sort_dirs) == len(sort_keys))
|
|
||||||
|
|
||||||
# Add sorting
|
|
||||||
for current_sort_key, current_sort_dir in zip(sort_keys, sort_dirs):
|
|
||||||
try:
|
|
||||||
sort_dir_func = {
|
|
||||||
'asc': sqlalchemy.asc,
|
|
||||||
'desc': sqlalchemy.desc,
|
|
||||||
}[current_sort_dir]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError(_("Unknown sort direction, "
|
|
||||||
"must be 'desc' or 'asc'"))
|
|
||||||
try:
|
|
||||||
sort_key_attr = getattr(model, current_sort_key)
|
|
||||||
except AttributeError:
|
|
||||||
raise InvalidSortKey()
|
|
||||||
query = query.order_by(sort_dir_func(sort_key_attr))
|
|
||||||
|
|
||||||
# Add pagination
|
|
||||||
if marker is not None:
|
|
||||||
marker_values = []
|
|
||||||
for sort_key in sort_keys:
|
|
||||||
v = getattr(marker, sort_key)
|
|
||||||
marker_values.append(v)
|
|
||||||
|
|
||||||
# Build up an array of sort criteria as in the docstring
|
|
||||||
criteria_list = []
|
|
||||||
for i in range(len(sort_keys)):
|
|
||||||
crit_attrs = []
|
|
||||||
for j in range(i):
|
|
||||||
model_attr = getattr(model, sort_keys[j])
|
|
||||||
crit_attrs.append((model_attr == marker_values[j]))
|
|
||||||
|
|
||||||
model_attr = getattr(model, sort_keys[i])
|
|
||||||
if sort_dirs[i] == 'desc':
|
|
||||||
crit_attrs.append((model_attr < marker_values[i]))
|
|
||||||
else:
|
|
||||||
crit_attrs.append((model_attr > marker_values[i]))
|
|
||||||
|
|
||||||
criteria = sqlalchemy.sql.and_(*crit_attrs)
|
|
||||||
criteria_list.append(criteria)
|
|
||||||
|
|
||||||
f = sqlalchemy.sql.or_(*criteria_list)
|
|
||||||
query = query.filter(f)
|
|
||||||
|
|
||||||
if limit is not None:
|
|
||||||
query = query.limit(limit)
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def _read_deleted_filter(query, db_model, read_deleted):
|
|
||||||
if 'deleted' not in db_model.__table__.columns:
|
|
||||||
raise ValueError(_("There is no `deleted` column in `%s` table. "
|
|
||||||
"Project doesn't use soft-deleted feature.")
|
|
||||||
% db_model.__name__)
|
|
||||||
|
|
||||||
default_deleted_value = db_model.__table__.c.deleted.default.arg
|
|
||||||
if read_deleted == 'no':
|
|
||||||
query = query.filter(db_model.deleted == default_deleted_value)
|
|
||||||
elif read_deleted == 'yes':
|
|
||||||
pass # omit the filter to include deleted and active
|
|
||||||
elif read_deleted == 'only':
|
|
||||||
query = query.filter(db_model.deleted != default_deleted_value)
|
|
||||||
else:
|
|
||||||
raise ValueError(_("Unrecognized read_deleted value '%s'")
|
|
||||||
% read_deleted)
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def _project_filter(query, db_model, context, project_only):
|
|
||||||
if project_only and 'project_id' not in db_model.__table__.columns:
|
|
||||||
raise ValueError(_("There is no `project_id` column in `%s` table.")
|
|
||||||
% db_model.__name__)
|
|
||||||
|
|
||||||
if request_context.is_user_context(context) and project_only:
|
|
||||||
if project_only == 'allow_none':
|
|
||||||
is_none = None
|
|
||||||
query = query.filter(or_(db_model.project_id == context.project_id,
|
|
||||||
db_model.project_id == is_none))
|
|
||||||
else:
|
|
||||||
query = query.filter(db_model.project_id == context.project_id)
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def model_query(context, model, session, args=None, project_only=False,
|
|
||||||
read_deleted=None):
|
|
||||||
"""Query helper that accounts for context's `read_deleted` field.
|
|
||||||
|
|
||||||
:param context: context to query under
|
|
||||||
|
|
||||||
:param model: Model to query. Must be a subclass of ModelBase.
|
|
||||||
:type model: models.ModelBase
|
|
||||||
|
|
||||||
:param session: The session to use.
|
|
||||||
:type session: sqlalchemy.orm.session.Session
|
|
||||||
|
|
||||||
:param args: Arguments to query. If None - model is used.
|
|
||||||
:type args: tuple
|
|
||||||
|
|
||||||
:param project_only: If present and context is user-type, then restrict
|
|
||||||
query to match the context's project_id. If set to
|
|
||||||
'allow_none', restriction includes project_id = None.
|
|
||||||
:type project_only: bool
|
|
||||||
|
|
||||||
:param read_deleted: If present, overrides context's read_deleted field.
|
|
||||||
:type read_deleted: bool
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
..code:: python
|
|
||||||
|
|
||||||
result = (utils.model_query(context, models.Instance, session=session)
|
|
||||||
.filter_by(uuid=instance_uuid)
|
|
||||||
.all())
|
|
||||||
|
|
||||||
query = utils.model_query(
|
|
||||||
context, Node,
|
|
||||||
session=session,
|
|
||||||
args=(func.count(Node.id), func.sum(Node.ram))
|
|
||||||
).filter_by(project_id=project_id)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not read_deleted:
|
|
||||||
if hasattr(context, 'read_deleted'):
|
|
||||||
# NOTE(viktors): some projects use `read_deleted` attribute in
|
|
||||||
# their contexts instead of `show_deleted`.
|
|
||||||
read_deleted = context.read_deleted
|
|
||||||
else:
|
|
||||||
read_deleted = context.show_deleted
|
|
||||||
|
|
||||||
if not issubclass(model, models.ModelBase):
|
|
||||||
raise TypeError(_("model should be a subclass of ModelBase"))
|
|
||||||
|
|
||||||
query = session.query(model) if not args else session.query(*args)
|
|
||||||
query = _read_deleted_filter(query, model, read_deleted)
|
|
||||||
query = _project_filter(query, model, context, project_only)
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def get_table(engine, name):
|
|
||||||
"""Returns an sqlalchemy table dynamically from db.
|
|
||||||
|
|
||||||
Needed because the models don't work for us in migrations
|
|
||||||
as models will be far out of sync with the current data.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Do not use this method when creating ForeignKeys in database migrations
|
|
||||||
because sqlalchemy needs the same MetaData object to hold information
|
|
||||||
about the parent table and the reference table in the ForeignKey. This
|
|
||||||
method uses a unique MetaData object per table object so it won't work
|
|
||||||
with ForeignKey creation.
|
|
||||||
"""
|
|
||||||
metadata = MetaData()
|
|
||||||
metadata.bind = engine
|
|
||||||
return Table(name, metadata, autoload=True)
|
|
||||||
|
|
||||||
|
|
||||||
class InsertFromSelect(UpdateBase):
|
|
||||||
"""Form the base for `INSERT INTO table (SELECT ... )` statement."""
|
|
||||||
def __init__(self, table, select):
|
|
||||||
self.table = table
|
|
||||||
self.select = select
|
|
||||||
|
|
||||||
|
|
||||||
@compiles(InsertFromSelect)
|
|
||||||
def visit_insert_from_select(element, compiler, **kw):
|
|
||||||
"""Form the `INSERT INTO table (SELECT ... )` statement."""
|
|
||||||
return "INSERT INTO %s %s" % (
|
|
||||||
compiler.process(element.table, asfrom=True),
|
|
||||||
compiler.process(element.select))
|
|
||||||
|
|
||||||
|
|
||||||
class ColumnError(Exception):
|
|
||||||
"""Error raised when no column or an invalid column is found."""
|
|
||||||
|
|
||||||
|
|
||||||
def _get_not_supported_column(col_name_col_instance, column_name):
|
|
||||||
try:
|
|
||||||
column = col_name_col_instance[column_name]
|
|
||||||
except KeyError:
|
|
||||||
msg = _("Please specify column %s in col_name_col_instance "
|
|
||||||
"param. It is required because column has unsupported "
|
|
||||||
"type by sqlite).")
|
|
||||||
raise ColumnError(msg % column_name)
|
|
||||||
|
|
||||||
if not isinstance(column, Column):
|
|
||||||
msg = _("col_name_col_instance param has wrong type of "
|
|
||||||
"column instance for column %s It should be instance "
|
|
||||||
"of sqlalchemy.Column.")
|
|
||||||
raise ColumnError(msg % column_name)
|
|
||||||
return column
|
|
||||||
|
|
||||||
|
|
||||||
def drop_unique_constraint(migrate_engine, table_name, uc_name, *columns,
|
|
||||||
**col_name_col_instance):
|
|
||||||
"""Drop unique constraint from table.
|
|
||||||
|
|
||||||
DEPRECATED: this function is deprecated and will be removed from mistral.db
|
|
||||||
in a few releases. Please use UniqueConstraint.drop() method directly for
|
|
||||||
sqlalchemy-migrate migration scripts.
|
|
||||||
|
|
||||||
This method drops UC from table and works for mysql, postgresql and sqlite.
|
|
||||||
In mysql and postgresql we are able to use "alter table" construction.
|
|
||||||
Sqlalchemy doesn't support some sqlite column types and replaces their
|
|
||||||
type with NullType in metadata. We process these columns and replace
|
|
||||||
NullType with the correct column type.
|
|
||||||
|
|
||||||
:param migrate_engine: sqlalchemy engine
|
|
||||||
:param table_name: name of table that contains uniq constraint.
|
|
||||||
:param uc_name: name of uniq constraint that will be dropped.
|
|
||||||
:param columns: columns that are in uniq constraint.
|
|
||||||
:param col_name_col_instance: contains pair column_name=column_instance.
|
|
||||||
column_instance is instance of Column. These params
|
|
||||||
are required only for columns that have unsupported
|
|
||||||
types by sqlite. For example BigInteger.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from migrate.changeset import UniqueConstraint
|
|
||||||
|
|
||||||
meta = MetaData()
|
|
||||||
meta.bind = migrate_engine
|
|
||||||
t = Table(table_name, meta, autoload=True)
|
|
||||||
|
|
||||||
if migrate_engine.name == "sqlite":
|
|
||||||
override_cols = [
|
|
||||||
_get_not_supported_column(col_name_col_instance, col.name)
|
|
||||||
for col in t.columns
|
|
||||||
if isinstance(col.type, NullType)
|
|
||||||
]
|
|
||||||
for col in override_cols:
|
|
||||||
t.columns.replace(col)
|
|
||||||
|
|
||||||
uc = UniqueConstraint(*columns, table=t, name=uc_name)
|
|
||||||
uc.drop()
|
|
||||||
|
|
||||||
|
|
||||||
def drop_old_duplicate_entries_from_table(migrate_engine, table_name,
|
|
||||||
use_soft_delete, *uc_column_names):
|
|
||||||
"""Drop all old rows having the same values for columns in uc_columns.
|
|
||||||
|
|
||||||
This method drop (or mark ad `deleted` if use_soft_delete is True) old
|
|
||||||
duplicate rows form table with name `table_name`.
|
|
||||||
|
|
||||||
:param migrate_engine: Sqlalchemy engine
|
|
||||||
:param table_name: Table with duplicates
|
|
||||||
:param use_soft_delete: If True - values will be marked as `deleted`,
|
|
||||||
if False - values will be removed from table
|
|
||||||
:param uc_column_names: Unique constraint columns
|
|
||||||
"""
|
|
||||||
meta = MetaData()
|
|
||||||
meta.bind = migrate_engine
|
|
||||||
|
|
||||||
table = Table(table_name, meta, autoload=True)
|
|
||||||
columns_for_group_by = [table.c[name] for name in uc_column_names]
|
|
||||||
|
|
||||||
columns_for_select = [func.max(table.c.id)]
|
|
||||||
columns_for_select.extend(columns_for_group_by)
|
|
||||||
|
|
||||||
duplicated_rows_select = sqlalchemy.sql.select(
|
|
||||||
columns_for_select, group_by=columns_for_group_by,
|
|
||||||
having=func.count(table.c.id) > 1)
|
|
||||||
|
|
||||||
for row in migrate_engine.execute(duplicated_rows_select):
|
|
||||||
# NOTE(boris-42): Do not remove row that has the biggest ID.
|
|
||||||
delete_condition = table.c.id != row[0]
|
|
||||||
is_none = None # workaround for pyflakes
|
|
||||||
delete_condition &= table.c.deleted_at == is_none
|
|
||||||
for name in uc_column_names:
|
|
||||||
delete_condition &= table.c[name] == row[name]
|
|
||||||
|
|
||||||
rows_to_delete_select = sqlalchemy.sql.select(
|
|
||||||
[table.c.id]).where(delete_condition)
|
|
||||||
for row in migrate_engine.execute(rows_to_delete_select).fetchall():
|
|
||||||
LOG.info(_LI("Deleting duplicated row with id: %(id)s from table: "
|
|
||||||
"%(table)s") % dict(id=row[0], table=table_name))
|
|
||||||
|
|
||||||
if use_soft_delete:
|
|
||||||
delete_statement = table.update().\
|
|
||||||
where(delete_condition).\
|
|
||||||
values({
|
|
||||||
'deleted': literal_column('id'),
|
|
||||||
'updated_at': literal_column('updated_at'),
|
|
||||||
'deleted_at': timeutils.utcnow()
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
delete_statement = table.delete().where(delete_condition)
|
|
||||||
migrate_engine.execute(delete_statement)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_default_deleted_value(table):
|
|
||||||
if isinstance(table.c.id.type, Integer):
|
|
||||||
return 0
|
|
||||||
if isinstance(table.c.id.type, String):
|
|
||||||
return ""
|
|
||||||
raise ColumnError(_("Unsupported id columns type"))
|
|
||||||
|
|
||||||
|
|
||||||
def _restore_indexes_on_deleted_columns(migrate_engine, table_name, indexes):
|
|
||||||
table = get_table(migrate_engine, table_name)
|
|
||||||
|
|
||||||
insp = reflection.Inspector.from_engine(migrate_engine)
|
|
||||||
real_indexes = insp.get_indexes(table_name)
|
|
||||||
existing_index_names = dict(
|
|
||||||
[(index['name'], index['column_names']) for index in real_indexes])
|
|
||||||
|
|
||||||
# NOTE(boris-42): Restore indexes on `deleted` column
|
|
||||||
for index in indexes:
|
|
||||||
if 'deleted' not in index['column_names']:
|
|
||||||
continue
|
|
||||||
name = index['name']
|
|
||||||
if name in existing_index_names:
|
|
||||||
column_names = [table.c[c] for c in existing_index_names[name]]
|
|
||||||
old_index = Index(name, *column_names, unique=index["unique"])
|
|
||||||
old_index.drop(migrate_engine)
|
|
||||||
|
|
||||||
column_names = [table.c[c] for c in index['column_names']]
|
|
||||||
new_index = Index(index["name"], *column_names, unique=index["unique"])
|
|
||||||
new_index.create(migrate_engine)
|
|
||||||
|
|
||||||
|
|
||||||
def change_deleted_column_type_to_boolean(migrate_engine, table_name,
|
|
||||||
**col_name_col_instance):
|
|
||||||
if migrate_engine.name == "sqlite":
|
|
||||||
return _change_deleted_column_type_to_boolean_sqlite(
|
|
||||||
migrate_engine, table_name, **col_name_col_instance)
|
|
||||||
insp = reflection.Inspector.from_engine(migrate_engine)
|
|
||||||
indexes = insp.get_indexes(table_name)
|
|
||||||
|
|
||||||
table = get_table(migrate_engine, table_name)
|
|
||||||
|
|
||||||
old_deleted = Column('old_deleted', Boolean, default=False)
|
|
||||||
old_deleted.create(table, populate_default=False)
|
|
||||||
|
|
||||||
table.update().\
|
|
||||||
where(table.c.deleted == table.c.id).\
|
|
||||||
values(old_deleted=True).\
|
|
||||||
execute()
|
|
||||||
|
|
||||||
table.c.deleted.drop()
|
|
||||||
table.c.old_deleted.alter(name="deleted")
|
|
||||||
|
|
||||||
_restore_indexes_on_deleted_columns(migrate_engine, table_name, indexes)
|
|
||||||
|
|
||||||
|
|
||||||
def _change_deleted_column_type_to_boolean_sqlite(migrate_engine, table_name,
|
|
||||||
**col_name_col_instance):
|
|
||||||
insp = reflection.Inspector.from_engine(migrate_engine)
|
|
||||||
table = get_table(migrate_engine, table_name)
|
|
||||||
|
|
||||||
columns = []
|
|
||||||
for column in table.columns:
|
|
||||||
column_copy = None
|
|
||||||
if column.name != "deleted":
|
|
||||||
if isinstance(column.type, NullType):
|
|
||||||
column_copy = _get_not_supported_column(col_name_col_instance,
|
|
||||||
column.name)
|
|
||||||
else:
|
|
||||||
column_copy = column.copy()
|
|
||||||
else:
|
|
||||||
column_copy = Column('deleted', Boolean, default=0)
|
|
||||||
columns.append(column_copy)
|
|
||||||
|
|
||||||
constraints = [constraint.copy() for constraint in table.constraints]
|
|
||||||
|
|
||||||
meta = table.metadata
|
|
||||||
new_table = Table(table_name + "__tmp__", meta,
|
|
||||||
*(columns + constraints))
|
|
||||||
new_table.create()
|
|
||||||
|
|
||||||
indexes = []
|
|
||||||
for index in insp.get_indexes(table_name):
|
|
||||||
column_names = [new_table.c[c] for c in index['column_names']]
|
|
||||||
indexes.append(Index(index["name"], *column_names,
|
|
||||||
unique=index["unique"]))
|
|
||||||
|
|
||||||
c_select = []
|
|
||||||
for c in table.c:
|
|
||||||
if c.name != "deleted":
|
|
||||||
c_select.append(c)
|
|
||||||
else:
|
|
||||||
c_select.append(table.c.deleted == table.c.id)
|
|
||||||
|
|
||||||
ins = InsertFromSelect(new_table, sqlalchemy.sql.select(c_select))
|
|
||||||
migrate_engine.execute(ins)
|
|
||||||
|
|
||||||
table.drop()
|
|
||||||
[index.create(migrate_engine) for index in indexes]
|
|
||||||
|
|
||||||
new_table.rename(table_name)
|
|
||||||
new_table.update().\
|
|
||||||
where(new_table.c.deleted == new_table.c.id).\
|
|
||||||
values(deleted=True).\
|
|
||||||
execute()
|
|
||||||
|
|
||||||
|
|
||||||
def change_deleted_column_type_to_id_type(migrate_engine, table_name,
|
|
||||||
**col_name_col_instance):
|
|
||||||
if migrate_engine.name == "sqlite":
|
|
||||||
return _change_deleted_column_type_to_id_type_sqlite(
|
|
||||||
migrate_engine, table_name, **col_name_col_instance)
|
|
||||||
insp = reflection.Inspector.from_engine(migrate_engine)
|
|
||||||
indexes = insp.get_indexes(table_name)
|
|
||||||
|
|
||||||
table = get_table(migrate_engine, table_name)
|
|
||||||
|
|
||||||
new_deleted = Column('new_deleted', table.c.id.type,
|
|
||||||
default=_get_default_deleted_value(table))
|
|
||||||
new_deleted.create(table, populate_default=True)
|
|
||||||
|
|
||||||
deleted = True # workaround for pyflakes
|
|
||||||
table.update().\
|
|
||||||
where(table.c.deleted == deleted).\
|
|
||||||
values(new_deleted=table.c.id).\
|
|
||||||
execute()
|
|
||||||
table.c.deleted.drop()
|
|
||||||
table.c.new_deleted.alter(name="deleted")
|
|
||||||
|
|
||||||
_restore_indexes_on_deleted_columns(migrate_engine, table_name, indexes)
|
|
||||||
|
|
||||||
|
|
||||||
def _change_deleted_column_type_to_id_type_sqlite(migrate_engine, table_name,
|
|
||||||
**col_name_col_instance):
|
|
||||||
# NOTE(boris-42): sqlaclhemy-migrate can't drop column with check
|
|
||||||
# constraints in sqlite DB and our `deleted` column has
|
|
||||||
# 2 check constraints. So there is only one way to remove
|
|
||||||
# these constraints:
|
|
||||||
# 1) Create new table with the same columns, constraints
|
|
||||||
# and indexes. (except deleted column).
|
|
||||||
# 2) Copy all data from old to new table.
|
|
||||||
# 3) Drop old table.
|
|
||||||
# 4) Rename new table to old table name.
|
|
||||||
insp = reflection.Inspector.from_engine(migrate_engine)
|
|
||||||
meta = MetaData(bind=migrate_engine)
|
|
||||||
table = Table(table_name, meta, autoload=True)
|
|
||||||
default_deleted_value = _get_default_deleted_value(table)
|
|
||||||
|
|
||||||
columns = []
|
|
||||||
for column in table.columns:
|
|
||||||
column_copy = None
|
|
||||||
if column.name != "deleted":
|
|
||||||
if isinstance(column.type, NullType):
|
|
||||||
column_copy = _get_not_supported_column(col_name_col_instance,
|
|
||||||
column.name)
|
|
||||||
else:
|
|
||||||
column_copy = column.copy()
|
|
||||||
else:
|
|
||||||
column_copy = Column('deleted', table.c.id.type,
|
|
||||||
default=default_deleted_value)
|
|
||||||
columns.append(column_copy)
|
|
||||||
|
|
||||||
def is_deleted_column_constraint(constraint):
|
|
||||||
# NOTE(boris-42): There is no other way to check is CheckConstraint
|
|
||||||
# associated with deleted column.
|
|
||||||
if not isinstance(constraint, CheckConstraint):
|
|
||||||
return False
|
|
||||||
sqltext = str(constraint.sqltext)
|
|
||||||
return (sqltext.endswith("deleted in (0, 1)") or
|
|
||||||
sqltext.endswith("deleted IN (:deleted_1, :deleted_2)"))
|
|
||||||
|
|
||||||
constraints = []
|
|
||||||
for constraint in table.constraints:
|
|
||||||
if not is_deleted_column_constraint(constraint):
|
|
||||||
constraints.append(constraint.copy())
|
|
||||||
|
|
||||||
new_table = Table(table_name + "__tmp__", meta,
|
|
||||||
*(columns + constraints))
|
|
||||||
new_table.create()
|
|
||||||
|
|
||||||
indexes = []
|
|
||||||
for index in insp.get_indexes(table_name):
|
|
||||||
column_names = [new_table.c[c] for c in index['column_names']]
|
|
||||||
indexes.append(Index(index["name"], *column_names,
|
|
||||||
unique=index["unique"]))
|
|
||||||
|
|
||||||
ins = InsertFromSelect(new_table, table.select())
|
|
||||||
migrate_engine.execute(ins)
|
|
||||||
|
|
||||||
table.drop()
|
|
||||||
[index.create(migrate_engine) for index in indexes]
|
|
||||||
|
|
||||||
new_table.rename(table_name)
|
|
||||||
deleted = True # workaround for pyflakes
|
|
||||||
new_table.update().\
|
|
||||||
where(new_table.c.deleted == deleted).\
|
|
||||||
values(deleted=new_table.c.id).\
|
|
||||||
execute()
|
|
||||||
|
|
||||||
# NOTE(boris-42): Fix value of deleted column: False -> "" or 0.
|
|
||||||
deleted = False # workaround for pyflakes
|
|
||||||
new_table.update().\
|
|
||||||
where(new_table.c.deleted == deleted).\
|
|
||||||
values(deleted=default_deleted_value).\
|
|
||||||
execute()
|
|
||||||
|
|
||||||
|
|
||||||
def get_connect_string(backend, database, user=None, passwd=None):
|
|
||||||
"""Get database connection
|
|
||||||
|
|
||||||
Try to get a connection with a very specific set of values, if we get
|
|
||||||
these then we'll run the tests, otherwise they are skipped
|
|
||||||
"""
|
|
||||||
args = {'backend': backend,
|
|
||||||
'user': user,
|
|
||||||
'passwd': passwd,
|
|
||||||
'database': database}
|
|
||||||
if backend == 'sqlite':
|
|
||||||
template = '%(backend)s:///%(database)s'
|
|
||||||
else:
|
|
||||||
template = "%(backend)s://%(user)s:%(passwd)s@localhost/%(database)s"
|
|
||||||
return template % args
|
|
||||||
|
|
||||||
|
|
||||||
def is_backend_avail(backend, database, user=None, passwd=None):
|
|
||||||
try:
|
|
||||||
connect_uri = get_connect_string(backend=backend,
|
|
||||||
database=database,
|
|
||||||
user=user,
|
|
||||||
passwd=passwd)
|
|
||||||
engine = sqlalchemy.create_engine(connect_uri)
|
|
||||||
connection = engine.connect()
|
|
||||||
except Exception:
|
|
||||||
# intentionally catch all to handle exceptions even if we don't
|
|
||||||
# have any backend code loaded.
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
connection.close()
|
|
||||||
engine.dispose()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection_info(conn_pieces):
|
|
||||||
database = conn_pieces.path.strip('/')
|
|
||||||
loc_pieces = conn_pieces.netloc.split('@')
|
|
||||||
host = loc_pieces[1]
|
|
||||||
|
|
||||||
auth_pieces = loc_pieces[0].split(':')
|
|
||||||
user = auth_pieces[0]
|
|
||||||
password = ""
|
|
||||||
if len(auth_pieces) > 1:
|
|
||||||
password = auth_pieces[1].strip()
|
|
||||||
|
|
||||||
return (user, password, database, host)
|
|
||||||
@@ -117,7 +117,7 @@ logging_cli_opts = [
|
|||||||
default=False,
|
default=False,
|
||||||
help='Use syslog for logging. '
|
help='Use syslog for logging. '
|
||||||
'Existing syslog format is DEPRECATED during I, '
|
'Existing syslog format is DEPRECATED during I, '
|
||||||
'and will chang in J to honor RFC5424.'),
|
'and will change in J to honor RFC5424.'),
|
||||||
cfg.BoolOpt('use-syslog-rfc-format',
|
cfg.BoolOpt('use-syslog-rfc-format',
|
||||||
# TODO(bogdando) remove or use True after existing
|
# TODO(bogdando) remove or use True after existing
|
||||||
# syslog format deprecation in J
|
# syslog format deprecation in J
|
||||||
@@ -424,9 +424,7 @@ class JSONFormatter(logging.Formatter):
|
|||||||
|
|
||||||
def _create_logging_excepthook(product_name):
|
def _create_logging_excepthook(product_name):
|
||||||
def logging_excepthook(exc_type, value, tb):
|
def logging_excepthook(exc_type, value, tb):
|
||||||
extra = {}
|
extra = {'exc_info': (exc_type, value, tb)}
|
||||||
if CONF.verbose or CONF.debug:
|
|
||||||
extra['exc_info'] = (exc_type, value, tb)
|
|
||||||
getLogger(product_name).critical(
|
getLogger(product_name).critical(
|
||||||
"".join(traceback.format_exception_only(exc_type, value)),
|
"".join(traceback.format_exception_only(exc_type, value)),
|
||||||
**extra)
|
**extra)
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
|
||||||
# The list of modules to copy from oslo-incubator.git
|
# The list of modules to copy from oslo-incubator.git
|
||||||
module=config
|
module=config.generator
|
||||||
module=cliutils
|
|
||||||
module=db
|
module=db
|
||||||
module=db.sqlalchemy
|
module=db.sqlalchemy.models
|
||||||
|
module=db.sqlalchemy.session
|
||||||
|
module=db.sqlalchemy.migration
|
||||||
module=log
|
module=log
|
||||||
module=test
|
|
||||||
module=jsonutils
|
module=jsonutils
|
||||||
module=loopingcall
|
module=loopingcall
|
||||||
module=periodic_task
|
module=periodic_task
|
||||||
module=threadgroup
|
module=threadgroup
|
||||||
module=timeutils
|
module=timeutils
|
||||||
|
module=importutils
|
||||||
|
module=strutils
|
||||||
|
module=uuidutils
|
||||||
|
|
||||||
# The base module to hold the copy of openstack.common
|
# The base module to hold the copy of openstack.common
|
||||||
base=mistral
|
base=mistral
|
||||||
|
|||||||
Reference in New Issue
Block a user