Merge lp:~rackspace-titan/nova/metadata_v1.1 into lp:~hudson-openstack/nova/trunk

Proposed by Dan Prince
Status: Merged
Merge reported by: Dan Prince
Merged at revision: not available
Proposed branch: lp:~rackspace-titan/nova/metadata_v1.1
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~rackspace-titan/nova/openstack-api-versioned-controllers
Diff against target: 405 lines (+343/-0)
7 files modified
Authors (+1/-0)
nova/api/openstack/__init__.py (+5/-0)
nova/api/openstack/server_metadata.py (+78/-0)
nova/compute/api.py (+15/-0)
nova/db/api.py (+18/-0)
nova/db/sqlalchemy/api.py (+62/-0)
nova/tests/api/openstack/test_server_metadata.py (+164/-0)
To merge this branch: bzr merge lp:~rackspace-titan/nova/metadata_v1.1
Reviewer Review Type Date Requested Status
Sandy Walsh (community) Approve
Matt Dietz (community) Approve
justinsb (community) Approve
Titan Pending
Review via email: mp+53952@code.launchpad.net

Description of the change

Implement metadata resource for Openstack API v1.1. Includes:
      -GET /servers/id/meta
      -POST /servers/id/meta
      -GET /servers/id/meta/key
      -PUT /servers/id/meta/key
      -DELETE /servers/id/meta/key

To post a comment you must log in.
Revision history for this message
justinsb (justin-fathomdb) wrote :

It looks like I can post multiple keys as long as one of those is the 'right' key e.g.

PUT /servers/1/meta/key1
{ "key1": "value1", "key2": "value2" }

That's fine with me, but might be unexpected.

I'm really confused by the spec. Sometimes it's meta, sometimes metadata, sometimes there's a values collection, sometimes there's not. So it's a bit difficult to verify this against the spec. But as long as you're consistent (and it looks like you are), we can always fix the spec to match...

This ones a total nit-pick: do you think that delete_instance_metadata should filter by deleted=False, so that we don't keep marking a key as deleted once it is deleted?

However, as a consequence: What happens if I delete a key, and then try to create (or update) the same key. It looks like you'll get a PK violation?

A general discussion point (not for this patch, probably): should our DB layer be throwing exceptions when it doesn't find things? It seems to cause complexity... I like the convention that get_X throws, whereas find_X returns None if not found.

What are we doing about XML formatting? I don't see how this gets marshalled to the correct XML (though it might be - I didn't actually try this) I'd rather see this merged and then XML fixed separately if there are problems. In general, I'd like to see us testing the XML, because the use of schema means that we get a lot of tests "for free".

They're all a bit nit-picky, but I'm marking as "Needs information" for now because collectively I think they're sort of important.

review: Needs Information
Revision history for this message
Dan Prince (dan-prince) wrote :

Hi Justin,

I made a couple of changes based on your comments:

--Regarding 'collections' within the openstack API. I didn't update the JSON/XML responses to nest things in 'values' collections yet. We'll handle this specific issue globally across the API. What I've done here should be consistent with what already exists within the API's.

--The PUT call now guards against requests with multiple keys.

--My update delete SQL command now filters by deleted=False. I've tested the PUT/DELETE/GET commands extensively and I we should be fine if someone deletes a key and then recreates the same key again. I did however modify the update command so that it sets 'deleted=0' on an update (thus reactivating a key). I think 'update_or_create_instance_metadata' should be good to go now.

--Regarding the XML serialization stuff. Nova already has issues here. A call to get metadata in XML format with return something that looks like this:

 <metadata> <key2> value2 </key2> <key1> value1 </key1> </metadata>

Again this isn't "too the spec" but we'll handle proper serialization as a separate issue across the board.

--Regarding the exception throwing. I throw an exception in the 'get_instance_metadata_item' if the key isn't found. In 'get_instance_metadata' I simply return an empty dict if there is not data sense no metadata implies that metadata is just empty. I don't see any calls with find in the name so I suppose we could start doing this as a convention but I'm not sure I need to change anything to follow it currently.

Let me know what you think with the latest updates. I think we should be good to go.

Revision history for this message
justinsb (justin-fathomdb) wrote :

Great changes, thanks Dan.

Agree that the 'off topic' issues like XML details are best dealt with separately.

review: Approve
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Now that openstack-api-versioned-controllers I think we need a freshen up (hard to tell what code is from what branch).

Also, do you have any information on what the expected metadata for a server would be?

Is this a user operation or a service provider operation?

I guess I'm not really clear on the purpose of the branch.

