Added UpdateProjectQuota

Accessable at v1/openstack/quotas/.

A GET request returns json specifiing quota size, the current quota,
the current usage, and some details of any currently active quota
change tasks.

A POST request will update the quota for the project to a given
size. The data must contain a JSON dict with 'size' in it,
which will be the name of one of the pre-defined sizes.
Optionally regions (a list of region names) can be specified
which will restrict the update operation to those regions.

Change-Id: I907664f79f6eef0b5239139999cc7a28d246e446
This commit is contained in:
Amelia Cordwell 2017-05-18 16:43:41 +12:00 committed by Adrian Turjak
parent 5e16b575ad
commit 6f60b059f4
19 changed files with 1738 additions and 69 deletions

View File

@ -133,6 +133,14 @@ For ease of integration with OpenStack, these endpoints are setup to work and pa
* setup basic networking if needed
* create user with random password
* set user given password on token submit
* ../v1/openstack/quotas/ - GET
* JSON containg the specifications of each quota size, and data about the quota size for all regions in the current project
* An additional parameter regions, containing a comma separated list of regions can be passed as well to limit the regions it will return data about.
* ../v1/openstack/quotas/ - POST
* Change the quota for all regions
* The quota will automatically update if the new quota level is adjacent to the current one and there has not been an update to that region in the past 30 days
* Other options will require admin approval before updating
* POST body should be a JSON dict containing the size ('size') and optionally 'regions', a list of regions to update to
#### (DEPRECATED) Default TaskView Endpoints:

View File

@ -217,3 +217,6 @@ class IdentityManager(object):
except ks_exceptions.NotFound:
region = None
return region
def list_regions(self, **kwargs):
return self.ks_client.regions.list(**kwargs)

View File

@ -17,6 +17,7 @@ from logging import getLogger
from django.conf import settings
from django.utils import timezone
from adjutant.common.quota import QuotaManager
from adjutant.actions import user_store
from adjutant.actions.models import Action
@ -219,6 +220,17 @@ class ResourceMixin(object):
self.domain_id = self.domain.id
return True
def _validate_region_exists(self, region):
# Check that the region actually exists
id_manager = user_store.IdentityManager()
v_region = id_manager.get_region(region)
if not v_region:
self.add_note('ERROR: Region: %s does not exist.' % region)
return False
self.add_note('Region: %s exists.' % region)
return True
class UserMixin(ResourceMixin):
"""Mixin with functions for users."""
@ -401,6 +413,44 @@ class ProjectMixin(ResourceMixin):
self.add_note("New project '%s' created." % project.name)
class QuotaMixin(ResourceMixin):
"""Mixin with functions for dealing with quotas and limits."""
def _region_usage_greater_than_quota(self, usage, quota):
for service, values in quota.items():
for resource, value in values.items():
try:
if usage[service][resource] > value and value >= 0:
return True
except KeyError:
pass
return False
def _usage_greater_than_quota(self, regions):
quota_manager = QuotaManager(
self.project_id,
size_difference_threshold=self.size_difference_threshold)
quota = settings.PROJECT_QUOTA_SIZES.get(self.size, {})
for region in regions:
current_usage = quota_manager.get_current_usage(region)
if self._region_usage_greater_than_quota(current_usage, quota):
return True
return False
def _validate_regions_exist(self):
# Check that all the regions in the list exist
for region in self.regions:
if not self._validate_region_exists(region):
return False
return True
def _validate_usage_lower_than_quota(self):
if self._usage_greater_than_quota(self.regions):
self.add_note("Current usage greater than new quota")
return False
return True
class UserIdAction(BaseAction):
def _get_target_user(self):

View File

