Add Cinder driver for TOYOU NetStor TYDS

* Supported Protocol
 - iSCSI

* Supported Feature
 - Create Volume
 - Delete Volume
 - Attach Volume
 - Detach Volume
 - Extend Volume
 - Create Snapshot
 - Delete Snapshot
 - Create Volume from Snapshot
 - Create Volume from Volume (clone)
 - Create lmage from Volume
 - Volume Migration (host assisted)

ThirdPartySystems: TOYOU TYDS CI
Implements: blueprint add-toyou-netstor-tyds-driver
Change-Id: I0a2b6e81c0440593a97c3aff2554cfdb96af77a5
This commit is contained in:
cccqqqlll 2023-06-26 03:07:36 +00:00
parent 2b0567b0e5
commit 6fcb495c8b
8 changed files with 1932 additions and 0 deletions

View File

@ -185,6 +185,8 @@ from cinder.volume.drivers.synology import synology_common as \
cinder_volume_drivers_synology_synologycommon
from cinder.volume.drivers.toyou.acs5000 import acs5000_common as \
cinder_volume_drivers_toyou_acs5000_acs5000common
from cinder.volume.drivers.toyou.tyds import tyds as \
cinder_volume_drivers_toyou_tyds_tyds
from cinder.volume.drivers.veritas_access import veritas_iscsi as \
cinder_volume_drivers_veritas_access_veritasiscsi
from cinder.volume.drivers.vmware import vmdk as \
@ -439,6 +441,7 @@ def list_opts():
cinder_volume_drivers_stx_common.common_opts,
cinder_volume_drivers_stx_common.iscsi_opts,
cinder_volume_drivers_synology_synologycommon.cinder_opts,
cinder_volume_drivers_toyou_tyds_tyds.tyds_opts,
cinder_volume_drivers_vmware_vmdk.vmdk_opts,
cinder_volume_drivers_vzstorage.vzstorage_opts,
cinder_volume_drivers_windows_iscsi.windows_opts,

View File

