1# Copyright (c) 2016 Zadara Storage, Inc.
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.
15"""
16Tests for Zadara VPSA volume driver
17"""
18import copy
19import mock
20import requests
21from six.moves.urllib import parse
22
23from cinder import exception
24from cinder import test
25from cinder.volume import configuration as conf
26from cinder.volume.drivers import zadara
27
28
29DEFAULT_RUNTIME_VARS = {
30    'status': 200,
31    'user': 'test',
32    'password': 'test_password',
33    'access_key': '0123456789ABCDEF',
34    'volumes': [],
35    'servers': [],
36    'controllers': [('active_ctrl', {'display-name': 'test_ctrl'})],
37    'counter': 1000,
38
39    'login': """
40            <hash>
41                <user>
42                    <updated-at type="datetime">2012-04-30...</updated-at>
43                    <access-key>%s</access-key>
44                    <id type="integer">1</id>
45                    <created-at type="datetime">2012-02-21...</created-at>
46                    <email>jsmith@example.com</email>
47                    <username>jsmith</username>
48                </user>
49                <status type="integer">0</status>
50            </hash>""",
51
52    'good': """
53            <hash>
54              <status type="integer">0</status>
55            </hash>""",
56
57    'bad_login': """
58            <hash>
59              <status type="integer">5</status>
60              <status-msg>Some message...</status-msg>
61            </hash>""",
62
63    'bad_volume': """
64            <hash>
65              <status type="integer">10081</status>
66              <status-msg>Virtual volume xxx not found</status-msg>
67            </hash>""",
68
69    'bad_server': """
70            <hash>
71              <status type="integer">10086</status>
72              <status-msg>Server xxx not found</status-msg>
73            </hash>""",
74
75    'server_created': """
76            <create-server-response>
77                <server-name>%s</server-name>
78                <status type='integer'>0</status>
79            </create-server-response>""",
80}
81
82RUNTIME_VARS = None
83
84
85class FakeResponse(object):
86    def __init__(self, method, url, body):
87        self.method = method
88        self.url = url
89        self.body = body
90        self.status = RUNTIME_VARS['status']
91
92    def read(self):
93        ops = {'POST': [('/api/users/login.xml', self._login),
94                        ('/api/volumes.xml', self._create_volume),
95                        ('/api/servers.xml', self._create_server),
96                        ('/api/servers/*/volumes.xml', self._attach),
97                        ('/api/volumes/*/detach.xml', self._detach),
98                        ('/api/volumes/*/expand.xml', self._expand),
99                        ('/api/consistency_groups/*/snapshots.xml',
100                         self._create_snapshot),
101                        ('/api/consistency_groups/*/clone.xml',
102                         self._create_clone)],
103               'DELETE': [('/api/volumes/*', self._delete),
104                          ('/api/snapshots/*', self._delete_snapshot)],
105               'GET': [('/api/volumes.xml', self._list_volumes),
106                       ('/api/pools.xml', self._list_pools),
107                       ('/api/vcontrollers.xml', self._list_controllers),
108                       ('/api/servers.xml', self._list_servers),
109                       ('/api/consistency_groups/*/snapshots.xml',
110                        self._list_vol_snapshots),
111                       ('/api/volumes/*/servers.xml',
112                        self._list_vol_attachments)]
113               }
114
115        ops_list = ops[self.method]
116        modified_url = self.url.split('?')[0]
117        for (templ_url, func) in ops_list:
118            if self._compare_url(modified_url, templ_url):
119                result = func()
120                return result
121
122    def _compare_url(self, url, template_url):
123        items = url.split('/')
124        titems = template_url.split('/')
125        for (i, titem) in enumerate(titems):
126            if titem != '*' and titem != items[i]:
127                return False
128        return True
129
130    def _get_parameters(self, data):
131        items = data.split('&')
132        params = {}
133        for item in items:
134            if item:
135                (k, v) = item.split('=')
136                params[k] = v
137        return params
138
139    def _get_counter(self):
140        cnt = RUNTIME_VARS['counter']
141        RUNTIME_VARS['counter'] += 1
142        return cnt
143
144    def _login(self):
145        params = self._get_parameters(self.body)
146        if (params['user'] == RUNTIME_VARS['user'] and
147                params['password'] == RUNTIME_VARS['password']):
148            return RUNTIME_VARS['login'] % RUNTIME_VARS['access_key']
149        else:
150            return RUNTIME_VARS['bad_login']
151
152    def _incorrect_access_key(self, params):
153        return (params['access_key'] != RUNTIME_VARS['access_key'])
154
155    def _create_volume(self):
156        params = self._get_parameters(self.body)
157        if self._incorrect_access_key(params):
158            return RUNTIME_VARS['bad_login']
159
160        params['display-name'] = params['name']
161        params['cg-name'] = params['name']
162        params['snapshots'] = []
163        params['attachments'] = []
164        vpsa_vol = 'volume-%07d' % self._get_counter()
165        RUNTIME_VARS['volumes'].append((vpsa_vol, params))
166        return RUNTIME_VARS['good']
167
168    def _create_server(self):
169        params = self._get_parameters(self.body)
170        if self._incorrect_access_key(params):
171            return RUNTIME_VARS['bad_login']
172
173        params['display-name'] = params['display_name']
174        vpsa_srv = 'srv-%07d' % self._get_counter()
175        RUNTIME_VARS['servers'].append((vpsa_srv, params))
176        return RUNTIME_VARS['server_created'] % vpsa_srv
177
178    def _attach(self):
179        params = self._get_parameters(self.body)
180        if self._incorrect_access_key(params):
181            return RUNTIME_VARS['bad_login']
182
183        srv = self.url.split('/')[3]
184        vol = params['volume_name[]']
185
186        for (vol_name, params) in RUNTIME_VARS['volumes']:
187            if vol_name == vol:
188                attachments = params['attachments']
189                if srv in attachments:
190                    # already attached - ok
191                    return RUNTIME_VARS['good']
192                else:
193                    attachments.append(srv)
194                    return RUNTIME_VARS['good']
195
196        return RUNTIME_VARS['bad_volume']
197
198    def _detach(self):
199        params = self._get_parameters(self.body)
200        if self._incorrect_access_key(params):
201            return RUNTIME_VARS['bad_login']
202
203        vol = self.url.split('/')[3]
204        srv = params['server_name[]']
205
206        for (vol_name, params) in RUNTIME_VARS['volumes']:
207            if vol_name == vol:
208                attachments = params['attachments']
209                if srv not in attachments:
210                    return RUNTIME_VARS['bad_server']
211                else:
212                    attachments.remove(srv)
213                    return RUNTIME_VARS['good']
214
215        return RUNTIME_VARS['bad_volume']
216
217    def _expand(self):
218        params = self._get_parameters(self.body)
219        if self._incorrect_access_key(params):
220            return RUNTIME_VARS['bad_login']
221
222        vol = self.url.split('/')[3]
223        capacity = params['capacity']
224
225        for (vol_name, params) in RUNTIME_VARS['volumes']:
226            if vol_name == vol:
227                params['capacity'] = capacity
228                return RUNTIME_VARS['good']
229
230        return RUNTIME_VARS['bad_volume']
231
232    def _create_snapshot(self):
233        params = self._get_parameters(self.body)
234        if self._incorrect_access_key(params):
235            return RUNTIME_VARS['bad_login']
236
237        cg_name = self.url.split('/')[3]
238        snap_name = params['display_name']
239
240        for (vol_name, params) in RUNTIME_VARS['volumes']:
241            if params['cg-name'] == cg_name:
242                snapshots = params['snapshots']
243                if snap_name in snapshots:
244                    # already attached
245                    return RUNTIME_VARS['bad_volume']
246                else:
247                    snapshots.append(snap_name)
248                    return RUNTIME_VARS['good']
249
250        return RUNTIME_VARS['bad_volume']
251
252    def _delete_snapshot(self):
253        snap = self.url.split('/')[3].split('.')[0]
254
255        for (vol_name, params) in RUNTIME_VARS['volumes']:
256            if snap in params['snapshots']:
257                params['snapshots'].remove(snap)
258                return RUNTIME_VARS['good']
259
260        return RUNTIME_VARS['bad_volume']
261
262    def _create_clone(self):
263        params = self._get_parameters(self.body)
264        if self._incorrect_access_key(params):
265            return RUNTIME_VARS['bad_login']
266
267        params['display-name'] = params['name']
268        params['cg-name'] = params['name']
269        params['capacity'] = 1
270        params['snapshots'] = []
271        params['attachments'] = []
272        vpsa_vol = 'volume-%07d' % self._get_counter()
273        RUNTIME_VARS['volumes'].append((vpsa_vol, params))
274        return RUNTIME_VARS['good']
275
276    def _delete(self):
277        vol = self.url.split('/')[3].split('.')[0]
278
279        for (vol_name, params) in RUNTIME_VARS['volumes']:
280            if vol_name == vol:
281                if params['attachments']:
282                    # there are attachments - should be volume busy error
283                    return RUNTIME_VARS['bad_volume']
284                else:
285                    RUNTIME_VARS['volumes'].remove((vol_name, params))
286                    return RUNTIME_VARS['good']
287
288        return RUNTIME_VARS['bad_volume']
289
290    def _generate_list_resp(self, header, footer, body, lst, vol):
291        resp = header
292        for (obj, params) in lst:
293            if vol:
294                resp += body % (obj,
295                                params['display-name'],
296                                params['cg-name'],
297                                params['capacity'])
298            else:
299                resp += body % (obj, params['display-name'])
300        resp += footer
301        return resp
302
303    def _list_volumes(self):
304        header = """<show-volumes-response>
305                    <status type='integer'>0</status>
306                    <volumes type='array'>"""
307        footer = "</volumes></show-volumes-response>"
308        body = """<volume>
309                    <name>%s</name>
310                    <display-name>%s</display-name>
311                    <cg-name>%s</cg-name>
312                    <status>Available</status>
313                    <virtual-capacity type='integer'>%s</virtual-capacity>
314                    <allocated-capacity type='integer'>1</allocated-capacity>
315                    <raid-group-name>r5</raid-group-name>
316                    <cache>write-through</cache>
317                    <created-at type='datetime'>2012-01-28...</created-at>
318                    <modified-at type='datetime'>2012-01-28...</modified-at>
319                </volume>"""
320        return self._generate_list_resp(header,
321                                        footer,
322                                        body,
323                                        RUNTIME_VARS['volumes'],
324                                        True)
325
326    def _list_controllers(self):
327        header = """<show-vcontrollers-response>
328                    <status type='integer'>0</status>
329                    <vcontrollers type='array'>"""
330        footer = "</vcontrollers></show-vcontrollers-response>"
331        body = """<vcontroller>
332                    <name>%s</name>
333                    <display-name>%s</display-name>
334                    <state>active</state>
335                    <target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
336                    <iscsi-ip>1.1.1.1</iscsi-ip>
337                    <mgmt-ip>1.1.1.1</mgmt-ip>
338                    <software-ver>0.0.09-05.1--77.7</software-ver>
339                    <heartbeat1>ok</heartbeat1>
340                    <heartbeat2>ok</heartbeat2>
341                    <vpsa-chap-user>test_chap_user</vpsa-chap-user>
342                    <vpsa-chap-secret>test_chap_secret</vpsa-chap-secret>
343                </vcontroller>"""
344        return self._generate_list_resp(header,
345                                        footer,
346                                        body,
347                                        RUNTIME_VARS['controllers'],
348                                        False)
349
350    def _list_pools(self):
351        header = """<show-pools-response>
352                     <status type="integer">0</status>
353                     <pools type="array">
354                 """
355        footer = "</pools></show-pools-response>"
356        return header + footer
357
358    def _list_servers(self):
359        header = """<show-servers-response>
360                    <status type='integer'>0</status>
361                    <servers type='array'>"""
362        footer = "</servers></show-servers-response>"
363        body = """<server>
364                    <name>%s</name>
365                    <display-name>%s</display-name>
366                    <iqn>%s</iqn>
367                    <status>Active</status>
368                    <created-at type='datetime'>2012-01-28...</created-at>
369                    <modified-at type='datetime'>2012-01-28...</modified-at>
370                </server>"""
371
372        resp = header
373        for (obj, params) in RUNTIME_VARS['servers']:
374            resp += body % (obj, params['display-name'], params['iqn'])
375        resp += footer
376        return resp
377
378    def _get_server_obj(self, name):
379        for (srv_name, params) in RUNTIME_VARS['servers']:
380            if srv_name == name:
381                return params
382
383    def _list_vol_attachments(self):
384        vol = self.url.split('/')[3]
385
386        header = """<show-servers-response>
387                    <status type="integer">0</status>
388                    <servers type="array">"""
389        footer = "</servers></show-servers-response>"
390        body = """<server>
391                    <name>%s</name>
392                    <display-name>%s</display-name>
393                    <iqn>%s</iqn>
394                    <target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
395                    <lun>0</lun>
396                </server>"""
397
398        for (vol_name, params) in RUNTIME_VARS['volumes']:
399            if vol_name == vol:
400                attachments = params['attachments']
401                resp = header
402                for server in attachments:
403                    srv_params = self._get_server_obj(server)
404                    resp += body % (server,
405                                    srv_params['display-name'],
406                                    srv_params['iqn'])
407                resp += footer
408                return resp
409
410        return RUNTIME_VARS['bad_volume']
411
412    def _list_vol_snapshots(self):
413        cg_name = self.url.split('/')[3]
414
415        header = """<show-snapshots-on-cg-response>
416                    <status type="integer">0</status>
417                    <snapshots type="array">"""
418        footer = "</snapshots></show-snapshots-on-cg-response>"
419
420        body = """<snapshot>
421                    <name>%s</name>
422                    <display-name>%s</display-name>
423                    <status>normal</status>
424                    <cg-name>%s</cg-name>
425                    <pool-name>pool-00000001</pool-name>
426                </snapshot>"""
427
428        for (vol_name, params) in RUNTIME_VARS['volumes']:
429            if params['cg-name'] == cg_name:
430                snapshots = params['snapshots']
431                resp = header
432                for snap in snapshots:
433                    resp += body % (snap, snap, cg_name)
434                resp += footer
435                return resp
436
437        return RUNTIME_VARS['bad_volume']
438
439
440class FakeRequests(object):
441    """A fake requests for zadara volume driver tests."""
442    def __init__(self, method, api_url, data, verify):
443        url = parse.urlparse(api_url).path
444        res = FakeResponse(method, url, data)
445        self.content = res.read()
446        self.status_code = res.status
447
448
449class ZadaraVPSADriverTestCase(test.TestCase):
450    """Test case for Zadara VPSA volume driver."""
451    @mock.patch.object(requests, 'request', FakeRequests)
452    def setUp(self):
453        super(ZadaraVPSADriverTestCase, self).setUp()
454
455        global RUNTIME_VARS
456        RUNTIME_VARS = copy.deepcopy(DEFAULT_RUNTIME_VARS)
457        self.configuration = mock.Mock(conf.Configuration(None))
458        self.configuration.append_config_values(zadara.zadara_opts)
459        self.configuration.reserved_percentage = 10
460        self.configuration.zadara_use_iser = True
461        self.configuration.zadara_vpsa_host = '192.168.5.5'
462        self.configuration.zadara_vpsa_port = '80'
463        self.configuration.zadara_user = 'test'
464        self.configuration.zadara_password = 'test_password'
465        self.configuration.zadara_vpsa_poolname = 'pool-0001'
466        self.configuration.zadara_vol_encrypt = False
467        self.configuration.zadara_vol_name_template = 'OS_%s'
468        self.configuration.zadara_vpsa_use_ssl = False
469        self.configuration.zadara_ssl_cert_verify = False
470        self.configuration.zadara_default_snap_policy = False
471        self.driver = (zadara.ZadaraVPSAISCSIDriver(
472                       configuration=self.configuration))
473        self.driver.do_setup(None)
474
475    @mock.patch.object(requests, 'request', FakeRequests)
476    def test_create_destroy(self):
477        """Create/Delete volume."""
478        volume = {'name': 'test_volume_01', 'size': 1}
479        self.driver.create_volume(volume)
480        self.driver.delete_volume(volume)
481
482    @mock.patch.object(requests, 'request', FakeRequests)
483    def test_create_destroy_multiple(self):
484        """Create/Delete multiple volumes."""
485        self.driver.create_volume({'name': 'test_volume_01', 'size': 1})
486        self.driver.create_volume({'name': 'test_volume_02', 'size': 2})
487        self.driver.create_volume({'name': 'test_volume_03', 'size': 3})
488        self.driver.delete_volume({'name': 'test_volume_02'})
489        self.driver.delete_volume({'name': 'test_volume_03'})
490        self.driver.delete_volume({'name': 'test_volume_01'})
491        self.driver.delete_volume({'name': 'test_volume_04'})
492
493    @mock.patch.object(requests, 'request', FakeRequests)
494    def test_destroy_non_existent(self):
495        """Delete non-existent volume."""
496        volume = {'name': 'test_volume_02', 'size': 1}
497        self.driver.delete_volume(volume)
498
499    @mock.patch.object(requests, 'request', FakeRequests)
500    def test_empty_apis(self):
501        """Test empty func (for coverage only)."""
502        context = None
503        volume = {'name': 'test_volume_01', 'size': 1}
504        self.driver.create_export(context, volume)
505        self.driver.ensure_export(context, volume)
506        self.driver.remove_export(context, volume)
507        self.assertRaises(NotImplementedError,
508                          self.driver.local_path,
509                          None)
510        self.driver.check_for_setup_error()
511
512    @mock.patch.object(requests, 'request', FakeRequests)
513    def test_volume_attach_detach(self):
514        """Test volume attachment and detach."""
515        volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
516        connector = dict(initiator='test_iqn.1')
517        self.driver.create_volume(volume)
518        props = self.driver.initialize_connection(volume, connector)
519        self.assertEqual('iser', props['driver_volume_type'])
520        data = props['data']
521        self.assertEqual('1.1.1.1:3260', data['target_portal'])
522        self.assertEqual('iqn.2011-04.com.zadarastorage:vsa-xxx:1',
523                         data['target_iqn'])
524        self.assertEqual(int('0'), data['target_lun'])
525        self.assertEqual(123, data['volume_id'])
526        self.assertEqual('CHAP', data['auth_method'])
527        self.assertEqual('test_chap_user', data['auth_username'])
528        self.assertEqual('test_chap_secret', data['auth_password'])
529        self.driver.terminate_connection(volume, connector)
530        self.driver.delete_volume(volume)
531
532    @mock.patch.object(requests, 'request', FakeRequests)
533    def test_volume_attach_multiple_detach(self):
534        """Test multiple volume attachment and detach."""
535        volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
536        connector1 = dict(initiator='test_iqn.1')
537        connector2 = dict(initiator='test_iqn.2')
538        connector3 = dict(initiator='test_iqn.3')
539
540        self.driver.create_volume(volume)
541        self.driver.initialize_connection(volume, connector1)
542        self.driver.initialize_connection(volume, connector2)
543        self.driver.initialize_connection(volume, connector3)
544
545        self.driver.terminate_connection(volume, connector1)
546        self.driver.terminate_connection(volume, connector3)
547        self.driver.terminate_connection(volume, connector2)
548        self.driver.delete_volume(volume)
549
550    @mock.patch.object(requests, 'request', FakeRequests)
551    def test_wrong_attach_params(self):
552        """Test different wrong attach scenarios."""
553        volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
554        connector1 = dict(initiator='test_iqn.1')
555        self.assertRaises(exception.VolumeNotFound,
556                          self.driver.initialize_connection,
557                          volume1, connector1)
558
559    @mock.patch.object(requests, 'request', FakeRequests)
560    def test_wrong_detach_params(self):
561        """Test different wrong detachment scenarios."""
562        volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
563        volume2 = {'name': 'test_volume_02', 'size': 1, 'id': 102}
564        volume3 = {'name': 'test_volume_03', 'size': 1, 'id': 103}
565        connector1 = dict(initiator='test_iqn.1')
566        connector2 = dict(initiator='test_iqn.2')
567        connector3 = dict(initiator='test_iqn.3')
568        self.driver.create_volume(volume1)
569        self.driver.create_volume(volume2)
570        self.driver.initialize_connection(volume1, connector1)
571        self.driver.initialize_connection(volume2, connector2)
572        self.assertRaises(exception.ZadaraServerNotFound,
573                          self.driver.terminate_connection,
574                          volume1, connector3)
575        self.assertRaises(exception.VolumeNotFound,
576                          self.driver.terminate_connection,
577                          volume3, connector1)
578        self.assertRaises(exception.FailedCmdWithDump,
579                          self.driver.terminate_connection,
580                          volume1, connector2)
581
582    @mock.patch.object(requests, 'request', FakeRequests)
583    def test_wrong_login_reply(self):
584        """Test wrong login reply."""
585
586        RUNTIME_VARS['login'] = """<hash>
587                    <access-key>%s</access-key>
588                    <status type="integer">0</status>
589                </hash>"""
590        self.assertRaises(exception.MalformedResponse,
591                          self.driver.do_setup, None)
592
593        RUNTIME_VARS['login'] = """
594            <hash>
595                <user>
596                    <updated-at type="datetime">2012-04-30...</updated-at>
597                    <id type="integer">1</id>
598                    <created-at type="datetime">2012-02-21...</created-at>
599                    <email>jsmith@example.com</email>
600                    <username>jsmith</username>
601                </user>
602                <access-key>%s</access-key>
603                <status type="integer">0</status>
604            </hash>"""
605        self.assertRaises(exception.MalformedResponse,
606                          self.driver.do_setup, None)
607
608    @mock.patch.object(requests, 'request')
609    def test_ssl_use(self, request):
610        """Coverage test for SSL connection."""
611        self.configuration.zadara_ssl_cert_verify = True
612        self.configuration.zadara_vpsa_use_ssl = True
613        self.configuration.driver_ssl_cert_path = '/path/to/cert'
614
615        good_response = mock.MagicMock()
616        good_response.status_code = RUNTIME_VARS['status']
617        good_response.content = RUNTIME_VARS['login']
618
619        def request_verify_cert(*args, **kwargs):
620            self.assertEqual(kwargs['verify'], '/path/to/cert')
621            return good_response
622
623        request.side_effect = request_verify_cert
624        self.driver.do_setup(None)
625
626    @mock.patch.object(requests, 'request', FakeRequests)
627    def test_bad_http_response(self):
628        """Coverage test for non-good HTTP response."""
629        RUNTIME_VARS['status'] = 400
630
631        volume = {'name': 'test_volume_01', 'size': 1}
632        self.assertRaises(exception.BadHTTPResponseStatus,
633                          self.driver.create_volume, volume)
634
635    @mock.patch.object(requests, 'request', FakeRequests)
636    def test_delete_without_detach(self):
637        """Test volume deletion without detach."""
638
639        volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
640        connector1 = dict(initiator='test_iqn.1')
641        connector2 = dict(initiator='test_iqn.2')
642        connector3 = dict(initiator='test_iqn.3')
643        self.driver.create_volume(volume1)
644        self.driver.initialize_connection(volume1, connector1)
645        self.driver.initialize_connection(volume1, connector2)
646        self.driver.initialize_connection(volume1, connector3)
647        self.driver.delete_volume(volume1)
648
649    @mock.patch.object(requests, 'request', FakeRequests)
650    def test_no_active_ctrl(self):
651
652        RUNTIME_VARS['controllers'] = []
653        volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
654        connector = dict(initiator='test_iqn.1')
655        self.driver.create_volume(volume)
656        self.assertRaises(exception.ZadaraVPSANoActiveController,
657                          self.driver.initialize_connection,
658                          volume, connector)
659
660    @mock.patch.object(requests, 'request', FakeRequests)
661    def test_create_destroy_snapshot(self):
662        """Create/Delete snapshot test."""
663        volume = {'name': 'test_volume_01', 'size': 1}
664        snapshot = {'name': 'snap_01',
665                    'volume_name': volume['name']}
666
667        self.driver.create_volume(volume)
668        self.assertRaises(exception.VolumeDriverException,
669                          self.driver.create_snapshot,
670                          {'name': snapshot['name'],
671                           'volume_name': 'wrong_vol'})
672
673        self.driver.create_snapshot(snapshot)
674
675        # Deleted should succeed for missing volume
676        self.driver.delete_snapshot({'name': snapshot['name'],
677                                     'volume_name': 'wrong_vol'})
678        # Deleted should succeed for missing snap
679        self.driver.delete_snapshot({'name': 'wrong_snap',
680                                     'volume_name': volume['name']})
681
682        self.driver.delete_snapshot(snapshot)
683        self.driver.delete_volume(volume)
684
685    @mock.patch.object(requests, 'request', FakeRequests)
686    def test_expand_volume(self):
687        """Expand volume test."""
688        volume = {'name': 'test_volume_01', 'size': 10}
689        volume2 = {'name': 'test_volume_02', 'size': 10}
690
691        self.driver.create_volume(volume)
692
693        self.assertRaises(exception.ZadaraVolumeNotFound,
694                          self.driver.extend_volume,
695                          volume2, 15)
696        self.assertRaises(exception.InvalidInput,
697                          self.driver.extend_volume,
698                          volume, 5)
699
700        self.driver.extend_volume(volume, 15)
701        self.driver.delete_volume(volume)
702
703    @mock.patch.object(requests, 'request', FakeRequests)
704    def test_create_destroy_clones(self):
705        """Create/Delete clones test."""
706        volume1 = {'name': 'test_volume_01', 'id': '01', 'size': 1}
707        volume2 = {'name': 'test_volume_02', 'id': '02', 'size': 2}
708        volume3 = {'name': 'test_volume_03', 'id': '03', 'size': 1}
709        snapshot = {'name': 'snap_01',
710                    'id': '01',
711                    'volume_name': volume1['name'],
712                    'volume_size': 1}
713
714        self.driver.create_volume(volume1)
715        self.driver.create_snapshot(snapshot)
716
717        # Test invalid vol reference
718        self.assertRaises(exception.VolumeNotFound,
719                          self.driver.create_volume_from_snapshot,
720                          volume2,
721                          {'name': snapshot['name'],
722                           'id': snapshot['id'],
723                           'volume_name': 'wrong_vol'})
724        # Test invalid snap reference
725        self.assertRaises(exception.SnapshotNotFound,
726                          self.driver.create_volume_from_snapshot,
727                          volume2,
728                          {'name': 'wrong_snap',
729                           'id': 'wrong_id',
730                           'volume_name': snapshot['volume_name']})
731        # Test invalid src_vref for volume clone
732        self.assertRaises(exception.VolumeNotFound,
733                          self.driver.create_cloned_volume,
734                          volume3, volume2)
735        self.driver.create_volume_from_snapshot(volume2, snapshot)
736        self.driver.create_cloned_volume(volume3, volume1)
737        self.driver.delete_volume(volume3)
738        self.driver.delete_volume(volume2)
739        self.driver.delete_snapshot(snapshot)
740        self.driver.delete_volume(volume1)
741
742    @mock.patch.object(requests, 'request', FakeRequests)
743    def test_get_volume_stats(self):
744        """Get stats test."""
745        self.configuration.safe_get.return_value = 'ZadaraVPSAISCSIDriver'
746        data = self.driver.get_volume_stats(True)
747        self.assertEqual('Zadara Storage', data['vendor_name'])
748        self.assertEqual('unknown', data['total_capacity_gb'])
749        self.assertEqual('unknown', data['free_capacity_gb'])
750        self.assertEqual({'total_capacity_gb': 'unknown',
751                          'free_capacity_gb': 'unknown',
752                          'reserved_percentage':
753                          self.configuration.reserved_percentage,
754                          'QoS_support': False,
755                          'vendor_name': 'Zadara Storage',
756                          'driver_version': self.driver.VERSION,
757                          'storage_protocol': 'iSER',
758                          'volume_backend_name': 'ZadaraVPSAISCSIDriver'},
759                         data)
760