merged quotas
This commit is contained in:
@@ -304,7 +304,10 @@ class APIRequestHandler(tornado.web.RequestHandler):
|
|||||||
try:
|
try:
|
||||||
failure.raiseException()
|
failure.raiseException()
|
||||||
except exception.ApiError as ex:
|
except exception.ApiError as ex:
|
||||||
self._error(type(ex).__name__ + "." + ex.code, ex.message)
|
if ex.code:
|
||||||
|
self._error(ex.code, ex.message)
|
||||||
|
else:
|
||||||
|
self._error(type(ex).__name__, ex.message)
|
||||||
# TODO(vish): do something more useful with unknown exceptions
|
# TODO(vish): do something more useful with unknown exceptions
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._error(type(ex).__name__, str(ex))
|
self._error(type(ex).__name__, str(ex))
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from twisted.internet import defer
|
|||||||
from nova import db
|
from nova import db
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import flags
|
from nova import flags
|
||||||
|
from nova import quota
|
||||||
from nova import rpc
|
from nova import rpc
|
||||||
from nova import utils
|
from nova import utils
|
||||||
from nova.auth import rbac
|
from nova.auth import rbac
|
||||||
@@ -44,6 +45,11 @@ FLAGS = flags.FLAGS
|
|||||||
flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
|
flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaError(exception.ApiError):
|
||||||
|
"""Quota Exceeeded"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _gen_key(user_id, key_name):
|
def _gen_key(user_id, key_name):
|
||||||
""" Tuck this into AuthManager """
|
""" Tuck this into AuthManager """
|
||||||
try:
|
try:
|
||||||
@@ -277,6 +283,14 @@ class CloudController(object):
|
|||||||
|
|
||||||
@rbac.allow('projectmanager', 'sysadmin')
|
@rbac.allow('projectmanager', 'sysadmin')
|
||||||
def create_volume(self, context, size, **kwargs):
|
def create_volume(self, context, size, **kwargs):
|
||||||
|
# check quota
|
||||||
|
size = int(size)
|
||||||
|
if quota.allowed_volumes(context, 1, size) < 1:
|
||||||
|
logging.warn("Quota exceeeded for %s, tried to create %sG volume",
|
||||||
|
context.project.id, size)
|
||||||
|
raise QuotaError("Volume quota exceeded. You cannot "
|
||||||
|
"create a volume of size %s" %
|
||||||
|
size)
|
||||||
vol = {}
|
vol = {}
|
||||||
vol['size'] = size
|
vol['size'] = size
|
||||||
vol['user_id'] = context.user.id
|
vol['user_id'] = context.user.id
|
||||||
@@ -441,6 +455,12 @@ class CloudController(object):
|
|||||||
@rbac.allow('netadmin')
|
@rbac.allow('netadmin')
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def allocate_address(self, context, **kwargs):
|
def allocate_address(self, context, **kwargs):
|
||||||
|
# check quota
|
||||||
|
if quota.allowed_floating_ips(context, 1) < 1:
|
||||||
|
logging.warn("Quota exceeeded for %s, tried to allocate address",
|
||||||
|
context.project.id)
|
||||||
|
raise QuotaError("Address quota exceeded. You cannot "
|
||||||
|
"allocate any more addresses")
|
||||||
network_topic = yield self._get_network_topic(context)
|
network_topic = yield self._get_network_topic(context)
|
||||||
public_ip = yield rpc.call(network_topic,
|
public_ip = yield rpc.call(network_topic,
|
||||||
{"method": "allocate_floating_ip",
|
{"method": "allocate_floating_ip",
|
||||||
@@ -500,6 +520,22 @@ class CloudController(object):
|
|||||||
@rbac.allow('projectmanager', 'sysadmin')
|
@rbac.allow('projectmanager', 'sysadmin')
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def run_instances(self, context, **kwargs):
|
def run_instances(self, context, **kwargs):
|
||||||
|
instance_type = kwargs.get('instance_type', 'm1.small')
|
||||||
|
if instance_type not in INSTANCE_TYPES:
|
||||||
|
raise exception.ApiError("Unknown instance type: %s",
|
||||||
|
instance_type)
|
||||||
|
# check quota
|
||||||
|
max_instances = int(kwargs.get('max_count', 1))
|
||||||
|
min_instances = int(kwargs.get('min_count', max_instances))
|
||||||
|
num_instances = quota.allowed_instances(context,
|
||||||
|
max_instances,
|
||||||
|
instance_type)
|
||||||
|
if num_instances < min_instances:
|
||||||
|
logging.warn("Quota exceeeded for %s, tried to run %s instances",
|
||||||
|
context.project.id, min_instances)
|
||||||
|
raise QuotaError("Instance quota exceeded. You can only "
|
||||||
|
"run %s more instances of this type." %
|
||||||
|
num_instances, "InstanceLimitExceeded")
|
||||||
# make sure user can access the image
|
# make sure user can access the image
|
||||||
# vpn image is private so it doesn't show up on lists
|
# vpn image is private so it doesn't show up on lists
|
||||||
vpn = kwargs['image_id'] == FLAGS.vpn_image_id
|
vpn = kwargs['image_id'] == FLAGS.vpn_image_id
|
||||||
@@ -521,7 +557,7 @@ class CloudController(object):
|
|||||||
images.get(context, kernel_id)
|
images.get(context, kernel_id)
|
||||||
images.get(context, ramdisk_id)
|
images.get(context, ramdisk_id)
|
||||||
|
|
||||||
logging.debug("Going to run instances...")
|
logging.debug("Going to run %s instances...", num_instances)
|
||||||
launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||||
key_data = None
|
key_data = None
|
||||||
if kwargs.has_key('key_name'):
|
if kwargs.has_key('key_name'):
|
||||||
@@ -546,10 +582,15 @@ class CloudController(object):
|
|||||||
base_options['user_id'] = context.user.id
|
base_options['user_id'] = context.user.id
|
||||||
base_options['project_id'] = context.project.id
|
base_options['project_id'] = context.project.id
|
||||||
base_options['user_data'] = kwargs.get('user_data', '')
|
base_options['user_data'] = kwargs.get('user_data', '')
|
||||||
base_options['instance_type'] = kwargs.get('instance_type', 'm1.small')
|
|
||||||
base_options['security_group'] = security_group
|
base_options['security_group'] = security_group
|
||||||
|
base_options['instance_type'] = instance_type
|
||||||
|
|
||||||
for num in range(int(kwargs['max_count'])):
|
type_data = INSTANCE_TYPES[instance_type]
|
||||||
|
base_options['memory_mb'] = type_data['memory_mb']
|
||||||
|
base_options['vcpus'] = type_data['vcpus']
|
||||||
|
base_options['local_gb'] = type_data['local_gb']
|
||||||
|
|
||||||
|
for num in range(num_instances):
|
||||||
instance_ref = db.instance_create(context, base_options)
|
instance_ref = db.instance_create(context, base_options)
|
||||||
inst_id = instance_ref['id']
|
inst_id = instance_ref['id']
|
||||||
|
|
||||||
|
|||||||
91
nova/quota.py
Normal file
91
nova/quota.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Quotas for instances, volumes, and floating ips
|
||||||
|
"""
|
||||||
|
|
||||||
|
from nova import db
|
||||||
|
from nova import exception
|
||||||
|
from nova import flags
|
||||||
|
from nova.compute import instance_types
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
|
||||||
|
flags.DEFINE_integer('quota_instances', 10,
|
||||||
|
'number of instances allowed per project')
|
||||||
|
flags.DEFINE_integer('quota_cores', 20,
|
||||||
|
'number of instance cores allowed per project')
|
||||||
|
flags.DEFINE_integer('quota_volumes', 10,
|
||||||
|
'number of volumes allowed per project')
|
||||||
|
flags.DEFINE_integer('quota_gigabytes', 1000,
|
||||||
|
'number of volume gigabytes allowed per project')
|
||||||
|
flags.DEFINE_integer('quota_floating_ips', 10,
|
||||||
|
'number of floating ips allowed per project')
|
||||||
|
|
||||||
|
def _get_quota(context, project_id):
|
||||||
|
rval = {'instances': FLAGS.quota_instances,
|
||||||
|
'cores': FLAGS.quota_cores,
|
||||||
|
'volumes': FLAGS.quota_volumes,
|
||||||
|
'gigabytes': FLAGS.quota_gigabytes,
|
||||||
|
'floating_ips': FLAGS.quota_floating_ips}
|
||||||
|
try:
|
||||||
|
quota = db.quota_get(context, project_id)
|
||||||
|
for key in rval.keys():
|
||||||
|
if quota[key] is not None:
|
||||||
|
rval[key] = quota[key]
|
||||||
|
except exception.NotFound:
|
||||||
|
pass
|
||||||
|
return rval
|
||||||
|
|
||||||
|
def allowed_instances(context, num_instances, instance_type):
|
||||||
|
"""Check quota and return min(num_instances, allowed_instances)"""
|
||||||
|
project_id = context.project.id
|
||||||
|
used_instances, used_cores = db.instance_data_get_for_project(context,
|
||||||
|
project_id)
|
||||||
|
quota = _get_quota(context, project_id)
|
||||||
|
allowed_instances = quota['instances'] - used_instances
|
||||||
|
allowed_cores = quota['cores'] - used_cores
|
||||||
|
type_cores = instance_types.INSTANCE_TYPES[instance_type]['vcpus']
|
||||||
|
num_cores = num_instances * type_cores
|
||||||
|
allowed_instances = min(allowed_instances,
|
||||||
|
int(allowed_cores // type_cores))
|
||||||
|
return min(num_instances, allowed_instances)
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_volumes(context, num_volumes, size):
|
||||||
|
"""Check quota and return min(num_volumes, allowed_volumes)"""
|
||||||
|
project_id = context.project.id
|
||||||
|
used_volumes, used_gigabytes = db.volume_data_get_for_project(context,
|
||||||
|
project_id)
|
||||||
|
quota = _get_quota(context, project_id)
|
||||||
|
allowed_volumes = quota['volumes'] - used_volumes
|
||||||
|
allowed_gigabytes = quota['gigabytes'] - used_gigabytes
|
||||||
|
num_gigabytes = num_volumes * size
|
||||||
|
allowed_volumes = min(allowed_volumes,
|
||||||
|
int(allowed_gigabytes // size))
|
||||||
|
return min(num_volumes, allowed_volumes)
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_floating_ips(context, num_floating_ips):
|
||||||
|
"""Check quota and return min(num_floating_ips, allowed_floating_ips)"""
|
||||||
|
project_id = context.project.id
|
||||||
|
used_floating_ips = db.floating_ip_count_by_project(context, project_id)
|
||||||
|
quota = _get_quota(context, project_id)
|
||||||
|
allowed_floating_ips = quota['floating_ips'] - used_floating_ips
|
||||||
|
return min(num_floating_ips, allowed_floating_ips)
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ class ComputeTestCase(test.TrialTestCase):
|
|||||||
def tearDown(self): # pylint: disable-msg=C0103
|
def tearDown(self): # pylint: disable-msg=C0103
|
||||||
self.manager.delete_user(self.user)
|
self.manager.delete_user(self.user)
|
||||||
self.manager.delete_project(self.project)
|
self.manager.delete_project(self.project)
|
||||||
|
super(ComputeTestCase, self).tearDown()
|
||||||
|
|
||||||
def _create_instance(self):
|
def _create_instance(self):
|
||||||
"""Create a test instance"""
|
"""Create a test instance"""
|
||||||
|
|||||||
155
nova/tests/quota_unittest.py
Normal file
155
nova/tests/quota_unittest.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
from nova import db
|
||||||
|
from nova import exception
|
||||||
|
from nova import flags
|
||||||
|
from nova import quota
|
||||||
|
from nova import test
|
||||||
|
from nova import utils
|
||||||
|
from nova.auth import manager
|
||||||
|
from nova.endpoint import cloud
|
||||||
|
from nova.endpoint import api
|
||||||
|
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaTestCase(test.TrialTestCase):
|
||||||
|
def setUp(self): # pylint: disable-msg=C0103
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
super(QuotaTestCase, self).setUp()
|
||||||
|
self.flags(connection_type='fake',
|
||||||
|
quota_instances=2,
|
||||||
|
quota_cores=4,
|
||||||
|
quota_volumes=2,
|
||||||
|
quota_gigabytes=20,
|
||||||
|
quota_floating_ips=1)
|
||||||
|
|
||||||
|
self.cloud = cloud.CloudController()
|
||||||
|
self.manager = manager.AuthManager()
|
||||||
|
self.user = self.manager.create_user('admin', 'admin', 'admin', True)
|
||||||
|
self.project = self.manager.create_project('admin', 'admin', 'admin')
|
||||||
|
self.network = utils.import_object(FLAGS.network_manager)
|
||||||
|
self.context = api.APIRequestContext(handler=None,
|
||||||
|
project=self.project,
|
||||||
|
user=self.user)
|
||||||
|
|
||||||
|
def tearDown(self): # pylint: disable-msg=C0103
|
||||||
|
manager.AuthManager().delete_project(self.project)
|
||||||
|
manager.AuthManager().delete_user(self.user)
|
||||||
|
super(QuotaTestCase, self).tearDown()
|
||||||
|
|
||||||
|
def _create_instance(self, cores=2):
|
||||||
|
"""Create a test instance"""
|
||||||
|
inst = {}
|
||||||
|
inst['image_id'] = 'ami-test'
|
||||||
|
inst['reservation_id'] = 'r-fakeres'
|
||||||
|
inst['user_id'] = self.user.id
|
||||||
|
inst['project_id'] = self.project.id
|
||||||
|
inst['instance_type'] = 'm1.large'
|
||||||
|
inst['vcpus'] = cores
|
||||||
|
inst['mac_address'] = utils.generate_mac()
|
||||||
|
return db.instance_create(self.context, inst)['id']
|
||||||
|
|
||||||
|
def _create_volume(self, size=10):
|
||||||
|
"""Create a test volume"""
|
||||||
|
vol = {}
|
||||||
|
vol['user_id'] = self.user.id
|
||||||
|
vol['project_id'] = self.project.id
|
||||||
|
vol['size'] = size
|
||||||
|
return db.volume_create(self.context, vol)['id']
|
||||||
|
|
||||||
|
def test_quota_overrides(self):
|
||||||
|
"""Make sure overriding a projects quotas works"""
|
||||||
|
num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
|
||||||
|
self.assertEqual(num_instances, 2)
|
||||||
|
db.quota_create(self.context, {'project_id': self.project.id,
|
||||||
|
'instances': 10})
|
||||||
|
num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
|
||||||
|
self.assertEqual(num_instances, 4)
|
||||||
|
db.quota_update(self.context, self.project.id, {'cores': 100})
|
||||||
|
num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
|
||||||
|
self.assertEqual(num_instances, 10)
|
||||||
|
db.quota_destroy(self.context, self.project.id)
|
||||||
|
|
||||||
|
def test_too_many_instances(self):
|
||||||
|
instance_ids = []
|
||||||
|
for i in range(FLAGS.quota_instances):
|
||||||
|
instance_id = self._create_instance()
|
||||||
|
instance_ids.append(instance_id)
|
||||||
|
self.assertFailure(self.cloud.run_instances(self.context,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
instance_type='m1.small'),
|
||||||
|
cloud.QuotaError)
|
||||||
|
for instance_id in instance_ids:
|
||||||
|
db.instance_destroy(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_too_many_cores(self):
|
||||||
|
instance_ids = []
|
||||||
|
instance_id = self._create_instance(cores=4)
|
||||||
|
instance_ids.append(instance_id)
|
||||||
|
self.assertFailure(self.cloud.run_instances(self.context,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
instance_type='m1.small'),
|
||||||
|
cloud.QuotaError)
|
||||||
|
for instance_id in instance_ids:
|
||||||
|
db.instance_destroy(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_too_many_volumes(self):
|
||||||
|
volume_ids = []
|
||||||
|
for i in range(FLAGS.quota_volumes):
|
||||||
|
volume_id = self._create_volume()
|
||||||
|
volume_ids.append(volume_id)
|
||||||
|
self.assertRaises(cloud.QuotaError,
|
||||||
|
self.cloud.create_volume,
|
||||||
|
self.context,
|
||||||
|
size=10)
|
||||||
|
for volume_id in volume_ids:
|
||||||
|
db.volume_destroy(self.context, volume_id)
|
||||||
|
|
||||||
|
def test_too_many_gigabytes(self):
|
||||||
|
volume_ids = []
|
||||||
|
volume_id = self._create_volume(size=20)
|
||||||
|
volume_ids.append(volume_id)
|
||||||
|
self.assertRaises(cloud.QuotaError,
|
||||||
|
self.cloud.create_volume,
|
||||||
|
self.context,
|
||||||
|
size=10)
|
||||||
|
for volume_id in volume_ids:
|
||||||
|
db.volume_destroy(self.context, volume_id)
|
||||||
|
|
||||||
|
def test_too_many_addresses(self):
|
||||||
|
address = '192.168.0.100'
|
||||||
|
try:
|
||||||
|
db.floating_ip_get_by_address(None, address)
|
||||||
|
except exception.NotFound:
|
||||||
|
db.floating_ip_create(None, {'address': address,
|
||||||
|
'host': FLAGS.host})
|
||||||
|
float_addr = self.network.allocate_floating_ip(self.context,
|
||||||
|
self.project.id)
|
||||||
|
# NOTE(vish): This assert never fails. When cloud attempts to
|
||||||
|
# make an rpc.call, the test just finishes with OK. It
|
||||||
|
# appears to be something in the magic inline callbacks
|
||||||
|
# that is breaking.
|
||||||
|
self.assertFailure(self.cloud.allocate_address(self.context),
|
||||||
|
cloud.QuotaError)
|
||||||
@@ -58,6 +58,7 @@ from nova.tests.flags_unittest import *
|
|||||||
from nova.tests.network_unittest import *
|
from nova.tests.network_unittest import *
|
||||||
from nova.tests.objectstore_unittest import *
|
from nova.tests.objectstore_unittest import *
|
||||||
from nova.tests.process_unittest import *
|
from nova.tests.process_unittest import *
|
||||||
|
from nova.tests.quota_unittest import *
|
||||||
from nova.tests.rpc_unittest import *
|
from nova.tests.rpc_unittest import *
|
||||||
from nova.tests.scheduler_unittest import *
|
from nova.tests.scheduler_unittest import *
|
||||||
from nova.tests.service_unittest import *
|
from nova.tests.service_unittest import *
|
||||||
|
|||||||
Reference in New Issue
Block a user