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:
parent
2b0567b0e5
commit
6fcb495c8b
@ -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,
|
||||
|
690
cinder/tests/unit/volume/drivers/toyou/test_tyds.py
Normal file
690
cinder/tests/unit/volume/drivers/toyou/test_tyds.py
Normal 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()
|
0
cinder/volume/drivers/toyou/tyds/__init__.py
Normal file
0
cinder/volume/drivers/toyou/tyds/__init__.py
Normal file
666
cinder/volume/drivers/toyou/tyds/tyds.py
Normal file
666
cinder/volume/drivers/toyou/tyds/tyds.py
Normal 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)
|
481
cinder/volume/drivers/toyou/tyds/tyds_client.py
Normal file
481
cinder/volume/drivers/toyou/tyds/tyds_client.py
Normal 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')
|
@ -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
|
@ -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
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
New ISCSI cinder volume driver for TOYOU NetStor TYDS Storage.
|
Loading…
x
Reference in New Issue
Block a user