From 417f3b73310996ca68345b9bc26c81e69e99c533 Mon Sep 17 00:00:00 2001 From: Neha Alhat Date: Fri, 29 Dec 2017 09:42:32 +0530 Subject: [PATCH] V3 jsonschema validation: snapshot_actions This patch adds jsonschema validation for below snapshot_actions API * POST /v3/{project_id}/snapshots/{snapshot_id}/action Change-Id: I810f72777864947e7e44ddf668732e06db68c1f3 Partial-Implements: bp json-schema-validation --- cinder/api/contrib/snapshot_actions.py | 19 +++----- cinder/api/schemas/snapshot_actions.py | 36 +++++++++++++++ cinder/api/validation/validators.py | 15 +++++++ .../unit/api/contrib/test_snapshot_actions.py | 45 +++++++++++++++++++ 4 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 cinder/api/schemas/snapshot_actions.py diff --git a/cinder/api/contrib/snapshot_actions.py b/cinder/api/contrib/snapshot_actions.py index 39733737e90..bd481cd6aa9 100644 --- a/cinder/api/contrib/snapshot_actions.py +++ b/cinder/api/contrib/snapshot_actions.py @@ -18,6 +18,8 @@ import webob from cinder.api import extensions from cinder.api.openstack import wsgi +from cinder.api.schemas import snapshot_actions +from cinder.api import validation from cinder.i18n import _ from cinder import objects from cinder.objects import fields @@ -31,6 +33,7 @@ class SnapshotActionsController(wsgi.Controller): LOG.debug("SnapshotActionsController initialized") @wsgi.action('os-update_snapshot_status') + @validation.schema(snapshot_actions.update_snapshot_status) def _update_snapshot_status(self, req, id, body): """Update database fields related to status of a snapshot. @@ -41,11 +44,8 @@ class SnapshotActionsController(wsgi.Controller): context = req.environ['cinder.context'] LOG.debug("body: %s", body) - try: - status = body['os-update_snapshot_status']['status'] - except KeyError: - msg = _("'status' must be specified.") - raise webob.exc.HTTPBadRequest(explanation=msg) + + status = body['os-update_snapshot_status']['status'] # Allowed state transitions status_map = {fields.SnapshotStatus.CREATING: @@ -78,15 +78,6 @@ class SnapshotActionsController(wsgi.Controller): progress = body['os-update_snapshot_status'].get('progress', None) if progress: - # This is expected to be a string like '73%' - msg = _('progress must be an integer percentage') - try: - integer = int(progress[:-1]) - except ValueError: - raise webob.exc.HTTPBadRequest(explanation=msg) - if integer < 0 or integer > 100 or progress[-1] != '%': - raise webob.exc.HTTPBadRequest(explanation=msg) - update_dict.update({'progress': progress}) LOG.info("Updating snapshot %(id)s with info %(dict)s", diff --git a/cinder/api/schemas/snapshot_actions.py b/cinder/api/schemas/snapshot_actions.py new file mode 100644 index 00000000000..5402720a1a2 --- /dev/null +++ b/cinder/api/schemas/snapshot_actions.py @@ -0,0 +1,36 @@ +# 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 snapshot actions API. + +""" + +update_snapshot_status = { + 'type': 'object', + 'properties': { + 'os-update_snapshot_status': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string'}, + 'progress': {'format': 'progress'}, + }, + 'required': ['status'], + 'additionalProperties': False, + }, + }, + 'required': ['os-update_snapshot_status'], + 'additionalProperties': False, +} diff --git a/cinder/api/validation/validators.py b/cinder/api/validation/validators.py index e6de9245dbb..65cb8055564 100644 --- a/cinder/api/validation/validators.py +++ b/cinder/api/validation/validators.py @@ -171,6 +171,21 @@ def _validate_status(param_value): return True +@jsonschema.FormatChecker.cls_checks('progress') +def _validate_progress(progress): + if progress: + try: + integer = int(progress[:-1]) + except ValueError: + msg = _('progress must be an integer percentage') + raise exception.InvalidInput(reason=msg) + if integer < 0 or integer > 100 or progress[-1] != '%': + msg = _('progress must be an integer percentage between' + ' 0 and 100') + raise exception.InvalidInput(reason=msg) + return True + + @jsonschema.FormatChecker.cls_checks('base64') def _validate_base64_format(instance): try: diff --git a/cinder/tests/unit/api/contrib/test_snapshot_actions.py b/cinder/tests/unit/api/contrib/test_snapshot_actions.py index 800c4a753f5..1efa89f56e2 100644 --- a/cinder/tests/unit/api/contrib/test_snapshot_actions.py +++ b/cinder/tests/unit/api/contrib/test_snapshot_actions.py @@ -12,13 +12,17 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from oslo_serialization import jsonutils from six.moves import http_client import webob +from cinder.api.contrib import snapshot_actions +from cinder.api import microversions as mv from cinder import context from cinder import db +from cinder import exception from cinder.objects import fields from cinder import test from cinder.tests.unit.api import fakes @@ -36,12 +40,14 @@ def fake_snapshot_get(context, snapshot_id): return snapshot +@ddt.ddt class SnapshotActionsTest(test.TestCase): def setUp(self): super(SnapshotActionsTest, self).setUp() self.user_ctxt = context.RequestContext( fake.USER_ID, fake.PROJECT_ID, auth_token=True) + self.controller = snapshot_actions.SnapshotActionsController() @mock.patch('cinder.db.snapshot_update', autospec=True) @mock.patch('cinder.db.sqlalchemy.api._snapshot_get', @@ -88,3 +94,42 @@ class SnapshotActionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) self.assertEqual(http_client.BAD_REQUEST, res.status_int) + + @mock.patch('cinder.db.snapshot_update', autospec=True) + @mock.patch('cinder.db.sqlalchemy.api._snapshot_get', + side_effect=fake_snapshot_get) + @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) + def test_update_snapshot_valid_progress(self, metadata_get, *args): + body = {'os-update_snapshot_status': + {'status': fields.SnapshotStatus.AVAILABLE, + 'progress': '50%'}} + req = webob.Request.blank('/v2/%s/snapshots/%s/action' % ( + fake.PROJECT_ID, fake.SNAPSHOT_ID)) + req.method = "POST" + req.body = jsonutils.dump_as_bytes(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.user_ctxt)) + self.assertEqual(http_client.ACCEPTED, res.status_int) + + @ddt.data(({'os-update_snapshot_status': + {'status': fields.SnapshotStatus.AVAILABLE, + 'progress': '50'}}, exception.InvalidInput), + ({'os-update_snapshot_status': + {'status': fields.SnapshotStatus.AVAILABLE, + 'progress': '103%'}}, exception.InvalidInput), + ({'os-update_snapshot_status': + {'status': fields.SnapshotStatus.AVAILABLE, + 'progress': " "}}, exception.InvalidInput), + ({'os-update_snapshot_status': + {'status': fields.SnapshotStatus.AVAILABLE, + 'progress': 50}}, exception.ValidationError)) + @ddt.unpack + def test_update_snapshot_invalid_progress(self, body, exception_class): + req = webob.Request.blank('/v3/%s/snapshots/%s/action' % ( + fake.PROJECT_ID, fake.SNAPSHOT_ID)) + req.api_version_request = mv.get_api_version(mv.BASE_VERSION) + self.assertRaises(exception_class, + self.controller._update_snapshot_status, + req, body=body)