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 cinder.api.openstack.volume import volumes
23
from cinder.api.openstack.volume import extensions
24
from cinder import exception
25
from cinder import flags
26
from cinder import test
27
from cinder.tests.api.openstack import fakes
28
from cinder.tests.image import fake as fake_image
29
from cinder.volume import api as volume_api
33
NS = '{http://docs.openstack.org/volume/api/v1}'
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
fake_image.stub_out_image_service(self.stubs)
59
self.controller = volumes.VolumeController(self.ext_mgr)
61
self.stubs.Set(db, 'volume_get_all', fakes.stub_volume_get_all)
62
self.stubs.Set(db, 'volume_get_all_by_project',
63
fakes.stub_volume_get_all_by_project)
64
self.stubs.Set(volume_api.API, 'get', fakes.stub_volume_get)
65
self.stubs.Set(volume_api.API, 'delete', fakes.stub_volume_delete)
67
def test_volume_create(self):
68
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
71
"display_name": "Volume Test Name",
72
"display_description": "Volume Test Desc",
73
"availability_zone": "zone1:host1"}
74
body = {"volume": vol}
75
req = fakes.HTTPRequest.blank('/v1/volumes')
76
res_dict = self.controller.create(req, body)
77
expected = {'volume': {'status': 'fakestatus',
78
'display_description': 'Volume Test Desc',
79
'availability_zone': 'zone1:host1',
80
'display_name': 'Volume Test Name',
81
'attachments': [{'device': '/',
82
'server_id': 'fakeuuid',
85
'volume_type': 'vol_type_name',
89
'created_at': datetime.datetime(1, 1, 1,
92
self.assertEqual(res_dict, expected)
94
def test_volume_creation_fails_with_bad_size(self):
96
"display_name": "Volume Test Name",
97
"display_description": "Volume Test Desc",
98
"availability_zone": "zone1:host1"}
99
body = {"volume": vol}
100
req = fakes.HTTPRequest.blank('/v1/volumes')
101
self.assertRaises(exception.InvalidInput,
102
self.controller.create,
106
def test_volume_create_with_image_id(self):
107
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
108
self.ext_mgr.extensions = {'os-image-create': 'fake'}
110
"display_name": "Volume Test Name",
111
"display_description": "Volume Test Desc",
112
"availability_zone": "nova",
113
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'}
114
expected = {'volume': {'status': 'fakestatus',
115
'display_description': 'Volume Test Desc',
116
'availability_zone': 'nova',
117
'display_name': 'Volume Test Name',
118
'attachments': [{'device': '/',
119
'server_id': 'fakeuuid',
122
'volume_type': 'vol_type_name',
123
'image_id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
127
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
130
body = {"volume": vol}
131
req = fakes.HTTPRequest.blank('/v1/volumes')
132
res_dict = self.controller.create(req, body)
133
self.assertEqual(res_dict, expected)
135
def test_volume_create_with_image_id_and_snapshot_id(self):
136
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
137
self.stubs.Set(volume_api.API, "get_snapshot", stub_snapshot_get)
138
self.ext_mgr.extensions = {'os-image-create': 'fake'}
140
"display_name": "Volume Test Name",
141
"display_description": "Volume Test Desc",
142
"availability_zone": "cinder",
143
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
144
"snapshot_id": TEST_SNAPSHOT_UUID}
145
body = {"volume": vol}
146
req = fakes.HTTPRequest.blank('/v1/volumes')
147
self.assertRaises(webob.exc.HTTPBadRequest,
148
self.controller.create,
152
def test_volume_create_with_image_id_is_integer(self):
153
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
154
self.ext_mgr.extensions = {'os-image-create': 'fake'}
156
"display_name": "Volume Test Name",
157
"display_description": "Volume Test Desc",
158
"availability_zone": "cinder",
160
body = {"volume": vol}
161
req = fakes.HTTPRequest.blank('/v1/volumes')
162
self.assertRaises(webob.exc.HTTPBadRequest,
163
self.controller.create,
167
def test_volume_create_with_image_id_not_uuid_format(self):
168
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
169
self.ext_mgr.extensions = {'os-image-create': 'fake'}
171
"display_name": "Volume Test Name",
172
"display_description": "Volume Test Desc",
173
"availability_zone": "cinder",
175
body = {"volume": vol}
176
req = fakes.HTTPRequest.blank('/v1/volumes')
177
self.assertRaises(webob.exc.HTTPBadRequest,
178
self.controller.create,
182
def test_volume_list(self):
183
self.stubs.Set(volume_api.API, 'get_all',
184
fakes.stub_volume_get_all_by_project)
186
req = fakes.HTTPRequest.blank('/v1/volumes')
187
res_dict = self.controller.index(req)
188
expected = {'volumes': [{'status': 'fakestatus',
189
'display_description': 'displaydesc',
190
'availability_zone': 'fakeaz',
191
'display_name': 'displayname',
192
'attachments': [{'device': '/',
193
'server_id': 'fakeuuid',
196
'volume_type': 'vol_type_name',
200
'created_at': datetime.datetime(1, 1, 1,
203
self.assertEqual(res_dict, expected)
205
def test_volume_list_detail(self):
206
self.stubs.Set(volume_api.API, 'get_all',
207
fakes.stub_volume_get_all_by_project)
208
req = fakes.HTTPRequest.blank('/v1/volumes/detail')
209
res_dict = self.controller.index(req)
210
expected = {'volumes': [{'status': 'fakestatus',
211
'display_description': 'displaydesc',
212
'availability_zone': 'fakeaz',
213
'display_name': 'displayname',
214
'attachments': [{'device': '/',
215
'server_id': 'fakeuuid',
218
'volume_type': 'vol_type_name',
222
'created_at': datetime.datetime(1, 1, 1,
225
self.assertEqual(res_dict, expected)
227
def test_volume_list_by_name(self):
228
def stub_volume_get_all_by_project(context, project_id):
230
fakes.stub_volume(1, display_name='vol1'),
231
fakes.stub_volume(2, display_name='vol2'),
232
fakes.stub_volume(3, display_name='vol3'),
234
self.stubs.Set(db, 'volume_get_all_by_project',
235
stub_volume_get_all_by_project)
237
# no display_name filter
238
req = fakes.HTTPRequest.blank('/v1/volumes')
239
resp = self.controller.index(req)
240
self.assertEqual(len(resp['volumes']), 3)
241
# filter on display_name
242
req = fakes.HTTPRequest.blank('/v1/volumes?display_name=vol2')
243
resp = self.controller.index(req)
244
self.assertEqual(len(resp['volumes']), 1)
245
self.assertEqual(resp['volumes'][0]['display_name'], 'vol2')
247
req = fakes.HTTPRequest.blank('/v1/volumes?display_name=vol4')
248
resp = self.controller.index(req)
249
self.assertEqual(len(resp['volumes']), 0)
251
def test_volume_list_by_status(self):
252
def stub_volume_get_all_by_project(context, project_id):
254
fakes.stub_volume(1, display_name='vol1', status='available'),
255
fakes.stub_volume(2, display_name='vol2', status='available'),
256
fakes.stub_volume(3, display_name='vol3', status='in-use'),
258
self.stubs.Set(db, 'volume_get_all_by_project',
259
stub_volume_get_all_by_project)
261
req = fakes.HTTPRequest.blank('/v1/volumes')
262
resp = self.controller.index(req)
263
self.assertEqual(len(resp['volumes']), 3)
265
req = fakes.HTTPRequest.blank('/v1/volumes?status=in-use')
266
resp = self.controller.index(req)
267
self.assertEqual(len(resp['volumes']), 1)
268
self.assertEqual(resp['volumes'][0]['status'], 'in-use')
270
req = fakes.HTTPRequest.blank('/v1/volumes?status=available')
271
resp = self.controller.index(req)
272
self.assertEqual(len(resp['volumes']), 2)
273
for volume in resp['volumes']:
274
self.assertEqual(volume['status'], 'available')
276
req = fakes.HTTPRequest.blank('/v1/volumes?status=available&'
278
resp = self.controller.index(req)
279
self.assertEqual(len(resp['volumes']), 1)
280
self.assertEqual(resp['volumes'][0]['display_name'], 'vol1')
281
self.assertEqual(resp['volumes'][0]['status'], 'available')
283
req = fakes.HTTPRequest.blank('/v1/volumes?status=in-use&'
285
resp = self.controller.index(req)
286
self.assertEqual(len(resp['volumes']), 0)
288
def test_volume_show(self):
289
req = fakes.HTTPRequest.blank('/v1/volumes/1')
290
res_dict = self.controller.show(req, '1')
291
expected = {'volume': {'status': 'fakestatus',
292
'display_description': 'displaydesc',
293
'availability_zone': 'fakeaz',
294
'display_name': 'displayname',
295
'attachments': [{'device': '/',
296
'server_id': 'fakeuuid',
299
'volume_type': 'vol_type_name',
303
'created_at': datetime.datetime(1, 1, 1,
306
self.assertEqual(res_dict, expected)
308
def test_volume_show_no_attachments(self):
309
def stub_volume_get(self, context, volume_id):
310
return fakes.stub_volume(volume_id, attach_status='detached')
312
self.stubs.Set(volume_api.API, 'get', stub_volume_get)
314
req = fakes.HTTPRequest.blank('/v1/volumes/1')
315
res_dict = self.controller.show(req, '1')
316
expected = {'volume': {'status': 'fakestatus',
317
'display_description': 'displaydesc',
318
'availability_zone': 'fakeaz',
319
'display_name': 'displayname',
321
'volume_type': 'vol_type_name',
325
'created_at': datetime.datetime(1, 1, 1,
328
self.assertEqual(res_dict, expected)
330
def test_volume_show_no_volume(self):
331
self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound)
333
req = fakes.HTTPRequest.blank('/v1/volumes/1')
334
self.assertRaises(webob.exc.HTTPNotFound,
335
self.controller.show,
339
def test_volume_delete(self):
340
req = fakes.HTTPRequest.blank('/v1/volumes/1')
341
resp = self.controller.delete(req, 1)
342
self.assertEqual(resp.status_int, 202)
344
def test_volume_delete_no_volume(self):
345
self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound)
347
req = fakes.HTTPRequest.blank('/v1/volumes/1')
348
self.assertRaises(webob.exc.HTTPNotFound,
349
self.controller.delete,
353
def test_admin_list_volumes_limited_to_project(self):
354
req = fakes.HTTPRequest.blank('/v1/fake/volumes',
355
use_admin_context=True)
356
res = self.controller.index(req)
358
self.assertTrue('volumes' in res)
359
self.assertEqual(1, len(res['volumes']))
361
def test_admin_list_volumes_all_tenants(self):
362
req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1',
363
use_admin_context=True)
364
res = self.controller.index(req)
365
self.assertTrue('volumes' in res)
366
self.assertEqual(3, len(res['volumes']))
368
def test_all_tenants_non_admin_gets_all_tenants(self):
369
req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1')
370
res = self.controller.index(req)
371
self.assertTrue('volumes' in res)
372
self.assertEqual(1, len(res['volumes']))
374
def test_non_admin_get_by_project(self):
375
req = fakes.HTTPRequest.blank('/v1/fake/volumes')
376
res = self.controller.index(req)
377
self.assertTrue('volumes' in res)
378
self.assertEqual(1, len(res['volumes']))
381
class VolumeSerializerTest(test.TestCase):
382
def _verify_volume_attachment(self, attach, tree):
383
for attr in ('id', 'volume_id', 'server_id', 'device'):
384
self.assertEqual(str(attach[attr]), tree.get(attr))
386
def _verify_volume(self, vol, tree):
387
self.assertEqual(tree.tag, NS + 'volume')
389
for attr in ('id', 'status', 'size', 'availability_zone', 'created_at',
390
'display_name', 'display_description', 'volume_type',
392
self.assertEqual(str(vol[attr]), tree.get(attr))
396
self.assertTrue(child.tag in (NS + 'attachments', NS + 'metadata'))
397
if child.tag == 'attachments':
398
self.assertEqual(1, len(child))
399
self.assertEqual('attachment', child[0].tag)
400
self._verify_volume_attachment(vol['attachments'][0], child[0])
401
elif child.tag == 'metadata':
402
not_seen = set(vol['metadata'].keys())
403
for gr_child in child:
404
self.assertTrue(gr_child.get("key") in not_seen)
405
self.assertEqual(str(vol['metadata'][gr_child.get("key")]),
407
not_seen.remove(gr_child.get('key'))
408
self.assertEqual(0, len(not_seen))
410
def test_volume_show_create_serializer(self):
411
serializer = volumes.VolumeTemplate()
416
availability_zone='vol_availability',
417
created_at=datetime.datetime.now(),
421
server_id='instance_uuid',
423
display_name='vol_name',
424
display_description='vol_desc',
425
volume_type='vol_type',
426
snapshot_id='snap_id',
432
text = serializer.serialize(dict(volume=raw_volume))
435
tree = etree.fromstring(text)
437
self._verify_volume(raw_volume, tree)
439
def test_volume_index_detail_serializer(self):
440
serializer = volumes.VolumesTemplate()
443
status='vol1_status',
445
availability_zone='vol1_availability',
446
created_at=datetime.datetime.now(),
450
server_id='instance_uuid',
452
display_name='vol1_name',
453
display_description='vol1_desc',
454
volume_type='vol1_type',
455
snapshot_id='snap1_id',
463
status='vol2_status',
465
availability_zone='vol2_availability',
466
created_at=datetime.datetime.now(),
470
server_id='instance_uuid',
472
display_name='vol2_name',
473
display_description='vol2_desc',
474
volume_type='vol2_type',
475
snapshot_id='snap2_id',
481
text = serializer.serialize(dict(volumes=raw_volumes))
484
tree = etree.fromstring(text)
486
self.assertEqual(NS + 'volumes', tree.tag)
487
self.assertEqual(len(raw_volumes), len(tree))
488
for idx, child in enumerate(tree):
489
self._verify_volume(raw_volumes[idx], child)
492
class TestVolumeCreateRequestXMLDeserializer(test.TestCase):
495
super(TestVolumeCreateRequestXMLDeserializer, self).setUp()
496
self.deserializer = volumes.CreateDeserializer()
498
def test_minimal_volume(self):
500
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
501
size="1"></volume>"""
502
request = self.deserializer.deserialize(self_request)
508
self.assertEquals(request['body'], expected)
510
def test_display_name(self):
512
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
514
display_name="Volume-xml"></volume>"""
515
request = self.deserializer.deserialize(self_request)
519
"display_name": "Volume-xml",
522
self.assertEquals(request['body'], expected)
524
def test_display_description(self):
526
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
528
display_name="Volume-xml"
529
display_description="description"></volume>"""
530
request = self.deserializer.deserialize(self_request)
534
"display_name": "Volume-xml",
535
"display_description": "description",
538
self.assertEquals(request['body'], expected)
540
def test_volume_type(self):
542
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
544
display_name="Volume-xml"
545
display_description="description"
546
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"></volume>"""
547
request = self.deserializer.deserialize(self_request)
550
"display_name": "Volume-xml",
552
"display_name": "Volume-xml",
553
"display_description": "description",
554
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
557
self.assertEquals(request['body'], expected)
559
def test_availability_zone(self):
561
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
563
display_name="Volume-xml"
564
display_description="description"
565
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
566
availability_zone="us-east1"></volume>"""
567
request = self.deserializer.deserialize(self_request)
571
"display_name": "Volume-xml",
572
"display_description": "description",
573
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
574
"availability_zone": "us-east1",
577
self.assertEquals(request['body'], expected)
579
def test_metadata(self):
581
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
582
display_name="Volume-xml"
584
<metadata><meta key="Type">work</meta></metadata></volume>"""
585
request = self.deserializer.deserialize(self_request)
588
"display_name": "Volume-xml",
595
self.assertEquals(request['body'], expected)
597
def test_full_volume(self):
599
<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
601
display_name="Volume-xml"
602
display_description="description"
603
volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
604
availability_zone="us-east1">
605
<metadata><meta key="Type">work</meta></metadata></volume>"""
606
request = self.deserializer.deserialize(self_request)
610
"display_name": "Volume-xml",
611
"display_description": "description",
612
"volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
613
"availability_zone": "us-east1",
619
self.assertEquals(request['body'], expected)
622
class VolumesUnprocessableEntityTestCase(test.TestCase):
625
Tests of places we throw 422 Unprocessable Entity from
629
super(VolumesUnprocessableEntityTestCase, self).setUp()
630
self.ext_mgr = extensions.ExtensionManager()
631
self.ext_mgr.extensions = {}
632
self.controller = volumes.VolumeController(self.ext_mgr)
634
def _unprocessable_volume_create(self, body):
635
req = fakes.HTTPRequest.blank('/v2/fake/volumes')
638
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
639
self.controller.create, req, body)
641
def test_create_no_body(self):
642
self._unprocessable_volume_create(body=None)
644
def test_create_missing_volume(self):
645
body = {'foo': {'a': 'b'}}
646
self._unprocessable_volume_create(body=body)
648
def test_create_malformed_entity(self):
649
body = {'volume': 'string'}
650
self._unprocessable_volume_create(body=body)