@ -22,7 +22,7 @@ from adjutant.actions.v1.users import (
UpdateUserEmailAction)
from adjutant.actions.v1.resources import (
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
SetProjectQuotaAction)
SetProjectQuotaAction, UpdateProjectQuotasAction)
from adjutant.actions.v1.misc import SendAdditionalEmailAction
@ -56,6 +56,8 @@ register_action_class(
serializers.NewProjectDefaultNetworkSerializer)
register_action_class(
SetProjectQuotaAction, serializers.SetProjectQuotaSerializer)
register_action_class(
UpdateProjectQuotasAction, serializers.UpdateProjectQuotasSerializer)
# Register Misc actions:
register_action_class(

View File

@ -1,4 +1,4 @@
# Copyright (C) 2015 Catalyst IT Ltd
# Copyright (C) 2015 Catalyst IT Ltd
#
# 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
@ -12,9 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
from adjutant.actions.v1.base import BaseAction, ProjectMixin
from django.conf import settings
from adjutant.actions.v1.base import BaseAction, ProjectMixin, QuotaMixin
from adjutant.actions import openstack_clients, user_store
from adjutant.api import models
from adjutant.common.quota import QuotaManager
from django.utils import timezone
from django.conf import settings
from datetime import timedelta
class NewDefaultNetworkAction(BaseAction, ProjectMixin):
@ -218,8 +224,16 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction):
self._create_network()
class SetProjectQuotaAction(BaseAction):
""" Updates quota for a given project to a configured quota level """
class UpdateProjectQuotasAction(BaseAction, QuotaMixin):
""" Updates quota for a project to a given size in a list of regions """
required = [
'size',
'project_id',
'regions',
]
default_days_between_autoapprove = 30
class ServiceQuotaFunctor(object):
def __call__(self, project_id, values):
@ -252,35 +266,161 @@ class SetProjectQuotaAction(BaseAction):
'neutron': ServiceQuotaNeutronFunctor
}
def _validate_project_exists(self):
if not self.project_id:
self.add_note('No project_id set, previous action should have '
'set it.')
return False
def __init__(self, *args, **kwargs):
super(UpdateProjectQuotasAction, self).__init__(*args, **kwargs)
self.size_difference_threshold = settings.TASK_SETTINGS.get(
self.action.task.task_type, {}).get(
'size_difference_threshold')
id_manager = user_store.IdentityManager()
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project with id %s does not exist.' %
self.project_id)
def _get_email(self):
if settings.USERNAME_IS_EMAIL:
return self.action.task.keystone_user['username']
else:
id_manager = user_store.IdentityManager()
user = id_manager.users.get(self.keystone_user['user_id'])
email = user.email
if email:
return email
self.add_note("User email address not set.")
return None
def _validate_quota_size_exists(self):
size_list = settings.PROJECT_QUOTA_SIZES.keys()
if self.size not in size_list:
self.add_note("Quota size: %s does not exist" % self.size)
return False
self.add_note('Project with id %s exists.' % self.project_id)
return True
def _pre_validate(self):
# Nothing to validate yet.
self.action.valid = True
self.action.save()
def _set_region_quota(self, region_name, quota_size):
# Set the quota for an individual region
quota_settings = settings.PROJECT_QUOTA_SIZES.get(quota_size, {})
if not quota_settings:
self.add_note(
"Project quota not defined for size '%s' in region %s." % (
quota_size, region_name))
return
for service_name, values in quota_settings.items():
updater_class = self._quota_updaters.get(service_name)
if not updater_class:
self.add_note("No quota updater found for %s. Ignoring" %
service_name)
continue
# functor for the service+region
service_functor = updater_class(region_name)
service_functor(self.project_id, values)
self.add_note("Project quota for region %s set to %s" % (
region_name, quota_size))
def _can_auto_approve(self):
wait_days = self.settings.get('days_between_autoapprove',
self.default_days_between_autoapprove)
task_list = models.Task.objects.filter(
completed_on__gte=timezone.now() - timedelta(days=wait_days),
task_type__exact=self.action.task.task_type,
cancelled__exact=False,
project_id__exact=self.project_id)
# Check to see if there have been any updates in the relavent regions
# recently
for task in task_list:
for action in task.actions:
intersect = set(action.action_data[
'regions']).intersection(self.regions)
if intersect:
self.add_note(
"Quota has already been updated within the auto "
"approve time limit.")
return False
region_sizes = []
quota_manager = QuotaManager(self.project_id,
self.size_difference_threshold)
for region in self.regions:
current_size = quota_manager.get_region_quota_data(
region)['current_quota_size']
region_sizes.append(current_size)
self.add_note(
"Project has size '%s' in region: '%s'" %
(current_size, region))
# Check for preapproved_quotas
preapproved_quotas = []
# If all region sizes are the same
if region_sizes.count(region_sizes[0]) == len(region_sizes):
preapproved_quotas = quota_manager.get_quota_change_options(
region_sizes[0])
if self.size not in preapproved_quotas:
self.add_note(
"Quota size '%s' not in preapproved list: %s" %
(self.size, preapproved_quotas))
return False
self.add_note(
"Quota size '%s' in preapproved list: %s" %
(self.size, preapproved_quotas))
return True
def _validate(self):
# Make sure the project id is valid and can be used
self.action.valid = (
self._validate_project_exists()
self._validate_project_id() and
self._validate_quota_size_exists() and
self._validate_regions_exist() and
self._validate_usage_lower_than_quota()
)
self.action.save()
def _pre_approve(self):
self._pre_validate()
self._validate()
# Set auto-approval
self.set_auto_approve(self._can_auto_approve())
def _post_approve(self):
self._validate()
if not self.valid or self.action.state == "completed":
return
for region in self.regions:
self._set_region_quota(region, self.size)
self.action.state = "completed"
self.action.task.cache['project_id'] = self.project_id
self.action.task.cache['size'] = self.size
self.action.save()
def _submit(self, token_data):
"""
Nothing to do here. Everything is done at post_approve.
"""
pass
class SetProjectQuotaAction(UpdateProjectQuotasAction):
""" Updates quota for a given project to a configured quota level """
required = []
def _get_email(self):
return None
def _validate(self):
# Make sure the project id is valid and can be used
self.action.valid = (
self._validate_project_id()
)
self.action.save()
def _pre_approve(self):
# Nothing to validate yet
self.action.valid = True
self.action.save()
def _post_approve(self):
# Assumption: another action has placed the project_id into the cache.
@ -294,24 +434,7 @@ class SetProjectQuotaAction(BaseAction):
regions_dict = self.settings.get('regions', {})
for region_name, region_settings in regions_dict.items():
quota_size = region_settings.get('quota_size')
quota_settings = settings.PROJECT_QUOTA_SIZES.get(quota_size, {})
if not quota_settings:
self.add_note(
"Project quota not defined for size '%s' in region %s." % (
quota_size, region_name))
continue
for service_name, values in quota_settings.items():
updater_class = self._quota_updaters.get(service_name)
if not updater_class:
self.add_note("No quota updater found for %s. Ignoring" %
service_name)
continue
# functor for the service+region
service_functor = updater_class(region_name)
service_functor(self.project_id, values)
self.add_note(
"Project quota for region %s set to %s" % (
region_name, quota_size))
self._set_region_quota(region_name, quota_size)
self.action.state = "completed"
self.action.save()

