Skip to content

Commit 4c11d54

Browse files
committed
Default user_id when not specified in check_num_instances_quota
The Quotas.check_deltas method needs a user_id keyword arg in order to scope a quota check to a particular user. However, when we call check_num_instances_quota we don't pass a project_id or user_id because at the time of the quota check, we have not yet created an instance record and thus will not use that to determine the appropriate project and user. Instead, we should rely on the RequestContext.project_id and RequestContext.user_id as defaults in this case, but check_num_instances_quota only defaults project_id and not user_id. check_num_instances_quota should also default user_id to the RequestContext.user_id when user_id is not explicitly passed. check_num_instances_quota should also check whether any per-user quota limits are defined for instance-related resources before passing along the user_id to scope resource counting and limit checking. Counting resources across a user is costly, so we should avoid it if it's not needed. Closes-Bug: #1893284 Change-Id: I3cfb1edc30b0bda4671e0d2cc2a8993055dcc9ff
1 parent 38bc8b8 commit 4c11d54

File tree

4 files changed

+66
-9
lines changed

4 files changed

+66
-9
lines changed

nova/compute/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1105,9 +1105,18 @@ def check_num_instances_quota(context, instance_type, min_count,
11051105
max_count, project_id=None, user_id=None,
11061106
orig_num_req=None):
11071107
"""Enforce quota limits on number of instances created."""
1108-
# project_id is used for the TooManyInstances error message
1108+
# project_id is also used for the TooManyInstances error message
11091109
if project_id is None:
11101110
project_id = context.project_id
1111+
if user_id is None:
1112+
user_id = context.user_id
1113+
# Check whether we need to count resources per-user and check a per-user
1114+
# quota limit. If we have no per-user quota limit defined for a
1115+
# project/user, we can avoid wasteful resource counting.
1116+
user_quotas = objects.Quotas.get_all_by_project_and_user(
1117+
context, project_id, user_id)
1118+
if not any(r in user_quotas for r in ['instances', 'cores', 'ram']):
1119+
user_id = None
11111120
# Determine requested cores and ram
11121121
req_cores = max_count * instance_type.vcpus
11131122
req_ram = max_count * instance_type.memory_mb

nova/tests/functional/regressions/test_bug_1893284.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,8 @@ def test_create_server_with_per_user_quota(self):
8484
server_req = self._build_server(
8585
image_uuid=fake_image.AUTO_DISK_CONFIG_ENABLED_IMAGE_UUID,
8686
networks='none')
87-
# FIXME(melwitt): Uncomment this when the bug is fixed. Because of the
88-
# the bug, the first request by the non-admin user will fail.
89-
# server = self.api.post_server({'server': server_req})
90-
# self._wait_for_state_change(server, 'ACTIVE')
87+
server = self.api.post_server({'server': server_req})
88+
self._wait_for_state_change(server, 'ACTIVE')
9189
# A request to boot a second instance should fail because the
9290
# non-admin has already booted 1 allowed instance.
9391
ex = self.assertRaises(

nova/tests/unit/compute/test_api.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ def test_create_with_networks_max_count_none(self, provision_instances,
232232
requested_networks=requested_networks,
233233
max_count=None)
234234

235+
@mock.patch('nova.objects.Quotas.get_all_by_project_and_user',
236+
new=mock.MagicMock())
235237
@mock.patch('nova.objects.Quotas.count_as_dict')
236238
@mock.patch('nova.objects.Quotas.limit_check')
237239
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
@@ -4203,6 +4205,8 @@ def test_check_injected_file_quota_onset_file_content_limit(self):
42034205
self._test_check_injected_file_quota_onset_file_limit_exceeded,
42044206
side_effect)
42054207

