Merge "Revert "Remove Huawei FusionStorage Driver""

This commit is contained in:
Zuul 2020-02-27 21:19:10 +00:00 committed by Gerrit Code Review
commit 9b99ac5e2f
14 changed files with 1769 additions and 7 deletions

View File

@ -86,6 +86,8 @@ from cinder.volume.drivers.dell_emc import xtremio as \
cinder_volume_drivers_dell_emc_xtremio
from cinder.volume.drivers.fujitsu.eternus_dx import eternus_dx_common as \
cinder_volume_drivers_fujitsu_eternus_dx_eternusdxcommon
from cinder.volume.drivers.fusionstorage import dsware as \
cinder_volume_drivers_fusionstorage_dsware
from cinder.volume.drivers.hpe import hpe_3par_common as \
cinder_volume_drivers_hpe_hpe3parcommon
from cinder.volume.drivers.hpe import hpe_lefthand_iscsi as \
@ -242,6 +244,7 @@ def list_opts():
cinder_volume_driver.scst_opts,
cinder_volume_driver.backup_opts,
cinder_volume_driver.image_opts,
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
cinder_volume_drivers_infortrend_raidcmd_cli_commoncli.
infortrend_opts,
cinder_volume_drivers_inspur_as13000_as13000driver.

View File

@ -0,0 +1,479 @@
# Copyright (c) 2018 Huawei 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
from unittest import mock
import uuid
import ddt
from cinder import exception
from cinder import objects
from cinder import test
from cinder.volume import configuration as config
from cinder.volume.drivers.fusionstorage import dsware
from cinder.volume.drivers.fusionstorage import fs_client
from cinder.volume.drivers.fusionstorage import fs_conf
from cinder.volume import volume_utils
class FakeDSWAREDriver(dsware.DSWAREDriver):
def __init__(self):
self.configuration = config.Configuration(None)
self.conf = fs_conf.FusionStorageConf(self.configuration, "cinder@fs")
self.client = None
@ddt.ddt
class TestDSWAREDriver(test.TestCase):
def setUp(self):
super(TestDSWAREDriver, self).setUp()
self.fake_driver = FakeDSWAREDriver()
self.client = fs_client.RestCommon(None, None, None)
def tearDown(self):
super(TestDSWAREDriver, self).tearDown()
@mock.patch.object(fs_client.RestCommon, 'login')
def test_do_setup(self, mock_login):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
update_mocker = self.mock_object(
self.fake_driver.conf, 'update_config_value')
self.fake_driver.configuration.san_address = 'https://fake_rest_site'
self.fake_driver.configuration.san_user = 'fake_san_user'
self.fake_driver.configuration.san_password = 'fake_san_password'
self.fake_driver.do_setup('context')
update_mocker.assert_called_once_with()
mock_login.assert_called_once_with()
@mock.patch.object(fs_client.RestCommon, 'query_pool_info')
def test_check_for_setup_error(self, mock_query_pool_info):
self.fake_driver.configuration.pools_name = ['fake_pool_name']
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
result1 = [{'poolName': 'fake_pool_name'},
{'poolName': 'fake_pool_name1'}]
result2 = [{'poolName': 'fake_pool_name1'},
{'poolName': 'fake_pool_name2'}]
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(fs_client.RestCommon, 'query_pool_info')
def test__update_pool_stats(self, mock_query_pool_info):
self.fake_driver.configuration.pools_name = ['fake_pool_name']
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
result = [{'poolName': 'fake_pool_name',
'totalCapacity': 2048, 'usedCapacity': 1024},
{'poolName': 'fake_pool_name1',
'totalCapacity': 2048, 'usedCapacity': 1024}]
mock_query_pool_info.return_value = result
retval = self.fake_driver._update_pool_stats()
self.assertDictEqual(
{"volume_backend_name": 'FakeDSWAREDriver',
"driver_version": "2.0.9",
"QoS_support": False,
"thin_provisioning_support": False,
"vendor_name": "Huawei",
"storage_protocol": "SCSI",
"pools":
[{"pool_name": 'fake_pool_name', "total_capacity_gb": 2.0,
"free_capacity_gb": 1.0}]}, retval)
mock_query_pool_info.assert_called_once_with()
@mock.patch.object(fs_client.RestCommon, 'keep_alive')
@mock.patch.object(dsware.DSWAREDriver, '_update_pool_stats')
def test_get_volume_stats(self, mock__update_pool_stats, mock_keep_alive):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
result = {"success"}
mock__update_pool_stats.return_value = result
retval = self.fake_driver.get_volume_stats()
self.assertEqual(result, retval)
mock_keep_alive.assert_called_once_with()
@mock.patch.object(fs_client.RestCommon, 'query_volume_by_name')
def test__check_volume_exist(self, mock_query_volume_by_name):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
result1 = {'volName': 'fake_name'}
result2 = None
mock_query_volume_by_name.return_value = result1
retval = self.fake_driver._check_volume_exist(volume)
self.assertEqual(retval, result1)
mock_query_volume_by_name.return_value = result2
retval = self.fake_driver._check_volume_exist(volume)
self.assertIsNone(retval)
@mock.patch.object(volume_utils, 'extract_host')
@mock.patch.object(fs_client.RestCommon, 'query_pool_info')
def test__get_pool_id(self, mock_query_pool_info, mock_extract_host):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(host='host')
pool_name1 = 'fake_pool_name1'
pool_name2 = 'fake_pool_name2'
pool_info = [{'poolName': 'fake_pool_name', 'poolId': 'fake_id'},
{'poolName': 'fake_pool_name1', 'poolId': 'fake_id1'}]
mock_query_pool_info.return_value = pool_info
mock_extract_host.return_value = pool_name1
retval = self.fake_driver._get_pool_id(volume)
self.assertEqual('fake_id1', retval)
mock_extract_host.return_value = pool_name2
try:
self.fake_driver._get_pool_id(volume)
except Exception as e:
self.assertEqual(exception.InvalidInput, type(e))
def test__get_vol_name(self):
volume1 = objects.Volume(_name_id=uuid.uuid4())
volume1.update(
{"provider_location": json.dumps({"name": "fake_name"})})
volume2 = objects.Volume(_name_id=uuid.uuid4())
retval = self.fake_driver._get_vol_name(volume1)
self.assertEqual("fake_name", retval)
retval = self.fake_driver._get_vol_name(volume2)
self.assertEqual(volume2.name, retval)
@mock.patch.object(fs_client.RestCommon, 'create_volume')
@mock.patch.object(dsware.DSWAREDriver, '_get_pool_id')
def test_create_volume(self, mock__get_pool_id, mock_create_volume):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
mock__get_pool_id.return_value = 'fake_poolID'
mock_create_volume.return_value = {'result': 0}
retval = self.fake_driver.create_volume(volume)
self.assertIsNone(retval)
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(fs_client.RestCommon, 'delete_volume')
def test_delete_volume(self, mock_delete_volume, mock__check_volume_exist):
result = True
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
mock_delete_volume.return_value = {'result': 0}
mock__check_volume_exist.return_value = result
retval = self.fake_driver.delete_volume(volume)
self.assertIsNone(retval)
mock__check_volume_exist.return_value = False
retval = self.fake_driver.delete_volume(volume)
self.assertIsNone(retval)
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(fs_client.RestCommon, 'expand_volume')
def test_extend_volume(self, mock_expand_volume, mock__check_volume_exist):
result1 = True
result2 = False
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4(), size=2)
mock_expand_volume.return_value = {
'volName': 'fake_name', 'size': 'new_size'}
mock__check_volume_exist.return_value = result1
retval = self.fake_driver.extend_volume(volume=volume, new_size=3)
self.assertIsNone(retval)
mock__check_volume_exist.return_value = result2
try:
self.fake_driver.extend_volume(volume=volume, new_size=3)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(dsware.DSWAREDriver, '_check_snapshot_exist')
@mock.patch.object(fs_client.RestCommon, 'create_volume_from_snapshot')
def test_create_volume_from_snapshot(
self, mock_create_volume_from_snapshot,
mock_check_snapshot_exist, mock_check_volume_exist):
result1 = True
result2 = False
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
snapshot = objects.Snapshot(
id=uuid.uuid4(), volume_size=2, volume=volume)
volume1 = objects.Volume(_name_id=uuid.uuid4(), size=2)
volume2 = objects.Volume(_name_id=uuid.uuid4(), size=1)
mock_create_volume_from_snapshot.return_value = {'result': 0}
mock_check_volume_exist.return_value = result2
mock_check_snapshot_exist.return_value = result1
retval = self.fake_driver.create_volume_from_snapshot(
volume1, snapshot)
self.assertIsNone(retval)
mock_check_volume_exist.return_value = result1
try:
self.fake_driver.create_volume_from_snapshot(volume1, snapshot)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
mock_check_volume_exist.return_value = result2
mock_check_snapshot_exist.return_value = result2
try:
self.fake_driver.create_volume_from_snapshot(volume1, snapshot)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
mock_check_volume_exist.return_value = result2
mock_check_snapshot_exist.return_value = result1
try:
self.fake_driver.create_volume_from_snapshot(volume2, snapshot)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(fs_client.RestCommon, 'create_volume_from_volume')
def test_cloned_volume(
self, mock_create_volume_from_volume, mock__check_volume_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4(), size=1)
src_volume = objects.Volume(_name_id=uuid.uuid4())
result1 = True
result2 = False
mock__check_volume_exist.return_value = result1
retval = self.fake_driver.create_cloned_volume(volume, src_volume)
self.assertIsNone(retval)
mock_create_volume_from_volume.assert_called_once_with(
vol_name=volume.name, vol_size=volume.size * 1024,
src_vol_name=src_volume.name)
mock__check_volume_exist.return_value = result2
try:
self.fake_driver.create_cloned_volume(volume, src_volume)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
def test__get_snapshot_name(self):
snapshot1 = objects.Snapshot(id=uuid.uuid4())
snapshot1.update(
{"provider_location": json.dumps({"name": "fake_name"})})
snapshot2 = objects.Snapshot(id=uuid.uuid4())
retval = self.fake_driver._get_snapshot_name(snapshot1)
self.assertEqual("fake_name", retval)
retval = self.fake_driver._get_snapshot_name(snapshot2)
self.assertEqual(snapshot2.name, retval)
@mock.patch.object(fs_client.RestCommon, 'query_snapshot_by_name')
@mock.patch.object(dsware.DSWAREDriver, '_get_pool_id')
def test__check_snapshot_exist(
self, mock_get_pool_id, mock_query_snapshot_by_name):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
snapshot = objects.Snapshot(id=uuid.uuid4())
result1 = {'name': 'fake_name', 'totalNum': 1}
result2 = {'name': 'fake_name', 'totalNum': 0}
mock_get_pool_id.return_value = "fake_pool_id"
mock_query_snapshot_by_name.return_value = result1
retval = self.fake_driver._check_snapshot_exist(volume, snapshot)
self.assertEqual({'name': 'fake_name', 'totalNum': 1}, retval)
mock_query_snapshot_by_name.return_value = result2
retval = self.fake_driver._check_snapshot_exist(volume, snapshot)
self.assertIsNone(retval)
@mock.patch.object(fs_client.RestCommon, 'create_snapshot')
def test_create_snapshot(self, mock_create_snapshot):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
snapshot = objects.Snapshot(id=uuid.uuid4(),
volume_id=uuid.uuid4(), volume=volume)
retval = self.fake_driver.create_snapshot(snapshot)
self.assertIsNone(retval)
mock_create_snapshot.assert_called_once_with(
snapshot_name=snapshot.name, vol_name=volume.name)
@mock.patch.object(dsware.DSWAREDriver, '_check_snapshot_exist')
@mock.patch.object(fs_client.RestCommon, 'delete_snapshot')
def test_delete_snapshot(self, mock_delete_snapshot,
mock_check_snapshot_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(id=uuid.uuid4())
snapshot = objects.Snapshot(id=uuid.uuid4(), volume=volume)
result = True
mock_delete_snapshot.return_valume = {'result': 0}
mock_check_snapshot_exist.return_value = result
retval = self.fake_driver.delete_snapshot(snapshot)
self.assertIsNone(retval)
mock_check_snapshot_exist.return_value = False
retval = self.fake_driver.delete_snapshot(snapshot)
self.assertIsNone(retval)
def test__get_manager_ip(self):
context = {'host': 'host1'}
host1 = {'host1': '1.1.1.1'}
host2 = {'host2': '1.1.1.1'}
self.fake_driver.configuration.manager_ips = host1
retval = self.fake_driver._get_manager_ip(context)
self.assertEqual('1.1.1.1', retval)
self.fake_driver.configuration.manager_ips = host2
try:
self.fake_driver._get_manager_ip(context)
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(dsware.DSWAREDriver, '_get_manager_ip')
@mock.patch.object(fs_client.RestCommon, 'attach_volume')
def test__attach_volume(self, mock_attach_volume,
mock__get_manager_ip, mock__check_volume_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
attach_result1 = {volume.name: [{'devName': 'fake_path'}]}
attach_result2 = {volume.name: [{'devName': ''}]}
result1 = True
result2 = False
mock__get_manager_ip.return_value = 'fake_ip'
mock__check_volume_exist.return_value = result1
mock_attach_volume.return_value = attach_result1
retval, vol = self.fake_driver._attach_volume(
"context", volume, "properties")
self.assertEqual(
({'device': {'path': 'fake_path'}}, volume), (retval, vol))
mock__get_manager_ip.assert_called_once_with("properties")
mock__check_volume_exist.assert_called_once_with(volume)
mock_attach_volume.assert_called_once_with(volume.name, 'fake_ip')
mock__check_volume_exist.return_value = result2
try:
self.fake_driver._attach_volume("context", volume, "properties")
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
mock__check_volume_exist.return_value = result1
mock_attach_volume.return_value = attach_result2
try:
self.fake_driver._attach_volume("context", volume, "properties")
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(dsware.DSWAREDriver, '_get_manager_ip')
@mock.patch.object(fs_client.RestCommon, 'detach_volume')
def test__detach_volume(self, mock_detach_volume,
mock__get_manager_ip, mock__check_volume_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
result1 = True
result2 = False
mock__get_manager_ip.return_value = 'fake_ip'
mock_detach_volume.return_value = {'result': 0}
mock__check_volume_exist.return_value = result1
retval = self.fake_driver._detach_volume(
'context', 'attach_info', volume, 'properties')
self.assertIsNone(retval)
mock__check_volume_exist.return_value = result2
retval = self.fake_driver._detach_volume(
'context', 'attach_info', volume, 'properties')
self.assertIsNone(retval)
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(dsware.DSWAREDriver, '_get_manager_ip')
@mock.patch.object(fs_client.RestCommon, 'attach_volume')
@mock.patch.object(fs_client.RestCommon, 'query_volume_by_name')
def test_initialize_connection(self, mock_query_volume_by_name,
mock_attach_volume,
mock__get_manager_ip,
mock__check_volume_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
attach_result = {volume.name: [{'devName': 'fake_path'}]}
result1 = True
result2 = False
mock__get_manager_ip.return_value = 'fake_ip'
mock_query_volume_by_name.return_value = {'wwn': 'fake_wwn',
'volName': 'fake_name'}
mock_attach_volume.return_value = attach_result
mock__check_volume_exist.return_value = result1
retval = self.fake_driver.initialize_connection(volume, 'connector')
self.assertDictEqual(
{'driver_volume_type': 'local',
'data': {'device_path': '/dev/disk/by-id/wwn-0xfake_wwn'}},
retval)
mock__check_volume_exist.return_value = result2
try:
self.fake_driver.initialize_connection(volume, 'connector')
except Exception as e:
self.assertEqual(exception.VolumeBackendAPIException, type(e))
@mock.patch.object(dsware.DSWAREDriver, '_check_volume_exist')
@mock.patch.object(dsware.DSWAREDriver, '_get_manager_ip')
@mock.patch.object(fs_client.RestCommon, 'detach_volume')
def test_terminate_connection(self, mock_detach_volume,
mock__get_manager_ip,
mock__check_volume_exist):
self.fake_driver.client = fs_client.RestCommon(
'https://fake_rest_site', 'user', 'password')
volume = objects.Volume(_name_id=uuid.uuid4())
result1 = True
result2 = False
mock__get_manager_ip.return_value = 'fake_ip'
mock__check_volume_exist.return_value = result1
retval = self.fake_driver.terminate_connection(volume, 'connector')
self.assertIsNone(retval)
mock_detach_volume.assert_called_once_with(volume.name, 'fake_ip')
mock__check_volume_exist.return_value = result2
retval = self.fake_driver.terminate_connection('volume', 'connector')
self.assertIsNone(retval)

View File

@ -0,0 +1,273 @@
# Copyright (c) 2018 Huawei 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
from unittest import mock
import requests
from cinder import test
from cinder.tests.unit.volume.drivers.fusionstorage import test_utils
from cinder.volume.drivers.fusionstorage import fs_client
class FakeSession(test_utils.FakeBaseSession):
method_map = {
'get': {
'rest/version':
{'currentVersion': 'fake_version'},
'/storagePool$':
{'storagePools': [{'poolName': 'fake_pool_name',
'poolId': 'fake_pool_id'}]},
r'/storagePool\?poolId=0':
{'storagePools': [{'poolName': 'fake_pool_name1',
'poolId': 0}]},
r'/volume/queryByName\?volName=fake_name':
{'errorCode': 0, 'lunDetailInfo':
[{'volume_id': 'fake_id',
'volume_name': 'fake_name'}]},
r'/volume/queryById\?volId=fake_id':
{'errorCode': 0, 'lunDetailInfo':
[{'volume_id': 'fake_id',
'volume_name': 'fake_name'}]},
r'/lun/wwn/list\?wwn=fake_wwn':
{'errorCode': 0, 'lunDetailInfo':
[{'volume_id': 'fake_id',
'volume_wwn': 'fake_wwn'}]},
},
'post': {
'/sec/login': {},
'/sec/logout': {'res': 'fake_logout'},
'/sec/keepAlive': {'res': 'fake_keepAlive'},
'/volume/list': {'errorCode': 0, 'volumeList': [
{'volName': 'fake_name1', 'volId': 'fake_id1'},
{'volName': 'fake_name2', 'volId': 'fake_id2'}]},
'/volume/create': {'ID': 'fake_volume_create_id'},
'/volume/delete': {'ID': 'fake_volume_delete_id'},
'/volume/attach':
{'fake_name': [{'errorCode': '0', 'ip': 'fake_ip'}]},
'/volume/detach/': {'ID': 'fake_volume_detach_id'},
'/volume/expand': {'ID': 'fake_volume_expend_id'},
'/volume/snapshot/list':
{"snapshotList": [{"snapshot": "fake_name",
"size": "fake_size"}]},
'/snapshot/list': {'totalNum': 'fake_snapshot_num',
'snapshotList':
[{'snapName': 'fake_snapName'}]},
'/snapshot/create/': {'ID': 'fake_snapshot_create_id'},
'/snapshot/delete/': {'ID': 'fake_snapshot_delete_id'},
'/snapshot/rollback': {'ID': 'fake_snapshot_delete_id'},
'/snapshot/volume/create/': {'ID': 'fake_vol_from_snap_id'},
}
}
class TestFsclient(test.TestCase):
def setUp(self):
super(TestFsclient, self).setUp()
self.mock_object(requests, 'Session', FakeSession)
self.client = fs_client.RestCommon('https://fake_rest_site',
'fake_user',
'fake_password')
self.client.login()
def tearDown(self):
super(TestFsclient, self).tearDown()
def test_login(self):
self.assertEqual('fake_version',
self.client.version)
self.assertEqual('fake_token',
self.client.session.headers['X-Auth-Token'])
def test_keep_alive(self):
retval = self.client.keep_alive()
self.assertIsNone(retval)
def test_logout(self):
self.assertIsNone(self.client.logout())
def test_query_all_pool_info(self):
with mock.patch.object(self.client.session, 'get',
wraps=self.client.session.get) as mocker:
retval = self.client.query_pool_info()
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/'
'fake_version/storagePool', timeout=50)
self.assertListEqual(
[{'poolName': 'fake_pool_name',
'poolId': 'fake_pool_id'}], retval)
def test_query_pool_info(self):
with mock.patch.object(self.client.session, 'get',
wraps=self.client.session.get) as mocker:
retval = self.client.query_pool_info(pool_id=0)
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/'
'fake_version/storagePool?poolId=0', timeout=50)
self.assertListEqual(
[{'poolName': 'fake_pool_name1', 'poolId': 0}], retval)
def test_query_volume_by_name(self):
with mock.patch.object(self.client.session, 'get',
wraps=self.client.session.get) as mocker:
retval = self.client.query_volume_by_name(vol_name='fake_name')
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/queryByName?volName=fake_name', timeout=50)
self.assertListEqual(
[{'volume_id': 'fake_id', 'volume_name': 'fake_name'}], retval)
def test_query_volume_by_id(self):
with mock.patch.object(self.client.session, 'get',
wraps=self.client.session.get) as mocker:
retval = self.client.query_volume_by_id(vol_id='fake_id')
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/queryById?volId=fake_id', timeout=50)
self.assertListEqual(
[{'volume_id': 'fake_id', 'volume_name': 'fake_name'}], retval)
def test_create_volume(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.create_volume(
vol_name='fake_name', vol_size=1, pool_id='fake_id')
except_data = json.dumps(
{"volName": "fake_name", "volSize": 1, "poolId": "fake_id"})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/create', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_delete_volume(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.delete_volume(vol_name='fake_name')
except_data = json.dumps({"volNames": ['fake_name']})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/delete', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_attach_volume(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.attach_volume(
vol_name='fake_name', manage_ip='fake_ip')
except_data = json.dumps(
{"volName": ['fake_name'], "ipList": ['fake_ip']})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/attach', data=except_data, timeout=50)
self.assertDictEqual(
{'result': 0,
'fake_name': [{'errorCode': '0', 'ip': 'fake_ip'}]},
retval)
def test_detach_volume(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.detach_volume(
vol_name='fake_name', manage_ip='fake_ip')
except_data = json.dumps(
{"volName": ['fake_name'], "ipList": ['fake_ip']})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/detach/', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_expand_volume(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.expand_volume(
vol_name='fake_name', new_vol_size=2)
except_data = json.dumps({"volName": 'fake_name', "newVolSize": 2})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'volume/expand', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_query_snapshot_by_name(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.query_snapshot_by_name(
pool_id='fake_id', snapshot_name='fake_name')
except_data = json.dumps(
{"poolId": 'fake_id', "pageNum": 1,
"pageSize": 1000, "filters": {"volumeName": 'fake_name'}})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'snapshot/list', data=except_data, timeout=50)
self.assertDictEqual(
{'result': 0, 'totalNum': 'fake_snapshot_num',
'snapshotList': [{'snapName': 'fake_snapName'}]}, retval)
def test_create_snapshot(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.create_snapshot(
snapshot_name='fake_snap', vol_name='fake_name')
except_data = json.dumps(
{"volName": "fake_name", "snapshotName": "fake_snap"})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'snapshot/create/', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_delete_snapshot(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.delete_snapshot(snapshot_name='fake_snap')
except_data = json.dumps({"snapshotName": "fake_snap"})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'snapshot/delete/', data=except_data, timeout=50)
self.assertIsNone(retval)
def test_create_volume_from_snapshot(self):
with mock.patch.object(self.client.session, 'post',
wraps=self.client.session.post) as mocker:
retval = self.client.create_volume_from_snapshot(
snapshot_name='fake_snap', vol_name='fake_name', vol_size=2)
except_data = json.dumps({"src": 'fake_snap',
"volName": 'fake_name',
"volSize": 2})
mocker.assert_called_once_with(
'https://fake_rest_site/dsware/service/fake_version/'
'snapshot/volume/create/', data=except_data, timeout=50)
self.assertIsNone(retval)
@mock.patch.object(fs_client.RestCommon, 'create_snapshot')
@mock.patch.object(fs_client.RestCommon, 'create_volume_from_snapshot')
@mock.patch.object(fs_client.RestCommon, 'delete_snapshot')
def test_create_volume_from_volume(
self, mock_delete_snapshot, mock_volume_from_snapshot,
mock_create_snapshot):
vol_name = 'fake_name'
vol_size = 3
src_vol_name = 'src_fake_name'
temp_snapshot_name = "temp" + src_vol_name + "clone" + vol_name
retval = self.client.create_volume_from_volume(
vol_name, vol_size, src_vol_name)
mock_create_snapshot.assert_called_once_with(
vol_name=src_vol_name, snapshot_name=temp_snapshot_name)
mock_volume_from_snapshot.assert_called_once_with(
snapshot_name=temp_snapshot_name,
vol_name=vol_name, vol_size=vol_size)
mock_delete_snapshot.assert_called_once_with(
snapshot_name=temp_snapshot_name)
self.assertIsNone(retval)

View File

@ -0,0 +1,155 @@
# Copyright (c) 2018 Huawei 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 os
import shutil
import tempfile
from unittest import mock
import ddt
from six.moves import configparser
from cinder import test
from cinder.volume.drivers.fusionstorage import fs_conf
@ddt.ddt
class FusionStorageConfTestCase(test.TestCase):
def setUp(self):
super(FusionStorageConfTestCase, self).setUp()
self.tmp_dir = tempfile.mkdtemp()
self.conf = mock.Mock()
self._create_fake_conf_file()
self.fusionstorage_conf = fs_conf.FusionStorageConf(
self.conf, "cinder@fs")
def tearDown(self):
shutil.rmtree(self.tmp_dir)
super(FusionStorageConfTestCase, self).tearDown()
def _create_fake_conf_file(self):
self.conf.cinder_fusionstorage_conf_file = (
self.tmp_dir + '/cinder.conf')
config = configparser.ConfigParser()
config.add_section('backend_name')
config.set('backend_name', 'dsware_rest_url', 'https://fake_rest_site')
config.set('backend_name', 'san_login', 'fake_user')
config.set('backend_name', 'san_password', 'fake_passwd')
config.set('backend_name', 'dsware_storage_pools', 'fake_pool')
config.add_section('manager_ip')
config.set('manager_ip', 'fake_host', 'fake_ip')
config.write(open(self.conf.cinder_fusionstorage_conf_file, 'w'))
@mock.patch.object(fs_conf.FusionStorageConf, '_encode_authentication')
@mock.patch.object(fs_conf.FusionStorageConf, '_pools_name')
@mock.patch.object(fs_conf.FusionStorageConf, '_san_address')
@mock.patch.object(fs_conf.FusionStorageConf, '_san_user')
@mock.patch.object(fs_conf.FusionStorageConf, '_san_password')
def test_update_config_value(self, mock_san_password, mock_san_user,
mock_san_address, mock_pools_name,
mock_encode_authentication):
self.fusionstorage_conf.update_config_value()
mock_encode_authentication.assert_called_once_with()
mock_pools_name.assert_called_once_with()
mock_san_address.assert_called_once_with()
mock_san_user.assert_called_once_with()
mock_san_password.assert_called_once_with()
@mock.patch.object(os.path, 'exists')
def test__encode_authentication(self, mock_exists):
config = configparser.ConfigParser()
config.read(self.conf.cinder_fusionstorage_conf_file)
mock_exists.return_value = False
user_name = 'fake_user'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=user_name)
self.fusionstorage_conf._encode_authentication()
password = 'fake_passwd'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=password)
self.fusionstorage_conf._encode_authentication()
@mock.patch.object(os.path, 'exists')
@mock.patch.object(configparser.ConfigParser, 'set')
def test__rewrite_conf(self, mock_set, mock_exists):
mock_exists.return_value = False
mock_set.return_value = "success"
self.fusionstorage_conf._rewrite_conf('fake_name', 'fake_pwd')
def test__san_address(self):
address = 'https://fake_rest_site'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=address)
self.fusionstorage_conf._san_address()
self.assertEqual('https://fake_rest_site',
self.fusionstorage_conf.configuration.san_address)
def test__san_user(self):
user = '!&&&ZmFrZV91c2Vy'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=user)
self.fusionstorage_conf._san_user()
self.assertEqual(
'fake_user', self.fusionstorage_conf.configuration.san_user)
user = 'fake_user_2'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=user)
self.fusionstorage_conf._san_user()
self.assertEqual(
'fake_user_2', self.fusionstorage_conf.configuration.san_user)
def test__san_password(self):
password = '!&&&ZmFrZV9wYXNzd2Q='
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=password)
self.fusionstorage_conf._san_password()
self.assertEqual(
'fake_passwd', self.fusionstorage_conf.configuration.san_password)
password = 'fake_passwd_2'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=password)
self.fusionstorage_conf._san_password()
self.assertEqual('fake_passwd_2',
self.fusionstorage_conf.configuration.san_password)
def test__pools_name(self):
pools_name = 'fake_pool'
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=pools_name)
self.fusionstorage_conf._pools_name()
self.assertListEqual(
['fake_pool'], self.fusionstorage_conf.configuration.pools_name)
def test__manager_ip(self):
manager_ips = {'fake_host': 'fake_ip'}
self.mock_object(
self.fusionstorage_conf.configuration, 'safe_get',
return_value=manager_ips)
self.fusionstorage_conf._manager_ip()
self.assertDictEqual({'fake_host': 'fake_ip'},
self.fusionstorage_conf.configuration.manager_ips)

