Add tag API

Change-Id: I4e8325866ef63e424d5c6a93138e731001731568
Implements: blueprint role-decomposition
This commit is contained in:
Viacheslav Valyavskiy 2016-11-10 21:53:10 +03:00 committed by Valyavskiy Viacheslav
parent da2b335d35
commit 520e230690
9 changed files with 549 additions and 0 deletions

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 six
from nailgun.api.v1.handlers import base
from nailgun.api.v1.handlers.base import handle_errors
from nailgun.api.v1.handlers.base import serialize
from nailgun.api.v1.handlers.base import validate
from nailgun.api.v1.validators.tag import TagValidator
from nailgun import errors
from nailgun import objects
from nailgun.objects.serializers.tag import TagSerializer
class TagMixIn(object):
def _get_object_or_404(self, obj_type, obj_id):
obj_cls = {
'releases': objects.Release,
'clusters': objects.Cluster,
}[obj_type]
return obj_cls, self.get_object_or_404(obj_cls, obj_id)
class TagHandler(base.SingleHandler, TagMixIn):
validator = TagValidator
def _check_tag(self, obj_cls, obj, tag_name):
if tag_name not in obj_cls.get_own_tags(obj):
raise self.http(
404,
"Tag '{}' is not found for the {} {}".format(
tag_name, obj_cls.__name__.lower(), obj.id))
@handle_errors
@validate
@serialize
def GET(self, obj_type, obj_id, tag_name):
"""Retrieve tag
:http:
* 200 (OK)
* 404 (no such object found)
"""
obj_cls, obj = self._get_object_or_404(obj_type, obj_id)
self._check_tag(obj_cls, obj, tag_name)
return TagSerializer.serialize_from_obj(obj_cls, obj, tag_name)
@handle_errors
@validate
@serialize
def PUT(self, obj_type, obj_id, tag_name):
"""Update tag
:http:
* 200 (OK)
* 400 (wrong data specified)
* 404 (no such object found)
"""
obj_cls, obj = self._get_object_or_404(obj_type, obj_id)
self._check_tag(obj_cls, obj, tag_name)
data = self.checked_data(
self.validator.validate_update, instance_cls=obj_cls, instance=obj)
obj_cls.update_tag(obj, data)
return TagSerializer.serialize_from_obj(obj_cls, obj, tag_name)
@handle_errors
def DELETE(self, obj_type, obj_id, tag_name):
"""Remove tag
:http:
* 204 (object successfully deleted)
* 400 (cannot delete object)
* 404 (no such object found)
"""
obj_cls, obj = self._get_object_or_404(obj_type, obj_id)
self._check_tag(obj_cls, obj, tag_name)
obj_cls.remove_tag(obj, tag_name)
raise self.http(204)
class TagCollectionHandler(base.CollectionHandler, TagMixIn):
validator = TagValidator
@handle_errors
@validate
def POST(self, obj_type, obj_id):
"""Create tag for release or cluster
:http:
* 201 (object successfully created)
* 400 (invalid object data specified)
* 409 (object with such parameters already exists)
* 404 (no such object found)
"""
obj_cls, obj = self._get_object_or_404(obj_type, obj_id)
try:
data = self.checked_data(
self.validator.validate_create,
instance_cls=obj_cls,
instance=obj)
except errors.AlreadyExists as exc:
raise self.http(409, exc.message)
tag_name = data['name']
obj_cls.update_tag(obj, data)
raise self.http(
201, TagSerializer.serialize_from_obj(obj_cls, obj, tag_name))
@handle_errors
@validate
@serialize
def GET(self, obj_type, obj_id):
"""Retrieve tag list of release or cluster
:http:
* 200 (OK)
* 404 (no such object found)
"""
obj_cls, obj = self._get_object_or_404(obj_type, obj_id)
tag_names = six.iterkeys(obj_cls.get_tags_metadata(obj))
return [TagSerializer.serialize_from_obj(obj_cls, obj, tag_name)
for tag_name in tag_names]

View File