View File

@ -14,12 +14,18 @@
from rest_framework import serializers
from django.conf import settings
from adjutant.actions import user_store
role_options = settings.DEFAULT_ACTION_SETTINGS.get("NewUserAction", {}).get(
"allowed_roles", [])
def get_region_choices():
id_manager = user_store.IdentityManager()
return (region.id for region in id_manager.list_regions())
class BaseUserNameSerializer(serializers.Serializer):
"""
A serializer where the user is identified by username/email.
@ -95,3 +101,26 @@ class SendAdditionalEmailSerializer(serializers.Serializer):
class UpdateUserEmailSerializer(BaseUserIdSerializer):
new_email = serializers.EmailField()
class UpdateProjectQuotasSerializer(serializers.Serializer):
project_id = serializers.CharField(max_length=64)
size = serializers.CharField(max_length=64)
def __init__(self, *args, **kwargs):
super(UpdateProjectQuotasSerializer, self).__init__(*args, **kwargs)
# NOTE(amelia): This overide is mostly in use so that it can be tested
# However it does take into account the improbable edge case that the
# regions have changed since the server was last started
self.fields['regions'] = serializers.MultipleChoiceField(
choices=get_region_choices())
def validate_size(self, value):
"""
Check that the size exists in the conf.
"""
size_list = settings.PROJECT_QUOTA_SIZES.keys()
if value not in size_list:
raise serializers.ValidationError("Quota size: %s is not valid"
% value)
return value

View File

@ -151,8 +151,8 @@ class FakeNovaClient(FakeOpenstackClient):
def __init__(self, data):
self.data = data
def get(self, project_id):
return self.LimitFake(self.data, project_id)
def get(self, tenant_id):
return self.LimitFake(self.data, tenant_id)
class LimitFake(object):
def __init__(self, data, project_id):

View File

@ -13,12 +13,13 @@
# under the License.
from django.test import TestCase
from django.test.utils import override_settings
import mock
from adjutant.actions.v1.resources import (
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
SetProjectQuotaAction)
SetProjectQuotaAction, UpdateProjectQuotasAction)
from adjutant.api.models import Task
from adjutant.api.v1.tests import (FakeManager, setup_temp_cache,
modify_dict_settings)
@ -454,9 +455,204 @@ class ProjectSetupActionTests(TestCase):
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 3)
# RegionTwo, cinder only
self.assertFalse('RegionTwo' in nova_cache)
r2_cinderquota = cinder_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(r2_cinderquota['gigabytes'], 73571)
self.assertEquals(r2_cinderquota['snapshots'], 73572)
self.assertEquals(r2_cinderquota['volumes'], 73573)
# RegionThree, cinder only
self.assertFalse('RegionThree' in nova_cache)
r2_cinderquota = cinder_cache['RegionThree'][
'test_project_id']['quota']
self.assertEquals(r2_cinderquota['gigabytes'], 50000)
self.assertEquals(r2_cinderquota['snapshots'], 600)
self.assertEquals(r2_cinderquota['volumes'], 200)
@mock.patch(
'adjutant.actions.user_store.IdentityManager',
FakeManager)
@mock.patch(
'adjutant.common.quota.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.common.quota.get_cinderclient',
get_fake_cinderclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_cinderclient',
get_fake_cinderclient)
class QuotaActionTests(TestCase):
def tearDown(self):
cinder_cache.clear()
nova_cache.clear()
neutron_cache.clear()
def test_update_quota(self):
"""
Sets a new quota on all services of a project in a single region
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project, }, {user.id: user})
setup_neutron_cache('RegionOne', 'test_project_id')
# Test sending to only a single region
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
'size': 'medium',
'regions': ['RegionOne'],
'user_id': user.id
}
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
# check the quotas were updated
# This relies on test_settings heavily.
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 10000)
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 327680)
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 5)
def test_update_quota_multi_region(self):
"""
Sets a new quota on all services of a project in multiple regions
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project, }, {user.id: user})
setup_mock_caches('RegionOne', project.id)
setup_mock_caches('RegionTwo', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
'size': 'large',
'domain_id': 'default',
'regions': ['RegionOne', 'RegionTwo'],
'user_id': 'user_id'
}
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
# check the quotas were updated
# This relies on test_settings heavily.
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 50000)
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 10)
cinderquota = cinder_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 50000)
novaquota = nova_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 10)
@override_settings(QUOTA_SIZES_ASC=[])
def test_update_quota_not_in_sizes_asc(self):
"""
Tests that the quota will still update to a size even if it is not
placed in QUOTA_SIZES_ASC
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project, }, {user.id: user})
setup_mock_caches('RegionOne', project.id)
setup_mock_caches('RegionTwo', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
'size': 'large',
'domain_id': 'default',
'regions': ['RegionOne', 'RegionTwo'],
}
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
# check the quotas were updated
# This relies on test_settings heavily.
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 50000)
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 10)
cinderquota = cinder_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 50000)
novaquota = nova_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 10)

View File

@ -49,3 +49,5 @@ register_taskview_class(
r'^openstack/users/email-update/?$', openstack.UserUpdateEmail)
register_taskview_class(
r'^openstack/sign-up/?$', openstack.SignUp)
register_taskview_class(
r'^openstack/quotas/?$', openstack.UpdateProjectQuotas)

View File