review: Needs Information
Revision history for this message
Dan Prince (dan-prince) wrote :

> Now that openstack-api-versioned-controllers I think we need a freshen up
> (hard to tell what code is from what branch).

Sandy. Good call. I just synced up with the latest lp:nova code again.

> Also, do you have any information on what the expected metadata for a server
> would be?
>
> Is this a user operation or a service provider operation?

This branch implements the v1.1 metadata API such that a user can manipulate server specific metadata. A request to /v1.1/servers/1/meta might return something like this:

{
  "metadata" : {
    "Server Label" : "Web Head 1",
    "Image Version" : "2.1"
  }
}

The full spec is here:

https://blueprints.launchpad.net/nova/+spec/openstack-api-1-1

Revision history for this message
Matt Dietz (cerberus) wrote :

Your database API method names aren't consistent with the rest of the DB API

159 +def delete_instance_metadata(context, instance_id, key):

Should be, for example:

159 +def instance_metadata_delete(context, instance_id, key):

With that said, I actually have reservations about whether or not this branch truly belongs in Nova. It seems like it would be better situated in a metadata service somewhere at the top of the stack, possibly attached to the API, and simply mapped to instance_ids.

Since I missed this spec originally I won't block it, but I'd like to maybe discuss moving the responsibility for this to a different layer post Cactus

review: Needs Fixing
Revision history for this message
Dan Prince (dan-prince) wrote :

> With that said, I actually have reservations about whether or not this branch
> truly belongs in Nova. It seems like it would be better situated in a metadata
> service somewhere at the top of the stack, possibly attached to the API, and
> simply mapped to instance_ids.
>
> Since I missed this spec originally I won't block it, but I'd like to maybe
> discuss moving the responsibility for this to a different layer post Cactus

--

Hey Matt. We are looking into renaming things now.

So on whether this should exist in nova... We had previously bounced the idea around about whether metadata should be a separate service. It certainly could be and that might work better in some cases. Probably best to discuss that as a separate issue/blueprint.

All this branch does is basically extends the v1.0 implementation (which already used the database) to make metadata a RESTful sub-resource under /servers in the API to support the v1.1 spec.

Revision history for this message
Dan Prince (dan-prince) wrote :

Hey Matt. Mark just updated the database API names so we should be good to go.

Revision history for this message
Matt Dietz (cerberus) wrote :

Thanks for the fixes :-)

And yes, I agree that it should be a separate blueprint. We can talk about it at the summit next month :-D

review: Approve
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Looks good ...

109 - is there a risk that 'metadata' key doesn't exist?

review: Needs Information
Revision history for this message
Dan Prince (dan-prince) wrote :

> Looks good ...
>
> 109 - is there a risk that 'metadata' key doesn't exist?

Hey Sandy,

The _get_metadata method always returns a dict with 'metadata' so I think we are good.

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Thanks for that

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/compute/api.py

Revision history for this message
Dan Prince (dan-prince) wrote :

Merged with trunk to resolve compute API conflict. Should be good to go again.

Revision history for this message
Dan Prince (dan-prince) wrote :

Whoops. Accidentally set to merged. Set back to needs review again.

Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Revision history for this message
Dan Prince (dan-prince) wrote :

