Merge "Add sandstone iscsi driver."
This commit is contained in:
commit
6221ff7887
|
@ -141,6 +141,8 @@ from cinder.volume.drivers import remotefs as cinder_volume_drivers_remotefs
|
|||
from cinder.volume.drivers.san.hp import hpmsa_common as \
|
||||
cinder_volume_drivers_san_hp_hpmsacommon
|
||||
from cinder.volume.drivers.san import san as cinder_volume_drivers_san_san
|
||||
from cinder.volume.drivers.sandstone import sds_driver as \
|
||||
cinder_volume_drivers_sandstone_sdsdriver
|
||||
from cinder.volume.drivers import solidfire as cinder_volume_drivers_solidfire
|
||||
from cinder.volume.drivers import storpool as cinder_volume_drivers_storpool
|
||||
from cinder.volume.drivers.stx import common as \
|
||||
|
@ -258,6 +260,7 @@ def list_opts():
|
|||
instorage_mcs_opts,
|
||||
cinder_volume_drivers_inspur_instorage_instorageiscsi.
|
||||
instorage_mcs_iscsi_opts,
|
||||
cinder_volume_drivers_sandstone_sdsdriver.sds_opts,
|
||||
cinder_volume_drivers_veritas_access_veritasiscsi.VA_VOL_OPTS,
|
||||
cinder_volume_manager.volume_manager_opts,
|
||||
cinder_wsgi_eventletserver.socket_opts,
|
||||
|
|
|
@ -0,0 +1,560 @@
|
|||
# Copyright (c) 2019 SandStone data Technologies 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.
|
||||
"""Unittest for sds_client."""
|
||||
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
import requests
|
||||
|
||||
from cinder import test
|
||||
from cinder.tests.unit.volume.drivers.sandstone import test_utils
|
||||
from cinder.volume.drivers.sandstone import sds_client
|
||||
|
||||
|
||||
class FakeSession(test_utils.FakeBaseSession):
|
||||
"""Fake request session."""
|
||||
|
||||
method_map = {
|
||||
'post': {
|
||||
'capacity': {'data': {'capacity_bytes': 1024, 'free_bytes': 1024}},
|
||||
'pool/list': {'data': [{'status': {'progress': 100},
|
||||
'pool_name': 'fake_pool',
|
||||
'realname': 'fake_pool',
|
||||
'storage_policy': 'fake_replicate',
|
||||
'domain_name': 'fake_domain',
|
||||
'pool_id': 3,
|
||||
'policy_type': 'replicated',
|
||||
'size': 2}]},
|
||||
'resource/initiator/list': {'data': {
|
||||
'results': [{'iqn': 'fake_iqn',
|
||||
'type': 'iscsi'}]}},
|
||||
'resource/target/get_target_acl_list': {'data': {
|
||||
'results': [{'autodiscovery': 'yes',
|
||||
'name': 'fake_iqn',
|
||||
'approved': 'yes',
|
||||
'manual': 'no',
|
||||
'ip': ''}]}},
|
||||
'block/gateway/server/list': {'data': [{
|
||||
'networks': [{'hostid': 'node0001',
|
||||
'address': '1.1.1.1',
|
||||
'type': 'iSCSI'}]}]},
|
||||
'resource/target/list': {'data': {
|
||||
'results': [{'status': 'fake_state',
|
||||
'node': ['node0001'],
|
||||
'name': 'fake_target',
|
||||
'type': 'iSCSI',
|
||||
'gateway': [{
|
||||
'hostid': 'node0001',
|
||||
'networks': [{
|
||||
'hostid': 'node0001',
|
||||
'type': 'iSCSI',
|
||||
'address': 'fake_address'}],
|
||||
'hostip': 'fake_hostip'}]}]}},
|
||||
'resource/target/get_chap_list': {'data': [{
|
||||
'user': 'fake_chapuser',
|
||||
'level': 'level1'}]},
|
||||
'resource/target/get_luns': {'data': {
|
||||
'results': [{'lid': 1,
|
||||
'name': 'fake_lun',
|
||||
'pool_id': 1}]}},
|
||||
'resource/lun/list': {'data': {
|
||||
'results': [{'volumeName': 'fake_lun',
|
||||
'pool_id': 1,
|
||||
'capacity_bytes': 1024}]}},
|
||||
'delaytask/list': {'data': {
|
||||
'results': [{'status': 'completed',
|
||||
'run_status': 'completed',
|
||||
'executor': 'LunFlatten',
|
||||
'progress': 100,
|
||||
'parameter': {'pool_id': 1,
|
||||
'lun_name': 'fake_lun'}}]}},
|
||||
'resource/snapshot/list': {'data': {
|
||||
'results': [{'snapName': 'fake_snapshot',
|
||||
'lunName': 'fake_lun'}]}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSdsclient(test.TestCase):
|
||||
"""Testcase sds client."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup."""
|
||||
super(TestSdsclient, self).setUp()
|
||||
self.mock_object(requests, 'Session', FakeSession)
|
||||
self.client = sds_client.RestCmd('192.168.200.100',
|
||||
'fake_user',
|
||||
'fake_password',
|
||||
True)
|
||||
self.client.login()
|
||||
|
||||
def test_login(self):
|
||||
"""Test login and check headers."""
|
||||
self.assertEqual('https://192.168.200.100',
|
||||
self.client.session.headers['Referer'])
|
||||
self.assertEqual('fake_token',
|
||||
self.client.session.headers['X-XSRF-Token'])
|
||||
self.assertEqual('XSRF-TOKEN=fake_token; username=fake_user; '
|
||||
'sdsom_sessionid=fake_session',
|
||||
self.client.session.headers['Cookie'])
|
||||
|
||||
def test_logout(self):
|
||||
"""Test logout."""
|
||||
retval = self.client.logout()
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_capacity_info(self):
|
||||
"""Test query cluster capacity."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_capacity_info()
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'capacity')
|
||||
self.assertDictEqual({'capacity_bytes': 1024, 'free_bytes': 1024},
|
||||
retval)
|
||||
|
||||
def test_query_pool_info(self):
|
||||
"""Test query pool status."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_pool_info()
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'pool/list')
|
||||
self.assertListEqual([{'status': {'progress': 100},
|
||||
'realname': 'fake_pool',
|
||||
'pool_name': 'fake_pool',
|
||||
'storage_policy': 'fake_replicate',
|
||||
'domain_name': 'fake_domain',
|
||||
'pool_id': 3,
|
||||
'policy_type': 'replicated',
|
||||
'size': 2}], retval)
|
||||
|
||||
def test_create_initiator(self):
|
||||
"""Test create initiator."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.create_initiator(
|
||||
initiator_name='fake_iqn')
|
||||
data = json.dumps(
|
||||
{'iqn': 'fake_iqn', 'type': 'iSCSI',
|
||||
'remark': 'Cinder iSCSI'})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/initiator/create', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd,
|
||||
"_judge_delaytask_status")
|
||||
def test_add_initiator_to_target(self,
|
||||
mock__judge_delaytask_status):
|
||||
"""Test add initiator to target."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
retval = self.client.add_initiator_to_target(
|
||||
target_name='fake_target',
|
||||
initiator_name='fake_iqn')
|
||||
data = json.dumps(
|
||||
{'targetName': 'fake_target',
|
||||
'iqns': [{'ip': '', 'iqn': 'fake_iqn'}]})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/add_initiator_to_target', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_initiator_by_name(self):
|
||||
"""Test query initiator exist or not."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_initiator_by_name(
|
||||
initiator_name='fake_iqn')
|
||||
data = json.dumps(
|
||||
{'initiatorMark': '', 'pageno': 1,
|
||||
'pagesize': 1000, 'type': 'iSCSI'})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/initiator/list', data=data)
|
||||
self.assertDictEqual({'iqn': 'fake_iqn',
|
||||
'type': 'iscsi'}, retval)
|
||||
|
||||
def test_query_target_initiatoracl(self):
|
||||
"""Test query target related initiator info."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_target_initiatoracl(
|
||||
target_name='fake_target',
|
||||
initiator_name='fake_iqn')
|
||||
data = json.dumps(
|
||||
{'pageno': 1, 'pagesize': 1000,
|
||||
'targetName': 'fake_target'})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/get_target_acl_list', data=data)
|
||||
self.assertListEqual([{'autodiscovery': 'yes',
|
||||
'name': 'fake_iqn',
|
||||
'approved': 'yes',
|
||||
'manual': 'no',
|
||||
'ip': ''}], retval)
|
||||
|
||||
def test_query_node_by_targetips(self):
|
||||
"""Test query node id and node ip, relation dict."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_node_by_targetips(
|
||||
target_ips=['1.1.1.1'])
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'block/gateway/server/list')
|
||||
self.assertDictEqual({'1.1.1.1': 'node0001'}, retval)
|
||||
|
||||
def test_query_target_by_name(self):
|
||||
"""Test query target exist or not."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_target_by_name(
|
||||
target_name='fake_target')
|
||||
data = json.dumps(
|
||||
{'pageno': 1, 'pagesize': 1000,
|
||||
"thirdParty": [0, 1],
|
||||
"targetMark": ""})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/list', data=data)
|
||||
self.assertDictEqual({
|
||||
'status': 'fake_state',
|
||||
'node': ['node0001'],
|
||||
'name': 'fake_target',
|
||||
'type': 'iSCSI',
|
||||
'gateway': [{'hostid': 'node0001',
|
||||
'networks': [{'hostid': 'node0001',
|
||||
'type': 'iSCSI',
|
||||
'address': 'fake_address'}],
|
||||
'hostip': 'fake_hostip'}]}, retval)
|
||||
|
||||
def test_create_target(self):
|
||||
"""Test create target."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.create_target(target_name='fake_target',
|
||||
targetip_to_hostid=
|
||||
{'1.1.1.1': 'node0001',
|
||||
'1.1.1.2': 'node0002',
|
||||
'1.1.1.3': 'node0003'})
|
||||
tip_to_hid = {'1.1.1.1': 'node0001',
|
||||
'1.1.1.2': 'node0002',
|
||||
'1.1.1.3': 'node0003'}
|
||||
data = json.dumps(
|
||||
{"type": "iSCSI", "readOnly": 0,
|
||||
"thirdParty": 1, "targetName": "fake_target",
|
||||
"networks": [{"hostid": host_id, "address": address}
|
||||
for address, host_id
|
||||
in tip_to_hid.items()]})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/create', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_add_chap_by_target(self):
|
||||
"""Test add chap to target."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.add_chap_by_target(
|
||||
target_name='fake_target',
|
||||
username='fake_chapuser',
|
||||
password='fake_chappassword')
|
||||
data = json.dumps(
|
||||
{"password": "fake_chappassword",
|
||||
"user": "fake_chapuser", "targetName": "fake_target"})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/add_chap', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_chapinfo_by_target(self):
|
||||
"""Test query target chap info."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_chapinfo_by_target(
|
||||
target_name='fake_target',
|
||||
username='fake_chapuser')
|
||||
data = json.dumps({"targetName": "fake_target"})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/get_chap_list', data=data)
|
||||
self.assertDictEqual({'user': 'fake_chapuser',
|
||||
'level': 'level1'}, retval)
|
||||
|
||||
def test_create_lun(self):
|
||||
"""Test create lun."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.create_lun(capacity_bytes=1024,
|
||||
poolid=1,
|
||||
volume_name='fake_lun')
|
||||
data = json.dumps({"capacity_bytes": 1024,
|
||||
"poolId": 1, "priority": "normal",
|
||||
"qosSettings": {}, "volumeName": 'fake_lun'})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/lun/add', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_delete_lun(self):
|
||||
"""Test delete lun."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.delete_lun(poolid=1,
|
||||
volume_name='fake_lun')
|
||||
data = json.dumps({"delayTime": 0, "volumeNameList": [{
|
||||
"poolId": 1,
|
||||
"volumeName": "fake_lun"}]})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/lun/batch_delete', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_extend_lun(self):
|
||||
"""Test resize lun."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.extend_lun(capacity_bytes=2048,
|
||||
poolid=1,
|
||||
volume_name='fake_lun')
|
||||
data = json.dumps({"capacity_bytes": 2048,
|
||||
"poolId": 1,
|
||||
"volumeName": 'fake_lun'})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/lun/resize', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
@mock.patch.object(sds_client.RestCmd, "query_lun_by_name")
|
||||
def test_unmap_lun(self, mock_query_lun_by_name,
|
||||
mock__judge_delaytask_status):
|
||||
"""Test unmap lun from target."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
lun_uuid = "c5c8533c-4ce0-11ea-bc01-005056a736f8"
|
||||
mock_query_lun_by_name.return_value = {'uuid': lun_uuid}
|
||||
retval = self.client.unmap_lun(target_name='fake_target',
|
||||
poolid=1,
|
||||
volume_name='fake_lun',
|
||||
pool_name='fake_pool')
|
||||
data = json.dumps({"targetName": "fake_target",
|
||||
"targetLunList": [lun_uuid],
|
||||
"targetSnapList": []})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/unmap_luns', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
@mock.patch.object(sds_client.RestCmd, "query_lun_by_name")
|
||||
def test_mapping_lun(self, mock_query_lun_by_name,
|
||||
mock__judge_delaytask_status):
|
||||
"""Test map lun to target."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
lun_uuid = "c5c8533c-4ce0-11ea-bc01-005056a736f8"
|
||||
mock_query_lun_by_name.return_value = {'uuid': lun_uuid}
|
||||
retval = self.client.mapping_lun(
|
||||
target_name='fake_target',
|
||||
poolid=1,
|
||||
volume_name='fake_lun',
|
||||
pool_name='fake_pool')
|
||||
data = json.dumps(
|
||||
{"targetName": 'fake_target',
|
||||
"targetLunList": [lun_uuid],
|
||||
"targetSnapList": []})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/map_luns', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_target_lunacl(self):
|
||||
"""Test query target related lun info."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_target_lunacl(target_name='fake_target',
|
||||
poolid=1,
|
||||
volume_name='fake_lun')
|
||||
data = json.dumps({"pageno": 1, "pagesize": 1000,
|
||||
"pools": [1],
|
||||
"targetName": "fake_target"})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/target/get_luns', data=data)
|
||||
self.assertEqual(1, retval)
|
||||
|
||||
def test_query_lun_by_name(self):
|
||||
"""Test query lun exist or not."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_lun_by_name(
|
||||
volume_name='fake_lun',
|
||||
poolid=1)
|
||||
data = json.dumps(
|
||||
{"pageno": 1, "pagesize": 1000, "volumeMark": "fake_lun",
|
||||
"sortType": "time", "sortOrder": "desc",
|
||||
"pools": [1],
|
||||
"thirdParty": [0, 1]})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/lun/list', data=data)
|
||||
self.assertDictEqual({'volumeName': 'fake_lun',
|
||||
'pool_id': 1,
|
||||
'capacity_bytes': 1024}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
def test_create_snapshot(self, mock__judge_delaytask_status):
|
||||
"""Test create snapshot."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
retval = self.client.create_snapshot(poolid=1,
|
||||
volume_name='fake_lun',
|
||||
snapshot_name='fake_snapshot')
|
||||
data = json.dumps(
|
||||
{"lunName": "fake_lun",
|
||||
"poolId": 1,
|
||||
"remark": "Cinder iSCSI snapshot.",
|
||||
"snapName": "fake_snapshot"})
|
||||
mocker.assert_called_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/snapshot/add', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
def test_delete_snapshot(self, mock__judge_delaytask_status):
|
||||
"""Test delete snapshot."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
retval = self.client.delete_snapshot(poolid=1,
|
||||
volume_name='fake_lun',
|
||||
snapshot_name='fake_snapshot')
|
||||
data = json.dumps(
|
||||
{"lunName": "fake_lun", "poolId": 1,
|
||||
"snapName": "fake_snapshot"})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/snapshot/delete', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "flatten_lun")
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
def test_create_lun_from_snapshot(self, mock__judge_delaytask_status,
|
||||
mock_flatten_lun):
|
||||
"""Test create lun from snapshot."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
mock_flatten_lun.return_value = None
|
||||
retval = self.client.create_lun_from_snapshot(
|
||||
snapshot_name='fake_snapshot',
|
||||
src_volume_name='fake_src_lun',
|
||||
poolid=1,
|
||||
dst_volume_name='fake_dst_lun')
|
||||
data = json.dumps(
|
||||
{"snapshot": {"poolId": 1,
|
||||
"lunName": "fake_src_lun",
|
||||
"snapName": "fake_snapshot"},
|
||||
"cloneLun": {"lunName": "fake_dst_lun",
|
||||
"poolId": 1}})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/snapshot/clone', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "_judge_delaytask_status")
|
||||
def test_flatten_lun(self, mock__judge_delaytask_status):
|
||||
"""Test flatten lun."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
mock__judge_delaytask_status.return_value = None
|
||||
retval = self.client.flatten_lun(volume_name='fake_lun',
|
||||
poolid=1)
|
||||
data = json.dumps(
|
||||
{"poolId": 1,
|
||||
"volumeName": "fake_lun"})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/lun/flatten', data=data)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_flatten_lun_process(self):
|
||||
"""Test query flatten process."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_flatten_lun_process(
|
||||
poolid=1,
|
||||
volume_name='fake_lun')
|
||||
data = json.dumps({"pageno": 1, "pagesize": 20})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/om/'
|
||||
'delaytask/list', data=data)
|
||||
self.assertDictEqual({'status': 'completed',
|
||||
'run_status': 'completed',
|
||||
'executor': 'LunFlatten',
|
||||
'progress': 100,
|
||||
'parameter': {'pool_id': 1,
|
||||
'lun_name': 'fake_lun'}},
|
||||
retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, "create_snapshot")
|
||||
@mock.patch.object(sds_client.RestCmd, "create_lun_from_snapshot")
|
||||
@mock.patch.object(sds_client.RestCmd, "flatten_lun")
|
||||
@mock.patch.object(sds_client.RestCmd, "delete_snapshot")
|
||||
def test_create_lun_from_lun(self, mock_delete_snapshot,
|
||||
mock_flatten_lun,
|
||||
mock_create_lun_from_snapshot,
|
||||
mock_create_snapshot):
|
||||
"""Test create clone lun."""
|
||||
self.client = sds_client.RestCmd(
|
||||
"https://192.168.200.100",
|
||||
"fake_user", "fake_password", True)
|
||||
mock_create_snapshot.return_value = {'success': 1}
|
||||
mock_create_lun_from_snapshot.return_value = {'success': 1}
|
||||
mock_flatten_lun.return_value = {'success': 1}
|
||||
mock_delete_snapshot.return_value = {'success': 1}
|
||||
retval = self.client.create_lun_from_lun(
|
||||
dst_volume_name='fake_dst_lun',
|
||||
poolid=1,
|
||||
src_volume_name='fake_src_lun')
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_query_snapshot_by_name(self):
|
||||
"""Test query snapshot exist or not."""
|
||||
with mock.patch.object(self.client.session, 'post',
|
||||
wraps=self.client.session.post) as mocker:
|
||||
retval = self.client.query_snapshot_by_name(
|
||||
volume_name='fake_lun',
|
||||
poolid=1,
|
||||
snapshot_name='fake_snapshot')
|
||||
data = json.dumps(
|
||||
{"lunName": "fake_lun", "pageno": 1,
|
||||
"pagesize": 1000, "poolId": 1,
|
||||
"snapMark": ""})
|
||||
mocker.assert_called_once_with(
|
||||
'https://192.168.200.100/api/storage/'
|
||||
'resource/snapshot/list', data=data)
|
||||
self.assertListEqual([{'snapName': 'fake_snapshot',
|
||||
'lunName': 'fake_lun'}], retval)
|
|
@ -0,0 +1,455 @@
|
|||
# Copyright (c) 2019 SandStone data Technologies 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.
|
||||
"""Unittest for sds_client."""
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder import objects
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as config
|
||||
from cinder.volume.drivers.san import san
|
||||
from cinder.volume.drivers.sandstone import sds_client
|
||||
from cinder.volume.drivers.sandstone import sds_driver
|
||||
|
||||
|
||||
class FakeSdsBaseDriver(sds_driver.SdsBaseDriver):
|
||||
"""Fake sds base driver."""
|
||||
|
||||
def __init__(self):
|
||||
"""Init conf client pool sds_client."""
|
||||
self.configuration = config.Configuration(None)
|
||||
self.configuration.append_config_values(sds_driver.sds_opts)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
self.configuration.suppress_requests_ssl_warnings = True
|
||||
self.client = None
|
||||
self.poolid = 1
|
||||
self.VERSION = '1.0'
|
||||
self.address = "192.168.200.100"
|
||||
self.user = "fake_user"
|
||||
self.password = "fake_password"
|
||||
self.pool = "fake_pool_name"
|
||||
self.iscsi_info = {"iqn.1994-05.com.redhat:899c5f9d15d":
|
||||
"1.1.1.1,1.1.1.2,1.1.1.3"}
|
||||
self.default_target_ips = ["1.1.1.1", "1.1.1.2", "1.1.1.3"]
|
||||
self.default_chap_info = "1234567891234,123456789123"
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestSdsBaseDriver(test.TestCase):
|
||||
"""Testcase sds base driver."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup."""
|
||||
super(TestSdsBaseDriver, self).setUp()
|
||||
self.fake_driver = FakeSdsBaseDriver()
|
||||
self.fake_driver.client = sds_client.RestCmd('192.168.200.100',
|
||||
'fake_user',
|
||||
'fake_password',
|
||||
True)
|
||||
|
||||
# @mock.patch.object(sds_client.RestCmd, 'login')
|
||||
def test_do_setup(self):
|
||||
"""Do setup."""
|
||||
self.fake_driver.client = sds_client.RestCmd(
|
||||
'fake_rest_ip', 'user', 'password', True)
|
||||
self.fake_driver.configuration.san_ip = 'fake_rest_ip'
|
||||
self.fake_driver.configuration.san_login = 'fake_san_user'
|
||||
self.fake_driver.configuration.san_password = 'fake_san_password'
|
||||
self.fake_driver.do_setup('context')
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_pool_info')
|
||||
@mock.patch.object(sds_client.RestCmd, 'get_poolid_from_poolname')
|
||||
@mock.patch.object(sds_client.RestCmd, 'login')
|
||||
def test_check_for_setup_error(self, mock_login,
|
||||
mock_get_poolid_from_poolname,
|
||||
mock_query_pool_info):
|
||||
"""Test pool status health or not."""
|
||||
result1 = [
|
||||
{'status': {'progress': 33, 'state': ['degraded'], 'flags': 4},
|
||||
'pool_name': 'fake_pool_name', 'used': 1792950890,
|
||||
'display_name': 'data', 'replicated_size': 2,
|
||||
'storage_policy': '2', 'domain_name': 'sandstone',
|
||||
'pool_id': 3, 'min_size': 1, 'erasure_code_profile': '',
|
||||
'policy_type': 'replicated', 'rule_id': 1,
|
||||
'size': 2},
|
||||
{'status': {'progress': 33, 'state': ['degraded'], 'flags': 4},
|
||||
'pool_name': 'vms1', 'used': 1792950890,
|
||||
'display_name': 'data', 'replicated_size': 2,
|
||||
'storage_policy': '2', 'domain_name': 'sandstone',
|
||||
'pool_id': 3, 'min_size': 1, 'erasure_code_profile': '',
|
||||
'policy_type': 'replicated', 'rule_id': 1,
|
||||
'size': 2}]
|
||||
result2 = [
|
||||
{'status': {'progress': 33, 'state': ['degraded'], 'flags': 4},
|
||||
'pool_name': 'vms', 'used': 1792950890,
|
||||
'display_name': 'data', 'replicated_size': 2,
|
||||
'storage_policy': '2', 'domain_name': 'sandstone',
|
||||
'pool_id': 3, 'min_size': 1, 'erasure_code_profile': '',
|
||||
'policy_type': 'replicated', 'rule_id': 1,
|
||||
'size': 2},
|
||||
{'status': {'progress': 33, 'state': ['degraded'], 'flags': 4},
|
||||
'pool_name': 'vms1', 'used': 1792950890,
|
||||
'display_name': 'data', 'replicated_size': 2,
|
||||
'storage_policy': '2', 'domain_name': 'sandstone',
|
||||
'pool_id': 3, 'min_size': 1, 'erasure_code_profile': '',
|
||||
'policy_type': 'replicated', 'rule_id': 1,
|
||||
'size': 2}]
|
||||
|
||||
mock_login.return_value = {"success": 1}
|
||||
mock_get_poolid_from_poolname.return_value = (
|
||||
{"fake_pool_name": 3})
|
||||
mock_query_pool_info.return_value = result1
|
||||
retval = self.fake_driver.check_for_setup_error()
|
||||
self.assertIsNone(retval)
|
||||
mock_query_pool_info.return_value = result2
|
||||
try:
|
||||
self.fake_driver.check_for_setup_error()
|
||||
except Exception as e:
|
||||
self.assertEqual(exception.InvalidInput, type(e))
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_capacity_info')
|
||||
def test__update_volume_stats(self, mock_query_capacity_info):
|
||||
"""Get cluster capacity."""
|
||||
result1 = {
|
||||
"capacity_bytes": 2 * units.Gi,
|
||||
"free_bytes": units.Gi
|
||||
}
|
||||
mock_query_capacity_info.return_value = result1
|
||||
retval = self.fake_driver._update_volume_stats(
|
||||
pool_name="fake_pool_name")
|
||||
self.assertDictEqual(
|
||||
{"pools": [dict(
|
||||
pool_name="fake_pool_name",
|
||||
vendor_name = 'SandStone USP',
|
||||
driver_version = self.fake_driver.VERSION,
|
||||
total_capacity_gb=2.0,
|
||||
free_capacity_gb=1.0,
|
||||
QoS_support=True,
|
||||
thin_provisioning_support=True,
|
||||
multiattach=False,)
|
||||
]}, retval)
|
||||
mock_query_capacity_info.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(sds_driver.SdsBaseDriver, 'get_volume_stats')
|
||||
def test_get_volume_stats(self, mock_get_volume_stats):
|
||||
"""Get cluster capacitys."""
|
||||
result1 = {"pool": dict(
|
||||
pool_name="fake_pool_name",
|
||||
total_capacity_gb=2.0,
|
||||
free_capacity_gb=1.0,
|
||||
QoS_support=True,
|
||||
thin_provisioning_support=True,
|
||||
multiattach=False,)}
|
||||
mock_get_volume_stats.return_value = result1
|
||||
retval = self.fake_driver.get_volume_stats()
|
||||
self.assertDictEqual(
|
||||
{"pool": dict(
|
||||
pool_name="fake_pool_name",
|
||||
total_capacity_gb=2.0,
|
||||
free_capacity_gb=1.0,
|
||||
QoS_support=True,
|
||||
thin_provisioning_support=True,
|
||||
multiattach=False,
|
||||
)}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_lun')
|
||||
def test_create_volume(self, mock_create_lun):
|
||||
"""Test create volume."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
mock_create_lun.return_value = {'success': 1}
|
||||
retval = self.fake_driver.create_volume(volume=volume)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'delete_lun')
|
||||
def test_delete_volume(self, mock_delete_):
|
||||
"""Test delete volume."""
|
||||
mock_delete_.return_value = {'success': 1}
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
retval = self.fake_driver.delete_volume(volume)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'extend_lun')
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_lun_from_snapshot')
|
||||
def test_create_volume_from_snapshot(self, mock_lun_from_snapshot,
|
||||
mock_extend_lun):
|
||||
"""Test create new volume from snapshot of src volume."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
snapshot = objects.Snapshot(
|
||||
id=uuid.uuid4(), volume_size=2, volume=volume)
|
||||
mock_lun_from_snapshot.return_value = {'success': 1}
|
||||
mock_extend_lun.return_value = {'success': 1}
|
||||
retval = self.fake_driver.create_volume_from_snapshot(volume, snapshot)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'extend_lun')
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_lun_from_lun')
|
||||
@mock.patch.object(sds_driver.SdsBaseDriver, '_check_volume_exist')
|
||||
def test_create_cloned_volume(self, mock__check_volume_exist,
|
||||
mock_create_lun_from_lun,
|
||||
mock_extend_lun):
|
||||
"""Test create clone volume."""
|
||||
mock__check_volume_exist.return_value = True
|
||||
mock_create_lun_from_lun.return_value = {'success': 1}
|
||||
mock_extend_lun.return_value = {'success': 1}
|
||||
dst_volume = objects.Volume(_name_id=uuid.uuid4(), size=2)
|
||||
src_volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
retval = self.fake_driver.create_cloned_volume(dst_volume, src_volume)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_lun_by_name')
|
||||
def test__check_volume_exist(self, mock_query_lun_by_name):
|
||||
"""Test volume exist or not."""
|
||||
mock_query_lun_by_name.return_value = {'success': 1}
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
retval = self.fake_driver._check_volume_exist(volume)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'extend_lun')
|
||||
@mock.patch.object(sds_driver.SdsBaseDriver, '_check_volume_exist')
|
||||
def test_extend_volume(self, mock__check_volume_exist, mock_extend_lun):
|
||||
"""Test resize volume."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
new_size = 3
|
||||
mock__check_volume_exist.return_value = {
|
||||
'capacity_bytes': units.Gi * 1}
|
||||
mock_extend_lun.return_value = {'success': 1}
|
||||
retval = self.fake_driver.extend_volume(volume, new_size)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_snapshot')
|
||||
def test_create_snapshot(self, mock_create_snapshot):
|
||||
"""Test create snapshot of volume."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
snapshot = objects.Snapshot(
|
||||
id=uuid.uuid4(), volume_size=2, volume=volume)
|
||||
mock_create_snapshot.return_value = {'success': 1}
|
||||
retval = self.fake_driver.create_snapshot(snapshot)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_snapshot_by_name')
|
||||
def test__check_snapshot_exist(self, mock_query_snapshot_by_name):
|
||||
"""Test snapshot exist or not."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
snapshot = objects.Snapshot(
|
||||
id=uuid.uuid4(), volume_size=2, volume=volume)
|
||||
mock_query_snapshot_by_name.return_value = {'success': 1}
|
||||
retval = self.fake_driver._check_snapshot_exist(snapshot)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'delete_snapshot')
|
||||
@mock.patch.object(sds_driver.SdsBaseDriver, '_check_snapshot_exist')
|
||||
def test_delete_snapshot(self, mock__check_snapshot_exist,
|
||||
mock_delete_snapshot):
|
||||
"""Test delete snapshot."""
|
||||
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
|
||||
snapshot = objects.Snapshot(
|
||||
id=uuid.uuid4(), volume_size=2, volume=volume)
|
||||
mock__check_snapshot_exist.return_value = True
|
||||
mock_delete_snapshot.return_value = {'success': 1}
|
||||
retval = self.fake_driver.delete_snapshot(snapshot)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
|
||||
class FakeSdsISCSIDriver(sds_driver.SdsISCSIDriver):
|
||||
"""Fake sds iscsi driver, include attach, detach."""
|
||||
|
||||
def __init__(self):
|
||||
"""Init conf client pool."""
|
||||
self.configuration = config.Configuration(None)
|
||||
self.client = None
|
||||
self.address = "192.168.200.100"
|
||||
self.user = "fake_user"
|
||||
self.password = "fake_password"
|
||||
self.pool = "fake_pool_name"
|
||||
self.poolid = 1
|
||||
self.iscsi_info = {"iqn.1994-05.com.redhat:899c5f9d15d":
|
||||
"1.1.1.1,1.1.1.2,1.1.1.3"}
|
||||
self.default_target_ips = ["1.1.1.1", "1.1.1.2", "1.1.1.3"]
|
||||
self.chap_username = "123456789123"
|
||||
self.chap_password = "1234567891234"
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestSdsISCSIDriver(test.TestCase):
|
||||
"""Testcase sds iscsi driver, include attach, detach."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup."""
|
||||
super(TestSdsISCSIDriver, self).setUp()
|
||||
self.fake_driver = FakeSdsISCSIDriver()
|
||||
self.fake_driver.client = sds_client.RestCmd("192.168.200.100",
|
||||
"fake_user",
|
||||
"fake_password",
|
||||
True)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_target_by_name')
|
||||
def test__check_target_exist(self, mock_query_target_by_name):
|
||||
"""Test target exist or not."""
|
||||
target_name = 'test_driver'
|
||||
mock_query_target_by_name.return_value = {'success': 1}
|
||||
retval = self.fake_driver._check_target_exist(target_name)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_initiator_by_name')
|
||||
def test__check_initiator_exist(self, mock_query_initiator_by_name):
|
||||
"""Test initiator exist or not."""
|
||||
initiator_name = 'test_driver'
|
||||
mock_query_initiator_by_name.return_value = {'success': 1}
|
||||
retval = self.fake_driver._check_initiator_exist(initiator_name)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_target_initiatoracl')
|
||||
def test__check_target_added_initiator(self,
|
||||
mock_query_target_initiatoracl):
|
||||
"""Test target added the initiator."""
|
||||
mock_query_target_initiatoracl.return_value = {'success': 1}
|
||||
target_name, initiator_name = 'test_driver', 'initiator_name'
|
||||
retval = self.fake_driver._check_target_added_initiator(target_name,
|
||||
initiator_name)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_target_lunacl')
|
||||
def test__check_target_added_lun(self, mock_query_target_lunacl):
|
||||
"""Test target added the lun."""
|
||||
mock_query_target_lunacl.return_value = {'success': 1}
|
||||
target_name, pool_name, volume_name = ('ccc', self.fake_driver.pool,
|
||||
'fcc')
|
||||
retval = self.fake_driver._check_target_added_lun(target_name,
|
||||
pool_name,
|
||||
volume_name)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_chapinfo_by_target')
|
||||
def test__check_target_added_chap(self, mock_query_chapinfo_by_target):
|
||||
"""Test target added chapuser."""
|
||||
mock_query_chapinfo_by_target.return_value = {'success': 1}
|
||||
target_name, user_name = 'ccc', 'fcc'
|
||||
retval = self.fake_driver._check_target_added_chap(target_name,
|
||||
user_name)
|
||||
self.assertEqual({'success': 1}, retval)
|
||||
|
||||
def test__get_target_ip(self):
|
||||
"""Test get target from targetip."""
|
||||
initiator = 'iqn.1994-05.com.redhat:899c5f9d15d'
|
||||
retval_target_ips = \
|
||||
self.fake_driver._get_target_ip(initiator)
|
||||
self.assertListEqual(['1.1.1.1', '1.1.1.2', '1.1.1.3'],
|
||||
retval_target_ips)
|
||||
|
||||
self.fake_driver.default_target_ips = \
|
||||
["1.1.1.1"]
|
||||
initiator = 'vms'
|
||||
retval_target_ips = \
|
||||
self.fake_driver._get_target_ip(initiator)
|
||||
self.assertListEqual(["1.1.1.1"], retval_target_ips)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'add_chap_by_target')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver, '_check_target_added_chap')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver, '_check_target_added_lun')
|
||||
@mock.patch.object(sds_client.RestCmd, 'mapping_lun')
|
||||
@mock.patch.object(sds_client.RestCmd, 'add_initiator_to_target')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver,
|
||||
'_check_target_added_initiator')
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_initiator')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver, '_check_initiator_exist')
|
||||
@mock.patch.object(sds_client.RestCmd, 'create_target')
|
||||
@mock.patch.object(sds_client.RestCmd, 'query_node_by_targetips')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver, '_check_target_exist')
|
||||
@mock.patch.object(sds_driver.SdsISCSIDriver, '_get_target_ip')
|
||||
def test_initialize_connection(self, mock__get_target_ip,
|
||||
mock__check_target_exist,
|
||||
mock_query_node_by_targetips,
|
||||
mock_create_target,
|
||||
mock__check_initiator_exist,
|
||||
mock_create_initiator,
|
||||
mock__check_target_added_initiator,
|
||||
mock_add_initiator_to_target,
|
||||
mock_mapping_lun,
|
||||
mock__check_target_added_lun,
|
||||
mock__check_target_added_chap,
|
||||
mock_add_chap_by_target):
|
||||
"""Test attach volume to kvm."""
|
||||
mock__get_target_ip.return_value = (['1.1.1.1', '1.1.1.2', '1.1.1.3'])
|
||||
mock__check_target_exist.return_value = False
|
||||
mock__check_initiator_exist.return_value = False
|
||||
mock__check_target_added_initiator.result_value = False
|
||||
mock__check_target_added_chap.return_value = False
|
||||
mock_query_node_by_targetips.return_value = {'host_id', 'address'}
|
||||
mock_create_target.return_value = {'success': 1}
|
||||
mock_create_initiator.return_value = {'success': 1}
|
||||
mock_add_initiator_to_target.result_value = {'success': 1}
|
||||
mock_mapping_lun.return_value = {'success': 1}
|
||||
mock__check_target_added_lun.return_value = 1
|
||||
mock_add_chap_by_target.return_value = {'success': 1}
|
||||
|
||||
volume1, connector1 = (objects.Volume(id=uuid.uuid4(),
|
||||
_name_id=uuid.uuid4(), size=1),
|
||||
{'initiator':
|
||||
'iqn.1994-05.com.redhat:899c5f9d15d',
|
||||
'multipath': True})
|
||||
initiator_name = connector1['initiator']
|
||||
iqn_end = initiator_name.split(':', 1)[1]
|
||||
target_head = 'iqn.2014-10.com.szsandstone:storage:'
|
||||
target_name = target_head + iqn_end
|
||||
result1 = {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {'target_discovered': True,
|
||||
'target_portals': ['1.1.1.1:3260',
|
||||
'1.1.1.2:3260',
|
||||
'1.1.1.3:3260'],
|
||||
'volume_id': volume1.id,
|
||||
'auth_method': 'CHAP',
|
||||
'auth_username': '123456789123',
|
||||
'auth_password': '1234567891234',
|
||||
'target_iqns': [target_name, target_name, target_name],
|
||||
'target_luns': [1, 1, 1]}}
|
||||
retval = self.fake_driver.initialize_connection(volume1, connector1)
|
||||
self.assertDictEqual(result1, retval)
|
||||
|
||||
volume2, connector2 = (objects.Volume(id=uuid.uuid4(),
|
||||
_name_id=uuid.uuid4(),
|
||||
size=2),
|
||||
{'initiator':
|
||||
'iqn.1994-05.com.redhat:899c5f9d15d'})
|
||||
mock__get_target_ip.return_value = (['1.1.1.1', '1.1.1.2', '1.1.1.3'])
|
||||
initiator_name = connector2['initiator']
|
||||
iqn_end = initiator_name.split(':', 1)[1]
|
||||
target_head = 'iqn.2014-10.com.szsandstone:storage:'
|
||||
target_name = target_head + iqn_end
|
||||
result2 = {'driver_volume_type': 'iscsi',
|
||||
'data': {'target_discovered': True,
|
||||
'target_portal': '1.1.1.1:3260',
|
||||
'volume_id': volume2.id,
|
||||
'target_iqn': target_name,
|
||||
'target_lun': 1,
|
||||
'auth_method': 'CHAP',
|
||||
'auth_username': '123456789123',
|
||||
'auth_password': '1234567891234'}}
|
||||
retval = self.fake_driver.initialize_connection(volume2, connector2)
|
||||
self.assertDictEqual(result2, retval)
|
||||
|
||||
@mock.patch.object(sds_client.RestCmd, 'unmap_lun')
|
||||
def test_terminate_connection(self, mock_unmap_lun):
|
||||
"""Test detach volume from kvm."""
|
||||
volume, connector = (objects.Volume(_name_id=uuid.uuid4(), size=1),
|
||||
{'initiator':
|
||||
'iqn.1994-05.com.redhat:899c5f9d15d'})
|
||||
mock_unmap_lun.result_value = {'success': 1}
|
||||
retval = self.fake_driver.terminate_connection(volume, connector)
|
||||
self.assertIsNone(retval)
|
|
@ -0,0 +1,54 @@
|
|||
# Copyright (c) 2019 ShenZhen SandStone Data Technologies 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.
|
||||
import json
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class FakeBaseSession(requests.Session):
|
||||
"""Redefine get and post method, fake it."""
|
||||
|
||||
method_map = {}
|
||||
|
||||
def _get_response(self, method, url):
|
||||
url_map = self.method_map.get(method, {})
|
||||
tmp = None
|
||||
data = {}
|
||||
for k in url_map:
|
||||
if re.search(k, url):
|
||||
if not tmp or len(tmp) < len(k):
|
||||
data = url_map[k]
|
||||
tmp = k
|
||||
|
||||
resp_content = {'success': 1}
|
||||
resp_content.update(data)
|
||||
resp = requests.Response()
|
||||
resp.cookies['XSRF-TOKEN'] = 'fake_token'
|
||||
resp.headers['Referer'] = 'fake_refer'
|
||||
resp.headers['Set-Cookie'] = 'sdsom_sessionid=fake_session;'
|
||||
resp.status_code = 200
|
||||
resp.encoding = 'utf-8'
|
||||
resp._content = json.dumps(resp_content).encode('utf-8')
|
||||
|
||||
return resp
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
"""Redefine get method."""
|
||||
return self._get_response('get', url)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
"""Redefine post method."""
|
||||
return self._get_response('post', url)
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) 2019 ShenZhen SandStone Data Technologies 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.
|
||||
"""SandStone iSCSI Driver Const."""
|
||||
|
||||
CONNECT_ERROR = 403
|
||||
|
||||
BASIC_URI = '/api/storage/'
|
||||
OM_URI = '/api/om/'
|
||||
PAGESIZE = 1000
|
|
@ -0,0 +1,711 @@
|
|||
# Copyright (c) 2019 ShenZhen SandStone Data Technologies 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.
|
||||
"""SandStone iSCSI Driver."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from oslo_log import log as logging
|
||||
import requests
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.volume.drivers.sandstone import constants
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RestCmd(object):
|
||||
"""Restful api class."""
|
||||
|
||||
def __init__(self, address, user, password,
|
||||
suppress_requests_ssl_warnings):
|
||||
"""Init RestCmd class.
|
||||
|
||||
:param address: Restapi uri.
|
||||
:param user: login web username.
|
||||
:param password: login web password.
|
||||
"""
|
||||
self.address = "https://%(address)s" % {"address": address}
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.pagesize = constants.PAGESIZE
|
||||
self.session = None
|
||||
self.short_wait = 10
|
||||
self.long_wait = 12000
|
||||
self.debug = True
|
||||
self._init_http_header()
|
||||
|
||||
def _init_http_header(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"Content-Type": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
})
|
||||
self.session.verify = False
|
||||
|
||||
def run(self, url, method, data=None, json_flag=True,
|
||||
filter_flag=False, om_op_flag=False):
|
||||
"""Run rest cmd function.
|
||||
|
||||
:param url: rest api uri resource.
|
||||
:param data: rest api uri json parameter.
|
||||
:param filter_flag: controller whether filter log. (default 'No')
|
||||
:param om_op_flag: api op have basic and om, use different prefix uri.
|
||||
"""
|
||||
kwargs = {}
|
||||
if data:
|
||||
kwargs["data"] = json.dumps(data)
|
||||
if om_op_flag:
|
||||
rest_url = self.address + constants.OM_URI + url
|
||||
else:
|
||||
rest_url = self.address + constants.BASIC_URI + url
|
||||
|
||||
func = getattr(self.session, method.lower())
|
||||
try:
|
||||
result = func(rest_url, **kwargs)
|
||||
except requests.RequestException as err:
|
||||
msg = _('Bad response from server: %(url)s. '
|
||||
'Error: %(err)s') % {'url': rest_url, 'err': err}
|
||||
raise exception.VolumeBackendAPIException(msg)
|
||||
|
||||
try:
|
||||
result.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
if exc.response.status_code == constants.CONNECT_ERROR:
|
||||
try:
|
||||
self.login()
|
||||
except requests.ConnectTimeout as err:
|
||||
msg = (_("Sandstone web server may be abnormal "
|
||||
"or storage may be poweroff. Error: %(err)s")
|
||||
% {'err': err})
|
||||
raise exception.VolumeBackendAPIException(msg)
|
||||
else:
|
||||
return {"error": {"code": exc.response.status_code,
|
||||
"description": six.text_type(exc)}}
|
||||
|
||||
if not filter_flag:
|
||||
LOG.info('''
|
||||
Request URL: %(url)s,
|
||||
Call Method: %(method)s,
|
||||
Request Data: %(data)s,
|
||||
Response Data: %(res)s,
|
||||
Result Data: %(res_json)s.''', {'url': url, 'method': method,
|
||||
'data': data, 'res': result,
|
||||
'res_json': result.json()})
|
||||
|
||||
if json_flag:
|
||||
return result.json()
|
||||
return result
|
||||
|
||||
def _assert_restapi_result(self, result, err):
|
||||
if result.get("success") != 1:
|
||||
msg = (_('%(err)s\nresult:%(res)s') % {"err": err,
|
||||
"res": result})
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
def login(self):
|
||||
"""Login web get with token session."""
|
||||
url = 'user/login'
|
||||
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(self.password.encode("utf8"))
|
||||
password = sha256.hexdigest()
|
||||
|
||||
data = {"username": self.user, "password": password}
|
||||
result = self.run(url=url, data=data, method='POST', json_flag=False,
|
||||
om_op_flag=True)
|
||||
self._assert_restapi_result(result.json(), _('Login error.'))
|
||||
cookies = result.cookies
|
||||
set_cookie = result.headers['Set-Cookie']
|
||||
self.session.headers['Cookie'] = ';'.join(
|
||||
['XSRF-TOKEN={}'.format(cookies['XSRF-TOKEN']),
|
||||
' username={}'.format(self.user),
|
||||
' sdsom_sessionid={}'.format(self._find_sessionid(set_cookie))])
|
||||
self.session.headers["Referer"] = self.address
|
||||
self.session.headers["X-XSRF-TOKEN"] = cookies["XSRF-TOKEN"]
|
||||
|
||||
def _find_sessionid(self, headers):
|
||||
sessionid = re.findall("sdsom_sessionid=(\\w+);", headers)
|
||||
if sessionid:
|
||||
return sessionid[0]
|
||||
return ""
|
||||
|
||||
def _check_special_result(self, result, contain):
|
||||
if result.get("success") == 0 and contain in result.get("data"):
|
||||
return True
|
||||
|
||||
def logout(self):
|
||||
"""Logout release resource."""
|
||||
url = 'user/logout'
|
||||
data = {"username": self.user}
|
||||
result = self.run(url, 'POST', data=data,
|
||||
om_op_flag=True)
|
||||
self._assert_restapi_result(result, _("Logout out error."))
|
||||
|
||||
def query_capacity_info(self):
|
||||
"""Query cluster capacity."""
|
||||
url = 'capacity'
|
||||
capacity_info = {}
|
||||
|
||||
result = self.run(url, 'POST', filter_flag=True)
|
||||
self._assert_restapi_result(result, _("Query capacity error."))
|
||||
capacity_info["capacity_bytes"] = result["data"].get(
|
||||
"capacity_bytes", 0)
|
||||
capacity_info["free_bytes"] = result["data"].get("free_bytes", 0)
|
||||
return capacity_info
|
||||
|
||||
def query_pool_info(self):
|
||||
"""Query use pool status."""
|
||||
url = 'pool/list'
|
||||
|
||||
result = self.run(url, 'POST')
|
||||
self._assert_restapi_result(result, _("Query pool status error."))
|
||||
return result["data"]
|
||||
|
||||
def get_poolid_from_poolname(self):
|
||||
"""Use poolname get poolid from pool/list maps."""
|
||||
data = self.query_pool_info()
|
||||
poolname_map_poolid = {}
|
||||
if data:
|
||||
for pool in data:
|
||||
poolname_map_poolid[pool["realname"]] = pool["pool_id"]
|
||||
return poolname_map_poolid
|
||||
|
||||
def create_initiator(self, initiator_name):
|
||||
"""Create client iqn in storage cluster."""
|
||||
url = 'resource/initiator/create'
|
||||
data = {"iqn": initiator_name, "type": "iSCSI",
|
||||
"remark": "Cinder iSCSI"}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# initiator exist, return no err.
|
||||
if self._check_special_result(result, "already exist"):
|
||||
return
|
||||
self._assert_restapi_result(result, _("Create initiator error."))
|
||||
|
||||
def _delaytask_list(self, pagesize=20):
|
||||
url = 'delaytask/list'
|
||||
data = {"pageno": 1, "pagesize": pagesize}
|
||||
return self.run(url, 'POST', data=data, om_op_flag=True)
|
||||
|
||||
def _judge_delaytask_status(self, wait_time, func_name, *args):
|
||||
# wait 10 seconds for task
|
||||
func = getattr(self, func_name.lower())
|
||||
for wait in range(1, wait_time + 1):
|
||||
try:
|
||||
task_status = func(*args)
|
||||
if self.debug:
|
||||
LOG.info(task_status)
|
||||
except exception.VolumeBackendAPIException as exc:
|
||||
msg = (_("Task: run %(task)s failed, "
|
||||
"err: %(err)s.")
|
||||
% {"task": func_name,
|
||||
"err": exc})
|
||||
LOG.error(msg)
|
||||
if task_status.get('run_status') == "failed":
|
||||
msg = (_("Task : run %(task)s failed, "
|
||||
"parameter : %(parameter)s, "
|
||||
"progress is %(process)d.")
|
||||
% {"task": func_name,
|
||||
"process": task_status.get('progress'),
|
||||
"parameter": args})
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
elif task_status.get('run_status') != "completed":
|
||||
msg = (_("Task : running %(task)s , "
|
||||
"parameter : %(parameter)s, "
|
||||
"progress is %(process)d, "
|
||||
"waited for 1 second, "
|
||||
"total waited %(total)d second.")
|
||||
% {"task": func_name,
|
||||
"process": task_status.get('progress', 0),
|
||||
"parameter": args,
|
||||
"total": wait})
|
||||
LOG.info(msg)
|
||||
time.sleep(1)
|
||||
elif task_status.get('run_status') == "completed":
|
||||
msg = (_("Task : running %(task)s successfully, "
|
||||
"parameter : %(parameter)s, "
|
||||
"progress is %(process)d, "
|
||||
"total spend %(total)d second.")
|
||||
% {"task": func_name,
|
||||
"process": task_status.get('progress'),
|
||||
"parameter": args,
|
||||
"total": wait})
|
||||
LOG.info(msg)
|
||||
break
|
||||
|
||||
def add_initiator_to_target(self, target_name, initiator_name):
|
||||
"""Bind client iqn to storage target iqn."""
|
||||
url = 'resource/target/add_initiator_to_target'
|
||||
data = {"targetName": target_name,
|
||||
"iqns": [{"ip": "", "iqn": initiator_name}]}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# wait 10 seconds to map initiator
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_map_initiator_porcess",
|
||||
target_name, initiator_name)
|
||||
self._assert_restapi_result(result, _("Add initiator "
|
||||
"to target error."))
|
||||
|
||||
def query_map_initiator_porcess(self, target_name,
|
||||
initiator_name):
|
||||
"""Query initiator add to target process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query mapping "
|
||||
"initiator process error."))
|
||||
|
||||
result = result["data"].get("results", None) or []
|
||||
expected_parameter = [{"target_name": target_name,
|
||||
"iqns": [{"ip": "", "iqn": initiator_name}]}]
|
||||
task = [map_initiator_task for map_initiator_task in result
|
||||
if map_initiator_task["executor"] == "MapInitiator"
|
||||
and map_initiator_task["parameter"] == expected_parameter]
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def query_initiator_by_name(self, initiator_name):
|
||||
"""Query initiator exist or not."""
|
||||
url = 'resource/initiator/list'
|
||||
data = {"initiatorMark": "", "pageno": 1,
|
||||
"pagesize": self.pagesize, "type": "iSCSI"}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query initiator "
|
||||
"by name error."))
|
||||
|
||||
result = result["data"].get("results", None) or []
|
||||
initiator_info = [initiator for initiator in result
|
||||
if initiator.get("iqn", None) == initiator_name]
|
||||
if initiator_info:
|
||||
return initiator_info[0]
|
||||
return None
|
||||
|
||||
def query_target_initiatoracl(self, target_name, initiator_name):
|
||||
"""Query target iqn bind client iqn info."""
|
||||
url = 'resource/target/get_target_acl_list'
|
||||
data = {"pageno": 1, "pagesize": self.pagesize,
|
||||
"targetName": target_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query target "
|
||||
"initiatoracl error."))
|
||||
|
||||
results = result["data"].get("results", None)
|
||||
acl_info = [acl for acl in results or []
|
||||
if acl.get("name", None) == initiator_name]
|
||||
return acl_info or None
|
||||
|
||||
def query_node_by_targetips(self, target_ips):
|
||||
"""Query target ip relation with node."""
|
||||
url = 'block/gateway/server/list'
|
||||
result = self.run(url, 'POST')
|
||||
self._assert_restapi_result(result, _("Query node by "
|
||||
"targetips error."))
|
||||
|
||||
targetip_to_hostid = {}
|
||||
for node in result["data"]:
|
||||
for node_access_ip in node.get("networks"):
|
||||
goal_ip = node_access_ip.get("address")
|
||||
if goal_ip in target_ips:
|
||||
targetip_to_hostid[goal_ip] =\
|
||||
node_access_ip.get("hostid", None)
|
||||
return targetip_to_hostid
|
||||
|
||||
def query_target_by_name(self, target_name):
|
||||
"""Query target iqn exist or not."""
|
||||
url = 'resource/target/list'
|
||||
data = {"pageno": 1, "pagesize": self.pagesize,
|
||||
"thirdParty": [0, 1], "targetMark": ""}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query target by name error."))
|
||||
|
||||
result = result["data"].get("results", None) or []
|
||||
target_info = [target for target in result
|
||||
if target.get("name", None) == target_name]
|
||||
if target_info:
|
||||
return target_info[0]
|
||||
return None
|
||||
|
||||
def create_target(self, target_name, targetip_to_hostid):
|
||||
"""Create target iqn."""
|
||||
url = 'resource/target/create'
|
||||
data = {"type": "iSCSI", "readOnly": 0,
|
||||
"thirdParty": 1, "targetName": target_name,
|
||||
"networks": [{"hostid": host_id, "address": address}
|
||||
for address, host_id in
|
||||
targetip_to_hostid.items()]}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# target exist, return no err.
|
||||
if self._check_special_result(result, "already exist"):
|
||||
return
|
||||
self._assert_restapi_result(result, _("Create target error."))
|
||||
|
||||
def add_chap_by_target(self, target_name, username, password):
|
||||
"""Add chap to target, only support forward."""
|
||||
url = 'resource/target/add_chap'
|
||||
data = {"password": password,
|
||||
"user": username, "targetName": target_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Add chap by target error."))
|
||||
|
||||
def query_chapinfo_by_target(self, target_name, username):
|
||||
"""Query chapinfo by target, check chap add target or not."""
|
||||
url = 'resource/target/get_chap_list'
|
||||
data = {"targetName": target_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query chapinfo "
|
||||
"by target error."))
|
||||
|
||||
result = result.get('data') or []
|
||||
chapinfo = [c for c in result if c.get("user") == username]
|
||||
if chapinfo:
|
||||
return chapinfo[0]
|
||||
return None
|
||||
|
||||
def create_lun(self, capacity_bytes, poolid, volume_name):
|
||||
"""Create lun resource."""
|
||||
url = 'resource/lun/add'
|
||||
data = {"capacity_bytes": capacity_bytes,
|
||||
"poolId": poolid, "priority": "normal",
|
||||
"qosSettings": {}, "volumeName": volume_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Create lun error."))
|
||||
|
||||
def delete_lun(self, poolid, volume_name):
|
||||
"""Delete lun resource."""
|
||||
url = 'resource/lun/batch_delete'
|
||||
data = {"delayTime": 0, "volumeNameList": [{
|
||||
"poolId": poolid,
|
||||
"volumeName": volume_name}]}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# lun deleted, return no err.
|
||||
if self._check_special_result(result, "not found"):
|
||||
return
|
||||
self._assert_restapi_result(result, _("Delete lun error."))
|
||||
|
||||
def extend_lun(self, capacity_bytes, poolid, volume_name):
|
||||
"""Extend lun, only support enlarge."""
|
||||
url = 'resource/lun/resize'
|
||||
|
||||
data = {"capacity_bytes": capacity_bytes,
|
||||
"poolId": poolid,
|
||||
"volumeName": volume_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Extend lun error."))
|
||||
|
||||
def unmap_lun(self, target_name, poolid, volume_name, pool_name):
|
||||
"""Unbind lun from target iqn."""
|
||||
url = 'resource/target/unmap_luns'
|
||||
volume_info = self.query_lun_by_name(volume_name, poolid)
|
||||
result = {"success": 0}
|
||||
if volume_info:
|
||||
uuid = volume_info.get("uuid", None)
|
||||
data = {"targetName": target_name,
|
||||
"targetLunList": [uuid],
|
||||
"targetSnapList": []}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# lun unmaped, return no err.
|
||||
if self._check_special_result(result, "not mapped"):
|
||||
return
|
||||
# wait for 10 seconds to unmap lun.
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_unmapping_lun_porcess",
|
||||
target_name, volume_name,
|
||||
uuid, pool_name)
|
||||
self._assert_restapi_result(result, _("Unmap lun error."))
|
||||
else:
|
||||
self._assert_restapi_result(result,
|
||||
_("Unmap lun error, uuid is None."))
|
||||
|
||||
def mapping_lun(self, target_name, poolid, volume_name, pool_name):
|
||||
"""Bind lun to target iqn."""
|
||||
url = 'resource/target/map_luns'
|
||||
volume_info = self.query_lun_by_name(volume_name, poolid)
|
||||
result = {"success": 0}
|
||||
if volume_info:
|
||||
uuid = volume_info.get("uuid", None)
|
||||
data = {"targetName": target_name,
|
||||
"targetLunList": [uuid],
|
||||
"targetSnapList": []}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# lun maped, return no err.
|
||||
if self._check_special_result(result, "already mapped"):
|
||||
return
|
||||
# wait for 10 seconds to map lun.
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_mapping_lun_porcess",
|
||||
target_name, volume_name,
|
||||
uuid, pool_name)
|
||||
self._assert_restapi_result(result, _("Map lun error."))
|
||||
else:
|
||||
self._assert_restapi_result(result,
|
||||
_("Map lun error, uuid is None."))
|
||||
|
||||
def query_mapping_lun_porcess(self, target_name, volume_name,
|
||||
uuid, pool_name):
|
||||
"""Query mapping lun process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query mapping "
|
||||
"lun process error."))
|
||||
|
||||
expected_parameter = {"target_name": target_name,
|
||||
"image_id": uuid,
|
||||
"target_realname": target_name,
|
||||
"meta_pool": pool_name,
|
||||
"image_realname": volume_name}
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [map_initiator_task for map_initiator_task in result
|
||||
if map_initiator_task["executor"] == "TargetMap"
|
||||
and map_initiator_task["parameter"] == expected_parameter]
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def query_unmapping_lun_porcess(self, target_name, volume_name,
|
||||
uuid, pool_name):
|
||||
"""Query mapping lun process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query mapping "
|
||||
"lun process error."))
|
||||
|
||||
expected_parameter = {"target_name": target_name,
|
||||
"image_id": uuid,
|
||||
"target_realname": target_name,
|
||||
"meta_pool": pool_name,
|
||||
"image_name": volume_name}
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [map_initiator_task for map_initiator_task in result
|
||||
if map_initiator_task["executor"] == "TargetUnmap"
|
||||
and map_initiator_task["parameter"] == expected_parameter]
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def query_target_lunacl(self, target_name, poolid, volume_name):
|
||||
"""Query target iqn relation with lun."""
|
||||
url = 'resource/target/get_luns'
|
||||
data = {"pageno": 1, "pagesize": self.pagesize,
|
||||
"pools": [poolid], "targetName": target_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query target lunacl error."))
|
||||
|
||||
# target get_luns use results
|
||||
result = result["data"].get("results", None) or []
|
||||
lunid = [volume.get("lid", None) for volume in result
|
||||
if volume.get("name", None) == volume_name
|
||||
and volume.get("pool_id") == poolid]
|
||||
if lunid:
|
||||
return lunid[0]
|
||||
return None
|
||||
|
||||
def query_lun_by_name(self, volume_name, poolid):
|
||||
"""Query lun exist or not."""
|
||||
url = 'resource/lun/list'
|
||||
data = {"pageno": 1, "pagesize": self.pagesize,
|
||||
"volumeMark": volume_name,
|
||||
"sortType": "time", "sortOrder": "desc",
|
||||
"pools": [poolid], "thirdParty": [0, 1]}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query lun by name error."))
|
||||
|
||||
result = result["data"].get("results", None) or []
|
||||
volume_info = [volume for volume in result
|
||||
if volume.get("volumeName", None) == volume_name]
|
||||
if volume_info:
|
||||
return volume_info[0]
|
||||
return None
|
||||
|
||||
def query_target_by_lun(self, volume_name, poolid):
|
||||
"""Query lun already mapped target name."""
|
||||
url = "resource/lun/targets"
|
||||
data = {"poolId": poolid, "volumeName": volume_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query target by lun error."))
|
||||
|
||||
data = result["data"]
|
||||
target_name = data[0].get("name", None)
|
||||
return target_name
|
||||
|
||||
def create_snapshot(self, poolid, volume_name, snapshot_name):
|
||||
"""Create lun snapshot."""
|
||||
url = 'resource/snapshot/add'
|
||||
data = {"lunName": volume_name,
|
||||
"poolId": poolid,
|
||||
"remark": "Cinder iSCSI snapshot.",
|
||||
"snapName": snapshot_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# snapshot existed, return no err.
|
||||
if self._check_special_result(result, "has exists"):
|
||||
return
|
||||
# wait for 10 seconds to create snapshot
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_create_snapshot_process",
|
||||
poolid, volume_name, snapshot_name)
|
||||
self._assert_restapi_result(result, _("Create snapshot error."))
|
||||
|
||||
def query_create_snapshot_process(self, poolid,
|
||||
volume_name, snapshot_name):
|
||||
"""Query create snapshot process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query flatten "
|
||||
"lun process error."))
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [flatten_task for flatten_task in result
|
||||
if flatten_task["executor"] == "SnapCreate"
|
||||
and flatten_task["parameter"].get("pool_id", None)
|
||||
== poolid
|
||||
and flatten_task["parameter"].get("snap_name", None)
|
||||
== snapshot_name
|
||||
and flatten_task["parameter"].get("lun_name", None)
|
||||
== volume_name]
|
||||
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def delete_snapshot(self, poolid, volume_name, snapshot_name):
|
||||
"""Delete lun snapshot."""
|
||||
url = 'resource/snapshot/delete'
|
||||
data = {"lunName": volume_name,
|
||||
"poolId": poolid, "snapName": snapshot_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# snapshot deleted, need return no err.
|
||||
if self._check_special_result(result, "not found"):
|
||||
return
|
||||
# wait for 10 seconds to delete snapshot
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_delete_snapshot_process",
|
||||
poolid, volume_name, snapshot_name)
|
||||
self._assert_restapi_result(result, _("Delete snapshot error."))
|
||||
|
||||
def query_delete_snapshot_process(self, poolid,
|
||||
volume_name, snapshot_name):
|
||||
"""Query delete snapshot process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query delete "
|
||||
"snapshot process error."))
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [flatten_task for flatten_task in result
|
||||
if flatten_task["executor"] == "SnapDelete"
|
||||
and flatten_task["parameter"].get("pool_id", None)
|
||||
== poolid
|
||||
and flatten_task["parameter"].get("snap_name", None)
|
||||
== snapshot_name
|
||||
and flatten_task["parameter"].get("lun_name", None)
|
||||
== volume_name]
|
||||
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def create_lun_from_snapshot(self, snapshot_name, src_volume_name,
|
||||
poolid, dst_volume_name):
|
||||
"""Create lun from source lun snapshot."""
|
||||
url = 'resource/snapshot/clone'
|
||||
data = {"snapshot": {"poolId": poolid,
|
||||
"lunName": src_volume_name,
|
||||
"snapName": snapshot_name},
|
||||
"cloneLun": {"lunName": dst_volume_name,
|
||||
"poolId": poolid}}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# clone volume exsited, return no err.
|
||||
if self._check_special_result(result, "already exists"):
|
||||
return
|
||||
# wait for 10 seconds to clone lun
|
||||
self._judge_delaytask_status(self.short_wait,
|
||||
"query_clone_lun_process",
|
||||
poolid, src_volume_name, snapshot_name)
|
||||
self._assert_restapi_result(result, _("Create lun "
|
||||
"from snapshot error."))
|
||||
self.flatten_lun(dst_volume_name, poolid)
|
||||
|
||||
def query_clone_lun_process(self, poolid, volume_name, snapshot_name):
|
||||
"""Query clone lun process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query flatten "
|
||||
"lun process error."))
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [flatten_task for flatten_task in result
|
||||
if flatten_task["executor"] == "SnapClone"
|
||||
and flatten_task["parameter"].get("pool_id", None)
|
||||
== poolid
|
||||
and flatten_task["parameter"].get("snap_name", None)
|
||||
== snapshot_name
|
||||
and flatten_task["parameter"].get("lun_name", None)
|
||||
== volume_name]
|
||||
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def flatten_lun(self, volume_name, poolid):
|
||||
"""Flatten lun."""
|
||||
url = 'resource/lun/flatten'
|
||||
data = {"poolId": poolid,
|
||||
"volumeName": volume_name}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
# volume flattened, return no err.
|
||||
if self._check_special_result(result, "not need flatten"):
|
||||
return
|
||||
# wait for longest 200 min to flatten
|
||||
self._judge_delaytask_status(self.long_wait,
|
||||
"query_flatten_lun_process",
|
||||
poolid, volume_name)
|
||||
self._assert_restapi_result(result, _("Flatten lun error."))
|
||||
|
||||
def query_flatten_lun_process(self, poolid, volume_name):
|
||||
"""Query flatten lun process."""
|
||||
result = self._delaytask_list()
|
||||
self._assert_restapi_result(result, _("Query flatten "
|
||||
"lun process error."))
|
||||
result = result["data"].get("results", None) or []
|
||||
task = [flatten_task for flatten_task in result
|
||||
if flatten_task["executor"] == "LunFlatten"
|
||||
and flatten_task["parameter"].get("pool_id", None)
|
||||
== poolid
|
||||
and flatten_task["parameter"].get("lun_name", None)
|
||||
== volume_name]
|
||||
if task:
|
||||
return task[0]
|
||||
return {}
|
||||
|
||||
def create_lun_from_lun(self, dst_volume_name, poolid, src_volume_name):
|
||||
"""Clone lun from source lun."""
|
||||
tmp_snapshot_name = 'temp' + src_volume_name + 'clone' +\
|
||||
dst_volume_name
|
||||
self.create_snapshot(poolid, src_volume_name, tmp_snapshot_name)
|
||||
self.create_lun_from_snapshot(tmp_snapshot_name, src_volume_name,
|
||||
poolid, dst_volume_name)
|
||||
self.flatten_lun(dst_volume_name, poolid)
|
||||
|
||||
self.delete_snapshot(poolid, src_volume_name, tmp_snapshot_name)
|
||||
|
||||
def query_snapshot_by_name(self, volume_name, poolid, snapshot_name):
|
||||
"""Query snapshot exist or not."""
|
||||
url = 'resource/snapshot/list'
|
||||
data = {"lunName": volume_name, "pageno": 1,
|
||||
"pagesize": self.pagesize, "poolId": poolid,
|
||||
"snapMark": ""}
|
||||
result = self.run(url, 'POST', data=data)
|
||||
self._assert_restapi_result(result, _("Query snapshot by name error."))
|
||||
|
||||
result = result["data"].get("results", None) or []
|
||||
snapshot_info = [snapshot for snapshot in result
|
||||
if snapshot.get("snapName", None) ==
|
||||
snapshot_name]
|
||||
return snapshot_info
|
|
@ -0,0 +1,513 @@
|
|||
# Copyright (c) 2019 ShenZhen SandStone Data Technologies 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.
|
||||
"""Volume Drivers for SandStone distributed storage."""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import interface
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.san import san
|
||||
from cinder.volume.drivers.sandstone.sds_client import RestCmd
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
sds_opts = [
|
||||
cfg.ListOpt("default_sandstone_target_ips",
|
||||
default=[],
|
||||
help="SandStone default target ip."),
|
||||
cfg.StrOpt("sandstone_pool",
|
||||
default="",
|
||||
help="SandStone storage pool resource name."),
|
||||
cfg.DictOpt("initiator_assign_sandstone_target_ip",
|
||||
default={},
|
||||
help="Support initiator assign target with assign ip.")
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(sds_opts)
|
||||
|
||||
|
||||
class SdsBaseDriver(driver.VolumeDriver):
|
||||
"""ISCSIDriver base class."""
|
||||
|
||||
# ThirdPartySytems wiki page
|
||||
VERSION = '1.0'
|
||||
CI_WIKI_NAME = "SandStone_Storage_CI"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Init configuration."""
|
||||
super(SdsBaseDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(sds_opts)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
|
||||
def do_setup(self, context):
|
||||
"""Instantiate common class and login storage system."""
|
||||
if not self.configuration:
|
||||
msg = _('Configuration is not found.')
|
||||
raise exception.InvalidConfigurationValue(msg)
|
||||
self.address = self.configuration.san_ip
|
||||
self.user = self.configuration.san_login
|
||||
self.password = self.configuration.san_password
|
||||
self.pool = self.configuration.sandstone_pool
|
||||
self.iscsi_info = (self.configuration.
|
||||
initiator_assign_sandstone_target_ip)
|
||||
self.default_target_ips = (self.configuration.
|
||||
default_sandstone_target_ips)
|
||||
self.chap_username = self.configuration.chap_username
|
||||
self.chap_password = self.configuration.chap_password
|
||||
self.suppress_requests_ssl_warnings = (self.configuration.
|
||||
suppress_requests_ssl_warnings)
|
||||
self.client = RestCmd(self.address, self.user, self.password,
|
||||
self.suppress_requests_ssl_warnings)
|
||||
LOG.debug("Run sandstone driver setup.")
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Check pool status and exist or not."""
|
||||
self.client.login()
|
||||
self.poolname_map_poolid = self.client.get_poolid_from_poolname()
|
||||
all_pools = self.client.query_pool_info()
|
||||
all_pools_name = [p['pool_name'] for p in all_pools
|
||||
if p.get('pool_name')]
|
||||
|
||||
if self.pool not in all_pools_name:
|
||||
msg = _('Storage pool %(pool)s does not exist '
|
||||
'in the cluster.') % {'pool': self.pool}
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
pool_status = [p['status'] for p in all_pools
|
||||
if p.get('pool_name') == self.pool]
|
||||
if pool_status:
|
||||
if ("health" not in pool_status[0].get('state') and
|
||||
pool_status[0].get("progress", 0) != 100):
|
||||
LOG.warning('Storage pool: %(poolName)s not healthy.',
|
||||
{"poolName": self.pool})
|
||||
if not self.poolname_map_poolid:
|
||||
err_msg = _('poolname_map_poolid info is empty.')
|
||||
self._raise_exception(err_msg)
|
||||
self.poolid = self.poolname_map_poolid.get(self.pool)
|
||||
if not self.poolid:
|
||||
err_msg = _('poolid is None.')
|
||||
self._raise_exception(err_msg)
|
||||
|
||||
def _update_volume_stats(self, pool_name):
|
||||
"""Get cluster capability and capacity."""
|
||||
data, pool = {}, {}
|
||||
data['pools'] = []
|
||||
|
||||
cluster_capacity = self.client.query_capacity_info()
|
||||
total_capacity_gb = (float(cluster_capacity.get("capacity_bytes", 0))
|
||||
/ units.Gi)
|
||||
free_capacity_gb = (float(cluster_capacity.get("free_bytes", 0))
|
||||
/ units.Gi)
|
||||
|
||||
self._stats = pool.update(dict(
|
||||
pool_name = pool_name,
|
||||
vendor_name = 'SandStone USP',
|
||||
driver_version = self.VERSION,
|
||||
total_capacity_gb = total_capacity_gb,
|
||||
free_capacity_gb = free_capacity_gb,
|
||||
QoS_support=True,
|
||||
thin_provisioning_support=True,
|
||||
multiattach=False,
|
||||
))
|
||||
data['pools'].append(pool)
|
||||
return data
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status and reload sandstone config file."""
|
||||
if refresh:
|
||||
return self._update_volume_stats(self.pool)
|
||||
return self._stats
|
||||
|
||||
def _raise_exception(self, msg):
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Create a volume."""
|
||||
capacity_bytes = int(volume.size) * units.Gi
|
||||
self.client.create_lun(capacity_bytes, self.poolid, volume.name)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Delete a volume."""
|
||||
LOG.debug("Delete volume %(volumeName)s from pool %(poolId)s",
|
||||
{"volumeName": volume.name,
|
||||
"poolId": self.poolid})
|
||||
self.client.delete_lun(self.poolid, volume.name)
|
||||
|
||||
def migrate_volume(self, ctxt, volume, host, new_type=None):
|
||||
"""Migrate a volume within the same array."""
|
||||
return (False, None)
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Create a volume from a snapshot.
|
||||
|
||||
We use LUNcopy to copy a new volume from snapshot.
|
||||
The time needed increases as volume size does.
|
||||
"""
|
||||
if snapshot.volume:
|
||||
source_vol_name = snapshot.volume.name
|
||||
source_vol_size = snapshot.volume.size * units.Gi
|
||||
destination_vol_name = volume.name
|
||||
destination_vol_size = volume.size * units.Gi
|
||||
snapshot_name = snapshot.name
|
||||
|
||||
self.client.create_lun_from_snapshot(snapshot_name,
|
||||
source_vol_name,
|
||||
self.poolid,
|
||||
destination_vol_name)
|
||||
if destination_vol_size > source_vol_size:
|
||||
self.client.extend_lun(destination_vol_size,
|
||||
self.poolid, volume.name)
|
||||
else:
|
||||
err_msg = _('No such snapshot volume.')
|
||||
self._raise_exception(err_msg)
|
||||
|
||||
def create_cloned_volume(self, dst_volume, src_volume):
|
||||
"""Clone a new volume from an existing volume."""
|
||||
if not self._check_volume_exist(src_volume.name):
|
||||
msg = (_('Source volume: %(volume_name)s does not exist.')
|
||||
% {'volume_name': src_volume.name})
|
||||
self._raise_exception(msg)
|
||||
self.client.create_lun_from_lun(dst_volume.name, self.poolid,
|
||||
src_volume.name)
|
||||
dst_vol_size = dst_volume.size * units.Gi
|
||||
src_vol_size = src_volume.size * units.Gi
|
||||
if dst_vol_size > src_vol_size:
|
||||
self.client.extend_lun(dst_vol_size, self.poolid, dst_volume.name)
|
||||
|
||||
def _check_volume_exist(self, volume):
|
||||
return self.client.query_lun_by_name(volume, self.poolid)
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend a volume."""
|
||||
old_volume = self._check_volume_exist(volume.name)
|
||||
if not old_volume:
|
||||
msg = (_('Not exist volume: %(volumeName)s')
|
||||
% {"volumeName": volume.name})
|
||||
self._raise_exception(msg)
|
||||
|
||||
old_size = old_volume.get("capacity_bytes")
|
||||
new_size = new_size * units.Gi
|
||||
if new_size == old_size:
|
||||
LOG.info("New size is equal to the real size from backend "
|
||||
"storage, no need to extend. "
|
||||
"realsize: %(oldsize)s, newsize: %(newsize)s.",
|
||||
{"oldsize": old_size,
|
||||
"newsize": new_size})
|
||||
return
|
||||
|
||||
if new_size < old_size:
|
||||
msg = (_("New size should be bigger than the real size from "
|
||||
"backend storage. "
|
||||
"realsize: %(oldsize)s, newsize: %(newsize)s.")
|
||||
% {"oldsize": old_size,
|
||||
"newsize": new_size})
|
||||
self._raise_exception(msg)
|
||||
|
||||
LOG.info(
|
||||
'Extend volume: %(volumename)s, '
|
||||
'oldsize: %(oldsize)s, newsize: %(newsize)s.',
|
||||
{"volumename": volume.name,
|
||||
"oldsize": old_size,
|
||||
"newsize": new_size})
|
||||
self.client.extend_lun(new_size, self.poolid, volume.name)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Create snapshot from volume."""
|
||||
volume = snapshot.volume
|
||||
|
||||
if not volume:
|
||||
msg = (_("Can't get volume id from snapshot, snapshot: %(id)s.")
|
||||
% {"id": snapshot.id})
|
||||
self._raise_exception(msg)
|
||||
|
||||
LOG.debug(
|
||||
"create snapshot from volumeName: %(volume)s, "
|
||||
"snap name: %(snapshot)s.",
|
||||
{"snapshot": snapshot.name,
|
||||
"volume": volume.name},)
|
||||
self.client.create_snapshot(self.poolid,
|
||||
volume.name,
|
||||
snapshot.name)
|
||||
|
||||
def _check_snapshot_exist(self, snapshot):
|
||||
return self.client.query_snapshot_by_name(snapshot.volume.name,
|
||||
self.poolid,
|
||||
snapshot.name)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Delete volume's snapshot."""
|
||||
snapshot_name = snapshot.name
|
||||
volume_name = snapshot.volume.name
|
||||
|
||||
if not self._check_snapshot_exist(snapshot):
|
||||
LOG.debug("not exist snapshot: %(snapshotName)s",
|
||||
{"snapshotName": snapshot.name})
|
||||
|
||||
LOG.info(
|
||||
'stop_snapshot: snapshot name: %(snapshot)s, '
|
||||
'volume name: %(volume)s.',
|
||||
{"snapshot": snapshot_name,
|
||||
"volume": volume_name},)
|
||||
|
||||
self.client.delete_snapshot(self.poolid,
|
||||
volume_name,
|
||||
snapshot_name)
|
||||
|
||||
def retype(self, ctxt, volume, new_type, diff, host):
|
||||
"""Convert the volume to be of the new type."""
|
||||
LOG.debug("Enter retype: id=%(id)s, new_type=%(new_type)s, "
|
||||
"diff=%(diff)s, host=%(host)s.", {'id': volume.id,
|
||||
'new_type': new_type,
|
||||
'diff': diff,
|
||||
'host': host})
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
"""Export a volume."""
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Synchronously recreate an export for a volume."""
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Remove an export for a volume."""
|
||||
pass
|
||||
|
||||
def create_export_snapshot(self, context, snapshot, connector):
|
||||
"""Export a snapshot."""
|
||||
pass
|
||||
|
||||
def remove_export_snapshot(self, context, snapshot):
|
||||
"""Remove an export for a snapshot."""
|
||||
pass
|
||||
|
||||
def backup_use_temp_snapshot(self):
|
||||
"""The config option has a default to be False, So just return it."""
|
||||
pass
|
||||
|
||||
def unmanage(self, volume):
|
||||
"""Export SandStone volume from Cinder."""
|
||||
LOG.debug("Unmanage volume: %s.", volume.id)
|
||||
|
||||
def unmanage_snapshot(self, snapshot):
|
||||
"""Unmanage the specified snapshot from Cinder management."""
|
||||
LOG.debug("Unmanage snapshot: %s.", snapshot.id)
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class SdsISCSIDriver(SdsBaseDriver, driver.ISCSIDriver):
|
||||
"""ISCSI driver for SandStone storage arrays.
|
||||
|
||||
Version history:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
1.0.0 - Initial driver
|
||||
Provide SandStone storage
|
||||
create volume support
|
||||
delete volume support
|
||||
create snapshot support
|
||||
delete snapshot support
|
||||
extend volume support
|
||||
create volume from snap support
|
||||
create cloned volume support
|
||||
nova volume-attach support
|
||||
nova volume-detach support
|
||||
"""
|
||||
|
||||
VERSION = "1.0.0"
|
||||
|
||||
def get_volume_stats(self, refresh):
|
||||
"""Get volume status and capality."""
|
||||
data = SdsBaseDriver.get_volume_stats(self, refresh)
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
data['volume_backend_name'] = backend_name or self.__class__.__name__
|
||||
data['storage_protocol'] = 'iSCSI'
|
||||
data['driver_version'] = self.VERSION
|
||||
data['vendor_name'] = 'SandStone USP'
|
||||
return data
|
||||
|
||||
def _check_target_exist(self, target_name):
|
||||
"""Check target name exist or not."""
|
||||
return self.client.query_target_by_name(target_name)
|
||||
|
||||
def _check_initiator_exist(self, initiator_name):
|
||||
"""Check initiator name exist or not."""
|
||||
return self.client.query_initiator_by_name(initiator_name)
|
||||
|
||||
def _check_target_added_initiator(self, target_name, initiator_name):
|
||||
return self.client.query_target_initiatoracl(target_name,
|
||||
initiator_name)
|
||||
|
||||
def _check_target_added_lun(self, target_name, poolid, volume_name):
|
||||
return self.client.query_target_lunacl(target_name, poolid,
|
||||
volume_name)
|
||||
|
||||
def _check_target_added_chap(self, target_name, username):
|
||||
return self.client.query_chapinfo_by_target(target_name, username)
|
||||
|
||||
def _get_target_ip(self, initiator):
|
||||
ini = self.iscsi_info.get(initiator)
|
||||
if ini:
|
||||
target_ips = [ip.strip() for ip in ini.split(',')
|
||||
if ip.strip()]
|
||||
else:
|
||||
target_ips = []
|
||||
|
||||
# If not specify target IP for some initiators, use default IP.
|
||||
if not target_ips:
|
||||
if self.default_target_ips:
|
||||
target_ips = self.default_target_ips
|
||||
else:
|
||||
msg = (_(
|
||||
'get_iscsi_params: Failed to get target IP '
|
||||
'for initiator %(ini)s, please check config file.')
|
||||
% {'ini': initiator})
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
return target_ips
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Map a volume to a host and return target iSCSI information."""
|
||||
initiator_name = connector['initiator']
|
||||
LOG.info(
|
||||
"initiator name: %(initiator_name)s, "
|
||||
"LUN ID: %(lun_id)s.",
|
||||
{"initiator_name": initiator_name,
|
||||
"lun_id": volume.name})
|
||||
|
||||
# Create target
|
||||
iqn_end = initiator_name.split(':')[-1]
|
||||
target_head = 'iqn.2014-10.com.szsandstone:storage:'
|
||||
target_name = target_head + iqn_end
|
||||
target_ips = self._get_target_ip(initiator_name)
|
||||
if not self._check_target_exist(iqn_end):
|
||||
targetip_to_hostid = (self.client.
|
||||
query_node_by_targetips(target_ips))
|
||||
self.client.create_target(iqn_end, targetip_to_hostid)
|
||||
else:
|
||||
# Target is exist, get target_name and nodes
|
||||
LOG.info("target is exist, don't repeat to create, "
|
||||
"iscsi_iqn: %(iscsi_iqn)s.",
|
||||
{"iscsi_iqn": target_name})
|
||||
|
||||
LOG.info("initialize_connection, iscsi_iqn: %(iscsi_iqn)s, "
|
||||
'target_ips: %(target_ips)s.',
|
||||
{'iscsi_iqn': target_name,
|
||||
'target_ips': target_ips})
|
||||
# Check initiator isn't exist
|
||||
if not self._check_initiator_exist(initiator_name):
|
||||
# Create initiator and add in storage
|
||||
self.client.create_initiator(initiator_name)
|
||||
else:
|
||||
LOG.info("initiator is exist, don't repeat to create "
|
||||
"initiator: %(initiator_name)s.",
|
||||
{"initiator_name": initiator_name})
|
||||
|
||||
# Check target added initiator or not
|
||||
if not self._check_target_added_initiator(iqn_end,
|
||||
initiator_name):
|
||||
# Add initiator to target
|
||||
self.client.add_initiator_to_target(iqn_end,
|
||||
initiator_name)
|
||||
else:
|
||||
LOG.info("initiator is added to target, no action needed, "
|
||||
"target: %(target_name)s, "
|
||||
"initiator: %(initiator_name)s.",
|
||||
{"initiator_name": initiator_name,
|
||||
"target_name": target_name})
|
||||
|
||||
lun_id = self._check_target_added_lun(iqn_end,
|
||||
self.poolid, volume.name)
|
||||
if not lun_id:
|
||||
# Mapping lun to target
|
||||
self.client.mapping_lun(iqn_end, self.poolid,
|
||||
volume.name, self.pool)
|
||||
lun_id = self._check_target_added_lun(iqn_end,
|
||||
self.poolid, volume.name)
|
||||
else:
|
||||
LOG.info("lun is added to target, don't repeat to add "
|
||||
"volume: %(volume_name)s, target: %(target_name)s.",
|
||||
{"volume_name": volume.name,
|
||||
"target_name": target_name})
|
||||
|
||||
# Mapping lungroup and hostgroup to view.
|
||||
LOG.info("initialize_connection, host lun id is: %(lun_id)d.",
|
||||
{"lun_id": lun_id})
|
||||
|
||||
# Return iSCSI properties.
|
||||
properties = {}
|
||||
properties['target_discovered'] = True
|
||||
properties['volume_id'] = volume.id
|
||||
multipath = connector.get('multipath', False)
|
||||
hostlun_id = lun_id
|
||||
if not multipath:
|
||||
properties['target_portal'] = ("%s:3260" % target_ips[0])
|
||||
properties['target_iqn'] = target_name
|
||||
properties['target_lun'] = hostlun_id
|
||||
else:
|
||||
properties['target_iqns'] = [target_name for i in
|
||||
range(len(target_ips))]
|
||||
properties['target_portals'] = [
|
||||
"%s:3260" % ip for ip in target_ips]
|
||||
properties['target_luns'] = [hostlun_id] * len(target_ips)
|
||||
|
||||
# If use CHAP, return CHAP info.
|
||||
if self.chap_username and self.chap_password:
|
||||
if not self._check_target_added_chap(iqn_end, self.chap_username):
|
||||
self.client.add_chap_by_target(iqn_end, self.chap_username,
|
||||
self.chap_password)
|
||||
else:
|
||||
LOG.info("chap username: %(chap_username)s exist, don't "
|
||||
"repeat to create, iscsi_iqn: %(iscsi_iqn)s.",
|
||||
{"iscsi_iqn": target_name,
|
||||
"chap_username": self.chap_username})
|
||||
|
||||
properties['auth_method'] = 'CHAP'
|
||||
properties['auth_username'] = self.chap_username
|
||||
properties['auth_password'] = self.chap_password
|
||||
|
||||
LOG.info("initialize_connection success. Return data: %(properties)s.",
|
||||
{"properties": properties})
|
||||
return {'driver_volume_type': 'iscsi', 'data': properties}
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Delete map between a volume and a host."""
|
||||
if not connector:
|
||||
target_name = self.client.query_target_by_lun(volume.name,
|
||||
self.poolid)
|
||||
self.client.unmap_lun(target_name, self.poolid,
|
||||
volume.name, self.pool)
|
||||
return
|
||||
initiator_name = connector['initiator']
|
||||
# Remove lun from target force.
|
||||
iqn_end = initiator_name.split(':')[-1]
|
||||
target_head = 'iqn.2014-10.com.szsandstone:storage:'
|
||||
target_name = target_head + iqn_end
|
||||
self.client.unmap_lun(iqn_end, self.poolid, volume.name, self.pool)
|
||||
LOG.info(
|
||||
"terminate_connection: initiator name: %(ini)s, "
|
||||
"LUN ID: %(lunid)s, "
|
||||
"Target Name: %(target_name)s.",
|
||||
{"ini": initiator_name,
|
||||
"lunid": volume.name,
|
||||
"target_name": target_name})
|
Loading…
Reference in New Issue