From e354a5693303a731ab47d008fbe3ce6a4fc7b50c Mon Sep 17 00:00:00 2001 From: Oleg Bondarev Date: Mon, 10 Jun 2013 16:40:41 +0400 Subject: [PATCH] Add "ExtendedVolumes" API extension - adds an API extension to include list of attached volumes with instance info - adds v3 api porting as well DocImpact Implements blueprint servers-add-volume-list Change-Id: If58dc40b093c2f61c6ae6b82fcd8f0bf53be464a --- .../all_extensions/extensions-get-resp.json | 8 ++ .../all_extensions/extensions-get-resp.xml | 3 + .../all_extensions/server-get-resp.json | 3 +- .../all_extensions/server-get-resp.xml | 2 +- .../all_extensions/servers-details-resp.json | 3 +- .../all_extensions/servers-details-resp.xml | 2 +- .../os-extended-volumes/server-get-resp.json | 58 +++++++++ .../os-extended-volumes/server-get-resp.xml | 21 ++++ .../os-extended-volumes/server-post-req.json | 16 +++ .../os-extended-volumes/server-post-req.xml | 19 +++ .../os-extended-volumes/server-post-resp.json | 16 +++ .../os-extended-volumes/server-post-resp.xml | 6 + .../servers-detail-resp.json | 60 ++++++++++ .../servers-detail-resp.xml | 23 ++++ etc/nova/policy.json | 2 + .../compute/contrib/extended_volumes.py | 103 ++++++++++++++++ .../compute/plugins/v3/extended_volumes.py | 105 ++++++++++++++++ .../compute/contrib/test_extended_volumes.py | 112 ++++++++++++++++++ .../plugins/v3/test_extended_volumes.py | 108 +++++++++++++++++ .../api/openstack/compute/test_extensions.py | 1 + nova/tests/api/openstack/fakes.py | 4 + nova/tests/fake_policy.py | 2 + .../extensions-get-resp.json.tpl | 8 ++ .../extensions-get-resp.xml.tpl | 3 + .../all_extensions/server-get-resp.json.tpl | 3 +- .../all_extensions/server-get-resp.xml.tpl | 2 +- .../servers-details-resp.json.tpl | 3 +- .../servers-details-resp.xml.tpl | 4 +- .../server-get-resp.json.tpl | 58 +++++++++ .../server-get-resp.xml.tpl | 21 ++++ .../server-post-req.json.tpl | 16 +++ .../server-post-req.xml.tpl | 19 +++ .../server-post-resp.json.tpl | 16 +++ .../server-post-resp.xml.tpl | 6 + .../servers-detail-resp.json.tpl | 59 +++++++++ .../servers-detail-resp.xml.tpl | 23 ++++ nova/tests/integrated/test_api_samples.py | 28 +++++ setup.cfg | 1 + 38 files changed, 938 insertions(+), 9 deletions(-) create mode 100644 doc/api_samples/os-extended-volumes/server-get-resp.json create mode 100644 doc/api_samples/os-extended-volumes/server-get-resp.xml create mode 100644 doc/api_samples/os-extended-volumes/server-post-req.json create mode 100644 doc/api_samples/os-extended-volumes/server-post-req.xml create mode 100644 doc/api_samples/os-extended-volumes/server-post-resp.json create mode 100644 doc/api_samples/os-extended-volumes/server-post-resp.xml create mode 100644 doc/api_samples/os-extended-volumes/servers-detail-resp.json create mode 100644 doc/api_samples/os-extended-volumes/servers-detail-resp.xml create mode 100644 nova/api/openstack/compute/contrib/extended_volumes.py create mode 100644 nova/api/openstack/compute/plugins/v3/extended_volumes.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_extended_volumes.py create mode 100644 nova/tests/api/openstack/compute/plugins/v3/test_extended_volumes.py create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 9d24626df4b9..3caa0e893bfd 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -64,6 +64,14 @@ "namespace": "http://docs.openstack.org/compute/ext/extended_status/api/v1.1", "updated": "2011-11-03T00:00:00+00:00" }, + { + "alias": "os-extended-volumes", + "description": "Extended Volumes support.", + "links": [], + "name": "ExtendedVolumes", + "namespace": "http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1", + "updated": "2013-06-07T00:00:00+00:00" + }, { "alias": "OS-EXT-VIF-NET", "description": "Adds network id parameter to the virtual interface list.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 36054fee8482..fd4fed4b1b45 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -24,6 +24,9 @@ Extended Status support. + + Extended Volumes support. + Adds network id parameter to the virtual interface list. diff --git a/doc/api_samples/all_extensions/server-get-resp.json b/doc/api_samples/all_extensions/server-get-resp.json index d5b99502757f..f3b679998eb6 100644 --- a/doc/api_samples/all_extensions/server-get-resp.json +++ b/doc/api_samples/all_extensions/server-get-resp.json @@ -68,6 +68,7 @@ "status": "ACTIVE", "tenant_id": "openstack", "updated": "2013-05-02T19:13:48Z", - "user_id": "fake" + "user_id": "fake", + "os-extended-volumes:volumes_attached": [] } } \ No newline at end of file diff --git a/doc/api_samples/all_extensions/server-get-resp.xml b/doc/api_samples/all_extensions/server-get-resp.xml index 42f62cfeb8f9..42fd2dd8cd43 100644 --- a/doc/api_samples/all_extensions/server-get-resp.xml +++ b/doc/api_samples/all_extensions/server-get-resp.xml @@ -1,5 +1,5 @@ - + diff --git a/doc/api_samples/all_extensions/servers-details-resp.json b/doc/api_samples/all_extensions/servers-details-resp.json index 038df104e362..552ba957c2bb 100644 --- a/doc/api_samples/all_extensions/servers-details-resp.json +++ b/doc/api_samples/all_extensions/servers-details-resp.json @@ -69,7 +69,8 @@ "status": "ACTIVE", "tenant_id": "openstack", "updated": "2013-05-02T19:14:01Z", - "user_id": "fake" + "user_id": "fake", + "os-extended-volumes:volumes_attached": [] } ] } \ No newline at end of file diff --git a/doc/api_samples/all_extensions/servers-details-resp.xml b/doc/api_samples/all_extensions/servers-details-resp.xml index 45a96d34976f..e58aa258081d 100644 --- a/doc/api_samples/all_extensions/servers-details-resp.xml +++ b/doc/api_samples/all_extensions/servers-details-resp.xml @@ -1,5 +1,5 @@ - + diff --git a/doc/api_samples/os-extended-volumes/server-get-resp.json b/doc/api_samples/os-extended-volumes/server-get-resp.json new file mode 100644 index 000000000000..ce9e6afa68eb --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-get-resp.json @@ -0,0 +1,58 @@ +{ + "server": { + "accessIPv4": "", + "accessIPv6": "", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "created": "2013-02-07T19:35:09Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "570eff4776ab310707d11d181037337197086998a8b3305c90bf87c8", + "id": "ecb5e433-fa75-4db2-af3d-a29ae8618edc", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/ecb5e433-fa75-4db2-af3d-a29ae8618edc", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/ecb5e433-fa75-4db2-af3d-a29ae8618edc", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2013-02-07T19:35:10Z", + "user_id": "fake", + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1"}, + {"id": "volume_id2"} + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/os-extended-volumes/server-get-resp.xml b/doc/api_samples/os-extended-volumes/server-get-resp.xml new file mode 100644 index 000000000000..ea839a2af396 --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-get-resp.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Apache1 + + + + + + + + + + + \ No newline at end of file diff --git a/doc/api_samples/os-extended-volumes/server-post-req.json b/doc/api_samples/os-extended-volumes/server-post-req.json new file mode 100644 index 000000000000..09366b4c934e --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-post-req.json @@ -0,0 +1,16 @@ +{ + "server" : { + "name" : "new-server-test", + "imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef" : "http://openstack.example.com/openstack/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/doc/api_samples/os-extended-volumes/server-post-req.xml b/doc/api_samples/os-extended-volumes/server-post-req.xml new file mode 100644 index 000000000000..077dd7618cf4 --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-post-req.xml @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + diff --git a/doc/api_samples/os-extended-volumes/server-post-resp.json b/doc/api_samples/os-extended-volumes/server-post-resp.json new file mode 100644 index 000000000000..db9ed3d6d1ed --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-post-resp.json @@ -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" + } + ] + } +} diff --git a/doc/api_samples/os-extended-volumes/server-post-resp.xml b/doc/api_samples/os-extended-volumes/server-post-resp.xml new file mode 100644 index 000000000000..68f0933c7710 --- /dev/null +++ b/doc/api_samples/os-extended-volumes/server-post-resp.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/doc/api_samples/os-extended-volumes/servers-detail-resp.json b/doc/api_samples/os-extended-volumes/servers-detail-resp.json new file mode 100644 index 000000000000..9bfd77685aae --- /dev/null +++ b/doc/api_samples/os-extended-volumes/servers-detail-resp.json @@ -0,0 +1,60 @@ +{ + "servers": [ + { + "accessIPv4": "", + "accessIPv6": "", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "created": "2012-12-05T07:34:10Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "585aa01f94eca692eff9f77ffe3eab866d8a819e97397e28c5c7df12", + "id": "030758aa-5c41-41c6-8fb4-29d44eb96a85", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/030758aa-5c41-41c6-8fb4-29d44eb96a85", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/030758aa-5c41-41c6-8fb4-29d44eb96a85", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-12-05T07:34:10Z", + "user_id": "fake", + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1"}, + {"id": "volume_id2"} + ] + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/os-extended-volumes/servers-detail-resp.xml b/doc/api_samples/os-extended-volumes/servers-detail-resp.xml new file mode 100644 index 000000000000..e95c17b44d35 --- /dev/null +++ b/doc/api_samples/os-extended-volumes/servers-detail-resp.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + Apache1 + + + + + + + + + + + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 7bc4023bd123..d1f8234cd390 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -58,6 +58,8 @@ "compute_extension:extended_ips_mac": "", "compute_extension:extended_vif_net": "", "compute_extension:v3:extension_info:discoverable": "", + "compute_extension:extended_volumes": "", + "compute_extension:v3:os-extended-volumes": "", "compute_extension:fixed_ips": "rule:admin_api", "compute_extension:v3:os-fixed-ips:discoverable": "", "compute_extension:v3:os-fixed-ips": "rule:admin_api", diff --git a/nova/api/openstack/compute/contrib/extended_volumes.py b/nova/api/openstack/compute/contrib/extended_volumes.py new file mode 100644 index 000000000000..22963c49e088 --- /dev/null +++ b/nova/api/openstack/compute/contrib/extended_volumes.py @@ -0,0 +1,103 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""The Extended Volumes API extension.""" + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute + +authorize = extensions.soft_extension_authorizer('compute', 'extended_volumes') + + +class ExtendedVolumesController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(ExtendedVolumesController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + def _extend_server(self, context, server, instance): + bdms = self.compute_api.get_instance_bdms(context, instance) + volume_ids = [bdm['volume_id'] for bdm in bdms if bdm['volume_id']] + key = "%s:volumes_attached" % Extended_volumes.alias + server[key] = [{'id': volume_id} for volume_id in volume_ids] + + @wsgi.extends + def show(self, req, resp_obj, id): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=ExtendedVolumesServerTemplate()) + server = resp_obj.obj['server'] + db_instance = req.get_db_instance(server['id']) + # server['id'] is guaranteed to be in the cache due to + # the core API adding it in its 'show' method. + self._extend_server(context, server, db_instance) + + @wsgi.extends + def detail(self, req, resp_obj): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=ExtendedVolumesServersTemplate()) + servers = list(resp_obj.obj['servers']) + for server in servers: + db_instance = req.get_db_instance(server['id']) + # server['id'] is guaranteed to be in the cache due to + # the core API adding it in its 'detail' method. + self._extend_server(context, server, db_instance) + + +class Extended_volumes(extensions.ExtensionDescriptor): + """Extended Volumes support.""" + + name = "ExtendedVolumes" + alias = "os-extended-volumes" + namespace = ("http://docs.openstack.org/compute/ext/" + "extended_volumes/api/v1.1") + updated = "2013-06-07T00:00:00+00:00" + + def get_controller_extensions(self): + controller = ExtendedVolumesController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] + + def get_resources(self): + return [] + + +def make_server(elem): + volumes = xmlutil.SubTemplateElement( + elem, '{%s}volume_attached' % Extended_volumes.namespace, + selector='%s:volumes_attached' % Extended_volumes.alias) + volumes.set('id') + + +class ExtendedVolumesServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + make_server(root) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + Extended_volumes.alias: Extended_volumes.namespace}) + + +class ExtendedVolumesServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + Extended_volumes.alias: Extended_volumes.namespace}) diff --git a/nova/api/openstack/compute/plugins/v3/extended_volumes.py b/nova/api/openstack/compute/plugins/v3/extended_volumes.py new file mode 100644 index 000000000000..be3c324ae701 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/extended_volumes.py @@ -0,0 +1,105 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""The Extended Volumes API extension.""" + +from nova.api.openstack.compute import servers +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute + +ALIAS = "os-extended-volumes" +authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS) + + +class ExtendedVolumesController(servers.Controller): + def __init__(self, *args, **kwargs): + super(ExtendedVolumesController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + def _extend_server(self, context, server, instance): + bdms = self.compute_api.get_instance_bdms(context, instance) + volume_ids = [bdm['volume_id'] for bdm in bdms if bdm['volume_id']] + key = "%s:volumes_attached" % ExtendedVolumes.alias + server[key] = [{'id': volume_id} for volume_id in volume_ids] + + @wsgi.extends + def show(self, req, resp_obj, id): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=ExtendedVolumesServerTemplate()) + server = resp_obj.obj['server'] + db_instance = req.get_db_instance(server['id']) + # server['id'] is guaranteed to be in the cache due to + # the core API adding it in its 'show' method. + self._extend_server(context, server, db_instance) + + @wsgi.extends + def detail(self, req, resp_obj): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=ExtendedVolumesServersTemplate()) + servers = list(resp_obj.obj['servers']) + for server in servers: + db_instance = req.get_db_instance(server['id']) + # server['id'] is guaranteed to be in the cache due to + # the core API adding it in its 'detail' method. + self._extend_server(context, server, db_instance) + + +class ExtendedVolumes(extensions.V3APIExtensionBase): + """Extended Volumes support.""" + + name = "ExtendedVolumes" + alias = ALIAS + namespace = ("http://docs.openstack.org/compute/ext/" + "extended_volumes/api/v3") + version = 1 + + def get_controller_extensions(self): + controller = ExtendedVolumesController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] + + def get_resources(self): + return [] + + +def make_server(elem): + volumes = xmlutil.SubTemplateElement( + elem, '{%s}volume_attached' % ExtendedVolumes.namespace, + selector='%s:volumes_attached' % ExtendedVolumes.alias) + volumes.set('id') + + +class ExtendedVolumesServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + make_server(root) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + ExtendedVolumes.alias: ExtendedVolumes.namespace}) + + +class ExtendedVolumesServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + ExtendedVolumes.alias: ExtendedVolumes.namespace}) diff --git a/nova/tests/api/openstack/compute/contrib/test_extended_volumes.py b/nova/tests/api/openstack/compute/contrib/test_extended_volumes.py new file mode 100644 index 000000000000..a7201667668c --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_extended_volumes.py @@ -0,0 +1,112 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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 lxml import etree +import webob + +from nova.api.openstack.compute.contrib import extended_volumes +from nova import compute +from nova.objects import instance as instance_obj +from nova.openstack.common import jsonutils +from nova import test +from nova.tests.api.openstack import fakes + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + return fakes.stub_instance(1, uuid=UUID1) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [fakes.stub_instance(1), fakes.stub_instance(2)] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + instance_obj.InstanceList(), + db_list, fields) + + +def fake_compute_get_instance_bdms(*args, **kwargs): + return [{'volume_id': UUID1}, {'volume_id': UUID2}] + + +class ExtendedVolumesTest(test.TestCase): + content_type = 'application/json' + prefix = 'os-extended-volumes:' + + def setUp(self): + super(ExtendedVolumesTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(compute.api.API, 'get_instance_bdms', + fake_compute_get_instance_bdms) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_volumes']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def test_show(self): + url = '/v2/fake/servers/%s' % UUID1 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + def test_detail(self): + url = '/v2/fake/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + for i, server in enumerate(self._get_servers(res.body)): + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + +class ExtendedVolumesXmlTest(ExtendedVolumesTest): + content_type = 'application/xml' + prefix = '{%s}' % extended_volumes.Extended_volumes.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_extended_volumes.py b/nova/tests/api/openstack/compute/plugins/v3/test_extended_volumes.py new file mode 100644 index 000000000000..4b7572ecc205 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_extended_volumes.py @@ -0,0 +1,108 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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 lxml import etree +import webob + +from nova.api.openstack.compute.plugins.v3 import extended_volumes +from nova import compute +from nova.openstack.common import jsonutils +from nova import test +from nova.tests.api.openstack import fakes + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + return fakes.stub_instance(1, uuid=UUID1) + + +def fake_compute_get_all(*args, **kwargs): + return [fakes.stub_instance(1), fakes.stub_instance(2)] + + +def fake_compute_get_instance_bdms(*args, **kwargs): + return [{'volume_id': UUID1}, {'volume_id': UUID2}] + + +class ExtendedVolumesTest(test.TestCase): + content_type = 'application/json' + prefix = 'os-extended-volumes:' + + def setUp(self): + super(ExtendedVolumesTest, self).setUp() + self.Controller = extended_volumes.ExtendedVolumesController() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(compute.api.API, 'get_instance_bdms', + fake_compute_get_instance_bdms) + self.app = fakes.wsgi_app_v3(init_only=('os-extended-volumes', + 'servers')) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(self.app) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def test_show(self): + url = '/v3/servers/%s' % UUID1 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + def test_detail(self): + url = '/v3/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + for i, server in enumerate(self._get_servers(res.body)): + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + +class ExtendedVolumesXmlTest(ExtendedVolumesTest): + content_type = 'application/xml' + prefix = '{%s}' % extended_volumes.ExtendedVolumes.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index 69952943e2a4..f5ecf3255494 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -193,6 +193,7 @@ class ExtensionControllerTest(ExtensionTestCase): "ExtendedVIFNet", "Evacuate", "ExtendedStatus", + "ExtendedVolumes", "ExtendedServerAttributes", "FixedIPs", "FlavorAccess", diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index aa8d3540e7d8..72f83acd7984 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -678,3 +678,7 @@ def stub_snapshot_get_all(self, context): return [stub_snapshot(100, project_id='fake'), stub_snapshot(101, project_id='superfake'), stub_snapshot(102, project_id='superduperfake')] + + +def stub_bdm_get_all_by_instance(context, instance_uuid): + return [{'volume_id': 'volume_id1'}, {'volume_id': 'volume_id2'}] diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index cf97540eb036..9160d4bd6416 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -136,6 +136,8 @@ policy_data = """ "compute_extension:extended_ips": "", "compute_extension:extended_ips_mac": "", "compute_extension:extended_vif_net": "", + "compute_extension:extended_volumes": "", + "compute_extension:v3:os-extended-volumes": "", "compute_extension:fixed_ips": "", "compute_extension:v3:os-fixed-ips": "", "compute_extension:flavor_access": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 6b5eee37ccfa..23ce1f611cec 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -64,6 +64,14 @@ "namespace": "http://docs.openstack.org/compute/ext/extended_status/api/v1.1", "updated": "%(timestamp)s" }, + { + "alias": "os-extended-volumes", + "description": "%(text)s", + "links": [], + "name": "ExtendedVolumes", + "namespace": "http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1", + "updated": "%(timestamp)s" + }, { "alias": "OS-EXT-VIF-NET", "description": "%(text)s", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index cb01ad50b79c..31a755ec2e2b 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -24,6 +24,9 @@ %(text)s + + %(text)s + %(text)s diff --git a/nova/tests/integrated/api_samples/all_extensions/server-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/server-get-resp.json.tpl index c05cbc557a4e..ca6967965d87 100644 --- a/nova/tests/integrated/api_samples/all_extensions/server-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/server-get-resp.json.tpl @@ -68,6 +68,7 @@ "status": "ACTIVE", "tenant_id": "openstack", "updated": "%(timestamp)s", - "user_id": "fake" + "user_id": "fake", + "os-extended-volumes:volumes_attached": [] } } diff --git a/nova/tests/integrated/api_samples/all_extensions/server-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/server-get-resp.xml.tpl index 176d6a834fec..e82dc92611ba 100644 --- a/nova/tests/integrated/api_samples/all_extensions/server-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/server-get-resp.xml.tpl @@ -1,5 +1,5 @@ - + diff --git a/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.json.tpl index f5d2614265e3..a4e9c40db50e 100644 --- a/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.json.tpl @@ -69,7 +69,8 @@ "status": "ACTIVE", "tenant_id": "openstack", "updated": "%(timestamp)s", - "user_id": "fake" + "user_id": "fake", + "os-extended-volumes:volumes_attached": [] } ] } diff --git a/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.xml.tpl index f7eba82bb3fa..67ced058cf84 100644 --- a/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/servers-details-resp.xml.tpl @@ -1,6 +1,6 @@ - - + + diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.json.tpl new file mode 100644 index 000000000000..19cdaa095bc0 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.json.tpl @@ -0,0 +1,58 @@ +{ + "server": { + "accessIPv4": "", + "accessIPv6": "", + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "version": 4 + } + ] + }, + "created": "%(timestamp)s", + "flavor": { + "id": "1", + "links": [ + { + "href": "%(host)s/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "%(hostid)s", + "id": "%(id)s", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(host)s/openstack/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "%(host)s/v2/openstack/servers/%(id)s", + "rel": "self" + }, + { + "href": "%(host)s/openstack/servers/%(id)s", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "%(timestamp)s", + "user_id": "fake", + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1"}, + {"id": "volume_id2"} + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.xml.tpl new file mode 100644 index 000000000000..0a3cba701ba0 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-get-resp.xml.tpl @@ -0,0 +1,21 @@ + + + + + + + + + + Apache1 + + + + + + + + + + + diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.json.tpl new file mode 100644 index 000000000000..d3916d1aa68a --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.json.tpl @@ -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==" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.xml.tpl new file mode 100644 index 000000000000..f92614984242 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-req.xml.tpl @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.json.tpl new file mode 100644 index 000000000000..d5f030c8730b --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.json.tpl @@ -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" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.xml.tpl new file mode 100644 index 000000000000..3bb13e69bd6d --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/server-post-resp.xml.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.json.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.json.tpl new file mode 100644 index 000000000000..3f59858bd90e --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.json.tpl @@ -0,0 +1,59 @@ +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "%(timestamp)s", + "user_id": "fake", + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "version": 4 + } + ] + }, + "links": [ + { + "href": "%(host)s/v2/openstack/servers/%(id)s", + "rel": "self" + }, + { + "href": "%(host)s/openstack/servers/%(id)s", + "rel": "bookmark" + } + ], + "created": "%(timestamp)s", + "name": "new-server-test", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(host)s/openstack/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "id": "%(uuid)s", + "accessIPv4": "", + "accessIPv6": "", + "tenant_id": "openstack", + "progress": 0, + "flavor": { + "id": "1", + "links": [ + { + "href": "%(host)s/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "%(hostid)s", + "metadata": { + "My Server Name": "Apache1" + }, + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1"}, + {"id": "volume_id2"} + ] + }] +} diff --git a/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.xml.tpl b/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.xml.tpl new file mode 100644 index 000000000000..9e63eabff11c --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-volumes/servers-detail-resp.xml.tpl @@ -0,0 +1,23 @@ + + + + + + + + + + + Apache1 + + + + + + + + + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 43815ef49a81..7958d544f2da 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -2448,6 +2448,34 @@ class ExtendedStatusSampleXmlTests(ExtendedStatusSampleJsonTests): ctype = 'xml' +class ExtendedVolumesSampleJsonTests(ServersSampleBase): + extension_name = ("nova.api.openstack.compute.contrib" + ".extended_volumes.Extended_volumes") + + def test_show(self): + uuid = self._post_server() + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fakes.stub_bdm_get_all_by_instance) + response = self._do_get('servers/%s' % uuid) + subs = self._get_regexes() + subs['hostid'] = '[a-f0-9]+' + self._verify_response('server-get-resp', subs, response, 200) + + def test_detail(self): + uuid = self._post_server() + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fakes.stub_bdm_get_all_by_instance) + response = self._do_get('servers/detail') + subs = self._get_regexes() + subs['id'] = uuid + subs['hostid'] = '[a-f0-9]+' + self._verify_response('servers-detail-resp', subs, response, 200) + + +class ExtendedVolumesSampleXmlTests(ExtendedVolumesSampleJsonTests): + ctype = 'xml' + + class ServerUsageSampleJsonTests(ServersSampleBase): extension_name = ("nova.api.openstack.compute.contrib" ".server_usage.Server_usage") diff --git a/setup.cfg b/setup.cfg index 0e889f371245..d1962c6236e2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,6 +62,7 @@ nova.api.v3.extensions = coverage = nova.api.openstack.compute.plugins.v3.coverage:Coverage evacuate = nova.api.openstack.compute.plugins.v3.evacuate:Evacuate extended_server_attributes = nova.api.openstack.compute.plugins.v3.extended_server_attributes:ExtendedServerAttributes + extended_volumes = nova.api.openstack.compute.plugins.v3.extended_volumes:ExtendedVolumes extension_info = nova.api.openstack.compute.plugins.v3.extension_info:ExtensionInfo fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs flavors = nova.api.openstack.compute.plugins.v3.flavors:Flavors