Coverage for manila/tests/api/v1/test_share_snapshots.py: 100%
222 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2012 NetApp
2# All Rights Reserved.
3#
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
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
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
14# under the License.
16from unittest import mock
18import ddt
19from oslo_serialization import jsonutils
20import webob
22from manila.api.v1 import share_snapshots
23from manila.common import constants
24from manila import context
25from manila import db
26from manila import exception
27from manila.share import api as share_api
28from manila import test
29from manila.tests.api.contrib import stubs
30from manila.tests.api import fakes
31from manila.tests import db_utils
32from manila.tests import fake_share
35@ddt.ddt
36class ShareSnapshotAPITest(test.TestCase):
37 """Share Snapshot API Test."""
39 def setUp(self):
40 super(ShareSnapshotAPITest, self).setUp()
41 self.controller = share_snapshots.ShareSnapshotsController()
43 self.mock_object(share_api.API, 'get', stubs.stub_share_get)
44 self.mock_object(share_api.API, 'get_all_snapshots',
45 stubs.stub_snapshot_get_all_by_project)
46 self.mock_object(share_api.API, 'get_snapshot',
47 stubs.stub_snapshot_get)
48 self.mock_object(share_api.API, 'snapshot_update',
49 stubs.stub_snapshot_update)
50 self.snp_example = {
51 'share_id': 100,
52 'size': 12,
53 'force': False,
54 'display_name': 'updated_share_name',
55 'display_description': 'updated_share_description',
56 }
57 self.maxDiff = None
59 def test_snapshot_show_status_none(self):
60 return_snapshot = {
61 'share_id': 100,
62 'name': 'fake_share_name',
63 'description': 'fake_share_description',
64 'status': None,
65 }
66 self.mock_object(share_api.API, 'get_snapshot',
67 mock.Mock(return_value=return_snapshot))
68 req = fakes.HTTPRequest.blank('/fake/snapshots/200')
69 self.assertRaises(webob.exc.HTTPNotFound,
70 self.controller.show,
71 req, '200')
73 @ddt.data('true', 'True', '<is> True', '1')
74 def test_snapshot_create(self, snapshot_support):
75 self.mock_object(share_api.API, 'create_snapshot',
76 stubs.stub_snapshot_create)
77 body = {
78 'snapshot': {
79 'share_id': 'fakeshareid',
80 'force': False,
81 'name': 'displaysnapname',
82 'description': 'displaysnapdesc',
83 }
84 }
85 req = fakes.HTTPRequest.blank('/fake/snapshots')
87 res_dict = self.controller.create(req, body)
89 expected = fake_share.expected_snapshot(id=200)
91 self.assertEqual(expected, res_dict)
93 @ddt.data(
94 {'name': 'name1', 'description': 'x' * 256},
95 {'name': 'x' * 256, 'description': 'description1'},
96 )
97 @ddt.unpack
98 def test_snapshot_create_invalid_input(self, name, description):
99 self.mock_object(share_api.API, 'create_snapshot')
100 self.mock_object(
101 share_api.API,
102 'get',
103 mock.Mock(return_value={'snapshot_support': True,
104 'is_soft_deleted': False}))
105 body = {
106 'snapshot': {
107 'share_id': 200,
108 'force': False,
109 'name': name,
110 'description': description,
111 }
112 }
113 req = fakes.HTTPRequest.blank('/fake/snapshots')
115 self.assertRaises(
116 exception.InvalidInput,
117 self.controller.create, req, body)
119 @ddt.data(0, False)
120 def test_snapshot_create_no_support(self, snapshot_support):
121 self.mock_object(share_api.API, 'create_snapshot')
122 self.mock_object(
123 share_api.API,
124 'get',
125 mock.Mock(return_value={'snapshot_support': snapshot_support}))
126 body = {
127 'snapshot': {
128 'share_id': 100,
129 'force': False,
130 'name': 'fake_share_name',
131 'description': 'fake_share_description',
132 }
133 }
134 req = fakes.HTTPRequest.blank('/fake/snapshots')
136 self.assertRaises(
137 webob.exc.HTTPUnprocessableEntity,
138 self.controller.create, req, body)
140 self.assertFalse(share_api.API.create_snapshot.called)
142 def test_snapshot_create_in_recycle_bin(self):
143 self.mock_object(share_api.API, 'create_snapshot')
144 self.mock_object(
145 share_api.API,
146 'get',
147 mock.Mock(return_value={'snapshot_support': True,
148 'is_soft_deleted': True}))
149 body = {
150 'snapshot': {
151 'share_id': 200,
152 'force': False,
153 'name': 'fake_share_name',
154 'description': 'fake_share_description',
155 }
156 }
157 req = fakes.HTTPRequest.blank('/fake/snapshots')
159 self.assertRaises(
160 webob.exc.HTTPForbidden,
161 self.controller.create, req, body)
163 self.assertFalse(share_api.API.create_snapshot.called)
165 def test_snapshot_create_no_body(self):
166 body = {}
167 req = fakes.HTTPRequest.blank('/fake/snapshots')
168 self.assertRaises(webob.exc.HTTPUnprocessableEntity,
169 self.controller.create,
170 req,
171 body)
173 def test_snapshot_delete(self):
174 self.mock_object(share_api.API, 'delete_snapshot',
175 stubs.stub_snapshot_delete)
176 req = fakes.HTTPRequest.blank('/fake/snapshots/200')
177 resp = self.controller.delete(req, 200)
178 self.assertEqual(202, resp.status_int)
180 def test_snapshot_delete_nofound(self):
181 self.mock_object(share_api.API, 'get_snapshot',
182 stubs.stub_snapshot_get_notfound)
183 req = fakes.HTTPRequest.blank('/fake/snapshots/200')
184 self.assertRaises(webob.exc.HTTPNotFound,
185 self.controller.delete,
186 req,
187 200)
189 def test_snapshot_show(self):
190 req = fakes.HTTPRequest.blank('/fake/snapshots/200')
191 res_dict = self.controller.show(req, 200)
192 expected = fake_share.expected_snapshot(id=200)
193 self.assertEqual(expected, res_dict)
195 def test_snapshot_show_nofound(self):
196 self.mock_object(share_api.API, 'get_snapshot',
197 stubs.stub_snapshot_get_notfound)
198 req = fakes.HTTPRequest.blank('/fake/snapshots/200')
199 self.assertRaises(webob.exc.HTTPNotFound,
200 self.controller.show,
201 req, '200')
203 def test_snapshot_list_summary(self):
204 self.mock_object(share_api.API, 'get_all_snapshots',
205 stubs.stub_snapshot_get_all_by_project)
206 req = fakes.HTTPRequest.blank('/fake/snapshots')
207 res_dict = self.controller.index(req)
208 expected = {
209 'snapshots': [
210 {
211 'name': 'displaysnapname',
212 'id': 2,
213 'links': [
214 {
215 'href': 'http://localhost/share/v1/fake/'
216 'snapshots/2',
217 'rel': 'self'
218 },
219 {
220 'href': 'http://localhost/share/fake/snapshots/2',
221 'rel': 'bookmark'
222 }
223 ],
224 }
225 ]
226 }
227 self.assertEqual(expected, res_dict)
229 def _snapshot_list_summary_with_search_opts(self, use_admin_context):
230 search_opts = fake_share.search_opts()
231 # fake_key should be filtered for non-admin
232 url = '/fake/snapshots?fake_key=fake_value'
233 for k, v in search_opts.items():
234 url = url + '&' + k + '=' + v
235 req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context)
237 db_snapshots = [
238 {'id': 'id1', 'display_name': 'n1',
239 'status': 'fake_status', 'share_id': 'fake_share_id'},
240 {'id': 'id2', 'display_name': 'n2',
241 'status': 'fake_status', 'share_id': 'fake_share_id'},
242 {'id': 'id3', 'display_name': 'n3',
243 'status': 'fake_status', 'share_id': 'fake_share_id'},
244 ]
245 snapshots = [db_snapshots[1]]
246 self.mock_object(share_api.API, 'get_all_snapshots',
247 mock.Mock(return_value=snapshots))
249 result = self.controller.index(req)
251 search_opts_expected = {
252 'display_name': search_opts['name'],
253 'status': search_opts['status'],
254 'share_id': search_opts['share_id'],
255 }
256 if use_admin_context:
257 search_opts_expected.update({'fake_key': 'fake_value'})
258 share_api.API.get_all_snapshots.assert_called_once_with(
259 req.environ['manila.context'],
260 limit=int(search_opts['limit']),
261 offset=int(search_opts['offset']),
262 sort_key=search_opts['sort_key'],
263 sort_dir=search_opts['sort_dir'],
264 search_opts=search_opts_expected,
265 )
266 self.assertEqual(1, len(result['snapshots']))
267 self.assertEqual(snapshots[0]['id'], result['snapshots'][0]['id'])
268 self.assertEqual(
269 snapshots[0]['display_name'], result['snapshots'][0]['name'])
271 def test_snapshot_list_summary_with_search_opts_by_non_admin(self):
272 self._snapshot_list_summary_with_search_opts(use_admin_context=False)
274 def test_snapshot_list_summary_with_search_opts_by_admin(self):
275 self._snapshot_list_summary_with_search_opts(use_admin_context=True)
277 def _snapshot_list_detail_with_search_opts(self, use_admin_context):
278 search_opts = fake_share.search_opts()
279 # fake_key should be filtered for non-admin
280 url = '/fake/shares/detail?fake_key=fake_value'
281 for k, v in search_opts.items():
282 url = url + '&' + k + '=' + v
283 req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context)
285 db_snapshots = [
286 {
287 'id': 'id1',
288 'display_name': 'n1',
289 'status': 'fake_status_other',
290 'aggregate_status': 'fake_status',
291 'share_id': 'fake_share_id',
292 },
293 {
294 'id': 'id2',
295 'display_name': 'n2',
296 'status': 'fake_status',
297 'aggregate_status': 'fake_status',
298 'share_id': 'fake_share_id',
299 },
300 {
301 'id': 'id3',
302 'display_name': 'n3',
303 'status': 'fake_status_other',
304 'aggregate_status': 'fake_status',
305 'share_id': 'fake_share_id',
306 },
307 ]
308 snapshots = [db_snapshots[1]]
310 self.mock_object(share_api.API, 'get_all_snapshots',
311 mock.Mock(return_value=snapshots))
313 result = self.controller.detail(req)
315 search_opts_expected = {
316 'display_name': search_opts['name'],
317 'status': search_opts['status'],
318 'share_id': search_opts['share_id'],
319 }
320 if use_admin_context:
321 search_opts_expected.update({'fake_key': 'fake_value'})
322 share_api.API.get_all_snapshots.assert_called_once_with(
323 req.environ['manila.context'],
324 limit=int(search_opts['limit']),
325 offset=int(search_opts['offset']),
326 sort_key=search_opts['sort_key'],
327 sort_dir=search_opts['sort_dir'],
328 search_opts=search_opts_expected,
329 )
330 self.assertEqual(1, len(result['snapshots']))
331 self.assertEqual(snapshots[0]['id'], result['snapshots'][0]['id'])
332 self.assertEqual(
333 snapshots[0]['display_name'], result['snapshots'][0]['name'])
334 self.assertEqual(
335 snapshots[0]['status'], result['snapshots'][0]['status'])
336 self.assertEqual(
337 snapshots[0]['share_id'], result['snapshots'][0]['share_id'])
339 def test_snapshot_list_detail_with_search_opts_by_non_admin(self):
340 self._snapshot_list_detail_with_search_opts(use_admin_context=False)
342 def test_snapshot_list_detail_with_search_opts_by_admin(self):
343 self._snapshot_list_detail_with_search_opts(use_admin_context=True)
345 def test_snapshot_list_detail(self):
346 env = {'QUERY_STRING': 'name=Share+Test+Name'}
347 req = fakes.HTTPRequest.blank('/fake/shares/detail', environ=env)
348 res_dict = self.controller.detail(req)
349 expected_s = fake_share.expected_snapshot(id=2)
350 expected = {'snapshots': [expected_s['snapshot']]}
351 self.assertEqual(expected, res_dict)
353 def test_snapshot_list_status_none(self):
354 snapshots = [
355 {
356 'id': 3,
357 'share_id': 'fakeshareid',
358 'size': 1,
359 'status': None,
360 'name': 'displaysnapname',
361 'description': 'displaysnapdesc',
362 }
363 ]
364 self.mock_object(share_api.API, 'get_all_snapshots',
365 mock.Mock(return_value=snapshots))
366 req = fakes.HTTPRequest.blank('/fake/snapshots')
367 result = self.controller.index(req)
368 self.assertEqual(1, len(result['snapshots']))
369 self.assertEqual(snapshots[0]['id'], result['snapshots'][0]['id'])
371 def test_snapshot_updates_description(self):
372 snp = self.snp_example
373 body = {"snapshot": snp}
375 req = fakes.HTTPRequest.blank('/fake/snapshot/1')
376 res_dict = self.controller.update(req, 1, body)
377 self.assertEqual(snp["display_name"], res_dict['snapshot']["name"])
379 def test_snapshot_updates_display_descr(self):
380 snp = self.snp_example
381 body = {"snapshot": snp}
383 req = fakes.HTTPRequest.blank('/fake/snapshot/1')
384 res_dict = self.controller.update(req, 1, body)
386 self.assertEqual(snp["display_description"],
387 res_dict['snapshot']["description"])
389 def test_share_not_updates_size(self):
390 snp = self.snp_example
391 body = {"snapshot": snp}
393 req = fakes.HTTPRequest.blank('/fake/snapshot/1')
394 res_dict = self.controller.update(req, 1, body)
396 self.assertNotEqual(snp["size"], res_dict['snapshot']["size"])
399@ddt.ddt
400class ShareSnapshotAdminActionsAPITest(test.TestCase):
402 def setUp(self):
403 super(ShareSnapshotAdminActionsAPITest, self).setUp()
404 self.controller = share_snapshots.ShareSnapshotsController()
405 self.flags(transport_url='rabbit://fake:fake@mqhost:5672')
406 self.admin_context = context.RequestContext('admin', 'fake', True)
407 self.member_context = context.RequestContext('fake', 'fake')
409 def _get_context(self, role):
410 return getattr(self, '%s_context' % role)
412 def _setup_snapshot_data(self, snapshot=None):
413 if snapshot is None:
414 share = db_utils.create_share()
415 snapshot = db_utils.create_snapshot(
416 status=constants.STATUS_AVAILABLE, share_id=share['id'])
417 req = fakes.HTTPRequest.blank('/v1/fake/snapshots/%s/action' %
418 snapshot['id'])
419 return snapshot, req
421 def _reset_status(self, ctxt, model, req, db_access_method,
422 valid_code, valid_status=None, body=None):
423 action_name = 'os-reset_status'
424 if body is None:
425 body = {action_name: {'status': constants.STATUS_ERROR}}
426 req.method = 'POST'
427 req.headers['content-type'] = 'application/json'
428 req.body = jsonutils.dumps(body).encode("utf-8")
429 req.environ['manila.context'] = ctxt
431 resp = req.get_response(fakes.app())
433 # validate response code and model status
434 self.assertEqual(valid_code, resp.status_int)
436 actual_model = db_access_method(ctxt, model['id'])
437 self.assertEqual(valid_status, actual_model['status'])
439 @ddt.data(*fakes.fixture_reset_status_with_different_roles_v1)
440 @ddt.unpack
441 def test_snapshot_reset_status_with_different_roles(self, role, valid_code,
442 valid_status):
443 ctxt = self._get_context(role)
444 snapshot, req = self._setup_snapshot_data()
446 self._reset_status(ctxt, snapshot, req, db.share_snapshot_get,
447 valid_code, valid_status)
449 @ddt.data(
450 {'os-reset_status': {'x-status': 'bad'}},
451 {'os-reset_status': {'status': 'invalid'}},
452 )
453 def test_snapshot_invalid_reset_status_body(self, body):
454 snapshot, req = self._setup_snapshot_data()
456 self._reset_status(self.admin_context, snapshot, req,
457 db.share_snapshot_get, 400,
458 constants.STATUS_AVAILABLE, body)
460 def _force_delete(self, ctxt, model, req, db_access_method, valid_code):
461 action_name = 'os-force_delete'
462 req.method = 'POST'
463 req.headers['content-type'] = 'application/json'
464 req.body = jsonutils.dumps({action_name: {}}).encode("utf-8")
465 req.environ['manila.context'] = ctxt
467 resp = req.get_response(fakes.app())
469 # Validate response
470 self.assertEqual(valid_code, resp.status_int)
472 @ddt.data(
473 {'role': 'admin', 'resp_code': 202},
474 {'role': 'member', 'resp_code': 403},
475 )
476 @ddt.unpack
477 def test_snapshot_force_delete_with_different_roles(self, role, resp_code):
478 ctxt = self._get_context(role)
479 snapshot, req = self._setup_snapshot_data()
481 self._force_delete(ctxt, snapshot, req, db.share_snapshot_get,
482 resp_code)
484 def test_snapshot_force_delete_missing(self):
485 ctxt = self._get_context('admin')
486 snapshot, req = self._setup_snapshot_data(snapshot={'id': 'fake'})
488 self._force_delete(ctxt, snapshot, req, db.share_snapshot_get, 404)