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 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 TaskHandler
@ -171,6 +174,12 @@ urls = (
'(?P<role_name>[a-zA-Z0-9-_]+)/?$',
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/?$',
ReleaseDeploymentGraphCollectionHandler,
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()
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
def _create_public_map(cls, instance, roles_metadata=None):
if instance.network_config.configuration_template is not None:
@ -824,6 +845,10 @@ class Cluster(NailgunObject):
def get_own_roles(cls, instance):
return instance.roles_metadata
@classmethod
def get_own_tags(cls, instance):
return instance.tags_metadata
@classmethod
def set_primary_tag(cls, instance, nodes, tag):
"""Method for assigning primary attribute for specific tag.

View File

@ -139,6 +139,27 @@ class Release(NailgunObject):
instance.volumes_metadata.changed()
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
def is_deployable(cls, instance):
"""Returns whether a given release deployable or not.
@ -317,6 +338,14 @@ class Release(NailgunObject):
def get_own_roles(cls, instance):
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
def _check_relation(cls, a, b, relation):
"""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
)
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):
cluster_data = {
'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'])