505 lines
20 KiB
Python
505 lines
20 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import mock
|
|
import os_resource_classes as orc
|
|
from oslo_utils.fixture import uuidsentinel
|
|
|
|
from placement import exception
|
|
from placement.objects import allocation as alloc_obj
|
|
from placement.objects import consumer as consumer_obj
|
|
from placement.objects import inventory as inv_obj
|
|
from placement.objects import usage as usage_obj
|
|
from placement.tests.functional.db import test_base as tb
|
|
|
|
|
|
class TestAllocationListCreateDelete(tb.PlacementDbBaseTestCase):
|
|
|
|
def test_allocation_checking(self):
|
|
"""Test that allocation check logic works with 2 resource classes on
|
|
one provider.
|
|
|
|
If this fails, we get a KeyError at replace_all()
|
|
"""
|
|
|
|
max_unit = 10
|
|
consumer_uuid = uuidsentinel.consumer
|
|
consumer_uuid2 = uuidsentinel.consumer2
|
|
|
|
# Create a consumer representing the two instances
|
|
consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=consumer_uuid, user=self.user_obj,
|
|
project=self.project_obj)
|
|
consumer.create()
|
|
consumer2 = consumer_obj.Consumer(
|
|
self.ctx, uuid=consumer_uuid2, user=self.user_obj,
|
|
project=self.project_obj)
|
|
consumer2.create()
|
|
|
|
# Create one resource provider with 2 classes
|
|
rp1_name = uuidsentinel.rp1_name
|
|
rp1_uuid = uuidsentinel.rp1_uuid
|
|
rp1_class = orc.DISK_GB
|
|
rp1_used = 6
|
|
|
|
rp2_class = orc.IPV4_ADDRESS
|
|
rp2_used = 2
|
|
|
|
rp1 = self._create_provider(rp1_name, uuid=rp1_uuid)
|
|
tb.add_inventory(rp1, rp1_class, 1024, max_unit=max_unit)
|
|
tb.add_inventory(rp1, rp2_class, 255, reserved=2, max_unit=max_unit)
|
|
|
|
# create the allocations for a first consumer
|
|
allocation_1 = alloc_obj.Allocation(
|
|
resource_provider=rp1, consumer=consumer, resource_class=rp1_class,
|
|
used=rp1_used)
|
|
allocation_2 = alloc_obj.Allocation(
|
|
resource_provider=rp1, consumer=consumer, resource_class=rp2_class,
|
|
used=rp2_used)
|
|
allocation_list = [allocation_1, allocation_2]
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
# create the allocations for a second consumer, until we have
|
|
# allocations for more than one consumer in the db, then we
|
|
# won't actually be doing real allocation math, which triggers
|
|
# the sql monster.
|
|
allocation_1 = alloc_obj.Allocation(
|
|
resource_provider=rp1, consumer=consumer2,
|
|
resource_class=rp1_class, used=rp1_used)
|
|
allocation_2 = alloc_obj.Allocation(
|
|
resource_provider=rp1, consumer=consumer2,
|
|
resource_class=rp2_class, used=rp2_used)
|
|
allocation_list = [allocation_1, allocation_2]
|
|
# If we are joining wrong, this will be a KeyError
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
def test_allocation_list_create(self):
|
|
max_unit = 10
|
|
consumer_uuid = uuidsentinel.consumer
|
|
|
|
# Create a consumer representing the instance
|
|
inst_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=consumer_uuid, user=self.user_obj,
|
|
project=self.project_obj)
|
|
inst_consumer.create()
|
|
|
|
# Create two resource providers
|
|
rp1_name = uuidsentinel.rp1_name
|
|
rp1_uuid = uuidsentinel.rp1_uuid
|
|
rp1_class = orc.DISK_GB
|
|
rp1_used = 6
|
|
|
|
rp2_name = uuidsentinel.rp2_name
|
|
rp2_uuid = uuidsentinel.rp2_uuid
|
|
rp2_class = orc.IPV4_ADDRESS
|
|
rp2_used = 2
|
|
|
|
rp1 = self._create_provider(rp1_name, uuid=rp1_uuid)
|
|
rp2 = self._create_provider(rp2_name, uuid=rp2_uuid)
|
|
|
|
# Two allocations, one for each resource provider.
|
|
allocation_1 = alloc_obj.Allocation(
|
|
resource_provider=rp1, consumer=inst_consumer,
|
|
resource_class=rp1_class, used=rp1_used)
|
|
allocation_2 = alloc_obj.Allocation(
|
|
resource_provider=rp2, consumer=inst_consumer,
|
|
resource_class=rp2_class, used=rp2_used)
|
|
allocation_list = [allocation_1, allocation_2]
|
|
|
|
# There's no inventory, we have a failure.
|
|
error = self.assertRaises(exception.InvalidInventory,
|
|
alloc_obj.replace_all, self.ctx,
|
|
allocation_list)
|
|
# Confirm that the resource class string, not index, is in
|
|
# the exception and resource providers are listed by uuid.
|
|
self.assertIn(rp1_class, str(error))
|
|
self.assertIn(rp2_class, str(error))
|
|
self.assertIn(rp1.uuid, str(error))
|
|
self.assertIn(rp2.uuid, str(error))
|
|
|
|
# Add inventory for one of the two resource providers. This should also
|
|
# fail, since rp2 has no inventory.
|
|
tb.add_inventory(rp1, rp1_class, 1024, max_unit=1)
|
|
self.assertRaises(exception.InvalidInventory,
|
|
alloc_obj.replace_all, self.ctx, allocation_list)
|
|
|
|
# Add inventory for the second resource provider
|
|
tb.add_inventory(rp2, rp2_class, 255, reserved=2, max_unit=1)
|
|
|
|
# Now the allocations will still fail because max_unit 1
|
|
self.assertRaises(exception.InvalidAllocationConstraintsViolated,
|
|
alloc_obj.replace_all, self.ctx, allocation_list)
|
|
inv1 = inv_obj.Inventory(resource_provider=rp1,
|
|
resource_class=rp1_class,
|
|
total=1024, max_unit=max_unit)
|
|
rp1.set_inventory([inv1])
|
|
inv2 = inv_obj.Inventory(resource_provider=rp2,
|
|
resource_class=rp2_class,
|
|
total=255, reserved=2, max_unit=max_unit)
|
|
rp2.set_inventory([inv2])
|
|
|
|
# Now we can finally allocate.
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
# Check that those allocations changed usage on each
|
|
# resource provider.
|
|
rp1_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp1_uuid)
|
|
rp2_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp2_uuid)
|
|
self.assertEqual(rp1_used, rp1_usage[0].usage)
|
|
self.assertEqual(rp2_used, rp2_usage[0].usage)
|
|
|
|
# redo one allocation
|
|
# TODO(cdent): This does not currently behave as expected
|
|
# because a new allocataion is created, adding to the total
|
|
# used, not replacing.
|
|
rp1_used += 1
|
|
self.allocate_from_provider(
|
|
rp1, rp1_class, rp1_used, consumer=inst_consumer)
|
|
|
|
rp1_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp1_uuid)
|
|
self.assertEqual(rp1_used, rp1_usage[0].usage)
|
|
|
|
# delete the allocations for the consumer
|
|
# NOTE(cdent): The database uses 'consumer_id' for the
|
|
# column, presumably because some ids might not be uuids, at
|
|
# some point in the future.
|
|
consumer_allocations = alloc_obj.get_all_by_consumer_id(
|
|
self.ctx, consumer_uuid)
|
|
alloc_obj.delete_all(self.ctx, consumer_allocations)
|
|
|
|
rp1_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp1_uuid)
|
|
rp2_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp2_uuid)
|
|
self.assertEqual(0, rp1_usage[0].usage)
|
|
self.assertEqual(0, rp2_usage[0].usage)
|
|
|
|
def _make_rp_and_inventory(self, **kwargs):
|
|
# Create one resource provider and set some inventory
|
|
rp_name = kwargs.get('rp_name') or uuidsentinel.rp_name
|
|
rp_uuid = kwargs.get('rp_uuid') or uuidsentinel.rp_uuid
|
|
rp = self._create_provider(rp_name, uuid=rp_uuid)
|
|
rc = kwargs.pop('resource_class')
|
|
tb.add_inventory(rp, rc, 1024, **kwargs)
|
|
return rp
|
|
|
|
def _validate_usage(self, rp, usage):
|
|
rp_usage = usage_obj.get_all_by_resource_provider_uuid(
|
|
self.ctx, rp.uuid)
|
|
self.assertEqual(usage, rp_usage[0].usage)
|
|
|
|
def _check_create_allocations(self, inventory_kwargs,
|
|
bad_used, good_used):
|
|
rp_class = orc.DISK_GB
|
|
rp = self._make_rp_and_inventory(resource_class=rp_class,
|
|
**inventory_kwargs)
|
|
|
|
# allocation, bad step_size
|
|
self.assertRaises(exception.InvalidAllocationConstraintsViolated,
|
|
self.allocate_from_provider, rp, rp_class, bad_used)
|
|
|
|
# correct for step size
|
|
self.allocate_from_provider(rp, rp_class, good_used)
|
|
|
|
# check usage
|
|
self._validate_usage(rp, good_used)
|
|
|
|
def test_create_all_step_size(self):
|
|
bad_used = 4
|
|
good_used = 5
|
|
inventory_kwargs = {'max_unit': 9999, 'step_size': 5}
|
|
|
|
self._check_create_allocations(inventory_kwargs,
|
|
bad_used, good_used)
|
|
|
|
def test_create_all_min_unit(self):
|
|
bad_used = 4
|
|
good_used = 5
|
|
inventory_kwargs = {'max_unit': 9999, 'min_unit': 5}
|
|
|
|
self._check_create_allocations(inventory_kwargs,
|
|
bad_used, good_used)
|
|
|
|
def test_create_all_max_unit(self):
|
|
bad_used = 5
|
|
good_used = 3
|
|
inventory_kwargs = {'max_unit': 3}
|
|
|
|
self._check_create_allocations(inventory_kwargs,
|
|
bad_used, good_used)
|
|
|
|
def test_create_and_clear(self):
|
|
"""Test that a used of 0 in an allocation wipes allocations."""
|
|
consumer_uuid = uuidsentinel.consumer
|
|
|
|
# Create a consumer representing the instance
|
|
inst_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=consumer_uuid, user=self.user_obj,
|
|
project=self.project_obj)
|
|
inst_consumer.create()
|
|
|
|
rp_class = orc.DISK_GB
|
|
target_rp = self._make_rp_and_inventory(resource_class=rp_class,
|
|
max_unit=500)
|
|
|
|
# Create two allocations with values and confirm the resulting
|
|
# usage is as expected.
|
|
allocation1 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=100)
|
|
allocation2 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=200)
|
|
allocation_list = [allocation1, allocation2]
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
allocations = alloc_obj.get_all_by_consumer_id(self.ctx, consumer_uuid)
|
|
self.assertEqual(2, len(allocations))
|
|
usage = sum(alloc.used for alloc in allocations)
|
|
self.assertEqual(300, usage)
|
|
|
|
# Create two allocations, one with 0 used, to confirm the
|
|
# resulting usage is only of one.
|
|
allocation1 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=0)
|
|
allocation2 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=200)
|
|
allocation_list = [allocation1, allocation2]
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
allocations = alloc_obj.get_all_by_consumer_id(self.ctx, consumer_uuid)
|
|
self.assertEqual(1, len(allocations))
|
|
usage = allocations[0].used
|
|
self.assertEqual(200, usage)
|
|
|
|
# add a source rp and a migration consumer
|
|
migration_uuid = uuidsentinel.migration
|
|
|
|
# Create a consumer representing the migration
|
|
mig_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=migration_uuid, user=self.user_obj,
|
|
project=self.project_obj)
|
|
mig_consumer.create()
|
|
|
|
source_rp = self._make_rp_and_inventory(
|
|
rp_name=uuidsentinel.source_name, rp_uuid=uuidsentinel.source_uuid,
|
|
resource_class=rp_class, max_unit=500)
|
|
|
|
# Create two allocations, one as the consumer, one as the
|
|
# migration.
|
|
allocation1 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=200)
|
|
allocation2 = alloc_obj.Allocation(
|
|
resource_provider=source_rp, consumer=mig_consumer,
|
|
resource_class=rp_class, used=200)
|
|
allocation_list = [allocation1, allocation2]
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
# Check primary consumer allocations.
|
|
allocations = alloc_obj.get_all_by_consumer_id(self.ctx, consumer_uuid)
|
|
self.assertEqual(1, len(allocations))
|
|
usage = allocations[0].used
|
|
self.assertEqual(200, usage)
|
|
|
|
# Check migration allocations.
|
|
allocations = alloc_obj.get_all_by_consumer_id(
|
|
self.ctx, migration_uuid)
|
|
self.assertEqual(1, len(allocations))
|
|
usage = allocations[0].used
|
|
self.assertEqual(200, usage)
|
|
|
|
# Clear the migration and confirm the target.
|
|
allocation1 = alloc_obj.Allocation(
|
|
resource_provider=target_rp, consumer=inst_consumer,
|
|
resource_class=rp_class, used=200)
|
|
allocation2 = alloc_obj.Allocation(
|
|
resource_provider=source_rp, consumer=mig_consumer,
|
|
resource_class=rp_class, used=0)
|
|
allocation_list = [allocation1, allocation2]
|
|
alloc_obj.replace_all(self.ctx, allocation_list)
|
|
|
|
allocations = alloc_obj.get_all_by_consumer_id(self.ctx, consumer_uuid)
|
|
self.assertEqual(1, len(allocations))
|
|
usage = allocations[0].used
|
|
self.assertEqual(200, usage)
|
|
|
|
allocations = alloc_obj.get_all_by_consumer_id(
|
|
self.ctx, migration_uuid)
|
|
self.assertEqual(0, len(allocations))
|
|
|
|
def test_create_exceeding_capacity_allocation(self):
|
|
"""Tests on a list of allocations which contains an invalid allocation
|
|
exceeds resource provider's capacity.
|
|
|
|
Expect InvalidAllocationCapacityExceeded to be raised and all
|
|
allocations in the list should not be applied.
|
|
|
|
"""
|
|
empty_rp = self._create_provider('empty_rp')
|
|
full_rp = self._create_provider('full_rp')
|
|
|
|
for rp in (empty_rp, full_rp):
|
|
tb.add_inventory(rp, orc.VCPU, 24,
|
|
allocation_ratio=16.0)
|
|
tb.add_inventory(rp, orc.MEMORY_MB, 1024,
|
|
min_unit=64,
|
|
max_unit=1024,
|
|
step_size=64)
|
|
|
|
# Create a consumer representing the instance
|
|
inst_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=uuidsentinel.instance, user=self.user_obj,
|
|
project=self.project_obj)
|
|
inst_consumer.create()
|
|
|
|
# First create a allocation to consume full_rp's resource.
|
|
alloc_list = [
|
|
alloc_obj.Allocation(
|
|
consumer=inst_consumer,
|
|
resource_provider=full_rp,
|
|
resource_class=orc.VCPU,
|
|
used=12),
|
|
alloc_obj.Allocation(
|
|
consumer=inst_consumer,
|
|
resource_provider=full_rp,
|
|
resource_class=orc.MEMORY_MB,
|
|
used=1024)
|
|
]
|
|
alloc_obj.replace_all(self.ctx, alloc_list)
|
|
|
|
# Create a consumer representing the second instance
|
|
inst2_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=uuidsentinel.instance2, user=self.user_obj,
|
|
project=self.project_obj)
|
|
inst2_consumer.create()
|
|
|
|
# Create an allocation list consisting of valid requests and an invalid
|
|
# request exceeding the memory full_rp can provide.
|
|
alloc_list = [
|
|
alloc_obj.Allocation(
|
|
consumer=inst2_consumer,
|
|
resource_provider=empty_rp,
|
|
resource_class=orc.VCPU,
|
|
used=12),
|
|
alloc_obj.Allocation(
|
|
consumer=inst2_consumer,
|
|
resource_provider=empty_rp,
|
|
resource_class=orc.MEMORY_MB,
|
|
used=512),
|
|
alloc_obj.Allocation(
|
|
consumer=inst2_consumer,
|
|
resource_provider=full_rp,
|
|
resource_class=orc.VCPU,
|
|
used=12),
|
|
alloc_obj.Allocation(
|
|
consumer=inst2_consumer,
|
|
resource_provider=full_rp,
|
|
resource_class=orc.MEMORY_MB,
|
|
used=512),
|
|
]
|
|
|
|
self.assertRaises(exception.InvalidAllocationCapacityExceeded,
|
|
alloc_obj.replace_all, self.ctx, alloc_list)
|
|
|
|
# Make sure that allocations of both empty_rp and full_rp remain
|
|
# unchanged.
|
|
allocations = alloc_obj.get_all_by_resource_provider(self.ctx, full_rp)
|
|
self.assertEqual(2, len(allocations))
|
|
|
|
allocations = alloc_obj.get_all_by_resource_provider(
|
|
self.ctx, empty_rp)
|
|
self.assertEqual(0, len(allocations))
|
|
|
|
@mock.patch('placement.objects.allocation.LOG')
|
|
def test_set_allocations_retry(self, mock_log):
|
|
"""Test server side allocation write retry handling."""
|
|
|
|
# Create a single resource provider and give it some inventory.
|
|
rp1 = self._create_provider('rp1')
|
|
tb.add_inventory(rp1, orc.VCPU, 24,
|
|
allocation_ratio=16.0)
|
|
tb.add_inventory(rp1, orc.MEMORY_MB, 1024,
|
|
min_unit=64,
|
|
max_unit=1024,
|
|
step_size=64)
|
|
original_generation = rp1.generation
|
|
# Verify the generation is what we expect (we'll be checking again
|
|
# later).
|
|
self.assertEqual(2, original_generation)
|
|
|
|
# Create a consumer and have it make an allocation.
|
|
inst_consumer = consumer_obj.Consumer(
|
|
self.ctx, uuid=uuidsentinel.instance, user=self.user_obj,
|
|
project=self.project_obj)
|
|
inst_consumer.create()
|
|
|
|
alloc_list = [
|
|
alloc_obj.Allocation(
|
|
consumer=inst_consumer,
|
|
resource_provider=rp1,
|
|
resource_class=orc.VCPU,
|
|
used=12),
|
|
alloc_obj.Allocation(
|
|
consumer=inst_consumer,
|
|
resource_provider=rp1,
|
|
resource_class=orc.MEMORY_MB,
|
|
used=1024)
|
|
]
|
|
|
|
# Make sure the right exception happens when the retry loop expires.
|
|
with mock.patch.object(alloc_obj, 'RP_CONFLICT_RETRY_COUNT', 0):
|
|
self.assertRaises(
|
|
exception.ResourceProviderConcurrentUpdateDetected,
|
|
alloc_obj.replace_all, self.ctx, alloc_list)
|
|
mock_log.warning.assert_called_with(
|
|
'Exceeded retry limit of %d on allocations write', 0)
|
|
|
|
# Make sure the right thing happens after a small number of failures.
|
|
# There's a bit of mock magic going on here to enusre that we can
|
|
# both do some side effects on _set_allocations as well as have the
|
|
# real behavior. Two generation conflicts and then a success.
|
|
mock_log.reset_mock()
|
|
with mock.patch.object(alloc_obj, 'RP_CONFLICT_RETRY_COUNT', 3):
|
|
unmocked_set = alloc_obj._set_allocations
|
|
with mock.patch('placement.objects.allocation.'
|
|
'_set_allocations') as mock_set:
|
|
exceptions = iter([
|
|
exception.ResourceProviderConcurrentUpdateDetected(),
|
|
exception.ResourceProviderConcurrentUpdateDetected(),
|
|
])
|
|
|
|
def side_effect(*args, **kwargs):
|
|
try:
|
|
raise next(exceptions)
|
|
except StopIteration:
|
|
return unmocked_set(*args, **kwargs)
|
|
|
|
mock_set.side_effect = side_effect
|
|
alloc_obj.replace_all(self.ctx, alloc_list)
|
|
self.assertEqual(2, mock_log.debug.call_count)
|
|
mock_log.debug.called_with(
|
|
'Retrying allocations write on resource provider '
|
|
'generation conflict')
|
|
self.assertEqual(3, mock_set.call_count)
|
|
|
|
# Confirm we're using a different rp object after the change
|
|
# and that it has a higher generation.
|
|
new_rp = alloc_list[0].resource_provider
|
|
self.assertEqual(original_generation, rp1.generation)
|
|
self.assertEqual(original_generation + 1, new_rp.generation)
|