@ -15,13 +15,15 @@
from django.conf import settings
from django.utils import timezone
from rest_framework.response import Response
from adjutant.actions import user_store
from adjutant.api import models
from adjutant.api import utils
from adjutant.api.v1 import tasks
from adjutant.api.v1.utils import add_task_id_for_roles
from adjutant.api.v1.utils import add_task_id_for_roles, create_notification
from adjutant.common.quota import QuotaManager
class UserList(tasks.InviteUser):
@ -316,3 +318,158 @@ class SignUp(tasks.CreateProject):
def post(self, request, format=None):
return super(SignUp, self).post(request)
class UpdateProjectQuotas(tasks.TaskView):
"""
The OpenStack endpoint to update the quota of a project in
one or more regions
"""
task_type = "update_quota"
default_actions = ["UpdateProjectQuotasAction", ]
_number_of_returned_tasks = 5
def get_region_quota_data(self, region_id):
quota_manager = QuotaManager(self.project_id)
current_quota = quota_manager.get_current_region_quota(region_id)
current_quota_size = quota_manager.get_quota_size(current_quota)
change_options = quota_manager.get_quota_change_options(
current_quota_size)
current_usage = quota_manager.get_current_usage(region_id)
return {"region": region_id,
"current_quota": current_quota,
"current_quota_size": current_quota_size,
"quota_change_options": change_options,
"current_usage": current_usage
}
def get_active_quota_tasks(self):
# Get the 5 last quota tasks.
task_list = models.Task.objects.filter(
task_type__exact=self.task_type,
project_id__exact=self.project_id,
cancelled=0,
).order_by('-created_on')[:self._number_of_returned_tasks]
response_tasks = []
for task in task_list:
status = "Awaiting Approval"
if task.completed:
status = "Completed"
task_data = {}
for action in task.actions:
task_data.update(action.action_data)
new_dict = {
"id": task.uuid,
"regions": task_data['regions'],
"size": task_data['size'],
"request_user":
task.keystone_user['username'],
"task_created": task.created_on,
"valid": all([a.valid for a in task.actions]),
"status": status
}
response_tasks.append(new_dict)
return response_tasks
def check_region_exists(self, region):
# Check that the region actually exists
id_manager = user_store.IdentityManager()
v_region = id_manager.get_region(region)
if not v_region:
return False
return True
@utils.mod_or_admin
def get(self, request):
"""
This endpoint returns data about what sizes are available
as well as the current status of a specified region's quotas.
"""
quota_settings = settings.PROJECT_QUOTA_SIZES
size_order = settings.QUOTA_SIZES_ASC
self.project_id = request.keystone_user['project_id']
regions = request.query_params.get('regions', None)
if regions:
regions = regions.split(",")
else:
id_manager = user_store.IdentityManager()
# Only get the region id as that is what will be passed from
# parameters otherwise
regions = (region.id for region in id_manager.list_regions())
region_quotas = []
quota_manager = QuotaManager(self.project_id)
for region in regions:
if self.check_region_exists(region):
region_quotas.append(quota_manager.get_region_quota_data(
region))
else:
return Response(
{"ERROR": ['Region: %s is not valid' % region]}, 400)
response_tasks = self.get_active_quota_tasks()
return Response({'regions': region_quotas,
"quota_sizes": quota_settings,
"quota_size_order": size_order,
"active_quota_tasks": response_tasks})
@utils.mod_or_admin
def post(self, request):
request.data['project_id'] = request.keystone_user['project_id']
self.project_id = request.keystone_user['project_id']
regions = request.data.get('regions', None)
if not regions:
id_manager = user_store.IdentityManager()
regions = [region.id for region in id_manager.list_regions()]
request.data['regions'] = regions
self.logger.info("(%s) - New UpdateProjectQuotas request."
% timezone.now())
processed, status = self.process_actions(request)
# check the status
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response(errors, status=status)
if processed.get('auto_approved', False):
response_dict = {'notes': processed['notes']}
return Response(response_dict, status=status)
task = processed['task']
action_models = task.actions
valid = all([act.valid for act in action_models])
if not valid:
return Response({'errors': ['Actions invalid. You may have usage '
'above the new quota level.']}, 400)
# Action needs to be manually approved
notes = {
'notes':
['New task for UpdateProjectQuotas.']
}
create_notification(processed['task'], notes)
self.logger.info("(%s) - Task processed. Awaiting Aprroval"
% timezone.now())
response_dict = {'notes': ['Task processed. Awaiting Aprroval.']}
return Response(response_dict, status=202)

View File

@ -0,0 +1,6 @@
This email is to confirm that the quota for your openstack project {{ task.cache.project_id }} has been changed to {{ task.cache.size }}.
If you did not do this yourself, please get in touch with your systems administrator to report suspicious activity and secure your account.
Kind regards,
The Openstack team

View File