@ -0,0 +1,690 @@
# Copyright 2023 toyou Corp.
# 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
from cinder import exception
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.volume import configuration as conf
from cinder.volume.drivers.toyou.tyds import tyds as driver
POOLS_NAME = ['pool1', 'pool2']
class TestTydsDriver(unittest.TestCase):
@mock.patch('cinder.volume.drivers.toyou.tyds.tyds_client.TydsClient',
autospec=True)
def setUp(self, mock_tyds_client):
"""Set up the test case.
- Creates a driver instance.
- Mocks the TydsClient and its methods.
- Initializes volumes and snapshots for testing.
"""
super().setUp()
self.mock_client = mock_tyds_client.return_value
self.mock_do_request = mock.MagicMock(
side_effect=self.mock_client.do_request)
self.mock_client.do_request = self.mock_do_request
self.configuration = mock.Mock(spec=conf.Configuration)
self.configuration.tyds_pools = POOLS_NAME
self.configuration.san_ip = "23.44.56.78"
self.configuration.tyds_http_port = 80
self.configuration.san_login = 'admin'
self.configuration.san_password = 'admin'
self.configuration.tyds_stripe_size = '4M'
self.configuration.tyds_clone_progress_interval = 3
self.configuration.tyds_copy_progress_interval = 3
self.driver = driver.TYDSDriver(configuration=self.configuration)
self.driver.do_setup(context=None)
self.driver.check_for_setup_error()
self.volume = fake_volume.fake_volume_obj(None)
self.volume.host = 'host@backend#pool1'
self.snapshot = fake_snapshot.fake_snapshot_obj(None)
self.snapshot.volume = self.volume
self.snapshot.volume_id = self.volume.id
self.target_volume = fake_volume.fake_volume_obj(None)
self.target_volume.host = 'host@backend#pool2'
self.src_vref = self.volume
def test_create_volume_success(self):
"""Test case for successful volume creation.
- Sets mock return value.
- Calls create_volume method.
- Verifies if the create_volume method is called with correct
arguments.
"""
self.mock_client.create_volume.return_value = self.volume
self.driver.create_volume(self.volume)
self.mock_client.create_volume.assert_called_once_with(
self.volume.name, self.volume.size * 1024, 'pool1', '4M')
def test_create_volume_failure(self):
"""Test case for volume creation failure.
- Sets the mock return value to simulate a failure.
- Calls the create_volume method.
- Verifies if the create_volume method raises the expected exception.
"""
# Set the mock return value to simulate a failure
self.mock_client.create_volume.side_effect = \
exception.VolumeBackendAPIException('API error')
# Call the create_volume method and check the result
self.assertRaises(
exception.VolumeBackendAPIException,
self.driver.create_volume,
self.volume
)
def test_delete_volume_success(self):
"""Test case for successful volume deletion.
- Mocks the _get_volume_by_name method to return a volume.
- Calls the delete_volume method.
- Verifies if the delete_volume method is called with the correct
volume ID.
"""
# Mock the _get_volume_by_name method to return a volume
self.driver._get_volume_by_name = mock.Mock(return_value={'id': '13'})
# Call the delete_volume method
self.driver.delete_volume(self.volume)
# Verify if the delete_volume method is called with the correct
# volume ID
self.mock_client.delete_volume.assert_called_once_with('13')
def test_delete_volume_failure(self):
"""Test case for volume deletion failure.
- Mocks the _get_volume_by_name method to return a volume.
- Sets the mock return value for delete_volume method to raise an
exception.
- Calls the delete_volume method.
- Verifies if the delete_volume method raises the expected exception.
"""
# Mock the _get_volume_by_name method to return a volume
self.driver._get_volume_by_name = mock.Mock(return_value={'id': '13'})
# Set the mock return value for delete_volume method to raise an
# exception
self.mock_client.delete_volume.side_effect = \
exception.VolumeBackendAPIException('API error')
# Call the delete_volume method and verify if it raises the expected
# exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.delete_volume, self.volume)
def test_create_snapshot_success(self):
"""Test case for successful snapshot creation.
- Sets the mock return value for create_snapshot method.
- Mocks the _get_volume_by_name method to return a volume.
- Calls the create_snapshot method.
- Verifies if the create_snapshot method is called with the correct
arguments.
"""
# Set the mock return value for create_snapshot method
self.mock_client.create_snapshot.return_value = self.snapshot
# Mock the _get_volume_by_name method to return a volume
self.driver._get_volume_by_name = mock.Mock(return_value={'id': '13'})
# Call the create_snapshot method
self.driver.create_snapshot(self.snapshot)
# Verify if the create_snapshot method is called with the correct
# arguments
self.mock_client.create_snapshot.assert_called_once_with(
self.snapshot.name, '13',
'%s/%s' % (self.volume.name, self.snapshot.name)
)
def test_create_snapshot_failure_with_no_volume(self):
"""Test case for snapshot creation failure when volume is not found.
- Mocks the _get_volume_by_name method to return None.
- Calls the create_snapshot method.
- Verifies if the create_snapshot method is not called.
"""
# Mock the _get_volume_by_name method to return None
self.driver._get_volume_by_name = mock.Mock(return_value=None)
# Call the create_snapshot method and check for exception
self.assertRaises(driver.TYDSDriverException,
self.driver.create_snapshot, self.snapshot)
# Verify if the create_snapshot method is not called
self.mock_client.create_snapshot.assert_not_called()
def test_create_snapshot_failure(self):
"""Test case for snapshot creation failure.
- Mocks the _get_volume_by_name method to return a volume.
- Sets the mock return value for create_snapshot to raise an exception.
- Calls the create_snapshot method.
- Verifies if the create_snapshot method is called with the correct
arguments.
"""
# Mock the _get_volume_by_name method to return a volume
self.driver._get_volume_by_name = mock.Mock(return_value={'id': '13'})
# Set the mock return value for create_snapshot to raise an exception
self.mock_client.create_snapshot.side_effect = \
exception.VolumeBackendAPIException('API error')
# Call the create_snapshot method and check for exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_snapshot, self.snapshot)
# Verify if the create_snapshot method is called with the correct
# arguments
self.mock_client.create_snapshot.assert_called_once_with(
self.snapshot.name, '13',
'%s/%s' % (self.volume.name, self.snapshot.name))
def test_delete_snapshot_success(self):
"""Test case for successful snapshot deletion.
- Mocks the _get_snapshot_by_name method to return a snapshot.
- Calls the delete_snapshot method.
- Verifies if the delete_snapshot method is called with the correct
arguments.
"""
# Mock the _get_snapshot_by_name method to return a snapshot
self.driver._get_snapshot_by_name = mock.Mock(
return_value={'id': 'volume_id'})
# Call the delete_snapshot method
self.driver.delete_snapshot(self.snapshot)
# Verify if the delete_snapshot method is called with the correct
# arguments
self.mock_client.delete_snapshot.assert_called_once_with('volume_id')
def test_delete_snapshot_failure(self):
"""Test case for snapshot deletion failure.
- Mocks the _get_snapshot_by_name method to return a snapshot.
- Sets the mock return value for delete_snapshot to raise an exception.
- Calls the delete_snapshot method.
- Verifies if the delete_snapshot method is called with the correct
arguments.
"""
# Mock the _get_snapshot_by_name method to return a snapshot
self.driver._get_snapshot_by_name = mock.Mock(
return_value={'id': 'volume_id'})
# Set the mock return value for delete_snapshot to raise an exception
self.mock_client.delete_snapshot.side_effect = \
exception.VolumeBackendAPIException('API error')
# Call the delete_snapshot method and check for exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.delete_snapshot,
self.snapshot)
# Verify if the delete_snapshot method is called once
self.mock_client.delete_snapshot.assert_called_once()
@mock.patch('time.sleep')
@mock.patch('cinder.coordination.synchronized', new=mock.MagicMock())
def test_create_volume_from_snapshot_success(self, mock_sleep):
"""Test case for successful volume creation from snapshot.
- Mocks the sleep function.
- Sets the mock return values for create_volume_from_snapshot,
_get_volume_by_name, and get_clone_progress.
- Calls the create_volume_from_snapshot method.
- Verifies if the create_volume_from_snapshot method is called with
the correct arguments.
- Verifies if the _get_volume_by_name method is called once.
"""
# Mock the sleep function
mock_sleep.return_value = None
# Set the mock return values for create_volume_from_snapshot,
# _get_volume_by_name, and get_clone_progress
self.mock_client.create_volume_from_snapshot.return_value = self.volume
self.driver._get_volume_by_name = mock.Mock(
return_value={'poolName': 'pool1',
'sizeMB': self.volume.size * 1024})
self.mock_client.get_clone_progress.return_value = {'progress': '100%'}
# Call the create_volume_from_snapshot method
self.driver.create_volume_from_snapshot(self.target_volume,
self.snapshot)
# Verify if the create_volume_from_snapshot method is called with the
# correct arguments
self.mock_client.create_volume_from_snapshot.assert_called_once_with(
self.target_volume.name, 'pool2', self.snapshot.name,
self.volume.name, 'pool1')
# Verify if the _get_volume_by_name method is called once
self.driver._get_volume_by_name.assert_called_once()
def test_create_volume_from_snapshot_failure(self):
"""Test case for volume creation from snapshot failure.
- Sets the mock return value for _get_volume_by_name to return None.
- Calls the create_volume_from_snapshot method.
- Verifies if the create_volume_from_snapshot method raises a
driver.TYDSDriverException.
"""
# Set the mock return value for _get_volume_by_name to return None
self.driver._get_volume_by_name = mock.Mock(return_value=None)
# Call the create_volume_from_snapshot method and check for exception
self.assertRaises(driver.TYDSDriverException,
self.driver.create_volume_from_snapshot,
self.volume, self.snapshot)
@mock.patch('cinder.coordination.synchronized', new=mock.MagicMock())
def test_create_cloned_volume_success(self):
"""Test case for successful cloned volume creation.
- Sets the mock return values for get_copy_progress, get_pools,
get_volumes, and _get_volume_by_name.
- Calls the create_cloned_volume method.
- Verifies if the create_clone_volume method is called with the correct
arguments.
"""
# Set the mock return values for get_copy_progress, get_pools,
# get_volumes, and _get_volume_by_name
self.mock_client.get_copy_progress.return_value = {'progress': '100%'}
self.driver.client.get_pools.return_value = [
{'name': 'pool1', 'id': 'pool1_id'},
{'name': 'pool2', 'id': 'pool2_id'}
]
self.driver.client.get_volumes.return_value = [
{'blockName': self.volume.name, 'poolName': 'pool1',
'id': 'source_volume_id'}
]
self.driver._get_volume_by_name = mock.Mock(
return_value={'name': self.volume.name, 'id': '13'})
# Call the create_cloned_volume method
self.driver.create_cloned_volume(self.target_volume, self.src_vref)
# Verify if the create_clone_volume method is called with the correct
# arguments
self.driver.client.create_clone_volume.assert_called_once_with(
'pool1', self.volume.name, 'source_volume_id', 'pool2', 'pool2_id',
self.target_volume.name
)
@mock.patch('cinder.coordination.synchronized', new=mock.MagicMock())
def test_create_cloned_volume_failure(self):
"""Test case for cloned volume creation failure.
- Sets the mock return values for get_pools and get_volumes.
- Calls the create_cloned_volume method.
- Verifies if the create_cloned_volume method raises a
driver.TYDSDriverException.
"""
# Set the mock return values for get_pools and get_volumes
self.driver.client.get_pools.return_value = [
{'name': 'pool1', 'id': 'pool1_id'},
{'name': 'pool2', 'id': 'pool2_id'}
]
self.driver.client.get_volumes.return_value = [
{'blockName': self.volume.name, 'poolName': None, 'id': '14'}
]
# Call the create_cloned_volume method and check for exception
self.assertRaises(driver.TYDSDriverException,
self.driver.create_cloned_volume,
self.target_volume,
self.src_vref)
def test_extend_volume_success(self):
"""Test case for successful volume extension.
- Sets the new size.
- Calls the extend_volume method.
- Verifies if the extend_volume method is called with the correct
arguments.
"""
new_size = 10
# Call the extend_volume method
self.driver.extend_volume(self.volume, new_size)
# Verify if the extend_volume method is called with the correct
# arguments
self.mock_client.extend_volume.assert_called_once_with(
self.volume.name, 'pool1', new_size * 1024)
def test_extend_volume_failure(self):
"""Test case for volume extension failure.
- Sets the new size and error message.
- Sets the mock side effect for extend_volume to raise an Exception.
- Calls the extend_volume method.
- Verifies if the extend_volume method raises the expected exception
and the error message matches.
- Verifies if the extend_volume method is called with the correct
arguments.
"""
new_size = 10
# Set the mock side effect for extend_volume to raise an Exception
self.mock_client.extend_volume.side_effect = \
exception.VolumeBackendAPIException('API Error: Volume extend')
# Call the extend_volume method and check for exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.extend_volume, self.volume, new_size)
# Verify if the extend_volume method is called with the correct
# arguments
self.mock_client.extend_volume.assert_called_once_with(
self.volume.name, 'pool1', new_size * 1024)
def test_get_volume_stats(self):
"""Test case for retrieving volume statistics.
- Sets the mock side effect for safe_get to return the appropriate
values.
- Sets the mock return values for get_pools and get_volumes.
- Calls the get_volume_stats method.
- Verifies if the get_pools and get_volumes methods are called once.
- Verifies if the retrieved statistics match the expected statistics.
"""
def safe_get_side_effect(param_name):
if param_name == 'volume_backend_name':
return 'toyou_backend'
# Set the mock side effect for safe_get to return the appropriate
# values
self.configuration.safe_get.side_effect = safe_get_side_effect
# Set the mock return values for get_pools and get_volumes
self.mock_client.get_pools.return_value = [
{'name': 'pool1',
'stats': {'max_avail': '107374182400', 'stored': '53687091200'}},
{'name': 'pool2',
'stats': {'max_avail': '214748364800', 'stored': '107374182400'}}
]
self.mock_client.get_volumes.return_value = [
{'poolName': 'pool1', 'sizeMB': '1024'},
{'poolName': 'pool1', 'sizeMB': '2048'},
{'poolName': 'pool2', 'sizeMB': '3072'}
]
# Call the get_volume_stats method
stats = self.driver.get_volume_stats()
# Verify if the get_pools and get_volumes methods are called once
self.mock_client.get_pools.assert_called_once()
self.mock_client.get_volumes.assert_called_once()
# Define the expected statistics
expected_stats = {
'vendor_name': 'TOYOU',
'driver_version': '1.0.0',
'volume_backend_name': 'toyou_backend',
'pools': [
{
'pool_name': 'pool1',
'total_capacity_gb': 100.0,
'free_capacity_gb': 50.0,
'provisioned_capacity_gb': 3.0,
'thin_provisioning_support': True,
'QoS_support': False,
'consistencygroup_support': False,
'total_volumes': 2,
'multiattach': False
},
{
'pool_name': 'pool2',
'total_capacity_gb': 200.0,
'free_capacity_gb': 100.0,
'provisioned_capacity_gb': 3.0,
'thin_provisioning_support': True,
'QoS_support': False,
'consistencygroup_support': False,
'total_volumes': 1,
'multiattach': False
}
],
'storage_protocol': 'iSCSI',
}
# Verify if the retrieved statistics match the expected statistics
self.assertEqual(stats, expected_stats)
def test_get_volume_stats_pool_not_found(self):
"""Test case for retrieving volume statistics when pool not found.
- Sets the mock return value for get_pools to an empty list.
- Calls the get_volume_stats method.
- Verifies if the get_pools method is called once.
- Verifies if the get_volume_stats method raises a
driver.TYDSDriverException.
"""
# Set the mock return value for get_pools to an empty list
self.mock_client.get_pools.return_value = []
# Call the get_volume_stats method and check for exception
self.assertRaises(driver.TYDSDriverException,
self.driver.get_volume_stats)
# Verify if the get_pools method is called once
self.mock_client.get_pools.assert_called_once()
def test_initialize_connection_success(self):
"""Test case for successful volume initialization.
- Sets the connector information.
- Sets the mock return values for get_initiator_list and get_target.
- Sets the mock return values and assertions for create_initiator_group
, create_target, modify_target, and generate_config.
- Calls the initialize_connection method.
- Verifies the expected return value and method calls.
"""
# Set the connector information
connector = {
'host': 'host1',
'initiator': 'iqn.1234',
'ip': '192.168.0.1',
'uuid': 'uuid1'
}
# Set the mock return values for get_initiator_list and get_target
self.mock_client.get_initiator_list.return_value = []
self.mock_client.get_target.return_value = [
{'name': 'iqn.2023-06.com.toyou:uuid1', 'ipAddr': '192.168.0.2'}]
# Set the mock return values and assertions for create_initiator_group,
# create_target, modify_target, and generate_config
self.mock_client.create_initiator_group.return_value = None
self.mock_client.create_target.return_value = None
self.mock_client.modify_target.return_value = None
self.mock_client.generate_config.return_value = None
self.mock_client.get_initiator_target_connections.side_effect = [
[], # First call returns an empty list
[{'target_name': 'iqn.2023-06.com.toyou:initiator-group-uuid1',
'target_iqn': 'iqn1',
'block': [{'name': 'volume1', 'lunid': 0}]}]
# Second call returns a non-empty dictionary
]
# Call the initialize_connection method
result = self.driver.initialize_connection(self.volume, connector)
# Define the expected return value
expected_return = {
'driver_volume_type': 'iscsi',
'data': {
'target_discovered': False,
'target_portal': '192.168.0.2:3260',
'target_lun': 0,
'target_iqns': ['iqn.2023-06.com.toyou:initiator-group-uuid1'],
'target_portals': ['192.168.0.2:3260'],
'target_luns': [0]
}
}
# Verify the method calls and return value
self.mock_client.get_initiator_list.assert_called_once()
self.mock_client.create_initiator_group.assert_called_once()
self.assertEqual(
self.mock_client.get_initiator_target_connections.call_count, 2)
self.assertEqual(self.mock_client.get_target.call_count, 2)
self.mock_client.modify_target.assert_not_called()
self.mock_client.create_target.assert_called_once()
self.mock_client.generate_config.assert_called_once()
self.assertEqual(result, expected_return)
def test_initialize_connection_failure(self):
"""Test case for failed volume initialization.
- Sets the connector information.
- Sets the mock return values for get_initiator_list and get_it.
- Calls the initialize_connection method.
- Verifies if the get_initiator_list method is called once.
- Verifies if the create_initiator_group method is not called.
- Verifies if the initialize_connection method raises an exception to
type exception.VolumeBackendAPIException.
"""
# Set the connector information
connector = {
'host': 'host1',
'initiator': 'iqn.1234',
'ip': '192.168.0.1',
'uuid': 'uuid1'
}
# Set the mock return values for get_initiator_list and get_it
self.mock_client.get_initiator_list.return_value = [
{'group_name': 'initiator-group-uuid1'}]
self.mock_client.get_initiator_target_connections.return_value = []
# Call the initialize_connection method and check for exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.initialize_connection, self.volume,
connector)
# Verify if the get_initiator_list method is called once
self.mock_client.get_initiator_list.assert_called_once()
# Verify if the create_initiator_group method is not called
self.mock_client.create_initiator_group.assert_not_called()
def test_terminate_connection_success(self):
"""Test case for successful termination of volume connection.
- Sets the connector information.
- Sets the mock return values for get_it and get_initiator_list.
- Calls the terminate_connection method with the required mock methods.
- Verifies the method calls using assertions.
"""
# Set the connector information
connector = {
'host': 'host1',
'initiator': 'iqn.1234',
'ip': '192.168.0.1',
'uuid': 'uuid1'
}
# Set the mock return values for get_it and get_initiator_list
self.mock_client.get_initiator_target_connections.return_value = [
{'target_iqn': 'target_iqn1',
'target_name': 'target1',
'hostName': ['host1'],
'block': [{'name': 'volume1', 'lunid': 1},
{'name': 'volume2', 'lunid': 2}]}
]
self.mock_client.get_initiator_list.return_value = [
{'group_name': 'initiator-group-uuid1', 'group_id': 'group_id1'}
]
# Call the terminate_connection method with the required mock methods
self.driver.terminate_connection(
self.volume,
connector,
mock_get_it=self.mock_client.get_initiator_target_connections,
mock_delete_target=self.mock_client.delete_target,
mock_get_initiator_list=self.mock_client.get_initiator_list,
mock_delete_initiator_group=self.mock_client
.delete_initiator_group,
mock_restart_service=self.mock_client.restart_service,
)
# Verify the method calls using assertions
self.mock_client.get_initiator_target_connections.assert_called_once()
self.mock_client.get_initiator_list.assert_not_called()
self.mock_client.delete_target.assert_not_called()
self.mock_client.delete_initiator_group.assert_not_called()
self.mock_client.restart_service.assert_not_called()
def test_terminate_connection_failure(self):
"""Test case for failed termination of volume connection.
- Sets the connector information.
- Sets the mock return values for get_it and get_initiator_list.
- Sets the delete_target method to raise an exception.
- Calls the terminate_connection method.
- Verifies the method calls and assertions.
"""
# Set the connector information
connector = {
'host': 'host1',
'initiator': 'iqn.1234',
'ip': '192.168.0.1',
'uuid': 'uuid1'
}
# Set the mock return values for get_it and get_initiator_list
self.mock_client.get_initiator_target_connections.return_value = [
{
'target_iqn': 'target_iqn1',
'target_name': 'iqn.2023-06.com.toyou:initiator-group-uuid1',
'hostName': ['host1'],
'block': [{'name': self.volume.name, 'lunid': 1}]
}
]
self.mock_client.get_initiator_list.return_value = [
{'group_name': 'initiator-group-uuid1', 'group_id': 'group_id1'}
]
# Set the delete_target method to raise an exception
self.mock_client.delete_target.side_effect = \
exception.VolumeBackendAPIException('API error')
# Assert that an exception to type exception.VolumeBackendAPIException
# is raised
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.terminate_connection,
self.volume,
connector)
# Verify method calls and assertions
self.mock_client.get_initiator_target_connections.assert_called_once()
self.mock_client.get_initiator_list.assert_not_called()
self.mock_client.delete_target.assert_called_once_with('target_iqn1')
self.mock_client.delete_initiator_group.assert_not_called()
self.mock_client.restart_service.assert_not_called()

