Merge "Get mandatory patch attrs from WSME properties"

This commit is contained in:
Jenkins 2016-08-26 21:19:08 +00:00 committed by Gerrit Code Review
commit 0d661c32f0
10 changed files with 158 additions and 139 deletions

View File

@ -40,22 +40,6 @@ from magnum.objects import fields
LOG = logging.getLogger(__name__)
class BayPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/baymodel_id']
@staticmethod
def internal_attrs():
internal_attrs = ['/api_address', '/node_addresses',
'/master_addresses', '/stack_id',
'/ca_cert_ref', '/magnum_cert_ref',
'/trust_id', '/trustee_user_name',
'/trustee_password', '/trustee_user_id']
return types.JsonPatchType.internal_attrs() + internal_attrs
class BayID(wtypes.Base):
uuid = types.uuid
@ -185,6 +169,19 @@ class Bay(base.APIBase):
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
class BayPatchType(types.JsonPatchType):
_api_base = Bay
@staticmethod
def internal_attrs():
internal_attrs = ['/api_address', '/node_addresses',
'/master_addresses', '/stack_id',
'/ca_cert_ref', '/magnum_cert_ref',
'/trust_id', '/trustee_user_name',
'/trustee_password', '/trustee_user_id']
return types.JsonPatchType.internal_attrs() + internal_attrs
class BayCollection(collection.Collection):
"""API representation of a collection of bays."""

View File

@ -33,15 +33,6 @@ from magnum import objects
from magnum.objects import fields
class BayModelPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/image_id', '/keypair_id', '/external_network_id', '/coe',
'/tls_disabled', '/public', '/registry_enabled',
'/server_type', '/cluster_distro', '/network_driver']
class BayModel(base.APIBase):
"""API representation of a baymodel.
@ -206,6 +197,14 @@ class BayModel(base.APIBase):
return cls._convert_with_links(sample, 'http://localhost:9511')
class BayModelPatchType(types.JsonPatchType):
_api_base = BayModel
_extra_non_removable_attrs = {'/network_driver', '/external_network_id',
'/tls_disabled', '/public', '/server_type',
'/coe', '/registry_enabled',
'/cluster_distro'}
class BayModelCollection(collection.Collection):
"""API representation of a collection of baymodels."""

View File

@ -40,21 +40,6 @@ from magnum.objects import fields
LOG = logging.getLogger(__name__)
class ClusterPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/cluster_template_id']
@staticmethod
def internal_attrs():
internal_attrs = ['/api_address', '/node_addresses',
'/master_addresses', '/stack_id',
'/ca_cert_ref', '/magnum_cert_ref',
'/trust_id', '/trustee_user_name',
'/trustee_password', '/trustee_user_id']
return types.JsonPatchType.internal_attrs() + internal_attrs
class ClusterID(wtypes.Base):
"""API representation of a cluster ID
@ -237,6 +222,19 @@ class Cluster(base.APIBase):
return d
class ClusterPatchType(types.JsonPatchType):
_api_base = Cluster
@staticmethod
def internal_attrs():
internal_attrs = ['/api_address', '/node_addresses',
'/master_addresses', '/stack_id',
'/ca_cert_ref', '/magnum_cert_ref',
'/trust_id', '/trustee_user_name',
'/trustee_password', '/trustee_user_id']
return types.JsonPatchType.internal_attrs() + internal_attrs
class ClusterCollection(collection.Collection):
"""API representation of a collection of clusters."""

View File

