Merge "Reworks Chassis validations"

This commit is contained in:
Jenkins 2013-12-18 00:41:06 +00:00 committed by Gerrit Code Review
commit 2ce7c44bd1
5 changed files with 28 additions and 153 deletions

View File

@ -35,19 +35,6 @@ class APIBase(wtypes.Base):
if hasattr(self, k) and
getattr(self, k) != wsme.Unset)
# TODO(lucasagomes): Deprecated. Remove it after updating the chassis
# and nodes elements
@classmethod
def from_rpc_object(cls, m, fields=None):
"""Convert a RPC object to an API object."""
obj_dict = m.as_dict()
# Unset non-required fields so they do not appear
# in the message body
obj_dict.update(dict((k, wsme.Unset)
for k in obj_dict.keys()
if fields and k not in fields))
return cls(**obj_dict)
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.

View File

@ -30,6 +30,7 @@ from ironic.api.controllers.v1 import base
from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import link
from ironic.api.controllers.v1 import node
from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils
from ironic.common import exception
from ironic import objects
@ -39,6 +40,10 @@ from ironic.openstack.common import log
LOG = log.getLogger(__name__)
class ChassisPatchType(types.JsonPatchType):
pass
class Chassis(base.APIBase):
"""API representation of a chassis.
@ -47,8 +52,7 @@ class Chassis(base.APIBase):
a chassis.
"""
# NOTE: translate 'id' publicly to 'uuid' internally
uuid = wtypes.text
uuid = types.uuid
"The UUID of the chassis"
description = wtypes.text
@ -70,17 +74,10 @@ class Chassis(base.APIBase):
@classmethod
def convert_with_links(cls, rpc_chassis, expand=True):
fields = ['uuid', 'description'] if not expand else None
chassis = Chassis.from_rpc_object(rpc_chassis, fields)
chassis.links = [link.Link.make_link('self',
pecan.request.host_url,
'chassis', chassis.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'chassis', chassis.uuid)
]
if expand:
chassis = Chassis(**rpc_chassis.as_dict())
if not expand:
chassis.unset_fields_except(['uuid', 'description'])
else:
chassis.nodes = [link.Link.make_link('self',
pecan.request.host_url,
'chassis',
@ -91,6 +88,13 @@ class Chassis(base.APIBase):
chassis.uuid + "/nodes",
bookmark=True)
]
chassis.links = [link.Link.make_link('self',
pecan.request.host_url,
'chassis', chassis.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'chassis', chassis.uuid)
]
return chassis
@ -196,41 +200,29 @@ class ChassisController(rest.RestController):
LOG.exception(e)
return Chassis.convert_with_links(new_chassis)
@wsme_pecan.wsexpose(Chassis, wtypes.text, body=[wtypes.text])
@wsme.validate(wtypes.text, [ChassisPatchType])
@wsme_pecan.wsexpose(Chassis, wtypes.text, body=[ChassisPatchType])
def patch(self, uuid, patch):
"""Update an existing chassis.
:param uuid: UUID of a chassis.
:param patch: a json PATCH document to apply to this chassis.
"""
chassis = objects.Chassis.get_by_uuid(pecan.request.context, uuid)
chassis_dict = chassis.as_dict()
utils.validate_patch(patch)
rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context, uuid)
try:
patched_chassis = jsonpatch.apply_patch(chassis_dict,
jsonpatch.JsonPatch(patch))
chassis = Chassis(**jsonpatch.apply_patch(rpc_chassis.as_dict(),
jsonpatch.JsonPatch(patch)))
except jsonpatch.JsonPatchException as e:
LOG.exception(e)
raise wsme.exc.ClientSideError(_("Patching Error: %s") % e)
defaults = objects.Chassis.get_defaults()
for key in defaults:
# Internal values that shouldn't be part of the patch
if key in ['id', 'updated_at', 'created_at']:
continue
# In case of a remove operation, add the missing fields back
# to the document with their default value
if key in chassis_dict and key not in patched_chassis:
patched_chassis[key] = defaults[key]
# Update only the fields that have changed
if chassis[key] != patched_chassis[key]:
chassis[key] = patched_chassis[key]
for field in objects.Chassis.fields:
if rpc_chassis[field] != getattr(chassis, field):
rpc_chassis[field] = getattr(chassis, field)
chassis.save()
return Chassis.convert_with_links(chassis)
rpc_chassis.save()
return Chassis.convert_with_links(rpc_chassis)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, uuid):

