horizon/openstack_dashboard/api/base.py

377 lines
12 KiB
Python

# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections.abc
import functools
from django.conf import settings
import semantic_version
from horizon import exceptions
__all__ = ('APIResourceWrapper', 'APIDictWrapper',
'get_service_from_catalog', 'url_for',)
@functools.total_ordering
class Version(object):
"""A class to handle API version.
The current OpenStack APIs use the versioning of "<major>.<minor>",
so this class supports this style only.
"""
# NOTE(amotoki): The implementation depends on the semantic_version library
# but we don't care the patch version in this class.
def __init__(self, version):
# NOTE(amotoki):
# All comparisons should use self.sem_ver as we would like to
# compare versions in the semantic versioning way.
# self.orig_ver should be used only in __str__ and __repr__
# to keep the original version information.
self.orig_ver = str(version)
self.sem_ver = semantic_version.Version.coerce(str(version))
@property
def major(self):
return self.sem_ver.major
@property
def minor(self):
return self.sem_ver.minor
def __eq__(self, other):
return self.sem_ver == Version(other).sem_ver
def __lt__(self, other):
return self.sem_ver < Version(other).sem_ver
def __repr__(self):
return "Version('%s')" % self.orig_ver
def __str__(self):
return str(self.orig_ver)
def __hash__(self):
return hash(str(self.sem_ver))
class APIVersionManager(object):
"""Object to store and manage API versioning data and utility methods."""
def __init__(self, service_type, preferred_version=None):
self.service_type = service_type
self.preferred = preferred_version
self._active = None
self.supported = {}
# As a convenience, we can drop in a placeholder for APIs that we
# have not yet needed to version. This is useful, for example, when
# panels such as the admin metadata_defs wants to check the active
# version even though it's not explicitly defined. Previously
# this caused a KeyError.
if self.preferred:
self.supported[self.preferred] = {"version": self.preferred}
@property
def active(self):
if self._active is None:
self.get_active_version()
return self._active
def load_supported_version(self, version, data):
version = Version(version)
self.supported[version] = data
def get_active_version(self):
if self._active is not None:
return self.supported[self._active]
key = settings.OPENSTACK_API_VERSIONS.get(self.service_type)
if key is None:
# TODO(gabriel): support API version discovery here; we'll leave
# the setting in as a way of overriding the latest available
# version.
key = self.preferred
version = Version(key)
# Provide a helpful error message if the specified version isn't in the
# supported list.
if version not in self.supported:
choices = ", ".join(str(k) for k in self.supported)
msg = ('%s is not a supported API version for the %s service, '
' choices are: %s' % (version, self.service_type, choices))
raise exceptions.ConfigurationError(msg)
self._active = version
return self.supported[self._active]
def clear_active_cache(self):
self._active = None
class APIResourceWrapper(object):
"""Simple wrapper for api objects.
Define _attrs on the child class and pass in the
api object as the only argument to the constructor
"""
_attrs = []
_apiresource = None # Make sure _apiresource is there even in __init__.
def __init__(self, apiresource):
self._apiresource = apiresource
def __getattribute__(self, attr):
try:
return object.__getattribute__(self, attr)
except AttributeError:
if attr not in self._attrs:
raise
# __getattr__ won't find properties
return getattr(self._apiresource, attr)
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__,
dict((attr, getattr(self, attr))
for attr in self._attrs
if hasattr(self, attr)))
def to_dict(self):
obj = {}
for key in self._attrs:
obj[key] = getattr(self, key, None)
return obj
@property
def name_or_id(self):
return self.name or '(%s)' % self.id[:13]
class APIDictWrapper(object):
"""Simple wrapper for api dictionaries
Some api calls return dictionaries. This class provides identical
behavior as APIResourceWrapper, except that it will also behave as a
dictionary, in addition to attribute accesses.
Attribute access is the preferred method of access, to be
consistent with api resource objects from novaclient.
"""
_apidict = {} # Make sure _apidict is there even in __init__.
def __init__(self, apidict):
self._apidict = apidict
def __getattribute__(self, attr):
try:
return object.__getattribute__(self, attr)
except AttributeError:
if attr not in self._apidict:
raise
return self._apidict[attr]
def __getitem__(self, item):
try:
return getattr(self, item)
except (AttributeError, TypeError) as e:
# caller is expecting a KeyError
raise KeyError(e)
def __contains__(self, item):
try:
return hasattr(self, item)
except TypeError:
return False
def get(self, item, default=None):
try:
return getattr(self, item)
except (AttributeError, TypeError):
return default
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self._apidict)
def to_dict(self):
return self._apidict
class Quota(object):
"""Wrapper for individual limits in a quota."""
def __init__(self, name, limit):
self.name = name
self.limit = limit
def __repr__(self):
return "<Quota: (%s, %s)>" % (self.name, self.limit)
class QuotaSet(collections.abc.Sequence):
"""Wrapper for client QuotaSet objects.
This turns the individual quotas into Quota objects
for easier handling/iteration.
`QuotaSet` objects support a mix of `list` and `dict` methods; you can use
the bracket notation (`qs["my_quota"] = 0`) to add new quota values, and
use the `get` method to retrieve a specific quota, but otherwise it
behaves much like a list or tuple, particularly in supporting iteration.
"""
ignore_quotas = {}
def __init__(self, apiresource=None):
self.items = []
if apiresource:
if hasattr(apiresource, '_info'):
items = apiresource._info.items()
else:
items = apiresource.items()
for k, v in items:
if k == 'id' or k in self.ignore_quotas:
continue
self[k] = v
def __setitem__(self, k, v):
v = int(v) if v is not None else v
q = Quota(k, v)
self.items.append(q)
def __getitem__(self, index):
return self.items[index]
def __add__(self, other):
"""Merge another QuotaSet into this one.
Existing quotas are not overridden.
"""
if not isinstance(other, QuotaSet):
msg = "Can only add QuotaSet to QuotaSet, " \
"but received %s instead" % type(other)
raise ValueError(msg)
for item in other:
if self.get(item.name).limit is None:
self.items.append(item)
return self
def __len__(self):
return len(self.items)
def __repr__(self):
return repr(self.items)
def get(self, key, default=None):
match = [quota for quota in self.items if quota.name == key]
return match.pop() if match else Quota(key, default)
def add(self, other):
return self.__add__(other)
def get_service_from_catalog(catalog, service_type):
if catalog:
for service in catalog:
if 'type' not in service:
continue
if service['type'] == service_type:
return service
return None
# Mapping of V2 Catalog Endpoint_type to V3 Catalog Interfaces
# TODO(e0ne): remove this mapping once OPENSTACK_ENDPOINT_TYPE config option
# will be removed.
ENDPOINT_TYPE_TO_INTERFACE = {
'publicURL': 'public',
'internalURL': 'internal',
'adminURL': 'admin',
}
def get_url_for_service(service, region, endpoint_type):
if 'type' not in service:
return None
service_endpoints = service.get('endpoints', [])
available_endpoints = [endpoint for endpoint in service_endpoints
if region == _get_endpoint_region(endpoint)]
# if we are dealing with the identity service and there is no endpoint
# in the current region, it is okay to use the first endpoint for any
# identity service endpoints and we can assume that it is global
if service['type'] == 'identity' and not available_endpoints:
available_endpoints = service_endpoints
for endpoint in available_endpoints:
try:
interface = \
ENDPOINT_TYPE_TO_INTERFACE.get(endpoint_type, '')
if endpoint['interface'] == interface:
return endpoint['url']
except KeyError:
# it could be that the current endpoint just doesn't match the
# type, continue trying the next one
pass
def url_for(request, service_type, endpoint_type=None, region=None):
endpoint_type = endpoint_type or settings.OPENSTACK_ENDPOINT_TYPE
fallback_endpoint_type = settings.SECONDARY_ENDPOINT_TYPE
catalog = request.user.service_catalog
service = get_service_from_catalog(catalog, service_type)
if service:
if not region:
region = request.user.services_region
url = get_url_for_service(service,
region,
endpoint_type)
if not url and fallback_endpoint_type:
url = get_url_for_service(service,
region,
fallback_endpoint_type)
if url:
return url
raise exceptions.ServiceCatalogException(service_type)
def is_service_enabled(request, service_type):
service = get_service_from_catalog(request.user.service_catalog,
service_type)
if service:
region = request.user.services_region
for endpoint in service.get('endpoints', []):
if 'type' not in service:
continue
# ignore region for identity
if service['type'] == 'identity' or \
_get_endpoint_region(endpoint) == region:
return True
return False
def _get_endpoint_region(endpoint):
"""Common function for getting the region from endpoint.
In Keystone V3, region has been deprecated in favor of
region_id.
This method provides a way to get region that works for
both Keystone V2 and V3.
"""
return endpoint.get('region_id') or endpoint.get('region')