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