# 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 ".", 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 "" % (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')