1776 lines
65 KiB
Python
1776 lines
65 KiB
Python
# Copyright (c) 2016 Synology Co., Ltd.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Tests for the Synology iSCSI volume driver."""
|
|
|
|
import copy
|
|
import json
|
|
import math
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
import mock
|
|
from oslo_utils import units
|
|
import requests
|
|
from six.moves import http_client
|
|
from six import string_types
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder import test
|
|
from cinder.tests.unit import fake_constants as fake
|
|
from cinder.tests.unit import fake_snapshot
|
|
from cinder.tests.unit import fake_volume
|
|
from cinder.volume import configuration as conf
|
|
from cinder.volume.drivers.synology import synology_common as common
|
|
|
|
VOLUME_ID = fake.VOLUME_ID
|
|
TARGET_NAME_PREFIX = 'Cinder-Target-'
|
|
IP = '10.0.0.1'
|
|
IQN = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + VOLUME_ID
|
|
TRG_ID = 1
|
|
CHAP_AUTH_USERNAME = 'username'
|
|
CHAP_AUTH_PASSWORD = 'password'
|
|
VOLUME = {
|
|
'_name_id': '',
|
|
'name': fake.VOLUME_NAME,
|
|
'id': VOLUME_ID,
|
|
'display_name': 'fake_volume',
|
|
'size': 10,
|
|
'provider_location': '%s:3260,%d %s 1' % (IP, TRG_ID, IQN),
|
|
'provider_auth': 'CHAP %(user)s %(pass)s' % {
|
|
'user': CHAP_AUTH_USERNAME,
|
|
'pass': CHAP_AUTH_PASSWORD},
|
|
}
|
|
NEW_VOLUME_ID = fake.VOLUME2_ID
|
|
IQN2 = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + NEW_VOLUME_ID
|
|
NEW_TRG_ID = 2
|
|
NEW_VOLUME = {
|
|
'name': fake.VOLUME2_NAME,
|
|
'id': NEW_VOLUME_ID,
|
|
'display_name': 'new_fake_volume',
|
|
'size': 10,
|
|
'provider_location': '%s:3260,%d %s 1' % (IP, NEW_TRG_ID, IQN2),
|
|
}
|
|
SNAPSHOT_ID = fake.SNAPSHOT_ID
|
|
DS_SNAPSHOT_UUID = 'ca86a56a-40d8-4210-974c-ef15dbf01cba'
|
|
SNAPSHOT_METADATA = {
|
|
'snap-meta1': 'value1',
|
|
'snap-meta2': 'value2',
|
|
'snap-meta3': 'value3',
|
|
}
|
|
SNAPSHOT = {
|
|
'name': fake.SNAPSHOT_NAME,
|
|
'id': SNAPSHOT_ID,
|
|
'volume_id': VOLUME_ID,
|
|
'volume_name': VOLUME['name'],
|
|
'volume_size': 10,
|
|
'display_name': 'fake_snapshot',
|
|
'volume': VOLUME,
|
|
'metadata': SNAPSHOT_METADATA,
|
|
}
|
|
SNAPSHOT_INFO = {
|
|
'is_action_locked': False,
|
|
'snapshot_id': 1,
|
|
'status': 'Healthy',
|
|
'uuid': DS_SNAPSHOT_UUID,
|
|
}
|
|
INITIATOR_IQN = 'iqn.1993-08.org.debian:01:604af6a341'
|
|
CONNECTOR = {
|
|
'initiator': INITIATOR_IQN,
|
|
}
|
|
CONTEXT = {
|
|
}
|
|
LOCAL_PATH = '/dev/isda'
|
|
IMAGE_SERVICE = 'image_service'
|
|
IMAGE_ID = 1
|
|
IMAGE_META = {
|
|
'id': IMAGE_ID
|
|
}
|
|
POOL_NAME = 'volume1'
|
|
NODE_UUID = '72003c93-2db2-4f00-a169-67c5eae86bb1'
|
|
NODE_UUID2 = '8e1e8b82-1ef9-4157-a4bf-e069355386c2'
|
|
HOST = {
|
|
'capabilities': {
|
|
'pool_name': 'volume2',
|
|
'backend_info': 'Synology:iscsi:' + NODE_UUID,
|
|
},
|
|
}
|
|
POOL_INFO = {
|
|
'display_name': 'Volume 1',
|
|
'raid_type': 'raid_1',
|
|
'readonly': False,
|
|
'fs_type': 'ext4',
|
|
'location': 'internal',
|
|
'eppool_used_byte': '139177984',
|
|
'size_total_byte': '487262806016',
|
|
'volume_id': 1,
|
|
'size_free_byte': '486521139200',
|
|
'container': 'internal',
|
|
'volume_path': '/volume1',
|
|
'single_volume': True
|
|
}
|
|
LUN_UUID = 'e1315f33-ba35-42c3-a3e7-5a06958eca30'
|
|
LUN_INFO = {
|
|
'status': '',
|
|
'is_action_locked': False,
|
|
'name': VOLUME['name'],
|
|
'extent_size': 0,
|
|
'allocated_size': 0,
|
|
'uuid': LUN_UUID,
|
|
'is_mapped': True,
|
|
'lun_id': 3,
|
|
'location': '/volume2',
|
|
'restored_time': 0,
|
|
'type': 143,
|
|
'size': 1073741824
|
|
}
|
|
FAKE_API = 'SYNO.Fake.API'
|
|
FAKE_METHOD = 'fake'
|
|
FAKE_PATH = 'fake.cgi'
|
|
|
|
|
|
class MockResponse(object):
|
|
def __init__(self, json_data, status_code):
|
|
self.json_data = json_data
|
|
self.status_code = status_code
|
|
|
|
def json(self):
|
|
return self.json_data
|
|
|
|
|
|
class SynoSessionTestCase(test.TestCase):
|
|
@mock.patch('requests.post', return_value=MockResponse(
|
|
{'data': {'sid': 'sid'}, 'success': True}, http_client.OK))
|
|
def setUp(self, _mock_post):
|
|
super(SynoSessionTestCase, self).setUp()
|
|
|
|
self.host = '127.0.0.1'
|
|
self.port = 5001
|
|
self.username = 'admin'
|
|
self.password = 'admin'
|
|
self.https = True
|
|
self.ssl_verify = False
|
|
self.one_time_pass = None
|
|
self.device_id = None
|
|
self.session = common.Session(self.host,
|
|
self.port,
|
|
self.username,
|
|
self.password,
|
|
self.https,
|
|
self.ssl_verify,
|
|
self.one_time_pass,
|
|
self.device_id)
|
|
self.session.__class__.__del__ = lambda x: x
|
|
|
|
def test_query(self):
|
|
out = {
|
|
'maxVersion': 3,
|
|
'minVersion': 1,
|
|
'path': FAKE_PATH,
|
|
'requestFormat': 'JSON'
|
|
}
|
|
data = {
|
|
'api': 'SYNO.API.Info',
|
|
'version': 1,
|
|
'method': 'query',
|
|
'query': FAKE_API
|
|
}
|
|
requests.post = mock.Mock(side_effect=[
|
|
MockResponse({
|
|
'data': {
|
|
FAKE_API: out
|
|
},
|
|
'success': True
|
|
}, http_client.OK),
|
|
MockResponse({
|
|
'data': {
|
|
FAKE_API: out
|
|
}
|
|
}, http_client.OK),
|
|
])
|
|
|
|
result = self.session.query(FAKE_API)
|
|
requests.post.assert_called_once_with(
|
|
'https://127.0.0.1:5001/webapi/query.cgi',
|
|
data=data,
|
|
verify=self.ssl_verify)
|
|
self.assertDictEqual(out, result)
|
|
|
|
result = self.session.query(FAKE_API)
|
|
self.assertIsNone(result)
|
|
|
|
def test__random_AES_passphrase(self):
|
|
lengths_to_test = [0, 1, 10, 128, 501, 1024, 4096]
|
|
for test_length in lengths_to_test:
|
|
self.assertEqual(
|
|
test_length,
|
|
len(self.session._random_AES_passphrase(test_length))
|
|
)
|
|
|
|
def test__encrypt_RSA(self):
|
|
# Initialize a fixed 1024 bit public/private key pair
|
|
public_numbers = rsa.RSAPublicNumbers(
|
|
int('10001', 16),
|
|
int('c42eadf905d47388d84baeec2d5391ba7f91b35912933032c9c8a32d6358'
|
|
'9cef1dfe532138adfad41fd41910cd12fbc05b8876f70aa1340fccf3227d'
|
|
'087d1e47256c60ae49abee7c779815ec085265518791da38168a0597091d'
|
|
'4c6ff10c0fa6616f250b85edfb4066f655695e304c0dc40c26fc11541e4c'
|
|
'1be47771fcc1d257cccbb656015c5daed64aad7c8ae024f82531b7e637f4'
|
|
'87530b77498d1bc7247687541fbbaa01112866da06f30185dde15131e89e'
|
|
'27b30f07f10ddef23dd4da7bf3e216c733a4004415c9d1dd9bd5032e8b55'
|
|
'4eb56efa9cd5cd1b416e0e55c903536787454ca3d3aba87edb70768f630c'
|
|
'beab3781848ff5ee40edfaee57ac87c9', 16)
|
|
)
|
|
private_numbers = rsa.RSAPrivateNumbers(
|
|
int('f0aa7e45ffb23ca683e1b01a9e1d77e5affaf9afa0094fb1eb89a3c8672b'
|
|
'43ab9beb11e4ecdd2c8f88738db56be4149c55c28379480ac68a5727ba28'
|
|
'4a47565579dbf083167a2845f5f267598febde3f7b12ba10da32ad2edff8'
|
|
'4efd019498e0d8e03f6ddb8a5e80cdb862da9c0c921571fdb56ae7e0480a'
|
|
'de846e328517aa23', 16),
|
|
int('d0ae9ce41716c4bdac074423d57e540b6f48ee42d9b06bdac3b3421ea2ae'
|
|
'e21088b3ae50acfe168edefda722dc15bc456bba76a98b8035ffa4da12dc'
|
|
'a92bad582c935791f9a48b416f53c728fd1866c8ecf2ca00dfa667a962d3'
|
|
'c9818cce540c5e9d2ef8843c5adfde0938ac8b5e2c592838c422ffac43ff'
|
|
'4a4907c129de7723', 16),
|
|
int('3733cf5e58069cefefb4f4269ee67a0619695d26fe340e86ec0299efe699'
|
|
'83a741305421eff9fcaf7db947c8537c38fcba84debccaefeb5f5ad33b6c'
|
|
'255c578dbb7910875a5197cccc362e4cf9567e0dfff0c98fa8bff3acb932'
|
|
'd6545566886ccfd3df7fab92f874f9c3eceab6472ecf5ccff2945127f352'
|
|
'8532b76d8aaadb4dbcf0e5bae8c9c8597511e0771942f12e29bbee1ceef5'
|
|
'4a6ba97e0096354b13ae4ca22e9be1a551a1bc8db9392de6bbad99b956b5'
|
|
'bb4b7f5094086e6eefd432066102a228bc18012cc31a7777e2e657eb115a'
|
|
'9d718d413f2bd7a448a783c049afaaf127486b2c17feebb930e7ac8e6a07'
|
|
'd9c843beedfa8cec52e1aba98099baa5', 16),
|
|
int('c8ab1050e36c457ffe550f56926235d7b18d8de5af86340a413fe9edae80'
|
|
'77933e9599bd0cf73a318feff1c7c4e74f7c2f51d9f82566beb71906ca04'
|
|
'd0327d3d16379a6a633286241778004ec05f46581e11b64d58f28a4e9c77'
|
|
'59bd423519e7d94dd9f58ae9ebf47013ff71124eb4fbe6a94a3c928d02e4'
|
|
'f536ecff78d40b8b', 16),
|
|
int('5bb873a2d8f71bf015dd77b89c4c931a1786a19a665de179dccc3c4284d4'
|
|
'82ee2b7776256573a46c955c3d8ad7db01ce2d645e6574b81c83c96c4420'
|
|
'1286ed00b54ee98d72813ce7bccbc0dca629847bc99188f1cb5b3372c2ca'
|
|
'3d6620824b74c85d23d8fd1e1dff09735a22947b06d90511b63b7fceb270'
|
|
'51b139a45007c4ab', 16),
|
|
int('cfeff2a88112512b327999eb926a0564c431ebed2e1456f51d274e4e6d7d'
|
|
'd75d5b26339bbca2807aa71008e9a08bd9fa0e53e3960e3b6e8c6e1a46d2'
|
|
'b8e89b218d3b453f7ed0020504d1679374cd884ae3bb3b88b54fb429f082'
|
|
'fa4e9d3f296c59d5d89fe16b0931dcf062bc309cf122c722c13ffb0fa0c5'
|
|
'77d0abddcc655017', 16),
|
|
public_numbers
|
|
)
|
|
private_key = private_numbers.private_key(default_backend())
|
|
|
|
# run the _encrypt_RSA method
|
|
original_text = 'test _encrypt_RSA'
|
|
encrypted_text = self.session._encrypt_RSA(
|
|
public_numbers.n,
|
|
public_numbers.e,
|
|
original_text
|
|
)
|
|
|
|
# decrypt the output using the corresponding private key
|
|
decrypted_bytes = private_key.decrypt(
|
|
encrypted_text,
|
|
padding.PKCS1v15()
|
|
)
|
|
decrypted_text = decrypted_bytes.decode('ascii')
|
|
self.assertEqual(original_text, decrypted_text)
|
|
|
|
def test__encrypt_params(self):
|
|
# setup mock
|
|
cipherkey = 'cipherkey'
|
|
self.session._get_enc_info = mock.Mock(return_value={
|
|
'public_key': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
'cipherkey': cipherkey,
|
|
'ciphertoken': 'ciphertoken',
|
|
'server_time': 1111111111,
|
|
})
|
|
self.session._encrypt_RSA = mock.Mock(
|
|
return_value=b'1234567890abcdef'
|
|
)
|
|
self.session._encrypt_AES = mock.Mock(
|
|
return_value=b'fedcba0987654321'
|
|
)
|
|
|
|
# call the method
|
|
params = {
|
|
'account': 'account',
|
|
'passwd': 'passwd',
|
|
'session': 'sessionid',
|
|
'format': 'sid'
|
|
}
|
|
encrypted_data = self.session._encrypt_params(params)
|
|
|
|
# check the format of the output
|
|
self.assertDictEqual(
|
|
json.loads(encrypted_data[cipherkey]),
|
|
{'rsa': 'MTIzNDU2Nzg5MGFiY2RlZg==',
|
|
'aes': 'ZmVkY2JhMDk4NzY1NDMyMQ=='}
|
|
)
|
|
|
|
|
|
class SynoAPIRequestTestCase(test.TestCase):
|
|
@mock.patch('requests.post')
|
|
def setUp(self, _mock_post):
|
|
super(SynoAPIRequestTestCase, self).setUp()
|
|
|
|
self.host = '127.0.0.1'
|
|
self.port = 5001
|
|
self.username = 'admin'
|
|
self.password = 'admin'
|
|
self.https = True
|
|
self.ssl_verify = False
|
|
self.one_time_pass = None
|
|
self.device_id = None
|
|
self.request = common.APIRequest(self.host,
|
|
self.port,
|
|
self.username,
|
|
self.password,
|
|
self.https,
|
|
self.ssl_verify,
|
|
self.one_time_pass,
|
|
self.device_id)
|
|
self.request._APIRequest__session._sid = 'sid'
|
|
self.request._APIRequest__session.__class__.__del__ = lambda x: x
|
|
|
|
@mock.patch.object(common, 'Session')
|
|
def test_new_session(self, _mock_session):
|
|
self.device_id = 'did'
|
|
self.request = common.APIRequest(self.host,
|
|
self.port,
|
|
self.username,
|
|
self.password,
|
|
self.https,
|
|
self.ssl_verify,
|
|
self.one_time_pass,
|
|
self.device_id)
|
|
|
|
result = self.request.new_session()
|
|
self.assertIsNone(result)
|
|
|
|
def test__start(self):
|
|
out = {
|
|
'maxVersion': 3,
|
|
'minVersion': 1,
|
|
'path': FAKE_PATH,
|
|
'requestFormat': 'JSON'
|
|
}
|
|
self.request._APIRequest__session.query = mock.Mock(return_value=out)
|
|
|
|
result = self.request._start(FAKE_API, 3)
|
|
(self.request._APIRequest__session.query.
|
|
assert_called_once_with(FAKE_API))
|
|
self.assertEqual(FAKE_PATH, result)
|
|
|
|
out.update(maxVersion=2)
|
|
self.assertRaises(exception.APIException,
|
|
self.request._start,
|
|
FAKE_API,
|
|
3)
|
|
|
|
def test__encode_param(self):
|
|
param = {
|
|
'api': FAKE_API,
|
|
'method': FAKE_METHOD,
|
|
'version': 1,
|
|
'_sid': 'sid'
|
|
}
|
|
self.request._jsonFormat = True
|
|
result = self.request._encode_param(param)
|
|
self.assertIsInstance(result, string_types)
|
|
|
|
def test_request(self):
|
|
version = 1
|
|
|
|
self.request._start = mock.Mock(return_value='fake.cgi')
|
|
self.request._encode_param = mock.Mock(side_effect=lambda x: x)
|
|
self.request.new_session = mock.Mock()
|
|
requests.post = mock.Mock(side_effect=[
|
|
MockResponse({'success': True}, http_client.OK),
|
|
MockResponse({'error': {'code': http_client.SWITCHING_PROTOCOLS},
|
|
'success': False}, http_client.OK),
|
|
MockResponse({'error': {'code': http_client.SWITCHING_PROTOCOLS}},
|
|
http_client.OK),
|
|
MockResponse({}, http_client.INTERNAL_SERVER_ERROR)
|
|
])
|
|
|
|
result = self.request.request(FAKE_API, FAKE_METHOD, version)
|
|
self.assertDictEqual({'success': True}, result)
|
|
|
|
result = self.request.request(FAKE_API, FAKE_METHOD, version)
|
|
self.assertDictEqual(
|
|
{'error': {'code': http_client.SWITCHING_PROTOCOLS},
|
|
'success': False}, result)
|
|
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.request.request,
|
|
FAKE_API,
|
|
FAKE_METHOD,
|
|
version)
|
|
|
|
result = self.request.request(FAKE_API, FAKE_METHOD, version)
|
|
self.assertDictEqual(
|
|
{'http_status': http_client.INTERNAL_SERVER_ERROR}, result)
|
|
|
|
@mock.patch.object(common.LOG, 'debug')
|
|
def test_request_auth_error(self, _log):
|
|
version = 1
|
|
|
|
self.request._start = mock.Mock(return_value='fake.cgi')
|
|
self.request._encode_param = mock.Mock(side_effect=lambda x: x)
|
|
self.request.new_session = mock.Mock()
|
|
requests.post = mock.Mock(return_value=
|
|
MockResponse({
|
|
'error': {'code': 105},
|
|
'success': False
|
|
}, http_client.OK))
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.request.request,
|
|
FAKE_API,
|
|
FAKE_METHOD,
|
|
version)
|
|
|
|
|
|
class SynoCommonTestCase(test.TestCase):
|
|
|
|
@mock.patch.object(common.SynoCommon,
|
|
'_get_node_uuid',
|
|
return_value=NODE_UUID)
|
|
@mock.patch.object(common, 'APIRequest')
|
|
def setUp(self, _request, _get_node_uuid):
|
|
super(SynoCommonTestCase, self).setUp()
|
|
|
|
self.conf = self.setup_configuration()
|
|
self.common = common.SynoCommon(self.conf, 'iscsi')
|
|
self.common.vendor_name = 'Synology'
|
|
self.common.driver_type = 'iscsi'
|
|
self.common.volume_backend_name = 'DiskStation'
|
|
self.common.target_port = 3260
|
|
|
|
def setup_configuration(self):
|
|
config = mock.Mock(spec=conf.Configuration)
|
|
config.use_chap_auth = False
|
|
config.target_protocol = 'iscsi'
|
|
config.target_ip_address = IP
|
|
config.target_port = 3260
|
|
config.synology_admin_port = 5000
|
|
config.synology_username = 'admin'
|
|
config.synology_password = 'admin'
|
|
config.synology_ssl_verify = True
|
|
config.synology_one_time_pass = '123456'
|
|
config.synology_pool_name = POOL_NAME
|
|
config.volume_dd_blocksize = 1
|
|
config.target_prefix = 'iqn.2000-01.com.synology:'
|
|
config.chap_username = 'abcd'
|
|
config.chap_password = 'qwerty'
|
|
config.reserved_percentage = 0
|
|
config.max_over_subscription_ratio = 20
|
|
|
|
return config
|
|
|
|
@mock.patch.object(common.SynoCommon,
|
|
'_get_node_uuid',
|
|
return_value=NODE_UUID)
|
|
@mock.patch.object(common, 'APIRequest')
|
|
def test___init__(self, _request, _get_node_uuid):
|
|
self.conf.safe_get = (mock.Mock(side_effect=[
|
|
self.conf.target_ip_address,
|
|
'',
|
|
'']))
|
|
|
|
self.assertRaises(exception.InvalidConfigurationValue,
|
|
self.common.__init__,
|
|
self.conf,
|
|
'iscsi')
|
|
|
|
self.assertRaises(exception.InvalidConfigurationValue,
|
|
self.common.__init__,
|
|
self.conf,
|
|
'iscsi')
|
|
|
|
def test__get_node_uuid(self):
|
|
out = {
|
|
'data': {
|
|
'nodes': [{
|
|
'uuid': NODE_UUID
|
|
}]
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._get_node_uuid()
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.Node',
|
|
'list',
|
|
mock.ANY))
|
|
self.assertEqual(NODE_UUID, result)
|
|
|
|
del out['data']['nodes']
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._get_node_uuid)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_node_uuid)
|
|
|
|
def test__get_pool_info(self):
|
|
out = {
|
|
'data': {
|
|
'volume': POOL_INFO
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
result = self.common._get_pool_info()
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.Storage.Volume',
|
|
'get',
|
|
mock.ANY,
|
|
volume_path='/' + POOL_NAME))
|
|
self.assertDictEqual(POOL_INFO, result)
|
|
|
|
del out['data']['volume']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_pool_info)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_pool_info)
|
|
|
|
self.conf.synology_pool_name = ''
|
|
self.assertRaises(exception.InvalidConfigurationValue,
|
|
self.common._get_pool_info)
|
|
|
|
def test__get_pool_size(self):
|
|
pool_info = copy.deepcopy(POOL_INFO)
|
|
self.common._get_pool_info = mock.Mock(return_value=pool_info)
|
|
|
|
result = self.common._get_pool_size()
|
|
|
|
self.assertEqual((int(int(POOL_INFO['size_free_byte']) / units.Gi),
|
|
int(int(POOL_INFO['size_total_byte']) / units.Gi),
|
|
math.ceil((float(POOL_INFO['size_total_byte']) -
|
|
float(POOL_INFO['size_free_byte']) -
|
|
float(POOL_INFO['eppool_used_byte'])) /
|
|
units.Gi)),
|
|
result)
|
|
|
|
del pool_info['size_free_byte']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_pool_size)
|
|
|
|
def test__get_pool_lun_provisioned_size(self):
|
|
out = {
|
|
'data': {
|
|
'luns': [{
|
|
'lun_id': 1,
|
|
'location': '/' + POOL_NAME,
|
|
'size': 5368709120
|
|
}, {
|
|
'lun_id': 2,
|
|
'location': '/' + POOL_NAME,
|
|
'size': 3221225472
|
|
}]
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
|
|
result = self.common._get_pool_lun_provisioned_size()
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'list',
|
|
mock.ANY,
|
|
location='/' + POOL_NAME))
|
|
self.assertEqual(int(math.ceil(float(5368709120 + 3221225472) /
|
|
units.Gi)),
|
|
result)
|
|
|
|
def test__get_pool_lun_provisioned_size_error(self):
|
|
out = {
|
|
'data': {},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_pool_lun_provisioned_size)
|
|
|
|
self.conf.synology_pool_name = ''
|
|
self.assertRaises(exception.InvalidConfigurationValue,
|
|
self.common._get_pool_lun_provisioned_size)
|
|
|
|
def test__get_lun_info(self):
|
|
out = {
|
|
'data': {
|
|
'lun': LUN_INFO
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
result = self.common._get_lun_info(VOLUME['name'],
|
|
['is_mapped'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'get',
|
|
mock.ANY,
|
|
uuid=VOLUME['name'],
|
|
additional=['is_mapped']))
|
|
self.assertDictEqual(LUN_INFO, result)
|
|
|
|
del out['data']['lun']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_lun_info,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_lun_info,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._get_lun_info,
|
|
'')
|
|
|
|
def test__get_lun_uuid(self):
|
|
lun_info = copy.deepcopy(LUN_INFO)
|
|
self.common._get_lun_info = (
|
|
mock.Mock(side_effect=[
|
|
lun_info,
|
|
lun_info,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._get_lun_uuid(VOLUME['name'])
|
|
self.assertEqual(LUN_UUID, result)
|
|
|
|
del lun_info['uuid']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_lun_uuid,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_lun_uuid,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._get_lun_uuid,
|
|
'')
|
|
|
|
def test__get_lun_status(self):
|
|
lun_info = copy.deepcopy(LUN_INFO)
|
|
self.common._get_lun_info = (
|
|
mock.Mock(side_effect=[
|
|
lun_info,
|
|
lun_info,
|
|
lun_info,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._get_lun_status(VOLUME['name'])
|
|
self.assertEqual((lun_info['status'], lun_info['is_action_locked']),
|
|
result)
|
|
|
|
del lun_info['is_action_locked']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_lun_status,
|
|
VOLUME['name'])
|
|
|
|
del lun_info['status']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_lun_status,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_lun_status,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._get_lun_status,
|
|
'')
|
|
|
|
def test__get_snapshot_info(self):
|
|
out = {
|
|
'data': {
|
|
'snapshot': SNAPSHOT_INFO
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
result = self.common._get_snapshot_info(DS_SNAPSHOT_UUID,
|
|
additional=['status'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'get_snapshot',
|
|
mock.ANY,
|
|
snapshot_uuid=DS_SNAPSHOT_UUID,
|
|
additional=['status']))
|
|
self.assertDictEqual(SNAPSHOT_INFO, result)
|
|
|
|
del out['data']['snapshot']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_snapshot_info,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_snapshot_info,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._get_snapshot_info,
|
|
'')
|
|
|
|
def test__get_snapshot_status(self):
|
|
snapshot_info = copy.deepcopy(SNAPSHOT_INFO)
|
|
self.common._get_snapshot_info = (
|
|
mock.Mock(side_effect=[
|
|
snapshot_info,
|
|
snapshot_info,
|
|
snapshot_info,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._get_snapshot_status(DS_SNAPSHOT_UUID)
|
|
self.assertEqual((snapshot_info['status'],
|
|
snapshot_info['is_action_locked']),
|
|
result)
|
|
|
|
del snapshot_info['is_action_locked']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_snapshot_status,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
del snapshot_info['status']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._get_snapshot_status,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._get_snapshot_status,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._get_snapshot_status,
|
|
'')
|
|
|
|
def test__get_metadata_value(self):
|
|
ctxt = context.get_admin_context()
|
|
fake_vol_obj = fake_volume.fake_volume_obj(ctxt)
|
|
self.assertRaises(exception.VolumeMetadataNotFound,
|
|
self.common._get_metadata_value,
|
|
fake_vol_obj,
|
|
'no_such_key')
|
|
|
|
fake_snap_obj = (fake_snapshot.
|
|
fake_snapshot_obj(ctxt,
|
|
expected_attrs=['metadata']))
|
|
self.assertRaises(exception.SnapshotMetadataNotFound,
|
|
self.common._get_metadata_value,
|
|
fake_snap_obj,
|
|
'no_such_key')
|
|
|
|
meta = {'snapshot_metadata': [{'key': 'ds_snapshot_UUID',
|
|
'value': DS_SNAPSHOT_UUID}],
|
|
'expected_attrs': ['metadata']}
|
|
|
|
fake_snap_obj = fake_snapshot.fake_snapshot_obj(ctxt,
|
|
**meta)
|
|
result = self.common._get_metadata_value(fake_snap_obj,
|
|
'ds_snapshot_UUID')
|
|
self.assertEqual(DS_SNAPSHOT_UUID, result)
|
|
|
|
self.assertRaises(exception.MetadataAbsent,
|
|
self.common._get_metadata_value,
|
|
SNAPSHOT,
|
|
'no_such_key')
|
|
|
|
def test__target_create_with_chap_auth(self):
|
|
out = {
|
|
'data': {
|
|
'target_id': TRG_ID
|
|
},
|
|
'success': True
|
|
}
|
|
trg_name = self.common.TARGET_NAME_PREFIX + VOLUME['id']
|
|
iqn = self.conf.target_prefix + trg_name
|
|
self.conf.use_chap_auth = True
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.conf.safe_get = (
|
|
mock.Mock(side_effect=[
|
|
self.conf.use_chap_auth,
|
|
'abcd',
|
|
'qwerty',
|
|
self.conf.target_prefix]))
|
|
result = self.common._target_create(VOLUME['id'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.Target',
|
|
'create',
|
|
mock.ANY,
|
|
name=trg_name,
|
|
iqn=iqn,
|
|
auth_type=1,
|
|
user='abcd',
|
|
password='qwerty',
|
|
max_sessions=0))
|
|
self.assertEqual((IQN, TRG_ID, 'CHAP abcd qwerty'), result)
|
|
|
|
def test__target_create_without_chap_auth(self):
|
|
out = {
|
|
'data': {
|
|
'target_id': TRG_ID
|
|
},
|
|
'success': True
|
|
}
|
|
trg_name = self.common.TARGET_NAME_PREFIX + VOLUME['id']
|
|
iqn = self.conf.target_prefix + trg_name
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.conf.safe_get = (
|
|
mock.Mock(side_effect=[
|
|
self.conf.use_chap_auth,
|
|
self.conf.target_prefix]))
|
|
result = self.common._target_create(VOLUME['id'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.Target',
|
|
'create',
|
|
mock.ANY,
|
|
name=trg_name,
|
|
iqn=iqn,
|
|
auth_type=0,
|
|
user='',
|
|
password='',
|
|
max_sessions=0))
|
|
self.assertEqual((IQN, TRG_ID, ''), result)
|
|
|
|
def test__target_create_error(self):
|
|
out = {
|
|
'data': {
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
self.conf.safe_get = (
|
|
mock.Mock(side_effect=[
|
|
self.conf.use_chap_auth,
|
|
self.conf.target_prefix,
|
|
self.conf.use_chap_auth,
|
|
self.conf.target_prefix]))
|
|
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._target_create,
|
|
VOLUME['id'])
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._target_create,
|
|
VOLUME['id'])
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._target_create,
|
|
'')
|
|
|
|
def test__target_delete(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._target_delete(TRG_ID)
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.Target',
|
|
'delete',
|
|
mock.ANY,
|
|
target_id=str(TRG_ID)))
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._target_delete,
|
|
TRG_ID)
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._target_delete,
|
|
-1)
|
|
|
|
def test__lun_map_unmap_target(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
self.common._get_lun_uuid = mock.Mock(return_value=LUN_UUID)
|
|
|
|
result = self.common._lun_map_unmap_target(VOLUME['name'],
|
|
True,
|
|
TRG_ID)
|
|
self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'map_target',
|
|
mock.ANY,
|
|
uuid=LUN_UUID,
|
|
target_ids=[str(TRG_ID)]))
|
|
self.assertIsNone(result)
|
|
|
|
result = self.common._lun_map_unmap_target(VOLUME['name'],
|
|
False,
|
|
TRG_ID)
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'unmap_target',
|
|
mock.ANY,
|
|
uuid=LUN_UUID,
|
|
target_ids=[str(TRG_ID)]))
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._lun_map_unmap_target,
|
|
VOLUME['name'],
|
|
True,
|
|
TRG_ID)
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common._lun_map_unmap_target,
|
|
mock.ANY,
|
|
mock.ANY,
|
|
-1)
|
|
|
|
def test__lun_map_target(self):
|
|
self.common._lun_map_unmap_target = mock.Mock()
|
|
|
|
result = self.common._lun_map_target(VOLUME, TRG_ID)
|
|
|
|
self.common._lun_map_unmap_target.assert_called_with(VOLUME,
|
|
True,
|
|
TRG_ID)
|
|
self.assertIsNone(result)
|
|
|
|
def test__lun_ummap_target(self):
|
|
self.common._lun_map_unmap_target = mock.Mock()
|
|
|
|
result = self.common._lun_unmap_target(VOLUME, TRG_ID)
|
|
|
|
self.common._lun_map_unmap_target.assert_called_with(VOLUME,
|
|
False,
|
|
TRG_ID)
|
|
self.assertIsNone(result)
|
|
|
|
def test__modify_lun_name(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common._modify_lun_name(VOLUME['name'],
|
|
NEW_VOLUME['name'])
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._modify_lun_name,
|
|
VOLUME['name'],
|
|
NEW_VOLUME['name'])
|
|
|
|
@mock.patch('eventlet.sleep')
|
|
def test__check_lun_status_normal(self, _patched_sleep):
|
|
self.common._get_lun_status = (
|
|
mock.Mock(side_effect=[
|
|
('normal', True),
|
|
('normal', False),
|
|
('cloning', False),
|
|
common.SynoLUNNotExist(message='dont care')]))
|
|
|
|
result = self.common._check_lun_status_normal(VOLUME['name'])
|
|
self.assertEqual(1, _patched_sleep.call_count)
|
|
self.assertEqual([mock.call(2)], _patched_sleep.call_args_list)
|
|
self.common._get_lun_status.assert_called_with(VOLUME['name'])
|
|
self.assertTrue(result)
|
|
|
|
result = self.common._check_lun_status_normal(VOLUME['name'])
|
|
self.assertFalse(result)
|
|
|
|
self.assertRaises(common.SynoLUNNotExist,
|
|
self.common._check_lun_status_normal,
|
|
VOLUME['name'])
|
|
|
|
@mock.patch('eventlet.sleep')
|
|
def test__check_snapshot_status_healthy(self, _patched_sleep):
|
|
self.common._get_snapshot_status = (
|
|
mock.Mock(side_effect=[
|
|
('Healthy', True),
|
|
('Healthy', False),
|
|
('Unhealthy', False),
|
|
common.SynoLUNNotExist(message='dont care')]))
|
|
|
|
result = self.common._check_snapshot_status_healthy(DS_SNAPSHOT_UUID)
|
|
self.assertEqual(1, _patched_sleep.call_count)
|
|
self.assertEqual([mock.call(2)], _patched_sleep.call_args_list)
|
|
self.common._get_snapshot_status.assert_called_with(DS_SNAPSHOT_UUID)
|
|
self.assertTrue(result)
|
|
|
|
result = self.common._check_snapshot_status_healthy(DS_SNAPSHOT_UUID)
|
|
self.assertFalse(result)
|
|
|
|
self.assertRaises(common.SynoLUNNotExist,
|
|
self.common._check_snapshot_status_healthy,
|
|
DS_SNAPSHOT_UUID)
|
|
|
|
def test__check_storage_response(self):
|
|
out = {
|
|
'success': False
|
|
}
|
|
result = self.common._check_storage_response(out)
|
|
self.assertEqual('Internal error', result[0])
|
|
self.assertIsInstance(result[1],
|
|
(exception.VolumeBackendAPIException))
|
|
|
|
def test__check_iscsi_response(self):
|
|
out = {
|
|
'success': False,
|
|
'error': {
|
|
}
|
|
}
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._check_iscsi_response,
|
|
out)
|
|
|
|
out['error'].update(code=18990505)
|
|
result = self.common._check_iscsi_response(out, uuid=LUN_UUID)
|
|
self.assertEqual('Bad LUN UUID [18990505]', result[0])
|
|
self.assertIsInstance(result[1],
|
|
(common.SynoLUNNotExist))
|
|
|
|
out['error'].update(code=18990532)
|
|
result = self.common._check_iscsi_response(out,
|
|
snapshot_id=SNAPSHOT_ID)
|
|
self.assertEqual('No such snapshot [18990532]', result[0])
|
|
self.assertIsInstance(result[1],
|
|
(exception.SnapshotNotFound))
|
|
|
|
out['error'].update(code=12345678)
|
|
result = self.common._check_iscsi_response(out, uuid=LUN_UUID)
|
|
self.assertEqual('Internal error [12345678]', result[0])
|
|
self.assertIsInstance(result[1],
|
|
(exception.VolumeBackendAPIException))
|
|
|
|
def test__check_ds_pool_status(self):
|
|
info = copy.deepcopy(POOL_INFO)
|
|
self.common._get_pool_info = mock.Mock(return_value=info)
|
|
|
|
result = self.common._check_ds_pool_status()
|
|
self.assertIsNone(result)
|
|
|
|
info['readonly'] = True
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_pool_status)
|
|
|
|
del info['readonly']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._check_ds_pool_status)
|
|
|
|
def test__check_ds_version(self):
|
|
ver1 = 'DSM 6.1-9999'
|
|
ver2 = 'DSM UC 1.0-9999 Update 2'
|
|
ver3 = 'DSM 6.0.1-9999 Update 2'
|
|
ver4 = 'DSM 6.0-9999 Update 2'
|
|
ver5 = 'DSM 5.2-9999'
|
|
out = {
|
|
'data': {
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._check_ds_version)
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.System',
|
|
'info',
|
|
mock.ANY,
|
|
type='firmware'))
|
|
|
|
out['data'].update(firmware_ver=ver1)
|
|
result = self.common._check_ds_version()
|
|
self.assertIsNone(result)
|
|
|
|
out['data'].update(firmware_ver=ver2)
|
|
result = self.common._check_ds_version()
|
|
self.assertIsNone(result)
|
|
|
|
out['data'].update(firmware_ver=ver3)
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_version)
|
|
|
|
out['data'].update(firmware_ver=ver4)
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_version)
|
|
|
|
out['data'].update(firmware_ver=ver5)
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_version)
|
|
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=
|
|
common.SynoAuthError(message='dont care')))
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._check_ds_version)
|
|
|
|
def test__check_ds_ability(self):
|
|
out = {
|
|
'data': {
|
|
'support_storage_mgr': 'yes',
|
|
'support_iscsi_target': 'yes',
|
|
'support_vaai': 'yes',
|
|
'supportsnapshot': 'yes',
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
result = self.common._check_ds_ability()
|
|
self.assertIsNone(result)
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.System',
|
|
'info',
|
|
mock.ANY,
|
|
type='define'))
|
|
|
|
out['data'].update(supportsnapshot='no')
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_ability)
|
|
|
|
out['data'].update(support_vaai='no')
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_ability)
|
|
|
|
out['data'].update(support_iscsi_target='no')
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_ability)
|
|
|
|
out['data'].update(support_storage_mgr='no')
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_ability)
|
|
|
|
out['data'].update(usbstation='yes')
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common._check_ds_ability)
|
|
|
|
del out['data']
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common._check_ds_ability)
|
|
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=
|
|
common.SynoAuthError(message='dont care')))
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common._check_ds_ability)
|
|
|
|
@mock.patch.object(common.LOG, 'exception')
|
|
def test_check_response(self, _logexc):
|
|
out = {
|
|
'success': True
|
|
}
|
|
bad_out1 = {
|
|
'api_info': {
|
|
'api': 'SYNO.Core.ISCSI.LUN',
|
|
'method': 'create',
|
|
'version': 1
|
|
},
|
|
'success': False
|
|
}
|
|
bad_out2 = {
|
|
'api_info': {
|
|
'api': 'SYNO.Core.Storage.Volume',
|
|
'method': 'get',
|
|
'version': 1
|
|
},
|
|
'success': False
|
|
}
|
|
bad_out3 = {
|
|
'api_info': {
|
|
'api': 'SYNO.Core.System',
|
|
'method': 'info',
|
|
'version': 1
|
|
},
|
|
'success': False
|
|
}
|
|
self.common._check_iscsi_response = (
|
|
mock.Mock(return_value=
|
|
('Bad LUN UUID',
|
|
common.SynoLUNNotExist(message='dont care'))))
|
|
self.common._check_storage_response = (
|
|
mock.Mock(return_value=
|
|
('Internal error',
|
|
exception.
|
|
VolumeBackendAPIException(message='dont care'))))
|
|
|
|
result = self.common.check_response(out)
|
|
self.assertEqual(0, _logexc.call_count)
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(common.SynoLUNNotExist,
|
|
self.common.check_response,
|
|
bad_out1)
|
|
self.assertRaises(exception.VolumeBackendAPIException,
|
|
self.common.check_response,
|
|
bad_out2)
|
|
self.assertRaises(exception.VolumeBackendAPIException,
|
|
self.common.check_response,
|
|
bad_out3)
|
|
|
|
def test_exec_webapi(self):
|
|
api = 'SYNO.Fake.WebAPI'
|
|
method = 'fake'
|
|
version = 1
|
|
resp = {}
|
|
bad_resp = {
|
|
'http_status': http_client.INTERNAL_SERVER_ERROR
|
|
}
|
|
expected = copy.deepcopy(resp)
|
|
expected.update(api_info={'api': api,
|
|
'method': method,
|
|
'version': version})
|
|
self.common.synoexec = mock.Mock(side_effect=[resp, bad_resp])
|
|
|
|
result = self.common.exec_webapi(api,
|
|
method,
|
|
version,
|
|
param1='value1',
|
|
param2='value2')
|
|
|
|
self.common.synoexec.assert_called_once_with(api,
|
|
method,
|
|
version,
|
|
param1='value1',
|
|
param2='value2')
|
|
self.assertDictEqual(expected, result)
|
|
|
|
self.assertRaises(common.SynoAPIHTTPError,
|
|
self.common.exec_webapi,
|
|
api,
|
|
method,
|
|
version,
|
|
param1='value1',
|
|
param2='value2')
|
|
|
|
def test_get_ip(self):
|
|
result = self.common.get_ip()
|
|
self.assertEqual(self.conf.target_ip_address, result)
|
|
|
|
def test_get_provider_location(self):
|
|
self.common.get_ip = (
|
|
mock.Mock(return_value=self.conf.target_ip_address))
|
|
self.conf.safe_get = (
|
|
mock.Mock(return_value=['10.0.0.2', '10.0.0.3']))
|
|
expected = ('10.0.0.1:3260;10.0.0.2:3260;10.0.0.3:3260' +
|
|
',%(tid)d %(iqn)s 0') % {'tid': TRG_ID, 'iqn': IQN}
|
|
|
|
result = self.common.get_provider_location(IQN, TRG_ID)
|
|
|
|
self.assertEqual(expected, result)
|
|
|
|
def test_is_lun_mapped(self):
|
|
bad_lun_info = copy.deepcopy(LUN_INFO)
|
|
del bad_lun_info['is_mapped']
|
|
self.common._get_lun_info = (
|
|
mock.Mock(side_effect=[
|
|
LUN_INFO,
|
|
common.SynoAuthError(message='dont care'),
|
|
bad_lun_info]))
|
|
|
|
result = self.common.is_lun_mapped(VOLUME['name'])
|
|
self.assertEqual(LUN_INFO['is_mapped'], result)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common.is_lun_mapped,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common.is_lun_mapped,
|
|
VOLUME['name'])
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common.is_lun_mapped,
|
|
'')
|
|
|
|
def test_check_for_setup_error(self):
|
|
self.common._check_ds_pool_status = mock.Mock()
|
|
self.common._check_ds_version = mock.Mock()
|
|
self.common._check_ds_ability = mock.Mock()
|
|
|
|
result = self.common.check_for_setup_error()
|
|
|
|
self.common._check_ds_pool_status.assert_called_once_with()
|
|
self.common._check_ds_version.assert_called_once_with()
|
|
self.common._check_ds_ability.assert_called_once_with()
|
|
self.assertIsNone(result)
|
|
|
|
def test_update_volume_stats(self):
|
|
self.common._get_pool_size = mock.Mock(return_value=(10, 100, 50))
|
|
self.common._get_pool_lun_provisioned_size = (
|
|
mock.Mock(return_value=300))
|
|
|
|
data = {
|
|
'volume_backend_name': 'DiskStation',
|
|
'vendor_name': 'Synology',
|
|
'storage_protocol': 'iscsi',
|
|
'consistencygroup_support': False,
|
|
'QoS_support': False,
|
|
'thin_provisioning_support': True,
|
|
'thick_provisioning_support': False,
|
|
'reserved_percentage': 0,
|
|
'free_capacity_gb': 10,
|
|
'total_capacity_gb': 100,
|
|
'provisioned_capacity_gb': 350,
|
|
'max_over_subscription_ratio': 20,
|
|
'target_ip_address': '10.0.0.1',
|
|
'pool_name': 'volume1',
|
|
'backend_info':
|
|
'Synology:iscsi:72003c93-2db2-4f00-a169-67c5eae86bb1'
|
|
}
|
|
|
|
result = self.common.update_volume_stats()
|
|
|
|
self.assertDictEqual(data, result)
|
|
|
|
def test_create_volume(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
out,
|
|
common.SynoAuthError(message='dont care')]))
|
|
self.common._check_lun_status_normal = (
|
|
mock.Mock(side_effect=[True, False, True]))
|
|
|
|
result = self.common.create_volume(VOLUME)
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'create',
|
|
mock.ANY,
|
|
name=VOLUME['name'],
|
|
type=self.common.CINDER_LUN,
|
|
location='/' + self.conf.synology_pool_name,
|
|
size=VOLUME['size'] * units.Gi))
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common.create_volume,
|
|
VOLUME)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common.create_volume,
|
|
VOLUME)
|
|
|
|
def test_delete_volume(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common._get_lun_uuid = mock.Mock(return_value=LUN_UUID)
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=[
|
|
out,
|
|
common.SynoLUNNotExist(message='dont care'),
|
|
common.SynoAuthError(message='dont care')]))
|
|
|
|
result = self.common.delete_volume(VOLUME)
|
|
self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'delete',
|
|
mock.ANY,
|
|
uuid=LUN_UUID))
|
|
self.assertIsNone(result)
|
|
|
|
result = self.common.delete_volume(VOLUME)
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common.delete_volume,
|
|
VOLUME)
|
|
|
|
def test_create_cloned_volume(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
new_volume = copy.deepcopy(NEW_VOLUME)
|
|
new_volume['size'] = 20
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.common._get_lun_uuid = (
|
|
mock.Mock(side_effect=[
|
|
LUN_UUID,
|
|
LUN_UUID,
|
|
LUN_UUID,
|
|
exception.InvalidParameterValue('dont care')]))
|
|
self.common.extend_volume = mock.Mock()
|
|
self.common._check_lun_status_normal = (
|
|
mock.Mock(side_effect=[True, True, False, False]))
|
|
result = self.common.create_cloned_volume(new_volume, VOLUME)
|
|
self.common._get_lun_uuid.assert_called_with(VOLUME['name'])
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'clone',
|
|
mock.ANY,
|
|
src_lun_uuid=LUN_UUID,
|
|
dst_lun_name=new_volume['name'],
|
|
is_same_pool=True,
|
|
clone_type='CINDER'))
|
|
(self.common._check_lun_status_normal.
|
|
assert_called_with(new_volume['name']))
|
|
self.common.extend_volume.assert_called_once_with(new_volume,
|
|
new_volume['size'])
|
|
self.assertIsNone(result)
|
|
|
|
new_volume['size'] = 10
|
|
result = self.common.create_cloned_volume(new_volume, VOLUME)
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common.create_cloned_volume,
|
|
new_volume,
|
|
VOLUME)
|
|
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common.create_cloned_volume,
|
|
new_volume,
|
|
VOLUME)
|
|
|
|
def test_extend_volume(self):
|
|
new_size = 20
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.common._get_lun_uuid = (
|
|
mock.Mock(side_effect=[
|
|
LUN_UUID,
|
|
exception.InvalidParameterValue('dont care')]))
|
|
|
|
result = self.common.extend_volume(VOLUME, new_size)
|
|
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'set',
|
|
mock.ANY,
|
|
uuid=LUN_UUID,
|
|
new_size=new_size * units.Gi))
|
|
self.assertIsNone(result)
|
|
self.assertRaises(exception.ExtendVolumeError,
|
|
self.common.extend_volume,
|
|
VOLUME,
|
|
new_size)
|
|
|
|
def test_update_migrated_volume(self):
|
|
expected = {
|
|
'_name_id': None
|
|
}
|
|
self.common._modify_lun_name = mock.Mock(side_effect=[None, Exception])
|
|
|
|
result = self.common.update_migrated_volume(VOLUME,
|
|
NEW_VOLUME)
|
|
|
|
self.common._modify_lun_name.assert_called_with(NEW_VOLUME['name'],
|
|
VOLUME['name'])
|
|
self.assertDictEqual(expected, result)
|
|
|
|
self.assertRaises(exception.VolumeMigrationFailed,
|
|
self.common.update_migrated_volume,
|
|
VOLUME,
|
|
NEW_VOLUME)
|
|
|
|
def test_create_snapshot(self):
|
|
expected_result = {
|
|
'metadata': {
|
|
self.common.METADATA_DS_SNAPSHOT_UUID: DS_SNAPSHOT_UUID
|
|
}
|
|
}
|
|
expected_result['metadata'].update(SNAPSHOT['metadata'])
|
|
|
|
out = {
|
|
'data': {
|
|
'snapshot_uuid': DS_SNAPSHOT_UUID,
|
|
'snapshot_id': SNAPSHOT_ID
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.common._check_snapshot_status_healthy = (
|
|
mock.Mock(side_effect=[True, False]))
|
|
|
|
result = self.common.create_snapshot(SNAPSHOT)
|
|
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'take_snapshot',
|
|
mock.ANY,
|
|
src_lun_uuid=SNAPSHOT['volume']['name'],
|
|
is_app_consistent=False,
|
|
is_locked=False,
|
|
taken_by='Cinder',
|
|
description='(Cinder) ' +
|
|
SNAPSHOT['id']))
|
|
self.assertDictEqual(expected_result, result)
|
|
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common.create_snapshot,
|
|
SNAPSHOT)
|
|
|
|
def test_create_snapshot_error(self):
|
|
out = {
|
|
'data': {
|
|
'snapshot_uuid': 1,
|
|
'snapshot_id': SNAPSHOT_ID
|
|
},
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
|
|
self.assertRaises(exception.MalformedResponse,
|
|
self.common.create_snapshot,
|
|
SNAPSHOT)
|
|
|
|
self.common.exec_webapi = (
|
|
mock.Mock(side_effect=common.SynoAuthError(reason='dont care')))
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common.create_snapshot,
|
|
SNAPSHOT)
|
|
|
|
def test_delete_snapshot(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.common._get_metadata_value = (
|
|
mock.Mock(side_effect=[
|
|
DS_SNAPSHOT_UUID,
|
|
exception.SnapshotMetadataNotFound(message='dont care'),
|
|
exception.MetadataAbsent]))
|
|
|
|
result = self.common.delete_snapshot(SNAPSHOT)
|
|
(self.common._get_metadata_value.
|
|
assert_called_with(SNAPSHOT,
|
|
self.common.METADATA_DS_SNAPSHOT_UUID))
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'delete_snapshot',
|
|
mock.ANY,
|
|
snapshot_uuid=DS_SNAPSHOT_UUID,
|
|
deleted_by='Cinder'))
|
|
self.assertIsNone(result)
|
|
|
|
result = self.common.delete_snapshot(SNAPSHOT)
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(exception.MetadataAbsent,
|
|
self.common.delete_snapshot,
|
|
SNAPSHOT)
|
|
|
|
def test_create_volume_from_snapshot(self):
|
|
out = {
|
|
'success': True
|
|
}
|
|
new_volume = copy.deepcopy(NEW_VOLUME)
|
|
new_volume['size'] = 20
|
|
self.common.exec_webapi = mock.Mock(return_value=out)
|
|
self.common._get_metadata_value = (
|
|
mock.Mock(side_effect=[
|
|
DS_SNAPSHOT_UUID,
|
|
DS_SNAPSHOT_UUID,
|
|
exception.SnapshotMetadataNotFound(message='dont care'),
|
|
common.SynoAuthError(message='dont care')]))
|
|
self.common._check_lun_status_normal = (
|
|
mock.Mock(side_effect=[True, False, True, True]))
|
|
self.common.extend_volume = mock.Mock()
|
|
|
|
result = self.common.create_volume_from_snapshot(new_volume, SNAPSHOT)
|
|
|
|
(self.common._get_metadata_value.
|
|
assert_called_with(SNAPSHOT,
|
|
self.common.METADATA_DS_SNAPSHOT_UUID))
|
|
(self.common.exec_webapi.
|
|
assert_called_with('SYNO.Core.ISCSI.LUN',
|
|
'clone_snapshot',
|
|
mock.ANY,
|
|
src_lun_uuid=SNAPSHOT['volume']['name'],
|
|
snapshot_uuid=DS_SNAPSHOT_UUID,
|
|
cloned_lun_name=new_volume['name'],
|
|
clone_type='CINDER'))
|
|
self.common.extend_volume.assert_called_once_with(new_volume,
|
|
new_volume['size'])
|
|
self.assertIsNone(result)
|
|
|
|
self.assertRaises(exception.VolumeDriverException,
|
|
self.common.create_volume_from_snapshot,
|
|
new_volume,
|
|
SNAPSHOT)
|
|
|
|
self.assertRaises(exception.SnapshotMetadataNotFound,
|
|
self.common.create_volume_from_snapshot,
|
|
new_volume,
|
|
SNAPSHOT)
|
|
|
|
self.assertRaises(common.SynoAuthError,
|
|
self.common.create_volume_from_snapshot,
|
|
new_volume,
|
|
SNAPSHOT)
|
|
|
|
def test_get_iqn_and_trgid(self):
|
|
location = '%s:3260,%d %s 1' % (IP, 1, IQN)
|
|
|
|
result = self.common.get_iqn_and_trgid(location)
|
|
|
|
self.assertEqual((IQN, 1), result)
|
|
|
|
location = ''
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common.get_iqn_and_trgid,
|
|
location)
|
|
|
|
location = 'BADINPUT'
|
|
self.assertRaises(exception.InvalidInput,
|
|
self.common.get_iqn_and_trgid,
|
|
location)
|
|
|
|
location = '%s:3260 %s 1' % (IP, IQN)
|
|
self.assertRaises(exception.InvalidInput,
|
|
self.common.get_iqn_and_trgid,
|
|
location)
|
|
|
|
def test_get_iscsi_properties(self):
|
|
volume = copy.deepcopy(VOLUME)
|
|
iscsi_properties = {
|
|
'target_discovered': False,
|
|
'target_iqn': IQN,
|
|
'target_portal': '%s:3260' % IP,
|
|
'volume_id': VOLUME['id'],
|
|
'access_mode': 'rw',
|
|
'discard': False,
|
|
'auth_method': 'CHAP',
|
|
'auth_username': CHAP_AUTH_USERNAME,
|
|
'auth_password': CHAP_AUTH_PASSWORD
|
|
}
|
|
self.common.get_ip = mock.Mock(return_value=IP)
|
|
self.conf.safe_get = mock.Mock(return_value=[])
|
|
|
|
result = self.common.get_iscsi_properties(volume)
|
|
self.assertDictEqual(iscsi_properties, result)
|
|
|
|
volume['provider_location'] = ''
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common.get_iscsi_properties,
|
|
volume)
|
|
|
|
def test_get_iscsi_properties_multipath(self):
|
|
volume = copy.deepcopy(VOLUME)
|
|
iscsi_properties = {
|
|
'target_discovered': False,
|
|
'target_iqn': IQN,
|
|
'target_iqns': [IQN] * 3,
|
|
'target_lun': 0,
|
|
'target_luns': [0] * 3,
|
|
'target_portal': '%s:3260' % IP,
|
|
'target_portals':
|
|
['%s:3260' % IP, '10.0.0.2:3260', '10.0.0.3:3260'],
|
|
'volume_id': VOLUME['id'],
|
|
'access_mode': 'rw',
|
|
'discard': False,
|
|
'auth_method': 'CHAP',
|
|
'auth_username': CHAP_AUTH_USERNAME,
|
|
'auth_password': CHAP_AUTH_PASSWORD
|
|
}
|
|
self.common.get_ip = mock.Mock(return_value=IP)
|
|
self.conf.safe_get = mock.Mock(return_value=['10.0.0.2', '10.0.0.3'])
|
|
|
|
result = self.common.get_iscsi_properties(volume)
|
|
self.assertDictEqual(iscsi_properties, result)
|
|
|
|
volume['provider_location'] = ''
|
|
self.assertRaises(exception.InvalidParameterValue,
|
|
self.common.get_iscsi_properties,
|
|
volume)
|
|
|
|
def test_get_iscsi_properties_without_chap(self):
|
|
volume = copy.deepcopy(VOLUME)
|
|
iscsi_properties = {
|
|
'target_discovered': False,
|
|
'target_iqn': IQN,
|
|
'target_portal': '%s:3260' % IP,
|
|
'volume_id': VOLUME['id'],
|
|
'access_mode': 'rw',
|
|
'discard': False
|
|
}
|
|
self.common.get_ip = mock.Mock(return_value=IP)
|
|
self.conf.safe_get = mock.Mock(return_value=[])
|
|
|
|
volume['provider_auth'] = 'abcde'
|
|
result = self.common.get_iscsi_properties(volume)
|
|
self.assertDictEqual(iscsi_properties, result)
|
|
|
|
volume['provider_auth'] = ''
|
|
result = self.common.get_iscsi_properties(volume)
|
|
self.assertDictEqual(iscsi_properties, result)
|
|
|
|
del volume['provider_auth']
|
|
result = self.common.get_iscsi_properties(volume)
|
|
self.assertDictEqual(iscsi_properties, result)
|
|
|
|
def test_create_iscsi_export(self):
|
|
self.common._target_create = (
|
|
mock.Mock(return_value=(IQN, TRG_ID, VOLUME['provider_auth'])))
|
|
self.common._lun_map_target = mock.Mock()
|
|
|
|
iqn, trg_id, provider_auth = (
|
|
self.common.create_iscsi_export(VOLUME['name'], VOLUME['id']))
|
|
|
|
self.common._target_create.assert_called_with(VOLUME['id'])
|
|
self.common._lun_map_target.assert_called_with(VOLUME['name'], trg_id)
|
|
self.assertEqual((IQN, TRG_ID, VOLUME['provider_auth']),
|
|
(iqn, trg_id, provider_auth))
|
|
|
|
def test_remove_iscsi_export(self):
|
|
trg_id = TRG_ID
|
|
self.common._lun_unmap_target = mock.Mock()
|
|
self.common._target_delete = mock.Mock()
|
|
|
|
result = self.common.remove_iscsi_export(VOLUME['name'], trg_id)
|
|
|
|
self.assertIsNone(result)
|
|
self.common._lun_unmap_target.assert_called_with(VOLUME['name'],
|
|
TRG_ID)
|
|
self.common._target_delete.assert_called_with(TRG_ID)
|