@ -112,6 +112,9 @@ from nailgun.api.v1.handlers.release import ReleaseNetworksHandler
from nailgun.api.v1.handlers.role import RoleCollectionHandler from nailgun.api.v1.handlers.role import RoleCollectionHandler
from nailgun.api.v1.handlers.role import RoleHandler from nailgun.api.v1.handlers.role import RoleHandler
from nailgun.api.v1.handlers.tag import TagCollectionHandler
from nailgun.api.v1.handlers.tag import TagHandler
from nailgun.api.v1.handlers.tasks import TaskCollectionHandler from nailgun.api.v1.handlers.tasks import TaskCollectionHandler
from nailgun.api.v1.handlers.tasks import TaskHandler from nailgun.api.v1.handlers.tasks import TaskHandler
@ -171,6 +174,12 @@ urls = (
'(?P<role_name>[a-zA-Z0-9-_]+)/?$', '(?P<role_name>[a-zA-Z0-9-_]+)/?$',
RoleHandler, RoleHandler,
r'/(?P<obj_type>releases|clusters)/(?P<obj_id>\d+)/tags/?$',
TagCollectionHandler,
r'/(?P<obj_type>releases|clusters)/(?P<obj_id>\d+)/tags/'
'(?P<tag_name>[a-zA-Z0-9-_]+)/?$',
TagHandler,
r'/releases/(?P<obj_id>\d+)/deployment_graphs/?$', r'/releases/(?P<obj_id>\d+)/deployment_graphs/?$',
ReleaseDeploymentGraphCollectionHandler, ReleaseDeploymentGraphCollectionHandler,
r'/releases/(?P<obj_id>\d+)/deployment_graphs/' r'/releases/(?P<obj_id>\d+)/deployment_graphs/'

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# 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.
TAG_META_INFO = {
"type": "object",
"required": ["has_primary"],
"properties": {
"has_primary": {
"type": "boolean",
"description": ("During orchestration this role"
" will be splitted into primary-role and role.")
}
}
}
SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Tag",
"description": "Serialized Tag object",
"type": "object",
"required": ['name'],
"properties": {
"name": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"},
"meta": TAG_META_INFO
}
}

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.api.v1.validators.base import BasicValidator
from nailgun.api.v1.validators.json_schema import tag
from nailgun import errors
class TagValidator(BasicValidator):
@classmethod
def validate(cls, data, instance=None):
parsed = super(TagValidator, cls).validate(data)
cls.validate_schema(parsed, tag.SCHEMA)
return parsed
@classmethod
def validate_update(cls, data, instance_cls, instance):
parsed = cls.validate(data, instance=instance)
return parsed
@classmethod
def validate_create(cls, data, instance_cls, instance):
parsed = cls.validate(data, instance=instance)
tag_name = parsed['name']
if tag_name in instance_cls.get_own_tags(instance):
raise errors.AlreadyExists(
"Tag with name '{}' already "
"exists for {} {}".format(
tag_name, instance_cls.__name__.lower(), instance.id))
return parsed

View File

@ -408,6 +408,27 @@ class Cluster(NailgunObject):
instance.volumes_metadata.changed() instance.volumes_metadata.changed()
return bool(result) return bool(result)
@classmethod
def update_tag(cls, instance, tag):
"""Update existing Cluster instance with specified tag.
Previous ones are deleted.
:param instance: a Cluster instance
:param role: a tag dict
:returns: None
"""
instance.tags_metadata[tag['name']] = tag['meta']
@classmethod
def remove_tag(cls, instance, tag_name):
for role, meta in six.iteritems(cls.get_own_roles(instance)):
tags = meta.get('tags', [])
if tag_name in tags:
tags.remove(tag_name)
instance.roles_metadata.changed()
return bool(instance.tags_metadata.pop(tag_name, None))
@classmethod @classmethod
def _create_public_map(cls, instance, roles_metadata=None): def _create_public_map(cls, instance, roles_metadata=None):
if instance.network_config.configuration_template is not None: if instance.network_config.configuration_template is not None:
@ -824,6 +845,10 @@ class Cluster(NailgunObject):
def get_own_roles(cls, instance): def get_own_roles(cls, instance):
return instance.roles_metadata return instance.roles_metadata
@classmethod
def get_own_tags(cls, instance):
return instance.tags_metadata
@classmethod @classmethod
def set_primary_tag(cls, instance, nodes, tag): def set_primary_tag(cls, instance, nodes, tag):
"""Method for assigning primary attribute for specific tag. """Method for assigning primary attribute for specific tag.

View File

