This finalizes the removal of the placement code from nova. This change primarily removes code and makes fixes to cmd, test and migration tooling to adapt to the removal. Placement tests and documention were already removed in early patches. A database migration that calls consumer_obj.create_incomplete_consumers in nova-manage has been removed. A functional test which confirms the default incomplete consumer user and project id has been changes so its its use of conf.placement.incomplete_* (now removed) is replaced with a constant. The placement server, running in the functional test, provides its own config. placement-related configuration is updated to only register those opts which are relevant on the nova side. This mostly means ksa-related opts. placement-database configuration is removed from nova/conf/database. tox.ini is updated to remove the group_regex required by the placement gabbi tests. This should probably have gone when the placement functional tests went, but was overlooked. A release note is added which describes that this is cleanup, the main action already happened, but points people to the nova to placement upgrade instructions in case they haven't done it yet. Change-Id: I4181f39dea7eb10b84e6f5057938767b3e422affchanges/15/618215/37
parent
760fc2de32
commit
70a2879b2c
@ -1,102 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack.placement import context
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Middleware(object):
|
||||
|
||||
def __init__(self, application, **kwargs):
|
||||
self.application = application
|
||||
|
||||
|
||||
# NOTE(cdent): Only to be used in tests where auth is being faked.
|
||||
class NoAuthMiddleware(Middleware):
|
||||
"""Require a token if one isn't present."""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
if req.environ['PATH_INFO'] == '/':
|
||||
return self.application
|
||||
|
||||
if 'X-Auth-Token' not in req.headers:
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
|
||||
token = req.headers['X-Auth-Token']
|
||||
user_id, _sep, project_id = token.partition(':')
|
||||
project_id = project_id or user_id
|
||||
if user_id == 'admin':
|
||||
roles = ['admin']
|
||||
else:
|
||||
roles = []
|
||||
req.headers['X_USER_ID'] = user_id
|
||||
req.headers['X_TENANT_ID'] = project_id
|
||||
req.headers['X_ROLES'] = ','.join(roles)
|
||||
return self.application
|
||||
|
||||
|
||||
class PlacementKeystoneContext(Middleware):
|
||||
"""Make a request context from keystone headers."""
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
||||
|
||||
ctx = context.RequestContext.from_environ(
|
||||
req.environ, request_id=req_id)
|
||||
|
||||
if ctx.user_id is None and req.environ['PATH_INFO'] != '/':
|
||||
LOG.debug("Neither X_USER_ID nor X_USER found in request")
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
|
||||
req.environ['placement.context'] = ctx
|
||||
return self.application
|
||||
|
||||
|
||||
class PlacementAuthProtocol(auth_token.AuthProtocol):
|
||||
"""A wrapper on Keystone auth_token middleware.
|
||||
|
||||
Does not perform verification of authentication tokens
|
||||
for root in the API.
|
||||
|
||||
"""
|
||||
def __init__(self, app, conf):
|
||||
self._placement_app = app
|
||||
super(PlacementAuthProtocol, self).__init__(app, conf)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if environ['PATH_INFO'] == '/':
|
||||
return self._placement_app(environ, start_response)
|
||||
|
||||
return super(PlacementAuthProtocol, self).__call__(
|
||||
environ, start_response)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def auth_filter(app):
|
||||
return PlacementAuthProtocol(app, conf)
|
||||
return auth_filter
|
@ -1,52 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_context import context
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
|
||||
from nova.api.openstack.placement import exception
|
||||
from nova.api.openstack.placement import policy
|
||||
|
||||
|
||||
@enginefacade.transaction_context_provider
|
||||
class RequestContext(context.RequestContext):
|
||||
|
||||
def can(self, action, target=None, fatal=True):
|
||||
"""Verifies that the given action is valid on the target in this
|
||||
context.
|
||||
|
||||
:param action: string representing the action to be checked.
|
||||
:param target: As much information about the object being operated on
|
||||
as possible. The target argument should be a dict instance or an
|
||||
instance of a class that fully supports the Mapping abstract base
|
||||
class and deep copying. For object creation this should be a
|
||||
dictionary representing the location of the object e.g.
|
||||
``{'project_id': context.project_id}``. If None, then this default
|
||||
target will be considered::
|
||||
|
||||
{'project_id': self.project_id, 'user_id': self.user_id}
|
||||
:param fatal: if False, will return False when an
|
||||
exception.PolicyNotAuthorized occurs.
|
||||
:raises nova.api.openstack.placement.exception.PolicyNotAuthorized:
|
||||
if verification fails and fatal is True.
|
||||
:return: returns a non-False value (not necessarily "True") if
|
||||
authorized and False if not authorized and fatal is False.
|
||||
"""
|
||||
if target is None:
|
||||
target = {'project_id': self.project_id,
|
||||
'user_id': self.user_id}
|
||||
try:
|
||||
return policy.authorize(self, action, target)
|
||||
except exception.PolicyNotAuthorized:
|
||||
if fatal:
|
||||
raise
|
||||
return False
|
@ -1,48 +0,0 @@
|
||||
# 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.
|
||||
"""Database context manager for placement database connection, kept in its
|
||||
own file so the nova db_api (which has cascading imports) is not imported.
|
||||
"""
|
||||
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
from oslo_log import log as logging
|
||||
|
||||
from nova.utils import run_once
|
||||
|
||||
placement_context_manager = enginefacade.transaction_context()
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_db_conf(conf_group):
|
||||
return dict(conf_group.items())
|
||||
|
||||
|
||||
@run_once("TransactionFactory already started, not reconfiguring.",
|
||||
LOG.warning)
|
||||
def configure(conf):
|
||||
# If [placement_database]/connection is not set in conf, then placement
|
||||
# data will be stored in the nova_api database.
|
||||
if conf.placement_database.connection is None:
|
||||
placement_context_manager.configure(
|
||||
**_get_db_conf(conf.api_database))
|
||||
else:
|
||||
placement_context_manager.configure(
|
||||
**_get_db_conf(conf.placement_database))
|
||||
|
||||
|
||||
def get_placement_engine():
|
||||
return placement_context_manager.writer.get_engine()
|
||||
|
||||
|
||||
@enginefacade.transaction_context_provider
|
||||
class DbContext(object):
|
||||
"""Stub class for db session handling outside of web requests."""
|
@ -1,120 +0,0 @@
|
||||
# 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.
|
||||
"""Deployment handling for Placmenent API."""
|
||||
|
||||
from microversion_parse import middleware as mp_middleware
|
||||
import oslo_middleware
|
||||
from oslo_middleware import cors
|
||||
|
||||
from nova.api.openstack.placement import auth
|
||||
from nova.api.openstack.placement import db_api
|
||||
from nova.api.openstack.placement import fault_wrap
|
||||
from nova.api.openstack.placement import handler
|
||||
from nova.api.openstack.placement import microversion
|
||||
from nova.api.openstack.placement.objects import resource_provider
|
||||
from nova.api.openstack.placement import requestlog
|
||||
from nova.api.openstack.placement import util
|
||||
|
||||
|
||||
# TODO(cdent): NAME points to the config project being used, so for
|
||||
# now this is "nova" but we probably want "placement" eventually.
|
||||
NAME = "nova"
|
||||
|
||||
|
||||
def deploy(conf):
|
||||
"""Assemble the middleware pipeline leading to the placement app."""
|
||||
if conf.api.auth_strategy == 'noauth2':
|
||||
auth_middleware = auth.NoAuthMiddleware
|
||||
else:
|
||||
# Do not use 'oslo_config_project' param here as the conf
|
||||
# location may have been overridden earlier in the deployment
|
||||
# process with OS_PLACEMENT_CONFIG_DIR in wsgi.py.
|
||||
auth_middleware = auth.filter_factory(
|
||||
{}, oslo_config_config=conf)
|
||||
|
||||
# Pass in our CORS config, if any, manually as that's a)
|
||||
# explicit, b) makes testing more straightfoward, c) let's
|
||||
# us control the use of cors by the presence of its config.
|
||||
conf.register_opts(cors.CORS_OPTS, 'cors')
|
||||
if conf.cors.allowed_origin:
|
||||
cors_middleware = oslo_middleware.CORS.factory(
|
||||
{}, **conf.cors)
|
||||
else:
|
||||
cors_middleware = None
|
||||
|
||||
context_middleware = auth.PlacementKeystoneContext
|
||||
req_id_middleware = oslo_middleware.RequestId
|
||||
microversion_middleware = mp_middleware.MicroversionMiddleware
|
||||
fault_middleware = fault_wrap.FaultWrapper
|
||||
request_log = requestlog.RequestLog
|
||||
|
||||
application = handler.PlacementHandler()
|
||||
# configure microversion middleware in the old school way
|
||||
application = microversion_middleware(
|
||||
application, microversion.SERVICE_TYPE, microversion.VERSIONS,
|
||||
json_error_formatter=util.json_error_formatter)
|
||||
|
||||
# NOTE(cdent): The ordering here is important. The list is ordered
|
||||
# from the inside out. For a single request req_id_middleware is called
|
||||
# first and microversion_middleware last. Then the request is finally
|
||||
# passed to the application (the PlacementHandler). At that point
|
||||
# the response ascends the middleware in the reverse of the
|
||||
# order the request went in. This order ensures that log messages
|
||||
# all see the same contextual information including request id and
|
||||
# authentication information.
|
||||
for middleware in (fault_middleware,
|
||||
request_log,
|
||||
context_middleware,
|
||||
auth_middleware,
|
||||
cors_middleware,
|
||||
req_id_middleware,
|
||||
):
|
||||
if middleware:
|
||||
application = middleware(application)
|
||||
|
||||
# NOTE(mriedem): Ignore scope check UserWarnings from oslo.policy.
|
||||
if not conf.oslo_policy.enforce_scope:
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore',
|
||||
message="Policy .* failed scope check",
|
||||
category=UserWarning)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def update_database():
|
||||
"""Do any database updates required at process boot time, such as
|
||||
updating the traits table.
|
||||
"""
|
||||
ctx = db_api.DbContext()
|
||||
resource_provider.ensure_trait_sync(ctx)
|
||||
resource_provider.ensure_rc_cache(ctx)
|
||||
|
||||
|
||||
# NOTE(cdent): Althought project_name is no longer used because of the
|
||||
# resolution of https://bugs.launchpad.net/nova/+bug/1734491, loadapp()
|
||||
# is considered a public interface for the creation of a placement
|
||||
# WSGI app so must maintain its interface. The canonical placement WSGI
|
||||
# app is created by init_application in wsgi.py, but this is not
|
||||
# required and in fact can be limiting. loadapp() may be used from
|
||||
# fixtures or arbitrary WSGI frameworks and loaders.
|
||||
def loadapp(config, project_name=NAME):
|
||||
"""WSGI application creator for placement.
|
||||
|
||||
:param config: An olso_config.cfg.ConfigOpts containing placement
|
||||
configuration.
|
||||
:param project_name: oslo_config project name. Ignored, preserved for
|
||||
backwards compatibility
|
||||
"""
|
||||
application = deploy(config)
|
||||
update_database()
|
||||
return application
|
@ -1,94 +0,0 @@
|
||||
# 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.
|
||||
"""Call any URI in the placement service directly without real HTTP.
|
||||
|
||||
This is useful for those cases where processes wish to manipulate the
|
||||
Placement datastore but do not want to run Placement as a long running
|
||||
service. A PlacementDirect context manager is provided. Within that
|
||||
HTTP requests may be made as normal but they will not actually traverse
|
||||
a real socket.
|
||||
"""
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from keystoneauth1 import session
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
import requests
|
||||
from wsgi_intercept import interceptor
|
||||
|
||||
from nova.api.openstack.placement import deploy
|
||||
|
||||
|
||||
class PlacementDirect(interceptor.RequestsInterceptor):
|
||||
"""Provide access to the placement service without real HTTP.
|
||||
|
||||
wsgi-intercept is used to provide a keystoneauth1 Adapter that has access
|
||||
to an in-process placement service. This provides access to making changes
|
||||
to the placement database without requiring HTTP over the network - it
|
||||
remains in-process.
|
||||
|
||||
Authentication to the service is turned off; admin access is assumed.
|
||||
|
||||
Access is provided via a context manager which is responsible for
|
||||
turning the wsgi-intercept on and off, and setting and removing
|
||||
mocks required to keystoneauth1 to work around endpoint discovery.
|
||||
|
||||
Example::
|
||||
|
||||
with PlacementDirect(cfg.CONF, latest_microversion=True) as client:
|
||||
allocations = client.get('/allocations/%s' % consumer)
|
||||
|
||||
:param conf: An oslo config with the options used to configure
|
||||
the placement service (notably database connection
|
||||
string).
|
||||
:param latest_microversion: If True, API requests will use the latest
|
||||
microversion if not otherwise specified. If
|
||||
False (the default), the base microversion is
|
||||
the default.
|
||||
"""
|
||||
|
||||
def __init__(self, conf, latest_microversion=False):
|
||||
conf.set_override('auth_strategy', 'noauth2', group='api')
|
||||
app = lambda: deploy.loadapp(conf)
|
||||
self.url = 'http://%s/placement' % str(uuidutils.generate_uuid())
|
||||
# Supply our own session so the wsgi-intercept can intercept
|
||||
# the right thing.
|
||||
request_session = requests.Session()
|
||||
headers = {
|
||||
'x-auth-token': 'admin',
|
||||
}
|
||||
# TODO(efried): See below
|
||||
if latest_microversion:
|
||||
headers['OpenStack-API-Version'] = 'placement latest'
|
||||
self.adapter = adapter.Adapter(
|
||||
session.Session(auth=None, session=request_session,
|
||||
additional_headers=headers),
|
||||
service_type='placement', raise_exc=False)
|
||||
# TODO(efried): Figure out why this isn't working:
|
||||
# default_microversion='latest' if latest_microversion else None)
|
||||
self._mocked_endpoint = mock.patch(
|
||||
'keystoneauth1.session.Session.get_endpoint',
|
||||
new=mock.Mock(return_value=self.url))
|
||||
super(PlacementDirect, self).__init__(app, url=self.url)
|
||||
|
||||
def __enter__(self):
|
||||
"""Start the wsgi-intercept interceptor and keystone endpoint mock.
|
||||
|
||||
A no auth ksa Adapter is provided to the context being managed.
|
||||
"""
|
||||
super(PlacementDirect, self).__enter__()
|
||||
self._mocked_endpoint.start()
|
||||
return self.adapter
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self._mocked_endpoint.stop()
|
||||
return super(PlacementDirect, self).__exit__(*exc)
|
@ -1,48 +0,0 @@
|
||||
# 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.
|
||||
"""Error code symbols to be used in structured JSON error responses.
|
||||
|
||||
These are strings to be used in the 'code' attribute, as described by
|
||||
the API guideline on `errors`_.
|
||||
|
||||
There must be only one instance of any string value and it should have
|
||||
only one associated constant SYMBOL.
|
||||
|
||||
In a WSGI handler (representing the sole handler for an HTTP method and
|
||||
URI) each error condition should get a separate error code. Reusing an
|
||||
error code in a different handler is not just acceptable, but useful.
|
||||
|
||||
For example 'placement.inventory.inuse' is meaningful and correct in both
|
||||
``PUT /resource_providers/{uuid}/inventories`` and ``DELETE`` on the same
|
||||
URI.
|
||||
|
||||
.. _errors: http://specs.openstack.org/openstack/api-wg/guidelines/errors.html
|
||||
"""
|
||||
|
||||
# NOTE(cdent): This is the simplest thing that can possibly work, for now.
|
||||
# If it turns out we want to automate this, or put different resources in
|
||||
# different files, or otherwise change things, that's fine. The only thing
|
||||
# that needs to be maintained as the same are the strings that API end
|
||||
# users use. How they are created is completely fungible.
|
||||
|
||||
|
||||
# Do not change the string values. Once set, they are set.
|
||||
# Do not reuse string values. There should be only one symbol for any
|
||||
# value.
|
||||
DEFAULT = 'placement.undefined_code'
|
||||
INVENTORY_INUSE = 'placement.inventory.inuse'
|
||||
CONCURRENT_UPDATE = 'placement.concurrent_update'
|
||||
DUPLICATE_NAME = 'placement.duplicate_name'
|
||||
PROVIDER_IN_USE = 'placement.resource_provider.inuse'
|
||||
PROVIDER_CANNOT_DELETE_PARENT = (
|
||||
'placement.resource_provider.cannot_delete_parent')
|
||||
RESOURCE_PROVIDER_NOT_FOUND = 'placement.resource_provider.not_found'
|
@ -1,207 +0,0 @@
|
||||
# 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.
|
||||
"""Exceptions for use in the Placement API."""
|
||||
|
||||
# NOTE(cdent): The exceptions are copied from nova.exception, where they
|
||||
# were originally used. To prepare for extracting placement to its own
|
||||
# repository we wish to no longer do that. Instead, exceptions used by
|
||||
# placement should be in the placement hierarchy.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from nova.i18n import _
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _BaseException(Exception):
|
||||
"""Base Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'msg_fmt' property. That msg_fmt will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
msg_fmt = _("An unknown exception occurred.")
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.msg_fmt % kwargs
|
||||
except Exception:
|
||||
# NOTE(melwitt): This is done in a separate method so it can be
|
||||
# monkey-patched during testing to make it a hard failure.
|
||||
self._log_exception()
|
||||
message = self.msg_fmt
|
||||
|
||||
self.message = message
|
||||
super(_BaseException, self).__init__(message)
|
||||
|
||||
def _log_exception(self):
|
||||
# kwargs doesn't match a variable in the message
|
||||
# log the issue and the kwargs
|
||||
LOG.exception('Exception in string format operation')
|
||||
for name, value in self.kwargs.items():
|
||||
LOG.error("%s: %s" % (name, value)) # noqa
|
||||
|
||||
def format_message(self):
|
||||
# Use the first argument to the python Exception object which
|
||||
# should be our full exception message, (see __init__).
|
||||
return self.args[0]
|
||||
|
||||
|
||||
class NotFound(_BaseException):
|
||||
msg_fmt = _("Resource could not be found.")
|
||||
|
||||
|
||||
class Exists(_BaseException):
|
||||
msg_fmt = _("Resource already exists.")
|
||||
|
||||
|
||||
class InvalidInventory(_BaseException):
|
||||
msg_fmt = _("Inventory for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s' invalid.")
|
||||
|
||||
|
||||
class CannotDeleteParentResourceProvider(_BaseException):
|
||||
msg_fmt = _("Cannot delete resource provider that is a parent of "
|
||||
"another. Delete child providers first.")
|
||||
|
||||
|
||||
class ConcurrentUpdateDetected(_BaseException):
|
||||
msg_fmt = _("Another thread concurrently updated the data. "
|
||||
"Please retry your update")
|
||||
|
||||
|
||||
class ResourceProviderConcurrentUpdateDetected(ConcurrentUpdateDetected):
|
||||
msg_fmt = _("Another thread concurrently updated the resource provider "
|
||||
"data. Please retry your update")
|
||||
|
||||
|
||||
class InvalidAllocationCapacityExceeded(InvalidInventory):
|
||||
msg_fmt = _("Unable to create allocation for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s'. The requested "
|
||||
"amount would exceed the capacity.")
|
||||
|
||||
|
||||
class InvalidAllocationConstraintsViolated(InvalidInventory):
|
||||
msg_fmt = _("Unable to create allocation for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s'. The requested "
|
||||
"amount would violate inventory constraints.")
|
||||
|
||||
|
||||
class InvalidInventoryCapacity(InvalidInventory):
|
||||
msg_fmt = _("Invalid inventory for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s'. "
|
||||
"The reserved value is greater than or equal to total.")
|
||||
|
||||
|
||||
class InvalidInventoryCapacityReservedCanBeTotal(InvalidInventoryCapacity):
|
||||
msg_fmt = _("Invalid inventory for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s'. "
|
||||
"The reserved value is greater than total.")
|
||||
|
||||
|
||||
# An exception with this name is used on both sides of the placement/
|
||||
# nova interaction.
|
||||
class InventoryInUse(InvalidInventory):
|
||||
# NOTE(mriedem): This message cannot change without impacting the
|
||||
# nova.scheduler.client.report._RE_INV_IN_USE regex.
|
||||
msg_fmt = _("Inventory for '%(resource_classes)s' on "
|
||||
"resource provider '%(resource_provider)s' in use.")
|
||||
|
||||
|
||||
class InventoryWithResourceClassNotFound(NotFound):
|
||||
msg_fmt = _("No inventory of class %(resource_class)s found.")
|
||||
|
||||
|
||||
class MaxDBRetriesExceeded(_BaseException):
|
||||
msg_fmt = _("Max retries of DB transaction exceeded attempting to "
|
||||
"perform %(action)s.")
|
||||
|
||||
|
||||
class ObjectActionError(_BaseException):
|
||||
msg_fmt = _('Object action %(action)s failed because: %(reason)s')
|
||||
|
||||
|
||||
class PolicyNotAuthorized(_BaseException):
|
||||
msg_fmt = _("Policy does not allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class ResourceClassCannotDeleteStandard(_BaseException):
|
||||
msg_fmt = _("Cannot delete standard resource class %(resource_class)s.")
|
||||
|
||||
|
||||
class ResourceClassCannotUpdateStandard(_BaseException):
|
||||
msg_fmt = _("Cannot update standard resource class %(resource_class)s.")
|
||||
|
||||
|
||||
class ResourceClassExists(_BaseException):
|
||||
msg_fmt = _("Resource class %(resource_class)s already exists.")
|
||||
|
||||
|
||||
class ResourceClassInUse(_BaseException):
|
||||
msg_fmt = _("Cannot delete resource class %(resource_class)s. "
|
||||
"Class is in use in inventory.")
|
||||
|
||||
|
||||
class ResourceClassNotFound(NotFound):
|
||||
msg_fmt = _("No such resource class %(resource_class)s.")
|
||||
|
||||
|
||||
# An exception with this name is used on both sides of the placement/
|
||||
# nova interaction.
|
||||
class ResourceProviderInUse(_BaseException):
|
||||
msg_fmt = _("Resource provider has allocations.")
|
||||
|
||||
|
||||
class TraitCannotDeleteStandard(_BaseException):
|
||||
msg_fmt = _("Cannot delete standard trait %(name)s.")
|
||||
|
||||
|
||||
class TraitExists(_BaseException):
|
||||
msg_fmt = _("The Trait %(name)s already exists")
|
||||
|
||||
|
||||
class TraitInUse(_BaseException):
|
||||
msg_fmt = _("The trait %(name)s is in use by a resource provider.")
|
||||
|
||||
|
||||
class TraitNotFound(NotFound):
|
||||
msg_fmt = _("No such trait(s): %(names)s.")
|
||||
|
||||
|
||||
class ProjectNotFound(NotFound):
|
||||
msg_fmt = _("No such project(s): %(external_id)s.")
|
||||
|
||||
|
||||
class ProjectExists(Exists):
|
||||
msg_fmt = _("The project %(external_id)s already exists.")
|
||||
|
||||
|
||||
class UserNotFound(NotFound):
|
||||
msg_fmt = _("No such user(s): %(external_id)s.")
|
||||
|
||||
|
||||
class UserExists(Exists):
|
||||
msg_fmt = _("The user %(external_id)s already exists.")
|
||||
|
||||
|
||||
class ConsumerNotFound(NotFound):
|
||||
msg_fmt = _("No such consumer(s): %(uuid)s.")
|
||||
|
||||
|
||||
class ConsumerExists(Exists):
|
||||
msg_fmt = _("The consumer %(uuid)s already exists.")
|
@ -1,48 +0,0 @@
|
||||
# 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.
|
||||
"""Simple middleware for safely catching unexpected exceptions."""
|
||||
|
||||
# NOTE(cdent): This is a super simplified replacement for the nova
|
||||
# FaultWrapper, which does more than placement needs.
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack.placement import util
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FaultWrapper(object):
|
||||
"""Turn an uncaught exception into a status 500.
|
||||
|
||||
Uncaught exceptions usually shouldn't happen, if it does it
|
||||
means there is a bug in the placement service, which should be
|
||||
fixed.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
return self.application(environ, start_response)
|
||||
except Exception as unexpected_exception:
|
||||
LOG.exception('Placement API unexpected error: %s',
|
||||
unexpected_exception)
|
||||
formatted_exception = exc.HTTPInternalServerError(
|
||||
six.text_type(unexpected_exception))
|
||||
formatted_exception.json_formatter = util.json_error_formatter
|
||||
return formatted_exception.generate_response(
|
||||
environ, start_response)
|
@ -1,231 +0,0 @@
|
||||
# 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.
|
||||
"""Handlers for placement API.
|
||||
|
||||
Individual handlers are associated with URL paths in the
|
||||
ROUTE_DECLARATIONS dictionary. At the top level each key is a Routes
|
||||
compliant path. The value of that key is a dictionary mapping
|
||||
individual HTTP request methods to a Python function representing a
|
||||
simple WSGI application for satisfying that request.
|
||||
|
||||
The ``make_map`` method processes ROUTE_DECLARATIONS to create a
|
||||
Routes.Mapper, including automatic handlers to respond with a
|
||||
405 when a request is made against a valid URL with an invalid
|
||||
method.
|
||||
"""
|
||||
|
||||
import routes
|
||||
import webob
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from nova.api.openstack.placement import exception
|
||||
from nova.api.openstack.placement.handlers import aggregate
|
||||
from nova.api.openstack.placement.handlers import allocation
|
||||
from nova.api.openstack.placement.handlers import allocation_candidate
|
||||
from nova.api.openstack.placement.handlers import inventory
|
||||
from nova.api.openstack.placement.handlers import reshaper
|
||||
from nova.api.openstack.placement.handlers import resource_class
|
||||
from nova.api.openstack.placement.handlers import resource_provider
|
||||
from nova.api.openstack.placement.handlers import root
|
||||
from nova.api.openstack.placement.handlers import trait
|
||||
from nova.api.openstack.placement.handlers import usage
|
||||
from nova.api.openstack.placement import util
|
||||
from nova.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# URLs and Handlers
|
||||
# NOTE(cdent): When adding URLs here, do not use regex patterns in
|
||||
# the path parameters (e.g. {uuid:[0-9a-zA-Z-]+}) as that will lead
|
||||
# to 404s that are controlled outside of the individual resources
|
||||
# and thus do not include specific information on the why of the 404.
|
||||
ROUTE_DECLARATIONS = {
|
||||
'/': {
|
||||
'GET': root.home,
|
||||
},
|
||||
# NOTE(cdent): This allows '/placement/' and '/placement' to
|
||||
# both work as the root of the service, which we probably want
|
||||
# for those situations where the service is mounted under a
|
||||
# prefix (as it is in devstack). While weird, an empty string is
|
||||
# a legit key in a dictionary and matches as desired in Routes.
|
||||
'': {
|
||||
'GET': root.home,
|
||||
},
|
||||
'/resource_classes': {
|
||||
'GET': resource_class.list_resource_classes,
|
||||
'POST': resource_class.create_resource_class
|
||||
},
|
||||
'/resource_classes/{name}': {
|
||||
'GET': resource_class.get_resource_class,
|
||||
'PUT': resource_class.update_resource_class,
|
||||
'DELETE': resource_class.delete_resource_class,
|
||||
},
|
||||
'/resource_providers': {
|
||||
'GET': resource_provider.list_resource_providers,
|
||||
'POST': resource_provider.create_resource_provider
|
||||
},
|
||||
'/resource_providers/{uuid}': {
|
||||
'GET': resource_provider.get_resource_provider,
|
||||
'DELETE': resource_provider.delete_resource_provider,
|
||||
'PUT': resource_provider.update_resource_provider
|
||||
},
|
||||
'/resource_providers/{uuid}/inventories': {
|
||||
'GET': inventory.get_inventories,
|
||||
'POST': inventory.create_inventory,
|
||||
'PUT': inventory.set_inventories,
|
||||
'DELETE': inventory.delete_inventories
|
||||
},
|
||||
'/resource_providers/{uuid}/inventories/{resource_class}': {
|
||||
'GET': inventory.get_inventory,
|
||||
'PUT': inventory.update_inventory,
|
||||
'DELETE': inventory.delete_inventory
|
||||
},
|
||||
'/resource_providers/{uuid}/usages': {
|
||||
'GET': usage.list_usages
|
||||
},
|
||||
'/resource_providers/{uuid}/aggregates': {
|
||||
'GET': aggregate.get_aggregates,
|
||||
'PUT': aggregate.set_aggregates
|
||||
},
|
||||
'/resource_providers/{uuid}/allocations': {
|
||||
'GET': allocation.list_for_resource_provider,
|
||||
},
|
||||
'/allocations': {
|
||||
'POST': allocation.set_allocations,
|
||||
},
|
||||
'/allocations/{consumer_uuid}': {
|
||||
'GET': allocation.list_for_consumer,
|
||||
'PUT': allocation.set_allocations_for_consumer,
|
||||
'DELETE': allocation.delete_allocations,
|
||||
},
|
||||
'/allocation_candidates': {
|
||||
'GET': allocation_candidate.list_allocation_candidates,
|
||||
},
|
||||
'/traits': {
|
||||
'GET': trait.list_traits,
|
||||
},
|
||||
'/traits/{name}': {
|
||||
'GET': trait.get_trait,
|
||||
'PUT': trait.put_trait,
|
||||
'DELETE': trait.delete_trait,
|
||||
},
|
||||
'/resource_providers/{uuid}/traits': {
|
||||
'GET': trait.list_traits_for_resource_provider,
|
||||
'PUT': trait.update_traits_for_resource_provider,
|
||||
'DELETE': trait.delete_traits_for_resource_provider
|
||||
},
|
||||
'/usages': {
|
||||
'GET': usage.get_total_usages,
|
||||
},
|
||||
'/reshaper': {
|
||||
'POST': reshaper.reshape,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def dispatch(environ, start_response, mapper):
|
||||
"""Find a matching route for the current request.
|
||||
|
||||
If no match is found, raise a 404 response.
|
||||
If there is a matching route, but no matching handler
|
||||
for the given method, raise a 405.
|
||||
"""
|
||||
result = mapper.match(environ=environ)
|
||||
if result is None:
|
||||
raise webob.exc.HTTPNotFound(
|
||||
json_formatter=util.json_error_formatter)
|
||||
# We can't reach this code without action being present.
|
||||
handler = result.pop('action')
|
||||
environ['wsgiorg.routing_args'] = ((), result)
|
||||
return handler(environ, start_response)
|
||||
|
||||
|
||||
def handle_405(environ, start_response):
|
||||
"""Return a 405 response when method is not allowed.
|
||||
|
||||
If _methods are in routing_args, send an allow header listing
|
||||
the methods that are possible on the provided URL.
|
||||
"""
|
||||
_methods = util.wsgi_path_item(environ, '_methods')
|
||||
headers = {}
|
||||
if _methods:
|
||||
# Ensure allow header is a python 2 or 3 native string (thus
|
||||
# not unicode in python 2 but stay a string in python 3)
|
||||
# In the process done by Routes to save the allowed methods
|
||||
# to its routing table they become unicode in py2.
|
||||
headers['allow'] = str(_methods)
|
||||
# Use Exception class as WSGI Application. We don't want to raise here.
|
||||
response = webob.exc.HTTPMethodNotAllowed(
|
||||
_('The method specified is not allowed for this resource.'),
|
||||
headers=headers, json_formatter=util.json_error_formatter)
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def make_map(declarations):
|
||||
"""Process route declarations to create a Route Mapper."""
|
||||
mapper = routes.Mapper()
|
||||
for route, targets in declarations.items():
|
||||
allowed_methods = []
|
||||
for method in targets:
|
||||
mapper.connect(route, action=targets[method],
|
||||
conditions=dict(method=[method]))
|
||||
allowed_methods.append(method)
|
||||
allowed_methods = ', '.join(allowed_methods)
|
||||
mapper.connect(route, action=handle_405, _methods=allowed_methods)
|
||||
return mapper
|
||||
|
||||
|
||||
class PlacementHandler(object):
|
||||
"""Serve Placement API.
|
||||
|
||||
Dispatch to handlers defined in ROUTE_DECLARATIONS.
|
||||
"""
|
||||
|
||||
def __init__(self, **local_config):
|
||||
# NOTE(cdent): Local config currently unused.
|
||||
self._map = make_map(ROUTE_DECLARATIONS)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# Check that an incoming request with a content-length header
|
||||
# that is an integer > 0 and not empty, also has a content-type
|
||||
# header that is not empty. If not raise a 400.
|
||||
clen = environ.get('CONTENT_LENGTH')
|
||||
try:
|
||||
if clen and (int(clen) > 0) and not environ.get('CONTENT_TYPE'):
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_('content-type header required when content-length > 0'),
|
||||
json_formatter=util.json_error_formatter)
|
||||
except ValueError as exc:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_('content-length header must be an integer'),
|
||||
json_formatter=util.json_error_formatter)
|
||||
try:
|
||||
return dispatch(environ, start_response, self._map)
|
||||
# Trap the NotFound exceptions raised by the objects used
|
||||
# with the API and transform them into webob.exc.HTTPNotFound.
|
||||
except exception.NotFound as exc:
|
||||
raise webob.exc.HTTPNotFound(
|
||||
exc, json_formatter=util.json_error_formatter)
|
||||
except exception.PolicyNotAuthorized as exc:
|
||||
raise webob.exc.HTTPForbidden(
|
||||
exc.format_message(),
|
||||
json_formatter=util.json_error_formatter)
|
||||
# Remaining uncaught exceptions will rise first to the Microversion
|
||||
# middleware, where any WebOb generated exceptions will be caught and
|
||||
# transformed into legit HTTP error responses (with microversion
|
||||
# headers added), and then to the FaultWrapper middleware which will
|
||||
# catch anything else and transform them into 500 responses.
|
||||
# NOTE(cdent): There should be very few uncaught exceptions which are
|
||||
# not WebOb exceptions at this stage as the handlers are contained by
|
||||
# the wsgify decorator which will transform those exceptions to
|
||||
# responses itself.
|
@ -1,133 +0,0 @@
|
||||
# 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.
|
||||
"""Aggregate handlers for Placement API."""
|
||||
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import errors
|
||||
from nova.api.openstack.placement import exception
|
||||
from nova.api.openstack.placement import microversion
|
||||
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
||||
from nova.api.openstack.placement.policies import aggregate as policies
|
||||
from nova.api.openstack.placement.schemas import aggregate as schema
|
||||
from nova.api.openstack.placement import util
|
||||
from nova.api.openstack.placement import wsgi_wrapper
|
||||
from nova.i18n import _
|
||||
|
||||
|
||||
_INCLUDE_GENERATION_VERSION = (1, 19)
|
||||
|
||||
|
||||
def _send_aggregates(req, resource_provider, aggregate_uuids):
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
response = req.response
|
||||
response.status = 200
|
||||
payload = _serialize_aggregates(aggregate_uuids)
|
||||
if want_version.matches(min_version=_INCLUDE_GENERATION_VERSION):
|
||||
payload['resource_provider_generation'] = resource_provider.generation
|
||||
response.body = encodeutils.to_utf8(
|
||||
jsonutils.dumps(payload))
|
||||
response.content_type = 'application/json'
|
||||
if want_version.matches((1, 15)):
|
||||
req.response.cache_control = 'no-cache'
|
||||
# We never get an aggregate itself, we get the list of aggregates
|
||||
# that are associated with a resource provider. We don't record the
|
||||
# time when that association was made and the time when an aggregate
|
||||
# uuid was created is not relevant, so here we punt and use utcnow.
|
||||
req.response.last_modified = timeutils.utcnow(with_timezone=True)
|
||||
return response
|
||||
|
||||
|
||||
def _serialize_aggregates(aggregate_uuids):
|
||||
return {'aggregates': aggregate_uuids}
|
||||
|
||||
|
||||
def _set_aggregates(resource_provider, aggregate_uuids,
|
||||
increment_generation=False):
|
||||
"""Set aggregates for the resource provider.
|
||||
|
||||
If increment generation is true, the resource provider generation
|
||||
will be incremented if possible. If that fails (because something
|
||||
else incremented the generation in another thread), a
|
||||
ConcurrentUpdateDetected will be raised.
|
||||
"""
|
||||
# NOTE(cdent): It's not clear what the DBDuplicateEntry handling
|
||||
# is doing here, set_aggregates already handles that, but I'm leaving
|
||||
# it here because it was already there.
|
||||
try:
|
||||
resource_provider.set_aggregates(
|
||||
aggregate_uuids, increment_generation=increment_generation)
|
||||
except exception.ConcurrentUpdateDetected as exc:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('Update conflict: %(error)s') % {'error': exc},
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
except db_exc.DBDuplicateEntry as exc:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('Update conflict: %(error)s') % {'error': exc})
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@util.check_accept('application/json')
|
||||
@microversion.version_handler('1.1')
|
||||
def get_aggregates(req):
|
||||
"""GET a list of aggregates associated with a resource provider.
|
||||
|
||||
If the resource provider does not exist return a 404.
|
||||
|
||||
On success return a 200 with an application/json body containing a
|
||||
list of aggregate uuids.
|
||||
"""
|
||||
context = req.environ['placement.context']
|
||||
context.can(policies.LIST)
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
||||
context, uuid)
|
||||
aggregate_uuids = resource_provider.get_aggregates()
|
||||
|
||||
return _send_aggregates(req, resource_provider, aggregate_uuids)
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@util.require_content('application/json')
|
||||
@microversion.version_handler('1.1')
|
||||
def set_aggregates(req):
|
||||
context = req.environ['placement.context']
|
||||
context.can(policies.UPDATE)
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
consider_generation = want_version.matches(
|
||||
min_version=_INCLUDE_GENERATION_VERSION)
|
||||
put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_1
|
||||
if consider_generation:
|
||||
put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_19
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
||||
context, uuid)
|
||||
data = util.extract_json(req.body, put_schema)
|
||||
if consider_generation:
|
||||
# Check for generation conflict
|
||||
rp_gen = data['resource_provider_generation']
|
||||
if resource_provider.generation != rp_gen:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_("Resource provider's generation already changed. Please "
|
||||
"update the generation and try again."),
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
aggregate_uuids = data['aggregates']
|
||||
else:
|
||||
aggregate_uuids = data
|
||||
_set_aggregates(resource_provider, aggregate_uuids,
|
||||
increment_generation=consider_generation)
|
||||
|
||||
return _send_aggregates(req, resource_provider, aggregate_uuids)
|
@ -1,576 +0,0 @@
|
||||
# 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.
|
||||
"""Placement API handlers for setting and deleting allocations."""
|
||||
|
||||
import collections
|
||||
import uuid
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import errors
|
||||
from nova.api.openstack.placement import exception
|
||||
from nova.api.openstack.placement import microversion
|
||||
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
||||
from nova.api.openstack.placement.policies import allocation as policies
|
||||
from nova.api.openstack.placement.schemas import allocation as schema
|
||||
from nova.api.openstack.placement import util
|
||||
from nova.api.openstack.placement import wsgi_wrapper
|
||||
from nova.i18n import _
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _last_modified_from_allocations(allocations, want_version):
|
||||
"""Given a set of allocation objects, returns the last modified timestamp.
|
||||
"""
|
||||
# NOTE(cdent): The last_modified for an allocation will always be
|
||||
# based off the created_at column because allocations are only
|
||||
# ever inserted, never updated.
|
||||
last_modified = None
|
||||
# Only calculate last-modified if we are using a microversion that
|
||||
# supports it.
|
||||
get_last_modified = want_version and want_version.matches((1, 15))
|
||||
for allocation in allocations:
|
||||
if get_last_modified:
|
||||
last_modified = util.pick_last_modified(last_modified, allocation)
|
||||
|
||||
last_modified = last_modified or timeutils.utcnow(with_timezone=True)
|
||||
return last_modified
|
||||
|
||||
|
||||
def _serialize_allocations_for_consumer(allocations, want_version):
|
||||
"""Turn a list of allocations into a dict by resource provider uuid.
|
||||
|
||||
{
|
||||
'allocations': {
|
||||
RP_UUID_1: {
|
||||
'generation': GENERATION,
|
||||
'resources': {
|
||||
'DISK_GB': 4,
|
||||
'VCPU': 2
|
||||
}
|
||||
},
|
||||
RP_UUID_2: {
|
||||
'generation': GENERATION,
|
||||
'resources': {
|
||||
'DISK_GB': 6,
|
||||
'VCPU': 3
|
||||
}
|
||||
}
|
||||
},
|
||||
# project_id and user_id are added with microverion 1.12
|
||||
'project_id': PROJECT_ID,
|
||||
'user_id': USER_ID,
|
||||
# Generation for consumer >= 1.28
|
||||
'consumer_generation': 1
|
||||
}
|
||||
"""
|
||||
allocation_data = collections.defaultdict(dict)
|
||||
for allocation in allocations:
|
||||
key = allocation.resource_provider.uuid
|
||||
if 'resources' not in allocation_data[key]:
|
||||
allocation_data[key]['resources'] = {}
|
||||
|
||||
resource_class = allocation.resource_class
|
||||
allocation_data[key]['resources'][resource_class] = allocation.used
|
||||
generation = allocation.resource_provider.generation
|
||||
allocation_data[key]['generation'] = generation
|
||||
|
||||
result = {'allocations': allocation_data}
|
||||
if allocations and want_version.matches((1, 12)):
|
||||
# We're looking at a list of allocations by consumer id so project and
|
||||
# user are consistent across the list
|
||||
consumer = allocations[0].consumer
|
||||
project_id = consumer.project.external_id
|
||||
user_id = consumer.user.external_id
|
||||
result['project_id'] = project_id
|
||||
result['user_id'] = user_id
|
||||
show_consumer_gen = want_version.matches((1, 28))
|
||||
if show_consumer_gen:
|
||||
result['consumer_generation'] = consumer.generation
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _serialize_allocations_for_resource_provider(allocations,
|
||||
resource_provider,
|
||||
want_version):
|
||||
"""Turn a list of allocations into a dict by consumer id.
|
||||
|
||||
{'resource_provider_generation': GENERATION,
|
||||
'allocations':
|
||||
CONSUMER_ID_1: {
|
||||
'resources': {
|
||||
'DISK_GB': 4,
|
||||
'VCPU': 2
|
||||
},
|
||||
# Generation for consumer >= 1.28
|
||||
'consumer_generation': 0
|
||||
},
|
||||
CONSUMER_ID_2: {
|
||||
'resources': {
|
||||
'DISK_GB': 6,
|
||||
'VCPU': 3
|
||||
},
|
||||
# Generation for consumer >= 1.28
|
||||
'consumer_generation': 0
|
||||
}
|
||||
}
|
||||
"""
|
||||
show_consumer_gen = want_version.matches((1, 28))
|
||||
allocation_data = collections.defaultdict(dict)
|
||||
for allocation in allocations:
|
||||
key = allocation.consumer.uuid
|
||||
if 'resources' not in allocation_data[key]:
|
||||
allocation_data[key]['resources'] = {}
|
||||
|
||||
resource_class = allocation.resource_class
|
||||
allocation_data[key]['resources'][resource_class] = allocation.used
|
||||
|
||||
if show_consumer_gen:
|
||||
consumer_gen = None
|
||||
if allocation.consumer is not None:
|
||||
consumer_gen = allocation.consumer.generation
|
||||
allocation_data[key]['consumer_generation'] = consumer_gen
|
||||
|
||||
result = {'allocations': allocation_data}
|
||||
result['resource_provider_generation'] = resource_provider.generation
|
||||
return result
|
||||
|
||||
|
||||
# TODO(cdent): Extracting this is useful, for reuse by reshaper code,
|
||||
# but having it in this file seems wrong, however, since it uses
|
||||
# _new_allocations it's being left here for now. We need a place for shared
|
||||
# handler code, but util.py is already too big and too diverse.
|
||||
def create_allocation_list(context, data, consumers):
|
||||
"""Create an AllocationList based on provided data.
|
||||
|
||||
:param context: The placement context.
|
||||
:param data: A dictionary of multiple allocations by consumer uuid.
|
||||
:param consumers: A dictionary, keyed by consumer UUID, of Consumer objects
|
||||
:return: An AllocationList.
|
||||
:raises: `webob.exc.HTTPBadRequest` if a resource provider included in the
|
||||
allocations does not exist.
|
||||
"""
|
||||
allocation_objects = []
|
||||
|
||||
for consumer_uuid in data:
|
||||
allocations = data[consumer_uuid]['allocations']
|
||||
consumer = consumers[consumer_uuid]
|
||||
if allocations:
|
||||
rp_objs = _resource_providers_by_uuid(context, allocations.keys())
|
||||
for resource_provider_uuid in allocations:
|
||||
resource_provider = rp_objs[resource_provider_uuid]
|
||||
resources = allocations[resource_provider_uuid]['resources']
|
||||
new_allocations = _new_allocations(context,
|
||||
resource_provider,
|
||||
consumer,
|
||||
resources)
|
||||
allocation_objects.extend(new_allocations)
|
||||
else:
|
||||
# The allocations are empty, which means wipe them out.
|
||||
# Internal to the allocation object this is signalled by a
|
||||
# used value of 0.
|
||||
allocations = rp_obj.AllocationList.get_all_by_consumer_id(
|
||||
context, consumer_uuid)
|
||||
for allocation in allocations:
|
||||
allocation.used = 0
|
||||
allocation_objects.append(allocation)
|
||||
|
||||
return rp_obj.AllocationList(context, objects=allocation_objects)
|
||||
|
||||
|
||||
def inspect_consumers(context, data, want_version):
|
||||
"""Look at consumer data in allocations and create consumers as needed.
|
||||
|
||||
Keep a record of the consumers that are created in case they need
|
||||
to be removed later.
|
||||
|
||||
If an exception is raised by ensure_consumer, commonly HTTPConflict but
|
||||
also anything else, the newly created consumers will be deleted and the
|
||||
exception reraised to the caller.
|
||||
|
||||
:param context: The placement context.
|
||||
:param data: A dictionary of multiple allocations by consumer uuid.
|
||||
:param want_version: the microversion matcher.
|
||||
:return: A tuple of a dict of all consumer objects (by consumer uuid)
|
||||
and a list of those consumer objects which are new.
|
||||
"""
|
||||
# First, ensure that all consumers referenced in the payload actually
|
||||
# exist. And if not, create them. Keep a record of auto-created consumers
|
||||
# so we can clean them up if the end allocation replace_all() fails.
|
||||
consumers = {} # dict of Consumer objects, keyed by consumer UUID
|
||||
new_consumers_created = []
|
||||
for consumer_uuid in data:
|
||||
project_id = data[consumer_uuid]['project_id']
|
||||
user_id = data[consumer_uuid]['user_id']
|
||||
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
||||
try:
|
||||
consumer, new_consumer_created = util.ensure_consumer(
|
||||
context, consumer_uuid, project_id, user_id,
|
||||
consumer_generation, want_version)
|
||||
if new_consumer_created:
|
||||
new_consumers_created.append(consumer)
|
||||
consumers[consumer_uuid] = consumer
|
||||
except Exception:
|
||||
# If any errors (for instance, a consumer generation conflict)
|
||||
# occur when ensuring consumer records above, make sure we delete
|
||||
# any auto-created consumers.
|
||||
with excutils.save_and_reraise_exception():
|
||||
delete_consumers(new_consumers_created)
|
||||
return consumers, new_consumers_created
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@util.check_accept('application/json')
|
||||
def list_for_consumer(req):
|
||||
"""List allocations associated with a consumer."""
|
||||
context = req.environ['placement.context']
|
||||
context.can(policies.ALLOC_LIST)
|
||||
consumer_id = util.wsgi_path_item(req.environ, 'consumer_uuid')
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
|
||||
# NOTE(cdent): There is no way for a 404 to be returned here,
|
||||
# only an empty result. We do not have a way to validate a
|
||||
# consumer id.
|
||||
allocations = rp_obj.AllocationList.get_all_by_consumer_id(
|
||||
context, consumer_id)
|
||||
|
||||
output = _serialize_allocations_for_consumer(allocations, want_version)
|
||||
last_modified = _last_modified_from_allocations(allocations, want_version)
|
||||
allocations_json = jsonutils.dumps(output)
|
||||
|
||||
response = req.response
|
||||
response.status = 200
|
||||
response.body = encodeutils.to_utf8(allocations_json)
|
||||
response.content_type = 'application/json'
|
||||
if want_version.matches((1, 15)):
|
||||
response.last_modified = last_modified
|
||||
response.cache_control = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@util.check_accept('application/json')
|
||||
def list_for_resource_provider(req):
|
||||
"""List allocations associated with a resource provider."""
|
||||
# TODO(cdent): On a shared resource provider (for example a
|
||||
# giant disk farm) this list could get very long. At the moment
|
||||
# we have no facility for limiting the output. Given that we are
|
||||
# using a dict of dicts for the output we are potentially limiting
|
||||
# ourselves in terms of sorting and filtering.
|
||||
context = req.environ['placement.context']
|
||||
context.can(policies.RP_ALLOC_LIST)
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
|
||||
# confirm existence of resource provider so we get a reasonable
|
||||
# 404 instead of empty list
|
||||
try:
|
||||
rp = rp_obj.ResourceProvider.get_by_uuid(context, uuid)
|
||||
except exception.NotFound as exc:
|
||||
raise webob.exc.HTTPNotFound(
|
||||
_("Resource provider '%(rp_uuid)s' not found: %(error)s") %
|
||||
{'rp_uuid': uuid, 'error': exc})
|
||||
|
||||
allocs = rp_obj.AllocationList.get_all_by_resource_provider(context, rp)
|
||||
|
||||
output = _serialize_allocations_for_resource_provider(
|
||||
allocs, rp, want_version)
|
||||
last_modified = _last_modified_from_allocations(allocs, want_version)
|
||||
allocations_json = jsonutils.dumps(output)
|
||||
|
||||
response = req.response
|
||||
response.status = 200
|
||||
response.body = encodeutils.to_utf8(allocations_json)
|
||||
response.content_type = 'application/json'
|
||||
if want_version.matches((1, 15)):
|
||||
response.last_modified = last_modified
|
||||
response.cache_control = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
def _resource_providers_by_uuid(ctx, rp_uuids):
|
||||
"""Helper method that returns a dict, keyed by resource provider UUID, of
|
||||
ResourceProvider objects.
|
||||
|
||||
:param ctx: The placement context.
|
||||
:param rp_uuids: iterable of UUIDs for providers to fetch.
|
||||
:raises: `webob.exc.HTTPBadRequest` if any of the UUIDs do not refer to
|
||||
an existing resource provider.
|
||||
"""
|
||||
res = {}
|
||||
for rp_uuid in rp_uuids:
|
||||
# TODO(jaypipes): Clearly, this is not efficient to do one query for
|
||||
# each resource provider UUID in the allocations instead of doing a
|
||||
# single query for all the UUIDs. However, since
|
||||
# ResourceProviderList.get_all_by_filters() is way too complicated for
|
||||
# this purpose and doesn't raise NotFound anyway, we'll do this.
|
||||
# Perhaps consider adding a ResourceProviderList.get_all_by_uuids()
|
||||
# later on?
|
||||
try:
|
||||
res[rp_uuid] = rp_obj.ResourceProvider.get_by_uuid(ctx, rp_uuid)
|
||||
except exception.NotFound:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_("Allocation for resource provider '%(rp_uuid)s' "
|
||||
"that does not exist.") %
|
||||
{'rp_uuid': rp_uuid})
|
||||
return res
|
||||
|
||||
|
||||