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:
Aneesh Pachilangottil 2022-07-08 19:22:44 +00:00
parent 4c9b76b937
commit 00cb2887ba
12 changed files with 18093 additions and 0 deletions

View File

@ -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,

View 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]

View 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()

View 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
}

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,4 @@
---
features:
- Added NVMe-TCP volume driver for Fungible Storage Cluster.