View File

@ -16,7 +16,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
import wsme
from oslo.config import cfg
@ -39,42 +38,6 @@ def validate_sort_dir(sort_dir):
return sort_dir
# TODO(lucasagomes): Deprecated. Remove it after updating chassis and nodes
def validate_patch(patch):
"""Performs a basic validation on patch."""
if not isinstance(patch, list):
patch = [patch]
path_pattern = re.compile("(/[\w-]+)+$")
for p in patch:
if not isinstance(p, dict) or \
any(key for key in ["path", "op"] if key not in p):
raise wsme.exc.ClientSideError(_("Invalid patch format: %s")
% str(p))
path = p["path"]
op = p["op"]
if op not in ["add", "replace", "remove"]:
raise wsme.exc.ClientSideError(_("Operation not supported: %s")
% op)
if not path_pattern.match(path):
raise wsme.exc.ClientSideError(_("Invalid path: %s") % path)
if op == "add":
if path.count('/') == 1:
raise wsme.exc.ClientSideError(_("Adding an additional "
"attribute (%s) to the "
"resource is not allowed")
% path)
if op in ["remove", "replace", "add"]:
if path.startswith('/uuid'):
raise wsme.exc.ClientSideError(_("UUIDs can not be removed "
"or replaced"))
class ValidTypes(wsme.types.UserType):
"""User type for validate that value has one of a few types."""

View File

@ -384,14 +384,6 @@ class IronicObject(object):
for k in self.fields
if hasattr(self, k))
# TODO(lucasagomes): Deprecated. Remove it after updating chassis and nodes
@classmethod
def get_defaults(cls):
"""Return a dict of its fields with their default value."""
return dict((k, v(None))
for k, v in cls.fields.iteritems()
if k != "id" and callable(v))
class ObjectListBase(object):
"""Mixin class for lists of objects.

View File

@ -49,65 +49,6 @@ class TestApiUtils(base.FunctionalTest):
utils.validate_sort_dir,
'fake-sort')
def test_validate_patch(self):
patch = [{'op': 'remove', 'value': 'bar', 'path': '/foo'}]
utils.validate_patch(patch)
patch = [{'op': 'add', 'value': 'bar', 'path': '/extra/foo'}]
utils.validate_patch(patch)
patch = [{'op': 'replace', 'value': 'bar', 'path': '/foo'}]
utils.validate_patch(patch)
def test_validate_patch_wrong_format(self):
# missing path
patch = [{'op': 'remove'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
# wrong op
patch = [{'op': 'foo', 'value': 'bar', 'path': '/foo'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
def test_validate_patch_wrong_path(self):
# non-alphanumeric characters
patch = [{'path': '/fo^o', 'op': 'remove'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
# empty path
patch = [{'path': '', 'op': 'remove'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
patch = [{'path': '/', 'op': 'remove'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
def test_validate_patch_uuid(self):
patch = [{'op': 'remove', 'path': '/uuid'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
patch = [{'op': 'replace',
'value': '105f5cd9-ae67-480a-8c10-62040213b8fd',
'path': '/uuid'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
patch = [{'op': 'add',
'value': '105f5cd9-ae67-480a-8c10-62040213b8fd',
'path': '/uuid'}]
self.assertRaises(wsme.exc.ClientSideError,
utils.validate_patch,
patch)
def test_valid_types(self):
vt = utils.ValidTypes(wsme.types.text, six.integer_types)