Add Synology DSM storage driver

This driver supports below features:
- Volume Create/Delete
- Volume Attach/Detach
- Snapshot Create/Delete
- Create Volume from Snapshot
- Get Volume Stats
- Copy Image to Volume
- Copy Volume to Image
- Clone Volume
- Extend Volume

DocImpact
Implements: blueprint synology-cinder-driver
Co-Authored-By: Taylor Huang<taylorh@synology.com>

Change-Id: I18d47f5d4cf10e4f481722406c6a5c071a521616
This commit is contained in:
arsenc 2016-06-23 16:06:01 +08:00
parent 751af8c86c
commit 78d124dee2
8 changed files with 3392 additions and 0 deletions

View File

@ -1181,3 +1181,16 @@ class GCSOAuth2Failure(BackupDriverException):
# Kaminario K2
class KaminarioCinderDriverException(VolumeDriverException):
message = _("KaminarioCinderDriver failure: %(reason)s")
# Synology driver
class SynoAPIHTTPError(CinderException):
message = _("HTTP exit code: [%(code)s]")
class SynoAuthError(CinderException):
pass
class SynoLUNNotExist(CinderException):
message = _("LUN not found by UUID: %(uuid)s.")

View File

@ -152,6 +152,8 @@ from cinder.volume.drivers import scality as cinder_volume_drivers_scality
from cinder.volume.drivers import sheepdog as cinder_volume_drivers_sheepdog
from cinder.volume.drivers import smbfs as cinder_volume_drivers_smbfs
from cinder.volume.drivers import solidfire as cinder_volume_drivers_solidfire
from cinder.volume.drivers.synology import synology_common as \
cinder_volume_drivers_synology_synologycommon
from cinder.volume.drivers import tegile as cinder_volume_drivers_tegile
from cinder.volume.drivers import tintri as cinder_volume_drivers_tintri
from cinder.volume.drivers.violin import v7000_common as \
@ -306,6 +308,7 @@ def list_opts():
cinder_volume_drivers_hitachi_hbsdfc.volume_opts,
cinder_quota.quota_opts,
cinder_volume_drivers_huawei_huaweidriver.huawei_opts,
cinder_volume_drivers_synology_synologycommon.cinder_opts,
cinder_volume_drivers_dell_dellstoragecentercommon.
common_opts,
cinder_scheduler_hostmanager.host_manager_opts,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,358 @@
# Copyright (c) 2016 Synology Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Tests for the Synology iSCSI volume driver."""
import mock
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_constants as fake
from cinder.volume import configuration as conf
from cinder.volume.drivers.synology import synology_common as common
from cinder.volume.drivers.synology import synology_iscsi
VOLUME_ID = fake.VOLUME_ID
TARGET_NAME_PREFIX = 'Cinder-Target-'
IP = '10.10.10.10'
IQN = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + VOLUME_ID
TRG_ID = 1
VOLUME = {
'name': fake.VOLUME_NAME,
'id': VOLUME_ID,
'display_name': 'fake_volume',
'size': 10,
'provider_location': '%s:3260,%d %s 1' % (IP, TRG_ID, IQN),
}
NEW_VOLUME_ID = fake.VOLUME2_ID
IQN2 = 'iqn.2000-01.com.synology:' + TARGET_NAME_PREFIX + NEW_VOLUME_ID
NEW_TRG_ID = 2
NEW_VOLUME = {
'name': fake.VOLUME2_NAME,
'id': NEW_VOLUME_ID,
'display_name': 'new_fake_volume',
'size': 10,
'provider_location': '%s:3260,%d %s 1' % (IP, NEW_TRG_ID, IQN2),
}
SNAPSHOT_ID = fake.SNAPSHOT_ID
SNAPSHOT = {
'name': fake.SNAPSHOT_NAME,
'id': SNAPSHOT_ID,
'volume_id': VOLUME_ID,
'volume_name': VOLUME['name'],
'volume_size': 10,
'display_name': 'fake_snapshot',
}
DS_SNAPSHOT_UUID = 'ca86a56a-40d8-4210-974c-ef15dbf01cba'
SNAPSHOT_METADATA = {
'metadata': {
'ds_snapshot_UUID': DS_SNAPSHOT_UUID
}
}
INITIATOR_IQN = 'iqn.1993-08.org.debian:01:604af6a341'
CONNECTOR = {
'initiator': INITIATOR_IQN,
}
CONTEXT = {
}
LOCAL_PATH = '/dev/isda'
IMAGE_SERVICE = 'image_service'
IMAGE_ID = 1
IMAGE_META = {
'id': IMAGE_ID
}
NODE_UUID = '72003c93-2db2-4f00-a169-67c5eae86bb1'
HOST = {
}
class SynoISCSIDriverTestCase(test.TestCase):
@mock.patch.object(common.SynoCommon,
'_get_node_uuid',
return_value=NODE_UUID)
@mock.patch.object(common, 'APIRequest')
def setUp(self, _request, _get_node_uuid):
super(SynoISCSIDriverTestCase, self).setUp()
self.conf = self.setup_configuration()
self.driver = synology_iscsi.SynoISCSIDriver(configuration=self.conf)
self.driver.common = common.SynoCommon(self.conf, 'iscsi')
def tearDown(self):
super(SynoISCSIDriverTestCase, self).tearDown()
def setup_configuration(self):
config = mock.Mock(spec=conf.Configuration)
config.use_chap_auth = False
config.iscsi_protocol = 'iscsi'
config.iscsi_ip_address = IP
config.admin_port = 5000
config.username = 'admin'
config.password = 'admin'
config.ssl_verify = True
config.one_time_pass = '123456'
config.volume_dd_blocksize = 1
return config
def test_check_for_setup_error(self):
self.driver.common.check_for_setup_error = mock.Mock()
result = self.driver.check_for_setup_error()
self.driver.common.check_for_setup_error.assert_called_with()
self.assertIsNone(result)
def test_create_volume(self):
self.driver.common.create_volume = mock.Mock()
result = self.driver.create_volume(VOLUME)
self.driver.common.create_volume.assert_called_with(VOLUME)
self.assertIsNone(result)
def test_delete_volume(self):
self.driver.common.delete_volume = mock.Mock()
result = self.driver.delete_volume(VOLUME)
self.driver.common.delete_volume.assert_called_with(VOLUME)
self.assertIsNone(result)
def test_create_cloned_volume(self):
self.driver.common.create_cloned_volume = mock.Mock()
result = self.driver.create_cloned_volume(VOLUME, NEW_VOLUME)
self.driver.common.create_cloned_volume.assert_called_with(
VOLUME, NEW_VOLUME)
self.assertIsNone(result)
def test_extend_volume(self):
new_size = 20
self.driver.common.extend_volume = mock.Mock()
result = self.driver.extend_volume(VOLUME, new_size)
self.driver.common.extend_volume.assert_called_with(
VOLUME, new_size)
self.assertIsNone(result)
def test_extend_volume_wrong_size(self):
wrong_new_size = 1
self.driver.common.extend_volume = mock.Mock()
result = self.driver.extend_volume(VOLUME, wrong_new_size)
self.driver.common.extend_volume.assert_not_called()
self.assertIsNone(result)
def test_create_volume_from_snapshot(self):
self.driver.common.create_volume_from_snapshot = mock.Mock()
result = self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
(self.driver.common.
create_volume_from_snapshot.assert_called_with(VOLUME, SNAPSHOT))
self.assertIsNone(result)
def test_update_migrated_volume(self):
fake_ret = {'_name_id': VOLUME['id']}
status = ''
self.driver.common.update_migrated_volume = (
mock.Mock(return_value=fake_ret))
result = self.driver.update_migrated_volume(CONTEXT,
VOLUME,
NEW_VOLUME,
status)
(self.driver.common.update_migrated_volume.
assert_called_with(VOLUME, NEW_VOLUME))
self.assertEqual(fake_ret, result)
def test_create_snapshot(self):
self.driver.common.create_snapshot = (
mock.Mock(return_value=SNAPSHOT_METADATA))
result = self.driver.create_snapshot(SNAPSHOT)
self.driver.common.create_snapshot.assert_called_with(SNAPSHOT)
self.assertDictMatch(SNAPSHOT_METADATA, result)
def test_delete_snapshot(self):
self.driver.common.delete_snapshot = mock.Mock()
result = self.driver.delete_snapshot(SNAPSHOT)
self.driver.common.delete_snapshot.assert_called_with(SNAPSHOT)
self.assertIsNone(result)
def test_get_volume_stats(self):
self.driver.common.update_volume_stats = mock.MagicMock()
result = self.driver.get_volume_stats(True)
self.driver.common.update_volume_stats.assert_called_with()
self.assertDictMatch(self.driver.stats, result)
result = self.driver.get_volume_stats(False)
self.driver.common.update_volume_stats.assert_called_with()
self.assertDictMatch(self.driver.stats, result)
def test_get_volume_stats_error(self):
self.driver.common.update_volume_stats = (
mock.MagicMock(side_effect=exception.VolumeDriverException(
message='dont care')))
self.assertRaises(exception.VolumeDriverException,
self.driver.get_volume_stats,
True)
def test_create_export(self):
provider_auth = 'CHAP username password'
provider_location = '%s:3260,%d %s 1' % (IP, TRG_ID, IQN)
self.driver.common.is_lun_mapped = mock.Mock(return_value=False)
self.driver.common.create_iscsi_export = (
mock.Mock(return_value=(IQN, TRG_ID, provider_auth)))
self.driver.common.get_provider_location = (
mock.Mock(return_value=provider_location))
result = self.driver.create_export(CONTEXT, VOLUME, CONNECTOR)
self.driver.common.is_lun_mapped.assert_called_with(VOLUME['name'])
(self.driver.common.create_iscsi_export.
assert_called_with(VOLUME['name'], VOLUME['id']))
self.driver.common.get_provider_location.assert_called_with(IQN,
TRG_ID)
self.assertEqual(provider_location, result['provider_location'])
self.assertEqual(provider_auth, result['provider_auth'])
def test_create_export_is_mapped(self):
self.driver.common.is_lun_mapped = mock.Mock(return_value=True)
self.driver.common.create_iscsi_export = mock.Mock()
self.driver.common.get_provider_location = mock.Mock()
result = self.driver.create_export(CONTEXT, VOLUME, CONNECTOR)
self.driver.common.is_lun_mapped.assert_called_with(VOLUME['name'])
self.driver.common.create_iscsi_export.assert_not_called()
self.driver.common.get_provider_location.assert_not_called()
self.assertEqual({}, result)
def test_create_export_error(self):
provider_location = '%s:3260,%d %s 1' % (IP, TRG_ID, IQN)
self.driver.common.is_lun_mapped = mock.Mock(return_value=False)
self.driver.common.create_iscsi_export = (
mock.Mock(side_effect=exception.InvalidInput(reason='dont care')))
self.driver.common.get_provider_location = (
mock.Mock(return_value=provider_location))
self.assertRaises(exception.ExportFailure,
self.driver.create_export,
CONTEXT,
VOLUME,
CONNECTOR)
self.driver.common.is_lun_mapped.assert_called_with(VOLUME['name'])
self.driver.common.get_provider_location.assert_not_called()
def test_remove_export(self):
self.driver.common.is_lun_mapped = mock.Mock(return_value=True)
self.driver.common.remove_iscsi_export = mock.Mock()
self.driver.common.get_iqn_and_trgid = (
mock.Mock(return_value=('', TRG_ID)))
_, trg_id = (self.driver.common.
get_iqn_and_trgid(VOLUME['provider_location']))
result = self.driver.remove_export(CONTEXT, VOLUME)
self.driver.common.is_lun_mapped.assert_called_with(VOLUME['name'])
(self.driver.common.get_iqn_and_trgid.
assert_called_with(VOLUME['provider_location']))
(self.driver.common.remove_iscsi_export.
assert_called_with(VOLUME['name'], trg_id))
self.assertIsNone(result)
def test_remove_export_not_mapped(self):
self.driver.common.is_lun_mapped = mock.Mock(return_value=False)
self.driver.common.remove_iscsi_export = mock.Mock()
self.driver.common.get_iqn_and_trgid = mock.Mock()
result = self.driver.remove_export(CONTEXT, VOLUME)
self.driver.common.is_lun_mapped.assert_called_with(VOLUME['name'])
self.driver.common.get_iqn_and_trgid.assert_not_called()
self.driver.common.remove_iscsi_export.assert_not_called()
self.assertIsNone(result)
def test_remove_export_error(self):
self.driver.common.is_lun_mapped = mock.Mock(return_value=True)
self.driver.common.remove_iscsi_export = (
mock.Mock(side_effect= exception.RemoveExportException(
volume=VOLUME, reason='dont care')))
self.assertRaises(exception.RemoveExportException,
self.driver.remove_export,
CONTEXT,
VOLUME)
def test_remove_export_error_get_lun_mapped(self):
self.driver.common.remove_iscsi_export = mock.Mock()
self.driver.common.get_iqn_and_trgid = mock.Mock()
self.driver.common.is_lun_mapped = (
mock.Mock(side_effect=exception.SynoLUNNotExist(
message='dont care')))
result = self.driver.remove_export(CONTEXT, VOLUME)
self.assertIsNone(result)
self.driver.common.get_iqn_and_trgid.assert_not_called()
self.driver.common.remove_iscsi_export.assert_not_called()
def test_initialize_connection(self):
iscsi_properties = {
'target_discovered': False,
'target_iqn': IQN,
'target_portal': '%s:3260' % self.conf.iscsi_ip_address,
'volume_id': VOLUME['id'],
'access_mode': 'rw',
'discard': False
}
self.driver.common.get_iscsi_properties = (
mock.Mock(return_value=iscsi_properties))
self.conf.safe_get = mock.Mock(return_value='iscsi')
result = self.driver.initialize_connection(VOLUME, CONNECTOR)
self.driver.common.get_iscsi_properties.assert_called_with(VOLUME)
self.conf.safe_get.assert_called_with('iscsi_protocol')
self.assertEqual('iscsi', result['driver_volume_type'])
self.assertDictMatch(iscsi_properties, result['data'])
def test_initialize_connection_error(self):
self.driver.common.get_iscsi_properties = (
mock.Mock(side_effect=exception.InvalidInput(reason='dont care')))
self.assertRaises(exception.InvalidInput,
self.driver.initialize_connection,
VOLUME,
CONNECTOR)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
# Copyright (c) 2016 Synology Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_utils import excutils
from cinder import exception
from cinder import interface
from cinder.i18n import _LE, _LW
from cinder.volume import driver
from cinder.volume.drivers.synology import synology_common as common
LOG = logging.getLogger(__name__)
@interface.volumedriver
class SynoISCSIDriver(driver.ISCSIDriver):
"""Openstack Cinder drivers for Synology storage.
Version history:
1.0.0 - Initial driver. Provide Cinder minimum features
"""
VERSION = '1.0.0'
def __init__(self, *args, **kwargs):
super(SynoISCSIDriver, self).__init__(*args, **kwargs)
self.common = None
self.configuration.append_config_values(common.cinder_opts)
self.stats = {}
def do_setup(self, context):
self.common = common.SynoCommon(self.configuration, 'iscsi')
def check_for_setup_error(self):
self.common.check_for_setup_error()
def create_volume(self, volume):
"""Creates a logical volume."""
self.common.create_volume(volume)
def delete_volume(self, volume):
"""Deletes a logical volume."""
self.common.delete_volume(volume)
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
self.common.create_cloned_volume(volume, src_vref)
def extend_volume(self, volume, new_size):
"""Extend an existing volume's size."""
if volume['size'] >= new_size:
LOG.error(_LE('New size is smaller than original size. '
'New: [%(new)d] Old: [%(old)d]'),
{'new': new_size,
'old': volume['size']})
return
self.common.extend_volume(volume, new_size)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
self.common.create_volume_from_snapshot(volume, snapshot)
def update_migrated_volume(self, ctxt, volume, new_volume, status):
"""Return model update for migrated volume."""
return self.common.update_migrated_volume(volume, new_volume)
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
return self.common.create_snapshot(snapshot)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
self.common.delete_snapshot(snapshot)
def get_volume_stats(self, refresh=False):
"""Get volume status.
If 'refresh' is True, run update the stats first.
"""
try:
if refresh or not self.stats:
self.stats = self.common.update_volume_stats()
self.stats['driver_version'] = self.VERSION
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to get_volume_stats.'))
return self.stats
def ensure_export(self, context, volume):
pass
def create_export(self, context, volume, connector):
model_update = {}
try:
if self.common.is_lun_mapped(volume['name']):
return model_update
iqn, trg_id, provider_auth = (self.common.create_iscsi_export
(volume['name'], volume['id']))
except Exception as e:
LOG.exception(_LE('Failed to remove_export.'))
raise exception.ExportFailure(reason=e)
model_update['provider_location'] = (self.common.get_provider_location
(iqn, trg_id))
model_update['provider_auth'] = provider_auth
return model_update
def remove_export(self, context, volume):
try:
if not self.common.is_lun_mapped(volume['name']):
return
except exception.SynoLUNNotExist:
LOG.warning(_LW("Volume not exist"))
return
try:
_, trg_id = (self.common.get_iqn_and_trgid
(volume['provider_location']))
self.common.remove_iscsi_export(volume['name'], trg_id)
except Exception as e:
LOG.exception(_LE('Failed to remove_export.'))
raise exception.RemoveExportException(volume=volume,
reason=e.msg)
def initialize_connection(self, volume, connector):
LOG.debug('iSCSI initiator: %s', connector['initiator'])
try:
iscsi_properties = self.common.get_iscsi_properties(volume)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to initialize_connection.'))
volume_type = self.configuration.safe_get('iscsi_protocol') or 'iscsi'
return {
'driver_volume_type': volume_type,
'data': iscsi_properties
}
def terminate_connection(self, volume, connector, **kwargs):
pass

View File

@ -0,0 +1,3 @@
---
features:
- Added backend driver for Synology iSCSI-supported storage.