@ -139,6 +139,27 @@ class Release(NailgunObject):
instance.volumes_metadata.changed() instance.volumes_metadata.changed()
return bool(result) return bool(result)
@classmethod
def update_tag(cls, instance, tag):
"""Update existing Cluster instance with specified tag.
Previous ones are deleted.
:param instance: a Cluster instance
:param role: a tag dict
:returns: None
"""
instance.tags_metadata[tag['name']] = tag['meta']
@classmethod
def remove_tag(cls, instance, tag_name):
for role, meta in six.iteritems(cls.get_own_roles(instance)):
tags = meta.get('tags', [])
if tag_name in tags:
tags.remove(tag_name)
instance.roles_metadata.changed()
return bool(instance.tags_metadata.pop(tag_name, None))
@classmethod @classmethod
def is_deployable(cls, instance): def is_deployable(cls, instance):
"""Returns whether a given release deployable or not. """Returns whether a given release deployable or not.
@ -317,6 +338,14 @@ class Release(NailgunObject):
def get_own_roles(cls, instance): def get_own_roles(cls, instance):
return instance.roles_metadata return instance.roles_metadata
@classmethod
def get_tags_metadata(cls, instance):
return cls.get_own_tags(instance)
@classmethod
def get_own_tags(cls, instance):
return instance.tags_metadata
@classmethod @classmethod
def _check_relation(cls, a, b, relation): def _check_relation(cls, a, b, relation):
"""Helper function to check commutative property for relations""" """Helper function to check commutative property for relations"""

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.objects.serializers.base import BasicSerializer
class TagSerializer(BasicSerializer):
@classmethod
def serialize_from_obj(cls, obj_cls, obj, tag_name):
tag_meta = obj_cls.get_tags_metadata(obj)[tag_name]
return {'name': tag_name, 'meta': tag_meta}

View File

