V3 jsonschema validation: Clusters
This patch adds jsonschema validation for below Clusters API's * PUT /v3/{project_id}/clusters/action Change-Id: I7ba0d9fb5292d0076fc99004cf1326d7f3fc86ee Partial-Implements: bp json-schema-validation
This commit is contained in:
parent
6dccf35746
commit
cdb3ae0ebd
47
cinder/api/schemas/clusters.py
Normal file
47
cinder/api/schemas/clusters.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Copyright (C) 2018 NTT DATA
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Schema for V3 Clusters API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from cinder.api.validation import parameter_types
|
||||||
|
|
||||||
|
|
||||||
|
disable_cluster = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': parameter_types.name,
|
||||||
|
'binary': parameter_types.nullable_string,
|
||||||
|
'disabled_reason': {
|
||||||
|
'type': ['string', 'null'], 'format': 'disabled_reason'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['name'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enable_cluster = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': parameter_types.name,
|
||||||
|
'binary': parameter_types.nullable_string
|
||||||
|
},
|
||||||
|
'required': ['name'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
|
from cinder.api.schemas import clusters as cluster
|
||||||
from cinder.api.v3.views import clusters as clusters_view
|
from cinder.api.v3.views import clusters as clusters_view
|
||||||
|
from cinder.api import validation
|
||||||
from cinder.common import constants
|
from cinder.common import constants
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
@ -102,15 +104,11 @@ class ClusterController(wsgi.Controller):
|
|||||||
raise exception.NotFound(message=_("Unknown action"))
|
raise exception.NotFound(message=_("Unknown action"))
|
||||||
|
|
||||||
disabled = id != 'enable'
|
disabled = id != 'enable'
|
||||||
disabled_reason = self._get_disabled_reason(body) if disabled else None
|
disabled_reason = self._disable_cluster(
|
||||||
|
req, body=body) if disabled else self._enable_cluster(
|
||||||
|
req, body=body)
|
||||||
|
|
||||||
if not disabled and disabled_reason:
|
name = body['name']
|
||||||
msg = _("Unexpected 'disabled_reason' found on enable request.")
|
|
||||||
raise exception.InvalidInput(reason=msg)
|
|
||||||
|
|
||||||
name = body.get('name')
|
|
||||||
if not name:
|
|
||||||
raise exception.MissingRequired(element='name')
|
|
||||||
|
|
||||||
binary = body.get('binary', constants.VOLUME_BINARY)
|
binary = body.get('binary', constants.VOLUME_BINARY)
|
||||||
|
|
||||||
@ -129,15 +127,17 @@ class ClusterController(wsgi.Controller):
|
|||||||
|
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
def _get_disabled_reason(self, body):
|
@validation.schema(cluster.disable_cluster)
|
||||||
|
def _disable_cluster(self, req, body):
|
||||||
reason = body.get('disabled_reason')
|
reason = body.get('disabled_reason')
|
||||||
if reason:
|
if reason:
|
||||||
# Let wsgi handle InvalidInput exception
|
|
||||||
reason = reason.strip()
|
reason = reason.strip()
|
||||||
utils.check_string_length(reason, 'Disabled reason', min_length=1,
|
|
||||||
max_length=255)
|
|
||||||
return reason
|
return reason
|
||||||
|
|
||||||
|
@validation.schema(cluster.enable_cluster)
|
||||||
|
def _enable_cluster(self, req, body):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
return wsgi.Resource(ClusterController())
|
return wsgi.Resource(ClusterController())
|
||||||
|
@ -26,10 +26,12 @@ from jsonschema import exceptions as jsonschema_exc
|
|||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import six
|
import six
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder.objects import fields as c_fields
|
from cinder.objects import fields as c_fields
|
||||||
|
from cinder import utils
|
||||||
|
|
||||||
|
|
||||||
def _soft_validate_additional_properties(
|
def _soft_validate_additional_properties(
|
||||||
@ -85,6 +87,34 @@ def _soft_validate_additional_properties(
|
|||||||
del param_value[prop]
|
del param_value[prop]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_string_length(value, entity_name, mandatory=False,
|
||||||
|
min_length=0, max_length=None,
|
||||||
|
remove_whitespaces=False):
|
||||||
|
"""Check the length of specified string.
|
||||||
|
|
||||||
|
:param value: the value of the string
|
||||||
|
:param entity_name: the name of the string
|
||||||
|
:mandatory: string is mandatory or not
|
||||||
|
:param min_length: the min_length of the string
|
||||||
|
:param max_length: the max_length of the string
|
||||||
|
:param remove_whitespaces: True if trimming whitespaces is needed
|
||||||
|
else False
|
||||||
|
"""
|
||||||
|
if not mandatory and not value:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if mandatory and not value:
|
||||||
|
msg = _("The '%s' can not be None.") % entity_name
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
if remove_whitespaces:
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
utils.check_string_length(value, entity_name,
|
||||||
|
min_length=min_length,
|
||||||
|
max_length=max_length)
|
||||||
|
|
||||||
|
|
||||||
@jsonschema.FormatChecker.cls_checks('date-time')
|
@jsonschema.FormatChecker.cls_checks('date-time')
|
||||||
def _validate_datetime_format(param_value):
|
def _validate_datetime_format(param_value):
|
||||||
try:
|
try:
|
||||||
@ -157,6 +187,14 @@ def _validate_base64_format(instance):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@jsonschema.FormatChecker.cls_checks('disabled_reason')
|
||||||
|
def _validate_disabled_reason(param_value):
|
||||||
|
_validate_string_length(param_value, 'disabled_reason',
|
||||||
|
mandatory=False, min_length=1, max_length=255,
|
||||||
|
remove_whitespaces=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FormatChecker(jsonschema.FormatChecker):
|
class FormatChecker(jsonschema.FormatChecker):
|
||||||
"""A FormatChecker can output the message from cause exception
|
"""A FormatChecker can output the message from cause exception
|
||||||
|
|
||||||
|
@ -218,15 +218,15 @@ class ClustersTestCase(test.TestCase):
|
|||||||
def test_show(self, get_mock):
|
def test_show(self, get_mock):
|
||||||
req = FakeRequest()
|
req = FakeRequest()
|
||||||
expected = {'cluster': self._get_expected()[0]}
|
expected = {'cluster': self._get_expected()[0]}
|
||||||
cluster = self.controller.show(req, mock.sentinel.name,
|
cluster = self.controller.show(req, 'cluster_name',
|
||||||
mock.sentinel.binary)
|
'cinder-volume')
|
||||||
self.assertEqual(expected, cluster)
|
self.assertEqual(expected, cluster)
|
||||||
get_mock.assert_called_once_with(
|
get_mock.assert_called_once_with(
|
||||||
req.environ['cinder.context'],
|
req.environ['cinder.context'],
|
||||||
None,
|
None,
|
||||||
services_summary=True,
|
services_summary=True,
|
||||||
name=mock.sentinel.name,
|
name='cluster_name',
|
||||||
binary=mock.sentinel.binary)
|
binary='cinder-volume')
|
||||||
|
|
||||||
def test_show_unauthorized(self):
|
def test_show_unauthorized(self):
|
||||||
req = FakeRequest(is_admin=False)
|
req = FakeRequest(is_admin=False)
|
||||||
@ -249,13 +249,13 @@ class ClustersTestCase(test.TestCase):
|
|||||||
'status': 'enabled',
|
'status': 'enabled',
|
||||||
'disabled_reason': None}}
|
'disabled_reason': None}}
|
||||||
res = self.controller.update(req, 'enable',
|
res = self.controller.update(req, 'enable',
|
||||||
{'name': mock.sentinel.name,
|
body={'name': 'cluster_name',
|
||||||
'binary': mock.sentinel.binary})
|
'binary': 'cinder-volume'})
|
||||||
self.assertEqual(expected, res)
|
self.assertEqual(expected, res)
|
||||||
ctxt = req.environ['cinder.context']
|
ctxt = req.environ['cinder.context']
|
||||||
get_mock.assert_called_once_with(ctxt,
|
get_mock.assert_called_once_with(ctxt,
|
||||||
None, binary=mock.sentinel.binary,
|
None, binary='cinder-volume',
|
||||||
name=mock.sentinel.name)
|
name='cluster_name')
|
||||||
update_mock.assert_called_once_with(ctxt, get_mock.return_value.id,
|
update_mock.assert_called_once_with(ctxt, get_mock.return_value.id,
|
||||||
{'disabled': False,
|
{'disabled': False,
|
||||||
'disabled_reason': None})
|
'disabled_reason': None})
|
||||||
@ -272,40 +272,55 @@ class ClustersTestCase(test.TestCase):
|
|||||||
'status': 'disabled',
|
'status': 'disabled',
|
||||||
'disabled_reason': disabled_reason}}
|
'disabled_reason': disabled_reason}}
|
||||||
res = self.controller.update(req, 'disable',
|
res = self.controller.update(req, 'disable',
|
||||||
{'name': mock.sentinel.name,
|
body={'name': 'cluster_name',
|
||||||
'binary': mock.sentinel.binary,
|
'binary': 'cinder-volume',
|
||||||
'disabled_reason': disabled_reason})
|
'disabled_reason': disabled_reason})
|
||||||
self.assertEqual(expected, res)
|
self.assertEqual(expected, res)
|
||||||
ctxt = req.environ['cinder.context']
|
ctxt = req.environ['cinder.context']
|
||||||
get_mock.assert_called_once_with(ctxt,
|
get_mock.assert_called_once_with(ctxt,
|
||||||
None, binary=mock.sentinel.binary,
|
None, binary='cinder-volume',
|
||||||
name=mock.sentinel.name)
|
name='cluster_name')
|
||||||
update_mock.assert_called_once_with(
|
update_mock.assert_called_once_with(
|
||||||
ctxt, get_mock.return_value.id,
|
ctxt, get_mock.return_value.id,
|
||||||
{'disabled': True, 'disabled_reason': disabled_reason})
|
{'disabled': True, 'disabled_reason': disabled_reason})
|
||||||
|
|
||||||
def test_update_wrong_action(self):
|
def test_update_wrong_action(self):
|
||||||
req = FakeRequest()
|
req = FakeRequest()
|
||||||
self.assertRaises(exception.NotFound, self.controller.update, req,
|
self.assertRaises(exception.NotFound, self.controller.update,
|
||||||
'action', {})
|
req, 'action', body={'name': 'cluster_name'})
|
||||||
|
|
||||||
@ddt.data('enable', 'disable')
|
@ddt.data('enable', 'disable')
|
||||||
def test_update_missing_name(self, action):
|
def test_update_missing_name(self, action):
|
||||||
req = FakeRequest()
|
req = FakeRequest()
|
||||||
self.assertRaises(exception.MissingRequired, self.controller.update,
|
self.assertRaises(exception.ValidationError, self.controller.update,
|
||||||
req, action, {'binary': mock.sentinel.binary})
|
req, action, body={'binary': 'cinder-volume'})
|
||||||
|
|
||||||
def test_update_wrong_disabled_reason(self):
|
def test_update_with_binary_more_than_255_characters(self):
|
||||||
req = FakeRequest()
|
req = FakeRequest()
|
||||||
self.assertRaises(exception.InvalidInput, self.controller.update, req,
|
self.assertRaises(exception.ValidationError, self.controller.update,
|
||||||
'disable', {'name': mock.sentinel.name,
|
req, 'enable', body={'name': 'cluster_name',
|
||||||
'disabled_reason': ' '})
|
'binary': 'a' * 256})
|
||||||
|
|
||||||
|
def test_update_with_name_more_than_255_characters(self):
|
||||||
|
req = FakeRequest()
|
||||||
|
self.assertRaises(exception.ValidationError, self.controller.update,
|
||||||
|
req, 'enable', body={'name': 'a' * 256,
|
||||||
|
'binary': 'cinder-volume'})
|
||||||
|
|
||||||
|
@ddt.data('a' * 256, ' ')
|
||||||
|
def test_update_wrong_disabled_reason(self, disabled_reason):
|
||||||
|
req = FakeRequest()
|
||||||
|
self.assertRaises(exception.InvalidInput, self.controller.update,
|
||||||
|
req, 'disable',
|
||||||
|
body={'name': 'cluster_name',
|
||||||
|
'disabled_reason': disabled_reason})
|
||||||
|
|
||||||
@ddt.data('enable', 'disable')
|
@ddt.data('enable', 'disable')
|
||||||
def test_update_unauthorized(self, action):
|
def test_update_unauthorized(self, action):
|
||||||
req = FakeRequest(is_admin=False)
|
req = FakeRequest(is_admin=False)
|
||||||
self.assertRaises(exception.PolicyNotAuthorized,
|
self.assertRaises(exception.PolicyNotAuthorized,
|
||||||
self.controller.update, req, action, {})
|
self.controller.update, req, action,
|
||||||
|
body={'name': 'fake_name'})
|
||||||
|
|
||||||
@ddt.data('enable', 'disable')
|
@ddt.data('enable', 'disable')
|
||||||
def test_update_wrong_version(self, action):
|
def test_update_wrong_version(self, action):
|
||||||
|
Loading…
Reference in New Issue
Block a user