Move ensure_consumer to a new placement.handlers.util
The driving reason for this change is to allow the placement.db_api module to be able, in subsequent patches, to import generic utility routines from placement.util without causing an cyclical import. It is moved into the handlers package because the only caller is in there too (in allocation.py). Relevant unit test are moved, and the callers of ensure_consumer are updated to reflect the new location. Change-Id: Ifd1b10e5568b9d6f641e396c3c49101901daff31
This commit is contained in:
parent
83e2ce65a0
commit
5d8aa50074
|
@ -24,6 +24,7 @@ import webob
|
|||
|
||||
from placement import errors
|
||||
from placement import exception
|
||||
from placement.handlers import util as data_util
|
||||
from placement.i18n import _
|
||||
from placement import microversion
|
||||
from placement.objects import resource_provider as rp_obj
|
||||
|
@ -222,7 +223,7 @@ def inspect_consumers(context, data, want_version):
|
|||
user_id = data[consumer_uuid]['user_id']
|
||||
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
||||
try:
|
||||
consumer, new_consumer_created = util.ensure_consumer(
|
||||
consumer, new_consumer_created = data_util.ensure_consumer(
|
||||
context, consumer_uuid, project_id, user_id,
|
||||
consumer_generation, want_version)
|
||||
if new_consumer_created:
|
||||
|
@ -408,9 +409,9 @@ def _set_allocations_for_consumer(req, schema):
|
|||
# NOTE(jaypipes): This will only occur 1.28+. The JSONSchema will
|
||||
# prevent an empty allocations object from being passed when there is
|
||||
# no consumer generation, so this is safe to do.
|
||||
util.ensure_consumer(context, consumer_uuid, data.get('project_id'),
|
||||
data.get('user_id'), data.get('consumer_generation'),
|
||||
want_version)
|
||||
data_util.ensure_consumer(context, consumer_uuid,
|
||||
data.get('project_id'), data.get('user_id'),
|
||||
data.get('consumer_generation'), want_version)
|
||||
allocations = rp_obj.AllocationList.get_all_by_consumer_id(
|
||||
context, consumer_uuid)
|
||||
for allocation in allocations:
|
||||
|
@ -420,7 +421,7 @@ def _set_allocations_for_consumer(req, schema):
|
|||
# If the body includes an allocation for a resource provider
|
||||
# that does not exist, raise a 400.
|
||||
rp_objs = _resource_providers_by_uuid(context, allocation_data.keys())
|
||||
consumer, created_new_consumer = util.ensure_consumer(
|
||||
consumer, created_new_consumer = data_util.ensure_consumer(
|
||||
context, consumer_uuid, data.get('project_id'),
|
||||
data.get('user_id'), data.get('consumer_generation'),
|
||||
want_version)
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
# 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.
|
||||
"""DB Utility methods for placement."""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import webob
|
||||
|
||||
from placement import errors
|
||||
from placement import exception
|
||||
from placement.i18n import _
|
||||
from placement.objects import consumer as consumer_obj
|
||||
from placement.objects import project as project_obj
|
||||
from placement.objects import user as user_obj
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||
consumer_generation, want_version):
|
||||
"""Ensures there are records in the consumers, projects and users table for
|
||||
the supplied external identifiers.
|
||||
|
||||
Returns a tuple containing the populated Consumer object containing Project
|
||||
and User sub-objects and a boolean indicating whether a new Consumer object
|
||||
was created (as opposed to an existing consumer record retrieved)
|
||||
|
||||
:note: If the supplied project or user external identifiers do not match an
|
||||
existing consumer's project and user identifiers, the existing
|
||||
consumer's project and user IDs are updated to reflect the supplied
|
||||
ones.
|
||||
|
||||
:param ctx: The request context.
|
||||
:param consumer_uuid: The uuid of the consumer of the resources.
|
||||
:param project_id: The external ID of the project consuming the resources.
|
||||
:param user_id: The external ID of the user consuming the resources.
|
||||
:param consumer_generation: The generation provided by the user for this
|
||||
consumer.
|
||||
:param want_version: the microversion matcher.
|
||||
:raises webob.exc.HTTPConflict if consumer generation is required and there
|
||||
was a mismatch
|
||||
"""
|
||||
created_new_consumer = False
|
||||
requires_consumer_generation = want_version.matches((1, 28))
|
||||
if project_id is None:
|
||||
project_id = CONF.placement.incomplete_consumer_project_id
|
||||
user_id = CONF.placement.incomplete_consumer_user_id
|
||||
try:
|
||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||
except exception.NotFound:
|
||||
# Auto-create the project if we found no record of it...
|
||||
try:
|
||||
proj = project_obj.Project(ctx, external_id=project_id)
|
||||
proj.create()
|
||||
except exception.ProjectExists:
|
||||
# No worries, another thread created this project already
|
||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||
try:
|
||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||
except exception.NotFound:
|
||||
# Auto-create the user if we found no record of it...
|
||||
try:
|
||||
user = user_obj.User(ctx, external_id=user_id)
|
||||
user.create()
|
||||
except exception.UserExists:
|
||||
# No worries, another thread created this user already
|
||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||
|
||||
try:
|
||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||
if requires_consumer_generation:
|
||||
if consumer.generation != consumer_generation:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('consumer generation conflict - '
|
||||
'expected %(expected_gen)s but got %(got_gen)s') %
|
||||
{
|
||||
'expected_gen': consumer.generation,
|
||||
'got_gen': consumer_generation,
|
||||
},
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
# NOTE(jaypipes): The user may have specified a different project and
|
||||
# user external ID than the one that we had for the consumer. If this
|
||||
# is the case, go ahead and modify the consumer record with the
|
||||
# newly-supplied project/user information, but do not bump the consumer
|
||||
# generation (since it will be bumped in the
|
||||
# AllocationList.replace_all() method).
|
||||
#
|
||||
# TODO(jaypipes): This means that there may be a partial update.
|
||||
# Imagine a scenario where a user calls POST /allocations, and the
|
||||
# payload references two consumers. The first consumer is a new
|
||||
# consumer and is auto-created. The second consumer is an existing
|
||||
# consumer, but contains a different project or user ID than the
|
||||
# existing consumer's record. If the eventual call to
|
||||
# AllocationList.replace_all() fails for whatever reason (say, a
|
||||
# resource provider generation conflict or out of resources failure),
|
||||
# we will end up deleting the auto-created consumer but we MAY not undo
|
||||
# the changes to the second consumer's project and user ID. I say MAY
|
||||
# and not WILL NOT because I'm not sure that the exception that gets
|
||||
# raised from AllocationList.replace_all() will cause the context
|
||||
# manager's transaction to rollback automatically. I believe that the
|
||||
# same transaction context is used for both util.ensure_consumer() and
|
||||
# AllocationList.replace_all() within the same HTTP request, but need
|
||||
# to test this to be 100% certain...
|
||||
if (project_id != consumer.project.external_id or
|
||||
user_id != consumer.user.external_id):
|
||||
LOG.debug("Supplied project or user ID for consumer %s was "
|
||||
"different than existing record. Updating consumer "
|
||||
"record.", consumer_uuid)
|
||||
consumer.project = proj
|
||||
consumer.user = user
|
||||
consumer.update()
|
||||
except exception.NotFound:
|
||||
# If we are attempting to modify or create allocations after 1.26, we
|
||||
# need a consumer generation specified. The user must have specified
|
||||
# None for the consumer generation if we get here, since there was no
|
||||
# existing consumer with this UUID and therefore the user should be
|
||||
# indicating that they expect the consumer did not exist.
|
||||
if requires_consumer_generation:
|
||||
if consumer_generation is not None:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('consumer generation conflict - '
|
||||
'expected null but got %s') % consumer_generation,
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
# No such consumer. This is common for new allocations. Create the
|
||||
# consumer record
|
||||
try:
|
||||
consumer = consumer_obj.Consumer(
|
||||
ctx, uuid=consumer_uuid, project=proj, user=user)
|
||||
consumer.create()
|
||||
created_new_consumer = True
|
||||
except exception.ConsumerExists:
|
||||
# No worries, another thread created this user already
|
||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||
return consumer, created_new_consumer
|
|
@ -0,0 +1,217 @@
|
|||
#
|
||||
# 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.
|
||||
"""Unit tests for the utility functions used by the placement DB."""
|
||||
|
||||
import fixtures
|
||||
import microversion_parse
|
||||
import mock
|
||||
from oslo_utils.fixture import uuidsentinel
|
||||
import testtools
|
||||
import webob
|
||||
|
||||
from placement import conf
|
||||
from placement import exception
|
||||
from placement.handlers import util
|
||||
from placement import microversion
|
||||
from placement.objects import consumer as consumer_obj
|
||||
from placement.objects import project as project_obj
|
||||
from placement.objects import user as user_obj
|
||||
|
||||
|
||||
CONF = conf.CONF
|
||||
|
||||
|
||||
class TestEnsureConsumer(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestEnsureConsumer, self).setUp()
|
||||
self.mock_project_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.project.'
|
||||
'Project.get_by_external_id')).mock
|
||||
self.mock_user_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.user.'
|
||||
'User.get_by_external_id')).mock
|
||||
self.mock_consumer_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.consumer.'
|
||||
'Consumer.get_by_uuid')).mock
|
||||
self.mock_project_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.project.'
|
||||
'Project.create')).mock
|
||||
self.mock_user_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.user.'
|
||||
'User.create')).mock
|
||||
self.mock_consumer_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.consumer.'
|
||||
'Consumer.create')).mock
|
||||
self.ctx = mock.sentinel.ctx
|
||||
self.consumer_id = uuidsentinel.consumer
|
||||
self.project_id = uuidsentinel.project
|
||||
self.user_id = uuidsentinel.user
|
||||
mv_parsed = microversion_parse.Version(1, 27)
|
||||
mv_parsed.max_version = microversion_parse.parse_version_string(
|
||||
microversion.max_version_string())
|
||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||
microversion.min_version_string())
|
||||
self.before_version = mv_parsed
|
||||
mv_parsed = microversion_parse.Version(1, 28)
|
||||
mv_parsed.max_version = microversion_parse.parse_version_string(
|
||||
microversion.max_version_string())
|
||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||
microversion.min_version_string())
|
||||
self.after_version = mv_parsed
|
||||
|
||||
def test_no_existing_project_user_consumer_before_gen_success(self):
|
||||
"""Tests that we don't require a consumer_generation=None before the
|
||||
appropriate microversion.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = 1 # should be ignored
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, self.project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, self.user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_no_existing_project_user_consumer_after_gen_success(self):
|
||||
"""Tests that we require a consumer_generation=None after the
|
||||
appropriate microversion.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, self.project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, self.user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_no_existing_project_user_consumer_after_gen_fail(self):
|
||||
"""Tests that we require a consumer_generation=None after the
|
||||
appropriate microversion and that None is the expected value.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = 1 # should NOT be ignored (and 1 is not expected)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
util.ensure_consumer,
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
def test_no_existing_project_user_consumer_use_incomplete(self):
|
||||
"""Verify that if the project_id arg is None, that we fall back to the
|
||||
CONF options for incomplete project and user ID.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, None, None,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, CONF.placement.incomplete_consumer_project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, CONF.placement.incomplete_consumer_user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_existing_project_no_existing_consumer_before_gen_success(self):
|
||||
"""Check that if we find an existing project and user, that we use
|
||||
those found objects in creating the consumer. Do not require a consumer
|
||||
generation before the appropriate microversion.
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should be ignored
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_create.assert_not_called()
|
||||
self.mock_user_create.assert_not_called()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_existing_consumer_after_gen_matches_supplied_gen(self):
|
||||
"""Tests that we require a consumer_generation after the
|
||||
appropriate microversion and that when the consumer already exists,
|
||||
then we ensure a matching generation is supplied
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
consumer = consumer_obj.Consumer(
|
||||
self.ctx, id=1, project=proj, user=user, generation=2)
|
||||
self.mock_consumer_get.return_value = consumer
|
||||
|
||||
consumer_gen = 2 # should NOT be ignored (and 2 is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
self.mock_project_create.assert_not_called()
|
||||
self.mock_user_create.assert_not_called()
|
||||
self.mock_consumer_create.assert_not_called()
|
||||
|
||||
def test_existing_consumer_after_gen_fail(self):
|
||||
"""Tests that we require a consumer_generation after the
|
||||
appropriate microversion and that when the consumer already exists,
|
||||
then we raise a 400 when there is a mismatch on the existing
|
||||
generation.
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
consumer = consumer_obj.Consumer(
|
||||
self.ctx, id=1, project=proj, user=user, generation=42)
|
||||
self.mock_consumer_get.return_value = consumer
|
||||
|
||||
consumer_gen = 2 # should NOT be ignored (and 2 is NOT expected)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
util.ensure_consumer,
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
|
@ -27,13 +27,9 @@ import webob
|
|||
|
||||
import six
|
||||
|
||||
from placement import exception
|
||||
from placement import lib as pl
|
||||
from placement import microversion
|
||||
from placement.objects import consumer as consumer_obj
|
||||
from placement.objects import project as project_obj
|
||||
from placement.objects import resource_provider as rp_obj
|
||||
from placement.objects import user as user_obj
|
||||
from placement import util
|
||||
|
||||
|
||||
|
@ -902,189 +898,3 @@ class TestPickLastModified(testtools.TestCase):
|
|||
None, self.resource_provider)
|
||||
self.assertEqual(now, chosen_time)
|
||||
mock_utc.assert_called_once_with(with_timezone=True)
|
||||
|
||||
|
||||
class TestEnsureConsumer(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestEnsureConsumer, self).setUp()
|
||||
self.mock_project_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.project.'
|
||||
'Project.get_by_external_id')).mock
|
||||
self.mock_user_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.user.'
|
||||
'User.get_by_external_id')).mock
|
||||
self.mock_consumer_get = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.consumer.'
|
||||
'Consumer.get_by_uuid')).mock
|
||||
self.mock_project_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.project.'
|
||||
'Project.create')).mock
|
||||
self.mock_user_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.user.'
|
||||
'User.create')).mock
|
||||
self.mock_consumer_create = self.useFixture(fixtures.MockPatch(
|
||||
'placement.objects.consumer.'
|
||||
'Consumer.create')).mock
|
||||
self.ctx = mock.sentinel.ctx
|
||||
self.consumer_id = uuidsentinel.consumer
|
||||
self.project_id = uuidsentinel.project
|
||||
self.user_id = uuidsentinel.user
|
||||
mv_parsed = microversion_parse.Version(1, 27)
|
||||
mv_parsed.max_version = microversion_parse.parse_version_string(
|
||||
microversion.max_version_string())
|
||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||
microversion.min_version_string())
|
||||
self.before_version = mv_parsed
|
||||
mv_parsed = microversion_parse.Version(1, 28)
|
||||
mv_parsed.max_version = microversion_parse.parse_version_string(
|
||||
microversion.max_version_string())
|
||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||
microversion.min_version_string())
|
||||
self.after_version = mv_parsed
|
||||
|
||||
def test_no_existing_project_user_consumer_before_gen_success(self):
|
||||
"""Tests that we don't require a consumer_generation=None before the
|
||||
appropriate microversion.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = 1 # should be ignored
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, self.project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, self.user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_no_existing_project_user_consumer_after_gen_success(self):
|
||||
"""Tests that we require a consumer_generation=None after the
|
||||
appropriate microversion.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, self.project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, self.user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_no_existing_project_user_consumer_after_gen_fail(self):
|
||||
"""Tests that we require a consumer_generation=None after the
|
||||
appropriate microversion and that None is the expected value.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = 1 # should NOT be ignored (and 1 is not expected)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
util.ensure_consumer,
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
def test_no_existing_project_user_consumer_use_incomplete(self):
|
||||
"""Verify that if the project_id arg is None, that we fall back to the
|
||||
CONF options for incomplete project and user ID.
|
||||
"""
|
||||
self.mock_project_get.side_effect = exception.NotFound
|
||||
self.mock_user_get.side_effect = exception.NotFound
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, None, None,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_get.assert_called_once_with(
|
||||
self.ctx, CONF.placement.incomplete_consumer_project_id)
|
||||
self.mock_user_get.assert_called_once_with(
|
||||
self.ctx, CONF.placement.incomplete_consumer_user_id)
|
||||
self.mock_consumer_get.assert_called_once_with(
|
||||
self.ctx, self.consumer_id)
|
||||
self.mock_project_create.assert_called_once()
|
||||
self.mock_user_create.assert_called_once()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_existing_project_no_existing_consumer_before_gen_success(self):
|
||||
"""Check that if we find an existing project and user, that we use
|
||||
those found objects in creating the consumer. Do not require a consumer
|
||||
generation before the appropriate microversion.
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
self.mock_consumer_get.side_effect = exception.NotFound
|
||||
|
||||
consumer_gen = None # should be ignored
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.before_version)
|
||||
|
||||
self.mock_project_create.assert_not_called()
|
||||
self.mock_user_create.assert_not_called()
|
||||
self.mock_consumer_create.assert_called_once()
|
||||
|
||||
def test_existing_consumer_after_gen_matches_supplied_gen(self):
|
||||
"""Tests that we require a consumer_generation after the
|
||||
appropriate microversion and that when the consumer already exists,
|
||||
then we ensure a matching generation is supplied
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
consumer = consumer_obj.Consumer(
|
||||
self.ctx, id=1, project=proj, user=user, generation=2)
|
||||
self.mock_consumer_get.return_value = consumer
|
||||
|
||||
consumer_gen = 2 # should NOT be ignored (and 2 is expected)
|
||||
util.ensure_consumer(
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
||||
self.mock_project_create.assert_not_called()
|
||||
self.mock_user_create.assert_not_called()
|
||||
self.mock_consumer_create.assert_not_called()
|
||||
|
||||
def test_existing_consumer_after_gen_fail(self):
|
||||
"""Tests that we require a consumer_generation after the
|
||||
appropriate microversion and that when the consumer already exists,
|
||||
then we raise a 400 when there is a mismatch on the existing
|
||||
generation.
|
||||
"""
|
||||
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||
self.mock_project_get.return_value = proj
|
||||
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||
self.mock_user_get.return_value = user
|
||||
consumer = consumer_obj.Consumer(
|
||||
self.ctx, id=1, project=proj, user=user, generation=42)
|
||||
self.mock_consumer_get.return_value = consumer
|
||||
|
||||
consumer_gen = 2 # should NOT be ignored (and 2 is NOT expected)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
util.ensure_consumer,
|
||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||
consumer_gen, self.after_version)
|
||||
|
|
|
@ -26,14 +26,10 @@ from oslo_utils import uuidutils
|
|||
import webob
|
||||
|
||||
from placement import errors
|
||||
from placement import exception
|
||||
from placement.i18n import _
|
||||
# NOTE(cdent): avoid cyclical import conflict between util and
|
||||
# microversion
|
||||
import placement.microversion
|
||||
from placement.objects import consumer as consumer_obj
|
||||
from placement.objects import project as project_obj
|
||||
from placement.objects import user as user_obj
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -402,124 +398,6 @@ def normalize_member_of_qs_param(value):
|
|||
return value
|
||||
|
||||
|
||||
def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||
consumer_generation, want_version):
|
||||
"""Ensures there are records in the consumers, projects and users table for
|
||||
the supplied external identifiers.
|
||||
|
||||
Returns a tuple containing the populated Consumer object containing Project
|
||||
and User sub-objects and a boolean indicating whether a new Consumer object
|
||||
was created (as opposed to an existing consumer record retrieved)
|
||||
|
||||
:note: If the supplied project or user external identifiers do not match an
|
||||
existing consumer's project and user identifiers, the existing
|
||||
consumer's project and user IDs are updated to reflect the supplied
|
||||
ones.
|
||||
|
||||
:param ctx: The request context.
|
||||
:param consumer_uuid: The uuid of the consumer of the resources.
|
||||
:param project_id: The external ID of the project consuming the resources.
|
||||
:param user_id: The external ID of the user consuming the resources.
|
||||
:param consumer_generation: The generation provided by the user for this
|
||||
consumer.
|
||||
:param want_version: the microversion matcher.
|
||||
:raises webob.exc.HTTPConflict if consumer generation is required and there
|
||||
was a mismatch
|
||||
"""
|
||||
created_new_consumer = False
|
||||
requires_consumer_generation = want_version.matches((1, 28))
|
||||
if project_id is None:
|
||||
project_id = CONF.placement.incomplete_consumer_project_id
|
||||
user_id = CONF.placement.incomplete_consumer_user_id
|
||||
try:
|
||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||
except exception.NotFound:
|
||||
# Auto-create the project if we found no record of it...
|
||||
try:
|
||||
proj = project_obj.Project(ctx, external_id=project_id)
|
||||
proj.create()
|
||||
except exception.ProjectExists:
|
||||
# No worries, another thread created this project already
|
||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||
try:
|
||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||
except exception.NotFound:
|
||||
# Auto-create the user if we found no record of it...
|
||||
try:
|
||||
user = user_obj.User(ctx, external_id=user_id)
|
||||
user.create()
|
||||
except exception.UserExists:
|
||||
# No worries, another thread created this user already
|
||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||
|
||||
try:
|
||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||
if requires_consumer_generation:
|
||||
if consumer.generation != consumer_generation:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('consumer generation conflict - '
|
||||
'expected %(expected_gen)s but got %(got_gen)s') %
|
||||
{
|
||||
'expected_gen': consumer.generation,
|
||||
'got_gen': consumer_generation,
|
||||
},
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
# NOTE(jaypipes): The user may have specified a different project and
|
||||
# user external ID than the one that we had for the consumer. If this
|
||||
# is the case, go ahead and modify the consumer record with the
|
||||
# newly-supplied project/user information, but do not bump the consumer
|
||||
# generation (since it will be bumped in the
|
||||
# AllocationList.replace_all() method).
|
||||
#
|
||||
# TODO(jaypipes): This means that there may be a partial update.
|
||||
# Imagine a scenario where a user calls POST /allocations, and the
|
||||
# payload references two consumers. The first consumer is a new
|
||||
# consumer and is auto-created. The second consumer is an existing
|
||||
# consumer, but contains a different project or user ID than the
|
||||
# existing consumer's record. If the eventual call to
|
||||
# AllocationList.replace_all() fails for whatever reason (say, a
|
||||
# resource provider generation conflict or out of resources failure),
|
||||
# we will end up deleting the auto-created consumer but we MAY not undo
|
||||
# the changes to the second consumer's project and user ID. I say MAY
|
||||
# and not WILL NOT because I'm not sure that the exception that gets
|
||||
# raised from AllocationList.replace_all() will cause the context
|
||||
# manager's transaction to rollback automatically. I believe that the
|
||||
# same transaction context is used for both util.ensure_consumer() and
|
||||
# AllocationList.replace_all() within the same HTTP request, but need
|
||||
# to test this to be 100% certain...
|
||||
if (project_id != consumer.project.external_id or
|
||||
user_id != consumer.user.external_id):
|
||||
LOG.debug("Supplied project or user ID for consumer %s was "
|
||||
"different than existing record. Updating consumer "
|
||||
"record.", consumer_uuid)
|
||||
consumer.project = proj
|
||||
consumer.user = user
|
||||
consumer.update()
|
||||
except exception.NotFound:
|
||||
# If we are attempting to modify or create allocations after 1.26, we
|
||||
# need a consumer generation specified. The user must have specified
|
||||
# None for the consumer generation if we get here, since there was no
|
||||
# existing consumer with this UUID and therefore the user should be
|
||||
# indicating that they expect the consumer did not exist.
|
||||
if requires_consumer_generation:
|
||||
if consumer_generation is not None:
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('consumer generation conflict - '
|
||||
'expected null but got %s') % consumer_generation,
|
||||
comment=errors.CONCURRENT_UPDATE)
|
||||
# No such consumer. This is common for new allocations. Create the
|
||||
# consumer record
|
||||
try:
|
||||
consumer = consumer_obj.Consumer(
|
||||
ctx, uuid=consumer_uuid, project=proj, user=user)
|
||||
consumer.create()
|
||||
created_new_consumer = True
|
||||
except exception.ConsumerExists:
|
||||
# No worries, another thread created this user already
|
||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||
return consumer, created_new_consumer
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tempdir(**kwargs):
|
||||
argdict = kwargs.copy()
|
||||
|
|
Loading…
Reference in New Issue