Merge "Revert "Remove Huawei FusionStorage Driver""
This commit is contained in:
commit
9b99ac5e2f
@ -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.
|
||||
|
479
cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py
Normal file
479
cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py
Normal 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)
|
273
cinder/tests/unit/volume/drivers/fusionstorage/test_fs_client.py
Normal file
273
cinder/tests/unit/volume/drivers/fusionstorage/test_fs_client.py
Normal 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)
|
155
cinder/tests/unit/volume/drivers/fusionstorage/test_fs_conf.py
Normal file
155
cinder/tests/unit/volume/drivers/fusionstorage/test_fs_conf.py
Normal 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)
|
48
cinder/tests/unit/volume/drivers/fusionstorage/test_utils.py
Normal file
48
cinder/tests/unit/volume/drivers/fusionstorage/test_utils.py
Normal 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)
|
0
cinder/volume/drivers/fusionstorage/__init__.py
Normal file
0
cinder/volume/drivers/fusionstorage/__init__.py
Normal file
30
cinder/volume/drivers/fusionstorage/constants.py
Normal file
30
cinder/volume/drivers/fusionstorage/constants.py
Normal 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"
|
384
cinder/volume/drivers/fusionstorage/dsware.py
Normal file
384
cinder/volume/drivers/fusionstorage/dsware.py
Normal 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
|
256
cinder/volume/drivers/fusionstorage/fs_client.py
Normal file
256
cinder/volume/drivers/fusionstorage/fs_client.py
Normal 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)
|
127
cinder/volume/drivers/fusionstorage/fs_conf.py
Normal file
127
cinder/volume/drivers/fusionstorage/fs_conf.py
Normal 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)
|
@ -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
|
||||
|
@ -87,7 +87,6 @@ release.
|
||||
* Nexenta Edge Storage Driver
|
||||
|
||||
* Ussuri
|
||||
* Huawei FusionStorage Driver
|
||||
* Nimble Storage Driver
|
||||
* ProphetStor Flexvisor Driver
|
||||
* Sheepdog Driver
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user