312 lines
13 KiB
Python
312 lines
13 KiB
Python
# Copyright 2022 StackHPC
|
|
#
|
|
# 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
|
|
|
|
from oslo_config import cfg
|
|
from oslo_limit import exception as limit_exceptions
|
|
from oslo_limit import limit
|
|
from oslo_utils.fixture import uuidsentinel as uuids
|
|
|
|
from nova import context
|
|
from nova import exception
|
|
from nova.limit import placement as placement_limits
|
|
from nova.limit import utils as limit_utils
|
|
from nova import objects
|
|
from nova import quota
|
|
from nova.scheduler.client import report
|
|
from nova import test
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
class TestGetUsage(test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(TestGetUsage, self).setUp()
|
|
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
|
|
self.context = context.RequestContext()
|
|
|
|
@mock.patch.object(quota, "is_qfd_populated")
|
|
@mock.patch.object(objects.InstanceMappingList, "get_counts")
|
|
@mock.patch.object(report.SchedulerReportClient,
|
|
"get_usages_counts_for_limits")
|
|
def test_get_usage(self, mock_placement, mock_inst, mock_qfd):
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB",
|
|
"class:CUSTOM_BAREMETAL"]
|
|
mock_qfd.return_value = True
|
|
mock_placement.return_value = {"VCPU": 1, "CUSTOM_BAREMETAL": 2}
|
|
mock_inst.return_value = {"project": {"instances": 42}}
|
|
|
|
usage = placement_limits._get_usage(self.context, uuids.project,
|
|
resources)
|
|
|
|
expected = {'class:MEMORY_MB': 0, 'class:VCPU': 1, 'servers': 42,
|
|
'class:CUSTOM_BAREMETAL': 2}
|
|
self.assertDictEqual(expected, usage)
|
|
|
|
def test_get_usage_bad_resources(self):
|
|
bad_resource = ["unknown_resource"]
|
|
self.assertRaises(ValueError, placement_limits._get_usage,
|
|
self.context, uuids.project, bad_resource)
|
|
bad_class = ["class:UNKNOWN_CLASS"]
|
|
self.assertRaises(ValueError, placement_limits._get_usage,
|
|
self.context, uuids.project, bad_class)
|
|
no_resources = []
|
|
self.assertRaises(ValueError, placement_limits._get_usage,
|
|
self.context, uuids.project, no_resources)
|
|
|
|
@mock.patch.object(quota, "is_qfd_populated")
|
|
def test_get_usage_bad_qfd(self, mock_qfd):
|
|
mock_qfd.return_value = False
|
|
resources = ["servers"]
|
|
e = self.assertRaises(ValueError, placement_limits._get_usage,
|
|
self.context, uuids.project, resources)
|
|
self.assertEqual("must first migrate instance mappings", str(e))
|
|
|
|
def test_get_usage_unified_limits_disabled(self):
|
|
self.flags(driver="nova.quota.NoopQuotaDriver", group="quota")
|
|
e = self.assertRaises(NotImplementedError, placement_limits._get_usage,
|
|
self.context, uuids.project, [])
|
|
self.assertEqual("unified limits is disabled", str(e))
|
|
|
|
@mock.patch.object(quota, "is_qfd_populated")
|
|
@mock.patch.object(objects.InstanceMappingList, "get_counts")
|
|
@mock.patch.object(report.SchedulerReportClient,
|
|
'get_usages_counts_for_limits')
|
|
def test_get_usage_placement_fail(self, mock_placement, mock_inst,
|
|
mock_qfd):
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB",
|
|
"class:CUSTOM_BAREMETAL"]
|
|
mock_qfd.return_value = True
|
|
mock_placement.side_effect = exception.UsagesRetrievalFailed(
|
|
project_id=uuids.project, user_id=uuids.user)
|
|
mock_inst.return_value = {"project": {"instances": 42}}
|
|
|
|
e = self.assertRaises(
|
|
exception.UsagesRetrievalFailed, placement_limits._get_usage,
|
|
self.context, uuids.project, resources)
|
|
|
|
expected = ("Failed to retrieve usages from placement while enforcing "
|
|
"%s quota limits." % ", ".join(resources))
|
|
self.assertEqual(expected, str(e))
|
|
|
|
@mock.patch.object(quota, "is_qfd_populated")
|
|
@mock.patch.object(objects.InstanceMappingList, "get_counts")
|
|
@mock.patch.object(report.SchedulerReportClient,
|
|
"get_usages_counts_for_limits")
|
|
def test_get_usage_pcpu_as_vcpu(self, mock_placement, mock_inst, mock_qfd):
|
|
# Test that when configured, PCPU count is merged into VCPU count when
|
|
# appropriate.
|
|
self.flags(unified_limits_count_pcpu_as_vcpu=True, group="workarounds")
|
|
mock_qfd.return_value = True
|
|
mock_inst.return_value = {"project": {"instances": 42}}
|
|
|
|
# PCPU was not specified in the flavor but usage was found in
|
|
# placement. PCPU count should be merged into VCPU count.
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB"]
|
|
mock_placement.return_value = {"VCPU": 1, "PCPU": 2}
|
|
|
|
usage = placement_limits._get_usage(self.context, uuids.project,
|
|
resources)
|
|
|
|
expected = {'class:MEMORY_MB': 0, 'class:VCPU': 3, 'servers': 42}
|
|
self.assertDictEqual(expected, usage)
|
|
|
|
# PCPU was not specified in the flavor and usage was found in placement
|
|
# and there was no VCPU usage in placement. The PCPU count should be
|
|
# returned as VCPU count.
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB"]
|
|
mock_placement.return_value = {"PCPU": 1}
|
|
|
|
usage = placement_limits._get_usage(self.context, uuids.project,
|
|
resources)
|
|
|
|
expected = {'class:MEMORY_MB': 0, 'class:VCPU': 1, 'servers': 42}
|
|
self.assertDictEqual(expected, usage)
|
|
|
|
# PCPU was not specified in the flavor but only VCPU usage was found in
|
|
# placement.
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB"]
|
|
mock_placement.return_value = {"VCPU": 1}
|
|
|
|
usage = placement_limits._get_usage(self.context, uuids.project,
|
|
resources)
|
|
|
|
expected = {'class:MEMORY_MB': 0, 'class:VCPU': 1, 'servers': 42}
|
|
self.assertDictEqual(expected, usage)
|
|
|
|
# PCPU was specified in the flavor, so the counts should be separate.
|
|
resources = ["servers", "class:VCPU", "class:MEMORY_MB", "class:PCPU"]
|
|
mock_placement.return_value = {"VCPU": 1, "PCPU": 2}
|
|
|
|
usage = placement_limits._get_usage(self.context, uuids.project,
|
|
resources)
|
|
|
|
expected = {'class:MEMORY_MB': 0, 'class:VCPU': 1, 'servers': 42,
|
|
'class:PCPU': 2}
|
|
self.assertDictEqual(expected, usage)
|
|
|
|
|
|
class TestGetDeltas(test.NoDBTestCase):
|
|
def test_get_deltas(self):
|
|
flavor = objects.Flavor(memory_mb=100, vcpus=10, swap=0,
|
|
ephemeral_gb=2, root_gb=5)
|
|
|
|
deltas = placement_limits._get_deltas_by_flavor(flavor, False, 2)
|
|
|
|
expected = {'servers': 2,
|
|
'class:VCPU': 20, 'class:MEMORY_MB': 200,
|
|
'class:DISK_GB': 14}
|
|
self.assertDictEqual(expected, deltas)
|
|
|
|
def test_get_deltas_recheck(self):
|
|
flavor = objects.Flavor(memory_mb=100, vcpus=10, swap=0,
|
|
ephemeral_gb=2, root_gb=5)
|
|
|
|
deltas = placement_limits._get_deltas_by_flavor(flavor, False, 0)
|
|
|
|
expected = {'servers': 0,
|
|
'class:VCPU': 0, 'class:MEMORY_MB': 0,
|
|
'class:DISK_GB': 0}
|
|
self.assertDictEqual(expected, deltas)
|
|
|
|
def test_get_deltas_check_baremetal(self):
|
|
extra_specs = {"resources:VCPU": 0, "resources:MEMORY_MB": 0,
|
|
"resources:DISK_GB": 0, "resources:CUSTOM_BAREMETAL": 1}
|
|
flavor = objects.Flavor(memory_mb=100, vcpus=10, swap=0,
|
|
ephemeral_gb=2, root_gb=5,
|
|
extra_specs=extra_specs)
|
|
|
|
deltas = placement_limits._get_deltas_by_flavor(flavor, True, 1)
|
|
|
|
expected = {'servers': 1, 'class:CUSTOM_BAREMETAL': 1}
|
|
self.assertDictEqual(expected, deltas)
|
|
|
|
def test_get_deltas_check_bfv(self):
|
|
flavor = objects.Flavor(memory_mb=100, vcpus=10, swap=0,
|
|
ephemeral_gb=2, root_gb=5)
|
|
|
|
deltas = placement_limits._get_deltas_by_flavor(flavor, True, 2)
|
|
|
|
expected = {'servers': 2,
|
|
'class:VCPU': 20, 'class:MEMORY_MB': 200,
|
|
'class:DISK_GB': 4}
|
|
self.assertDictEqual(expected, deltas)
|
|
|
|
|
|
class TestEnforce(test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(TestEnforce, self).setUp()
|
|
self.context = context.RequestContext()
|
|
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
|
|
|
|
placement_limits._ENFORCER = mock.Mock(limit.Enforcer)
|
|
self.flavor = objects.Flavor(memory_mb=100, vcpus=10, swap=0,
|
|
ephemeral_gb=2, root_gb=5)
|
|
|
|
def test_enforce_num_instances_and_flavor_disabled(self):
|
|
self.flags(driver="nova.quota.NoopQuotaDriver", group="quota")
|
|
count = placement_limits.enforce_num_instances_and_flavor(
|
|
self.context, uuids.project_id, "flavor", False, 0, 42)
|
|
self.assertEqual(42, count)
|
|
|
|
@mock.patch('oslo_limit.limit.Enforcer')
|
|
def test_enforce_num_instances_and_flavor(self, mock_limit):
|
|
mock_enforcer = mock.MagicMock()
|
|
mock_limit.return_value = mock_enforcer
|
|
|
|
count = placement_limits.enforce_num_instances_and_flavor(
|
|
self.context, uuids.project_id, self.flavor, False, 0, 2)
|
|
|
|
self.assertEqual(2, count)
|
|
mock_limit.assert_called_once_with(mock.ANY)
|
|
mock_enforcer.enforce.assert_called_once_with(
|
|
uuids.project_id,
|
|
{'servers': 2, 'class:VCPU': 20, 'class:MEMORY_MB': 200,
|
|
'class:DISK_GB': 14})
|
|
|
|
@mock.patch('oslo_limit.limit.Enforcer')
|
|
def test_enforce_num_instances_and_flavor_recheck(self, mock_limit):
|
|
mock_enforcer = mock.MagicMock()
|
|
mock_limit.return_value = mock_enforcer
|
|
|
|
count = placement_limits.enforce_num_instances_and_flavor(
|
|
self.context, uuids.project_id, self.flavor, False, 0, 0)
|
|
|
|
self.assertEqual(0, count)
|
|
mock_limit.assert_called_once_with(mock.ANY)
|
|
mock_enforcer.enforce.assert_called_once_with(
|
|
uuids.project_id,
|
|
{'servers': 0, 'class:VCPU': 0, 'class:MEMORY_MB': 0,
|
|
'class:DISK_GB': 0})
|
|
|
|
@mock.patch('oslo_limit.limit.Enforcer')
|
|
def test_enforce_num_instances_and_flavor_retry(self, mock_limit):
|
|
mock_enforcer = mock.MagicMock()
|
|
mock_limit.return_value = mock_enforcer
|
|
over_limit_info_list = [
|
|
limit_exceptions.OverLimitInfo("class:VCPU", 12, 0, 30)
|
|
]
|
|
mock_enforcer.enforce.side_effect = [
|
|
limit_exceptions.ProjectOverLimit(
|
|
uuids.project_id, over_limit_info_list),
|
|
None]
|
|
|
|
count = placement_limits.enforce_num_instances_and_flavor(
|
|
self.context, uuids.project_id, self.flavor, True, 0, 3)
|
|
|
|
self.assertEqual(2, count)
|
|
self.assertEqual(2, mock_enforcer.enforce.call_count)
|
|
mock_enforcer.enforce.assert_called_with(
|
|
uuids.project_id,
|
|
{'servers': 2, 'class:VCPU': 20, 'class:MEMORY_MB': 200,
|
|
'class:DISK_GB': 4})
|
|
|
|
@mock.patch('oslo_limit.limit.Enforcer')
|
|
def test_enforce_num_instances_and_flavor_fails(self, mock_limit):
|
|
mock_enforcer = mock.MagicMock()
|
|
mock_limit.return_value = mock_enforcer
|
|
over_limit_info_list = [
|
|
limit_exceptions.OverLimitInfo("class:VCPU", 12, 0, 20),
|
|
limit_exceptions.OverLimitInfo("servers", 2, 1, 2)
|
|
]
|
|
expected = limit_exceptions.ProjectOverLimit(uuids.project_id,
|
|
over_limit_info_list)
|
|
mock_enforcer.enforce.side_effect = expected
|
|
|
|
# Verify that the oslo.limit ProjectOverLimit gets translated to a
|
|
# TooManyInstances that the API knows how to handle
|
|
e = self.assertRaises(
|
|
exception.TooManyInstances,
|
|
placement_limits.enforce_num_instances_and_flavor, self.context,
|
|
uuids.project_id, self.flavor, True, 2, 4)
|
|
|
|
self.assertEqual(str(expected), str(e))
|
|
self.assertEqual(3, mock_enforcer.enforce.call_count)
|
|
|
|
@mock.patch('oslo_limit.limit.Enforcer')
|
|
def test_enforce_num_instances_and_flavor_placement_fail(self, mock_limit):
|
|
mock_enforcer = mock.MagicMock()
|
|
mock_limit.return_value = mock_enforcer
|
|
mock_enforcer.enforce.side_effect = exception.UsagesRetrievalFailed(
|
|
'Failed to retrieve usages')
|
|
|
|
e = self.assertRaises(
|
|
exception.UsagesRetrievalFailed,
|
|
placement_limits.enforce_num_instances_and_flavor, self.context,
|
|
uuids.project, self.flavor, True, 0, 5)
|
|
|
|
expected = str(mock_enforcer.enforce.side_effect)
|
|
self.assertEqual(expected, str(e))
|