@ -33,15 +33,6 @@ from magnum import objects
from magnum.objects import fields
class ClusterTemplatePatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/image_id', '/keypair_id', '/external_network_id', '/coe',
'/tls_disabled', '/public', '/registry_enabled',
'/server_type', '/cluster_distro', '/network_driver']
class ClusterTemplate(base.APIBase):
"""API representation of a clustertemplate.
@ -209,6 +200,14 @@ class ClusterTemplate(base.APIBase):
return cls._convert_with_links(sample, 'http://localhost:9511')
class ClusterTemplatePatchType(types.JsonPatchType):
_api_base = ClusterTemplate
_extra_non_removable_attrs = {'/network_driver', '/external_network_id',
'/tls_disabled', '/public', '/server_type',
'/coe', '/registry_enabled',
'/cluster_distro'}
class ClusterTemplateCollection(collection.Collection):
"""API representation of a collection of clustertemplates."""

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import inspect
from oslo_utils import strutils
from oslo_utils import uuidutils
import wsme
@ -143,6 +145,17 @@ class JsonPatchType(wtypes.Base):
mandatory=True)
value = MultiType(wtypes.text, int)
# The class of the objects being patched. Override this in subclasses.
# Should probably be a subclass of magnum.api.controllers.base.APIBase.
_api_base = None
# Attributes that are not required for construction, but which may not be
# removed if set. Override in subclasses if needed.
_extra_non_removable_attrs = set()
# Set of non-removable attributes, calculated lazily.
_non_removable_attrs = None
@staticmethod
def internal_attrs():
"""Returns a list of internal attributes.
@ -154,15 +167,24 @@ class JsonPatchType(wtypes.Base):
return ['/created_at', '/id', '/links', '/updated_at',
'/uuid', '/project_id', '/user_id']
@staticmethod
def mandatory_attrs():
"""Retruns a list of mandatory attributes.
Mandatory attributes can't be removed from the document. This
method should be overwritten by derived class.
@classmethod
def non_removable_attrs(self):
"""Returns a set of names of attributes that may not be removed.
Attributes whose 'mandatory' property is True are automatically added
to this set. To add additional attributes to the set, override the
field _extra_non_removable_attrs in subclasses, with a set of the form
{'/foo', '/bar'}.
"""
return []
if self._non_removable_attrs is None:
self._non_removable_attrs = self._extra_non_removable_attrs.copy()
if self._api_base:
fields = inspect.getmembers(self._api_base,
lambda a: not inspect.isroutine(a))
for name, field in fields:
if getattr(field, 'mandatory', False):
self._non_removable_attrs.add('/%s' % name)
return self._non_removable_attrs
@staticmethod
def validate(patch):
@ -170,7 +192,7 @@ class JsonPatchType(wtypes.Base):
msg = _("'%s' is an internal attribute and can not be updated")
raise wsme.exc.ClientSideError(msg % patch.path)
if patch.path in patch.mandatory_attrs() and patch.op == 'remove':
if patch.path in patch.non_removable_attrs() and patch.op == 'remove':
msg = _("'%s' is a mandatory attribute and can not be removed")
raise wsme.exc.ClientSideError(msg % patch.path)

View File

@ -384,21 +384,15 @@ class TestPatch(api_base.FunctionalTest):
self.assertEqual(self.bay.name, response['name'])
self.assertEqual(self.bay.master_count, response['master_count'])
def test_remove_uuid(self):
response = self.patch_json('/bays/%s' % self.bay.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_baymodel_id(self):
response = self.patch_json('/bays/%s' % self.bay.uuid,
[{'path': '/baymodel_id', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_mandatory_property_fail(self):
mandatory_properties = ('/uuid', '/baymodel_id')
for p in mandatory_properties:
response = self.patch_json('/bays/%s' % self.bay.uuid,
[{'path': p, 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_non_existent_property(self):
response = self.patch_json(

View File

@ -368,39 +368,37 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['errors'])
def test_remove_singular(self):
baymodel = obj_utils.create_test_baymodel(
self.context,
uuid=uuidutils.generate_uuid())
response = self.get_json('/baymodels/%s' % baymodel.uuid)
response = self.get_json('/baymodels/%s' % self.baymodel.uuid)
self.assertIsNotNone(response['dns_nameserver'])
response = self.patch_json('/baymodels/%s' % baymodel.uuid,
response = self.patch_json('/baymodels/%s' % self.baymodel.uuid,
[{'path': '/dns_nameserver',
'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/baymodels/%s' % baymodel.uuid)
response = self.get_json('/baymodels/%s' % self.baymodel.uuid)
self.assertIsNone(response['dns_nameserver'])
# Assert nothing else was changed
self.assertEqual(baymodel.uuid, response['uuid'])
self.assertEqual(baymodel.name, response['name'])
self.assertEqual(baymodel.apiserver_port, response['apiserver_port'])
self.assertEqual(baymodel.image_id,
self.assertEqual(self.baymodel.uuid, response['uuid'])
self.assertEqual(self.baymodel.name, response['name'])
self.assertEqual(self.baymodel.apiserver_port,
response['apiserver_port'])
self.assertEqual(self.baymodel.image_id,
response['image_id'])
self.assertEqual(baymodel.fixed_network,
self.assertEqual(self.baymodel.fixed_network,
response['fixed_network'])
self.assertEqual(baymodel.network_driver,
self.assertEqual(self.baymodel.network_driver,
response['network_driver'])
self.assertEqual(baymodel.volume_driver,
self.assertEqual(self.baymodel.volume_driver,
response['volume_driver'])
self.assertEqual(baymodel.docker_volume_size,
self.assertEqual(self.baymodel.docker_volume_size,
response['docker_volume_size'])
self.assertEqual(baymodel.coe, response['coe'])
self.assertEqual(baymodel.http_proxy, response['http_proxy'])
self.assertEqual(baymodel.https_proxy, response['https_proxy'])
self.assertEqual(baymodel.no_proxy, response['no_proxy'])
self.assertEqual(baymodel.labels, response['labels'])
self.assertEqual(self.baymodel.coe, response['coe'])
self.assertEqual(self.baymodel.http_proxy, response['http_proxy'])
self.assertEqual(self.baymodel.https_proxy, response['https_proxy'])
self.assertEqual(self.baymodel.no_proxy, response['no_proxy'])
self.assertEqual(self.baymodel.labels, response['labels'])
def test_remove_non_existent_property_fail(self):
response = self.patch_json('/baymodels/%s' % self.baymodel.uuid,
@ -411,10 +409,10 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['errors'])
def test_remove_mandatory_property_fail(self):
mandatory_properties = ('/image_id', '/keypair_id',
'/external_network_id', '/coe',
mandatory_properties = ('/image_id', '/keypair_id', '/coe',
'/external_network_id', '/server_type',
'/tls_disabled', '/public',
'/registry_enabled', '/server_type',
'/registry_enabled',
'/cluster_distro', '/network_driver')
for p in mandatory_properties:
response = self.patch_json('/baymodels/%s' % self.baymodel.uuid,

View File

@ -391,22 +391,15 @@ class TestPatch(api_base.FunctionalTest):
self.assertEqual(self.cluster_obj.master_count,
response['master_count'])
def test_remove_uuid(self):
response = self.patch_json('/clusters/%s' % self.cluster_obj.uuid,
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_cluster_template_id(self):
response = self.patch_json('/clusters/%s' % self.cluster_obj.uuid,
[{'path': '/cluster_template_id',
'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_mandatory_property_fail(self):
mandatory_properties = ('/uuid', '/cluster_template_id')
for p in mandatory_properties:
response = self.patch_json('/clusters/%s' % self.cluster_obj.uuid,
[{'path': p, 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_remove_non_existent_property(self):
response = self.patch_json(

View File

@ -394,43 +394,42 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['errors'])
def test_remove_singular(self):
cluster_template = obj_utils.create_test_cluster_template(
self.context,
uuid=uuidutils.generate_uuid())
response = self.get_json('/clustertemplates/%s' %
cluster_template.uuid)
self.cluster_template.uuid)
self.assertIsNotNone(response['dns_nameserver'])
response = self.patch_json('/clustertemplates/%s' %
cluster_template.uuid,
self.cluster_template.uuid,
[{'path': '/dns_nameserver',
'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/clustertemplates/%s' %
cluster_template.uuid)
self.cluster_template.uuid)
self.assertIsNone(response['dns_nameserver'])
# Assert nothing else was changed
self.assertEqual(cluster_template.uuid, response['uuid'])
self.assertEqual(cluster_template.name, response['name'])
self.assertEqual(cluster_template.apiserver_port,
self.assertEqual(self.cluster_template.uuid, response['uuid'])
self.assertEqual(self.cluster_template.name, response['name'])
self.assertEqual(self.cluster_template.apiserver_port,
response['apiserver_port'])
self.assertEqual(cluster_template.image_id,
self.assertEqual(self.cluster_template.image_id,
response['image_id'])
self.assertEqual(cluster_template.fixed_network,
self.assertEqual(self.cluster_template.fixed_network,
response['fixed_network'])
self.assertEqual(cluster_template.network_driver,
self.assertEqual(self.cluster_template.network_driver,
response['network_driver'])
self.assertEqual(cluster_template.volume_driver,
self.assertEqual(self.cluster_template.volume_driver,
response['volume_driver'])
self.assertEqual(cluster_template.docker_volume_size,
self.assertEqual(self.cluster_template.docker_volume_size,
response['docker_volume_size'])
self.assertEqual(cluster_template.coe, response['coe'])
self.assertEqual(cluster_template.http_proxy, response['http_proxy'])
self.assertEqual(cluster_template.https_proxy, response['https_proxy'])
self.assertEqual(cluster_template.no_proxy, response['no_proxy'])
self.assertEqual(cluster_template.labels, response['labels'])
self.assertEqual(self.cluster_template.coe, response['coe'])
self.assertEqual(self.cluster_template.http_proxy,
response['http_proxy'])
self.assertEqual(self.cluster_template.https_proxy,
response['https_proxy'])
self.assertEqual(self.cluster_template.no_proxy, response['no_proxy'])
self.assertEqual(self.cluster_template.labels, response['labels'])
def test_remove_non_existent_property_fail(self):
response = self.patch_json('/clustertemplates/%s' %
@ -443,10 +442,10 @@ class TestPatch(api_base.FunctionalTest):
self.assertTrue(response.json['errors'])
def test_remove_mandatory_property_fail(self):
mandatory_properties = ('/image_id', '/keypair_id',
'/external_network_id', '/coe',
mandatory_properties = ('/image_id', '/keypair_id', '/coe',
'/external_network_id', '/server_type',
'/tls_disabled', '/public',
'/registry_enabled', '/server_type',
'/registry_enabled',
'/cluster_distro', '/network_driver')
for p in mandatory_properties:
response = self.patch_json('/clustertemplates/%s' %

View File

@ -18,6 +18,7 @@ import mock
import six
import webtest
import wsme
from wsme import types as wtypes
from magnum.api.controllers.v1 import types
from magnum.common import exception
@ -61,12 +62,15 @@ class TestUuidType(base.FunctionalTest):
types.UuidType.validate, 'invalid-uuid')
class MyBaseType(object):
"""Helper class, patched by objects of type MyPatchType"""
mandatory = wsme.wsattr(wtypes.text, mandatory=True)
class MyPatchType(types.JsonPatchType):
"""Helper class for TestJsonPatchType tests."""
@staticmethod
def mandatory_attrs():
return ['/mandatory']
_api_base = MyBaseType
_extra_non_removable_attrs = {'/non_removable'}
@staticmethod
def internal_attrs():
@ -108,17 +112,33 @@ class TestJsonPatchType(base.FunctionalTest):
ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int)
def test_mandatory_attr(self):
patch = [{'op': 'replace', 'path': '/mandatory', 'value': 'foo'}]
def test_cannot_remove_internal_attr(self):
patch = [{'path': '/internal', 'op': 'remove'}]
ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int)
def test_cannot_add_internal_attr(self):
patch = [{'path': '/internal', 'op': 'add', 'value': 'foo'}]
ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int)
def test_update_mandatory_attr(self):
patch = [{'path': '/mandatory', 'op': 'replace', 'value': 'foo'}]
ret = self._patch_json(patch, False)
self.assertEqual(200, ret.status_int)
self.assertEqual(patch, ret.json)
def test_cannot_remove_mandatory_attr(self):
patch = [{'op': 'remove', 'path': '/mandatory'}]
patch = [{'path': '/mandatory', 'op': 'remove'}]
ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int)
def test_cannot_remove_extra_non_removable_attr(self):
patch = [{'path': '/non_removable', 'op': 'remove'}]
ret = self._patch_json(patch, True)
self.assertEqual(400, ret.status_int)
self.assertTrue(ret.json['faultstring'])
def test_missing_required_fields_path(self):
missing_path = [{'op': 'remove'}]
ret = self._patch_json(missing_path, True)