@ -39,9 +39,12 @@ def setup_temp_cache(projects, users):
users.update({admin_user.id: admin_user})
region_one = mock.Mock()
region_one.id = 'region_id_0'
region_one.id = 'RegionOne'
region_one.name = 'RegionOne'
region_two = mock.Mock()
region_two.id = 'RegionTwo'
global temp_cache
# TODO(adriant): region and project keys are name, should be ID.
@ -58,6 +61,7 @@ def setup_temp_cache(projects, users):
},
'regions': {
'RegionOne': region_one,
'RegionTwo': region_two
},
'domains': {
default_domain.id: default_domain,
@ -267,6 +271,10 @@ class FakeManager(object):
global temp_cache
return temp_cache['regions'].get(region_id, None)
def list_regions(self):
global temp_cache
return temp_cache['regions'].values()
class modify_dict_settings(override_settings):
"""

View File

@ -18,9 +18,17 @@ from rest_framework import status
from rest_framework.test import APITestCase
from django.test.utils import override_settings
from django.utils import timezone
from django.conf import settings
from adjutant.api.models import Token
from adjutant.api.models import Token, Task
from adjutant.api.v1.tests import FakeManager, setup_temp_cache
from adjutant.actions.v1.tests import (
get_fake_neutron, get_fake_novaclient, get_fake_cinderclient,
cinder_cache, nova_cache, neutron_cache,
setup_mock_caches, setup_quota_cache, FakeResource)
from datetime import timedelta
@mock.patch('adjutant.actions.user_store.IdentityManager',
@ -310,3 +318,766 @@ class OpenstackAPITests(APITestCase):
data = {'password': 'testpassword'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch(
'adjutant.actions.user_store.IdentityManager',
FakeManager)
@mock.patch(
'adjutant.common.quota.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.common.quota.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_cinderclient',
get_fake_cinderclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_cinderclient',
get_fake_cinderclient)
class QuotaAPITests(APITestCase):
def tearDown(self):
""" Clears quota caches """
global cinder_cache
cinder_cache.clear()
global nova_cache
nova_cache.clear()
global neutron_cache
neutron_cache.clear()
super(QuotaAPITests, self).tearDown()
def setUp(self):
super(QuotaAPITests, self).setUp()
setup_mock_caches('RegionOne', 'test_project_id')
setup_mock_caches('RegionTwo', 'test_project_id')
def check_quota_cache(self, region_name, project_id, size):
"""
Helper function to check if the global quota caches now match the size
defined in the config
"""
cinderquota = cinder_cache[region_name][project_id]['quota']
gigabytes = settings.PROJECT_QUOTA_SIZES[size]['cinder']['gigabytes']
self.assertEquals(cinderquota['gigabytes'], gigabytes)
novaquota = nova_cache[region_name][project_id]['quota']
ram = settings.PROJECT_QUOTA_SIZES[size]['nova']['ram']
self.assertEquals(novaquota['ram'], ram)
neutronquota = neutron_cache[region_name][project_id]['quota']
network = settings.PROJECT_QUOTA_SIZES[size]['neutron']['network']
self.assertEquals(neutronquota['network'], network)
def test_update_quota_no_history(self):
""" Update the quota size of a project with no history """
admin_headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
def test_update_quota_history(self):
"""
Update the quota size of a project with a quota change recently
It should update the quota the first time but wait for admin approval
the second time
"""
admin_headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
data = {'size': 'small',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have not changed
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
# Approve the quota change as admin
headers = {
'project_name': "admin_project",
'project_id': "test_project_id",
'roles': "admin,_member_",
'username': "admin",
'user_id': "admin_id",
'authenticated': True
}
# Grab the details for the second task and approve it
new_task = Task.objects.all()[1]
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{'notes': ['Task completed successfully.']}
)
# Quotas should have changed to small
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
def test_update_quota_old_history(self):
"""
Update the quota size of a project with a quota change 31 days ago
It should update the quota the first time without approval
"""
admin_headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
# Fudge the data to make the task occur 31 days ago
task = Task.objects.all()[0]
task.completed_on = timezone.now() - timedelta(days=32)
task.save()
data = {'size': 'small',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
def test_update_quota_other_project_history(self):
"""
Tests that a quota update to another project does not interfer
with the 30 days per project limit.
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
project2 = mock.Mock()
project2.id = 'second_project_id'
project2.name = 'second_project'
project2.domain = 'default'
project2.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project, 'second_project': project2},
{user.id: user})
setup_mock_caches('RegionOne', 'second_project_id')
# setup_quota_cache('RegionOne', 'second_project_id', 'small')
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
headers = {
'project_name': "second_project",
'project_id': "second_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test2@example.com",
'user_id': user.id,
'authenticated': True
}
data = {'regions': ["RegionOne"], 'size': 'medium',
'project_id': 'second_project_id'}
response = self.client.post(url, data, headers=headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache('RegionOne', 'second_project_id', 'medium')
def test_update_quota_outside_range(self):
"""
Attempts to update the quota size to a value outside of the
project's pre-approved range.
"""
admin_headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'large',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Then check to see the quotas have not changed (stayed small)
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
# Approve and test for change
# Approve the quota change as admin
headers = {
'project_name': "admin_project",
'project_id': "test_project_id",
'roles': "admin,_member_",
'username': "admin",
'user_id': "admin_id",
'authenticated': True
}
# Grab the details for the task and approve it
new_task = Task.objects.all()[0]
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{'notes': ['Task completed successfully.']}
)
self.check_quota_cache('RegionOne', 'test_project_id', 'large')
def test_calculate_custom_quota_size(self):
"""
Calculates the best 'fit' quota size from a custom quota.
"""
admin_headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id})
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
cinderquota['gigabytes'] = 6000
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
novaquota['ram'] = 70000
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
neutronquota['network'] = 4
url = "/v1/openstack/quotas/?regions=RegionOne"
response = self.client.get(url, headers=admin_headers)
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['regions'][0]['current_quota_size'], 'small')
def test_return_quota_history(self):
"""
Ensures that the correct quota history and usage data is returned
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project_id': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'large',
'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
response = self.client.get(url, headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
recent_task = response.data['active_quota_tasks'][0]
self.assertEqual(
recent_task['size'], 'large')
self.assertEqual(
recent_task['request_user'], 'test@example.com')
self.assertEqual(
recent_task['status'], 'Awaiting Approval')
def test_set_multi_region_quota(self):
""" Sets a quota to all to all regions in a project """
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'medium', 'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
self.check_quota_cache('RegionTwo', 'test_project_id', 'medium')
def test_set_multi_region_quota_history(self):
"""
Attempts to set a multi region quota with a multi region update history
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
self.check_quota_cache('RegionTwo', 'test_project_id', 'medium')
data = {'size': 'small',
'project_id': 'test_project_id'}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# All of them stay the same
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
self.check_quota_cache('RegionTwo', 'test_project_id', 'medium')
# Approve the task
headers = {
'project_name': "admin_project",
'project_id': "test_project_id",
'roles': "admin,_member_",
'username': "admin",
'user_id': "admin_id",
'authenticated': True
}
new_task = Task.objects.all()[1]
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{'notes': ['Task completed successfully.']}
)
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
self.check_quota_cache('RegionTwo', 'test_project_id', 'small')
def test_set_multi_quota_single_history(self):
"""
Attempts to set a multi region quota with a single region quota
update history
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
# Setup custom parts of the quota still within 'small' however
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data, headers=headers, format='json')
# First check we can actually access the page correctly
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
url = "/v1/openstack/quotas/"
data = {'size': 'small',
'regions': ['RegionOne', 'RegionTwo']}
response = self.client.post(url, data, headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
# Quotas stay the same
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
self.check_quota_cache('RegionTwo', 'test_project_id', 'small')
headers = {
'project_name': "admin_project",
'project_id': "test_project_id",
'roles': "admin,_member_",
'username': "admin",
'user_id': "admin_id",
'authenticated': True
}
new_task = Task.objects.all()[1]
url = "/v1/tasks/" + new_task.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{'notes': ['Task completed successfully.']}
)
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
self.check_quota_cache('RegionTwo', 'test_project_id', 'small')
def test_set_quota_over_limit(self):
""" Attempts to set a smaller quota than the current usage """
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
setup_quota_cache('RegionOne', 'test_project_id', 'medium')
# Setup current quota as medium
# Create a number of lists with limits higher than the small quota
global nova_cache
nova_cache['RegionOne']['test_project_id'][
'absolute']["totalInstancesUsed"] = 11
url = "/v1/openstack/quotas/"
data = {'size': 'small',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
data = {'size': 'small',
'regions': ['RegionOne']}
nova_cache['RegionOne']['test_project_id'][
'absolute']["totalInstancesUsed"] = 10
# Test for cinder resources
volume_list = [FakeResource(10) for i in range(21)]
cinder_cache['RegionOne']['test_project_id']['volumes'] = volume_list
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
# Test for neutron resources
cinder_cache['RegionOne']['test_project_id']['volumes'] = []
net_list = [{} for i in range(4)]
neutron_cache['RegionOne']['test_project_id']['networks'] = net_list
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.check_quota_cache('RegionOne', 'test_project_id', 'medium')
# Check that after they are all cleared to sub small levels
# the quota updates
neutron_cache['RegionOne']['test_project_id']['networks'] = []
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.check_quota_cache('RegionOne', 'test_project_id', 'small')
def test_set_quota_invalid_region(self):
""" Attempts to set a quota on a non-existent region """
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_temp_cache({'test_project': project}, {user.id: user})
url = "/v1/openstack/quotas/"
data = {'size': 'small',
'regions': ['RegionThree']}
response = self.client.post(url, data,
headers=headers, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

View File

@ -781,7 +781,7 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(mail.outbox[0].subject, 'email_update_additional')
self.assertEqual(mail.outbox[1].to, ['new_test@example.com'])
self.assertEqual(mail.outbox[1].subject, 'Your Token')
self.assertEqual(mail.outbox[1].subject, 'email_update_token')
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -845,7 +845,7 @@ class TaskViewTests(AdjutantAPITestCase):
self.assertEqual(mail.outbox[0].subject, 'email_update_additional')
self.assertEqual(mail.outbox[1].to, ['new_test@example.com'])
self.assertEqual(mail.outbox[1].subject, 'Your Token')
self.assertEqual(mail.outbox[1].subject, 'email_update_token')
new_token = Token.objects.all()[0]
url = "/v1/tokens/" + new_token.token
@ -1170,7 +1170,7 @@ class TaskViewTests(AdjutantAPITestCase):
"""
# NOTE(amelia): sending this email here is probably not the intended
# case. It would be more useful in cases such as a quota update or a
# case. It would be more useful in utils such as a quota update or a
# child project being created that all the project admins should be
# notified of