4208+
@mock.patch('nova.objects.Quotas.get_all_by_project_and_user',
4209+
new=mock.MagicMock())
42064210
@mock.patch('nova.objects.Quotas.count_as_dict')
42074211
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
42084212
@mock.patch('nova.objects.Instance.save')
@@ -4227,9 +4231,11 @@ def test_restore_by_admin(self, update_qfd, action_start, instance_save,
42274231
self.assertEqual(instance.task_state, task_states.RESTORING)
42284232
# mock.ANY might be 'instances', 'cores', or 'ram' depending on how the
42294233
# deltas dict is iterated in check_deltas
4234+
# user_id is expected to be None because no per-user quotas have been
4235+
# defined
42304236
quota_count.assert_called_once_with(admin_context, mock.ANY,
42314237
instance.project_id,
4232-
user_id=instance.user_id)
4238+
user_id=None)
42334239
quota_check.assert_called_once_with(
42344240
admin_context,
42354241
user_values={'instances': 2,
@@ -4238,9 +4244,11 @@ def test_restore_by_admin(self, update_qfd, action_start, instance_save,
42384244
project_values={'instances': 2,
42394245
'cores': 1 + instance.flavor.vcpus,
42404246
'ram': 512 + instance.flavor.memory_mb},
4241-
project_id=instance.project_id, user_id=instance.user_id)
4247+
project_id=instance.project_id)
42424248
update_qfd.assert_called_once_with(admin_context, instance, False)
42434249

4250+
@mock.patch('nova.objects.Quotas.get_all_by_project_and_user',
4251+
new=mock.MagicMock())
42444252
@mock.patch('nova.objects.Quotas.count_as_dict')
42454253
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
42464254
@mock.patch('nova.objects.Instance.save')
@@ -4264,9 +4272,11 @@ def test_restore_by_instance_owner(self, update_qfd, action_start,
42644272
self.assertEqual(instance.task_state, task_states.RESTORING)
42654273
# mock.ANY might be 'instances', 'cores', or 'ram' depending on how the
42664274
# deltas dict is iterated in check_deltas
4275+
# user_id is expected to be None because no per-user quotas have been
4276+
# defined
42674277
quota_count.assert_called_once_with(self.context, mock.ANY,
42684278
instance.project_id,
4269-
user_id=instance.user_id)
4279+
user_id=None)
42704280
quota_check.assert_called_once_with(
42714281
self.context,
42724282
user_values={'instances': 2,
@@ -4275,7 +4285,7 @@ def test_restore_by_instance_owner(self, update_qfd, action_start,
42754285
project_values={'instances': 2,
42764286
'cores': 1 + instance.flavor.vcpus,
42774287
'ram': 512 + instance.flavor.memory_mb},
4278-
project_id=instance.project_id, user_id=instance.user_id)
4288+
project_id=instance.project_id)
42794289
update_qfd.assert_called_once_with(self.context, instance, False)
42804290

42814291
@mock.patch.object(objects.InstanceAction, 'action_start')

nova/tests/unit/compute/test_compute_utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,46 @@ def test_check_instance_quota_exceeds_with_multiple_resources(self,
13961396
else:
13971397
self.fail("Exception not raised")
13981398

1399+
@mock.patch('nova.objects.Quotas.get_all_by_project_and_user')
1400+
@mock.patch('nova.objects.Quotas.check_deltas')
1401+
def test_check_num_instances_omits_user_if_no_user_quota(self, mock_check,
1402+
mock_get):
1403+
# Return no per-user quota.
1404+
mock_get.return_value = {'project_id': self.context.project_id,
1405+
'user_id': self.context.user_id}
1406+
fake_flavor = objects.Flavor(vcpus=1, memory_mb=512)
1407+
compute_utils.check_num_instances_quota(
1408+
self.context, fake_flavor, 1, 1)
1409+
deltas = {'instances': 1, 'cores': 1, 'ram': 512}
1410+
# Verify that user_id has not been passed along to scope the resource
1411+
# counting.
1412+
mock_check.assert_called_once_with(
1413+
self.context, deltas, self.context.project_id, user_id=None,
1414+
check_project_id=self.context.project_id, check_user_id=None)
1415+
1416+
@mock.patch('nova.objects.Quotas.get_all_by_project_and_user')
1417+
@mock.patch('nova.objects.Quotas.check_deltas')
1418+
def test_check_num_instances_passes_user_if_user_quota(self, mock_check,
1419+
mock_get):
1420+
for resource in ['instances', 'cores', 'ram']:
1421+
# Return some per-user quota for each of the instance-related
1422+
# resources.
1423+
mock_get.return_value = {'project_id': self.context.project_id,
1424+
'user_id': self.context.user_id,
1425+
resource: 5}
1426+
fake_flavor = objects.Flavor(vcpus=1, memory_mb=512)
1427+
compute_utils.check_num_instances_quota(
1428+
self.context, fake_flavor, 1, 1)
1429+
deltas = {'instances': 1, 'cores': 1, 'ram': 512}
1430+
# Verify that user_id is passed along to scope the resource
1431+
# counting and limit checking.
1432+
mock_check.assert_called_once_with(
1433+
self.context, deltas, self.context.project_id,
1434+
user_id=self.context.user_id,
1435+
check_project_id=self.context.project_id,
1436+
check_user_id=self.context.user_id)
1437+
mock_check.reset_mock()
1438+
13991439

14001440
class IsVolumeBackedInstanceTestCase(test.TestCase):
14011441
def setUp(self):

0 commit comments

Comments
 (0)