Introduce common class for QuotaSet
Multiple services are using QuotaSet resource in the same style. Add a common implementation for it. Change-Id: Ic5523e48a9e284744945b02b0f0b3eaf049133d0
This commit is contained in:
parent
21c3c2d6ec
commit
94cb864d86
129
openstack/common/quota_set.py
Normal file
129
openstack/common/quota_set.py
Normal file
@ -0,0 +1,129 @@
|
||||
# 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 openstack import exceptions
|
||||
from openstack import resource
|
||||
|
||||
|
||||
# ATTENTION: Please do not inherit this class for anything else then QuotaSet,
|
||||
# since attribute processing is here very different!
|
||||
class QuotaSet(resource.Resource):
|
||||
resource_key = 'quota_set'
|
||||
# ATTENTION: different services might be using different base_path
|
||||
base_path = '/os-quota-sets/%(project_id)s'
|
||||
|
||||
# capabilities
|
||||
allow_create = True
|
||||
allow_fetch = True
|
||||
allow_delete = True
|
||||
allow_commit = True
|
||||
|
||||
_query_mapping = resource.QueryParameters(
|
||||
"usage")
|
||||
|
||||
# NOTE(gtema) Sadly this attribute is useless in all the methods, but keep
|
||||
# it here extra as a reminder
|
||||
requires_id = False
|
||||
|
||||
# Quota-sets are not very well designed. We must keep what is
|
||||
# there and try to process it on best effort
|
||||
_allow_unknown_attrs_in_body = True
|
||||
|
||||
#: Properties
|
||||
#: Current reservations
|
||||
#: *type:dict*
|
||||
reservation = resource.Body('reservation', type=dict)
|
||||
#: Quota usage
|
||||
#: *type:dict*
|
||||
usage = resource.Body('usage', type=dict)
|
||||
|
||||
project_id = resource.URI('project_id')
|
||||
|
||||
def fetch(self, session, requires_id=False,
|
||||
base_path=None, error_message=None, **params):
|
||||
return super(QuotaSet, self).fetch(
|
||||
session,
|
||||
requires_id=False,
|
||||
base_path=base_path,
|
||||
error_message=error_message,
|
||||
**params
|
||||
)
|
||||
|
||||
def _translate_response(self, response, has_body=None, error_message=None):
|
||||
"""Given a KSA response, inflate this instance with its data
|
||||
|
||||
DELETE operations don't return a body, so only try to work
|
||||
with a body when has_body is True.
|
||||
|
||||
This method updates attributes that correspond to headers
|
||||
and body on this instance and clears the dirty set.
|
||||
"""
|
||||
if has_body is None:
|
||||
has_body = self.has_body
|
||||
exceptions.raise_from_response(response, error_message=error_message)
|
||||
if has_body:
|
||||
try:
|
||||
body = response.json()
|
||||
if self.resource_key and self.resource_key in body:
|
||||
body = body[self.resource_key]
|
||||
|
||||
# Do not allow keys called "self" through. Glance chose
|
||||
# to name a key "self", so we need to pop it out because
|
||||
# we can't send it through cls.existing and into the
|
||||
# Resource initializer. "self" is already the first
|
||||
# argument and is practically a reserved word.
|
||||
body.pop("self", None)
|
||||
|
||||
# Process body_attrs to strip usage and reservation out
|
||||
normalized_attrs = dict(
|
||||
reservation={},
|
||||
usage={},
|
||||
)
|
||||
|
||||
for key, val in body.items():
|
||||
if isinstance(val, dict):
|
||||
if 'in_use' in val:
|
||||
normalized_attrs['usage'][key] = val['in_use']
|
||||
if 'reserved' in val:
|
||||
normalized_attrs['reservation'][key] = \
|
||||
val['reserved']
|
||||
if 'limit' in val:
|
||||
normalized_attrs[key] = val['limit']
|
||||
else:
|
||||
normalized_attrs[key] = val
|
||||
|
||||
self._unknown_attrs_in_body.update(normalized_attrs)
|
||||
|
||||
self._body.attributes.update(normalized_attrs)
|
||||
self._body.clean()
|
||||
if self.commit_jsonpatch or self.allow_patch:
|
||||
# We need the original body to compare against
|
||||
self._original_body = normalized_attrs.copy()
|
||||
except ValueError:
|
||||
# Server returned not parsable response (202, 204, etc)
|
||||
# Do simply nothing
|
||||
pass
|
||||
|
||||
headers = self._consume_header_attrs(response.headers)
|
||||
self._header.attributes.update(headers)
|
||||
self._header.clean()
|
||||
self._update_location()
|
||||
dict.update(self, self.to_dict())
|
||||
|
||||
def _prepare_request_body(self, patch, prepend_key):
|
||||
body = self._body.dirty
|
||||
# Ensure we never try to send meta props reservation and usage
|
||||
body.pop('reservation', None)
|
||||
body.pop('usage', None)
|
||||
|
||||
if prepend_key and self.resource_key is not None:
|
||||
body = {self.resource_key: body}
|
||||
return body
|
173
openstack/tests/unit/common/test_quota_set.py
Normal file
173
openstack/tests/unit/common/test_quota_set.py
Normal file
@ -0,0 +1,173 @@
|
||||
# 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 copy
|
||||
from unittest import mock
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
|
||||
from openstack.common import quota_set as _qs
|
||||
from openstack.tests.unit import base
|
||||
|
||||
|
||||
BASIC_EXAMPLE = {
|
||||
"backup_gigabytes": 1000,
|
||||
"backups": 10,
|
||||
"gigabytes___DEFAULT__": -1,
|
||||
}
|
||||
|
||||
USAGE_EXAMPLE = {
|
||||
"backup_gigabytes": {
|
||||
"in_use": 0,
|
||||
"limit": 1000,
|
||||
"reserved": 0
|
||||
},
|
||||
"backups": {
|
||||
"in_use": 0,
|
||||
"limit": 10,
|
||||
"reserved": 0
|
||||
},
|
||||
"gigabytes___DEFAULT__": {
|
||||
"in_use": 0,
|
||||
"limit": -1,
|
||||
"reserved": 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestQuotaSet(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestQuotaSet, self).setUp()
|
||||
self.sess = mock.Mock(spec=adapter.Adapter)
|
||||
self.sess.default_microversion = 1
|
||||
self.sess._get_connection = mock.Mock(return_value=self.cloud)
|
||||
self.sess.retriable_status_codes = set()
|
||||
|
||||
def test_basic(self):
|
||||
sot = _qs.QuotaSet()
|
||||
self.assertEqual('quota_set', sot.resource_key)
|
||||
self.assertIsNone(sot.resources_key)
|
||||
self.assertEqual('/os-quota-sets/%(project_id)s', sot.base_path)
|
||||
self.assertTrue(sot.allow_create)
|
||||
self.assertTrue(sot.allow_fetch)
|
||||
self.assertTrue(sot.allow_delete)
|
||||
self.assertFalse(sot.allow_list)
|
||||
self.assertTrue(sot.allow_commit)
|
||||
|
||||
self.assertDictEqual(
|
||||
{"usage": "usage",
|
||||
"limit": "limit",
|
||||
"marker": "marker"},
|
||||
sot._query_mapping._mapping)
|
||||
|
||||
def test_make_basic(self):
|
||||
sot = _qs.QuotaSet(**BASIC_EXAMPLE)
|
||||
|
||||
self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups)
|
||||
|
||||
def test_get(self):
|
||||
sot = _qs.QuotaSet(project_id='proj')
|
||||
|
||||
resp = mock.Mock()
|
||||
resp.body = {'quota_set': copy.deepcopy(BASIC_EXAMPLE)}
|
||||
resp.json = mock.Mock(return_value=resp.body)
|
||||
resp.status_code = 200
|
||||
resp.headers = {}
|
||||
self.sess.get = mock.Mock(return_value=resp)
|
||||
|
||||
sot.fetch(self.sess)
|
||||
|
||||
self.sess.get.assert_called_with(
|
||||
'/os-quota-sets/proj',
|
||||
microversion=1,
|
||||
params={})
|
||||
|
||||
self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups)
|
||||
self.assertEqual({}, sot.reservation)
|
||||
self.assertEqual({}, sot.usage)
|
||||
|
||||
def test_get_usage(self):
|
||||
sot = _qs.QuotaSet(project_id='proj')
|
||||
|
||||
resp = mock.Mock()
|
||||
resp.body = {'quota_set': copy.deepcopy(USAGE_EXAMPLE)}
|
||||
resp.json = mock.Mock(return_value=resp.body)
|
||||
resp.status_code = 200
|
||||
resp.headers = {}
|
||||
self.sess.get = mock.Mock(return_value=resp)
|
||||
|
||||
sot.fetch(self.sess, usage=True)
|
||||
|
||||
self.sess.get.assert_called_with(
|
||||
'/os-quota-sets/proj',
|
||||
microversion=1,
|
||||
params={'usage': True})
|
||||
|
||||
self.assertEqual(
|
||||
USAGE_EXAMPLE['backups']['limit'],
|
||||
sot.backups)
|
||||
|
||||
def test_update_quota(self):
|
||||
# Use QuotaSet as if it was returned by get(usage=True)
|
||||
sot = _qs.QuotaSet.existing(
|
||||
project_id='proj',
|
||||
reservation={'a': 'b'},
|
||||
usage={'c': 'd'},
|
||||
foo='bar')
|
||||
|
||||
resp = mock.Mock()
|
||||
resp.body = {'quota_set': copy.deepcopy(BASIC_EXAMPLE)}
|
||||
resp.json = mock.Mock(return_value=resp.body)
|
||||
resp.status_code = 200
|
||||
resp.headers = {}
|
||||
self.sess.put = mock.Mock(return_value=resp)
|
||||
|
||||
sot._update(
|
||||
reservation={'b': 'd'},
|
||||
backups=15,
|
||||
something_else=20)
|
||||
|
||||
sot.commit(self.sess)
|
||||
|
||||
self.sess.put.assert_called_with(
|
||||
'/os-quota-sets/proj',
|
||||
microversion=1,
|
||||
headers={},
|
||||
json={
|
||||
'quota_set': {
|
||||
'backups': 15,
|
||||
'something_else': 20
|
||||
}
|
||||
})
|
||||
|
||||
def test_delete_quota(self):
|
||||
# Use QuotaSet as if it was returned by get(usage=True)
|
||||
sot = _qs.QuotaSet.existing(
|
||||
project_id='proj',
|
||||
reservation={'a': 'b'},
|
||||
usage={'c': 'd'},
|
||||
foo='bar')
|
||||
|
||||
resp = mock.Mock()
|
||||
resp.body = None
|
||||
resp.json = mock.Mock(return_value=resp.body)
|
||||
resp.status_code = 200
|
||||
resp.headers = {}
|
||||
self.sess.delete = mock.Mock(return_value=resp)
|
||||
|
||||
sot.delete(self.sess)
|
||||
|
||||
self.sess.delete.assert_called_with(
|
||||
'/os-quota-sets/proj',
|
||||
microversion=1,
|
||||
headers={},
|
||||
)
|
Loading…
Reference in New Issue
Block a user