View File

@ -0,0 +1,48 @@
# Copyright (c) 2018 Huawei 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):
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 = {'result': 0}
resp_content.update(data)
resp = requests.Response()
resp.headers['X-Auth-Token'] = 'fake_token'
resp.status_code = 0
resp.encoding = 'utf-8'
resp._content = json.dumps(resp_content).encode('utf-8')
return resp
def get(self, url, **kwargs):
return self._get_response('get', url)
def post(self, url, **kwargs):
return self._get_response('post', url)

View File

@ -0,0 +1,30 @@
# Copyright (c) 2016 Huawei 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.
DEFAULT_TIMEOUT = 50
LOGIN_SOCKET_TIMEOUT = 32
CONNECT_ERROR = 403
ERROR_UNAUTHORIZED = 10000003
VOLUME_NOT_EXIST = (31000000, 50150005)
BASIC_URI = '/dsware/service/'
CONF_PATH = "/etc/cinder/cinder.conf"
CONF_ADDRESS = "dsware_rest_url"
CONF_MANAGER_IP = "manager_ips"
CONF_POOLS = "dsware_storage_pools"
CONF_PWD = "san_password"
CONF_USER = "san_login"

View File

@ -0,0 +1,384 @@
# Copyright (c) 2018 Huawei 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
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.fusionstorage import fs_client
from cinder.volume.drivers.fusionstorage import fs_conf
from cinder.volume.drivers.san import san
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
volume_opts = [
cfg.BoolOpt("dsware_isthin",
default=False,
help='The flag of thin storage allocation.',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.StrOpt("dsware_manager",
default='',
help='Fusionstorage manager ip addr for cinder-volume.',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.StrOpt('fusionstorageagent',
default='',
help='Fusionstorage agent ip addr range',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.StrOpt('pool_type',
default='default',
help='Pool type, like sata-2copy',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.ListOpt('pool_id_filter',
default=[],
help='Pool id permit to use',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.IntOpt('clone_volume_timeout',
default=680,
help='Create clone volume timeout',
deprecated_for_removal=True,
deprecated_since='14.0.0',
deprecated_reason='FusionStorage cinder driver refactored the '
'code with Restful method and the old CLI '
'mode has been abandon. So those '
'configuration items are no longer used.'),
cfg.DictOpt('manager_ips',
default={},
help='This option is to support the FSA to mount across the '
'different nodes. The parameters takes the standard dict '
'config form, manager_ips = host1:ip1, host2:ip2...'),
cfg.StrOpt('dsware_rest_url',
default='',
help='The address of FusionStorage array. For example, '
'"dsware_rest_url=xxx"'),
cfg.StrOpt('dsware_storage_pools',
default="",
help='The list of pools on the FusionStorage array, the '
'semicolon(;) was used to split the storage pools, '
'"dsware_storage_pools = xxx1; xxx2; xxx3"')
]
CONF = cfg.CONF
CONF.register_opts(volume_opts)
@interface.volumedriver
class DSWAREDriver(driver.VolumeDriver):
VERSION = '2.0'
CI_WIKI_NAME = 'Huawei_FusionStorage_CI'
# TODO(jsbryant) Remove driver in the 'U' release due to no py37 support.
SUPPORTED = False
def __init__(self, *args, **kwargs):
super(DSWAREDriver, self).__init__(*args, **kwargs)
if not self.configuration:
msg = _('Configuration is not found.')
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
self.configuration.append_config_values(volume_opts)
self.configuration.append_config_values(san.san_opts)
self.conf = fs_conf.FusionStorageConf(self.configuration, self.host)
self.client = None
@staticmethod
def get_driver_options():
return volume_opts
def do_setup(self, context):
self.conf.update_config_value()
url_str = self.configuration.san_address
url_user = self.configuration.san_user
url_password = self.configuration.san_password
self.client = fs_client.RestCommon(
fs_address=url_str, fs_user=url_user,
fs_password=url_password)
self.client.login()
def check_for_setup_error(self):
all_pools = self.client.query_pool_info()
all_pools_name = [p['poolName'] for p in all_pools
if p.get('poolName')]
for pool in self.configuration.pools_name:
if pool not in all_pools_name:
msg = _('Storage pool %(pool)s does not exist '
'in the FusionStorage.') % {'pool': pool}
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
def _update_pool_stats(self):
backend_name = self.configuration.safe_get(
'volume_backend_name') or self.__class__.__name__
data = {"volume_backend_name": backend_name,
"driver_version": "2.0.9",
"QoS_support": False,
"thin_provisioning_support": False,
"pools": [],
"vendor_name": "Huawei",
"storage_protocol": "SCSI",
}
all_pools = self.client.query_pool_info()
for pool in all_pools:
if pool['poolName'] in self.configuration.pools_name:
single_pool_info = self._update_single_pool_info_status(pool)
data['pools'].append(single_pool_info)
return data
def _get_capacity(self, pool_info):
pool_capacity = {}
total = float(pool_info['totalCapacity']) / units.Ki
free = (float(pool_info['totalCapacity']) -
float(pool_info['usedCapacity'])) / units.Ki
pool_capacity['total_capacity_gb'] = total
pool_capacity['free_capacity_gb'] = free
return pool_capacity
def _update_single_pool_info_status(self, pool_info):
status = {}
capacity = self._get_capacity(pool_info=pool_info)
status.update({
"pool_name": pool_info['poolName'],
"total_capacity_gb": capacity['total_capacity_gb'],
"free_capacity_gb": capacity['free_capacity_gb'],
})
return status
def get_volume_stats(self, refresh=False):
self.client.keep_alive()
stats = self._update_pool_stats()
return stats
def _check_volume_exist(self, volume):
vol_name = self._get_vol_name(volume)
result = self.client.query_volume_by_name(vol_name=vol_name)
if result:
return result
def _raise_exception(self, msg):
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def _get_pool_id(self, volume):
pool_id = None
pool_name = volume_utils.extract_host(volume.host, level='pool')
all_pools = self.client.query_pool_info()
for pool in all_pools:
if pool_name == pool['poolName']:
pool_id = pool['poolId']
if pool_id is None:
msg = _('Storage pool %(pool)s does not exist on the array. '
'Please check.') % {"pool": pool_id}
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
return pool_id
def _get_vol_name(self, volume):
provider_location = volume.get("provider_location", None)
if provider_location:
vol_name = json.loads(provider_location).get("name")
else:
vol_name = volume.name
return vol_name
def create_volume(self, volume):
pool_id = self._get_pool_id(volume)
vol_name = volume.name
vol_size = volume.size
vol_size *= units.Ki
self.client.create_volume(
pool_id=pool_id, vol_name=vol_name, vol_size=vol_size)
def delete_volume(self, volume):
vol_name = self._get_vol_name(volume)
if self._check_volume_exist(volume):
self.client.delete_volume(vol_name=vol_name)
def extend_volume(self, volume, new_size):
vol_name = self._get_vol_name(volume)
if not self._check_volume_exist(volume):
msg = _("Volume: %(vol_name)s does not exist!"
) % {"vol_name": vol_name}
self._raise_exception(msg)
else:
new_size *= units.Ki
self.client.expand_volume(vol_name, new_size)
def _check_snapshot_exist(self, volume, snapshot):
pool_id = self._get_pool_id(volume)
snapshot_name = self._get_snapshot_name(snapshot)
result = self.client.query_snapshot_by_name(
pool_id=pool_id, snapshot_name=snapshot_name)
if result.get('totalNum'):
return result
def _get_snapshot_name(self, snapshot):
provider_location = snapshot.get("provider_location", None)
if provider_location:
snapshot_name = json.loads(provider_location).get("name")
else:
snapshot_name = snapshot.name
return snapshot_name
def create_volume_from_snapshot(self, volume, snapshot):
vol_name = self._get_vol_name(volume)
snapshot_name = self._get_snapshot_name(snapshot)
vol_size = volume.size
if not self._check_snapshot_exist(snapshot.volume, snapshot):
msg = _("Snapshot: %(name)s does not exist!"
) % {"name": snapshot_name}
self._raise_exception(msg)
elif self._check_volume_exist(volume):
msg = _("Volume: %(vol_name)s already exists!"
) % {'vol_name': vol_name}
self._raise_exception(msg)
else:
vol_size *= units.Ki
self.client.create_volume_from_snapshot(
snapshot_name=snapshot_name, vol_name=vol_name,
vol_size=vol_size)
def create_cloned_volume(self, volume, src_volume):
vol_name = self._get_vol_name(volume)
src_vol_name = self._get_vol_name(src_volume)
vol_size = volume.size
vol_size *= units.Ki
if not self._check_volume_exist(src_volume):
msg = _("Volume: %(vol_name)s does not exist!"
) % {"vol_name": src_vol_name}
self._raise_exception(msg)
else:
self.client.create_volume_from_volume(
vol_name=vol_name, vol_size=vol_size,
src_vol_name=src_vol_name)
def create_snapshot(self, snapshot):
snapshot_name = self._get_snapshot_name(snapshot)
vol_name = self._get_vol_name(snapshot.volume)
self.client.create_snapshot(
snapshot_name=snapshot_name, vol_name=vol_name)
def delete_snapshot(self, snapshot):
snapshot_name = self._get_snapshot_name(snapshot)
if self._check_snapshot_exist(snapshot.volume, snapshot):
self.client.delete_snapshot(snapshot_name=snapshot_name)
def _get_manager_ip(self, context):
if self.configuration.manager_ips.get(context['host']):
return self.configuration.manager_ips.get(context['host'])
else:
msg = _("The required host: %(host)s and its manager ip are not "
"included in the configuration file."
) % {"host": context['host']}
LOG.error(msg)
raise exception.VolumeBackendAPIException(msg)
def _attach_volume(self, context, volume, properties, remote=False):
vol_name = self._get_vol_name(volume)
if not self._check_volume_exist(volume):
msg = _("Volume: %(vol_name)s does not exist!"
) % {"vol_name": vol_name}
self._raise_exception(msg)
manager_ip = self._get_manager_ip(properties)
result = self.client.attach_volume(vol_name, manager_ip)
attach_path = result[vol_name][0]['devName']
attach_info = dict()
attach_info['device'] = dict()
attach_info['device']['path'] = attach_path
if attach_path == '':
msg = _("Host attach volume failed!")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return attach_info, volume
def _detach_volume(self, context, attach_info, volume, properties,
force=False, remote=False, ignore_errors=False):
vol_name = self._get_vol_name(volume)
if self._check_volume_exist(volume):
manager_ip = self._get_manager_ip(properties)
self.client.detach_volume(vol_name, manager_ip)
def initialize_connection(self, volume, connector):
vol_name = self._get_vol_name(volume)
manager_ip = self._get_manager_ip(connector)
if not self._check_volume_exist(volume):
msg = _("Volume: %(vol_name)s does not exist!"
) % {"vol_name": vol_name}
self._raise_exception(msg)
self.client.attach_volume(vol_name, manager_ip)
volume_info = self.client.query_volume_by_name(vol_name=vol_name)
vol_wwn = volume_info.get('wwn')
by_id_path = "/dev/disk/by-id/" + "wwn-0x%s" % vol_wwn
properties = {'device_path': by_id_path}
return {'driver_volume_type': 'local',
'data': properties}
def terminate_connection(self, volume, connector, **kwargs):
if self._check_volume_exist(volume):
manager_ip = self._get_manager_ip(connector)
vol_name = self._get_vol_name(volume)
self.client.detach_volume(vol_name, manager_ip)
def create_export(self, context, volume, connector):
pass
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass

View File

@ -0,0 +1,256 @@
# Copyright (c) 2018 Huawei 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
from oslo_log import log as logging
import requests
import six
from cinder import exception
from cinder.i18n import _
from cinder.volume.drivers.fusionstorage import constants
LOG = logging.getLogger(__name__)
class RestCommon(object):
def __init__(self, fs_address, fs_user, fs_password):
self.address = fs_address
self.user = fs_user
self.password = fs_password
self.session = None
self.token = None
self.version = None
self.init_http_head()
LOG.warning("Suppressing requests library SSL Warnings")
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecureRequestWarning)
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecurePlatformWarning)
def init_http_head(self):
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json;charset=UTF-8",
})
self.session.verify = False
def call(self, url, method, data=None,
call_timeout=constants.DEFAULT_TIMEOUT,
get_version=False, filter_flag=False, json_flag=False):
kwargs = {'timeout': call_timeout}
if data:
kwargs['data'] = json.dumps(data)
if not get_version:
call_url = self.address + constants.BASIC_URI + self.version + url
else:
call_url = self.address + constants.BASIC_URI + url
func = getattr(self.session, method.lower())
try:
result = func(call_url, **kwargs)
except Exception as err:
LOG.error('Bad response from server: %(url)s. '
'Error: %(err)s'), {'url': url, 'err': err}
return {"error": {
"code": constants.CONNECT_ERROR,
"description": "Connect to server error."}}
try:
result.raise_for_status()
except requests.HTTPError as exc:
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
else:
return result.json()
def _assert_rest_result(self, result, err_str):
if result.get('result') != 0:
msg = (_('%(err)s\nresult: %(res)s.') % {'err': err_str,
'res': result})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def get_version(self):
url = 'rest/version'
self.session.headers.update({
"Referer": self.address + constants.BASIC_URI
})
result = self.call(url=url, method='GET', get_version=True)
self._assert_rest_result(result, _('Get version session error.'))
if result.get("currentVersion"):
self.version = result["currentVersion"]
def login(self):
self.get_version()
url = '/sec/login'
data = {"userName": self.user, "password": self.password}
result = self.call(url, 'POST', data=data,
call_timeout=constants.LOGIN_SOCKET_TIMEOUT,
filter_flag=True, json_flag=True)
self._assert_rest_result(result.json(), _('Login session error.'))
self.token = result.headers['X-Auth-Token']
self.session.headers.update({
"x-auth-token": self.token
})
def logout(self):
url = '/sec/logout'
if self.address:
result = self.call(url, 'POST')
self._assert_rest_result(result, _('Logout session error.'))
def keep_alive(self):
url = '/sec/keepAlive'
result = self.call(url, 'POST', filter_flag=True)
if result.get('result') == constants.ERROR_UNAUTHORIZED:
try:
self.login()
except Exception:
LOG.error('The FusionStorage may have been powered off. '
'Power on the FusionStorage and then log in.')
raise
else:
self._assert_rest_result(result, _('Keep alive session error.'))
def query_pool_info(self, pool_id=None):
pool_id = str(pool_id)
if pool_id != 'None':
url = '/storagePool' + '?poolId=' + pool_id
else:
url = '/storagePool'
result = self.call(url, 'GET', filter_flag=True)
self._assert_rest_result(result, _("Query pool session error."))
return result['storagePools']
def query_volume_by_name(self, vol_name):
url = '/volume/queryByName?volName=' + vol_name
result = self.call(url, 'GET')
if result.get('errorCode') in constants.VOLUME_NOT_EXIST:
return None
self._assert_rest_result(
result, _("Query volume by name session error"))
return result.get('lunDetailInfo')
def query_volume_by_id(self, vol_id):
url = '/volume/queryById?volId=' + vol_id
result = self.call(url, 'GET')
if result.get('errorCode') in constants.VOLUME_NOT_EXIST:
return None
self._assert_rest_result(
result, _("Query volume by ID session error"))
return result.get('lunDetailInfo')
def create_volume(self, vol_name, vol_size, pool_id):
url = '/volume/create'
params = {"volName": vol_name, "volSize": vol_size, "poolId": pool_id}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Create volume session error.'))
def delete_volume(self, vol_name):
url = '/volume/delete'
params = {"volNames": [vol_name]}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Delete volume session error.'))
def attach_volume(self, vol_name, manage_ip):
url = '/volume/attach'
params = {"volName": [vol_name], "ipList": [manage_ip]}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Attach volume session error.'))
if int(result[vol_name][0]['errorCode']) != 0:
msg = _("Host attach volume failed!")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return result
def detach_volume(self, vol_name, manage_ip):
url = '/volume/detach/'
params = {"volName": [vol_name], "ipList": [manage_ip]}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Detach volume session error.'))
def expand_volume(self, vol_name, new_vol_size):
url = '/volume/expand'
params = {"volName": vol_name, "newVolSize": new_vol_size}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Expand volume session error.'))
def query_snapshot_by_name(self, pool_id, snapshot_name, page_num=1,
page_size=1000):
# Filter the snapshot according to the name, while the "page_num" and
# "page_size" must be set while using the interface.
url = '/snapshot/list'
params = {"poolId": pool_id, "pageNum": page_num,
"pageSize": page_size,
"filters": {"volumeName": snapshot_name}}
result = self.call(url, "POST", params)
self._assert_rest_result(
result, _('query snapshot list session error.'))
return result
def create_snapshot(self, snapshot_name, vol_name):
url = '/snapshot/create/'
params = {"volName": vol_name, "snapshotName": snapshot_name}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Create snapshot error.'))
def delete_snapshot(self, snapshot_name):
url = '/snapshot/delete/'
params = {"snapshotName": snapshot_name}
result = self.call(url, "POST", params)
self._assert_rest_result(result, _('Delete snapshot session error.'))
def create_volume_from_snapshot(self, snapshot_name, vol_name, vol_size):
url = '/snapshot/volume/create/'
params = {"src": snapshot_name, "volName": vol_name,
"volSize": vol_size}
result = self.call(url, "POST", params)
self._assert_rest_result(
result, _('create volume from snapshot session error.'))
def create_volume_from_volume(self, vol_name, vol_size, src_vol_name):
temp_snapshot_name = "temp" + src_vol_name + "clone" + vol_name
self.create_snapshot(vol_name=src_vol_name,
snapshot_name=temp_snapshot_name)
self.create_volume_from_snapshot(snapshot_name=temp_snapshot_name,
vol_name=vol_name, vol_size=vol_size)
self.delete_snapshot(snapshot_name=temp_snapshot_name)

View File

@ -0,0 +1,127 @@
# Copyright (c) 2018 Huawei 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 base64
import os
from oslo_log import log as logging
import six
from six.moves import configparser
from cinder import exception
from cinder.i18n import _
from cinder import utils
from cinder.volume.drivers.fusionstorage import constants
LOG = logging.getLogger(__name__)
class FusionStorageConf(object):
def __init__(self, configuration, host):
self.configuration = configuration
self._check_host(host)
def _check_host(self, host):
if host and len(host.split('@')) > 1:
self.host = host.split('@')[1]
else:
msg = _("The host %s is not reliable. Please check cinder-volume "
"backend.") % host
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
def update_config_value(self):
self._encode_authentication()
self._pools_name()
self._san_address()
self._san_user()
self._san_password()
def _encode_authentication(self):
name_node = self.configuration.safe_get(constants.CONF_USER)
pwd_node = self.configuration.safe_get(constants.CONF_PWD)
need_encode = False
if name_node is not None and not name_node.startswith('!&&&'):
encoded = base64.b64encode(six.b(name_node)).decode()
name_node = '!&&&' + encoded
need_encode = True
if pwd_node is not None and not pwd_node.startswith('!&&&'):
encoded = base64.b64encode(six.b(pwd_node)).decode()
pwd_node = '!&&&' + encoded
need_encode = True
if need_encode:
self._rewrite_conf(name_node, pwd_node)
def _rewrite_conf(self, name_node, pwd_node):
if os.path.exists(constants.CONF_PATH):
utils.execute("chmod", "666", constants.CONF_PATH,
run_as_root=True)
conf = configparser.ConfigParser()
conf.read(constants.CONF_PATH)
if name_node:
conf.set(self.host, constants.CONF_USER, name_node)
if pwd_node:
conf.set(self.host, constants.CONF_PWD, pwd_node)
fh = open(constants.CONF_PATH, 'w')
conf.write(fh)
fh.close()
utils.execute("chmod", "644", constants.CONF_PATH,
run_as_root=True)
def _assert_text_result(self, text, mess):
if not text:
msg = _("%s is not configured.") % mess
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
def _san_address(self):
address = self.configuration.safe_get(constants.CONF_ADDRESS)
self._assert_text_result(address, mess=constants.CONF_ADDRESS)
setattr(self.configuration, 'san_address', address)
def _decode_text(self, text):
return (base64.b64decode(six.b(text[4:])).decode() if
text.startswith('!&&&') else text)
def _san_user(self):
user_text = self.configuration.safe_get(constants.CONF_USER)
self._assert_text_result(user_text, mess=constants.CONF_USER)
user = self._decode_text(user_text)
setattr(self.configuration, 'san_user', user)
def _san_password(self):
pwd_text = self.configuration.safe_get(constants.CONF_PWD)
self._assert_text_result(pwd_text, mess=constants.CONF_PWD)
pwd = self._decode_text(pwd_text)
setattr(self.configuration, 'san_password', pwd)
def _pools_name(self):
pools_name = self.configuration.safe_get(constants.CONF_POOLS)
self._assert_text_result(pools_name, mess=constants.CONF_POOLS)
pools = set(x.strip() for x in pools_name.split(';') if x.strip())
if not pools:
msg = _('No valid storage pool configured.')
LOG.error(msg)
raise exception.InvalidInput(msg)
setattr(self.configuration, 'pools_name', list(pools))
def _manager_ip(self):
manager_ips = self.configuration.safe_get(constants.CONF_MANAGER_IP)
self._assert_text_result(manager_ips, mess=constants.CONF_MANAGER_IP)
setattr(self.configuration, 'manager_ips', manager_ips)

View File

@ -78,6 +78,9 @@ title=Huawei 18000 Series Driver (iSCSI, FC)
[driver.huawei_dorado]
title=Huawei Dorado V3, V6 Series Driver (iSCSI, FC)
[driver.huawei_fusionstorage]
title=Huawei FusionStorage Driver (dsware)
[driver.ibm_ds8k]
title=IBM DS8k Storage Driver (FC)
@ -202,6 +205,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=complete
driver.ibm_ds8k=complete
driver.ibm_flashsystem=missing
@ -260,6 +264,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=complete
driver.infinidat=complete
driver.ibm_ds8k=complete
driver.ibm_flashsystem=complete
@ -318,6 +323,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=complete
driver.infinidat=missing
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing
@ -379,6 +385,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=complete
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing
@ -439,6 +446,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=missing
driver.ibm_ds8k=complete
driver.ibm_flashsystem=missing
@ -500,6 +508,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=missing
driver.ibm_ds8k=complete
driver.ibm_flashsystem=missing
@ -560,6 +569,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=complete
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing
@ -621,6 +631,7 @@ driver.huawei_v5=complete
driver.huawei_f_v5=complete
driver.huawei_18000=complete
driver.huawei_dorado=complete
driver.huawei_fusionstorage=missing
driver.infinidat=missing
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing
@ -682,6 +693,7 @@ driver.huawei_v5=missing
driver.huawei_f_v5=missing
driver.huawei_18000=missing
driver.huawei_dorado=missing
driver.huawei_fusionstorage=missing
driver.infinidat=complete
driver.ibm_ds8k=complete
driver.ibm_flashsystem=missing
@ -740,6 +752,7 @@ driver.huawei_f_v5=missing
driver.huawei_v5=missing
driver.huawei_18000=missing
driver.huawei_dorado=missing
driver.huawei_fusionstorage=missing
driver.infinidat=missing
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing
@ -802,6 +815,7 @@ driver.huawei_f_v5=missing
driver.huawei_v5=missing
driver.huawei_18000=missing
driver.huawei_dorado=missing
driver.huawei_fusionstorage=missing
driver.infinidat=missing
driver.ibm_ds8k=missing
driver.ibm_flashsystem=missing

View File

@ -87,7 +87,6 @@ release.
* Nexenta Edge Storage Driver
* Ussuri
* Huawei FusionStorage Driver
* Nimble Storage Driver
* ProphetStor Flexvisor Driver
* Sheepdog Driver

View File

@ -1,6 +0,0 @@
upgrade:
- |
The Huawei FusionStorage driver was marked unsupported in the
Train release and has now been removed. All data on
FusionStorage backends should be migrated to a supported
storage backend before upgrading your Cinder installation.