View File

@ -0,0 +1,666 @@
# Copyright 2023 toyou Corp.
# 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.
"""
Cinder driver for Toyou distributed storage.
"""
import re
import time
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
from cinder.common import constants
from cinder import coordination
from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder import utils as cinder_utils
from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume.drivers.toyou.tyds import tyds_client
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
tyds_opts = [
cfg.ListOpt('tyds_pools',
default=['pool01'],
help='The pool name where volumes are stored.'),
cfg.PortOpt('tyds_http_port',
default=80,
help='The port that connects to the http api.'),
cfg.StrOpt('tyds_stripe_size',
default='4M',
help='Volume stripe size.'),
cfg.IntOpt('tyds_clone_progress_interval',
default=3,
help='Interval (in seconds) for retrieving clone progress.'),
cfg.IntOpt('tyds_copy_progress_interval',
default=3,
help='Interval (in seconds) for retrieving copy progress.')
]
CONF = cfg.CONF
CONF.register_opts(tyds_opts, group=configuration.SHARED_CONF_GROUP)
class TYDSDriverException(exception.VolumeDriverException):
message = _("TYDS Cinder toyou failure: %(reason)s")
CREATE_VOLUME_SUCCESS = ('[Success] Cinder: Create Block Device, '
'Block Name: %s, Size in MB: %s, Pool Name: %s, '
'Stripe Size: %s.')
CREATE_VOLUME_FAILED = ('[Failed] Cinder: Create Block Device, '
'Block Name: %s, Size in MB: %s, Pool Name: %s, '
'Stripe Size: %s.')
DELETE_VOLUME_SUCCESS = ('[Success] Cinder: Delete Block Device, Block Name: '
'%s.')
DELETE_VOLUME_FAILED = ('[Failed] Cinder: delete failed, the volume: %s '
'has normal_snaps: %s, please delete '
'normal_snaps first.')
ATTACH_VOLUME_SUCCESS = ('[Success] Cinder: Attach Block Device, Block Name: '
'%s, IP Address: %s, Host: %s.')
DETACH_VOLUME_SUCCESS = ('[Success] Cinder: Detach Block Device, Block Name: '
'%s, IP Address: %s, Host: %s.')
EXTEND_VOLUME_SUCCESS = ('[Success] Cinder: Extend volume: %s from %sMB to '
'%sMB.')
CREATE_SNAPSHOT_SUCCESS = '[Success] Cinder: Create snapshot: %s, volume: %s.'
DELETE_SNAPSHOT_SUCCESS = '[Success] Cinder: Delete snapshot: %s, volume: %s.'
CREATE_VOLUME_FROM_SNAPSHOT_SUCCESS = ('[Success] Cinder: Create volume: %s, '
'pool name: %s; from snapshot: %s '
'source volume: %s, source pool name: '
'%s.')
CREATE_VOLUME_FROM_SNAPSHOT_DONE = ('[Success] Cinder: Create volume: %s '
'done, pool name: %s; from snapshot:'
' %s source volume: %s, source pool '
'name: %s.')
COPY_VOLUME_DONE = ('[Success] Cinder: Copy volume done, '
'pool_name: %s; block_name: %s '
'target_pool_name: %s, target_block_name: %s.')
COPY_VOLUME_FAILED = ('[Failed] Cinder: Copy volume failed, '
'pool_name: %s; block_name: %s '
'target_pool_name: %s, target_block_name: %s.')
@interface.volumedriver
class TYDSDriver(driver.MigrateVD, driver.BaseVD):
"""TOYOU distributed storage abstract common class.
.. code-block:: none
Version history:
1.0.0 - Initial TOYOU NetStor TYDS Driver
"""
VENDOR = 'TOYOU'
VERSION = '1.0.0'
CI_WIKI_NAME = 'TOYOU_TYDS_CI'
def __init__(self, *args, **kwargs):
super(TYDSDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(tyds_opts)
self.configuration.append_config_values(san.san_opts)
self.ip = self.configuration.san_ip
self.port = self.configuration.tyds_http_port
self.username = self.configuration.san_login
self.password = self.configuration.san_password
self.pools = self.configuration.tyds_pools
self.client = None
self.storage_protocol = constants.ISCSI
@staticmethod
def get_driver_options():
additional_opts = driver.BaseVD._get_oslo_driver_opts(
'san_ip', 'san_login', 'san_password'
)
return tyds_opts + additional_opts
def do_setup(self, context):
LOG.debug("Start setup Tyds client")
self.client = tyds_client.TydsClient(self.ip,
self.port,
self.username,
self.password)
LOG.info("Initialized Tyds Driver Client.")
def check_for_setup_error(self):
required = [
'san_ip',
'san_login',
'san_password',
'tyds_pools'
]
missing_params = [param for param in required if
not self.configuration.safe_get(param)]
if missing_params:
missing_params_str = ', '.join(missing_params)
msg = _("The following parameters are not set: %s" %
missing_params_str)
raise exception.InvalidInput(
reason=msg)
def _update_volume_stats(self):
"""Update the backend stats including TOYOU info and pools info."""
backend_name = self.configuration.safe_get('volume_backend_name')
self._stats = {
'vendor_name': self.VENDOR,
'driver_version': self.VERSION,
'volume_backend_name': backend_name,
'pools': self._get_pools_stats(),
'storage_protocol': self.storage_protocol,
}
LOG.debug('Update volume stats: %s.', self._stats)
def _get_pools_stats(self):
"""Get pools statistics."""
pools_data = self.client.get_pools()
volumes_list = self.client.get_volumes()
pools_stats = []
for pool_name in self.pools:
pool_info = next(
(data for data in pools_data if data['name'] == pool_name),
None
)
if pool_info:
max_avail = int(pool_info['stats']['max_avail'])
stored = int(pool_info['stats']['stored'])
free_capacity = self._convert_gb(max_avail - stored, "B")
total_capacity = self._convert_gb(max_avail, "B")
allocated_capacity = 0
total_volumes = 0
for vol in volumes_list:
if vol['poolName'] == pool_name:
allocated_capacity += self._convert_gb(
int(vol['sizeMB']), "MB")
total_volumes += 1
pools_stats.append({
'pool_name': pool_name,
'total_capacity_gb': total_capacity,
'free_capacity_gb': free_capacity,
'provisioned_capacity_gb': allocated_capacity,
'thin_provisioning_support': True,
'QoS_support': False,
'consistencygroup_support': False,
'total_volumes': total_volumes,
'multiattach': False
})
else:
raise TYDSDriverException(
reason=_(
'Backend storage pool "%s" not found.') % pool_name
)
return pools_stats
def _get_volume_by_name(self, volume_name):
"""Get volume information by name."""
volume_list = self.client.get_volumes()
for vol in volume_list:
if vol.get('blockName') == volume_name:
return vol
# Returns an empty dictionary indicating that the volume with the
# corresponding name was not found
return {}
def _get_snapshot_by_name(self, snapshot_name, volume_id=None):
"""Get snapshot information by name and optional volume ID."""
snapshot_list = self.client.get_snapshot(volume_id)
for snap in snapshot_list:
if snap.get('snapShotName') == snapshot_name:
return snap
# Returns an empty dictionary indicating that a snapshot with the
# corresponding name was not found
return {}
@staticmethod
def _convert_gb(size, unit):
"""Convert size from the given unit to GB."""
size_gb = 0
if unit in ['B', '']:
size_gb = size / units.Gi
elif unit in ['M', 'MB']:
size_gb = size / units.Ki
return float('%.0f' % size_gb)
def _clone_volume(self, pool_name, block_name, block_id, target_pool_name,
target_pool_id, target_block_name):
self.client.create_clone_volume(
pool_name,
block_name,
block_id,
target_pool_name,
target_pool_id,
target_block_name
)
@coordination.synchronized('tyds-copy-{lun_name}-progress')
def _wait_copy_progress(lun_id, lun_name, target_block):
try:
ret = False
while_exit = False
rescan = 0
interval = self.configuration.tyds_copy_progress_interval
while True:
rescan += 1
progress_data = self.client.get_copy_progress(
lun_id, lun_name, target_block)
progress = progress_data.get('progress')
# finished clone
if progress == '100%':
# check new volume existence
target = self._get_volume_by_name(target_block)
if not target:
LOG.info(
'Clone rescan: %(rescan)s, target volume '
'completed delayed, from %(block_name)s to '
'%(target_block_name)s.',
{'rescan': rescan, 'block_name': lun_name,
'target_block_name': target_block})
time.sleep(interval)
continue
LOG.info(
'Clone rescan: %(rescan)s, task done from '
'%(block_name)s to %(target_block_name)s.',
{'rescan': rescan, 'block_name': lun_name,
'target_block_name': target_block})
while_exit = True
ret = True
elif progress:
LOG.info(
"Clone rescan: %(rescan)s, progress: %(progress)s,"
" block_name: %(block_name)s, target_block_name: "
"%(target_block_name)s",
{"rescan": rescan, "progress": progress,
"block_name": lun_name,
"target_block_name": target_block})
else:
LOG.error(
'Copy: rescan: %(rescan)s, task error from '
'%(block_name)s to %(target_block_name)s.',
{'rescan': rescan, 'block_name': lun_name,
'target_block_name': target_block_name})
while_exit = True
if while_exit:
break
time.sleep(interval)
return ret
except Exception as err:
LOG.error('Copy volume failed reason: %s', err)
return False
if _wait_copy_progress(block_id, block_name, target_block_name):
LOG.debug(COPY_VOLUME_DONE, pool_name,
block_name, target_pool_name, target_block_name)
else:
self._delete_volume_if_clone_failed(target_block_name, pool_name,
block_name, target_block_name)
msg = _("copy volume failed from %s to %s") % (
block_name, target_block_name)
raise TYDSDriverException(reason=msg)
def _delete_volume_if_clone_failed(self, target_block_name, pool_name,
block_name, target_pool_name):
target_volume = self._get_volume_by_name(target_block_name)
if target_volume:
self.client.delete_volume(target_volume.get('id'))
LOG.debug(COPY_VOLUME_FAILED, pool_name, block_name,
target_pool_name, target_block_name)
def create_export(self, context, volume, connector):
pass
def create_volume(self, volume):
LOG.info("Creating volume '%s'", volume.name)
vol_name = cinder_utils.convert_str(volume.name)
size = int(volume.size) * 1024
pool_name = volume_utils.extract_host(volume.host, 'pool')
stripe_size = self.configuration.tyds_stripe_size
self.client.create_volume(vol_name, size, pool_name, stripe_size)
LOG.debug(CREATE_VOLUME_SUCCESS, vol_name, size, pool_name,
stripe_size)
def retype(self, context, volume, new_type, diff, host):
# success
return True, None
def delete_volume(self, volume):
LOG.debug("deleting volume '%s'", volume.name)
vol_name = cinder_utils.convert_str(volume.name)
vol = self._get_volume_by_name(vol_name)
if vol and vol.get('id'):
self.client.delete_volume(vol.get('id'))
LOG.debug(DELETE_VOLUME_SUCCESS, vol_name)
else:
LOG.info('Delete volume %s not found.', vol_name)
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass
def initialize_connection(self, volume, connector):
LOG.debug('initialize_connection: volume %(vol)s with connector '
'%(conn)s', {'vol': volume.name, 'conn': connector})
pool_name = volume_utils.extract_host(volume.host, 'pool')
volume_name = cinder_utils.convert_str(volume.name)
group_name = "initiator-group-" + cinder_utils.convert_str(
connector['uuid'])
vol_info = {"name": volume_name, "size": volume.size,
"pool": pool_name}
# Check initiator existence
initiator_list = self.client.get_initiator_list()
initiator_existence = False
if initiator_list:
initiator_existence = any(
initiator['group_name'] == group_name for initiator in
initiator_list
)
if not initiator_existence:
# Create initiator
client = [{"ip": connector["ip"], "iqn": connector["initiator"]}]
self.client.create_initiator_group(group_name=group_name,
client=client)
# Check Initiator-Target connection existence
# add new volume to existing Initiator-Target connection
it_list = self.client.get_initiator_target_connections()
it_info = None
if it_list:
it_info = next((it for it in it_list if group_name in
it['target_name']), None)
if it_info:
target_iqn = it_info['target_iqn']
lun_info = next((lun for lun in it_info['block'] if
lun['name'] == volume_name), None)
if not lun_info:
# Add new volume to existing Initiator-Target connection
target_name_list = it_info['hostName']
vols_info = it_info['block']
vols_info.append(vol_info)
self.client.modify_target(target_iqn, target_name_list,
vols_info)
else:
# Create new Initiator-Target connection
target_node_list = self.client.get_target()
target_name_list = [target['name'] for target in target_node_list]
self.client.create_target(group_name, target_name_list, [vol_info])
it_list = self.client.get_initiator_target_connections()
if it_list:
it_info = next(
(it for it in it_list if group_name in it['target_name']),
None)
if it_info:
target_name = it_info['target_name']
target_iqn = it_info['target_iqn']
lun_info = next((lun for lun in it_info['block'] if lun['name']
== volume_name), None)
lun_id = lun_info['lunid'] if lun_info else 0
# Generate config
self.client.generate_config(target_iqn)
# Generate return info
target_node_list = self.client.get_target()
target_node = target_node_list[0]
target_ip = target_node['ipAddr']
target_portal = '[%s]:3260' % target_ip if ':' in target_ip \
else '%s:3260' % target_ip
target_iqns = [target_name] * len(target_node_list)
target_portals = ['[%s]:3260' % p['ipAddr'] if ':' in p['ipAddr']
else '%s:3260' % p['ipAddr']
for p in target_node_list]
target_luns = [lun_id] * len(target_node_list)
properties = {
'target_discovered': False,
'target_portal': target_portal,
'target_lun': lun_id,
'target_iqns': target_iqns,
'target_portals': target_portals,
'target_luns': target_luns
}
LOG.debug('connection properties: %s', properties)
LOG.debug(ATTACH_VOLUME_SUCCESS, volume_name,
connector.get('ip'), connector.get('host'))
return {'driver_volume_type': 'iscsi', 'data': properties}
else:
raise exception.VolumeBackendAPIException(
data=_('initialize_connection: Failed to create IT '
'connection for volume %s') % volume_name)
def terminate_connection(self, volume, connector, **kwargs):
if not connector:
# If the connector is empty, the info log is recorded and
# returned directly, without subsequent separation operations
LOG.info(
"Connector is None. Skipping termination for volume %s.",
volume.name)
return
volume_name = cinder_utils.convert_str(volume.name)
group_name = "initiator-group-" + cinder_utils.convert_str(
connector['uuid'])
data = {}
# Check Initiator-Target connection existence and remove volume from
# Initiator-Target connection if it exists
it_list = self.client.get_initiator_target_connections()
it_info = next((it for it in it_list if group_name in
it['target_name']), None)
if it_info:
target_iqn = it_info['target_iqn']
target_name_list = it_info['hostName']
vols_info = it_info['block']
vols_info = [vol for vol in vols_info if
vol['name'] != volume_name]
if not vols_info:
self.client.delete_target(it_info['target_iqn'])
initiator_list = self.client.get_initiator_list()
initiator_to_delete = None
if initiator_list:
initiator_to_delete = next(
(initiator for initiator in initiator_list if
initiator['group_name'] == group_name), None)
if initiator_to_delete:
self.client.delete_initiator_group(
initiator_to_delete['group_id'])
self.client.restart_service(host_name=it_info['hostName'])
else:
self.client.modify_target(target_iqn, target_name_list,
vols_info)
# record log
LOG.debug(DETACH_VOLUME_SUCCESS, volume_name, connector.get(
'ip'), connector.get('host'))
LOG.info('Detach volume %s successfully', volume_name)
target_node_list = self.client.get_target()
target_portals = ['%s:3260' % p['ipAddr']
for p in target_node_list]
data['ports'] = target_portals
return {'driver_volume_type': 'iscsi', 'data': data}
def migrate_volume(self):
pass
def extend_volume(self, volume, new_size):
volume_name = cinder_utils.convert_str(volume.name)
pool_name = volume_utils.extract_host(volume.host, 'pool')
size_mb = int(new_size) * 1024
self.client.extend_volume(volume_name, pool_name, size_mb)
LOG.debug(EXTEND_VOLUME_SUCCESS, volume_name, volume.size *
1024, size_mb)
def create_cloned_volume(self, volume, src_vref):
"""Clone a volume."""
# find pool_id to create clone volume
try:
target_pool_name = volume_utils.extract_host(volume.host, 'pool')
except Exception as err:
msg = _('target_pool_name must be specified. '
'extra err msg was: %s') % err
raise TYDSDriverException(reason=msg)
target_pool_id = None
pool_list = self.client.get_pools()
for pool in pool_list:
if target_pool_name == pool.get('name'):
target_pool_id = pool.get('id')
break
if not target_pool_id:
msg = _('target_pool_id: must be specified.')
raise TYDSDriverException(reason=msg)
# find volume id to create
volume_list = self.client.get_volumes()
block_name = cinder_utils.convert_str(src_vref.name)
pool_name = None
block_id = None
for vol in volume_list:
if block_name == vol.get('blockName'):
pool_name = vol.get('poolName')
block_id = vol.get('id')
break
if (not pool_name) or (not block_id):
msg = _('block_name: %(block_name)s does not matched a '
'pool_name or a block_id.') % {'block_name': block_name}
raise TYDSDriverException(reason=msg)
# get a name from new volume
target_block_name = cinder_utils.convert_str(volume.name)
# ready to create clone volume
self._clone_volume(pool_name, block_name, block_id, target_pool_name,
target_pool_id, target_block_name)
# handle the case where the new volume size is larger than the source
if volume['size'] > src_vref.get('size'):
size_mb = int(volume['size']) * 1024
self.client.extend_volume(target_block_name, target_pool_name,
size_mb)
LOG.debug(EXTEND_VOLUME_SUCCESS, target_block_name,
src_vref.get('size') * 1024, size_mb)
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
volume_name = cinder_utils.convert_str(snapshot.volume_name)
snapshot_name = cinder_utils.convert_str(snapshot.name)
vol = self._get_volume_by_name(volume_name)
if vol and vol.get('id'):
comment = '%s/%s' % (volume_name, snapshot_name)
self.client.create_snapshot(snapshot_name, vol.get('id'), comment)
LOG.debug(CREATE_SNAPSHOT_SUCCESS, snapshot_name,
volume_name)
else:
msg = _('Volume "%s" not found.') % volume_name
raise TYDSDriverException(reason=msg)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
snapshot_name = cinder_utils.convert_str(snapshot.name)
volume_name = cinder_utils.convert_str(snapshot.volume_name)
snap = self._get_snapshot_by_name(snapshot_name)
if snap and snap.get('id'):
self.client.delete_snapshot(snap.get('id'))
LOG.debug(DELETE_SNAPSHOT_SUCCESS, snapshot_name,
volume_name)
else:
LOG.info('Delete snapshot %s not found.', snapshot_name)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
snapshot_name = cinder_utils.convert_str(snapshot.name)
volume_name = cinder_utils.convert_str(volume.name)
pool_name = volume_utils.extract_host(volume.host, 'pool')
source_volume = cinder_utils.convert_str(snapshot.volume_name)
src_vol = self._get_volume_by_name(source_volume)
if not src_vol:
msg = _('Volume "%s" not found in '
'create_volume_from_snapshot.') % volume_name
raise TYDSDriverException(reason=msg)
self.client.create_volume_from_snapshot(volume_name, pool_name,
snapshot_name, source_volume,
src_vol.get('poolName'))
LOG.debug(CREATE_VOLUME_FROM_SNAPSHOT_SUCCESS, volume_name,
pool_name, snapshot_name, source_volume,
src_vol.get('poolName'))
@coordination.synchronized('tyds-clone-{source_name}-progress')
def _wait_clone_progress(task_id, source_name, target_name):
ret = False
while_exit = False
rescan = 0
interval = self.configuration.tyds_clone_progress_interval
while True:
rescan += 1
progress = self.client.get_clone_progress(
task_id, source_name).get('progress', '')
if progress == '100%':
target = self._get_volume_by_name(target_name)
if not target:
LOG.info('Clone: rescan: %(rescan)s, task not begin, '
'from %(source)s to %(target)s.',
{'rescan': rescan,
'source': source_name,
'target': target_name})
time.sleep(interval)
continue
LOG.info('Clone: rescan: %(rescan)s, task done from '
'%(source)s to %(target)s.',
{'rescan': rescan,
'source': source_name,
'target': target_name})
while_exit = True
ret = True
elif re.fullmatch(r'^\d{1,2}%$', progress):
LOG.info('Clone: rescan: %(rescan)s, task progress: '
'%(progress)s, from %(source)s to %(target)s.',
{'rescan': rescan,
'progress': progress,
'source': source_name,
'target': target_name})
else:
while_exit = True
LOG.error('Clone: rescan: %(rescan)s, task error from '
'%(source)s to %(target)s.',
{'rescan': rescan,
'source': source_name,
'target': target_name})
if while_exit:
break
time.sleep(interval)
return ret
if _wait_clone_progress(src_vol.get('id'), source_volume, volume_name):
LOG.debug(CREATE_VOLUME_FROM_SNAPSHOT_DONE,
volume_name, pool_name, snapshot_name, source_volume,
src_vol.get('poolName'))
# handle the case where the new volume size is larger than the source
new_size = volume.size * 1024
old_size = int(src_vol['sizeMB'])
if new_size > old_size:
self.client.extend_volume(volume_name, pool_name, new_size)
LOG.debug(EXTEND_VOLUME_SUCCESS, volume_name, old_size,
new_size)

View File

@ -0,0 +1,481 @@
# Copyright 2023 toyou Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import base64
import json
import time
from oslo_log import log as logging
from oslo_utils import netutils
import requests
from cinder import exception
from cinder.i18n import _
LOG = logging.getLogger(__name__)
class TydsClient(object):
def __init__(self, hostname, port, username, password):
"""Initializes a new instance of the TydsClient.
:param hostname: IP address of the Toyou distributed storage system.
:param port: The port to connect to the Toyou distributed storage
system.
:param username: The username for authentication.
:param password: The password for authentication.
"""
self._username = username
self._password = base64.standard_b64encode(password.encode('utf-8')
).decode('utf-8')
self._baseurl = f"http://{hostname}:{port}/api"
self._snapshot_count = 999
self._token = None
self._token_expiration = 0
self._ip = self._get_local_ip()
def get_token(self):
if self._token and time.time() < self._token_expiration:
# Token is not expired, directly return the existing Token
return self._token
# Token has expired or has not been obtained before,
# retrieving the Token again
self._token = self.login()
self._token_expiration = time.time() + 710 * 60
return self._token
def send_http_api(self, url, params=None, method='post'):
"""Send an HTTP API request to the storage.
:param url: The URL for the API request.
:param params: The parameters for the API request.
:param method: The HTTP method for the API request. Default is 'post'.
:return: The response from the API request.
:raises VolumeBackendAPIException: If the API request fails.
"""
if params:
params = json.dumps(params)
url = f"{self._baseurl}/{url}"
header = {
'Authorization': self.get_token(),
'Content-Type': 'application/json'
}
LOG.debug(
"Toyou Cinder Driver Requests: http_process header: %(header)s "
"url: %(url)s method: %(method)s",
{'header': header, 'url': url, 'method': method}
)
response = self.do_request(method, url, header, params)
return response
@staticmethod
def _get_local_ip():
"""Get the local IP address.
:return: The local IP address.
"""
return netutils.get_my_ipv4()
def login(self):
"""Perform login to obtain an authentication token.
:return: The authentication token.
:raises VolumeBackendAPIException: If the login request fails or the
authentication token cannot be
obtained.
"""
params = {
'REMOTE_ADDR': self._ip,
'username': self._username,
'password': self._password
}
data = json.dumps(params)
url = f"{self._baseurl}/auth/login/"
response = self.do_request(method='post',
url=url,
header={'Content-Type': 'application/json'},
data=data)
self._token = response.get('token')
return self._token
@staticmethod
def do_request(method, url, header, data):
"""Send request to the storage and handle the response.
:param method: The HTTP method to use for the request. Valid methods
are 'post', 'get', 'put', and 'delete'.
:param url: The URL to send the request to.
:param header: The headers to include in the request.
:param data: The data to send in the request body.
:return: The response data returned by the storage system.
:raises VolumeBackendAPIException: If the request fails or the response
from the storage system is not as
expected.
"""
valid_methods = ['post', 'get', 'put', 'delete']
if method not in valid_methods:
raise exception.VolumeBackendAPIException(
data=_('Unsupported request type: %s.') % method
)
try:
req = getattr(requests, method)(url, data=data, headers=header)
req.raise_for_status()
response = req.json()
except requests.exceptions.RequestException as e:
msg = (_('Request to %(url)s failed: %(error)s') %
{'url': url, 'error': str(e)})
raise exception.VolumeBackendAPIException(data=msg)
except ValueError as e:
msg = (_('Failed to parse response from %(url)s: %(error)s') %
{'url': url, 'error': str(e)})
raise exception.VolumeBackendAPIException(data=msg)
LOG.debug('URL: %(url)s, TYPE: %(type)s, CODE: %(code)s, '
'RESPONSE: %(response)s.',
{'url': req.url,
'type': method,
'code': req.status_code,
'response': response})
# Response Error
if response.get('code') != '0000':
msg = (_('ERROR RESPONSE: %(response)s URL: %(url)s PARAMS: '
'%(params)s.') %
{'response': response, 'url': url, 'params': data})
raise exception.VolumeBackendAPIException(data=msg)
# return result
return response.get('data')
def get_pools(self):
"""Query pool information.
:return: A list of pool information.
"""
url = 'pool/pool/'
response = self.send_http_api(url=url, method='get')
pool_list = response.get('poolList', [])
return pool_list
def get_volumes(self):
"""Query volume information.
:return: A list of volume information.
"""
url = 'block/blocks'
vol_list = self.send_http_api(url=url, method='get').get('blockList')
return vol_list
def create_volume(self, vol_name, size, pool_name, stripe_size):
"""Create a volume.
:param vol_name: The name of the volume.
:param size: The size of the volume in MB.
:param pool_name: The name of the pool to create the volume in.
:param stripe_size: The stripe size of the volume.
:return: The response from the API call.
"""
url = 'block/blocks/'
params = {'blockName': vol_name,
'sizeMB': size,
'poolName': pool_name,
'stripSize': stripe_size}
return self.send_http_api(url=url, method='post', params=params)
def delete_volume(self, vol_id):
"""Delete a volume.
:param vol_id: The ID of the volume to delete.
"""
url = 'block/recycle/forceCreate/'
params = {'id': [vol_id]}
self.send_http_api(url=url, method='post', params=params)
def extend_volume(self, vol_name, pool_name, size_mb):
"""Extend the size of a volume.
:param vol_name: The name of the volume to extend.
:param pool_name: The name of the pool where the volume resides.
:param size_mb: The new size of the volume in MB.
"""
url = 'block/blocks/%s/' % vol_name
params = {'blockName': vol_name,
'sizeMB': size_mb,
'poolName': pool_name}
self.send_http_api(url=url, method='put', params=params)
def create_clone_volume(self, *args):
"""Create a clone of a volume.
:param args: The arguments needed for cloning a volume.
Args:
- pool_name: The name of the source pool.
- block_name: The name of the source block.
- block_id: The ID of the source block.
- target_pool_name: The name of the target pool.
- target_pool_id: The ID of the target pool.
- target_block_name: The name of the target block.
"""
pool_name, block_name, block_id, target_pool_name, target_pool_id,\
target_block_name = args
params = {
'poolName': pool_name,
'blockName': block_name,
'blockId': block_id,
'copyType': 0, # 0 means shallow copy, currently copy volume first
# default shallow copy, 1 means deep copy
'metapoolName': 'NULL',
'targetMetapoolName': 'NULL',
'targetPoolName': target_pool_name,
'targetPoolId': target_pool_id,
'targetBlockName': target_block_name
}
url = 'block/block/copy/'
self.send_http_api(url=url, params=params)
def get_snapshot(self, volume_id=None):
"""Get a list of snapshots.
:param volume_id: The ID of the volume to filter snapshots (default:
None).
:return: The list of snapshots.
"""
url = 'block/snapshot?pageNumber=1'
if volume_id:
url += '&blockId=%s' % volume_id
url += '&pageSize=%s'
response = self.send_http_api(
url=url % self._snapshot_count, method='get')
if self._snapshot_count < int(response.get('total')):
self._snapshot_count = int(response.get('total'))
response = self.send_http_api(
url=url % self._snapshot_count, method='get')
snapshot_list = response.get('snapShotList')
return snapshot_list
def create_snapshot(self, name, volume_id, comment=''):
"""Create a snapshot of a volume.
:param name: The name of the snapshot.
:param volume_id: The ID of the volume to create a snapshot from.
:param comment: The optional comment for the snapshot (default: '').
"""
url = 'block/snapshot/'
params = {'sourceBlock': volume_id,
'snapShotName': name,
'remark': comment}
self.send_http_api(url=url, method='post', params=params)
def delete_snapshot(self, snapshot_id):
"""Delete a snapshot.
:param snapshot_id: The ID of the snapshot to delete.
"""
url = 'block/snapshot/%s/' % snapshot_id
self.send_http_api(url=url, method='delete')
def create_volume_from_snapshot(self, volume_name, pool_name,
snapshot_name, source_volume_name,
source_pool_name):
"""Create a volume from a snapshot.
:param volume_name: The name of the new volume.
:param pool_name: The name of the pool for the new volume.
:param snapshot_name: The name of the snapshot to create the volume
from.
:param source_volume_name: The name of the source volume (snapshot's
origin).
:param source_pool_name: The name of the pool for the source volume.
"""
url = 'block/clone/'
params = {'cloneBlockName': volume_name,
'targetPoolName': pool_name,
'snapName': snapshot_name,
'blockName': source_volume_name,
'poolName': source_pool_name,
'targetMetapoolName': 'NULL'}
self.send_http_api(url=url, method='post', params=params)
def get_clone_progress(self, volume_id, volume_name):
"""Get the progress of a volume clone operation.
:param volume_id: The ID of the volume being cloned.
:param volume_name: The name of the volume being cloned.
:return: The progress of the clone operation.
"""
url = 'block/clone/progress/'
params = {'blockId': volume_id,
'blockName': volume_name}
progress = self.send_http_api(url=url, method='post', params=params)
return progress
def get_copy_progress(self, block_id, block_name, target_block_name):
"""Get the progress of a block copy operation.
:param block_id: The ID of the block being copied.
:param block_name: The name of the block being copied.
:param target_block_name: The name of the target block.
:return: The progress of the copy operation.
"""
url = 'block/block/copyprogress/'
params = {
'blockId': block_id,
'blockName': block_name,
'targetBlockName': target_block_name
}
progress_data = self.send_http_api(url=url, params=params)
return progress_data
def create_initiator_group(self, group_name, client):
"""Create an initiator group.
:param group_name: The name of the initiator group.
:param client: The client information for the initiator group.
"""
url = 'iscsi/client-group/'
params = {
'group_name': group_name,
'client': client,
'chap_auth': 0,
'mode': 'ISCSI'
}
self.send_http_api(url=url, params=params)
def delete_initiator_group(self, group_id):
"""Delete an initiator group.
:param group_id: The ID of the initiator group.
:return: The response from the API call.
"""
url = 'iscsi/client-group/?group_id=%s' % group_id
return self.send_http_api(url=url, method='delete')
def get_initiator_list(self):
"""Get the list of initiators.
:return: The list of initiators.
"""
url = 'iscsi/client-group/'
res = self.send_http_api(url=url, method='get')
initiator_list = res.get('client_group_list')
return initiator_list
def get_target(self):
"""Get the list of target hosts.
:return: The list of target hosts.
"""
url = '/host/host/'
res = self.send_http_api(url=url, method='get')
target = res.get('hostList')
return target
def create_target(self, group_name, target_list, vols_info):
"""Create a target.
:param group_name: The name of the initiator group.
:param target_list: The list of target hosts.
:param vols_info: The information of the volumes.
:return: The response from the API call.
"""
url = 'iscsi/target/'
params = {"group_name": group_name,
"chap_auth": 0,
"write_cache": 1,
"hostName": ",".join(target_list),
"block": vols_info}
return self.send_http_api(url=url, params=params, method='post')
def delete_target(self, target_name):
"""Delete a target.
:param target_name: The name of the target.
:return: The response from the API call.
"""
url = 'iscsi/target/?targetIqn=%s' % target_name
return self.send_http_api(url=url, method='delete')
def modify_target(self, target_name, target_list, vol_info):
"""Modify a target.
:param target_name: The name of the target.
:param target_list: The list of target hosts.
:param vol_info: The information of the volumes.
:return: The response from the API call.
"""
url = 'iscsi/target/'
params = {
"targetIqn": target_name,
"chap_auth": 0,
"hostName": target_list,
"block": vol_info
}
return self.send_http_api(url=url, params=params, method='put')
def get_initiator_target_connections(self):
"""Get the list of IT (Initiator-Target) connections.
:return: The list of IT connections.
"""
url = 'iscsi/target/'
res = self.send_http_api(url=url, method='get')
target_list = res.get('target_list')
return target_list
def generate_config(self, target_name):
"""Generate configuration for a target.
:param target_name: The name of the target.
"""
url = 'iscsi/target-config/'
params = {
'targetName': target_name
}
self.send_http_api(url=url, params=params, method='post')
def restart_service(self, host_name):
"""Restart the iSCSI service on a host.
:param host_name: The name of the host.
"""
url = 'iscsi/service/restart/'
params = {
"hostName": host_name
}
self.send_http_api(url=url, params=params, method='post')

View File

@ -0,0 +1,75 @@
================================
TOYOU NetStor TYDS Cinder driver
================================
TOYOU NetStor TYDS series volume driver provides OpenStack Compute instances
with access to TOYOU NetStor TYDS series storage systems.
TOYOU NetStor TYDS storage can be used with iSCSI connection.
This documentation explains how to configure and connect the block storage
nodes to TOYOU NetStor TYDS series storage.
Driver options
~~~~~~~~~~~~~~
The following table contains the configuration options supported by the
TOYOU NetStor TYDS iSCSI driver.
.. config-table::
:config-target: TOYOU NetStor TYDS
cinder.volume.drivers.toyou.tyds.tyds
Supported operations
~~~~~~~~~~~~~~~~~~~~
- Create Volume.
- Delete Volume.
- Attach Volume.
- Detach Volume.
- Extend Volume
- Create Snapshot.
- Delete Snapshot.
- Create Volume from Snapshot.
- Create Volume from Volume (clone).
- Create lmage from Volume.
- Volume Migration (host assisted).
Configure TOYOU NetStor TOYOU TYDS iSCSI backend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This section details the steps required to configure the TOYOU NetStor
TYDS storage cinder driver.
#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]``
section, set the enabled_backends parameter
with the iSCSI back-end group.
.. code-block:: ini
[DEFAULT]
enabled_backends = toyou-tyds-iscsi-1
#. 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
[toyou-tyds-iscsi-1]
# The TOYOU NetStor TYDS driver path
volume_driver = cinder.volume.drivers.toyou.tyds.tyds.TYDSDriver
# Management http ip of TOYOU NetStor TYDS storage
san_ip = 10.0.0.10
# Management http username of TOYOU NetStor TYDS storage
san_login = superuser
# Management http password of TOYOU NetStor TYDS storage
san_password = Toyou@123
# The Pool used to allocated volumes
tyds_pools = pool01
# Backend name
volume_backend_name = toyou-tyds-iscsi-1

View File

@ -204,6 +204,9 @@ title=Synology Storage Driver (iSCSI)
[driver.toyou_netstor]
title=TOYOU NetStor Storage Driver (iSCSI, FC)
[driver.toyou_netstor_tyds]
title=TOYOU NetStor TYDS Storage Driver (iSCSI)
[driver.vrtsaccess]
title=Veritas Access iSCSI Driver (iSCSI)
@ -301,6 +304,7 @@ driver.seagate=complete
driver.storpool=complete
driver.synology=complete
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=complete
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -378,6 +382,7 @@ driver.seagate=complete
driver.storpool=complete
driver.synology=complete
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=complete
driver.vrtsaccess=complete
driver.vrtscnfs=complete
driver.vzstorage=complete
@ -458,6 +463,7 @@ driver.seagate=missing
driver.storpool=missing
driver.synology=missing
driver.toyou_netstor=missing
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -537,6 +543,7 @@ driver.seagate=missing
driver.storpool=complete
driver.synology=missing
driver.toyou_netstor=missing
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -617,6 +624,7 @@ driver.seagate=missing
driver.storpool=missing
driver.synology=missing
driver.toyou_netstor=missing
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -696,6 +704,7 @@ driver.seagate=missing
driver.storpool=complete
driver.synology=missing
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=complete
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -776,6 +785,7 @@ driver.seagate=missing
driver.storpool=complete
driver.synology=missing
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -856,6 +866,7 @@ driver.seagate=complete
driver.storpool=complete
driver.synology=missing
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -933,6 +944,7 @@ driver.seagate=missing
driver.storpool=missing
driver.synology=missing
driver.toyou_netstor=complete
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
@ -1014,6 +1026,7 @@ driver.seagate=missing
driver.storpool=missing
driver.synology=missing
driver.toyou_netstor=missing
driver.toyou_netstor_tyds=missing
driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing

View File

@ -0,0 +1,4 @@
---
features:
- |
New ISCSI cinder volume driver for TOYOU NetStor TYDS Storage.