Added server tags support in nova-api

Added new API microversion which allows the following:
- add tag to the server
- replace set of server tags with new set of tags
- get information about server, including list of tags for server
- get just list of tags for server
- check if tag exists on a server
- remove specified tag from server
- remove all tags from server
- search servers by tags

DocImpact
APIImpact

Implements: blueprint tag-instances

Change-Id: I9573aa52aae9f49945d8806ca5e52ada29fb087a
This commit is contained in:
Sergey Nikitin 2016-01-15 17:11:05 +03:00
parent bfe8e7484d
commit 537df23d85
23 changed files with 519 additions and 6 deletions

View File

@ -0,0 +1,3 @@
{
"tags": ["sometag"]
}

View File

@ -0,0 +1,5 @@
{
"tags": [
"sometag"
]
}

View File

@ -0,0 +1,62 @@
{
"server": {
"tags": [
"sometag"
],
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "2012-12-02T02:11:55Z",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "c949ab4256cea23b6089b710aa2df48bf6577ed915278b62e33ad8bb",
"id": "5046e2f2-3b33-4041-b3cf-e085f73e78e7",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/5046e2f2-3b33-4041-b3cf-e085f73e78e7",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/5046e2f2-3b33-4041-b3cf-e085f73e78e7",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "6f70656e737461636b20342065766572",
"updated": "2012-12-02T02:11:55Z",
"key_name": null,
"user_id": "fake",
"locked": false,
"description": null
}
}

View File

