NVMe-TCP volume driver for Fungible Storage
Add NVMe-TCP cinder volume driver for the Fungible Storage Cluster. Implements: blueprint fungible-volume-driver Change-Id: Ie3a8ef6694b1c6ac857d3314890f6b8eeef44bab
This commit is contained in:
parent
4c9b76b937
commit
00cb2887ba
@ -100,6 +100,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.fungible import driver as \
|
||||
cinder_volume_drivers_fungible_driver
|
||||
from cinder.volume.drivers.fusionstorage import dsware as \
|
||||
cinder_volume_drivers_fusionstorage_dsware
|
||||
from cinder.volume.drivers.hitachi import hbsd_common as \
|
||||
@ -284,6 +286,7 @@ def list_opts():
|
||||
cinder_volume_driver.backup_opts,
|
||||
cinder_volume_driver.image_opts,
|
||||
cinder_volume_drivers_datera_dateraiscsi.d_opts,
|
||||
cinder_volume_drivers_fungible_driver.fungible_opts,
|
||||
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
|
||||
cinder_volume_drivers_infortrend_raidcmd_cli_commoncli.
|
||||
infortrend_opts,
|
||||
|
26
cinder/tests/unit/volume/drivers/fungible/test_adapter.py
Normal file
26
cinder/tests/unit/volume/drivers/fungible/test_adapter.py
Normal file
@ -0,0 +1,26 @@
|
||||
# (c) Copyright 2022 Fungible, Inc. 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
|
||||
class MockResource(mock.Mock):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MockResource, self).__init__(*args, **kwargs)
|
||||
if 'name' in kwargs:
|
||||
self.name = kwargs['name']
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.kwargs[key]
|
935
cinder/tests/unit/volume/drivers/fungible/test_driver.py
Normal file
935
cinder/tests/unit/volume/drivers/fungible/test_driver.py
Normal file
@ -0,0 +1,935 @@
|
||||
# (c) Copyright 2022 Fungible, Inc. 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 unittest
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder.image import image_utils
|
||||
from cinder.objects import fields
|
||||
from cinder.tests.unit import fake_constants
|
||||
from cinder.tests.unit import fake_snapshot
|
||||
from cinder.tests.unit import fake_volume
|
||||
from cinder.tests.unit.image import fake as fake_image
|
||||
from cinder.tests.unit.volume.drivers.fungible import test_adapter
|
||||
from cinder import volume
|
||||
from cinder.volume import configuration
|
||||
from cinder.volume.drivers.fungible import constants
|
||||
from cinder.volume.drivers.fungible import driver
|
||||
from cinder.volume.drivers.fungible import rest_client
|
||||
from cinder.volume.drivers.fungible import \
|
||||
swagger_api_client as swagger_client
|
||||
from cinder.volume import volume_types
|
||||
from cinder.volume import volume_utils
|
||||
|
||||
common_success_res = swagger_client.CommonResponseFields(
|
||||
status=True, message='healthy')
|
||||
common_failure_res = swagger_client.CommonResponseFields(
|
||||
status=False, message='error')
|
||||
success_uuid = swagger_client.ResponseDataWithCreateUuid(
|
||||
status=True, data={'uuid': 'mock_id'})
|
||||
success_response = swagger_client.SuccessResponseFields(
|
||||
status=True, message="mock_message")
|
||||
get_volume_details = swagger_client.ResponseDataWithCreateUuid(
|
||||
status=True,
|
||||
data={"dpu": "mock_dpu_uuid", "secy_dpu": "mock_dpu_uuid",
|
||||
"ports": {"mock_id": {"host_nqn": "mock_nqn",
|
||||
"host_uuid": "mock_host_id", "transport": "TCP"}}})
|
||||
get_topology = swagger_client.ResponseDpuDriveHierarchy(
|
||||
status=True,
|
||||
data={"mock_device_uuid": {
|
||||
"available": True, "dpus": [{"dataplane_ip": "mock_dataplae_ip",
|
||||
"uuid": "mock_dpu_uuid"}]}})
|
||||
get_host_id_list = swagger_client.ResponseDataWithListOfHostUuids(
|
||||
status=True,
|
||||
data={"total_hosts_with_fac": 0, "total_hosts_without_fac": 1,
|
||||
"host_uuids": ["mock_host_id"]})
|
||||
get_host_info = swagger_client.ResponseDataWithHostInfo(
|
||||
status=True,
|
||||
data={"host_uuid": "mock_host_id", "host_nqn": "mock_nqn",
|
||||
"fac_enabled": False})
|
||||
fetch_hosts_with_ids = swagger_client.ResponseDataWithListOfHosts(
|
||||
status=True,
|
||||
data=[
|
||||
{
|
||||
"host_uuid": "mock_host_id", "host_nqn": "mock_nqn",
|
||||
"fac_enabled": False
|
||||
}
|
||||
])
|
||||
create_copy_task = swagger_client.ResponseCreateVolumeCopyTask(
|
||||
status=True, data={'task_uuid': 'mock_id'})
|
||||
get_task_success = swagger_client.ResponseGetVolumeCopyTask(
|
||||
status=True, data={'task_state': 'SUCCESS'})
|
||||
|
||||
|
||||
class FungibleDriverTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(FungibleDriverTest, self).setUp()
|
||||
self.configuration = mock.Mock(spec=configuration.Configuration)
|
||||
self.configuration.san_ip = '127.0.0.1'
|
||||
self.configuration.san_api_port = 443
|
||||
self.configuration.san_login = 'admin'
|
||||
self.configuration.san_password = 'password'
|
||||
self.configuration.nvme_connect_port = 4420
|
||||
self.configuration.api_enable_ssl = False
|
||||
self.driver = driver.FungibleDriver(configuration=self.configuration)
|
||||
self.driver.do_setup(context=None)
|
||||
self.context = context.get_admin_context()
|
||||
self.api_exception = swagger_client.ApiException(
|
||||
status=400,
|
||||
reason="Bad Request",
|
||||
http_resp=self.get_api_exception_response())
|
||||
|
||||
@staticmethod
|
||||
def get_volume():
|
||||
volume = fake_volume.fake_volume_obj(mock.MagicMock())
|
||||
volume.size = 1
|
||||
volume.provider_id = fake_constants.UUID1
|
||||
volume.migration_status = ''
|
||||
volume.id = str(uuid.uuid4())
|
||||
volume.display_name = 'volume'
|
||||
volume.host = 'mock_host_name'
|
||||
volume.volume_type_id = 'mock_volume_type_id'
|
||||
volume.metadata = {}
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def get_snapshot():
|
||||
snapshot = fake_snapshot.fake_snapshot_obj(mock.MagicMock())
|
||||
snapshot.display_name = 'snapshot'
|
||||
snapshot.provider_id = fake_constants.UUID1
|
||||
snapshot.id = str(uuid.uuid4())
|
||||
snapshot.volume = FungibleDriverTest.get_volume()
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def get_connector():
|
||||
return {"nqn": "mock_nqn"}
|
||||
|
||||
@staticmethod
|
||||
def get_specs():
|
||||
return {
|
||||
constants.FSC_SPACE_ALLOCATION_POLICY: "write_optimized",
|
||||
constants.FSC_COMPRESSION: "true",
|
||||
constants.FSC_QOS_BAND: "gold",
|
||||
constants.FSC_SNAPSHOTS: "false",
|
||||
constants.FSC_BLK_SIZE: "4096"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_metadata():
|
||||
return {
|
||||
constants.FSC_SPACE_ALLOCATION_POLICY: "write_optimized",
|
||||
constants.FSC_COMPRESSION: "false",
|
||||
constants.FSC_QOS_BAND: "bronze",
|
||||
constants.FSC_EC_SCHEME: constants.EC_4_2
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_api_exception_response():
|
||||
return test_adapter.MockResource(
|
||||
status=False,
|
||||
data='{"error_message":"mock_error_message","status":false}')
|
||||
|
||||
'''@staticmethod
|
||||
def get_volume_details():
|
||||
return {
|
||||
"data": {
|
||||
"ports": {
|
||||
"mock_id": {
|
||||
"host_nqn": "mock_nqn",
|
||||
"ip": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'''
|
||||
|
||||
def test_get_driver_options(self):
|
||||
self.assertIsNotNone(self.driver.get_driver_options())
|
||||
|
||||
def test_volume_stats(self):
|
||||
self.assertIsNotNone(self.driver.get_volume_stats())
|
||||
|
||||
@mock.patch.object(swagger_client.ApigatewayApi, 'get_fc_health')
|
||||
def test_check_for_setup_error_success(self, mock_success_response):
|
||||
mock_success_response.return_value = common_success_res
|
||||
result = self.driver.check_for_setup_error()
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch.object(swagger_client.ApigatewayApi, 'get_fc_health')
|
||||
def test_check_for_setup_error_fail(self, mock_staus):
|
||||
mock_staus.return_value = common_failure_res
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'check_for_setup_error')
|
||||
def test_check_for_setup_error_exception(self, mock_staus):
|
||||
mock_staus.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'check_for_setup_error')
|
||||
def test_check_for_setup_error_api_exception(self, mock_exception):
|
||||
mock_exception.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_get_volume_stats_without_volume_type(self, mock_get_volume_type):
|
||||
volume = self.get_volume()
|
||||
volume.volume_type_id = "mock_id"
|
||||
mock_get_volume_type.return_value = {"extra_specs": self.get_specs()}
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver._get_volume_type_extra_specs(self, volume=volume)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_get_volume_stats_with_volume_type(self, mock_get_volume_type):
|
||||
volume = {"volume_type_id": "mock_id"}
|
||||
extra_specs = self.get_specs()
|
||||
extra_specs.update({constants.FSC_VOL_TYPE: constants.VOLUME_TYPE_RAW})
|
||||
mock_get_volume_type.return_value = {"extra_specs": extra_specs}
|
||||
self.assertIsNotNone(
|
||||
self.driver._get_volume_type_extra_specs(self, volume=volume))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_volume(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_ec_volume_8_2(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{constants.FSC_EC_SCHEME: "8_2"},
|
||||
constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_replicated_volume(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_REPLICA])
|
||||
ret = self.driver.create_volume(volume)
|
||||
mock_create_volume.return_value = success_uuid
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_volume_with_specs(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
mock_ret = self.get_specs()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[mock_ret, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_volume_with_metadata(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
volume['metadata'].update(self.get_metadata())
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_volume_with_fault_domains(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_RAW])
|
||||
volume['metadata'].update(self.get_metadata())
|
||||
volume['metadata'].update({constants.FSC_FD_IDS: 'fake_id1, fake_id2'})
|
||||
volume['metadata'].update(
|
||||
{constants.FSC_FD_OP: constants.FSC_FD_OPS[0]})
|
||||
mock_create_volume.return_value = success_uuid
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_negative_create_volume_with_fault_domains(
|
||||
self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_RAW])
|
||||
volume['metadata'].update(self.get_metadata())
|
||||
volume['metadata'].update(
|
||||
{constants.FSC_FD_IDS: 'fake_id1,fake_id2,fake_id3'})
|
||||
volume['metadata'].update({constants.FSC_FD_OP: 'mock_value'})
|
||||
mock_create_volume.return_value = success_uuid
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_negative_create_volume_without_fault_domains_op(
|
||||
self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_RAW])
|
||||
volume['metadata'].update(self.get_metadata())
|
||||
volume['metadata'].update({constants.FSC_FD_IDS: 'fake_id1,fake_id2'})
|
||||
mock_create_volume.return_value = success_uuid
|
||||
ret = self.driver.create_volume(volume)
|
||||
self.assertIsNotNone(ret)
|
||||
self.assertEqual(volume['size'], ret['size'])
|
||||
|
||||
def test_negative_create_volume_with_metadata(self):
|
||||
volume = self.get_volume()
|
||||
volume['metadata'].update(self.get_specs())
|
||||
volume['metadata'].update({constants.FSC_QOS_BAND: 'wrong value'})
|
||||
volume['metadata'].update(
|
||||
{constants.FSC_SPACE_ALLOCATION_POLICY: 'wrong value'})
|
||||
volume['metadata'].update({constants.FSC_COMPRESSION: 'wrong value'})
|
||||
volume['metadata'].update({constants.FSC_EC_SCHEME: 'wrong value'})
|
||||
volume['metadata'].update({constants.FSC_SNAPSHOTS: 'wrong value'})
|
||||
volume['metadata'].update({constants.FSC_BLK_SIZE: 'wrong value'})
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
def test_negative_encrypted_create_volume(self):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
volume['metadata'].update(self.get_metadata())
|
||||
volume['metadata'].update({constants.FSC_KMIP_SECRET_KEY: 'fake key'})
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
def test_negative_create_volume_with_specs(self):
|
||||
volume = self.get_volume()
|
||||
mock_ret = self.get_specs()
|
||||
mock_ret.update({constants.FSC_QOS_BAND: 'wrong value'})
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[mock_ret, constants.VOLUME_TYPE_EC])
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'create_volume')
|
||||
def test_negative_create_volume_api_exception(self, mock_create_volume):
|
||||
volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_volume')
|
||||
def test_delete_volume(self, mock_delete_volume):
|
||||
volume = self.get_volume()
|
||||
mock_delete_volume.return_value = success_response
|
||||
self.assertIsNone(self.driver.delete_volume(volume))
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'delete_volume')
|
||||
def test_negative_delete_volume_exception(self, mock_delete_volume):
|
||||
mock_volume = self.get_volume()
|
||||
mock_volume['provider_id'] = fake_constants.UUID1
|
||||
mock_delete_volume.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_volume(mock_volume)
|
||||
|
||||
def test_negative_delete_volume_without_provider_id(self):
|
||||
volume = self.get_volume()
|
||||
volume['provider_id'] = None
|
||||
self.assertIsNone(self.driver.delete_volume(volume))
|
||||
|
||||
def test_negative_delete_volume_without_provider_id_attr(self):
|
||||
volume = self.get_volume()
|
||||
del volume.provider_id
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_volume')
|
||||
def test_negative_delete_volume_api_exception(self, mock_delete_volume):
|
||||
volume = self.get_volume()
|
||||
mock_delete_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'attach_volume')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_id_list')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_info')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_hierarchical_topology')
|
||||
def test_initialize_connection(
|
||||
self, mock_get_topology, mock_get_host_info, mock_get_host_id_list,
|
||||
mock_attach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_attach_volume.return_value = success_uuid
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_get_host_id_list.return_value = get_host_id_list
|
||||
mock_get_host_info.return_value = get_host_info
|
||||
mock_get_topology.return_value = get_topology
|
||||
conn_info = self.driver.initialize_connection(mock_volume, connector)
|
||||
self.assertIsNotNone(conn_info)
|
||||
self.assertEqual(conn_info.get("driver_volume_type"), "nvmeof")
|
||||
self.assertIsNotNone(conn_info.get("data"))
|
||||
self.assertEqual(
|
||||
conn_info.get("data").get("vol_uuid"), fake_constants.UUID1)
|
||||
self.assertEqual(conn_info.get("data").get("host_nqn"),
|
||||
self.get_connector().get("nqn"))
|
||||
'''Add more validation here'''
|
||||
|
||||
def test_negative_initialize_connection_without_nqn(self):
|
||||
mock_volume = self.get_volume()
|
||||
connector = {}
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.initialize_connection(mock_volume, connector)
|
||||
|
||||
def test_negative_initialize_connection_without_provider_id(self):
|
||||
mock_volume = {}
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_volume["provider_id"] = None
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.initialize_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'attach_volume')
|
||||
def test_negative_initialize_connection_api_exception(
|
||||
self, mock_attach_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_attach_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.initialize_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'attach_volume')
|
||||
def test_initialize_connection_exception(self, mock_attach_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_attach_volume.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.initialize_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'attach_volume')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_id_list')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_info')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_hierarchical_topology')
|
||||
def test_initialize_connection_iops_connection(
|
||||
self, mock_get_topology, mock_get_host_info, mock_get_host_id_list,
|
||||
mock_attach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_attach_volume.return_value = success_uuid
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_get_host_id_list.return_value = get_host_id_list
|
||||
mock_get_host_info.return_value = get_host_info
|
||||
mock_get_topology.return_value = get_topology
|
||||
connector[constants.FSC_IOPS_IMG_MIG] = True
|
||||
conn_info = self.driver.initialize_connection(mock_volume, connector)
|
||||
self.assertIsNotNone(conn_info)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'attach_volume')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_id_list')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_info')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_hierarchical_topology')
|
||||
def test_initialize_connection_iops_migration(
|
||||
self, mock_get_topology, mock_get_host_info, mock_get_host_id_list,
|
||||
mock_attach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_attach_volume.return_value = success_uuid
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_get_host_id_list.return_value = get_host_id_list
|
||||
mock_get_host_info.return_value = get_host_info
|
||||
mock_get_topology.return_value = get_topology
|
||||
mock_volume['migration_status'] = "migrating"
|
||||
conn_info = self.driver.initialize_connection(mock_volume, connector)
|
||||
self.assertIsNotNone(conn_info)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_port')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_id_list')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'fetch_hosts_with_ids')
|
||||
def test_terminate_connection(
|
||||
self, mock_fetch_hosts_with_ids, mock_get_host_id_list,
|
||||
mock_detach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_get_host_id_list.return_value = get_host_id_list
|
||||
mock_fetch_hosts_with_ids.return_value = fetch_hosts_with_ids
|
||||
mock_detach_volume.return_value = success_uuid
|
||||
self.assertIsNone(self.driver.terminate_connection(
|
||||
mock_volume, connector))
|
||||
|
||||
def test_negative_terminate_connection_without_provider_id(self):
|
||||
mock_volume = {}
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_volume["provider_id"] = None
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.terminate_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_port')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'get_host_id_list')
|
||||
@mock.patch.object(swagger_client.TopologyApi, 'fetch_hosts_with_ids')
|
||||
def test_terminate_connection_force_detach(
|
||||
self, mock_fetch_hosts_with_ids, mock_get_host_id_list,
|
||||
mock_detach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_get_host_id_list.return_value = get_host_id_list
|
||||
mock_fetch_hosts_with_ids.return_value = fetch_hosts_with_ids
|
||||
mock_detach_volume.return_value = success_uuid
|
||||
connector = None
|
||||
self.assertIsNone(self.driver.terminate_connection(
|
||||
mock_volume, connector))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
def test_negative_terminate_connection_without_nqn(self, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
connector = {}
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.terminate_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'get_volume_detail')
|
||||
def test_negative_terminate_connection_without_port(self, mock_output):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_output.return_value = {'data': {'ports': None}}
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.terminate_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'get_volume_detail')
|
||||
def test_negative_terminate_connection_with_invalid_port(
|
||||
self, mock_output):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_output.return_value = get_volume_details
|
||||
connector['nqn'] = "dummy_nqn"
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.terminate_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_port')
|
||||
def test_negative_terminate_connection_api_exception(
|
||||
self, mock_detach_volume, mock_get_volume):
|
||||
mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
connector = self.get_connector()
|
||||
mock_get_volume.return_value = get_volume_details
|
||||
mock_detach_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.terminate_connection(mock_volume, connector)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_volume_from_ec_snapshot(self, mock_create_volume):
|
||||
mock_snapshot = self.get_snapshot()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_snapshot.volume = self.get_volume()
|
||||
mock_snapshot.provider_id = fake_constants.UUID1
|
||||
mock_volume2 = self.get_volume()
|
||||
mock_create_volume.return_value = success_uuid
|
||||
new_vol_ret = self.driver.create_volume_from_snapshot(
|
||||
mock_volume2, mock_snapshot)
|
||||
self.assertIsNotNone(new_vol_ret)
|
||||
self.assertEqual(mock_volume2['size'], new_vol_ret['size'])
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'create_volume')
|
||||
def test_create_volume_from_snapshot_exception(
|
||||
self, mock_get_volume_detail):
|
||||
mock_volume = self.get_volume()
|
||||
mock_snapshot = self.get_snapshot()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_get_volume_detail.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume_from_snapshot(mock_volume, mock_snapshot)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'create_volume')
|
||||
def test_create_volume_from_snapshot_APIException(
|
||||
self, mock_create_volume):
|
||||
mock_volume = self.get_volume()
|
||||
mock_snapshot = self.get_snapshot()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_volume_from_snapshot(mock_volume, mock_snapshot)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_snapshot')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_snapshot')
|
||||
def test_create_cloned_ec_volume(
|
||||
self, mock_create_snapshot, mock_create_volume,
|
||||
mock_create_volume_copy_task, mock_get_task, mock_delete_snapshot,
|
||||
mock_delete_task):
|
||||
target_mock_volume = self.get_volume()
|
||||
source_mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_snapshot.return_value = success_uuid
|
||||
mock_create_volume.return_value = success_uuid
|
||||
mock_create_volume_copy_task.return_value = create_copy_task
|
||||
mock_get_task.return_value = get_task_success
|
||||
mock_delete_snapshot.return_value = success_response
|
||||
mock_delete_task.return_value = success_response
|
||||
self.assertIsNotNone(self.driver.create_cloned_volume(
|
||||
target_mock_volume, source_mock_volume))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_snapshot')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_snapshot')
|
||||
def test_create_cloned_ec_volume_delete_task_exception(
|
||||
self, mock_create_snapshot, mock_create_volume,
|
||||
mock_create_volume_copy_task, mock_get_task, mock_delete_snapshot,
|
||||
mock_delete_task):
|
||||
target_mock_volume = self.get_volume()
|
||||
source_mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_snapshot.return_value = success_uuid
|
||||
mock_create_volume.return_value = success_uuid
|
||||
mock_create_volume_copy_task.return_value = create_copy_task
|
||||
mock_get_task.return_value = get_task_success
|
||||
mock_delete_snapshot.return_value = success_response
|
||||
mock_delete_task.side_effect = self.api_exception
|
||||
self.assertIsNotNone(self.driver.create_cloned_volume(
|
||||
target_mock_volume, source_mock_volume))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'get_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume_copy_task')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_cloned_ec_volume_get_task_api_exception(
|
||||
self, mock_create_volume, mock_create_volume_copy_task,
|
||||
mock_get_task):
|
||||
target_mock_volume = self.get_volume()
|
||||
source_mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
mock_create_volume_copy_task.return_value = create_copy_task
|
||||
mock_get_task.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_cloned_volume(
|
||||
target_mock_volume, source_mock_volume)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'copy_volume')
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_volume')
|
||||
def test_create_cloned_ec_volume_copy_task_exception(
|
||||
self, mock_create_volume, mock_create_volume_copy_task):
|
||||
target_mock_volume = self.get_volume()
|
||||
source_mock_volume = self.get_volume()
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
mock_create_volume.return_value = success_uuid
|
||||
mock_create_volume_copy_task.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_cloned_volume(
|
||||
target_mock_volume, source_mock_volume)
|
||||
|
||||
def test_create_clone_in_use_volume(self):
|
||||
target_mock_volume = self.get_volume()
|
||||
source_mock_volume = self.get_volume()
|
||||
source_mock_volume['attach_status'] = "attached"
|
||||
self.driver._get_volume_type_extra_specs = mock.Mock(
|
||||
return_value=[{}, constants.VOLUME_TYPE_EC])
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_cloned_volume(
|
||||
target_mock_volume, source_mock_volume)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'create_snapshot')
|
||||
def test_create_snapshot(self, mock_create_snapshot):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot['volume'] = mock_volume
|
||||
mock_create_snapshot.return_value = success_uuid
|
||||
ret = self.driver.create_snapshot(snapshot)
|
||||
self.assertIsNotNone(ret)
|
||||
|
||||
def test_negative_create_snapshot_without_provider_id(self):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot['volume'] = mock_volume
|
||||
snapshot['volume']['provider_id'] = None
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
def test_negative_create_snapshot_without_provider_id_attr(self):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot.volume = mock_volume
|
||||
del snapshot.volume.provider_id
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'create_snapshot')
|
||||
def test_negative_create_snapshot_exception(self, mock_create_snapshot):
|
||||
snapshot = self.get_snapshot()
|
||||
mock_volume = self.get_volume()
|
||||
mock_volume['provider_id'] = fake_constants.UUID1
|
||||
snapshot['volume'] = mock_volume
|
||||
mock_create_snapshot.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'create_snapshot')
|
||||
def test_negative_create_snapshot_api_exception(
|
||||
self, mock_create_snapshot):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot.volume = mock_volume
|
||||
mock_create_snapshot.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'delete_snapshot')
|
||||
def test_delete_snapshot(self, mock_delete_snapshot):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot['volume'] = mock_volume
|
||||
mock_delete_snapshot.return_value = success_response
|
||||
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
||||
|
||||
def test_negative_delete_snapshot_without_provider_id(self):
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot['provider_id'] = None
|
||||
self.assertIsNone(self.driver.delete_snapshot(snapshot))
|
||||
|
||||
def test_negative_delete_snapshot_without_provider_id_attr(self):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot.volume = mock_volume
|
||||
del snapshot.provider_id
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'delete_snapshot')
|
||||
def test_negative_delete_snapshot_exception(self, mock_delete_snapshot):
|
||||
snapshot = self.get_snapshot()
|
||||
mock_volume = self.get_volume()
|
||||
mock_volume['provider_id'] = fake_constants.UUID1
|
||||
snapshot['volume'] = mock_volume
|
||||
mock_delete_snapshot.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'delete_snapshot')
|
||||
def test_negative_delete_snapshot_api_exception(
|
||||
self, mock_delete_snapshot):
|
||||
mock_volume = self.get_volume()
|
||||
snapshot = self.get_snapshot()
|
||||
snapshot['volume'] = mock_volume
|
||||
mock_delete_snapshot.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_extend_ec_volume_success(self, mock_update_volume):
|
||||
mock_volume = self.get_volume()
|
||||
new_size = 100
|
||||
mock_update_volume.return_value = success_response
|
||||
ret = self.driver.extend_volume(mock_volume, new_size)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def test_negative_extend_volume_without_provider_id(self):
|
||||
mock_volume = self.get_volume()
|
||||
new_size = 100
|
||||
mock_volume['provider_id'] = None
|
||||
self.assertIsNone(self.driver.extend_volume(mock_volume, new_size))
|
||||
|
||||
def test_negative_extend_volume__without_provider_id_attr(self):
|
||||
mock_volume = self.get_volume()
|
||||
new_size = 100
|
||||
del mock_volume.provider_id
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.extend_volume(mock_volume, new_size)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_extend_volume_exception(self, mock_update_volume):
|
||||
mock_volume = self.get_volume()
|
||||
new_size = 100
|
||||
mock_update_volume.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.extend_volume(mock_volume, new_size)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_extend_volume_api_exception(self, mock_update_volume):
|
||||
mock_volume = self.get_volume()
|
||||
new_size = 100
|
||||
mock_update_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.extend_volume(mock_volume, new_size)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_update_migrated_volume_success(self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
source_mock_volume['host'] = "FSC1"
|
||||
destination_mock_volume['host'] = "FSC1"
|
||||
mock_rename_volume.return_value = success_response
|
||||
self.assertIsNotNone(self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_update_migrated_volume_without_destination_provider_id(
|
||||
self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
destination_mock_volume['provider_id'] = None
|
||||
mock_rename_volume.side_effect = success_response
|
||||
self.assertIsNotNone(self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_update_migrated_volume_without_source_provider_id(
|
||||
self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
source_mock_volume['provider_id'] = None
|
||||
mock_rename_volume.return_value = success_response
|
||||
self.assertIsNotNone(self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE))
|
||||
|
||||
@mock.patch.object(rest_client.RestClient, 'rename_volume')
|
||||
def test_update_migrated_volume_api_exception(self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
mock_rename_volume[0].side_effect = self.api_exception
|
||||
mock_rename_volume[1].return_value = success_response
|
||||
self.assertIsNotNone(self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE))
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_update_migrated_volume_backend_exception(
|
||||
self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
source_mock_volume['provider_id'] = None
|
||||
mock_rename_volume.side_effect = self.api_exception
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE)
|
||||
|
||||
@mock.patch.object(swagger_client.StorageApi, 'update_volume')
|
||||
def test_update_migrated_volume_exception(self, mock_rename_volume):
|
||||
source_mock_volume = self.get_volume()
|
||||
destination_mock_volume = self.get_volume()
|
||||
source_mock_volume['provider_id'] = None
|
||||
mock_rename_volume.side_effect = Exception("mock exception")
|
||||
with self.assertRaises(exception.VolumeBackendAPIException):
|
||||
self.driver.update_migrated_volume(
|
||||
self.context, source_mock_volume, destination_mock_volume,
|
||||
fields.VolumeStatus.AVAILABLE)
|
||||
|
||||
@mock.patch.object(volume.driver.BaseVD, '_detach_volume')
|
||||
@mock.patch.object(image_utils, 'upload_volume')
|
||||
@mock.patch.object(volume.driver.BaseVD, '_attach_volume')
|
||||
@mock.patch.object(volume_utils, 'brick_get_connector_properties')
|
||||
def test_copy_volume_to_image(
|
||||
self, mock_get_connector, mock_attach_volume,
|
||||
mock_upload_volume, mock_detach):
|
||||
mock_volume = self.get_volume()
|
||||
image_service = fake_image.FakeImageService()
|
||||
self.configuration.use_multipath_for_image_xfer = False
|
||||
self.configuration.enforce_multipath_for_image_xfer = False
|
||||
local_path = 'dev/sda'
|
||||
mock_get_connector.return_value = {}
|
||||
attach_info = {'device': {'path': local_path},
|
||||
'conn': {'driver_volume_type': 'nvme',
|
||||
'data': {}, }}
|
||||
mock_attach_volume.return_value = [attach_info, mock_volume]
|
||||
mock_upload_volume.return_value = None
|
||||
mock_detach.return_value = None
|
||||
self.driver.wait_for_device = mock.Mock(
|
||||
return_value=True)
|
||||
self.assertIsNone(
|
||||
self.driver.copy_volume_to_image(
|
||||
self.context, mock_volume, image_service,
|
||||
fake_constants.IMAGE_ID))
|
||||
|
||||
@mock.patch.object(volume.driver.BaseVD, '_detach_volume')
|
||||
@mock.patch.object(image_utils, 'fetch_to_raw')
|
||||
@mock.patch.object(volume.driver.BaseVD, '_attach_volume')
|
||||
@mock.patch.object(volume_utils, 'brick_get_connector_properties')
|
||||
def test_copy_image_to_volume(
|
||||
self, mock_get_connector, mock_attach_volume,
|
||||
mock_fetch_to_raw, mock_detach):
|
||||
mock_volume = self.get_volume()
|
||||
image_service = fake_image.FakeImageService()
|
||||
self.configuration.use_multipath_for_image_xfer = False
|
||||
self.configuration.enforce_multipath_for_image_xfer = False
|
||||
self.configuration.volume_dd_blocksize = 8
|
||||
local_path = 'dev/sda'
|
||||
mock_get_connector.return_value = {}
|
||||
attach_info = {'device': {'path': local_path},
|
||||
'conn': {'driver_volume_type': 'nvme',
|
||||
'data': {}, }}
|
||||
mock_attach_volume.return_value = [attach_info, mock_volume]
|
||||
mock_fetch_to_raw.return_value = None
|
||||
mock_detach.return_value = None
|
||||
self.driver.wait_for_device = mock.Mock(
|
||||
return_value=True)
|
||||
self.assertIsNone(
|
||||
self.driver.copy_image_to_volume(
|
||||
self.context, mock_volume, image_service,
|
||||
fake_constants.IMAGE_ID))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
0
cinder/volume/drivers/fungible/__init__.py
Normal file
0
cinder/volume/drivers/fungible/__init__.py
Normal file
59
cinder/volume/drivers/fungible/constants.py
Normal file
59
cinder/volume/drivers/fungible/constants.py
Normal file
@ -0,0 +1,59 @@
|
||||
# (c) Copyright 2022 Fungible, Inc. 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.
|
||||
|
||||
"""
|
||||
Define all constants required for fungible driver
|
||||
"""
|
||||
# API constants
|
||||
VERSION = '1.0.0'
|
||||
STATIC_URL = '/FunCC/v1'
|
||||
|
||||
# Volume type constants
|
||||
VOLUME_TYPE_EC = 'VOL_TYPE_BLK_EC'
|
||||
VOLUME_TYPE_REPLICA = 'VOL_TYPE_BLK_REPLICA'
|
||||
VOLUME_TYPE_RAW = 'VOL_TYPE_BLK_LOCAL_THIN'
|
||||
VOLUME_TYPE_RF1 = 'VOL_TYPE_BLK_RF1'
|
||||
|
||||
# General constants
|
||||
FALSE = 'false'
|
||||
TRUE = 'true'
|
||||
BOOLEAN = [TRUE, FALSE]
|
||||
BYTES_PER_GIB = 1073741824
|
||||
FSC_IOPS_IMG_MIG = "iops_for_image_migration"
|
||||
|
||||
# Extra specs constants
|
||||
FSC_QOS_BAND = 'fungible:qos_band'
|
||||
FSC_SPACE_ALLOCATION_POLICY = 'fungible:space_allocation_policy'
|
||||
FSC_COMPRESSION = 'fungible:compression'
|
||||
FSC_EC_SCHEME = 'fungible:ec_scheme'
|
||||
FSC_SNAPSHOTS = "fungible:snapshots"
|
||||
FSC_KMIP_SECRET_KEY = 'fungible:kmip_secret_key'
|
||||
FSC_VOL_TYPE = 'fungible:vol_type'
|
||||
FSC_BLK_SIZE = "fungible:block_size"
|
||||
FSC_FD_IDS = 'fungible:fault_domain_ids'
|
||||
FSC_FD_OP = 'fungible:fd_op'
|
||||
BLOCK_SIZE_4K = '4096'
|
||||
BLOCK_SIZE_8K = '8192'
|
||||
BLOCK_SIZE_16K = '16384'
|
||||
BLOCK_SIZE = [BLOCK_SIZE_4K, BLOCK_SIZE_8K, BLOCK_SIZE_16K]
|
||||
FSC_FD_OPS = ['SUGGESTED_FD_IDS', 'EXCLUDE_FD_IDS', 'ASSIGNED_FD_ID']
|
||||
SPACE_ALLOCATION_POLICY = ['balanced', 'write_optimized', 'capacity_optimized']
|
||||
EC_8_2 = '8_2'
|
||||
EC_4_2 = '4_2'
|
||||
EC_2_1 = '2_1'
|
||||
QOS_BAND = {
|
||||
'gold': 0,
|
||||
'silver': 1,
|
||||
'bronze': 2
|
||||
}
|
1144
cinder/volume/drivers/fungible/driver.py
Normal file
1144
cinder/volume/drivers/fungible/driver.py
Normal file
File diff suppressed because it is too large
Load Diff
424
cinder/volume/drivers/fungible/rest_client.py
Normal file
424
cinder/volume/drivers/fungible/rest_client.py
Normal file
@ -0,0 +1,424 @@
|
||||
# (c) Copyright 2022 Fungible, Inc. 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.volume.drivers.fungible import constants
|
||||
from cinder.volume.drivers.fungible import \
|
||||
swagger_api_client as swagger_client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RestClient(object):
|
||||
def __init__(self, configuration):
|
||||
"""Initialize the api request fields."""
|
||||
self.configuration = configuration
|
||||
self.rest_ip = None
|
||||
self.rest_port = None
|
||||
self.is_configured = False
|
||||
|
||||
@staticmethod
|
||||
def log_error(error_msg):
|
||||
"""Raise exception with error message"""
|
||||
LOG.exception(error_msg)
|
||||
raise exception.VolumeBackendAPIException(data=error_msg)
|
||||
|
||||
def do_setup(self):
|
||||
"""Initial setup of API request variables"""
|
||||
get_config_value = self.configuration.safe_get
|
||||
self.client = swagger_client.Configuration()
|
||||
self.client.username = get_config_value("san_login")
|
||||
self.client.password = get_config_value("san_password")
|
||||
self.rest_ip = get_config_value("san_ip")
|
||||
self.rest_port = get_config_value("san_api_port")
|
||||
protocol = "https"
|
||||
self.client.host = f"{protocol}://{self.rest_ip}{constants.STATIC_URL}"
|
||||
self.client.verify_ssl = False
|
||||
if not self.configuration.api_enable_ssl:
|
||||
protocol = "http"
|
||||
self.client.host = (f"{protocol}://{self.rest_ip}:{self.rest_port}"
|
||||
f"{constants.STATIC_URL}")
|
||||
LOG.info("REST server IP: %(ip)s, port: %(port)s, "
|
||||
"username: %(user)s.",
|
||||
{
|
||||
"ip": self.rest_ip,
|
||||
"port": self.rest_port,
|
||||
"user": self.client.username,
|
||||
})
|
||||
self.api_storage = swagger_client.StorageApi(
|
||||
swagger_client.ApiClient(self.client))
|
||||
self.api_gateway = swagger_client.ApigatewayApi(
|
||||
swagger_client.ApiClient(self.client))
|
||||
self.api_topology = swagger_client.TopologyApi(
|
||||
swagger_client.ApiClient(self.client))
|
||||
self.is_configured = True
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Check status of fungible storage clusters."""
|
||||
api_response = self.api_gateway.get_fc_health().to_dict()
|
||||
return api_response
|
||||
|
||||
def create_volume(self, volume, fungible_specs, volume_type,
|
||||
snapshot=None):
|
||||
"""Creates new volume using the specified parameters"""
|
||||
|
||||
# Convert GB to bytes, Default 1 GB size
|
||||
volume_size = constants.BYTES_PER_GIB
|
||||
if volume['size']:
|
||||
volume_size = constants.BYTES_PER_GIB * volume['size']
|
||||
fungible_request_obj = {
|
||||
"name": volume['id'],
|
||||
"vol_type": volume_type.upper(),
|
||||
"capacity": volume_size,
|
||||
"is_clone": False,
|
||||
"encrypt": False,
|
||||
"qos_band": constants.QOS_BAND.get('silver'),
|
||||
"block_size": int(constants.BLOCK_SIZE_4K)
|
||||
}
|
||||
data_protection = {
|
||||
"num_failed_disks": 2,
|
||||
"num_data_disks": 4,
|
||||
"num_redundant_dpus": 1,
|
||||
}
|
||||
durable_param = {
|
||||
"compression_effort": 2,
|
||||
"snap_support": True,
|
||||
"space_allocation_policy": 'balanced',
|
||||
}
|
||||
# Create Volume From Snapshot
|
||||
if snapshot is not None:
|
||||
fungible_request_obj["is_clone"] = True
|
||||
fungible_request_obj["clone_source_volume_uuid"] = \
|
||||
snapshot['provider_id']
|
||||
|
||||
errors = []
|
||||
# Validation check for Extraspecs
|
||||
self._validation_check(fungible_request_obj,
|
||||
fungible_specs, volume_type, errors,
|
||||
"ExtraSpecs", durable_param, data_protection)
|
||||
# Validation check for Metadata
|
||||
self._validation_check(fungible_request_obj,
|
||||
volume.get('metadata', {}), volume_type, errors,
|
||||
"Metadata", durable_param, data_protection)
|
||||
if len(errors) != 0:
|
||||
msg = "ERROR: "
|
||||
for error in errors:
|
||||
msg = msg + " | " + error
|
||||
self.log_error(error_msg=msg)
|
||||
|
||||
LOG.info("create_volume: "
|
||||
"fungible_request_obj=%(fungible_request_obj)s",
|
||||
{'fungible_request_obj': fungible_request_obj})
|
||||
api_response = self.api_storage.create_volume(
|
||||
body_volume_intent_create=fungible_request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def _validation_check(self, fungible_obj, data, volume_type, errors,
|
||||
prefix, durable_param, data_protection):
|
||||
if constants.FSC_KMIP_SECRET_KEY in data:
|
||||
if data[constants.FSC_KMIP_SECRET_KEY]:
|
||||
fungible_obj['encrypt'] = True
|
||||
fungible_obj['kmip_secret_key'] = data[
|
||||
constants.FSC_KMIP_SECRET_KEY]
|
||||
|
||||
if constants.FSC_BLK_SIZE in data:
|
||||
if data[constants.FSC_BLK_SIZE] in constants.BLOCK_SIZE:
|
||||
if (volume_type.upper() == constants.VOLUME_TYPE_RF1 and
|
||||
data[constants.FSC_BLK_SIZE] !=
|
||||
constants.BLOCK_SIZE_16K):
|
||||
msg = (
|
||||
f"{prefix} {constants.FSC_BLK_SIZE} value is invalid \
|
||||
for the volume type specified")
|
||||
errors.append(msg)
|
||||
else:
|
||||
fungible_obj['block_size'] = int(
|
||||
data[constants.FSC_BLK_SIZE])
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_BLK_SIZE} value is invalid")
|
||||
errors.append(msg)
|
||||
elif volume_type.upper() == constants.VOLUME_TYPE_RF1:
|
||||
# Set default block size for RF1 to 16K
|
||||
fungible_obj['block_size'] = int(constants.BLOCK_SIZE_16K)
|
||||
|
||||
if constants.FSC_QOS_BAND in data:
|
||||
if data[constants.FSC_QOS_BAND].lower() in constants.QOS_BAND:
|
||||
fungible_obj['qos_band'] = constants.QOS_BAND.get(
|
||||
data[constants.FSC_QOS_BAND].lower())
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_QOS_BAND} value is invalid")
|
||||
errors.append(msg)
|
||||
|
||||
if (volume_type.upper() == constants.VOLUME_TYPE_RAW or
|
||||
volume_type.upper() == constants.VOLUME_TYPE_RF1):
|
||||
if constants.FSC_FD_IDS in data:
|
||||
ids = data[constants.FSC_FD_IDS].split(',', 2)
|
||||
if len(ids) <= 2:
|
||||
ids = [item.strip() for item in ids]
|
||||
fungible_obj['fault_domain_ids'] = ids
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_FD_IDS} - "
|
||||
f"Only two fault domain ids can be provided.")
|
||||
errors.append(msg)
|
||||
|
||||
if constants.FSC_FD_OP in data:
|
||||
if (data[constants.FSC_FD_OP].upper()
|
||||
in constants.FSC_FD_OPS):
|
||||
fungible_obj['fd_op'] = data[constants.FSC_FD_OP]
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_FD_OP} "
|
||||
f"value is invalid")
|
||||
errors.append(msg)
|
||||
|
||||
if (volume_type.upper() == constants.VOLUME_TYPE_REPLICA or
|
||||
volume_type.upper() == constants.VOLUME_TYPE_EC or
|
||||
volume_type.upper() == constants.VOLUME_TYPE_RF1):
|
||||
if constants.FSC_SPACE_ALLOCATION_POLICY in data:
|
||||
if (data[constants.FSC_SPACE_ALLOCATION_POLICY].lower()
|
||||
in constants.SPACE_ALLOCATION_POLICY):
|
||||
durable_param['space_allocation_policy'] = data[
|
||||
constants.FSC_SPACE_ALLOCATION_POLICY]
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_SPACE_ALLOCATION_POLICY}"
|
||||
f" value is invalid")
|
||||
errors.append(msg)
|
||||
|
||||
if constants.FSC_COMPRESSION in data:
|
||||
if (data[constants.FSC_COMPRESSION].lower() ==
|
||||
constants.FALSE):
|
||||
durable_param['compression_effort'] = 0
|
||||
elif (data[constants.FSC_COMPRESSION].lower() ==
|
||||
constants.TRUE):
|
||||
durable_param['compression_effort'] = 2
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_COMPRESSION} value is "
|
||||
f"invalid")
|
||||
errors.append(msg)
|
||||
|
||||
if constants.FSC_SNAPSHOTS in data:
|
||||
if (data[constants.FSC_SNAPSHOTS].lower()
|
||||
in constants.BOOLEAN):
|
||||
if (data[constants.FSC_SNAPSHOTS].lower() ==
|
||||
constants.FALSE):
|
||||
durable_param['snap_support'] = False
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_SNAPSHOTS} value is "
|
||||
f"invalid")
|
||||
errors.append(msg)
|
||||
|
||||
if volume_type.upper() == constants.VOLUME_TYPE_EC:
|
||||
fungible_obj.update(durable_param)
|
||||
if constants.FSC_EC_SCHEME in data:
|
||||
if data[constants.FSC_EC_SCHEME] == constants.EC_8_2:
|
||||
data_protection['num_data_disks'] = 8
|
||||
elif data[constants.FSC_EC_SCHEME] == constants.EC_4_2:
|
||||
data_protection['num_data_disks'] = 4
|
||||
elif data[constants.FSC_EC_SCHEME] == constants.EC_2_1:
|
||||
data_protection['num_data_disks'] = 2
|
||||
data_protection['num_failed_disks'] = 1
|
||||
else:
|
||||
msg = (f"{prefix} {constants.FSC_EC_SCHEME} value is "
|
||||
f"invalid")
|
||||
errors.append(msg)
|
||||
fungible_obj["data_protection"] = data_protection
|
||||
|
||||
elif volume_type.upper() == constants.VOLUME_TYPE_REPLICA:
|
||||
fungible_obj.update(durable_param)
|
||||
data_protection = {
|
||||
"num_failed_disks": 1,
|
||||
"num_data_disks": 1,
|
||||
"num_redundant_dpus": 1,
|
||||
}
|
||||
fungible_obj["data_protection"] = data_protection
|
||||
|
||||
elif volume_type.upper() == constants.VOLUME_TYPE_RF1:
|
||||
fungible_obj.update(durable_param)
|
||||
pass
|
||||
|
||||
def delete_volume(self, volume_uuid):
|
||||
"""Deletes the specified volume"""
|
||||
LOG.info("delete_volume: volume_uuid=%(volume_uuid)s",
|
||||
{'volume_uuid': volume_uuid})
|
||||
api_response = self.api_storage.delete_volume(
|
||||
volume_uuid=volume_uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def get_volume_detail(self, uuid):
|
||||
"""Get volume details by uuid"""
|
||||
api_response = self.api_storage.get_volume(volume_uuid=uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def get_host_uuid_from_host_nqn(self, host_nqn):
|
||||
"""Get host uuid from the host_nqn supplied"""
|
||||
api_response = self.api_topology.get_host_id_list(
|
||||
host_nqn_contains=host_nqn).to_dict()
|
||||
|
||||
host_uuids = api_response.get("data").get("host_uuids")
|
||||
if len(host_uuids) == 1:
|
||||
return host_uuids[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_host_details(self, host_uuid):
|
||||
"""Get host details for the host_uuid supplied"""
|
||||
api_response = self.api_topology.get_host_info(
|
||||
host_uuid=host_uuid).to_dict()
|
||||
|
||||
host = api_response.get("data")
|
||||
return host
|
||||
|
||||
def get_hosts_subset(self, host_uuids):
|
||||
"""Get host details in a list for the list of host_uuids supplied"""
|
||||
request_obj = {
|
||||
"host_id_list": host_uuids
|
||||
}
|
||||
api_response = self.api_topology.fetch_hosts_with_ids(
|
||||
body_fetch_hosts_with_ids=request_obj).to_dict()
|
||||
|
||||
hosts = api_response.get("data")
|
||||
return hosts
|
||||
|
||||
def create_host(self, host_nqn):
|
||||
"""Create host with the host_nqn supplied"""
|
||||
request_obj = {
|
||||
"host_name": host_nqn,
|
||||
"host_nqn": host_nqn,
|
||||
"fac_enabled": False
|
||||
}
|
||||
LOG.info("create_host: request_obj=%(request_obj)s",
|
||||
{'request_obj': request_obj})
|
||||
api_response = self.api_topology.add_host(
|
||||
body_host_create=request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def attach_volume(self, uuid, host_uuid, fac_enabled, iops=False):
|
||||
"""Attaches a volume to a host server,
|
||||
|
||||
using the specified transport method
|
||||
"""
|
||||
if fac_enabled:
|
||||
request_obj = {
|
||||
"transport": 'PCI',
|
||||
"host_uuid": host_uuid,
|
||||
"fnid": 3,
|
||||
"huid": 1,
|
||||
"ctlid": 0
|
||||
}
|
||||
else:
|
||||
request_obj = {
|
||||
"transport": 'TCP',
|
||||
"host_uuid": host_uuid
|
||||
}
|
||||
# high iops set when uploading, downloading or migrating volume
|
||||
if iops:
|
||||
request_obj["max_read_iops"] = self.configuration.safe_get(
|
||||
'iops_for_image_migration')
|
||||
|
||||
LOG.info("attach_volume: uuid=%(uuid)s "
|
||||
"request_obj=%(request_obj)s",
|
||||
{'uuid': uuid, 'request_obj': request_obj})
|
||||
api_response = self.api_storage.attach_volume(
|
||||
volume_uuid=uuid,
|
||||
body_volume_attach=request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def detach_volume(self, port_uuid):
|
||||
"""Detach the volume specified port"""
|
||||
LOG.info("detach_volume: port_uuid=%(port_uuid)s",
|
||||
{'port_uuid': port_uuid})
|
||||
api_response = self.api_storage.delete_port(
|
||||
port_uuid=port_uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def create_snapshot(self, uuid, snapshot_name):
|
||||
"""Create snapshot of volume with specified uuid"""
|
||||
fungible_request_obj = {
|
||||
"name": snapshot_name
|
||||
}
|
||||
api_response = self.api_storage.create_snapshot(
|
||||
volume_uuid=uuid,
|
||||
body_volume_snapshot_create=fungible_request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def delete_snapshot(self, uuid):
|
||||
"""Delete snapshot with specified uuid"""
|
||||
api_response = self.api_storage.delete_snapshot(
|
||||
snapshot_uuid=uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def extend_volume(self, uuid, new_size):
|
||||
"""Update volume size to new size"""
|
||||
fungible_request_obj = {
|
||||
"op": "UPDATE_CAPACITY",
|
||||
"capacity": constants.BYTES_PER_GIB * new_size,
|
||||
}
|
||||
api_response = self.api_storage.update_volume(
|
||||
volume_uuid=uuid,
|
||||
body_volume_update=fungible_request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def rename_volume(self, uuid, new_name):
|
||||
"""Update volume name to new name"""
|
||||
fungible_request_obj = {
|
||||
"op": "RENAME_VOLUME",
|
||||
"new_vol_name": new_name,
|
||||
}
|
||||
api_response = self.api_storage.update_volume(
|
||||
volume_uuid=uuid,
|
||||
body_volume_update=fungible_request_obj).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def copy_volume(self, volumeId, src_vrefId):
|
||||
"""Submit copy volume task."""
|
||||
payload = {
|
||||
"src_volume_uuid": src_vrefId,
|
||||
"dest_volume_uuid": volumeId,
|
||||
"timeout": self.configuration.safe_get(
|
||||
'fsc_clone_volume_timeout')
|
||||
}
|
||||
|
||||
LOG.info("Volume clone payload: %(payload)s.", {'payload': payload})
|
||||
api_response = self.api_storage.create_volume_copy_task(
|
||||
body_create_volume_copy_task=payload).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def get_volume_copy_task(self, task_uuid):
|
||||
"""Get volume copy task status"""
|
||||
api_response = self.api_storage.get_volume_copy_task(
|
||||
task_uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def delete_volume_copy_task(self, task_uuid):
|
||||
"""Delete volume copy task"""
|
||||
api_response = self.api_storage.delete_volume_copy_task(
|
||||
task_uuid).to_dict()
|
||||
|
||||
return api_response
|
||||
|
||||
def get_topology(self):
|
||||
api_response = self.api_topology.get_hierarchical_topology().to_dict()
|
||||
return api_response
|
15403
cinder/volume/drivers/fungible/swagger_api_client.py
Normal file
15403
cinder/volume/drivers/fungible/swagger_api_client.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,82 @@
|
||||
==============================
|
||||
Fungible Storage Driver
|
||||
==============================
|
||||
|
||||
Fungible Storage volume driver provides OpenStack Compute instances
|
||||
with access to Fungible Storage Cluster.
|
||||
|
||||
This documentation explains how to configure Cinder for use with the
|
||||
Fungible Storage Cluster.
|
||||
|
||||
Driver requirements
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Fungible Storage Cluster
|
||||
|
||||
- FSC version >= 4.0
|
||||
|
||||
- nvme cli version >= v1.13
|
||||
|
||||
- The Block Storage Node should also have a data path to the
|
||||
Fungible Storage Cluster for the following operations:
|
||||
|
||||
- Copy volume to image
|
||||
- Copy image to volume
|
||||
|
||||
Driver options
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The following table contains the configuration options supported by the
|
||||
Fungible Storage driver.
|
||||
|
||||
.. config-table::
|
||||
:config-target: Fungible Storage Cluster
|
||||
|
||||
cinder.volume.drivers.fungible.driver
|
||||
|
||||
Supported operations
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Create, list, delete, attach and detach volumes
|
||||
- Create, list and delete volume snapshots
|
||||
- Copy image to volume
|
||||
- Copy volume to image
|
||||
- Create volume from snapshot
|
||||
- Clone volume
|
||||
- Extend volume
|
||||
|
||||
Configure Fungible Storage Cluster backend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This section details the steps required to configure the
|
||||
Fungible Storage cinder driver.
|
||||
|
||||
#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]``
|
||||
section, set the enabled_backends parameter.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[DEFAULT]
|
||||
enabled_backends = fungible
|
||||
|
||||
#. Add a backend group section for the backend group specified
|
||||
in the enabled_backends parameter.
|
||||
|
||||
#. In the newly created backend group section, set the
|
||||
following configuration options:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[fungible]
|
||||
# Backend name
|
||||
volume_backend_name=fungible
|
||||
# The driver path
|
||||
volume_driver=cinder.volume.drivers.fungible.driver.FungibleDriver
|
||||
# Fungible composer details
|
||||
san_ip = <composer node VIP>
|
||||
san_login = <composer username>
|
||||
san_password = <composer password>
|
||||
# List below are optional
|
||||
nvme_connect_port = <nvme target endpoint port>
|
||||
api_enable_ssl = True/False
|
||||
iops_for_image_migration = <IOPS value>
|
@ -57,6 +57,9 @@ title=Dell VNX Storage Driver (FC, iSCSI)
|
||||
[driver.fujitsu_eternus]
|
||||
title=Fujitsu ETERNUS Driver (FC, iSCSI)
|
||||
|
||||
[driver.fungible]
|
||||
title=Fungible Storage Driver (NVMe-TCP)
|
||||
|
||||
[driver.hitachi_vsp]
|
||||
title=Hitachi VSP Storage Driver (FC, iSCSI)
|
||||
|
||||
@ -246,6 +249,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=complete
|
||||
driver.fujitsu_eternus=complete
|
||||
driver.fungible=complete
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=complete
|
||||
@ -321,6 +325,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=complete
|
||||
driver.fujitsu_eternus=complete
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=complete
|
||||
@ -399,6 +404,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=missing
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=missing
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=missing
|
||||
@ -476,6 +482,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=missing
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=missing
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=missing
|
||||
@ -554,6 +561,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=complete
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=missing
|
||||
@ -630,6 +638,7 @@ driver.dell_emc_vmax_3=complete
|
||||
driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=complete
|
||||
driver.fungible=missing
|
||||
driver.fujitsu_eternus=complete
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
@ -709,6 +718,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=missing
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=missing
|
||||
driver.hpe_3par=missing
|
||||
driver.hpe_msa=missing
|
||||
@ -787,6 +797,7 @@ driver.dell_emc_vnx=missing
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=complete
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=complete
|
||||
@ -862,6 +873,7 @@ driver.dell_emc_vnx=complete
|
||||
driver.dell_emc_powerflex=complete
|
||||
driver.dell_emc_xtremio=missing
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=complete
|
||||
driver.hpe_3par=complete
|
||||
driver.hpe_msa=missing
|
||||
@ -941,6 +953,7 @@ driver.dell_emc_vnx=missing
|
||||
driver.dell_emc_powerflex=missing
|
||||
driver.dell_emc_xtremio=missing
|
||||
driver.fujitsu_eternus=missing
|
||||
driver.fungible=missing
|
||||
driver.hitachi_vsp=missing
|
||||
driver.hpe_3par=missing
|
||||
driver.hpe_msa=missing
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Added NVMe-TCP volume driver for Fungible Storage Cluster.
|
||||
|
Loading…
Reference in New Issue
Block a user