View File

169
adjutant/common/quota.py Normal file
View File

@ -0,0 +1,169 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# 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 adjutant.actions.openstack_clients import (
get_novaclient, get_cinderclient, get_neutronclient)
from django.conf import settings
class QuotaManager(object):
"""
A manager to allow easier updating and access to quota information
across all services.
"""
default_size_diff_threshold = .2
def __init__(self, project_id, size_difference_threshold=None):
self.project_id = project_id
self.size_diff_threshold = (size_difference_threshold or
self.default_size_diff_threshold)
def get_current_region_quota(self, region_id):
ci_quota = get_cinderclient(region_id) \
.quotas.get(self.project_id).to_dict()
neutron_quota = get_neutronclient(region_id) \
.show_quota(self.project_id)['quota']
nova_quota = get_novaclient(region_id) \
.quotas.get(self.project_id).to_dict()
return {'cinder': ci_quota,
'nova': nova_quota,
'neutron': neutron_quota}
def get_quota_size(self, current_quota, difference_threshold=None):
""" Gets the closest matching quota size for a given quota """
quota_differences = {}
for size, setting in settings.PROJECT_QUOTA_SIZES.items():
match_percentages = []
for service_name, values in setting.items():
for name, value in values.items():
if value != 0:
match_percentages.append(
float(current_quota[service_name][name]) / value)
# Calculate the average of how much it matches the setting
difference = abs(
(sum(match_percentages) / float(len(match_percentages))) - 1)
# TODO(amelia): Nicer form of this due to the new way of doing
# per action settings
if (difference <= self.size_diff_threshold):
quota_differences[size] = difference
if len(quota_differences) > 0:
return min(quota_differences, key=quota_differences.get)
# If we don't get a match return custom which means the project will
# need admin approval for any change
return 'custom'
def get_quota_change_options(self, quota_size):
""" Get's the pre-approved quota change options for a given size """
quota_list = settings.QUOTA_SIZES_ASC
try:
list_position = quota_list.index(quota_size)
except ValueError:
return []
quota_change_list = []
if list_position - 1 >= 0:
quota_change_list.append(quota_list[list_position - 1])
if list_position + 1 < len(quota_list):
quota_change_list.append(quota_list[list_position + 1])
return quota_change_list
def get_region_quota_data(self, region_id):
current_quota = self.get_current_region_quota(region_id)
current_quota_size = self.get_quota_size(current_quota)
change_options = self.get_quota_change_options(current_quota_size)
current_usage = self.get_current_usage(region_id)
return {'region': region_id,
"current_quota": current_quota,
"current_quota_size": current_quota_size,
"quota_change_options": change_options,
"current_usage": current_usage
}
def get_current_usage(self, region_id):
cinder_usage = self.get_cinder_usage(region_id)
nova_usage = self.get_nova_usage(region_id)
neutron_usage = self.get_neutron_usage(region_id)
return {'cinder': cinder_usage,
'nova': nova_usage,
'neutron': neutron_usage}
def get_cinder_usage(self, region_id):
client = get_cinderclient(region_id)
volumes = client.volumes.list(
search_opts={'all_tenants': 1, 'project_id': self.project_id})
snapshots = client.volume_snapshots.list(
search_opts={'all_tenants': 1, 'project_id': self.project_id})
# gigabytesUsed should be a total of volumes and snapshots
gigabytes = sum([getattr(volume, 'size', 0) for volume
in volumes])
gigabytes += sum([getattr(snap, 'size', 0) for snap
in snapshots])
return {'gigabytes': gigabytes,
'volumes': len(volumes),
'snapshots': len(snapshots)
}
def get_neutron_usage(self, region_id):
client = get_neutronclient(region_id)
networks = client.list_networks(
tenant_id=self.project_id)['networks']
routers = client.list_routers(
tenant_id=self.project_id)['routers']
floatingips = client.list_floatingips(
tenant_id=self.project_id)['floatingips']
ports = client.list_ports(
tenant_id=self.project_id)['ports']
subnets = client.list_subnets(
tenant_id=self.project_id)['subnets']
security_groups = client.list_security_groups(
tenant_id=self.project_id)['security_groups']
security_group_rules = client.list_security_group_rules(
tenant_id=self.project_id)['security_group_rules']
return {'network': len(networks),
'router': len(routers),
'floatingip': len(floatingips),
'port': len(ports),
'subnet': len(subnets),
'secuirty_group': len(security_groups),
'security_group_rule': len(security_group_rules)
}
def get_nova_usage(self, region_id):
client = get_novaclient(region_id)
nova_usage = client.limits.get(
tenant_id=self.project_id).to_dict()['absolute']
nova_usage_keys = [
('instances', 'totalInstancesUsed'),
('floating_ips', 'totalFloatingIpsUsed'),
('ram', 'totalRAMUsed'),
('cores', 'totalCoresUsed'),
('secuirty_groups', 'totalSecurityGroupsUsed')
]
nova_usage_dict = {}
for key, usage_key in nova_usage_keys:
nova_usage_dict[key] = nova_usage[usage_key]
return nova_usage_dict

