servers list API support specify multi-status

Currently the service list API allows the user to specify an optional status
value to use as a filter - for example to limit the list to only servers with
a status of Active.

However often the user wants to filter the list by a set of status values,
for example list servers with a status of Active or Error,
which requires two separate API calls.

Allowing the API to accept a list of status values would reduce this to a
single API call.
Allow to specify status value for many times in a request.
For example::
    GET /v2/{tenant_id}/servers?status=ACTIVE&status=ERROR
    GET /v3/servers?status=ACTIVE&status=ERROR
V2 API extension::
    {
        "alias": "os-server-list-multi-status",
        "description": "Allow to filter the
            servers by a set of status values.",
        "links": [],
        "name": "ServerListMultiStatus",
        "namespace": "http://docs.openstack.org/compute/ext/
            os-server-list-multi-status/api/v2",
        "updated": "2014-05-11T00:00:00Z"
    }

DocImpact: Adds os-server-list-multi-status extension.

blueprint servers-list-support-multi-status

Change-Id: Id0109c56070e2f920be0f95738749aa969258bc1
This commit is contained in:
boh.ricky 2014-06-03 22:58:15 +08:00
parent 3b49adeba8
commit ab32995fed
24 changed files with 304 additions and 13 deletions

View File

@ -560,6 +560,14 @@
"namespace": "http://docs.openstack.org/compute/ext/servergroups/api/v2",
"updated": "2013-06-20T00:00:00Z"
},
{
"alias": "os-server-list-multi-status",
"description": "Allow to filter the servers by a set of status values.",
"links": [],
"name": "ServerListMultiStatus",
"namespace": "http://docs.openstack.org/compute/ext/os-server-list-multi-status/api/v2",
"updated": "2014-05-11T00:00:00Z"
},
{
"alias": "os-server-password",
"description": "Server password support.",
@ -665,4 +673,4 @@
"updated": "2011-03-25T00:00:00Z"
}
]
}
}

View File

@ -228,6 +228,9 @@
<extension alias="os-server-groups" updated="2013-06-20T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/servergroups/api/v2" name="ServerGroups">
<description>Server group support.</description>
</extension>
<extension alias="os-server-list-multi-status" updated="2014-05-11T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/os-server-list-multi-status/api/v2" name="ServerListMultiStatus">
<description>Allow to filter the servers by a set of status values.</description>
</extension>
<extension alias="os-server-password" updated="2012-11-29T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/server-password/api/v2" name="ServerPassword">
<description>Server password support.</description>
</extension>
@ -267,4 +270,4 @@
<extension alias="os-volumes" updated="2011-03-25T00:00:00Z" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
<description>Volumes support.</description>
</extension>
</extensions>
</extensions>

View File

