1# Copyright (c) 2016 Synology Co., Ltd.
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
16"""Tests for the Synology iSCSI volume driver."""
17
18import copy
19import json
20import math
21
22from cryptography.hazmat.backends import default_backend
23from cryptography.hazmat.primitives.asymmetric import padding
24from cryptography.hazmat.primitives.asymmetric import rsa
25import mock
26from oslo_utils import units
27import requests
28from six.moves import http_client
29from six import string_types
30
31from cinder import context
32from cinder import exception
33from cinder import test
34from cinder.tests.unit import fake_constants as fake
35from cinder.tests.unit import fake_snapshot
36from cinder.tests.unit import fake_volume
37from cinder.volume import configuration as conf
38from cinder.volume.drivers.synology import synology_common as common
39
40VOLUME_ID = fake.VOLUME_ID
41TARGET_NAME_PREFIX = 'Cinder-Target-'
42IP = '10.0.0.1'
43IQN = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + VOLUME_ID
44TRG_ID = 1
45CHAP_AUTH_USERNAME = 'username'
46CHAP_AUTH_PASSWORD = 'password'
47VOLUME = {
48    '_name_id': '',
49    'name': fake.VOLUME_NAME,
50    'id': VOLUME_ID,
51    'display_name': 'fake_volume',
52    'size': 10,
53    'provider_location': '%s:3260,%d %s 1' % (IP, TRG_ID, IQN),
54    'provider_auth': 'CHAP %(user)s %(pass)s' % {
55        'user': CHAP_AUTH_USERNAME,
56        'pass': CHAP_AUTH_PASSWORD},
57}
58NEW_VOLUME_ID = fake.VOLUME2_ID
59IQN2 = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + NEW_VOLUME_ID
60NEW_TRG_ID = 2
61NEW_VOLUME = {
62    'name': fake.VOLUME2_NAME,
63    'id': NEW_VOLUME_ID,
64    'display_name': 'new_fake_volume',
65    'size': 10,
66    'provider_location': '%s:3260,%d %s 1' % (IP, NEW_TRG_ID, IQN2),
67}
68SNAPSHOT_ID = fake.SNAPSHOT_ID
69DS_SNAPSHOT_UUID = 'ca86a56a-40d8-4210-974c-ef15dbf01cba'
70SNAPSHOT_METADATA = {
71    'snap-meta1': 'value1',
72    'snap-meta2': 'value2',
73    'snap-meta3': 'value3',
74}
75SNAPSHOT = {
76    'name': fake.SNAPSHOT_NAME,
77    'id': SNAPSHOT_ID,
78    'volume_id': VOLUME_ID,
79    'volume_name': VOLUME['name'],
80    'volume_size': 10,
81    'display_name': 'fake_snapshot',
82    'volume': VOLUME,
83    'metadata': SNAPSHOT_METADATA,
84}
85SNAPSHOT_INFO = {
86    'is_action_locked': False,
87    'snapshot_id': 1,
88    'status': 'Healthy',
89    'uuid': DS_SNAPSHOT_UUID,
90}
91INITIATOR_IQN = 'iqn.1993-08.org.debian:01:604af6a341'
92CONNECTOR = {
93    'initiator': INITIATOR_IQN,
94}
95CONTEXT = {
96}
97LOCAL_PATH = '/dev/isda'
98IMAGE_SERVICE = 'image_service'
99IMAGE_ID = 1
100IMAGE_META = {
101    'id': IMAGE_ID
102}
103POOL_NAME = 'volume1'
104NODE_UUID = '72003c93-2db2-4f00-a169-67c5eae86bb1'
105NODE_UUID2 = '8e1e8b82-1ef9-4157-a4bf-e069355386c2'
106HOST = {
107    'capabilities': {
108        'pool_name': 'volume2',
109        'backend_info': 'Synology:iscsi:' + NODE_UUID,
110    },
111}
112POOL_INFO = {
113    'display_name': 'Volume 1',
114    'raid_type': 'raid_1',
115    'readonly': False,
116    'fs_type': 'ext4',
117    'location': 'internal',
118    'eppool_used_byte': '139177984',
119    'size_total_byte': '487262806016',
120    'volume_id': 1,
121    'size_free_byte': '486521139200',
122    'container': 'internal',
123    'volume_path': '/volume1',
124    'single_volume': True
125}
126LUN_UUID = 'e1315f33-ba35-42c3-a3e7-5a06958eca30'
127LUN_INFO = {
128    'status': '',
129    'is_action_locked': False,
130    'name': VOLUME['name'],
131    'extent_size': 0,
132    'allocated_size': 0,
133    'uuid': LUN_UUID,
134    'is_mapped': True,
135    'lun_id': 3,
136    'location': '/volume2',
137    'restored_time': 0,
138    'type': 143,
139    'size': 1073741824
140}
141FAKE_API = 'SYNO.Fake.API'
142FAKE_METHOD = 'fake'
143FAKE_PATH = 'fake.cgi'
144
145
146class MockResponse(object):
147        def __init__(self, json_data, status_code):
148            self.json_data = json_data
149            self.status_code = status_code
150
151        def json(self):
152            return self.json_data
153
154
155class SynoSessionTestCase(test.TestCase):
156    @mock.patch('requests.post', return_value=MockResponse(
157        {'data': {'sid': 'sid'}, 'success': True}, http_client.OK))
158    def setUp(self, _mock_post):
159        super(SynoSessionTestCase, self).setUp()
160
161        self.host = '127.0.0.1'
162        self.port = 5001
163        self.username = 'admin'
164        self.password = 'admin'
165        self.https = True
166        self.ssl_verify = False
167        self.one_time_pass = None
168        self.device_id = None
169        self.session = common.Session(self.host,
170                                      self.port,
171                                      self.username,
172                                      self.password,
173                                      self.https,
174                                      self.ssl_verify,
175                                      self.one_time_pass,
176                                      self.device_id)
177        self.session.__class__.__del__ = lambda x: x
178
179    def test_query(self):
180        out = {
181            'maxVersion': 3,
182            'minVersion': 1,
183            'path': FAKE_PATH,
184            'requestFormat': 'JSON'
185        }
186        data = {
187            'api': 'SYNO.API.Info',
188            'version': 1,
189            'method': 'query',
190            'query': FAKE_API
191        }
192        requests.post = mock.Mock(side_effect=[
193            MockResponse({
194                'data': {
195                    FAKE_API: out
196                },
197                'success': True
198            }, http_client.OK),
199            MockResponse({
200                'data': {
201                    FAKE_API: out
202                }
203            }, http_client.OK),
204        ])
205
206        result = self.session.query(FAKE_API)
207        requests.post.assert_called_once_with(
208            'https://127.0.0.1:5001/webapi/query.cgi',
209            data=data,
210            verify=self.ssl_verify)
211        self.assertDictEqual(out, result)
212
213        result = self.session.query(FAKE_API)
214        self.assertIsNone(result)
215
216    def test__random_AES_passphrase(self):
217        lengths_to_test = [0, 1, 10, 128, 501, 1024, 4096]
218        for test_length in lengths_to_test:
219            self.assertEqual(
220                test_length,
221                len(self.session._random_AES_passphrase(test_length))
222            )
223
224    def test__encrypt_RSA(self):
225        # Initialize a fixed 1024 bit public/private key pair
226        public_numbers = rsa.RSAPublicNumbers(
227            int('10001', 16),
228            int('c42eadf905d47388d84baeec2d5391ba7f91b35912933032c9c8a32d6358'
229                '9cef1dfe532138adfad41fd41910cd12fbc05b8876f70aa1340fccf3227d'
230                '087d1e47256c60ae49abee7c779815ec085265518791da38168a0597091d'
231                '4c6ff10c0fa6616f250b85edfb4066f655695e304c0dc40c26fc11541e4c'
232                '1be47771fcc1d257cccbb656015c5daed64aad7c8ae024f82531b7e637f4'
233                '87530b77498d1bc7247687541fbbaa01112866da06f30185dde15131e89e'
234                '27b30f07f10ddef23dd4da7bf3e216c733a4004415c9d1dd9bd5032e8b55'
235                '4eb56efa9cd5cd1b416e0e55c903536787454ca3d3aba87edb70768f630c'
236                'beab3781848ff5ee40edfaee57ac87c9', 16)
237        )
238        private_numbers = rsa.RSAPrivateNumbers(
239            int('f0aa7e45ffb23ca683e1b01a9e1d77e5affaf9afa0094fb1eb89a3c8672b'
240                '43ab9beb11e4ecdd2c8f88738db56be4149c55c28379480ac68a5727ba28'
241                '4a47565579dbf083167a2845f5f267598febde3f7b12ba10da32ad2edff8'
242                '4efd019498e0d8e03f6ddb8a5e80cdb862da9c0c921571fdb56ae7e0480a'
243                'de846e328517aa23', 16),
244            int('d0ae9ce41716c4bdac074423d57e540b6f48ee42d9b06bdac3b3421ea2ae'
245                'e21088b3ae50acfe168edefda722dc15bc456bba76a98b8035ffa4da12dc'
246                'a92bad582c935791f9a48b416f53c728fd1866c8ecf2ca00dfa667a962d3'
247                'c9818cce540c5e9d2ef8843c5adfde0938ac8b5e2c592838c422ffac43ff'
248                '4a4907c129de7723', 16),
249            int('3733cf5e58069cefefb4f4269ee67a0619695d26fe340e86ec0299efe699'
250                '83a741305421eff9fcaf7db947c8537c38fcba84debccaefeb5f5ad33b6c'
251                '255c578dbb7910875a5197cccc362e4cf9567e0dfff0c98fa8bff3acb932'
252                'd6545566886ccfd3df7fab92f874f9c3eceab6472ecf5ccff2945127f352'
253                '8532b76d8aaadb4dbcf0e5bae8c9c8597511e0771942f12e29bbee1ceef5'
254                '4a6ba97e0096354b13ae4ca22e9be1a551a1bc8db9392de6bbad99b956b5'
255                'bb4b7f5094086e6eefd432066102a228bc18012cc31a7777e2e657eb115a'
256                '9d718d413f2bd7a448a783c049afaaf127486b2c17feebb930e7ac8e6a07'
257                'd9c843beedfa8cec52e1aba98099baa5', 16),
258            int('c8ab1050e36c457ffe550f56926235d7b18d8de5af86340a413fe9edae80'
259                '77933e9599bd0cf73a318feff1c7c4e74f7c2f51d9f82566beb71906ca04'
260                'd0327d3d16379a6a633286241778004ec05f46581e11b64d58f28a4e9c77'
261                '59bd423519e7d94dd9f58ae9ebf47013ff71124eb4fbe6a94a3c928d02e4'
262                'f536ecff78d40b8b', 16),
263            int('5bb873a2d8f71bf015dd77b89c4c931a1786a19a665de179dccc3c4284d4'
264                '82ee2b7776256573a46c955c3d8ad7db01ce2d645e6574b81c83c96c4420'
265                '1286ed00b54ee98d72813ce7bccbc0dca629847bc99188f1cb5b3372c2ca'
266                '3d6620824b74c85d23d8fd1e1dff09735a22947b06d90511b63b7fceb270'
267                '51b139a45007c4ab', 16),
268            int('cfeff2a88112512b327999eb926a0564c431ebed2e1456f51d274e4e6d7d'
269                'd75d5b26339bbca2807aa71008e9a08bd9fa0e53e3960e3b6e8c6e1a46d2'
270                'b8e89b218d3b453f7ed0020504d1679374cd884ae3bb3b88b54fb429f082'
271                'fa4e9d3f296c59d5d89fe16b0931dcf062bc309cf122c722c13ffb0fa0c5'
272                '77d0abddcc655017', 16),
273            public_numbers
274        )
275        private_key = private_numbers.private_key(default_backend())
276
277        # run the _encrypt_RSA method
278        original_text = 'test _encrypt_RSA'
279        encrypted_text = self.session._encrypt_RSA(
280            public_numbers.n,
281            public_numbers.e,
282            original_text
283        )
284
285        # decrypt the output using the corresponding private key
286        decrypted_bytes = private_key.decrypt(
287            encrypted_text,
288            padding.PKCS1v15()
289        )
290        decrypted_text = decrypted_bytes.decode('ascii')
291        self.assertEqual(original_text, decrypted_text)
292
293    def test__encrypt_params(self):
294        # setup mock
295        cipherkey = 'cipherkey'
296        self.session._get_enc_info = mock.Mock(return_value={
297            'public_key': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
298            'cipherkey': cipherkey,
299            'ciphertoken': 'ciphertoken',
300            'server_time': 1111111111,
301        })
302        self.session._encrypt_RSA = mock.Mock(
303            return_value=b'1234567890abcdef'
304        )
305        self.session._encrypt_AES = mock.Mock(
306            return_value=b'fedcba0987654321'
307        )
308
309        # call the method
310        params = {
311            'account': 'account',
312            'passwd': 'passwd',
313            'session': 'sessionid',
314            'format': 'sid'
315        }
316        encrypted_data = self.session._encrypt_params(params)
317
318        # check the format of the output
319        self.assertDictEqual(
320            json.loads(encrypted_data[cipherkey]),
321            {'rsa': 'MTIzNDU2Nzg5MGFiY2RlZg==',
322             'aes': 'ZmVkY2JhMDk4NzY1NDMyMQ=='}
323        )
324
325
326class SynoAPIRequestTestCase(test.TestCase):
327    @mock.patch('requests.post')
328    def setUp(self, _mock_post):
329        super(SynoAPIRequestTestCase, self).setUp()
330
331        self.host = '127.0.0.1'
332        self.port = 5001
333        self.username = 'admin'
334        self.password = 'admin'
335        self.https = True
336        self.ssl_verify = False
337        self.one_time_pass = None
338        self.device_id = None
339        self.request = common.APIRequest(self.host,
340                                         self.port,
341                                         self.username,
342                                         self.password,
343                                         self.https,
344                                         self.ssl_verify,
345                                         self.one_time_pass,
346                                         self.device_id)
347        self.request._APIRequest__session._sid = 'sid'
348        self.request._APIRequest__session.__class__.__del__ = lambda x: x
349
350    @mock.patch.object(common, 'Session')
351    def test_new_session(self, _mock_session):
352        self.device_id = 'did'
353        self.request = common.APIRequest(self.host,
354                                         self.port,
355                                         self.username,
356                                         self.password,
357                                         self.https,
358                                         self.ssl_verify,
359                                         self.one_time_pass,
360                                         self.device_id)
361
362        result = self.request.new_session()
363        self.assertIsNone(result)
364
365    def test__start(self):
366        out = {
367            'maxVersion': 3,
368            'minVersion': 1,
369            'path': FAKE_PATH,
370            'requestFormat': 'JSON'
371        }
372        self.request._APIRequest__session.query = mock.Mock(return_value=out)
373
374        result = self.request._start(FAKE_API, 3)
375        (self.request._APIRequest__session.query.
376            assert_called_once_with(FAKE_API))
377        self.assertEqual(FAKE_PATH, result)
378
379        out.update(maxVersion=2)
380        self.assertRaises(exception.APIException,
381                          self.request._start,
382                          FAKE_API,
383                          3)
384
385    def test__encode_param(self):
386        param = {
387            'api': FAKE_API,
388            'method': FAKE_METHOD,
389            'version': 1,
390            '_sid': 'sid'
391        }
392        self.request._jsonFormat = True
393        result = self.request._encode_param(param)
394        self.assertIsInstance(result, string_types)
395
396    def test_request(self):
397        version = 1
398
399        self.request._start = mock.Mock(return_value='fake.cgi')
400        self.request._encode_param = mock.Mock(side_effect=lambda x: x)
401        self.request.new_session = mock.Mock()
402        requests.post = mock.Mock(side_effect=[
403            MockResponse({'success': True}, http_client.OK),
404            MockResponse({'error': {'code': http_client.SWITCHING_PROTOCOLS},
405                          'success': False}, http_client.OK),
406            MockResponse({'error': {'code': http_client.SWITCHING_PROTOCOLS}},
407                         http_client.OK),
408            MockResponse({}, http_client.INTERNAL_SERVER_ERROR)
409        ])
410
411        result = self.request.request(FAKE_API, FAKE_METHOD, version)
412        self.assertDictEqual({'success': True}, result)
413
414        result = self.request.request(FAKE_API, FAKE_METHOD, version)
415        self.assertDictEqual(
416            {'error': {'code': http_client.SWITCHING_PROTOCOLS},
417             'success': False}, result)
418
419        self.assertRaises(exception.MalformedResponse,
420                          self.request.request,
421                          FAKE_API,
422                          FAKE_METHOD,
423                          version)
424
425        result = self.request.request(FAKE_API, FAKE_METHOD, version)
426        self.assertDictEqual(
427            {'http_status': http_client.INTERNAL_SERVER_ERROR}, result)
428
429    @mock.patch.object(common.LOG, 'debug')
430    def test_request_auth_error(self, _log):
431        version = 1
432
433        self.request._start = mock.Mock(return_value='fake.cgi')
434        self.request._encode_param = mock.Mock(side_effect=lambda x: x)
435        self.request.new_session = mock.Mock()
436        requests.post = mock.Mock(return_value=
437                                  MockResponse({
438                                      'error': {'code': 105},
439                                      'success': False
440                                  }, http_client.OK))
441
442        self.assertRaises(exception.SynoAuthError,
443                          self.request.request,
444                          FAKE_API,
445                          FAKE_METHOD,
446                          version)
447
448
449class SynoCommonTestCase(test.TestCase):
450
451    @mock.patch.object(common.SynoCommon,
452                       '_get_node_uuid',
453                       return_value=NODE_UUID)
454    @mock.patch.object(common, 'APIRequest')
455    def setUp(self, _request, _get_node_uuid):
456        super(SynoCommonTestCase, self).setUp()
457
458        self.conf = self.setup_configuration()
459        self.common = common.SynoCommon(self.conf, 'iscsi')
460        self.common.vendor_name = 'Synology'
461        self.common.driver_type = 'iscsi'
462        self.common.volume_backend_name = 'DiskStation'
463        self.common.target_port = 3260
464
465    def setup_configuration(self):
466        config = mock.Mock(spec=conf.Configuration)
467        config.use_chap_auth = False
468        config.target_protocol = 'iscsi'
469        config.target_ip_address = IP
470        config.target_port = 3260
471        config.synology_admin_port = 5000
472        config.synology_username = 'admin'
473        config.synology_password = 'admin'
474        config.synology_ssl_verify = True
475        config.synology_one_time_pass = '123456'
476        config.synology_pool_name = POOL_NAME
477        config.volume_dd_blocksize = 1
478        config.target_prefix = 'iqn.2000-01.com.synology:'
479        config.chap_username = 'abcd'
480        config.chap_password = 'qwerty'
481        config.reserved_percentage = 0
482        config.max_over_subscription_ratio = 20
483
484        return config
485
486    @mock.patch.object(common.SynoCommon,
487                       '_get_node_uuid',
488                       return_value=NODE_UUID)
489    @mock.patch.object(common, 'APIRequest')
490    def test___init__(self, _request, _get_node_uuid):
491        self.conf.safe_get = (mock.Mock(side_effect=[
492            self.conf.target_ip_address,
493            '',
494            '']))
495
496        self.assertRaises(exception.InvalidConfigurationValue,
497                          self.common.__init__,
498                          self.conf,
499                          'iscsi')
500
501        self.assertRaises(exception.InvalidConfigurationValue,
502                          self.common.__init__,
503                          self.conf,
504                          'iscsi')
505
506    def test__get_node_uuid(self):
507        out = {
508            'data': {
509                'nodes': [{
510                    'uuid': NODE_UUID
511                }]
512            },
513            'success': True
514        }
515        self.common.exec_webapi = (
516            mock.Mock(side_effect=[
517                      out,
518                      out,
519                      exception.SynoAuthError(message='dont care')]))
520
521        result = self.common._get_node_uuid()
522        (self.common.exec_webapi.
523            assert_called_with('SYNO.Core.ISCSI.Node',
524                               'list',
525                               mock.ANY))
526        self.assertEqual(NODE_UUID, result)
527
528        del out['data']['nodes']
529        self.assertRaises(exception.VolumeDriverException,
530                          self.common._get_node_uuid)
531
532        self.assertRaises(exception.SynoAuthError,
533                          self.common._get_node_uuid)
534
535    def test__get_pool_info(self):
536        out = {
537            'data': {
538                'volume': POOL_INFO
539            },
540            'success': True
541        }
542        self.common.exec_webapi = (
543            mock.Mock(side_effect=[
544                      out,
545                      out,
546                      exception.SynoAuthError(message='dont care')]))
547        result = self.common._get_pool_info()
548        (self.common.exec_webapi.
549            assert_called_with('SYNO.Core.Storage.Volume',
550                               'get',
551                               mock.ANY,
552                               volume_path='/' + POOL_NAME))
553        self.assertDictEqual(POOL_INFO, result)
554
555        del out['data']['volume']
556        self.assertRaises(exception.MalformedResponse,
557                          self.common._get_pool_info)
558
559        self.assertRaises(exception.SynoAuthError,
560                          self.common._get_pool_info)
561
562        self.conf.synology_pool_name = ''
563        self.assertRaises(exception.InvalidConfigurationValue,
564                          self.common._get_pool_info)
565
566    def test__get_pool_size(self):
567        pool_info = copy.deepcopy(POOL_INFO)
568        self.common._get_pool_info = mock.Mock(return_value=pool_info)
569
570        result = self.common._get_pool_size()
571
572        self.assertEqual((int(int(POOL_INFO['size_free_byte']) / units.Gi),
573                          int(int(POOL_INFO['size_total_byte']) / units.Gi),
574                          math.ceil((float(POOL_INFO['size_total_byte']) -
575                                     float(POOL_INFO['size_free_byte']) -
576                                     float(POOL_INFO['eppool_used_byte'])) /
577                                    units.Gi)),
578                         result)
579
580        del pool_info['size_free_byte']
581        self.assertRaises(exception.MalformedResponse,
582                          self.common._get_pool_size)
583
584    def test__get_pool_lun_provisioned_size(self):
585        out = {
586            'data': {
587                'luns': [{
588                    'lun_id': 1,
589                    'location': '/' + POOL_NAME,
590                    'size': 5368709120
591                }, {
592                    'lun_id': 2,
593                    'location': '/' + POOL_NAME,
594                    'size': 3221225472
595                }]
596            },
597            'success': True
598        }
599        self.common.exec_webapi = mock.Mock(return_value=out)
600
601        result = self.common._get_pool_lun_provisioned_size()
602        (self.common.exec_webapi.
603            assert_called_with('SYNO.Core.ISCSI.LUN',
604                               'list',
605                               mock.ANY,
606                               location='/' + POOL_NAME))
607        self.assertEqual(int(math.ceil(float(5368709120 + 3221225472) /
608                             units.Gi)),
609                         result)
610
611    def test__get_pool_lun_provisioned_size_error(self):
612        out = {
613            'data': {},
614            'success': True
615        }
616        self.common.exec_webapi = mock.Mock(return_value=out)
617
618        self.assertRaises(exception.MalformedResponse,
619                          self.common._get_pool_lun_provisioned_size)
620
621        self.conf.synology_pool_name = ''
622        self.assertRaises(exception.InvalidConfigurationValue,
623                          self.common._get_pool_lun_provisioned_size)
624
625    def test__get_lun_info(self):
626        out = {
627            'data': {
628                'lun': LUN_INFO
629            },
630            'success': True
631        }
632        self.common.exec_webapi = (
633            mock.Mock(side_effect=[
634                      out,
635                      out,
636                      exception.SynoAuthError(message='dont care')]))
637        result = self.common._get_lun_info(VOLUME['name'],
638                                           ['is_mapped'])
639        (self.common.exec_webapi.
640            assert_called_with('SYNO.Core.ISCSI.LUN',
641                               'get',
642                               mock.ANY,
643                               uuid=VOLUME['name'],
644                               additional=['is_mapped']))
645        self.assertDictEqual(LUN_INFO, result)
646
647        del out['data']['lun']
648        self.assertRaises(exception.MalformedResponse,
649                          self.common._get_lun_info,
650                          VOLUME['name'])
651
652        self.assertRaises(exception.SynoAuthError,
653                          self.common._get_lun_info,
654                          VOLUME['name'])
655
656        self.assertRaises(exception.InvalidParameterValue,
657                          self.common._get_lun_info,
658                          '')
659
660    def test__get_lun_uuid(self):
661        lun_info = copy.deepcopy(LUN_INFO)
662        self.common._get_lun_info = (
663            mock.Mock(side_effect=[
664                      lun_info,
665                      lun_info,
666                      exception.SynoAuthError(message='dont care')]))
667
668        result = self.common._get_lun_uuid(VOLUME['name'])
669        self.assertEqual(LUN_UUID, result)
670
671        del lun_info['uuid']
672        self.assertRaises(exception.MalformedResponse,
673                          self.common._get_lun_uuid,
674                          VOLUME['name'])
675
676        self.assertRaises(exception.SynoAuthError,
677                          self.common._get_lun_uuid,
678                          VOLUME['name'])
679
680        self.assertRaises(exception.InvalidParameterValue,
681                          self.common._get_lun_uuid,
682                          '')
683
684    def test__get_lun_status(self):
685        lun_info = copy.deepcopy(LUN_INFO)
686        self.common._get_lun_info = (
687            mock.Mock(side_effect=[
688                      lun_info,
689                      lun_info,
690                      lun_info,
691                      exception.SynoAuthError(message='dont care')]))
692
693        result = self.common._get_lun_status(VOLUME['name'])
694        self.assertEqual((lun_info['status'], lun_info['is_action_locked']),
695                         result)
696
697        del lun_info['is_action_locked']
698        self.assertRaises(exception.MalformedResponse,
699                          self.common._get_lun_status,
700                          VOLUME['name'])
701
702        del lun_info['status']
703        self.assertRaises(exception.MalformedResponse,
704                          self.common._get_lun_status,
705                          VOLUME['name'])
706
707        self.assertRaises(exception.SynoAuthError,
708                          self.common._get_lun_status,
709                          VOLUME['name'])
710
711        self.assertRaises(exception.InvalidParameterValue,
712                          self.common._get_lun_status,
713                          '')
714
715    def test__get_snapshot_info(self):
716        out = {
717            'data': {
718                'snapshot': SNAPSHOT_INFO
719            },
720            'success': True
721        }
722        self.common.exec_webapi = (
723            mock.Mock(side_effect=[
724                      out,
725                      out,
726                      exception.SynoAuthError(message='dont care')]))
727        result = self.common._get_snapshot_info(DS_SNAPSHOT_UUID,
728                                                additional=['status'])
729        (self.common.exec_webapi.
730            assert_called_with('SYNO.Core.ISCSI.LUN',
731                               'get_snapshot',
732                               mock.ANY,
733                               snapshot_uuid=DS_SNAPSHOT_UUID,
734                               additional=['status']))
735        self.assertDictEqual(SNAPSHOT_INFO, result)
736
737        del out['data']['snapshot']
738        self.assertRaises(exception.MalformedResponse,
739                          self.common._get_snapshot_info,
740                          DS_SNAPSHOT_UUID)
741
742        self.assertRaises(exception.SynoAuthError,
743                          self.common._get_snapshot_info,
744                          DS_SNAPSHOT_UUID)
745
746        self.assertRaises(exception.InvalidParameterValue,
747                          self.common._get_snapshot_info,
748                          '')
749
750    def test__get_snapshot_status(self):
751        snapshot_info = copy.deepcopy(SNAPSHOT_INFO)
752        self.common._get_snapshot_info = (
753            mock.Mock(side_effect=[
754                      snapshot_info,
755                      snapshot_info,
756                      snapshot_info,
757                      exception.SynoAuthError(message='dont care')]))
758
759        result = self.common._get_snapshot_status(DS_SNAPSHOT_UUID)
760        self.assertEqual((snapshot_info['status'],
761                          snapshot_info['is_action_locked']),
762                         result)
763
764        del snapshot_info['is_action_locked']
765        self.assertRaises(exception.MalformedResponse,
766                          self.common._get_snapshot_status,
767                          DS_SNAPSHOT_UUID)
768
769        del snapshot_info['status']
770        self.assertRaises(exception.MalformedResponse,
771                          self.common._get_snapshot_status,
772                          DS_SNAPSHOT_UUID)
773
774        self.assertRaises(exception.SynoAuthError,
775                          self.common._get_snapshot_status,
776                          DS_SNAPSHOT_UUID)
777
778        self.assertRaises(exception.InvalidParameterValue,
779                          self.common._get_snapshot_status,
780                          '')
781
782    def test__get_metadata_value(self):
783        ctxt = context.get_admin_context()
784        fake_vol_obj = fake_volume.fake_volume_obj(ctxt)
785        self.assertRaises(exception.VolumeMetadataNotFound,
786                          self.common._get_metadata_value,
787                          fake_vol_obj,
788                          'no_such_key')
789
790        fake_snap_obj = (fake_snapshot.
791                         fake_snapshot_obj(ctxt,
792                                           expected_attrs=['metadata']))
793        self.assertRaises(exception.SnapshotMetadataNotFound,
794                          self.common._get_metadata_value,
795                          fake_snap_obj,
796                          'no_such_key')
797
798        meta = {'snapshot_metadata': [{'key': 'ds_snapshot_UUID',
799                                       'value': DS_SNAPSHOT_UUID}],
800                'expected_attrs': ['metadata']}
801
802        fake_snap_obj = fake_snapshot.fake_snapshot_obj(ctxt,
803                                                        **meta)
804        result = self.common._get_metadata_value(fake_snap_obj,
805                                                 'ds_snapshot_UUID')
806        self.assertEqual(DS_SNAPSHOT_UUID, result)
807
808        self.assertRaises(exception.MetadataAbsent,
809                          self.common._get_metadata_value,
810                          SNAPSHOT,
811                          'no_such_key')
812
813    def test__target_create_with_chap_auth(self):
814        out = {
815            'data': {
816                'target_id': TRG_ID
817            },
818            'success': True
819        }
820        trg_name = self.common.TARGET_NAME_PREFIX + VOLUME['id']
821        iqn = self.conf.target_prefix + trg_name
822        self.conf.use_chap_auth = True
823        self.common.exec_webapi = mock.Mock(return_value=out)
824        self.conf.safe_get = (
825            mock.Mock(side_effect=[
826                      self.conf.use_chap_auth,
827                      'abcd',
828                      'qwerty',
829                      self.conf.target_prefix]))
830        result = self.common._target_create(VOLUME['id'])
831        (self.common.exec_webapi.
832            assert_called_with('SYNO.Core.ISCSI.Target',
833                               'create',
834                               mock.ANY,
835                               name=trg_name,
836                               iqn=iqn,
837                               auth_type=1,
838                               user='abcd',
839                               password='qwerty',
840                               max_sessions=0))
841        self.assertEqual((IQN, TRG_ID, 'CHAP abcd qwerty'), result)
842
843    def test__target_create_without_chap_auth(self):
844        out = {
845            'data': {
846                'target_id': TRG_ID
847            },
848            'success': True
849        }
850        trg_name = self.common.TARGET_NAME_PREFIX + VOLUME['id']
851        iqn = self.conf.target_prefix + trg_name
852        self.common.exec_webapi = mock.Mock(return_value=out)
853        self.conf.safe_get = (
854            mock.Mock(side_effect=[
855                      self.conf.use_chap_auth,
856                      self.conf.target_prefix]))
857        result = self.common._target_create(VOLUME['id'])
858        (self.common.exec_webapi.
859            assert_called_with('SYNO.Core.ISCSI.Target',
860                               'create',
861                               mock.ANY,
862                               name=trg_name,
863                               iqn=iqn,
864                               auth_type=0,
865                               user='',
866                               password='',
867                               max_sessions=0))
868        self.assertEqual((IQN, TRG_ID, ''), result)
869
870    def test__target_create_error(self):
871        out = {
872            'data': {
873            },
874            'success': True
875        }
876        self.common.exec_webapi = (
877            mock.Mock(side_effect=[
878                      out,
879                      exception.SynoAuthError(message='dont care')]))
880        self.conf.safe_get = (
881            mock.Mock(side_effect=[
882                      self.conf.use_chap_auth,
883                      self.conf.target_prefix,
884                      self.conf.use_chap_auth,
885                      self.conf.target_prefix]))
886
887        self.assertRaises(exception.VolumeDriverException,
888                          self.common._target_create,
889                          VOLUME['id'])
890
891        self.assertRaises(exception.SynoAuthError,
892                          self.common._target_create,
893                          VOLUME['id'])
894
895        self.assertRaises(exception.InvalidParameterValue,
896                          self.common._target_create,
897                          '')
898
899    def test__target_delete(self):
900        out = {
901            'success': True
902        }
903        self.common.exec_webapi = (
904            mock.Mock(side_effect=[
905                      out,
906                      exception.SynoAuthError(message='dont care')]))
907
908        result = self.common._target_delete(TRG_ID)
909        (self.common.exec_webapi.
910            assert_called_with('SYNO.Core.ISCSI.Target',
911                               'delete',
912                               mock.ANY,
913                               target_id=str(TRG_ID)))
914        self.assertIsNone(result)
915
916        self.assertRaises(exception.SynoAuthError,
917                          self.common._target_delete,
918                          TRG_ID)
919
920        self.assertRaises(exception.InvalidParameterValue,
921                          self.common._target_delete,
922                          -1)
923
924    def test__lun_map_unmap_target(self):
925        out = {
926            'success': True
927        }
928        self.common.exec_webapi = (
929            mock.Mock(side_effect=[
930                      out,
931                      out,
932                      exception.SynoAuthError(message='dont care')]))
933        self.common._get_lun_uuid = mock.Mock(return_value=LUN_UUID)
934
935        result = self.common._lun_map_unmap_target(VOLUME['name'],
936                                                   True,
937                                                   TRG_ID)
938        self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
939        (self.common.exec_webapi.
940            assert_called_with('SYNO.Core.ISCSI.LUN',
941                               'map_target',
942                               mock.ANY,
943                               uuid=LUN_UUID,
944                               target_ids=[str(TRG_ID)]))
945        self.assertIsNone(result)
946
947        result = self.common._lun_map_unmap_target(VOLUME['name'],
948                                                   False,
949                                                   TRG_ID)
950        (self.common.exec_webapi.
951            assert_called_with('SYNO.Core.ISCSI.LUN',
952                               'unmap_target',
953                               mock.ANY,
954                               uuid=LUN_UUID,
955                               target_ids=[str(TRG_ID)]))
956        self.assertIsNone(result)
957
958        self.assertRaises(exception.SynoAuthError,
959                          self.common._lun_map_unmap_target,
960                          VOLUME['name'],
961                          True,
962                          TRG_ID)
963
964        self.assertRaises(exception.InvalidParameterValue,
965                          self.common._lun_map_unmap_target,
966                          mock.ANY,
967                          mock.ANY,
968                          -1)
969
970    def test__lun_map_target(self):
971        self.common._lun_map_unmap_target = mock.Mock()
972
973        result = self.common._lun_map_target(VOLUME, TRG_ID)
974
975        self.common._lun_map_unmap_target.assert_called_with(VOLUME,
976                                                             True,
977                                                             TRG_ID)
978        self.assertIsNone(result)
979
980    def test__lun_ummap_target(self):
981        self.common._lun_map_unmap_target = mock.Mock()
982
983        result = self.common._lun_unmap_target(VOLUME, TRG_ID)
984
985        self.common._lun_map_unmap_target.assert_called_with(VOLUME,
986                                                             False,
987                                                             TRG_ID)
988        self.assertIsNone(result)
989
990    def test__modify_lun_name(self):
991        out = {
992            'success': True
993        }
994        self.common.exec_webapi = (
995            mock.Mock(side_effect=[
996                      out,
997                      exception.SynoAuthError(message='dont care')]))
998
999        result = self.common._modify_lun_name(VOLUME['name'],
1000                                              NEW_VOLUME['name'])
1001        self.assertIsNone(result)
1002
1003        self.assertRaises(exception.SynoAuthError,
1004                          self.common._modify_lun_name,
1005                          VOLUME['name'],
1006                          NEW_VOLUME['name'])
1007
1008    @mock.patch('eventlet.sleep')
1009    def test__check_lun_status_normal(self, _patched_sleep):
1010        self.common._get_lun_status = (
1011            mock.Mock(side_effect=[
1012                      ('normal', True),
1013                      ('normal', False),
1014                      ('cloning', False),
1015                      exception.SynoLUNNotExist(message='dont care')]))
1016
1017        result = self.common._check_lun_status_normal(VOLUME['name'])
1018        self.assertEqual(1, _patched_sleep.call_count)
1019        self.assertEqual([mock.call(2)], _patched_sleep.call_args_list)
1020        self.common._get_lun_status.assert_called_with(VOLUME['name'])
1021        self.assertTrue(result)
1022
1023        result = self.common._check_lun_status_normal(VOLUME['name'])
1024        self.assertFalse(result)
1025
1026        self.assertRaises(exception.SynoLUNNotExist,
1027                          self.common._check_lun_status_normal,
1028                          VOLUME['name'])
1029
1030    @mock.patch('eventlet.sleep')
1031    def test__check_snapshot_status_healthy(self, _patched_sleep):
1032        self.common._get_snapshot_status = (
1033            mock.Mock(side_effect=[
1034                      ('Healthy', True),
1035                      ('Healthy', False),
1036                      ('Unhealthy', False),
1037                      exception.SynoLUNNotExist(message='dont care')]))
1038
1039        result = self.common._check_snapshot_status_healthy(DS_SNAPSHOT_UUID)
1040        self.assertEqual(1, _patched_sleep.call_count)
1041        self.assertEqual([mock.call(2)], _patched_sleep.call_args_list)
1042        self.common._get_snapshot_status.assert_called_with(DS_SNAPSHOT_UUID)
1043        self.assertTrue(result)
1044
1045        result = self.common._check_snapshot_status_healthy(DS_SNAPSHOT_UUID)
1046        self.assertFalse(result)
1047
1048        self.assertRaises(exception.SynoLUNNotExist,
1049                          self.common._check_snapshot_status_healthy,
1050                          DS_SNAPSHOT_UUID)
1051
1052    def test__check_storage_response(self):
1053        out = {
1054            'success': False
1055        }
1056        result = self.common._check_storage_response(out)
1057        self.assertEqual('Internal error', result[0])
1058        self.assertIsInstance(result[1],
1059                              (exception.VolumeBackendAPIException))
1060
1061    def test__check_iscsi_response(self):
1062        out = {
1063            'success': False,
1064            'error': {
1065            }
1066        }
1067        self.assertRaises(exception.MalformedResponse,
1068                          self.common._check_iscsi_response,
1069                          out)
1070
1071        out['error'].update(code=18990505)
1072        result = self.common._check_iscsi_response(out, uuid=LUN_UUID)
1073        self.assertEqual('Bad LUN UUID [18990505]', result[0])
1074        self.assertIsInstance(result[1],
1075                              (exception.SynoLUNNotExist))
1076
1077        out['error'].update(code=18990532)
1078        result = self.common._check_iscsi_response(out,
1079                                                   snapshot_id=SNAPSHOT_ID)
1080        self.assertEqual('No such snapshot [18990532]', result[0])
1081        self.assertIsInstance(result[1],
1082                              (exception.SnapshotNotFound))
1083
1084        out['error'].update(code=12345678)
1085        result = self.common._check_iscsi_response(out, uuid=LUN_UUID)
1086        self.assertEqual('Internal error [12345678]', result[0])
1087        self.assertIsInstance(result[1],
1088                              (exception.VolumeBackendAPIException))
1089
1090    def test__check_ds_pool_status(self):
1091        info = copy.deepcopy(POOL_INFO)
1092        self.common._get_pool_info = mock.Mock(return_value=info)
1093
1094        result = self.common._check_ds_pool_status()
1095        self.assertIsNone(result)
1096
1097        info['readonly'] = True
1098        self.assertRaises(exception.VolumeDriverException,
1099                          self.common._check_ds_pool_status)
1100
1101        del info['readonly']
1102        self.assertRaises(exception.MalformedResponse,
1103                          self.common._check_ds_pool_status)
1104
1105    def test__check_ds_version(self):
1106        ver1 = 'DSM 6.1-9999'
1107        ver2 = 'DSM 6.0.2-9999'
1108        ver3 = 'DSM 6.0.1-9999 Update 2'
1109        ver4 = 'DSM 6.0-9999 Update 2'
1110        ver5 = 'DSM 5.2-9999 '
1111        out = {
1112            'data': {
1113            },
1114            'success': True
1115        }
1116        self.common.exec_webapi = mock.Mock(return_value=out)
1117        self.assertRaises(exception.MalformedResponse,
1118                          self.common._check_ds_version)
1119        (self.common.exec_webapi.
1120            assert_called_with('SYNO.Core.System',
1121                               'info',
1122                               mock.ANY,
1123                               type='firmware'))
1124
1125        out['data'].update(firmware_ver=ver1)
1126        result = self.common._check_ds_version()
1127        self.assertIsNone(result)
1128
1129        out['data'].update(firmware_ver=ver2)
1130        result = self.common._check_ds_version()
1131        self.assertIsNone(result)
1132
1133        out['data'].update(firmware_ver=ver3)
1134        self.assertRaises(exception.VolumeDriverException,
1135                          self.common._check_ds_version)
1136
1137        out['data'].update(firmware_ver=ver4)
1138        self.assertRaises(exception.VolumeDriverException,
1139                          self.common._check_ds_version)
1140
1141        out['data'].update(firmware_ver=ver5)
1142        self.assertRaises(exception.VolumeDriverException,
1143                          self.common._check_ds_version)
1144
1145        self.common.exec_webapi = (
1146            mock.Mock(side_effect=
1147                      exception.SynoAuthError(message='dont care')))
1148        self.assertRaises(exception.SynoAuthError,
1149                          self.common._check_ds_version)
1150
1151    def test__check_ds_ability(self):
1152        out = {
1153            'data': {
1154                'support_storage_mgr': 'yes',
1155                'support_iscsi_target': 'yes',
1156                'support_vaai': 'yes',
1157                'supportsnapshot': 'yes',
1158            },
1159            'success': True
1160        }
1161        self.common.exec_webapi = mock.Mock(return_value=out)
1162        result = self.common._check_ds_ability()
1163        self.assertIsNone(result)
1164        (self.common.exec_webapi.
1165            assert_called_with('SYNO.Core.System',
1166                               'info',
1167                               mock.ANY,
1168                               type='define'))
1169
1170        out['data'].update(supportsnapshot='no')
1171        self.assertRaises(exception.VolumeDriverException,
1172                          self.common._check_ds_ability)
1173
1174        out['data'].update(support_vaai='no')
1175        self.assertRaises(exception.VolumeDriverException,
1176                          self.common._check_ds_ability)
1177
1178        out['data'].update(support_iscsi_target='no')
1179        self.assertRaises(exception.VolumeDriverException,
1180                          self.common._check_ds_ability)
1181
1182        out['data'].update(support_storage_mgr='no')
1183        self.assertRaises(exception.VolumeDriverException,
1184                          self.common._check_ds_ability)
1185
1186        out['data'].update(usbstation='yes')
1187        self.assertRaises(exception.VolumeDriverException,
1188                          self.common._check_ds_ability)
1189
1190        del out['data']
1191        self.assertRaises(exception.MalformedResponse,
1192                          self.common._check_ds_ability)
1193
1194        self.common.exec_webapi = (
1195            mock.Mock(side_effect=
1196                      exception.SynoAuthError(message='dont care')))
1197        self.assertRaises(exception.SynoAuthError,
1198                          self.common._check_ds_ability)
1199
1200    @mock.patch.object(common.LOG, 'exception')
1201    def test_check_response(self, _logexc):
1202        out = {
1203            'success': True
1204        }
1205        bad_out1 = {
1206            'api_info': {
1207                'api': 'SYNO.Core.ISCSI.LUN',
1208                'method': 'create',
1209                'version': 1
1210            },
1211            'success': False
1212        }
1213        bad_out2 = {
1214            'api_info': {
1215                'api': 'SYNO.Core.Storage.Volume',
1216                'method': 'get',
1217                'version': 1
1218            },
1219            'success': False
1220        }
1221        bad_out3 = {
1222            'api_info': {
1223                'api': 'SYNO.Core.System',
1224                'method': 'info',
1225                'version': 1
1226            },
1227            'success': False
1228        }
1229        self.common._check_iscsi_response = (
1230            mock.Mock(return_value=
1231                      ('Bad LUN UUID',
1232                       exception.SynoLUNNotExist(message='dont care'))))
1233        self.common._check_storage_response = (
1234            mock.Mock(return_value=
1235                      ('Internal error',
1236                       exception.
1237                       VolumeBackendAPIException(message='dont care'))))
1238
1239        result = self.common.check_response(out)
1240        self.assertEqual(0, _logexc.call_count)
1241        self.assertIsNone(result)
1242
1243        self.assertRaises(exception.SynoLUNNotExist,
1244                          self.common.check_response,
1245                          bad_out1)
1246        self.assertRaises(exception.VolumeBackendAPIException,
1247                          self.common.check_response,
1248                          bad_out2)
1249        self.assertRaises(exception.VolumeBackendAPIException,
1250                          self.common.check_response,
1251                          bad_out3)
1252
1253    def test_exec_webapi(self):
1254        api = 'SYNO.Fake.WebAPI'
1255        method = 'fake'
1256        version = 1
1257        resp = {}
1258        bad_resp = {
1259            'http_status': http_client.INTERNAL_SERVER_ERROR
1260        }
1261        expected = copy.deepcopy(resp)
1262        expected.update(api_info={'api': api,
1263                                  'method': method,
1264                                  'version': version})
1265        self.common.synoexec = mock.Mock(side_effect=[resp, bad_resp])
1266
1267        result = self.common.exec_webapi(api,
1268                                         method,
1269                                         version,
1270                                         param1='value1',
1271                                         param2='value2')
1272
1273        self.common.synoexec.assert_called_once_with(api,
1274                                                     method,
1275                                                     version,
1276                                                     param1='value1',
1277                                                     param2='value2')
1278        self.assertDictEqual(expected, result)
1279
1280        self.assertRaises(exception.SynoAPIHTTPError,
1281                          self.common.exec_webapi,
1282                          api,
1283                          method,
1284                          version,
1285                          param1='value1',
1286                          param2='value2')
1287
1288    def test_get_ip(self):
1289        result = self.common.get_ip()
1290        self.assertEqual(self.conf.target_ip_address, result)
1291
1292    def test_get_provider_location(self):
1293        self.common.get_ip = (
1294            mock.Mock(return_value=self.conf.target_ip_address))
1295        self.conf.safe_get = (
1296            mock.Mock(return_value=['10.0.0.2', '10.0.0.3']))
1297        expected = ('10.0.0.1:3260;10.0.0.2:3260;10.0.0.3:3260' +
1298                    ',%(tid)d %(iqn)s 0') % {'tid': TRG_ID, 'iqn': IQN}
1299
1300        result = self.common.get_provider_location(IQN, TRG_ID)
1301
1302        self.assertEqual(expected, result)
1303
1304    def test_is_lun_mapped(self):
1305        bad_lun_info = copy.deepcopy(LUN_INFO)
1306        del bad_lun_info['is_mapped']
1307        self.common._get_lun_info = (
1308            mock.Mock(side_effect=[
1309                      LUN_INFO,
1310                      exception.SynoAuthError(message='dont care'),
1311                      bad_lun_info]))
1312
1313        result = self.common.is_lun_mapped(VOLUME['name'])
1314        self.assertEqual(LUN_INFO['is_mapped'], result)
1315
1316        self.assertRaises(exception.SynoAuthError,
1317                          self.common.is_lun_mapped,
1318                          VOLUME['name'])
1319
1320        self.assertRaises(exception.MalformedResponse,
1321                          self.common.is_lun_mapped,
1322                          VOLUME['name'])
1323
1324        self.assertRaises(exception.InvalidParameterValue,
1325                          self.common.is_lun_mapped,
1326                          '')
1327
1328    def test_check_for_setup_error(self):
1329        self.common._check_ds_pool_status = mock.Mock()
1330        self.common._check_ds_version = mock.Mock()
1331        self.common._check_ds_ability = mock.Mock()
1332
1333        result = self.common.check_for_setup_error()
1334
1335        self.common._check_ds_pool_status.assert_called_once_with()
1336        self.common._check_ds_version.assert_called_once_with()
1337        self.common._check_ds_ability.assert_called_once_with()
1338        self.assertIsNone(result)
1339
1340    def test_update_volume_stats(self):
1341        self.common._get_pool_size = mock.Mock(return_value=(10, 100, 50))
1342        self.common._get_pool_lun_provisioned_size = (
1343            mock.Mock(return_value=300))
1344
1345        data = {
1346            'volume_backend_name': 'DiskStation',
1347            'vendor_name': 'Synology',
1348            'storage_protocol': 'iscsi',
1349            'consistencygroup_support': False,
1350            'QoS_support': False,
1351            'thin_provisioning_support': True,
1352            'thick_provisioning_support': False,
1353            'reserved_percentage': 0,
1354            'free_capacity_gb': 10,
1355            'total_capacity_gb': 100,
1356            'provisioned_capacity_gb': 350,
1357            'max_over_subscription_ratio': 20,
1358            'target_ip_address': '10.0.0.1',
1359            'pool_name': 'volume1',
1360            'backend_info':
1361                'Synology:iscsi:72003c93-2db2-4f00-a169-67c5eae86bb1'
1362        }
1363
1364        result = self.common.update_volume_stats()
1365
1366        self.assertDictEqual(data, result)
1367
1368    def test_create_volume(self):
1369        out = {
1370            'success': True
1371        }
1372        self.common.exec_webapi = (
1373            mock.Mock(side_effect=[
1374                      out,
1375                      out,
1376                      exception.SynoAuthError(message='dont care')]))
1377        self.common._check_lun_status_normal = (
1378            mock.Mock(side_effect=[True, False, True]))
1379
1380        result = self.common.create_volume(VOLUME)
1381        (self.common.exec_webapi.
1382            assert_called_with('SYNO.Core.ISCSI.LUN',
1383                               'create',
1384                               mock.ANY,
1385                               name=VOLUME['name'],
1386                               type=self.common.CINDER_LUN,
1387                               location='/' + self.conf.synology_pool_name,
1388                               size=VOLUME['size'] * units.Gi))
1389        self.assertIsNone(result)
1390
1391        self.assertRaises(exception.VolumeDriverException,
1392                          self.common.create_volume,
1393                          VOLUME)
1394
1395        self.assertRaises(exception.SynoAuthError,
1396                          self.common.create_volume,
1397                          VOLUME)
1398
1399    def test_delete_volume(self):
1400        out = {
1401            'success': True
1402        }
1403        self.common._get_lun_uuid = mock.Mock(return_value=LUN_UUID)
1404        self.common.exec_webapi = (
1405            mock.Mock(side_effect=[
1406                      out,
1407                      exception.SynoLUNNotExist(message='dont care'),
1408                      exception.SynoAuthError(message='dont care')]))
1409
1410        result = self.common.delete_volume(VOLUME)
1411        self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
1412        (self.common.exec_webapi.
1413            assert_called_with('SYNO.Core.ISCSI.LUN',
1414                               'delete',
1415                               mock.ANY,
1416                               uuid=LUN_UUID))
1417        self.assertIsNone(result)
1418
1419        result = self.common.delete_volume(VOLUME)
1420        self.assertIsNone(result)
1421
1422        self.assertRaises(exception.SynoAuthError,
1423                          self.common.delete_volume,
1424                          VOLUME)
1425
1426    def test_create_cloned_volume(self):
1427        out = {
1428            'success': True
1429        }
1430        new_volume = copy.deepcopy(NEW_VOLUME)
1431        new_volume['size'] = 20
1432        self.common.exec_webapi = mock.Mock(return_value=out)
1433        self.common._get_lun_uuid = (
1434            mock.Mock(side_effect=[
1435                      LUN_UUID,
1436                      LUN_UUID,
1437                      LUN_UUID,
1438                      exception.InvalidParameterValue('dont care')]))
1439        self.common.extend_volume = mock.Mock()
1440        self.common._check_lun_status_normal = (
1441            mock.Mock(side_effect=[True, True, False, False]))
1442        result = self.common.create_cloned_volume(new_volume, VOLUME)
1443        self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
1444        (self.common.exec_webapi.
1445            assert_called_with('SYNO.Core.ISCSI.LUN',
1446                               'clone',
1447                               mock.ANY,
1448                               src_lun_uuid=LUN_UUID,
1449                               dst_lun_name=new_volume['name'],
1450                               is_same_pool=True,
1451                               clone_type='CINDER'))
1452        (self.common._check_lun_status_normal.
1453            assert_called_with(new_volume['name']))
1454        self.common.extend_volume.assert_called_once_with(new_volume,
1455                                                          new_volume['size'])
1456        self.assertIsNone(result)
1457
1458        new_volume['size'] = 10
1459        result = self.common.create_cloned_volume(new_volume, VOLUME)
1460        self.assertIsNone(result)
1461
1462        self.assertRaises(exception.VolumeDriverException,
1463                          self.common.create_cloned_volume,
1464                          new_volume,
1465                          VOLUME)
1466
1467        self.assertRaises(exception.InvalidParameterValue,
1468                          self.common.create_cloned_volume,
1469                          new_volume,
1470                          VOLUME)
1471
1472    def test_extend_volume(self):
1473        new_size = 20
1474        out = {
1475            'success': True
1476        }
1477        self.common.exec_webapi = mock.Mock(return_value=out)
1478        self.common._get_lun_uuid = (
1479            mock.Mock(side_effect=[
1480                      LUN_UUID,
1481                      exception.InvalidParameterValue('dont care')]))
1482
1483        result = self.common.extend_volume(VOLUME, new_size)
1484
1485        (self.common.exec_webapi.
1486            assert_called_with('SYNO.Core.ISCSI.LUN',
1487                               'set',
1488                               mock.ANY,
1489                               uuid=LUN_UUID,
1490                               new_size=new_size * units.Gi))
1491        self.assertIsNone(result)
1492        self.assertRaises(exception.ExtendVolumeError,
1493                          self.common.extend_volume,
1494                          VOLUME,
1495                          new_size)
1496
1497    def test_update_migrated_volume(self):
1498        expected = {
1499            '_name_id': None
1500        }
1501        self.common._modify_lun_name = mock.Mock(side_effect=[None, Exception])
1502
1503        result = self.common.update_migrated_volume(VOLUME,
1504                                                    NEW_VOLUME)
1505
1506        self.common._modify_lun_name.assert_called_with(NEW_VOLUME['name'],
1507                                                        VOLUME['name'])
1508        self.assertDictEqual(expected, result)
1509
1510        self.assertRaises(exception.VolumeMigrationFailed,
1511                          self.common.update_migrated_volume,
1512                          VOLUME,
1513                          NEW_VOLUME)
1514
1515    def test_create_snapshot(self):
1516        expected_result = {
1517            'metadata': {
1518                self.common.METADATA_DS_SNAPSHOT_UUID: DS_SNAPSHOT_UUID
1519            }
1520        }
1521        expected_result['metadata'].update(SNAPSHOT['metadata'])
1522
1523        out = {
1524            'data': {
1525                'snapshot_uuid': DS_SNAPSHOT_UUID,
1526                'snapshot_id': SNAPSHOT_ID
1527            },
1528            'success': True
1529        }
1530        self.common.exec_webapi = mock.Mock(return_value=out)
1531        self.common._check_snapshot_status_healthy = (
1532            mock.Mock(side_effect=[True, False]))
1533
1534        result = self.common.create_snapshot(SNAPSHOT)
1535
1536        (self.common.exec_webapi.
1537            assert_called_with('SYNO.Core.ISCSI.LUN',
1538                               'take_snapshot',
1539                               mock.ANY,
1540                               src_lun_uuid=SNAPSHOT['volume']['name'],
1541                               is_app_consistent=False,
1542                               is_locked=False,
1543                               taken_by='Cinder',
1544                               description='(Cinder) ' +
1545                               SNAPSHOT['id']))
1546        self.assertDictEqual(expected_result, result)
1547
1548        self.assertRaises(exception.VolumeDriverException,
1549                          self.common.create_snapshot,
1550                          SNAPSHOT)
1551
1552    def test_create_snapshot_error(self):
1553        out = {
1554            'data': {
1555                'snapshot_uuid': 1,
1556                'snapshot_id': SNAPSHOT_ID
1557            },
1558            'success': True
1559        }
1560        self.common.exec_webapi = mock.Mock(return_value=out)
1561
1562        self.assertRaises(exception.MalformedResponse,
1563                          self.common.create_snapshot,
1564                          SNAPSHOT)
1565
1566        self.common.exec_webapi = (
1567            mock.Mock(side_effect=exception.SynoAuthError(reason='dont care')))
1568
1569        self.assertRaises(exception.SynoAuthError,
1570                          self.common.create_snapshot,
1571                          SNAPSHOT)
1572
1573    def test_delete_snapshot(self):
1574        out = {
1575            'success': True
1576        }
1577        self.common.exec_webapi = mock.Mock(return_value=out)
1578        self.common._get_metadata_value = (
1579            mock.Mock(side_effect=[
1580                      DS_SNAPSHOT_UUID,
1581                      exception.SnapshotMetadataNotFound(message='dont care'),
1582                      exception.MetadataAbsent]))
1583
1584        result = self.common.delete_snapshot(SNAPSHOT)
1585        (self.common._get_metadata_value.
1586            assert_called_with(SNAPSHOT,
1587                               self.common.METADATA_DS_SNAPSHOT_UUID))
1588        (self.common.exec_webapi.
1589            assert_called_with('SYNO.Core.ISCSI.LUN',
1590                               'delete_snapshot',
1591                               mock.ANY,
1592                               snapshot_uuid=DS_SNAPSHOT_UUID,
1593                               deleted_by='Cinder'))
1594        self.assertIsNone(result)
1595
1596        result = self.common.delete_snapshot(SNAPSHOT)
1597        self.assertIsNone(result)
1598
1599        self.assertRaises(exception.MetadataAbsent,
1600                          self.common.delete_snapshot,
1601                          SNAPSHOT)
1602
1603    def test_create_volume_from_snapshot(self):
1604        out = {
1605            'success': True
1606        }
1607        new_volume = copy.deepcopy(NEW_VOLUME)
1608        new_volume['size'] = 20
1609        self.common.exec_webapi = mock.Mock(return_value=out)
1610        self.common._get_metadata_value = (
1611            mock.Mock(side_effect=[
1612                      DS_SNAPSHOT_UUID,
1613                      DS_SNAPSHOT_UUID,
1614                      exception.SnapshotMetadataNotFound(message='dont care'),
1615                      exception.SynoAuthError(message='dont care')]))
1616        self.common._check_lun_status_normal = (
1617            mock.Mock(side_effect=[True, False, True, True]))
1618        self.common.extend_volume = mock.Mock()
1619
1620        result = self.common.create_volume_from_snapshot(new_volume, SNAPSHOT)
1621
1622        (self.common._get_metadata_value.
1623            assert_called_with(SNAPSHOT,
1624                               self.common.METADATA_DS_SNAPSHOT_UUID))
1625        (self.common.exec_webapi.
1626            assert_called_with('SYNO.Core.ISCSI.LUN',
1627                               'clone_snapshot',
1628                               mock.ANY,
1629                               src_lun_uuid=SNAPSHOT['volume']['name'],
1630                               snapshot_uuid=DS_SNAPSHOT_UUID,
1631                               cloned_lun_name=new_volume['name'],
1632                               clone_type='CINDER'))
1633        self.common.extend_volume.assert_called_once_with(new_volume,
1634                                                          new_volume['size'])
1635        self.assertIsNone(result)
1636
1637        self.assertRaises(exception.VolumeDriverException,
1638                          self.common.create_volume_from_snapshot,
1639                          new_volume,
1640                          SNAPSHOT)
1641
1642        self.assertRaises(exception.SnapshotMetadataNotFound,
1643                          self.common.create_volume_from_snapshot,
1644                          new_volume,
1645                          SNAPSHOT)
1646
1647        self.assertRaises(exception.SynoAuthError,
1648                          self.common.create_volume_from_snapshot,
1649                          new_volume,
1650                          SNAPSHOT)
1651
1652    def test_get_iqn_and_trgid(self):
1653        location = '%s:3260,%d %s 1' % (IP, 1, IQN)
1654
1655        result = self.common.get_iqn_and_trgid(location)
1656
1657        self.assertEqual((IQN, 1), result)
1658
1659        location = ''
1660        self.assertRaises(exception.InvalidParameterValue,
1661                          self.common.get_iqn_and_trgid,
1662                          location)
1663
1664        location = 'BADINPUT'
1665        self.assertRaises(exception.InvalidInput,
1666                          self.common.get_iqn_and_trgid,
1667                          location)
1668
1669        location = '%s:3260 %s 1' % (IP, IQN)
1670        self.assertRaises(exception.InvalidInput,
1671                          self.common.get_iqn_and_trgid,
1672                          location)
1673
1674    def test_get_iscsi_properties(self):
1675        volume = copy.deepcopy(VOLUME)
1676        iscsi_properties = {
1677            'target_discovered': False,
1678            'target_iqn': IQN,
1679            'target_portal': '%s:3260' % IP,
1680            'volume_id': VOLUME['id'],
1681            'access_mode': 'rw',
1682            'discard': False,
1683            'auth_method': 'CHAP',
1684            'auth_username': CHAP_AUTH_USERNAME,
1685            'auth_password': CHAP_AUTH_PASSWORD
1686        }
1687        self.common.get_ip = mock.Mock(return_value=IP)
1688        self.conf.safe_get = mock.Mock(return_value=[])
1689
1690        result = self.common.get_iscsi_properties(volume)
1691        self.assertDictEqual(iscsi_properties, result)
1692
1693        volume['provider_location'] = ''
1694        self.assertRaises(exception.InvalidParameterValue,
1695                          self.common.get_iscsi_properties,
1696                          volume)
1697
1698    def test_get_iscsi_properties_multipath(self):
1699        volume = copy.deepcopy(VOLUME)
1700        iscsi_properties = {
1701            'target_discovered': False,
1702            'target_iqn': IQN,
1703            'target_iqns': [IQN] * 3,
1704            'target_lun': 0,
1705            'target_luns': [0] * 3,
1706            'target_portal': '%s:3260' % IP,
1707            'target_portals':
1708                ['%s:3260' % IP, '10.0.0.2:3260', '10.0.0.3:3260'],
1709            'volume_id': VOLUME['id'],
1710            'access_mode': 'rw',
1711            'discard': False,
1712            'auth_method': 'CHAP',
1713            'auth_username': CHAP_AUTH_USERNAME,
1714            'auth_password': CHAP_AUTH_PASSWORD
1715        }
1716        self.common.get_ip = mock.Mock(return_value=IP)
1717        self.conf.safe_get = mock.Mock(return_value=['10.0.0.2', '10.0.0.3'])
1718
1719        result = self.common.get_iscsi_properties(volume)
1720        self.assertDictEqual(iscsi_properties, result)
1721
1722        volume['provider_location'] = ''
1723        self.assertRaises(exception.InvalidParameterValue,
1724                          self.common.get_iscsi_properties,
1725                          volume)
1726
1727    def test_get_iscsi_properties_without_chap(self):
1728        volume = copy.deepcopy(VOLUME)
1729        iscsi_properties = {
1730            'target_discovered': False,
1731            'target_iqn': IQN,
1732            'target_portal': '%s:3260' % IP,
1733            'volume_id': VOLUME['id'],
1734            'access_mode': 'rw',
1735            'discard': False
1736        }
1737        self.common.get_ip = mock.Mock(return_value=IP)
1738        self.conf.safe_get = mock.Mock(return_value=[])
1739
1740        volume['provider_auth'] = 'abcde'
1741        result = self.common.get_iscsi_properties(volume)
1742        self.assertDictEqual(iscsi_properties, result)
1743
1744        volume['provider_auth'] = ''
1745        result = self.common.get_iscsi_properties(volume)
1746        self.assertDictEqual(iscsi_properties, result)
1747
1748        del volume['provider_auth']
1749        result = self.common.get_iscsi_properties(volume)
1750        self.assertDictEqual(iscsi_properties, result)
1751
1752    def test_create_iscsi_export(self):
1753        self.common._target_create = (
1754            mock.Mock(return_value=(IQN, TRG_ID, VOLUME['provider_auth'])))
1755        self.common._lun_map_target = mock.Mock()
1756
1757        iqn, trg_id, provider_auth = (
1758            self.common.create_iscsi_export(VOLUME['name'], VOLUME['id']))
1759
1760        self.common._target_create.assert_called_with(VOLUME['id'])
1761        self.common._lun_map_target.assert_called_with(VOLUME['name'], trg_id)
1762        self.assertEqual((IQN, TRG_ID, VOLUME['provider_auth']),
1763                         (iqn, trg_id, provider_auth))
1764
1765    def test_remove_iscsi_export(self):
1766        trg_id = TRG_ID
1767        self.common._lun_unmap_target = mock.Mock()
1768        self.common._target_delete = mock.Mock()
1769
1770        result = self.common.remove_iscsi_export(VOLUME['name'], trg_id)
1771
1772        self.assertIsNone(result)
1773        self.common._lun_unmap_target.assert_called_with(VOLUME['name'],
1774                                                         TRG_ID)
1775        self.common._target_delete.assert_called_with(TRG_ID)
1776