@ -262,6 +262,65 @@ class EnvironmentManager(object):
expect_errors=expect_errors expect_errors=expect_errors
) )
def get_all_tags(self, obj_type, obj_id, expect_errors=False):
return self.app.get(
reverse(
'TagCollectionHandler',
kwargs={'obj_id': obj_id, 'obj_type': obj_type}
),
headers=self.default_headers,
expect_errors=expect_errors
)
def get_tag(self, obj_type, obj_id, tag_name, expect_errors=False):
return self.app.get(
reverse(
'TagHandler',
kwargs={'obj_id': obj_id,
'obj_type': obj_type,
'tag_name': tag_name}
),
headers=self.default_headers,
expect_errors=expect_errors
)
def update_tag(self, obj_type, obj_id, tag_name, data,
expect_errors=False):
return self.app.put(
reverse(
'TagHandler',
kwargs={'obj_id': obj_id,
'obj_type': obj_type,
'tag_name': tag_name}
),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=expect_errors
)
def delete_tag(self, obj_type, obj_id, tag_name, expect_errors=False):
return self.app.delete(
reverse(
'TagHandler',
kwargs={'obj_id': obj_id,
'obj_type': obj_type,
'tag_name': tag_name}
),
headers=self.default_headers,
expect_errors=expect_errors
)
def create_tag(self, obj_type, obj_id, data, expect_errors=False):
return self.app.post(
reverse(
'TagCollectionHandler',
kwargs={'obj_id': obj_id, 'obj_type': obj_type}
),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=expect_errors
)
def create_cluster(self, api=True, exclude=None, **kwargs): def create_cluster(self, api=True, exclude=None, **kwargs):
cluster_data = { cluster_data = {
'name': 'cluster-api-' + str(randint(0, 1000000)), 'name': 'cluster-api-' + str(randint(0, 1000000)),

View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.test import base
class TestReleaseTagsHandler(base.BaseTestCase):
def setUp(self):
super(TestReleaseTagsHandler, self).setUp()
self.release = self.env.create_release()
self.tag_data = {'name': 'my_tag', 'meta': {'has_primary': False}}
def test_get_all_tags(self):
owner_type, owner_id = 'releases', self.release.id
resp = self.env.get_all_tags(owner_type, owner_id)
self.assertEqual(
len(self.release.tags_metadata.keys()),
len(resp.json))
def test_create_tag(self):
owner_type, owner_id = 'releases', self.release.id
resp = self.env.create_tag(owner_type, owner_id, self.tag_data)
self.assertEqual(resp.json['meta'], self.tag_data['meta'])
resp = self.env.get_all_tags(owner_type, owner_id)
created_tag = next((
tag
for tag in resp.json if tag['name'] == self.tag_data['name']))
self.assertEqual(created_tag, self.tag_data)
def test_update_tag(self):
has_primary = True
owner_type, owner_id = 'releases', self.release.id
resp = self.env.create_tag(owner_type, owner_id, self.tag_data)
data = resp.json
data['meta']['has_primary'] = has_primary
resp = self.env.update_tag(owner_type, owner_id, data['name'], data)
self.assertTrue(resp.json['meta']['has_primary'])
def test_update_tag_not_present(self):
owner_type, owner_id = 'releases', self.release.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
tag_name = 'blah_tag'
resp = self.env.update_tag(owner_type,
owner_id,
tag_name,
self.tag_data,
expect_errors=True)
self.assertEqual(404, resp.status_code)
self.assertIn('is not found for the release', resp.body)
def test_delete_tag(self):
owner_type, owner_id = 'releases', self.release.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
delete_resp = self.env.delete_tag(
owner_type, owner_id, self.tag_data['name'])
self.assertEqual(delete_resp.status_code, 204)
def test_delete_tag_not_present(self):
owner_type, owner_id = 'releases', self.release.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
tag_name = 'blah_tag'
delete_resp = self.env.delete_tag(
owner_type, owner_id, tag_name, expect_errors=True)
self.assertEqual(delete_resp.status_code, 404)
self.assertIn('is not found for the release', delete_resp.body)
def test_get_tag(self):
owner_type, owner_id = 'releases', self.release.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
tag = self.env.get_tag(owner_type, owner_id, self.tag_data['name'])
self.assertEqual(tag.status_code, 200)
self.assertEqual(tag.json['name'], self.tag_data['name'])
def test_get_tag_not_present(self):
owner_type, owner_id = 'releases', self.release.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
tag_name = 'blah_tag'
resp = self.env.get_tag(
owner_type, owner_id, tag_name, expect_errors=True)
self.assertEqual(resp.status_code, 404)
self.assertIn('is not found for the release', resp.body)
def test_create_tag_with_special_symbols(self):
owner_type, owner_id = 'releases', self.release.id
self.tag_data['name'] = '@#$%^&*()'
resp = self.env.create_tag(
owner_type, owner_id, self.tag_data, expect_errors=True)
self.assertEqual(resp.status_code, 400)
class TestClusterTagsHandler(base.BaseTestCase):
def setUp(self):
super(TestClusterTagsHandler, self).setUp()
self.cluster = self.env.create_cluster(api=False)
self.tag_data = {'name': 'my_tag', 'meta': {'has_primary': False}}
def test_get_all_tags(self):
owner_type, owner_id = 'clusters', self.cluster.id
resp = self.env.get_all_tags(owner_type, owner_id)
self.assertEqual(
len(self.cluster.release.tags_metadata.keys() +
self.cluster.tags_metadata.keys()),
len(resp.json))
def test_create_tag(self):
owner_type, owner_id = 'clusters', self.cluster.id
resp = self.env.create_tag(owner_type, owner_id, self.tag_data)
self.assertEqual(resp.json['meta'], self.tag_data['meta'])
resp = self.env.get_all_tags(owner_type, owner_id)
created_tag = next((
tag
for tag in resp.json if tag['name'] == self.tag_data['name']))
self.assertEqual(created_tag, self.tag_data)
def test_update_tag(self):
changed_name = 'Another name'
owner_type, owner_id = 'clusters', self.cluster.id
resp = self.env.create_tag(owner_type, owner_id, self.tag_data)
data = resp.json
data['meta']['name'] = changed_name
resp = self.env.update_tag(owner_type, owner_id, data['name'], data)
self.assertEqual(resp.json['meta']['name'], changed_name)
def test_get_tag(self):
owner_type, owner_id = 'clusters', self.cluster.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
tag = self.env.get_tag(owner_type, owner_id, self.tag_data['name'])
self.assertEqual(tag.status_code, 200)
self.assertEqual(tag.json['name'], self.tag_data['name'])
def test_delete_tag(self):
owner_type, owner_id = 'clusters', self.cluster.id
self.env.create_tag(owner_type, owner_id, self.tag_data)
delete_resp = self.env.delete_tag(
owner_type, owner_id, self.tag_data['name'])
self.assertEqual(delete_resp.status_code, 204)
def test_error_tag_not_present(self):
owner_type, owner_id = 'clusters', self.cluster.id
tag_name = 'blah_tag'
resp = self.env.get_tag(
owner_type, owner_id, tag_name, expect_errors=True)
self.assertEqual(resp.status_code, 404)
self.assertIn("is not found for the cluster",
resp.json_body['message'])