@ -0,0 +1,16 @@
{
"server": {
"flavorRef": "http://openstack.example.com/openstack/flavors/1",
"imageRef": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"personality": [
{
"contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==",
"path": "/etc/banner.txt"
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "MVk5HPrazHcG",
"id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/5bbcc3c4-1da2-4437-a48a-66f15b1b13f9",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="ea8417a1-7957-4ed5-8b3c-0befc1724308" adminPass="FoFw84XtQj3c">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/ea8417a1-7957-4ed5-8b3c-0befc1724308" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/ea8417a1-7957-4ed5-8b3c-0befc1724308" rel="bookmark"/>
</server>

View File

@ -0,0 +1,18 @@
{
"servers": [
{
"id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "new-server-test"
}
]
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<servers xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1">
<server name="new-server-test" id="b626796d-d585-4874-b178-78c65289bba4">
<atom:link href="http://openstack.example.com/v2/openstack/servers/b626796d-d585-4874-b178-78c65289bba4" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/b626796d-d585-4874-b178-78c65289bba4" rel="bookmark"/>
</server>
</servers>

View File

@ -136,16 +136,17 @@ def status_from_state(vm_state, task_state='default'):
return status
def task_and_vm_state_from_status(status):
"""Map the server status string to list of vm states and
def task_and_vm_state_from_status(statuses):
"""Map the server's multiple status strings to list of vm states and
list of task states.
"""
vm_states = set()
task_states = set()
lower_statuses = [status.lower() for status in statuses]
for state, task_map in _STATE_MAP.iteritems():
for task_state, mapped_state in task_map.iteritems():
status_string = mapped_state
if status.lower() == status_string.lower():
if status_string.lower() in lower_statuses:
vm_states.add(state)
task_states.add(task_state)
# Add sort to avoid different order on set in Python 3

View File

@ -0,0 +1,25 @@
# 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.api.openstack import extensions
class Server_list_multi_status(extensions.ExtensionDescriptor):
"""Allow to specify multiple status values concurrently in the servers
list API..
"""
name = "ServerListMultiStatus"
alias = "os-server-list-multi-status"
namespace = ("http://docs.openstack.org/compute/ext/"
"os-server-list-multi-status/api/v2")
updated = "2014-05-11T00:00:00Z"

View File

@ -195,9 +195,11 @@ class ServersController(wsgi.Controller):
# Verify search by 'status' contains a valid status.
# Convert it to filter by vm_state or task_state for compute_api.
status = search_opts.pop('status', None)
if status is not None:
vm_state, task_state = common.task_and_vm_state_from_status(status)
search_opts.pop('status', None)
if 'status' in req.GET.keys():
statuses = req.GET.getall('status')
states = common.task_and_vm_state_from_status(statuses)
vm_state, task_state = states
if not vm_state and not task_state:
return {'servers': []}
search_opts['vm_state'] = vm_state

View File

@ -534,9 +534,11 @@ class Controller(wsgi.Controller):
# Verify search by 'status' contains a valid status.
# Convert it to filter by vm_state or task_state for compute_api.
status = search_opts.pop('status', None)
if status is not None:
vm_state, task_state = common.task_and_vm_state_from_status(status)
search_opts.pop('status', None)
if 'status' in req.GET.keys():
statuses = req.GET.getall('status')
states = common.task_and_vm_state_from_status(statuses)
vm_state, task_state = states
if not vm_state and not task_state:
return {'servers': []}
search_opts['vm_state'] = vm_state

View File

@ -235,6 +235,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"SecurityGroupDefaultRules",
"SecurityGroups",
"ServerDiagnostics",
"ServerListMultiStatus",
"ServerPassword",
"ServerStartStop",
"Services",

View File

@ -872,6 +872,51 @@ class ServersControllerTest(ControllerTest):
self.assertEqual(len(servers), 1)
self.assertEqual(servers[0]['id'], server_uuid)
@mock.patch.object(compute_api.API, 'get_all')
def test_get_servers_allows_multi_status(self, get_all_mock):
server_uuid0 = str(uuid.uuid4())
server_uuid1 = str(uuid.uuid4())
db_list = [fakes.stub_instance(100, uuid=server_uuid0),
fakes.stub_instance(101, uuid=server_uuid1)]
get_all_mock.return_value = instance_obj._make_instance_list(
context, instance_obj.InstanceList(), db_list, FIELDS)
req = fakes.HTTPRequest.blank(
'/fake/servers?status=active&status=error')
servers = self.controller.index(req)['servers']
self.assertEqual(2, len(servers))
self.assertEqual(server_uuid0, servers[0]['id'])
self.assertEqual(server_uuid1, servers[1]['id'])
expected_search_opts = dict(deleted=False,
vm_state=[vm_states.ACTIVE,
vm_states.ERROR],
project_id='fake')
get_all_mock.assert_called_once_with(mock.ANY,
search_opts=expected_search_opts, limit=mock.ANY,
marker=mock.ANY, want_objects=mock.ANY)
@mock.patch.object(compute_api.API, 'get_all')
def test_get_servers_allows_invalid_status(self, get_all_mock):
server_uuid0 = str(uuid.uuid4())
server_uuid1 = str(uuid.uuid4())
db_list = [fakes.stub_instance(100, uuid=server_uuid0),
fakes.stub_instance(101, uuid=server_uuid1)]
get_all_mock.return_value = instance_obj._make_instance_list(
context, instance_obj.InstanceList(), db_list, FIELDS)
req = fakes.HTTPRequest.blank(
'/fake/servers?status=active&status=invalid')
servers = self.controller.index(req)['servers']
self.assertEqual(2, len(servers))
self.assertEqual(server_uuid0, servers[0]['id'])
self.assertEqual(server_uuid1, servers[1]['id'])
expected_search_opts = dict(deleted=False,
vm_state=[vm_states.ACTIVE],
project_id='fake')
get_all_mock.assert_called_once_with(mock.ANY,
search_opts=expected_search_opts, limit=mock.ANY,
marker=mock.ANY, want_objects=mock.ANY)
def test_get_servers_allows_task_status(self):
server_uuid = str(uuid.uuid4())
task_state = task_states.REBOOTING

View File

@ -360,14 +360,14 @@ class MiscFunctionsTest(test.TestCase):
self.assertEqual(expected, actual)
def test_task_and_vm_state_from_status(self):
fixture1 = 'reboot'
fixture1 = ['reboot']
actual = common.task_and_vm_state_from_status(fixture1)
expected = [vm_states.ACTIVE], [task_states.REBOOT_PENDING,
task_states.REBOOT_STARTED,
task_states.REBOOTING]
self.assertEqual(expected, actual)
fixture2 = 'resize'
fixture2 = ['resize']
actual = common.task_and_vm_state_from_status(fixture2)
expected = ([vm_states.ACTIVE, vm_states.STOPPED],
[task_states.RESIZE_FINISH,
@ -376,6 +376,18 @@ class MiscFunctionsTest(test.TestCase):
task_states.RESIZE_PREP])
self.assertEqual(expected, actual)
fixture3 = ['resize', 'reboot']
actual = common.task_and_vm_state_from_status(fixture3)
expected = ([vm_states.ACTIVE, vm_states.STOPPED],
[task_states.REBOOT_PENDING,
task_states.REBOOT_STARTED,
task_states.REBOOTING,
task_states.RESIZE_FINISH,
task_states.RESIZE_MIGRATED,
task_states.RESIZE_MIGRATING,
task_states.RESIZE_PREP])
self.assertEqual(expected, actual)
class TestCollectionLinks(test.NoDBTestCase):
"""Tests the _get_collection_links method."""

View File

@ -536,6 +536,14 @@
"namespace": "http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1",
"updated": "%(isotime)s"
},
{
"alias": "os-server-list-multi-status",
"description": "%(text)s",
"links": [],
"name": "ServerListMultiStatus",
"namespace": "http://docs.openstack.org/compute/ext/os-server-list-multi-status/api/v2",
"updated": "%(isotime)s"
},
{
"alias": "os-server-password",
"description": "%(text)s",

View File

@ -192,6 +192,9 @@
<extension alias="os-server-diagnostics" updated="%(isotime)s" namespace="http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1" name="ServerDiagnostics">
<description>%(text)s</description>
</extension>
<extension alias="os-server-list-multi-status" updated="%(isotime)s" namespace="http://docs.openstack.org/compute/ext/os-server-list-multi-status/api/v2" name="ServerListMultiStatus">
<description>%(text)s</description>
</extension>
<extension alias="os-server-password" updated="%(isotime)s" namespace="http://docs.openstack.org/compute/ext/server-password/api/v2" name="ServerPassword">
<description>%(text)s</description>
</extension>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "%(host)s/openstack/images/%(image_id)s",
"flavorRef" : "%(host)s/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
<metadata/>
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
</server>

View File

@ -0,0 +1,18 @@
{
"servers": [
{
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(id)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(id)s",
"rel": "bookmark"
}
],
"name": "new-server-test"
}
]
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<servers xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1">
<server name="new-server-test" id="%(id)s">
<atom:link href="%(host)s/v2/openstack/servers/%(id)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(id)s" rel="bookmark"/>
</server>
</servers>

View File

@ -234,6 +234,23 @@ class ServersSampleHideAddressesXMLTest(ServersSampleHideAddressesJsonTest):
ctype = 'xml'
class ServersSampleMultiStatusJsonTest(ServersSampleBase):
extension_name = '.'.join(('nova.api.openstack.compute.contrib',
'server_list_multi_status',
'Server_list_multi_status'))
def test_servers_list(self):
uuid = self._post_server()
response = self._do_get('servers?status=active&status=error')
subs = self._get_regexes()
subs['id'] = uuid
self._verify_response('servers-list-resp', subs, response, 200)
class ServersSampleMultiStatusXMLTest(ServersSampleMultiStatusJsonTest):
ctype = 'xml'
class ServersMetadataJsonTest(ServersSampleBase):
def _create_and_set(self, subs):
uuid = self._post_server()