1
# Copyright 2013 Josh Durgin
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
18
from lxml import etree
21
from nova.api.openstack.volume import extensions
22
from nova.api.openstack.volume import volumes
24
from nova import exception
25
from nova import flags
26
from nova.openstack.common import timeutils
28
from nova.tests.api.openstack import fakes
29
from nova.tests.image import fake as fake_image
30
from nova.volume import api as volume_api
35
TEST_SNAPSHOT_UUID = '00000000-0000-0000-0000-000000000001'
38
def stub_snapshot_get(self, context, snapshot_id):
39
if snapshot_id != TEST_SNAPSHOT_UUID:
40
raise exception.NotFound
45
'status': 'available',
48
'display_name': 'Default name',
49
'display_description': 'Default description',
53
class VolumeApiTest(test.TestCase):
55
super(VolumeApiTest, self).setUp()
56
self.ext_mgr = extensions.ExtensionManager()
57
self.ext_mgr.extensions = {}
58
self.controller = volumes.VolumeController(self.ext_mgr)
60
self.stubs.Set(db, 'volume_get_all', fakes.stub_volume_get_all)
61
self.stubs.Set(db, 'volume_get_all_by_project',
62
fakes.stub_volume_get_all_by_project)
63
self.stubs.Set(volume_api.API, 'get', fakes.stub_volume_get)
64
self.stubs.Set(volume_api.API, 'delete', fakes.stub_volume_delete)
66
def test_volume_create(self):
67
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
70
"display_name": "Volume Test Name",
71
"display_description": "Volume Test Desc",
72
"availability_zone": "zone1:host1"}
73
body = {"volume": vol}
74
req = fakes.HTTPRequest.blank('/v1/volumes')
75
res = self.controller.create(req, body)
76
expected = {'volume': {'status': 'fakestatus',
77
'display_description': 'Volume Test Desc',
78
'availability_zone': 'zone1:host1',
79
'display_name': 'Volume Test Name',
80
'attachments': [{'device': '/',
81
'server_id': 'fakeuuid',
84
'volume_type': 'vol_type_name',
88
'created_at': datetime.datetime(1999, 1, 1,
91
self.assertEqual(res.obj, expected)
92
self.assertEqual(res.code, 200)
93
self.assertTrue('location' in res.headers)
95
def test_volume_creation_fails_with_bad_size(self):
97
"display_name": "Volume Test Name",
98
"display_description": "Volume Test Desc",
99
"availability_zone": "zone1:host1"}
100
body = {"volume": vol}
101
req = fakes.HTTPRequest.blank('/v1/volumes')
102
self.assertRaises(exception.InvalidInput,
103
self.controller.create,
107
def test_volume_create_no_body(self):
109
req = fakes.HTTPRequest.blank('/v1/volumes')
110
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
111
self.controller.create,
115
def test_volume_create_with_image_id(self):
116
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
117
self.ext_mgr.extensions = {'os-image-create': 'fake'}
119
"display_name": "Volume Test Name",
120
"display_description": "Volume Test Desc",
121
"availability_zone": "nova",
122
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'}
123
expected = {'volume': {'status': 'fakestatus',
124
'display_description': 'Volume Test Desc',
125
'availability_zone': 'nova',
126
'display_name': 'Volume Test Name',
127
'attachments': [{'device': '/',
128
'server_id': 'fakeuuid',
131
'volume_type': 'vol_type_name',
132
'image_id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
136
'created_at': datetime.datetime(1999, 1, 1,
140
body = {"volume": vol}
141
req = fakes.HTTPRequest.blank('/v1/volumes')
142
res = self.controller.create(req, body)
144
self.assertEqual(res.obj, expected)
146
def test_volume_create_with_image_id_and_snapshot_id(self):
147
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
148
self.stubs.Set(volume_api.API, "get_snapshot", stub_snapshot_get)
149
self.ext_mgr.extensions = {'os-image-create': 'fake'}
151
"display_name": "Volume Test Name",
152
"display_description": "Volume Test Desc",
153
"availability_zone": "nova",
154
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
155
"snapshot_id": TEST_SNAPSHOT_UUID}
156
body = {"volume": vol}
157
req = fakes.HTTPRequest.blank('/v1/volumes')
158
self.assertRaises(webob.exc.HTTPBadRequest,
159
self.controller.create,
163
def test_volume_create_with_image_id_is_integer(self):
164
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
165
self.ext_mgr.extensions = {'os-image-create': 'fake'}
167
"display_name": "Volume Test Name",
168
"display_description": "Volume Test Desc",
169
"availability_zone": "nova",
171
body = {"volume": vol}
172
req = fakes.HTTPRequest.blank('/v1/volumes')
173
self.assertRaises(webob.exc.HTTPBadRequest,
174
self.controller.create,
178
def test_volume_create_with_image_id_not_uuid_format(self):
179
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
180
self.ext_mgr.extensions = {'os-image-create': 'fake'}
182
"display_name": "Volume Test Name",
183
"display_description": "Volume Test Desc",
184
"availability_zone": "nova",
186
body = {"volume": vol}
187
req = fakes.HTTPRequest.blank('/v1/volumes')
188
self.assertRaises(webob.exc.HTTPBadRequest,
189
self.controller.create,
193
def test_volume_list(self):
194
self.stubs.Set(volume_api.API, 'get_all',
195
fakes.stub_volume_get_all_by_project)
197
req = fakes.HTTPRequest.blank('/v1/volumes')
198
res_dict = self.controller.index(req)
199
expected = {'volumes': [{'status': 'fakestatus',
200
'display_description': 'displaydesc',
201
'availability_zone': 'fakeaz',
202
'display_name': 'displayname',
203
'attachments': [{'device': '/',
204
'server_id': 'fakeuuid',
207
'volume_type': 'vol_type_name',
211
'created_at': datetime.datetime(1999, 1, 1,
215
self.assertEqual(res_dict, expected)
217
def test_volume_list_detail(self):
218
self.stubs.Set(volume_api.API, 'get_all',
219
fakes.stub_volume_get_all_by_project)
221
req = fakes.HTTPRequest.blank('/v1/volumes/detail')
222
res_dict = self.controller.index(req)
223
expected = {'volumes': [{'status': 'fakestatus',
224
'display_description': 'displaydesc',
225
'availability_zone': 'fakeaz',
226
'display_name': 'displayname',
227
'attachments': [{'device': '/',
228
'server_id': 'fakeuuid',
231
'volume_type': 'vol_type_name',
235
'created_at': datetime.datetime(1999, 1, 1,
238
self.assertEqual(res_dict, expected)
240
def test_volume_show(self):
241
req = fakes.HTTPRequest.blank('/v1/volumes/1')
242
res_dict = self.controller.show(req, '1')
243
expected = {'volume': {'status': 'fakestatus',
244
'display_description': 'displaydesc',
245
'availability_zone': 'fakeaz',
246
'display_name': 'displayname',
247
'attachments': [{'device': '/',
248
'server_id': 'fakeuuid',
251
'volume_type': 'vol_type_name',
255
'created_at': datetime.datetime(1999, 1, 1,
258
self.assertEqual(res_dict, expected)
260
def test_volume_show_no_attachments(self):
261
def stub_volume_get(self, context, volume_id):
262
return fakes.stub_volume(volume_id, attach_status='detached')
264
self.stubs.Set(volume_api.API, 'get', stub_volume_get)
266
req = fakes.HTTPRequest.blank('/v1/volumes/1')
267
res_dict = self.controller.show(req, '1')
268
expected = {'volume': {'status': 'fakestatus',
269
'display_description': 'displaydesc',
270
'availability_zone': 'fakeaz',
271
'display_name': 'displayname',
273
'volume_type': 'vol_type_name',
277
'created_at': datetime.datetime(1999, 1, 1,
280
self.assertEqual(res_dict, expected)
282
def test_volume_show_no_volume(self):
283
self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound)
285
req = fakes.HTTPRequest.blank('/v1/volumes/1')
286
self.assertRaises(webob.exc.HTTPNotFound,
287
self.controller.show,
291
def test_volume_delete(self):
292
req = fakes.HTTPRequest.blank('/v1/volumes/1')
293
resp = self.controller.delete(req, 1)
294
self.assertEqual(resp.status_int, 202)
296
def test_volume_delete_no_volume(self):
297
self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound)
299
req = fakes.HTTPRequest.blank('/v1/volumes/1')
300
self.assertRaises(webob.exc.HTTPNotFound,
301
self.controller.delete,
305
def test_admin_list_volumes_limited_to_project(self):
306
req = fakes.HTTPRequest.blank('/v1/fake/volumes',
307
use_admin_context=True)
308
res = self.controller.index(req)
310
self.assertTrue('volumes' in res)
311
self.assertEqual(1, len(res['volumes']))
313
def test_admin_list_volumes_all_tenants(self):
314
req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1',
315
use_admin_context=True)
316
res = self.controller.index(req)
317
self.assertTrue('volumes' in res)
318
self.assertEqual(3, len(res['volumes']))
320
def test_all_tenants_non_admin_gets_all_tenants(self):
321
req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1')
322
res = self.controller.index(req)
323
self.assertTrue('volumes' in res)
324
self.assertEqual(1, len(res['volumes']))
326
def test_non_admin_get_by_project(self):
327
req = fakes.HTTPRequest.blank('/v1/fake/volumes')
328
res = self.controller.index(req)
329
self.assertTrue('volumes' in res)
330
self.assertEqual(1, len(res['volumes']))
333
class VolumeSerializerTest(test.TestCase):
334
def _verify_volume_attachment(self, attach, tree):
335
for attr in ('id', 'volume_id', 'server_id', 'device'):
336
self.assertEqual(str(attach[attr]), tree.get(attr))
338
def _verify_volume(self, vol, tree):
339
self.assertEqual(tree.tag, 'volume')
341
for attr in ('id', 'status', 'size', 'availability_zone', 'created_at',
342
'display_name', 'display_description', 'volume_type',
344
self.assertEqual(str(vol[attr]), tree.get(attr))
347
self.assertTrue(child.tag in ('attachments', 'metadata'))
348
if child.tag == 'attachments':
349
self.assertEqual(1, len(child))
350
self.assertEqual('attachment', child[0].tag)
351
self._verify_volume_attachment(vol['attachments'][0], child[0])
352
elif child.tag == 'metadata':
353
not_seen = set(vol['metadata'].keys())
354
for gr_child in child:
355
self.assertTrue(gr_child.get("key") in not_seen)
356
self.assertEqual(str(vol['metadata'][gr_child.get("key")]),
358
not_seen.remove(gr_child.get("key"))
359
self.assertEqual(0, len(not_seen))
361
def test_volume_show_create_serializer(self):
362
serializer = volumes.VolumeTemplate()
367
availability_zone='vol_availability',
368
created_at=timeutils.utcnow(),
372
server_id='instance_uuid',
374
display_name='vol_name',
375
display_description='vol_desc',
376
volume_type='vol_type',
377
snapshot_id='snap_id',
383
text = serializer.serialize(dict(volume=raw_volume))
386
tree = etree.fromstring(text)
388
self._verify_volume(raw_volume, tree)
390
def test_volume_index_detail_serializer(self):
391
serializer = volumes.VolumesTemplate()
394
status='vol1_status',
396
availability_zone='vol1_availability',
397
created_at=timeutils.utcnow(),
401
server_id='instance_uuid',
403
display_name='vol1_name',
404
display_description='vol1_desc',
405
volume_type='vol1_type',
406
snapshot_id='snap1_id',
414
status='vol2_status',
416
availability_zone='vol2_availability',
417
created_at=timeutils.utcnow(),
421
server_id='instance_uuid',
423
display_name='vol2_name',
424
display_description='vol2_desc',
425
volume_type='vol2_type',
426
snapshot_id='snap2_id',
432
text = serializer.serialize(dict(volumes=raw_volumes))
435
tree = etree.fromstring(text)
437
self.assertEqual('volumes', tree.tag)
438
self.assertEqual(len(raw_volumes), len(tree))
439
for idx, child in enumerate(tree):
440
self._verify_volume(raw_volumes[idx], child)
443
class TestVolumeCreateRequestXMLDeserializer(test.TestCase):
446
super(TestVolumeCreateRequestXMLDeserializer, self).setUp()
447
self.deserializer = volumes.CreateDeserializer()
449
def test_minimal_volume(self):
451
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
452
size="1"></volume>"""
453
request = self.deserializer.deserialize(self_request)
459
self.assertEquals(request['body'], expected)
461
def test_display_name(self):
463
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
465
display_name="Volume-xml"></volume>"""
466
request = self.deserializer.deserialize(self_request)
470
"display_name": "Volume-xml",
473
self.assertEquals(request['body'], expected)
475
def test_display_description(self):
477
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
479
display_name="Volume-xml"
480
display_description="description"></volume>"""
481
request = self.deserializer.deserialize(self_request)
485
"display_name": "Volume-xml",
486
"display_description": "description",
489
self.assertEquals(request['body'], expected)
491
def test_volume_type(self):
493
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
495
display_name="Volume-xml"
496
display_description="description"
497
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"></volume>"""
498
request = self.deserializer.deserialize(self_request)
501
"display_name": "Volume-xml",
503
"display_name": "Volume-xml",
504
"display_description": "description",
505
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
508
self.assertEquals(request['body'], expected)
510
def test_availability_zone(self):
512
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
514
display_name="Volume-xml"
515
display_description="description"
516
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
517
availability_zone="us-east1"></volume>"""
518
request = self.deserializer.deserialize(self_request)
522
"display_name": "Volume-xml",
523
"display_description": "description",
524
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
525
"availability_zone": "us-east1",
528
self.assertEquals(request['body'], expected)
530
def test_metadata(self):
532
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
533
display_name="Volume-xml"
535
<metadata><meta key="Type">work</meta></metadata></volume>"""
536
request = self.deserializer.deserialize(self_request)
539
"display_name": "Volume-xml",
546
self.assertEquals(request['body'], expected)
548
def test_full_volume(self):
550
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
552
display_name="Volume-xml"
553
display_description="description"
554
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
555
availability_zone="us-east1">
556
<metadata><meta key="Type">work</meta></metadata></volume>"""
557
request = self.deserializer.deserialize(self_request)
561
"display_name": "Volume-xml",
562
"display_description": "description",
563
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
564
"availability_zone": "us-east1",
571
self.assertEquals(request['body'], expected)
574
class VolumesUnprocessableEntityTestCase(test.TestCase):
577
Tests of places we throw 422 Unprocessable Entity from
581
super(VolumesUnprocessableEntityTestCase, self).setUp()
582
self.ext_mgr = extensions.ExtensionManager()
583
self.ext_mgr.extensions = {}
584
self.controller = volumes.VolumeController(self.ext_mgr)
586
def _unprocessable_volume_create(self, body):
587
req = fakes.HTTPRequest.blank('/v2/fake/volumes')
590
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
591
self.controller.create, req, body)
593
def test_create_no_body(self):
594
self._unprocessable_volume_create(body=None)
596
def test_create_missing_volume(self):
597
body = {'foo': {'a': 'b'}}
598
self._unprocessable_volume_create(body=body)
600
def test_create_malformed_entity(self):
601
body = {'volume': 'string'}
602
self._unprocessable_volume_create(body=body)