@ -0,0 +1,62 @@
{
"servers": [
{
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "2013-09-03T04:01:32Z",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "bcf92836fc9ed4203a75cb0337afc7f917d2be504164b995c2334b25",
"id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "6f70656e737461636b20342065766572",
"updated": "2013-09-03T04:01:32Z",
"user_id": "fake",
"locked": false,
"tags": ["sometag"],
"description": null
}
]
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.25",
"version": "2.26",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.25",
"version": "2.26",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -71,7 +71,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.24 - Add API to cancel a running live migration
* 2.25 - Make block_migration support 'auto' and remove
disk_over_commit for os-migrateLive.
* 2.26 - Adds support of server tags
"""
# The minimum and maximum versions of the API supported
@ -80,7 +80,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.25"
_MAX_API_VERSION = "2.26"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -82,7 +82,8 @@ v21_to_v2_extension_list_mapping = {
v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors',
'os-block-device-mapping-v1', 'os-consoles',
'extensions', 'image-metadata', 'ips', 'limits',
'server-metadata', 'server-migrations'
'server-metadata', 'server-migrations',
'os-server-tags'
]
# v2.1 plugins which should appear under a different name in v2

View File

@ -35,6 +35,7 @@ def _get_tags_names(tags):
class ServerTagsController(wsgi.Controller):
_view_builder_class = server_tags.ViewBuilder
@wsgi.Controller.api_version("2.26")
@wsgi.response(204)
@extensions.expected_errors(404)
def show(self, req, server_id, id):
@ -51,6 +52,7 @@ class ServerTagsController(wsgi.Controller):
% {'server_id': server_id, 'tag': id})
raise exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version("2.26")
@extensions.expected_errors(404)
def index(self, req, server_id):
context = req.environ["nova.context"]
@ -63,6 +65,7 @@ class ServerTagsController(wsgi.Controller):
return {'tags': _get_tags_names(tags)}
@wsgi.Controller.api_version("2.26")
@extensions.expected_errors((400, 404))
@validation.schema(schema.update)
def update(self, req, server_id, id, body):
@ -109,6 +112,7 @@ class ServerTagsController(wsgi.Controller):
req, server_id, id)
return response
@wsgi.Controller.api_version("2.26")
@extensions.expected_errors((400, 404))
@validation.schema(schema.update_all)
def update_all(self, req, server_id, body):
@ -149,6 +153,7 @@ class ServerTagsController(wsgi.Controller):
return {'tags': _get_tags_names(tags)}
@wsgi.Controller.api_version("2.26")
@wsgi.response(204)
@extensions.expected_errors(404)
def delete(self, req, server_id, id):
@ -162,6 +167,7 @@ class ServerTagsController(wsgi.Controller):
except exception.InstanceNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
@wsgi.Controller.api_version("2.26")
@wsgi.response(204)
@extensions.expected_errors(404)
def delete_all(self, req, server_id):

View File

@ -45,6 +45,7 @@ from nova import objects
from nova import utils
ALIAS = 'servers'
TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any')
CONF = cfg.CONF
CONF.import_opt('enable_instance_password',
@ -353,6 +354,12 @@ class ServersController(wsgi.Controller):
msg = _("Only administrators may list deleted instances")
raise exc.HTTPForbidden(explanation=msg)
if api_version_request.is_supported(req, min_version='2.26'):
for tag_filter in TAG_SEARCH_FILTERS:
if tag_filter in search_opts:
search_opts[tag_filter] = search_opts[
tag_filter].split(',')
# If tenant_id is passed as a search parameter this should
# imply that all_tenants is also enabled unless explicitly
# disabled. Note that the tenant_id parameter is filtered out
@ -397,6 +404,9 @@ class ServersController(wsgi.Controller):
expected_attrs = ['pci_devices']
if is_detail:
if api_version_request.is_supported(req, '2.26'):
expected_attrs.append("tags")
# merge our expected attrs with what the view builder needs for
# showing details
expected_attrs = self._view_builder.get_show_expected_attrs(
@ -1141,6 +1151,8 @@ class ServersController(wsgi.Controller):
'ip', 'changes-since', 'all_tenants')
if api_version_request.is_supported(req, min_version='2.5'):
opt_list += ('ip6',)
if api_version_request.is_supported(req, min_version='2.26'):
opt_list += TAG_SEARCH_FILTERS
return opt_list
def _get_instance(self, context, instance_uuid):

View File

@ -313,4 +313,7 @@ class ViewBuilderV21(ViewBuilder):
server["server"]["description"] = instance.get(
"display_description")
if api_version_request.is_supported(request, min_version="2.26"):
server["server"]["tags"] = [t.tag for t in instance.tags]
return server

View File

@ -218,3 +218,68 @@ user documentation.
Modify input parameter for ``os-migrateLive``. The block_migration will
support 'auto' value, and disk_over_commit flag will be removed.
2.26
----
Added support of server tags.
A user can create, update, delete or check existence of simple string tags
for servers by the os-server-tags plugin.
The resource point for these operations is /servers/<server_id>/tags
A user can add a single tag to the server by sending PUT request to the
/servers/<server_id>/tags/<tag>
where <tag> is any valid tag name.
A user can replace **all** current server tags to the new set of tags
by sending PUT request to the /servers/<server_id>/tags. New set of tags
must be specified in request body. This set must be in list 'tags'.
A user can remove specified tag from the server by sending DELETE request
to the /servers/<server_id>/tags/<tag>
where <tag> is tag name which user wants to remove.
A user can remove **all** tags from the server by sending DELETE request
to the /servers/<server_id>/tags
A user can get a set of server tags with information about server by sending
GET request to the /servers/<server_id>
Request returns dictionary with information about specified server, including
list 'tags' ::
{
'id': {server_id},
...
'tags': ['foo', 'bar', 'baz']
}
A user can get **only** a set of server tags by sending GET request to the
/servers/<server_id>/tags
Response ::
{
'tags': ['foo', 'bar', 'baz']
}
A user can check if a tag exists or not on a server by sending
GET /servers/{server_id}/tags/{tag}
Request returns `204 No Content` if tag exist on a server or `404 Not Found`
if tag doesn't exist on a server.
A user can filter servers in GET /servers request by new filters:
* tags
* tags-any
* not-tags
* not-tags-any
These filters can be combined. Also user can use more than one string tags
for each filter. In this case string tags for each filter must be separated
by comma: GET /servers?tags=red&tags-any=green,orange

View File

@ -0,0 +1,62 @@
{
"server": {
"tags": [
"%(tag)s"
],
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(id)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "6f70656e737461636b20342065766572",
"updated": "%(isotime)s",
"key_name": null,
"user_id": "fake",
"locked": false,
"description": null
}
}

View File

@ -0,0 +1,62 @@
{
"servers": [
{
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "%(ip)s",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(id)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(id)s",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "6f70656e737461636b20342065766572",
"updated": "%(isotime)s",
"user_id": "fake",
"locked": false,
"tags": ["%(tag)s"],
"description": null
}
]
}

View File

@ -0,0 +1,96 @@
# 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 nova.db.sqlalchemy import models
from nova.tests.functional.api_sample_tests import test_servers
TAG = 'sometag'
class ServerTagsJsonTest(test_servers.ServersSampleBase):
extension_name = 'os-server-tags'
microversion = '2.26'
scenarios = [('v2_26', {'api_major_version': 'v2.1'})]
def _get_create_subs(self):
return {'tag': TAG}
def _put_server_tags(self):
"""Verify the response status and returns the UUID of the
newly created server with tags.
"""
uuid = self._post_server()
subs = self._get_create_subs()
response = self._do_put('servers/%s/tags' % uuid,
'server-tags-put-all-req', subs)
self.assertEqual(200, response.status_code)
return uuid
def test_server_tags_update_all(self):
self._put_server_tags()
def test_server_tags_show(self):
uuid = self._put_server_tags()
response = self._do_get('servers/%s/tags/%s' % (uuid, TAG))
self.assertEqual(204, response.status_code)
def test_server_tags_show_with_details_information(self):
uuid = self._put_server_tags()
response = self._do_get('servers/%s' % uuid)
subs = self._get_regexes()
subs['hostid'] = '[a-f0-9]+'
subs['tag'] = '[0-9a-zA-Z]+'
subs['access_ip_v4'] = '1.2.3.4'
subs['access_ip_v6'] = '80fe::'
self._verify_response('server-tags-show-details-resp',
subs, response, 200)
def test_server_tags_list_with_details_information(self):
self._put_server_tags()
response = self._do_get('servers/detail')
subs = self._get_regexes()
subs['hostid'] = '[a-f0-9]+'
subs['tag'] = '[0-9a-zA-Z]+'
subs['access_ip_v4'] = '1.2.3.4'
subs['access_ip_v6'] = '80fe::'
self._verify_response('servers-tags-details-resp', subs, response, 200)
def test_server_tags_index(self):
uuid = self._put_server_tags()
response = self._do_get('servers/%s/tags' % uuid)
subs = self._get_regexes()
subs['tag'] = '[0-9a-zA-Z]+'
self._verify_response('server-tags-index-resp', subs, response, 200)
def test_server_tags_update(self):
uuid = self._put_server_tags()
tag = models.Tag()
tag.resource_id = uuid
tag.tag = 'OtherTag'
response = self._do_put('servers/%s/tags/%s' % (uuid, tag.tag),
'server-tags-put-req', {})
self.assertEqual(201, response.status_code)
expected_location = "%s/servers/%s/tags/%s" % (
self._get_vers_compute_endpoint(), uuid, tag.tag)
self.assertEqual(expected_location, response.headers['Location'])
def test_server_tags_delete(self):
uuid = self._put_server_tags()
response = self._do_delete('servers/%s/tags/%s' % (uuid, TAG))
self.assertEqual(204, response.status_code)
self.assertEqual('', response.content)
def test_server_tags_delete_all(self):
uuid = self._put_server_tags()
response = self._do_delete('servers/%s/tags' % uuid)
self.assertEqual(204, response.status_code)
self.assertEqual('', response.content)

View File

@ -32,6 +32,8 @@ NON_EXISTING_UUID = '123'
class ServerTagsTest(test.TestCase):
api_version = '2.26'
def setUp(self):
super(ServerTagsTest, self).setUp()
self.controller = server_tags.ServerTagsController()
@ -43,7 +45,7 @@ class ServerTagsTest(test.TestCase):
return tag
def _get_request(self, url, method):
request = fakes.HTTPRequest.blank(url)
request = fakes.HTTPRequest.blank(url, version=self.api_version)
request.method = method
return request

View File

@ -1462,6 +1462,65 @@ class ServersControllerTestV219(ServersControllerTest):
self._test_list_server_detail_with_descriptions('desc1', 'desc2')
class ServersControllerTestV226(ControllerTest):
wsgi_api_version = '2.26'
@mock.patch.object(compute_api.API, 'get')
def test_get_server_with_tags_by_id(self, mock_get):
req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID,
version=self.wsgi_api_version)
ctxt = req.environ['nova.context']
fake_server = fakes.stub_instance_obj(
ctxt, id=2, vm_state=vm_states.ACTIVE, progress=100)
tags = ['tag1', 'tag2']
tag_list = objects.TagList(objects=[
objects.Tag(resource_id=FAKE_UUID, tag=tag)
for tag in tags])
fake_server.tags = tag_list
mock_get.return_value = fake_server
res_dict = self.controller.show(req, FAKE_UUID)
self.assertIn('tags', res_dict['server'])
self.assertEqual(res_dict['server']['tags'], tags)
@mock.patch.object(compute_api.API, 'get_all')
def _test_get_servers_allows_tag_filters(self, filter_name, mock_get_all):
server_uuid = str(uuid.uuid4())
req = fakes.HTTPRequest.blank('/fake/servers?%s=t1,t2' % filter_name,
version=self.wsgi_api_version)
ctxt = req.environ['nova.context']
def fake_get_all(*a, **kw):
self.assertIsNotNone(kw['search_opts'])
self.assertIn(filter_name, kw['search_opts'])
self.assertEqual(kw['search_opts'][filter_name], ['t1', 't2'])
return objects.InstanceList(
objects=[fakes.stub_instance_obj(ctxt, uuid=server_uuid)])
mock_get_all.side_effect = fake_get_all
servers = self.controller.index(req)['servers']
self.assertEqual(len(servers), 1)
self.assertEqual(servers[0]['id'], server_uuid)
def test_get_servers_allows_tags_filter(self):
self._test_get_servers_allows_tag_filters('tags')
def test_get_servers_allows_tags_any_filter(self):
self._test_get_servers_allows_tag_filters('tags-any')
def test_get_servers_allows_not_tags_filter(self):
self._test_get_servers_allows_tag_filters('not-tags')
def test_get_servers_allows_not_tags_any_filter(self):
self._test_get_servers_allows_tag_filters('not-tags-any')
class ServersControllerDeleteTest(ControllerTest):
def setUp(self):

View File

@ -0,0 +1,4 @@
---
features:
- Microversion v2.26 allows to create/update/delete simple string tags.
They can be used for filtering servers by these tags.

View File

@ -142,6 +142,7 @@ nova.api.v21.extensions =
server_metadata = nova.api.openstack.compute.server_metadata:ServerMetadata
server_migrations = nova.api.openstack.compute.server_migrations:ServerMigrations
server_password = nova.api.openstack.compute.server_password:ServerPassword
server_tags = nova.api.openstack.compute.server_tags:ServerTags
server_usage = nova.api.openstack.compute.server_usage:ServerUsage
server_groups = nova.api.openstack.compute.server_groups:ServerGroups
servers = nova.api.openstack.compute.servers:Servers