View File

@ -167,6 +167,8 @@ ROLES_MAPPING = CONFIG['ROLES_MAPPING']
PROJECT_QUOTA_SIZES = CONFIG.get('PROJECT_QUOTA_SIZES')
QUOTA_SIZES_ASC = CONFIG.get('QUOTA_SIZES_ASC', [])
# Defaults for backwards compatibility.
ACTIVE_TASKVIEWS = CONFIG.get(
'ACTIVE_TASKVIEWS',

View File

@ -86,7 +86,8 @@ ACTIVE_TASKVIEWS = [
'InviteUser',
'ResetPassword',
'EditUser',
'UpdateEmail'
'UpdateEmail',
'UpdateProjectQuotas',
]
DEFAULT_TASK_SETTINGS = {
@ -162,8 +163,8 @@ DEFAULT_ACTION_SETTINGS = {
'RegionOne': {
'quota_size': 'small'
},
'RegionTwo': {
'quota_size': 'large'
'RegionThree': {
'quota_size': 'large_cinder_only'
}
},
},
@ -183,7 +184,7 @@ DEFAULT_ACTION_SETTINGS = {
},
'ResetUserPasswordAction': {
'blacklisted_roles': ['admin'],
}
},
}
TASK_SETTINGS = {
@ -252,11 +253,23 @@ TASK_SETTINGS = {
'update_email': {
'emails': {
'initial': None,
'token': {
'subject': 'email_update_token',
'template': 'email_update_token.txt'
},
'completed': {
'subject': 'Email Update Complete',
'template': 'email_update_completed.txt'
}
},
},
'edit_user': {
'role_blacklist': ['admin']
}
},
'update_quota': {
'duplicate_policy': 'cancel',
'days_between_autoapprove': 30,
},
}
ROLES_MAPPING = {
@ -301,15 +314,76 @@ PROJECT_QUOTA_SIZES = {
'subnet': 3,
},
},
'large': {
'cinder': {
'gigabytes': 73571,
'snapshots': 73572,
'volumes': 73573,
"medium": {
"cinder": {
"gigabytes": 10000,
"volumes": 100,
"snapshots": 300
},
"nova": {
"metadata_items": 128,
"injected_file_content_bytes": 10240,
"ram": 327680,
"floating_ips": 25,
"key_pairs": 50,
"instances": 50,
"security_group_rules": 400,
"injected_files": 5,
"cores": 100,
"fixed_ips": 0,
"security_groups": 50
},
"neutron": {
"security_group_rule": 400,
"subnet": 5,
"network": 5,
"floatingip": 25,
"security_group": 50,
"router": 5,
"port": 250
}
},
"large": {
"cinder": {
"gigabytes": 50000,
"volumes": 200,
"snapshots": 600
},
"nova": {
"metadata_items": 128,
"injected_file_content_bytes": 10240,
"ram": 655360,
"floating_ips": 50,
"key_pairs": 50,
"instances": 100,
"security_group_rules": 800,
"injected_files": 5,
"cores": 200,
"fixed_ips": 0,
"security_groups": 100
},
"neutron": {
"security_group_rule": 800,
"subnet": 10,
"network": 10,
"floatingip": 50,
"security_group": 100,
"router": 10,
"port": 500
}
},
"large_cinder_only": {
"cinder": {
"gigabytes": 50000,
"volumes": 200,
"snapshots": 600
},
},
}
QUOTA_SIZES_ASC = ['small', 'medium', 'large']
SHOW_ACTION_ENDPOINTS = True
conf_dict = {
@ -330,4 +404,5 @@ conf_dict = {
"ROLES_MAPPING": ROLES_MAPPING,
"PROJECT_QUOTA_SIZES": PROJECT_QUOTA_SIZES,
"SHOW_ACTION_ENDPOINTS": SHOW_ACTION_ENDPOINTS,
"QUOTA_SIZES_ASC": QUOTA_SIZES_ASC,
}

View File

@ -70,6 +70,7 @@ ACTIVE_TASKVIEWS:
- RoleList
- SignUp
- UserUpdateEmail
- UpdateProjectQuotas
DEFAULT_TASK_SETTINGS:
emails:
@ -173,6 +174,8 @@ DEFAULT_ACTION_SETTINGS:
regions:
RegionOne:
quota_size: small
UpdateProjectQuotasAction:
days_between_autoapprove: 30
SendAdditionalEmailAction:
initial:
email_current_user: false
@ -235,13 +238,13 @@ TASK_SETTINGS:
emails:
- signups@example.com
default_region: RegionOne
# If 'None' (null in yaml), will default to domain as parent.
# If domain isn't set explicity, will service user domain (see KEYSTONE).
# If 'None' (null in yaml) will default to domain as parent.
# If domain isn't set explicity will service user domain (see KEYSTONE).
default_parent_id: null
invite_user:
duplicate_policy: cancel
emails:
# To not send this email, set the value to null
# To not send this email set the value to null
initial: null
token:
subject: Invitation to an OpenStack project
@ -303,6 +306,15 @@ TASK_SETTINGS:
subject: OpenStack Email Update Requested
template: email_update_started.txt
email_current_user: True
update_quota:
duplicate_policy: cancel
size_difference_threshold: 0.1
emails:
initial: null
token: null
completed:
subject: Openstack Quota updated
template: quota_completed.txt
# mapping between roles and managable roles
ROLES_MAPPING:
@ -347,3 +359,59 @@ PROJECT_QUOTA_SIZES:
security_group: 20
security_group_rule: 100
subnet: 3
medium:
cinder:
gigabytes: 10000
volumes: 100
snapshots: 300
nova:
metadata_items: 128
injected_file_content_bytes: 10240
ram: 327680
floating_ips: 25
key_pairs: 50
instances: 50
security_group_rules: 400
injected_files: 5
cores: 100
fixed_ips: 0
security_groups: 50
neutron:
security_group_rule: 400
subnet: 5
network: 5
floatingip: 25
security_group: 50
router: 5
port: 250
large:
cinder:
gigabytes: 50000
volumes: 200
snapshots: 600
nova:
metadata_items: 128
injected_file_content_bytes: 10240
ram: 655360
floating_ips: 50
key_pairs: 50
instances: 100
security_group_rules: 800
injected_files: 5
cores: 200
fixed_ips: 0
security_groups: 100
neutron:
security_group_rule: 800
subnet: 10
network: 10
floatingip: 50
security_group: 100
router: 10
port: 500
# Ordered list of quota sizes from smallest to biggest
QUOTA_SIZES_ASC:
- small
- medium
- large