Support quota for keypairs in Mogan(part-three)
Introduce the quota management in keypair creation and deletion. Change-Id: I8241b0bfb38a9c41b564df07a7799e4bc5438c0d Implements: blueprint quota-support
This commit is contained in:
parent
b58a2d26fc
commit
a7d6d054c1
@ -32,6 +32,12 @@ quota_opts = [
|
|||||||
cfg.IntOpt('max_age',
|
cfg.IntOpt('max_age',
|
||||||
default=0,
|
default=0,
|
||||||
help=_('Number of seconds between subsequent usage refreshes')),
|
help=_('Number of seconds between subsequent usage refreshes')),
|
||||||
|
cfg.IntOpt('servers_hard_limit',
|
||||||
|
default=10,
|
||||||
|
help=_('Number of servers quota hard limit.')),
|
||||||
|
cfg.IntOpt('keypairs_hard_limit',
|
||||||
|
default=100,
|
||||||
|
help=_('Number of keypairs quota hard limit.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
opt_quota_group = cfg.OptGroup(name='quota',
|
opt_quota_group = cfg.OptGroup(name='quota',
|
||||||
|
@ -180,6 +180,7 @@ def upgrade():
|
|||||||
sa.Column('id', sa.Integer(), primary_key=True, nullable=False),
|
sa.Column('id', sa.Integer(), primary_key=True, nullable=False),
|
||||||
sa.Column('name', sa.String(length=255), nullable=False),
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
sa.Column('user_id', sa.String(length=255), nullable=True),
|
sa.Column('user_id', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('project_id', sa.String(length=36), nullable=True),
|
||||||
sa.Column('fingerprint', sa.String(255)),
|
sa.Column('fingerprint', sa.String(255)),
|
||||||
sa.Column('public_key', sa.Text()),
|
sa.Column('public_key', sa.Text()),
|
||||||
sa.Column('type', sa.Enum('ssh', 'x509'), nullable=False,
|
sa.Column('type', sa.Enum('ssh', 'x509'), nullable=False,
|
||||||
|
@ -104,7 +104,8 @@ class Connection(api.Connection):
|
|||||||
"""SqlAlchemy connection."""
|
"""SqlAlchemy connection."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.QUOTA_SYNC_FUNCTIONS = {'_sync_servers': self._sync_servers}
|
self.QUOTA_SYNC_FUNCTIONS = {'_sync_servers': self._sync_servers,
|
||||||
|
'_sync_keypairs': self._sync_keypairs}
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _add_servers_filters(self, context, query, filters):
|
def _add_servers_filters(self, context, query, filters):
|
||||||
@ -515,6 +516,11 @@ class Connection(api.Connection):
|
|||||||
filter_by(project_id=project_id).all()
|
filter_by(project_id=project_id).all()
|
||||||
return {'servers': len(query) or 0}
|
return {'servers': len(query) or 0}
|
||||||
|
|
||||||
|
def _sync_keypairs(self, context, project_id):
|
||||||
|
query = model_query(context, models.KeyPair).\
|
||||||
|
filter_by(project_id=project_id).all()
|
||||||
|
return {'keypairs': len(query) or 0}
|
||||||
|
|
||||||
@oslo_db_api.retry_on_deadlock
|
@oslo_db_api.retry_on_deadlock
|
||||||
def quota_reserve(self, context, resources, quotas, deltas, expire,
|
def quota_reserve(self, context, resources, quotas, deltas, expire,
|
||||||
until_refresh, max_age, project_id,
|
until_refresh, max_age, project_id,
|
||||||
|
@ -260,6 +260,8 @@ class KeyPair(Base):
|
|||||||
|
|
||||||
user_id = Column(String(255), nullable=False)
|
user_id = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
project_id = Column(String(255), nullable=False)
|
||||||
|
|
||||||
fingerprint = Column(String(255))
|
fingerprint = Column(String(255))
|
||||||
public_key = Column(Text())
|
public_key = Column(Text())
|
||||||
type = Column(Enum('ssh', 'x509', name='keypair_types'),
|
type = Column(Enum('ssh', 'x509', name='keypair_types'),
|
||||||
|
@ -70,6 +70,7 @@ class API(object):
|
|||||||
self.network_api = network.API()
|
self.network_api = network.API()
|
||||||
self.quota = quota.Quota()
|
self.quota = quota.Quota()
|
||||||
self.quota.register_resource(objects.quota.ServerResource())
|
self.quota.register_resource(objects.quota.ServerResource())
|
||||||
|
self.quota.register_resource(objects.quota.KeyPairResource())
|
||||||
self.consoleauth_rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
|
self.consoleauth_rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
|
||||||
|
|
||||||
def _get_image(self, context, image_uuid):
|
def _get_image(self, context, image_uuid):
|
||||||
@ -182,6 +183,21 @@ class API(object):
|
|||||||
else:
|
else:
|
||||||
raise exception.OverQuota(overs='servers')
|
raise exception.OverQuota(overs='servers')
|
||||||
|
|
||||||
|
def _check_num_keypairs_quota(self, context, count):
|
||||||
|
keypair_resource = self.quota.resources['keypairs']
|
||||||
|
quotas = self.quota.get_quota_limit_and_usage(context,
|
||||||
|
{'keyparis':
|
||||||
|
keypair_resource},
|
||||||
|
context.tenant)
|
||||||
|
limit = quotas['keypairs']['limit']
|
||||||
|
in_use = quotas['keypairs']['in_use']
|
||||||
|
reserved = quotas['keypairs']['reserved']
|
||||||
|
available_quota = limit - in_use - reserved
|
||||||
|
if count <= available_quota:
|
||||||
|
return count
|
||||||
|
else:
|
||||||
|
raise exception.OverQuota(overs='keypairs')
|
||||||
|
|
||||||
def _decode_files(self, injected_files):
|
def _decode_files(self, injected_files):
|
||||||
"""Base64 decode the list of files to inject."""
|
"""Base64 decode the list of files to inject."""
|
||||||
if not injected_files:
|
if not injected_files:
|
||||||
@ -470,13 +486,21 @@ class API(object):
|
|||||||
self._validate_new_key_pair(context, user_id, key_name, key_type)
|
self._validate_new_key_pair(context, user_id, key_name, key_type)
|
||||||
private_key, public_key, fingerprint = self._generate_key_pair(
|
private_key, public_key, fingerprint = self._generate_key_pair(
|
||||||
user_id, key_type)
|
user_id, key_type)
|
||||||
|
# Create the keypair reservations
|
||||||
|
num_keypairs = self._check_num_keypairs_quota(context, 1)
|
||||||
|
reserve_opts = {'keypairs': num_keypairs}
|
||||||
|
reservations = self.quota.reserve(context, **reserve_opts)
|
||||||
keypair = objects.KeyPair(context)
|
keypair = objects.KeyPair(context)
|
||||||
keypair.user_id = user_id
|
keypair.user_id = user_id
|
||||||
keypair.name = key_name
|
keypair.name = key_name
|
||||||
keypair.type = key_type
|
keypair.type = key_type
|
||||||
keypair.fingerprint = fingerprint
|
keypair.fingerprint = fingerprint
|
||||||
keypair.public_key = public_key
|
keypair.public_key = public_key
|
||||||
|
keypair.project_id = context.tenant
|
||||||
keypair.create()
|
keypair.create()
|
||||||
|
# Commit keypairs reservations
|
||||||
|
if reservations:
|
||||||
|
self.quota.commit(context, reservations)
|
||||||
return keypair, private_key
|
return keypair, private_key
|
||||||
|
|
||||||
def _generate_fingerprint(self, public_key, key_type):
|
def _generate_fingerprint(self, public_key, key_type):
|
||||||
@ -494,6 +518,10 @@ class API(object):
|
|||||||
def delete_key_pair(self, context, user_id, key_name):
|
def delete_key_pair(self, context, user_id, key_name):
|
||||||
"""Delete a keypair by name."""
|
"""Delete a keypair by name."""
|
||||||
objects.KeyPair.destroy_by_name(context, user_id, key_name)
|
objects.KeyPair.destroy_by_name(context, user_id, key_name)
|
||||||
|
reserve_opts = {'keypairs': -1}
|
||||||
|
reservations = self.quota.reserve(context, **reserve_opts)
|
||||||
|
if reservations:
|
||||||
|
self.quota.commit(context, reservations)
|
||||||
|
|
||||||
def get_key_pairs(self, context, user_id):
|
def get_key_pairs(self, context, user_id):
|
||||||
"""List key pairs."""
|
"""List key pairs."""
|
||||||
|
@ -36,6 +36,7 @@ class KeyPair(base.MoganObject):
|
|||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.StringField(nullable=False),
|
'name': fields.StringField(nullable=False),
|
||||||
'user_id': fields.StringField(nullable=True),
|
'user_id': fields.StringField(nullable=True),
|
||||||
|
'project_id': fields.StringField(nullable=True),
|
||||||
'fingerprint': fields.StringField(nullable=True),
|
'fingerprint': fields.StringField(nullable=True),
|
||||||
'public_key': fields.StringField(nullable=True),
|
'public_key': fields.StringField(nullable=True),
|
||||||
'type': fields.StringField(nullable=False),
|
'type': fields.StringField(nullable=False),
|
||||||
|
@ -185,11 +185,18 @@ class DbQuotaDriver(object):
|
|||||||
for p_quota in res:
|
for p_quota in res:
|
||||||
project_quotas[p_quota.resource_name] = p_quota.hard_limit
|
project_quotas[p_quota.resource_name] = p_quota.hard_limit
|
||||||
if project_quotas == {}:
|
if project_quotas == {}:
|
||||||
|
servers_hard_limit = CONF.quota.servers_hard_limit
|
||||||
self.dbapi.quota_create(context, {'resource_name': 'servers',
|
self.dbapi.quota_create(context, {'resource_name': 'servers',
|
||||||
'project_id': project_id,
|
'project_id': project_id,
|
||||||
'hard_limit': 10,
|
'hard_limit': servers_hard_limit,
|
||||||
'allocated': 0})
|
'allocated': 0})
|
||||||
project_quotas['servers'] = 10
|
project_quotas['servers'] = servers_hard_limit
|
||||||
|
kpairs_hard_limit = CONF.quota.keypairs_hard_limit
|
||||||
|
self.dbapi.quota_create(context, {'resource_name': 'keypairs',
|
||||||
|
'project_id': project_id,
|
||||||
|
'hard_limit': kpairs_hard_limit,
|
||||||
|
'allocated': 0})
|
||||||
|
project_quotas['keypairs'] = kpairs_hard_limit
|
||||||
allocated_quotas = None
|
allocated_quotas = None
|
||||||
if usages:
|
if usages:
|
||||||
project_usages = self.dbapi.quota_usage_get_all_by_project(
|
project_usages = self.dbapi.quota_usage_get_all_by_project(
|
||||||
@ -410,9 +417,22 @@ class BaseResource(object):
|
|||||||
class ServerResource(BaseResource):
|
class ServerResource(BaseResource):
|
||||||
"""ReservableResource for a specific server."""
|
"""ReservableResource for a specific server."""
|
||||||
|
|
||||||
def __init__(self, name='servers'):
|
def __init__(self):
|
||||||
"""Initializes a ServerResource.
|
"""Initializes a ServerResource.
|
||||||
|
|
||||||
:param name: The kind of resource, i.e., "servers".
|
|
||||||
"""
|
"""
|
||||||
super(ServerResource, self).__init__(name, "_sync_%s" % name)
|
self.name = 'servers'
|
||||||
|
super(ServerResource, self).__init__(self.name,
|
||||||
|
"_sync_%s" % self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyPairResource(BaseResource):
|
||||||
|
"""ReservableResource for a specific keypair."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initializes a KeyPairResource.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.name = 'keypairs'
|
||||||
|
super(KeyPairResource, self).__init__(self.name,
|
||||||
|
"_sync_%s" % self.name)
|
||||||
|
@ -365,3 +365,33 @@ class ComputeAPIUnitTest(base.DbTestCase):
|
|||||||
self.engine_api.detach_interface(self.context, fake_server_obj,
|
self.engine_api.detach_interface(self.context, fake_server_obj,
|
||||||
fake_server_obj['nics'][0]['port_id'])
|
fake_server_obj['nics'][0]['port_id'])
|
||||||
self.assertTrue(mock_detach_interface.called)
|
self.assertTrue(mock_detach_interface.called)
|
||||||
|
|
||||||
|
def test_create_key_pairs_with_quota(self):
|
||||||
|
res = self.dbapi._get_quota_usages(self.context, self.project_id)
|
||||||
|
before_in_use = 0
|
||||||
|
if res.get('keypairs') is not None:
|
||||||
|
before_in_use = res.get('keypairs').in_use
|
||||||
|
self.engine_api.create_key_pair(self.context, self.user_id,
|
||||||
|
'test_keypair')
|
||||||
|
res = self.dbapi._get_quota_usages(self.context, self.project_id)
|
||||||
|
after_in_use = res.get('keypairs').in_use
|
||||||
|
self.assertEqual(before_in_use + 1, after_in_use)
|
||||||
|
|
||||||
|
def test_create_key_pairs_with_over_quota_limit(self):
|
||||||
|
res = self.dbapi._get_quota_usages(self.context, self.project_id)
|
||||||
|
before_in_use = 0
|
||||||
|
if res.get('keypairs') is not None:
|
||||||
|
before_in_use = res.get('keypairs').in_use
|
||||||
|
for i in range(1, 101):
|
||||||
|
key_name = 'test_keypair_%s' % str(i)
|
||||||
|
self.engine_api.create_key_pair(self.context, self.user_id,
|
||||||
|
key_name)
|
||||||
|
res = self.dbapi._get_quota_usages(self.context, self.project_id)
|
||||||
|
after_in_use = res.get('keypairs').in_use
|
||||||
|
self.assertEqual(before_in_use + 100, after_in_use)
|
||||||
|
self.assertRaises(
|
||||||
|
exception.OverQuota,
|
||||||
|
self.engine_api.create_key_pair,
|
||||||
|
self.context,
|
||||||
|
self.user_id,
|
||||||
|
'test_keypair')
|
||||||
|
@ -28,6 +28,7 @@ class TestKeyPairObject(base.DbTestCase):
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"public_key": "fake-publick-key",
|
"public_key": "fake-publick-key",
|
||||||
"user_id": "e78b60069fc9467e97fb4b74de9cadc1",
|
"user_id": "e78b60069fc9467e97fb4b74de9cadc1",
|
||||||
|
"project_id": "c18e8a1a870d4c08a0b51ced6e0b6459",
|
||||||
"name": "test_key",
|
"name": "test_key",
|
||||||
"fingerprint": "f1:83:34:02:f9:63:79:d4:bd:2a:1d:50:16:61:1b:cc",
|
"fingerprint": "f1:83:34:02:f9:63:79:d4:bd:2a:1d:50:16:61:1b:cc",
|
||||||
"type": "ssh",
|
"type": "ssh",
|
||||||
|
@ -390,7 +390,7 @@ expected_object_fingerprints = {
|
|||||||
'ServerNic': '1.0-0494306157ef437802260ff8b51cf5cf',
|
'ServerNic': '1.0-0494306157ef437802260ff8b51cf5cf',
|
||||||
'ServerNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
'ServerNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
||||||
'Quota': '1.0-c8caa082f4d726cb63fdc5943f7cd186',
|
'Quota': '1.0-c8caa082f4d726cb63fdc5943f7cd186',
|
||||||
'KeyPair': '1.0-c6820166e307676c5900f7801831b84c',
|
'KeyPair': '1.0-1a1ea1f9b4d03503f5c13b52d1432fa9',
|
||||||
'KeyPairList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
'KeyPairList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
||||||
'Aggregate': '1.0-e2f3060705a18e1de8daae3f1af6b104',
|
'Aggregate': '1.0-e2f3060705a18e1de8daae3f1af6b104',
|
||||||
'AggregateList': '1.0-33a2e1bb91ad4082f9f63429b77c1244'
|
'AggregateList': '1.0-33a2e1bb91ad4082f9f63429b77c1244'
|
||||||
|
Loading…
Reference in New Issue
Block a user