Marking this as merged. Looks like Hudson failed because of a dependent branch or something.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Authors'
--- Authors 2011-03-24 22:43:46 +0000
+++ Authors 2011-03-24 22:51:33 +0000
@@ -21,6 +21,7 @@
21Eric Day <eday@oddments.org>21Eric Day <eday@oddments.org>
22Eric Windisch <eric@cloudscaling.com>22Eric Windisch <eric@cloudscaling.com>
23Ewan Mellor <ewan.mellor@citrix.com>23Ewan Mellor <ewan.mellor@citrix.com>
24Gabe Westmaas <gabe.westmaas@rackspace.com>
24Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>25Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
25Hisaki Ohara <hisaki.ohara@intel.com>26Hisaki Ohara <hisaki.ohara@intel.com>
26Ilya Alekseyev <ialekseev@griddynamics.com>27Ilya Alekseyev <ialekseev@griddynamics.com>
2728
=== modified file 'nova/api/openstack/__init__.py'
--- nova/api/openstack/__init__.py 2011-03-24 22:26:47 +0000
+++ nova/api/openstack/__init__.py 2011-03-24 22:51:33 +0000
@@ -35,6 +35,7 @@
35from nova.api.openstack import images35from nova.api.openstack import images
36from nova.api.openstack import limits36from nova.api.openstack import limits
37from nova.api.openstack import servers37from nova.api.openstack import servers
38from nova.api.openstack import server_metadata
38from nova.api.openstack import shared_ip_groups39from nova.api.openstack import shared_ip_groups
39from nova.api.openstack import users40from nova.api.openstack import users
40from nova.api.openstack import zones41from nova.api.openstack import zones
@@ -148,6 +149,10 @@
148 controller=servers.ControllerV11(),149 controller=servers.ControllerV11(),
149 collection={'detail': 'GET'},150 collection={'detail': 'GET'},
150 member=self.server_members)151 member=self.server_members)
152 mapper.resource("server_meta", "meta",
153 controller=server_metadata.Controller(),
154 parent_resource=dict(member_name='server',
155 collection_name='servers'))
151156
152157
153class Versions(wsgi.Application):158class Versions(wsgi.Application):
154159
=== added file 'nova/api/openstack/server_metadata.py'
--- nova/api/openstack/server_metadata.py 1970-01-01 00:00:00 +0000
+++ nova/api/openstack/server_metadata.py 2011-03-24 22:51:33 +0000
@@ -0,0 +1,78 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18from webob import exc
19
20from nova import compute
21from nova import wsgi
22from nova.api.openstack import faults
23
24
25class Controller(wsgi.Controller):
26 """ The server metadata API controller for the Openstack API """
27
28 def __init__(self):
29 self.compute_api = compute.API()
30 super(Controller, self).__init__()
31
32 def _get_metadata(self, context, server_id):
33 metadata = self.compute_api.get_instance_metadata(context, server_id)
34 meta_dict = {}
35 for key, value in metadata.iteritems():
36 meta_dict[key] = value
37 return dict(metadata=meta_dict)
38
39 def index(self, req, server_id):
40 """ Returns the list of metadata for a given instance """
41 context = req.environ['nova.context']
42 return self._get_metadata(context, server_id)
43
44 def create(self, req, server_id):
45 context = req.environ['nova.context']
46 body = self._deserialize(req.body, req.get_content_type())
47 self.compute_api.update_or_create_instance_metadata(context,
48 server_id,
49 body['metadata'])
50 return req.body
51
52 def update(self, req, server_id, id):
53 context = req.environ['nova.context']
54 body = self._deserialize(req.body, req.get_content_type())
55 if not id in body:
56 expl = _('Request body and URI mismatch')
57 raise exc.HTTPBadRequest(explanation=expl)
58 if len(body) > 1:
59 expl = _('Request body contains too many items')
60 raise exc.HTTPBadRequest(explanation=expl)
61 self.compute_api.update_or_create_instance_metadata(context,
62 server_id,
63 body)
64 return req.body
65
66 def show(self, req, server_id, id):
67 """ Return a single metadata item """
68 context = req.environ['nova.context']
69 data = self._get_metadata(context, server_id)
70 if id in data['metadata']:
71 return {id: data['metadata'][id]}
72 else:
73 return faults.Fault(exc.HTTPNotFound())
74
75 def delete(self, req, server_id, id):
76 """ Deletes an existing metadata """
77 context = req.environ['nova.context']
78 self.compute_api.delete_instance_metadata(context, server_id, id)
079
=== modified file 'nova/compute/api.py'
--- nova/compute/api.py 2011-03-24 21:50:27 +0000
+++ nova/compute/api.py 2011-03-24 22:51:33 +0000
@@ -673,3 +673,18 @@
673 self.network_api.associate_floating_ip(context,673 self.network_api.associate_floating_ip(context,
674 floating_ip=address,674 floating_ip=address,
675 fixed_ip=instance['fixed_ip'])675 fixed_ip=instance['fixed_ip'])
676
677 def get_instance_metadata(self, context, instance_id):
678 """Get all metadata associated with an instance."""
679 rv = self.db.instance_metadata_get(context, instance_id)
680 return dict(rv.iteritems())
681
682 def delete_instance_metadata(self, context, instance_id, key):
683 """Delete the given metadata item"""
684 self.db.instance_metadata_delete(context, instance_id, key)
685
686 def update_or_create_instance_metadata(self, context, instance_id,
687 metadata):
688 """Updates or creates instance metadata"""
689 self.db.instance_metadata_update_or_create(context, instance_id,
690 metadata)
676691
=== modified file 'nova/db/api.py'
--- nova/db/api.py 2011-03-24 19:04:24 +0000
+++ nova/db/api.py 2011-03-24 22:51:33 +0000
@@ -1172,3 +1172,21 @@
1172def zone_get_all(context):1172def zone_get_all(context):
1173 """Get all child Zones."""1173 """Get all child Zones."""
1174 return IMPL.zone_get_all(context)1174 return IMPL.zone_get_all(context)
1175
1176
1177####################
1178
1179
1180def instance_metadata_get(context, instance_id):
1181 """Get all metadata for an instance"""
1182 return IMPL.instance_metadata_get(context, instance_id)
1183
1184
1185def instance_metadata_delete(context, instance_id, key):
1186 """Delete the given metadata item"""
1187 IMPL.instance_metadata_delete(context, instance_id, key)
1188
1189
1190def instance_metadata_update_or_create(context, instance_id, metadata):
1191 """Create or update instance metadata"""
1192 IMPL.instance_metadata_update_or_create(context, instance_id, metadata)
11751193
=== modified file 'nova/db/sqlalchemy/api.py'
--- nova/db/sqlalchemy/api.py 2011-03-24 18:09:13 +0000
+++ nova/db/sqlalchemy/api.py 2011-03-24 22:51:33 +0000
@@ -2466,3 +2466,65 @@
2466def zone_get_all(context):2466def zone_get_all(context):
2467 session = get_session()2467 session = get_session()
2468 return session.query(models.Zone).all()2468 return session.query(models.Zone).all()
2469
2470
2471####################
2472
2473@require_context
2474def instance_metadata_get(context, instance_id):
2475 session = get_session()
2476
2477 meta_results = session.query(models.InstanceMetadata).\
2478 filter_by(instance_id=instance_id).\
2479 filter_by(deleted=False).\
2480 all()
2481
2482 meta_dict = {}
2483 for i in meta_results:
2484 meta_dict[i['key']] = i['value']
2485 return meta_dict
2486
2487
2488@require_context
2489def instance_metadata_delete(context, instance_id, key):
2490 session = get_session()
2491 session.query(models.InstanceMetadata).\
2492 filter_by(instance_id=instance_id).\
2493 filter_by(key=key).\
2494 filter_by(deleted=False).\
2495 update({'deleted': 1,
2496 'deleted_at': datetime.datetime.utcnow(),
2497 'updated_at': literal_column('updated_at')})
2498
2499
2500@require_context
2501def instance_metadata_get_item(context, instance_id, key):
2502 session = get_session()
2503
2504 meta_result = session.query(models.InstanceMetadata).\
2505 filter_by(instance_id=instance_id).\
2506 filter_by(key=key).\
2507 filter_by(deleted=False).\
2508 first()
2509
2510 if not meta_result:
2511 raise exception.NotFound(_('Invalid metadata key for instance %s') %
2512 instance_id)
2513 return meta_result
2514
2515
2516@require_context
2517def instance_metadata_update_or_create(context, instance_id, metadata):
2518 session = get_session()
2519 meta_ref = None
2520 for key, value in metadata.iteritems():
2521 try:
2522 meta_ref = instance_metadata_get_item(context, instance_id, key,
2523 session)
2524 except:
2525 meta_ref = models.InstanceMetadata()
2526 meta_ref.update({"key": key, "value": value,
2527 "instance_id": instance_id,
2528 "deleted": 0})
2529 meta_ref.save(session=session)
2530 return metadata
24692531
=== added file 'nova/tests/api/openstack/test_server_metadata.py'
--- nova/tests/api/openstack/test_server_metadata.py 1970-01-01 00:00:00 +0000
+++ nova/tests/api/openstack/test_server_metadata.py 2011-03-24 22:51:33 +0000
@@ -0,0 +1,164 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import json
19import stubout
20import unittest
21import webob
22
23
24from nova.api import openstack
25from nova.tests.api.openstack import fakes
26import nova.wsgi
27
28
29def return_create_instance_metadata(context, server_id, metadata):
30 return stub_server_metadata()
31
32
33def return_server_metadata(context, server_id):
34 return stub_server_metadata()
35
36
37def return_empty_server_metadata(context, server_id):
38 return {}
39
40
41def delete_server_metadata(context, server_id, key):
42 pass
43
44
45def stub_server_metadata():
46 metadata = {
47 "key1": "value1",
48 "key2": "value2",
49 "key3": "value3",
50 "key4": "value4",
51 "key5": "value5"
52 }
53 return metadata
54
55
56class ServerMetaDataTest(unittest.TestCase):
57
58 def setUp(self):
59 super(ServerMetaDataTest, self).setUp()
60 self.stubs = stubout.StubOutForTesting()
61 fakes.FakeAuthManager.auth_data = {}
62 fakes.FakeAuthDatabase.data = {}
63 fakes.stub_out_auth(self.stubs)
64 fakes.stub_out_key_pair_funcs(self.stubs)
65
66 def tearDown(self):
67 self.stubs.UnsetAll()
68 super(ServerMetaDataTest, self).tearDown()
69
70 def test_index(self):
71 self.stubs.Set(nova.db.api, 'instance_metadata_get',
72 return_server_metadata)
73 req = webob.Request.blank('/v1.1/servers/1/meta')
74 req.environ['api.version'] = '1.1'
75 res = req.get_response(fakes.wsgi_app())
76 res_dict = json.loads(res.body)
77 self.assertEqual(200, res.status_int)
78 self.assertEqual('value1', res_dict['metadata']['key1'])
79
80 def test_index_no_data(self):
81 self.stubs.Set(nova.db.api, 'instance_metadata_get',
82 return_empty_server_metadata)
83 req = webob.Request.blank('/v1.1/servers/1/meta')
84 req.environ['api.version'] = '1.1'
85 res = req.get_response(fakes.wsgi_app())
86 res_dict = json.loads(res.body)
87 self.assertEqual(200, res.status_int)
88 self.assertEqual(0, len(res_dict['metadata']))
89
90 def test_show(self):
91 self.stubs.Set(nova.db.api, 'instance_metadata_get',
92 return_server_metadata)
93 req = webob.Request.blank('/v1.1/servers/1/meta/key5')
94 req.environ['api.version'] = '1.1'
95 res = req.get_response(fakes.wsgi_app())
96 res_dict = json.loads(res.body)
97 self.assertEqual(200, res.status_int)
98 self.assertEqual('value5', res_dict['key5'])
99
100 def test_show_meta_not_found(self):
101 self.stubs.Set(nova.db.api, 'instance_metadata_get',
102 return_empty_server_metadata)
103 req = webob.Request.blank('/v1.1/servers/1/meta/key6')
104 req.environ['api.version'] = '1.1'
105 res = req.get_response(fakes.wsgi_app())
106 res_dict = json.loads(res.body)
107 self.assertEqual(404, res.status_int)
108
109 def test_delete(self):
110 self.stubs.Set(nova.db.api, 'instance_metadata_delete',
111 delete_server_metadata)
112 req = webob.Request.blank('/v1.1/servers/1/meta/key5')
113 req.environ['api.version'] = '1.1'
114 req.method = 'DELETE'
115 res = req.get_response(fakes.wsgi_app())
116 self.assertEqual(200, res.status_int)
117
118 def test_create(self):
119 self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
120 return_create_instance_metadata)
121 req = webob.Request.blank('/v1.1/servers/1/meta')
122 req.environ['api.version'] = '1.1'
123 req.method = 'POST'
124 req.body = '{"metadata": {"key1": "value1"}}'
125 req.headers["content-type"] = "application/json"
126 res = req.get_response(fakes.wsgi_app())
127 res_dict = json.loads(res.body)
128 self.assertEqual(200, res.status_int)
129 self.assertEqual('value1', res_dict['metadata']['key1'])
130
131 def test_update_item(self):
132 self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
133 return_create_instance_metadata)
134 req = webob.Request.blank('/v1.1/servers/1/meta/key1')
135 req.environ['api.version'] = '1.1'
136 req.method = 'PUT'
137 req.body = '{"key1": "value1"}'
138 req.headers["content-type"] = "application/json"
139 res = req.get_response(fakes.wsgi_app())
140 self.assertEqual(200, res.status_int)
141 res_dict = json.loads(res.body)
142 self.assertEqual('value1', res_dict['key1'])
143
144 def test_update_item_too_many_keys(self):
145 self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
146 return_create_instance_metadata)
147 req = webob.Request.blank('/v1.1/servers/1/meta/key1')
148 req.environ['api.version'] = '1.1'
149 req.method = 'PUT'
150 req.body = '{"key1": "value1", "key2": "value2"}'
151 req.headers["content-type"] = "application/json"
152 res = req.get_response(fakes.wsgi_app())
153 self.assertEqual(400, res.status_int)
154
155 def test_update_item_body_uri_mismatch(self):
156 self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
157 return_create_instance_metadata)
158 req = webob.Request.blank('/v1.1/servers/1/meta/bad')
159 req.environ['api.version'] = '1.1'
160 req.method = 'PUT'
161 req.body = '{"key1": "value1"}'
162 req.headers["content-type"] = "application/json"
163 res = req.get_response(fakes.wsgi_app())
164 self.assertEqual(400, res.status_int)