From fb37f5453cf6f4ebb3d54abe3ba8cb3c0c6bb1a2 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 15 Aug 2014 15:49:10 +0930 Subject: [PATCH] Port v2 image_metadata extension to work in v2.1(v3) framework Port v2 image_metadata extension and adapts it to the v2.1/v3 API framework. API behaviour is identical with the exception that there is no support for XML. Also - unittest code modified to share existing testing with both v2/v2.1 - Adds expected error decorators for API methods - Adds API samples Partially implements blueprint v2-on-v3-api Change-Id: Ibc8dc897f3449a1c70bc7ac1510445f48fddb291 --- doc/v3/api_samples/images/image-get-resp.json | 33 +++ .../images/image-meta-key-get.json | 5 + .../images/image-meta-key-put-req.json | 5 + .../images/image-meta-key-put-resp.json | 5 + .../images/image-metadata-get-resp.json | 8 + .../images/image-metadata-post-req.json | 6 + .../images/image-metadata-post-resp.json | 9 + .../images/image-metadata-put-req.json | 6 + .../images/image-metadata-put-resp.json | 6 + .../images/images-details-get-resp.json | 212 ++++++++++++++++++ .../images/images-list-get-resp.json | 137 +++++++++++ .../compute/plugins/v3/image_metadata.py | 157 +++++++++++++ .../openstack/compute/test_image_metadata.py | 30 ++- .../images/image-get-resp.json.tpl | 33 +++ .../images/image-meta-key-get.json.tpl | 5 + .../images/image-meta-key-put-req.json.tpl | 5 + .../images/image-meta-key-put-resp.json.tpl | 5 + .../images/image-metadata-get-resp.json.tpl | 8 + .../images/image-metadata-post-req.json.tpl | 6 + .../images/image-metadata-post-resp.json.tpl | 9 + .../images/image-metadata-put-req.json.tpl | 6 + .../images/image-metadata-put-resp.json.tpl | 6 + .../images/images-details-get-resp.json.tpl | 212 ++++++++++++++++++ .../images/images-list-get-resp.json.tpl | 137 +++++++++++ nova/tests/integrated/v3/test_images.py | 85 +++++++ setup.cfg | 1 + 26 files changed, 1133 insertions(+), 4 deletions(-) create mode 100644 doc/v3/api_samples/images/image-get-resp.json create mode 100644 doc/v3/api_samples/images/image-meta-key-get.json create mode 100644 doc/v3/api_samples/images/image-meta-key-put-req.json create mode 100644 doc/v3/api_samples/images/image-meta-key-put-resp.json create mode 100644 doc/v3/api_samples/images/image-metadata-get-resp.json create mode 100644 doc/v3/api_samples/images/image-metadata-post-req.json create mode 100644 doc/v3/api_samples/images/image-metadata-post-resp.json create mode 100644 doc/v3/api_samples/images/image-metadata-put-req.json create mode 100644 doc/v3/api_samples/images/image-metadata-put-resp.json create mode 100644 doc/v3/api_samples/images/images-details-get-resp.json create mode 100644 doc/v3/api_samples/images/images-list-get-resp.json create mode 100644 nova/api/openstack/compute/plugins/v3/image_metadata.py create mode 100644 nova/tests/integrated/v3/api_samples/images/image-get-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-meta-key-get.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-meta-key-put-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-meta-key-put-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-metadata-get-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-metadata-post-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-metadata-post-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-metadata-put-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/image-metadata-put-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/images-details-get-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/images/images-list-get-resp.json.tpl create mode 100644 nova/tests/integrated/v3/test_images.py diff --git a/doc/v3/api_samples/images/image-get-resp.json b/doc/v3/api_samples/images/image-get-resp.json new file mode 100644 index 0000000000..9b1abf02d2 --- /dev/null +++ b/doc/v3/api_samples/images/image-get-resp.json @@ -0,0 +1,33 @@ +{ + "image": { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-meta-key-get.json b/doc/v3/api_samples/images/image-meta-key-get.json new file mode 100644 index 0000000000..6d022eb97d --- /dev/null +++ b/doc/v3/api_samples/images/image-meta-key-get.json @@ -0,0 +1,5 @@ +{ + "meta": { + "kernel_id": "nokernel" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-meta-key-put-req.json b/doc/v3/api_samples/images/image-meta-key-put-req.json new file mode 100644 index 0000000000..01528f1ce6 --- /dev/null +++ b/doc/v3/api_samples/images/image-meta-key-put-req.json @@ -0,0 +1,5 @@ +{ + "meta": { + "auto_disk_config": "False" + } +} diff --git a/doc/v3/api_samples/images/image-meta-key-put-resp.json b/doc/v3/api_samples/images/image-meta-key-put-resp.json new file mode 100644 index 0000000000..3db563ec14 --- /dev/null +++ b/doc/v3/api_samples/images/image-meta-key-put-resp.json @@ -0,0 +1,5 @@ +{ + "meta": { + "auto_disk_config": "False" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-metadata-get-resp.json b/doc/v3/api_samples/images/image-metadata-get-resp.json new file mode 100644 index 0000000000..588f688d5a --- /dev/null +++ b/doc/v3/api_samples/images/image-metadata-get-resp.json @@ -0,0 +1,8 @@ +{ + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-metadata-post-req.json b/doc/v3/api_samples/images/image-metadata-post-req.json new file mode 100644 index 0000000000..8447c3dec0 --- /dev/null +++ b/doc/v3/api_samples/images/image-metadata-post-req.json @@ -0,0 +1,6 @@ +{ + "metadata": { + "kernel_id": "False", + "Label": "UpdatedImage" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-metadata-post-resp.json b/doc/v3/api_samples/images/image-metadata-post-resp.json new file mode 100644 index 0000000000..9479bb3395 --- /dev/null +++ b/doc/v3/api_samples/images/image-metadata-post-resp.json @@ -0,0 +1,9 @@ +{ + "metadata": { + "Label": "UpdatedImage", + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "False", + "ramdisk_id": "nokernel" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-metadata-put-req.json b/doc/v3/api_samples/images/image-metadata-put-req.json new file mode 100644 index 0000000000..36fbc003dc --- /dev/null +++ b/doc/v3/api_samples/images/image-metadata-put-req.json @@ -0,0 +1,6 @@ +{ + "metadata": { + "auto_disk_config": "True", + "Label": "Changed" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/image-metadata-put-resp.json b/doc/v3/api_samples/images/image-metadata-put-resp.json new file mode 100644 index 0000000000..c8c5ee9c4a --- /dev/null +++ b/doc/v3/api_samples/images/image-metadata-put-resp.json @@ -0,0 +1,6 @@ +{ + "metadata": { + "Label": "Changed", + "auto_disk_config": "True" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/images-details-get-resp.json b/doc/v3/api_samples/images/images-details-get-resp.json new file mode 100644 index 0000000000..91e9750def --- /dev/null +++ b/doc/v3/api_samples/images/images-details-get-resp.json @@ -0,0 +1,212 @@ +{ + "images": [ + { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "a2459075-d96c-40d5-893e-577ff92e721c", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "a440c04b-79fa-479c-bed1-0b816eaec379", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "False", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage6", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "links": [ + { + "href": "http://openstack.example.com/v3/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "ramdisk_id": null + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "cedef40a-ed67-4d10-800e-17455edce175", + "links": [ + { + "href": "http://openstack.example.com/v3/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + } + ] +} \ No newline at end of file diff --git a/doc/v3/api_samples/images/images-list-get-resp.json b/doc/v3/api_samples/images/images-list-get-resp.json new file mode 100644 index 0000000000..19b39fdc5a --- /dev/null +++ b/doc/v3/api_samples/images/images-list-get-resp.json @@ -0,0 +1,137 @@ +{ + "images": [ + { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage7" + }, + { + "id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "a2459075-d96c-40d5-893e-577ff92e721c", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "a440c04b-79fa-479c-bed1-0b816eaec379", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage6" + }, + { + "id": "c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "links": [ + { + "href": "http://openstack.example.com/v3/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "cedef40a-ed67-4d10-800e-17455edce175", + "links": [ + { + "href": "http://openstack.example.com/v3/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + } + ] +} \ No newline at end of file diff --git a/nova/api/openstack/compute/plugins/v3/image_metadata.py b/nova/api/openstack/compute/plugins/v3/image_metadata.py new file mode 100644 index 0000000000..e9273f4377 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/image_metadata.py @@ -0,0 +1,157 @@ +# Copyright 2011 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 webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import exception +from nova.i18n import _ +import nova.image + +ALIAS = 'image-metadata' + + +class ImageMetadataController(object): + """The image metadata API controller for the OpenStack API.""" + + def __init__(self): + self.image_api = nova.image.API() + + def _get_image(self, context, image_id): + try: + return self.image_api.get(context, image_id) + except exception.ImageNotAuthorized as e: + raise exc.HTTPForbidden(explanation=e.format_message()) + except exception.ImageNotFound: + msg = _("Image not found.") + raise exc.HTTPNotFound(explanation=msg) + + @extensions.expected_errors((403, 404)) + def index(self, req, image_id): + """Returns the list of metadata for a given instance.""" + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + return dict(metadata=metadata) + + @extensions.expected_errors((403, 404)) + def show(self, req, image_id, id): + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + if id in metadata: + return {'meta': {id: metadata[id]}} + else: + raise exc.HTTPNotFound() + + @extensions.expected_errors((400, 403, 404, 413)) + def create(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if 'metadata' in body: + for key, value in body['metadata'].iteritems(): + image['properties'][key] = value + common.check_img_metadata_properties_quota(context, + image['properties']) + try: + image = self.image_api.update(context, image_id, image, data=None, + purge_props=True) + except exception.ImageNotAuthorized as e: + raise exc.HTTPForbidden(explanation=e.format_message()) + return dict(metadata=image['properties']) + + @extensions.expected_errors((400, 403, 404, 413)) + def update(self, req, image_id, id, body): + context = req.environ['nova.context'] + + try: + meta = body['meta'] + except KeyError: + expl = _('Incorrect request body format') + raise exc.HTTPBadRequest(explanation=expl) + + if id not in meta: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(meta) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + + image = self._get_image(context, image_id) + image['properties'][id] = meta[id] + common.check_img_metadata_properties_quota(context, + image['properties']) + try: + self.image_api.update(context, image_id, image, data=None, + purge_props=True) + except exception.ImageNotAuthorized as e: + raise exc.HTTPForbidden(explanation=e.format_message()) + return dict(meta=meta) + + @extensions.expected_errors((400, 403, 404, 413)) + def update_all(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + metadata = body.get('metadata', {}) + common.check_img_metadata_properties_quota(context, metadata) + image['properties'] = metadata + try: + self.image_api.update(context, image_id, image, data=None, + purge_props=True) + except exception.ImageNotAuthorized as e: + raise exc.HTTPForbidden(explanation=e.format_message()) + return dict(metadata=metadata) + + @extensions.expected_errors((403, 404)) + @wsgi.response(204) + def delete(self, req, image_id, id): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if id not in image['properties']: + msg = _("Invalid metadata key") + raise exc.HTTPNotFound(explanation=msg) + image['properties'].pop(id) + try: + self.image_api.update(context, image_id, image, data=None, + purge_props=True) + except exception.ImageNotAuthorized as e: + raise exc.HTTPForbidden(explanation=e.format_message()) + + +class ImageMetadata(extensions.V3APIExtensionBase): + """Image Metadata API.""" + name = "ImageMetadata" + alias = ALIAS + version = 1 + + def get_resources(self): + parent = {'member_name': 'image', + 'collection_name': 'images'} + resources = [extensions.ResourceExtension('metadata', + ImageMetadataController(), + member_name='image_meta', + parent=parent, + custom_routes_fn= + self.image_metadata_map + )] + return resources + + def get_controller_extensions(self): + return [] + + def image_metadata_map(self, mapper, wsgi_resource): + mapper.connect("metadata", "/images/{image_id}/metadata", + controller=wsgi_resource, + action='update_all', conditions={"method": ['PUT']}) diff --git a/nova/tests/api/openstack/compute/test_image_metadata.py b/nova/tests/api/openstack/compute/test_image_metadata.py index ac43c7768c..2fbf6d777a 100644 --- a/nova/tests/api/openstack/compute/test_image_metadata.py +++ b/nova/tests/api/openstack/compute/test_image_metadata.py @@ -19,6 +19,8 @@ import mock import webob from nova.api.openstack.compute import image_metadata +from nova.api.openstack.compute.plugins.v3 import image_metadata \ + as image_metadata_v21 from nova import exception from nova.openstack.common import jsonutils from nova import test @@ -33,11 +35,12 @@ def get_image_123(): return copy.deepcopy(IMAGE_FIXTURES)[0] -class ImageMetaDataTest(test.NoDBTestCase): +class ImageMetaDataTestV21(test.NoDBTestCase): + controller_class = image_metadata_v21.ImageMetadataController def setUp(self): - super(ImageMetaDataTest, self).setUp() - self.controller = image_metadata.Controller() + super(ImageMetaDataTestV21, self).setUp() + self.controller = self.controller_class() @mock.patch('nova.image.api.API.get', return_value=get_image_123()) def test_index(self, get_all_mocked): @@ -243,7 +246,7 @@ class ImageMetaDataTest(test.NoDBTestCase): expected = copy.deepcopy(get_image_123()) expected['properties'] = {} update_mocked.assert_called_once_with(mock.ANY, '123', expected, - data=None, purge_props=True) + data=None, purge_props=True) self.assertIsNone(res) @@ -342,3 +345,22 @@ class ImageMetaDataTest(test.NoDBTestCase): self.assertRaises(webob.exc.HTTPForbidden, self.controller.create, req, image_id, body) + + +class ImageMetaDataTestV2(ImageMetaDataTestV21): + controller_class = image_metadata.Controller + + # NOTE(cyeoh): This duplicate unittest is necessary for a race condition + # with the V21 unittests. It's mock issue. + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_delete(self, _get_mocked, update_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'DELETE' + res = self.controller.delete(req, '123', 'key1') + expected = copy.deepcopy(get_image_123()) + expected['properties'] = {} + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + self.assertIsNone(res) diff --git a/nova/tests/integrated/v3/api_samples/images/image-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-get-resp.json.tpl new file mode 100644 index 0000000000..9b1abf02d2 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-get-resp.json.tpl @@ -0,0 +1,33 @@ +{ + "image": { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/image-meta-key-get.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-meta-key-get.json.tpl new file mode 100644 index 0000000000..6d022eb97d --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-meta-key-get.json.tpl @@ -0,0 +1,5 @@ +{ + "meta": { + "kernel_id": "nokernel" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-req.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-req.json.tpl new file mode 100644 index 0000000000..01528f1ce6 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-req.json.tpl @@ -0,0 +1,5 @@ +{ + "meta": { + "auto_disk_config": "False" + } +} diff --git a/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-resp.json.tpl new file mode 100644 index 0000000000..3db563ec14 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-meta-key-put-resp.json.tpl @@ -0,0 +1,5 @@ +{ + "meta": { + "auto_disk_config": "False" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/image-metadata-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-metadata-get-resp.json.tpl new file mode 100644 index 0000000000..588f688d5a --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-metadata-get-resp.json.tpl @@ -0,0 +1,8 @@ +{ + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/image-metadata-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-metadata-post-req.json.tpl new file mode 100644 index 0000000000..b51e5f00fc --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-metadata-post-req.json.tpl @@ -0,0 +1,6 @@ +{ + "metadata": { + "kernel_id": "False", + "Label": "UpdatedImage" + } +} diff --git a/nova/tests/integrated/v3/api_samples/images/image-metadata-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-metadata-post-resp.json.tpl new file mode 100644 index 0000000000..9479bb3395 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-metadata-post-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "metadata": { + "Label": "UpdatedImage", + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "False", + "ramdisk_id": "nokernel" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/image-metadata-put-req.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-metadata-put-req.json.tpl new file mode 100644 index 0000000000..eec6152d77 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-metadata-put-req.json.tpl @@ -0,0 +1,6 @@ +{ + "metadata": { + "auto_disk_config": "True", + "Label": "Changed" + } +} diff --git a/nova/tests/integrated/v3/api_samples/images/image-metadata-put-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/image-metadata-put-resp.json.tpl new file mode 100644 index 0000000000..c8c5ee9c4a --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/image-metadata-put-resp.json.tpl @@ -0,0 +1,6 @@ +{ + "metadata": { + "Label": "Changed", + "auto_disk_config": "True" + } +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/images-details-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/images-details-get-resp.json.tpl new file mode 100644 index 0000000000..91e9750def --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/images-details-get-resp.json.tpl @@ -0,0 +1,212 @@ +{ + "images": [ + { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "a2459075-d96c-40d5-893e-577ff92e721c", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "a440c04b-79fa-479c-bed1-0b816eaec379", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "False", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage6", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "links": [ + { + "href": "http://openstack.example.com/v3/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "ramdisk_id": null + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "cedef40a-ed67-4d10-800e-17455edce175", + "links": [ + { + "href": "http://openstack.example.com/v3/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + }, + { + "created": "2011-01-01T01:02:03Z", + "id": "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage123456", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z" + } + ] +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/api_samples/images/images-list-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/images/images-list-get-resp.json.tpl new file mode 100644 index 0000000000..19b39fdc5a --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/images/images-list-get-resp.json.tpl @@ -0,0 +1,137 @@ +{ + "images": [ + { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v3/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage7" + }, + { + "id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/155d900f-4e14-4e4c-a73d-069cbf4541e6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "a2459075-d96c-40d5-893e-577ff92e721c", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a2459075-d96c-40d5-893e-577ff92e721c", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "a440c04b-79fa-479c-bed1-0b816eaec379", + "links": [ + { + "href": "http://openstack.example.com/v3/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/a440c04b-79fa-479c-bed1-0b816eaec379", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage6" + }, + { + "id": "c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "links": [ + { + "href": "http://openstack.example.com/v3/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/c905cedb-7281-47e4-8a62-f26bc5fc4c77", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "cedef40a-ed67-4d10-800e-17455edce175", + "links": [ + { + "href": "http://openstack.example.com/v3/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/cedef40a-ed67-4d10-800e-17455edce175", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + }, + { + "id": "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "links": [ + { + "href": "http://openstack.example.com/v3/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "self" + }, + { + "href": "http://openstack.example.com/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "bookmark" + }, + { + "href": "http://glance.openstack.example.com/openstack/images/76fa36fc-c930-4bf3-8c8a-ea2a2420deb6", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "name": "fakeimage123456" + } + ] +} \ No newline at end of file diff --git a/nova/tests/integrated/v3/test_images.py b/nova/tests/integrated/v3/test_images.py new file mode 100644 index 0000000000..e6982d2963 --- /dev/null +++ b/nova/tests/integrated/v3/test_images.py @@ -0,0 +1,85 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2013 IBM Corp. +# +# 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.tests.image import fake +from nova.tests.integrated.v3 import api_sample_base + + +class ImagesSampleJsonTest(api_sample_base.ApiSampleTestBaseV3): + extension_name = 'images' + extra_extensions_to_load = ["image-metadata"] + + def test_images_list(self): + # Get api sample of images get list request. + response = self._do_get('images') + subs = self._get_regexes() + self._verify_response('images-list-get-resp', subs, response, 200) + + def test_image_get(self): + # Get api sample of one single image details request. + image_id = fake.get_valid_image_id() + response = self._do_get('images/%s' % image_id) + subs = self._get_regexes() + subs['image_id'] = image_id + self._verify_response('image-get-resp', subs, response, 200) + + def test_images_details(self): + # Get api sample of all images details request. + response = self._do_get('images/detail') + subs = self._get_regexes() + self._verify_response('images-details-get-resp', subs, response, 200) + + def test_image_metadata_get(self): + # Get api sample of an image metadata request. + image_id = fake.get_valid_image_id() + response = self._do_get('images/%s/metadata' % image_id) + subs = self._get_regexes() + subs['image_id'] = image_id + self._verify_response('image-metadata-get-resp', subs, response, 200) + + def test_image_metadata_post(self): + # Get api sample to update metadata of an image metadata request. + image_id = fake.get_valid_image_id() + response = self._do_post( + 'images/%s/metadata' % image_id, + 'image-metadata-post-req', {}) + subs = self._get_regexes() + self._verify_response('image-metadata-post-resp', subs, response, 200) + + def test_image_metadata_put(self): + # Get api sample of image metadata put request. + image_id = fake.get_valid_image_id() + response = self._do_put('images/%s/metadata' % image_id, + 'image-metadata-put-req', {}) + subs = self._get_regexes() + self._verify_response('image-metadata-put-resp', subs, response, 200) + + def test_image_meta_key_get(self): + # Get api sample of an image metadata key request. + image_id = fake.get_valid_image_id() + key = "kernel_id" + response = self._do_get('images/%s/metadata/%s' % (image_id, key)) + subs = self._get_regexes() + self._verify_response('image-meta-key-get', subs, response, 200) + + def test_image_meta_key_put(self): + # Get api sample of image metadata key put request. + image_id = fake.get_valid_image_id() + key = "auto_disk_config" + response = self._do_put('images/%s/metadata/%s' % (image_id, key), + 'image-meta-key-put-req', {}) + subs = self._get_regexes() + self._verify_response('image-meta-key-put-resp', subs, response, 200) diff --git a/setup.cfg b/setup.cfg index fd55ad3956..b20fceaece 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,7 @@ nova.api.v3.extensions = hosts = nova.api.openstack.compute.plugins.v3.hosts:Hosts hypervisors = nova.api.openstack.compute.plugins.v3.hypervisors:Hypervisors images = nova.api.openstack.compute.plugins.v3.images:Images + image_metadata = nova.api.openstack.compute.plugins.v3.image_metadata:ImageMetadata instance_actions = nova.api.openstack.compute.plugins.v3.instance_actions:InstanceActions ips = nova.api.openstack.compute.plugins.v3.ips:IPs keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs