Merge lp:~rackspace-titan/nova/metadata_v1.1 into lp:~hudson-openstack/nova/trunk
- metadata_v1.1
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
OpenStack API 1.1
(Medium)
|
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 |
Commit message
Description of the change
Implement metadata resource for Openstack API v1.1. Includes:
-GET /servers/id/meta
-POST /servers/id/meta
-GET /servers/
-PUT /servers/
-DELETE /servers/
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_
--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_
Let me know what you think with the latest updates. I think we should be good to go.
justinsb (justin-fathomdb) wrote : | # |
Great changes, thanks Dan.
Agree that the 'off topic' issues like XML details are best dealt with separately.
Sandy Walsh (sandy-walsh) wrote : | # |
Now that openstack-
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.
Dan Prince (dan-prince) wrote : | # |
> Now that openstack-
> (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/
{
"metadata" : {
"Server Label" : "Web Head 1",
"Image Version" : "2.1"
}
}
The full spec is here:
https:/
Matt Dietz (cerberus) wrote : | # |
Your database API method names aren't consistent with the rest of the DB API
159 +def delete_
Should be, for example:
159 +def instance_
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
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.
Dan Prince (dan-prince) wrote : | # |
Hey Matt. Mark just updated the database API names so we should be good to go.
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
Sandy Walsh (sandy-walsh) wrote : | # |
Looks good ...
109 - is there a risk that 'metadata' key doesn't exist?
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.
Sandy Walsh (sandy-walsh) wrote : | # |
Thanks for that
OpenStack Infra (hudson-openstack) wrote : | # |
Attempt to merge into lp:nova failed due to conflicts:
text conflict in nova/compute/api.py
Dan Prince (dan-prince) wrote : | # |
Merged with trunk to resolve compute API conflict. Should be good to go again.
Dan Prince (dan-prince) wrote : | # |
Whoops. Accidentally set to merged. Set back to needs review again.
OpenStack Infra (hudson-openstack) wrote : | # |
No proposals found for merge of lp:~rackspace-titan/nova/openstack-api-versioned-controllers into lp:nova.
Dan Prince (dan-prince) wrote : | # |
Marking this as merged. Looks like Hudson failed because of a dependent branch or something.
Preview Diff
1 | === modified file 'Authors' |
2 | --- Authors 2011-03-24 22:43:46 +0000 |
3 | +++ Authors 2011-03-24 22:51:33 +0000 |
4 | @@ -21,6 +21,7 @@ |
5 | Eric Day <eday@oddments.org> |
6 | Eric Windisch <eric@cloudscaling.com> |
7 | Ewan Mellor <ewan.mellor@citrix.com> |
8 | +Gabe Westmaas <gabe.westmaas@rackspace.com> |
9 | Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp> |
10 | Hisaki Ohara <hisaki.ohara@intel.com> |
11 | Ilya Alekseyev <ialekseev@griddynamics.com> |
12 | |
13 | === modified file 'nova/api/openstack/__init__.py' |
14 | --- nova/api/openstack/__init__.py 2011-03-24 22:26:47 +0000 |
15 | +++ nova/api/openstack/__init__.py 2011-03-24 22:51:33 +0000 |
16 | @@ -35,6 +35,7 @@ |
17 | from nova.api.openstack import images |
18 | from nova.api.openstack import limits |
19 | from nova.api.openstack import servers |
20 | +from nova.api.openstack import server_metadata |
21 | from nova.api.openstack import shared_ip_groups |
22 | from nova.api.openstack import users |
23 | from nova.api.openstack import zones |
24 | @@ -148,6 +149,10 @@ |
25 | controller=servers.ControllerV11(), |
26 | collection={'detail': 'GET'}, |
27 | member=self.server_members) |
28 | + mapper.resource("server_meta", "meta", |
29 | + controller=server_metadata.Controller(), |
30 | + parent_resource=dict(member_name='server', |
31 | + collection_name='servers')) |
32 | |
33 | |
34 | class Versions(wsgi.Application): |
35 | |
36 | === added file 'nova/api/openstack/server_metadata.py' |
37 | --- nova/api/openstack/server_metadata.py 1970-01-01 00:00:00 +0000 |
38 | +++ nova/api/openstack/server_metadata.py 2011-03-24 22:51:33 +0000 |
39 | @@ -0,0 +1,78 @@ |
40 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
41 | + |
42 | +# Copyright 2011 OpenStack LLC. |
43 | +# All Rights Reserved. |
44 | +# |
45 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
46 | +# not use this file except in compliance with the License. You may obtain |
47 | +# a copy of the License at |
48 | +# |
49 | +# http://www.apache.org/licenses/LICENSE-2.0 |
50 | +# |
51 | +# Unless required by applicable law or agreed to in writing, software |
52 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
53 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
54 | +# License for the specific language governing permissions and limitations |
55 | +# under the License. |
56 | + |
57 | +from webob import exc |
58 | + |
59 | +from nova import compute |
60 | +from nova import wsgi |
61 | +from nova.api.openstack import faults |
62 | + |
63 | + |
64 | +class Controller(wsgi.Controller): |
65 | + """ The server metadata API controller for the Openstack API """ |
66 | + |
67 | + def __init__(self): |
68 | + self.compute_api = compute.API() |
69 | + super(Controller, self).__init__() |
70 | + |
71 | + def _get_metadata(self, context, server_id): |
72 | + metadata = self.compute_api.get_instance_metadata(context, server_id) |
73 | + meta_dict = {} |
74 | + for key, value in metadata.iteritems(): |
75 | + meta_dict[key] = value |
76 | + return dict(metadata=meta_dict) |
77 | + |
78 | + def index(self, req, server_id): |
79 | + """ Returns the list of metadata for a given instance """ |
80 | + context = req.environ['nova.context'] |
81 | + return self._get_metadata(context, server_id) |
82 | + |
83 | + def create(self, req, server_id): |
84 | + context = req.environ['nova.context'] |
85 | + body = self._deserialize(req.body, req.get_content_type()) |
86 | + self.compute_api.update_or_create_instance_metadata(context, |
87 | + server_id, |
88 | + body['metadata']) |
89 | + return req.body |
90 | + |
91 | + def update(self, req, server_id, id): |
92 | + context = req.environ['nova.context'] |
93 | + body = self._deserialize(req.body, req.get_content_type()) |
94 | + if not id in body: |
95 | + expl = _('Request body and URI mismatch') |
96 | + raise exc.HTTPBadRequest(explanation=expl) |
97 | + if len(body) > 1: |
98 | + expl = _('Request body contains too many items') |
99 | + raise exc.HTTPBadRequest(explanation=expl) |
100 | + self.compute_api.update_or_create_instance_metadata(context, |
101 | + server_id, |
102 | + body) |
103 | + return req.body |
104 | + |
105 | + def show(self, req, server_id, id): |
106 | + """ Return a single metadata item """ |
107 | + context = req.environ['nova.context'] |
108 | + data = self._get_metadata(context, server_id) |
109 | + if id in data['metadata']: |
110 | + return {id: data['metadata'][id]} |
111 | + else: |
112 | + return faults.Fault(exc.HTTPNotFound()) |
113 | + |
114 | + def delete(self, req, server_id, id): |
115 | + """ Deletes an existing metadata """ |
116 | + context = req.environ['nova.context'] |
117 | + self.compute_api.delete_instance_metadata(context, server_id, id) |
118 | |
119 | === modified file 'nova/compute/api.py' |
120 | --- nova/compute/api.py 2011-03-24 21:50:27 +0000 |
121 | +++ nova/compute/api.py 2011-03-24 22:51:33 +0000 |
122 | @@ -673,3 +673,18 @@ |
123 | self.network_api.associate_floating_ip(context, |
124 | floating_ip=address, |
125 | fixed_ip=instance['fixed_ip']) |
126 | + |
127 | + def get_instance_metadata(self, context, instance_id): |
128 | + """Get all metadata associated with an instance.""" |
129 | + rv = self.db.instance_metadata_get(context, instance_id) |
130 | + return dict(rv.iteritems()) |
131 | + |
132 | + def delete_instance_metadata(self, context, instance_id, key): |
133 | + """Delete the given metadata item""" |
134 | + self.db.instance_metadata_delete(context, instance_id, key) |
135 | + |
136 | + def update_or_create_instance_metadata(self, context, instance_id, |
137 | + metadata): |
138 | + """Updates or creates instance metadata""" |
139 | + self.db.instance_metadata_update_or_create(context, instance_id, |
140 | + metadata) |
141 | |
142 | === modified file 'nova/db/api.py' |
143 | --- nova/db/api.py 2011-03-24 19:04:24 +0000 |
144 | +++ nova/db/api.py 2011-03-24 22:51:33 +0000 |
145 | @@ -1172,3 +1172,21 @@ |
146 | def zone_get_all(context): |
147 | """Get all child Zones.""" |
148 | return IMPL.zone_get_all(context) |
149 | + |
150 | + |
151 | +#################### |
152 | + |
153 | + |
154 | +def instance_metadata_get(context, instance_id): |
155 | + """Get all metadata for an instance""" |
156 | + return IMPL.instance_metadata_get(context, instance_id) |
157 | + |
158 | + |
159 | +def instance_metadata_delete(context, instance_id, key): |
160 | + """Delete the given metadata item""" |
161 | + IMPL.instance_metadata_delete(context, instance_id, key) |
162 | + |
163 | + |
164 | +def instance_metadata_update_or_create(context, instance_id, metadata): |
165 | + """Create or update instance metadata""" |
166 | + IMPL.instance_metadata_update_or_create(context, instance_id, metadata) |
167 | |
168 | === modified file 'nova/db/sqlalchemy/api.py' |
169 | --- nova/db/sqlalchemy/api.py 2011-03-24 18:09:13 +0000 |
170 | +++ nova/db/sqlalchemy/api.py 2011-03-24 22:51:33 +0000 |
171 | @@ -2466,3 +2466,65 @@ |
172 | def zone_get_all(context): |
173 | session = get_session() |
174 | return session.query(models.Zone).all() |
175 | + |
176 | + |
177 | +#################### |
178 | + |
179 | +@require_context |
180 | +def instance_metadata_get(context, instance_id): |
181 | + session = get_session() |
182 | + |
183 | + meta_results = session.query(models.InstanceMetadata).\ |
184 | + filter_by(instance_id=instance_id).\ |
185 | + filter_by(deleted=False).\ |
186 | + all() |
187 | + |
188 | + meta_dict = {} |
189 | + for i in meta_results: |
190 | + meta_dict[i['key']] = i['value'] |
191 | + return meta_dict |
192 | + |
193 | + |
194 | +@require_context |
195 | +def instance_metadata_delete(context, instance_id, key): |
196 | + session = get_session() |
197 | + session.query(models.InstanceMetadata).\ |
198 | + filter_by(instance_id=instance_id).\ |
199 | + filter_by(key=key).\ |
200 | + filter_by(deleted=False).\ |
201 | + update({'deleted': 1, |
202 | + 'deleted_at': datetime.datetime.utcnow(), |
203 | + 'updated_at': literal_column('updated_at')}) |
204 | + |
205 | + |
206 | +@require_context |
207 | +def instance_metadata_get_item(context, instance_id, key): |
208 | + session = get_session() |
209 | + |
210 | + meta_result = session.query(models.InstanceMetadata).\ |
211 | + filter_by(instance_id=instance_id).\ |
212 | + filter_by(key=key).\ |
213 | + filter_by(deleted=False).\ |
214 | + first() |
215 | + |
216 | + if not meta_result: |
217 | + raise exception.NotFound(_('Invalid metadata key for instance %s') % |
218 | + instance_id) |
219 | + return meta_result |
220 | + |
221 | + |
222 | +@require_context |
223 | +def instance_metadata_update_or_create(context, instance_id, metadata): |
224 | + session = get_session() |
225 | + meta_ref = None |
226 | + for key, value in metadata.iteritems(): |
227 | + try: |
228 | + meta_ref = instance_metadata_get_item(context, instance_id, key, |
229 | + session) |
230 | + except: |
231 | + meta_ref = models.InstanceMetadata() |
232 | + meta_ref.update({"key": key, "value": value, |
233 | + "instance_id": instance_id, |
234 | + "deleted": 0}) |
235 | + meta_ref.save(session=session) |
236 | + return metadata |
237 | |
238 | === added file 'nova/tests/api/openstack/test_server_metadata.py' |
239 | --- nova/tests/api/openstack/test_server_metadata.py 1970-01-01 00:00:00 +0000 |
240 | +++ nova/tests/api/openstack/test_server_metadata.py 2011-03-24 22:51:33 +0000 |
241 | @@ -0,0 +1,164 @@ |
242 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
243 | + |
244 | +# Copyright 2011 OpenStack LLC. |
245 | +# All Rights Reserved. |
246 | +# |
247 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
248 | +# not use this file except in compliance with the License. You may obtain |
249 | +# a copy of the License at |
250 | +# |
251 | +# http://www.apache.org/licenses/LICENSE-2.0 |
252 | +# |
253 | +# Unless required by applicable law or agreed to in writing, software |
254 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
255 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
256 | +# License for the specific language governing permissions and limitations |
257 | +# under the License. |
258 | + |
259 | +import json |
260 | +import stubout |
261 | +import unittest |
262 | +import webob |
263 | + |
264 | + |
265 | +from nova.api import openstack |
266 | +from nova.tests.api.openstack import fakes |
267 | +import nova.wsgi |
268 | + |
269 | + |
270 | +def return_create_instance_metadata(context, server_id, metadata): |
271 | + return stub_server_metadata() |
272 | + |
273 | + |
274 | +def return_server_metadata(context, server_id): |
275 | + return stub_server_metadata() |
276 | + |
277 | + |
278 | +def return_empty_server_metadata(context, server_id): |
279 | + return {} |
280 | + |
281 | + |
282 | +def delete_server_metadata(context, server_id, key): |
283 | + pass |
284 | + |
285 | + |
286 | +def stub_server_metadata(): |
287 | + metadata = { |
288 | + "key1": "value1", |
289 | + "key2": "value2", |
290 | + "key3": "value3", |
291 | + "key4": "value4", |
292 | + "key5": "value5" |
293 | + } |
294 | + return metadata |
295 | + |
296 | + |
297 | +class ServerMetaDataTest(unittest.TestCase): |
298 | + |
299 | + def setUp(self): |
300 | + super(ServerMetaDataTest, self).setUp() |
301 | + self.stubs = stubout.StubOutForTesting() |
302 | + fakes.FakeAuthManager.auth_data = {} |
303 | + fakes.FakeAuthDatabase.data = {} |
304 | + fakes.stub_out_auth(self.stubs) |
305 | + fakes.stub_out_key_pair_funcs(self.stubs) |
306 | + |
307 | + def tearDown(self): |
308 | + self.stubs.UnsetAll() |
309 | + super(ServerMetaDataTest, self).tearDown() |
310 | + |
311 | + def test_index(self): |
312 | + self.stubs.Set(nova.db.api, 'instance_metadata_get', |
313 | + return_server_metadata) |
314 | + req = webob.Request.blank('/v1.1/servers/1/meta') |
315 | + req.environ['api.version'] = '1.1' |
316 | + res = req.get_response(fakes.wsgi_app()) |
317 | + res_dict = json.loads(res.body) |
318 | + self.assertEqual(200, res.status_int) |
319 | + self.assertEqual('value1', res_dict['metadata']['key1']) |
320 | + |
321 | + def test_index_no_data(self): |
322 | + self.stubs.Set(nova.db.api, 'instance_metadata_get', |
323 | + return_empty_server_metadata) |
324 | + req = webob.Request.blank('/v1.1/servers/1/meta') |
325 | + req.environ['api.version'] = '1.1' |
326 | + res = req.get_response(fakes.wsgi_app()) |
327 | + res_dict = json.loads(res.body) |
328 | + self.assertEqual(200, res.status_int) |
329 | + self.assertEqual(0, len(res_dict['metadata'])) |
330 | + |
331 | + def test_show(self): |
332 | + self.stubs.Set(nova.db.api, 'instance_metadata_get', |
333 | + return_server_metadata) |
334 | + req = webob.Request.blank('/v1.1/servers/1/meta/key5') |
335 | + req.environ['api.version'] = '1.1' |
336 | + res = req.get_response(fakes.wsgi_app()) |
337 | + res_dict = json.loads(res.body) |
338 | + self.assertEqual(200, res.status_int) |
339 | + self.assertEqual('value5', res_dict['key5']) |
340 | + |
341 | + def test_show_meta_not_found(self): |
342 | + self.stubs.Set(nova.db.api, 'instance_metadata_get', |
343 | + return_empty_server_metadata) |
344 | + req = webob.Request.blank('/v1.1/servers/1/meta/key6') |
345 | + req.environ['api.version'] = '1.1' |
346 | + res = req.get_response(fakes.wsgi_app()) |
347 | + res_dict = json.loads(res.body) |
348 | + self.assertEqual(404, res.status_int) |
349 | + |
350 | + def test_delete(self): |
351 | + self.stubs.Set(nova.db.api, 'instance_metadata_delete', |
352 | + delete_server_metadata) |
353 | + req = webob.Request.blank('/v1.1/servers/1/meta/key5') |
354 | + req.environ['api.version'] = '1.1' |
355 | + req.method = 'DELETE' |
356 | + res = req.get_response(fakes.wsgi_app()) |
357 | + self.assertEqual(200, res.status_int) |
358 | + |
359 | + def test_create(self): |
360 | + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', |
361 | + return_create_instance_metadata) |
362 | + req = webob.Request.blank('/v1.1/servers/1/meta') |
363 | + req.environ['api.version'] = '1.1' |
364 | + req.method = 'POST' |
365 | + req.body = '{"metadata": {"key1": "value1"}}' |
366 | + req.headers["content-type"] = "application/json" |
367 | + res = req.get_response(fakes.wsgi_app()) |
368 | + res_dict = json.loads(res.body) |
369 | + self.assertEqual(200, res.status_int) |
370 | + self.assertEqual('value1', res_dict['metadata']['key1']) |
371 | + |
372 | + def test_update_item(self): |
373 | + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', |
374 | + return_create_instance_metadata) |
375 | + req = webob.Request.blank('/v1.1/servers/1/meta/key1') |
376 | + req.environ['api.version'] = '1.1' |
377 | + req.method = 'PUT' |
378 | + req.body = '{"key1": "value1"}' |
379 | + req.headers["content-type"] = "application/json" |
380 | + res = req.get_response(fakes.wsgi_app()) |
381 | + self.assertEqual(200, res.status_int) |
382 | + res_dict = json.loads(res.body) |
383 | + self.assertEqual('value1', res_dict['key1']) |
384 | + |
385 | + def test_update_item_too_many_keys(self): |
386 | + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', |
387 | + return_create_instance_metadata) |
388 | + req = webob.Request.blank('/v1.1/servers/1/meta/key1') |
389 | + req.environ['api.version'] = '1.1' |
390 | + req.method = 'PUT' |
391 | + req.body = '{"key1": "value1", "key2": "value2"}' |
392 | + req.headers["content-type"] = "application/json" |
393 | + res = req.get_response(fakes.wsgi_app()) |
394 | + self.assertEqual(400, res.status_int) |
395 | + |
396 | + def test_update_item_body_uri_mismatch(self): |
397 | + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', |
398 | + return_create_instance_metadata) |
399 | + req = webob.Request.blank('/v1.1/servers/1/meta/bad') |
400 | + req.environ['api.version'] = '1.1' |
401 | + req.method = 'PUT' |
402 | + req.body = '{"key1": "value1"}' |
403 | + req.headers["content-type"] = "application/json" |
404 | + res = req.get_response(fakes.wsgi_app()) |
405 | + self.assertEqual(400, res.status_int) |
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.