Merge "Revise Synology DSM storage driver"

This commit is contained in:
Jenkins
2016-08-18 03:35:09 +00:00
committed by Gerrit Code Review
3 changed files with 124 additions and 21 deletions

View File

@@ -1285,7 +1285,7 @@ class SynoAPIHTTPError(CinderException):
class SynoAuthError(CinderException): class SynoAuthError(CinderException):
pass message = _("Synology driver authentication failed: %(reason)s.")
class SynoLUNNotExist(CinderException): class SynoLUNNotExist(CinderException):

View File

@@ -16,6 +16,7 @@
"""Tests for the Synology iSCSI volume driver.""" """Tests for the Synology iSCSI volume driver."""
import copy import copy
import math
import mock import mock
from oslo_utils import units from oslo_utils import units
@@ -109,6 +110,7 @@ POOL_INFO = {
'readonly': False, 'readonly': False,
'fs_type': 'ext4', 'fs_type': 'ext4',
'location': 'internal', 'location': 'internal',
'eppool_used_byte': '139177984',
'size_total_byte': '487262806016', 'size_total_byte': '487262806016',
'volume_id': 1, 'volume_id': 1,
'size_free_byte': '486521139200', 'size_free_byte': '486521139200',
@@ -367,6 +369,8 @@ class SynoCommonTestCase(test.TestCase):
config.pool_name = POOL_NAME config.pool_name = POOL_NAME
config.chap_username = 'abcd' config.chap_username = 'abcd'
config.chap_password = 'qwerty' config.chap_password = 'qwerty'
config.reserved_percentage = 0
config.max_over_subscription_ratio = 20
return config return config
@@ -457,13 +461,58 @@ class SynoCommonTestCase(test.TestCase):
result = self.common._get_pool_size() result = self.common._get_pool_size()
self.assertEqual((int(int(POOL_INFO['size_free_byte']) / units.Gi), self.assertEqual((int(int(POOL_INFO['size_free_byte']) / units.Gi),
int(int(POOL_INFO['size_total_byte']) / units.Gi)), int(int(POOL_INFO['size_total_byte']) / units.Gi),
math.ceil((float(POOL_INFO['size_total_byte']) -
float(POOL_INFO['size_free_byte']) -
float(POOL_INFO['eppool_used_byte'])) /
units.Gi)),
result) result)
del pool_info['size_free_byte'] del pool_info['size_free_byte']
self.assertRaises(exception.MalformedResponse, self.assertRaises(exception.MalformedResponse,
self.common._get_pool_size) self.common._get_pool_size)
def test__get_pool_lun_provisioned_size(self):
out = {
'data': {
'luns': [{
'lun_id': 1,
'location': '/' + POOL_NAME,
'size': 5368709120
}, {
'lun_id': 2,
'location': '/' + POOL_NAME,
'size': 3221225472
}]
},
'success': True
}
self.common.exec_webapi = mock.Mock(return_value=out)
result = self.common._get_pool_lun_provisioned_size()
(self.common.exec_webapi.
assert_called_with('SYNO.Core.ISCSI.LUN',
'list',
mock.ANY,
location='/' + POOL_NAME))
self.assertEqual(int(math.ceil(float(5368709120 + 3221225472) /
units.Gi)),
result)
def test__get_pool_lun_provisioned_size_error(self):
out = {
'data': {},
'success': True
}
self.common.exec_webapi = mock.Mock(return_value=out)
self.assertRaises(exception.MalformedResponse,
self.common._get_pool_lun_provisioned_size)
self.conf.pool_name = ''
self.assertRaises(exception.InvalidConfigurationValue,
self.common._get_pool_lun_provisioned_size)
def test__get_lun_info(self): def test__get_lun_info(self):
out = { out = {
'data': { 'data': {
@@ -847,7 +896,7 @@ class SynoCommonTestCase(test.TestCase):
VOLUME['name'], VOLUME['name'],
NEW_VOLUME['name']) NEW_VOLUME['name'])
@mock.patch('time.sleep') @mock.patch('eventlet.sleep')
def test__check_lun_status_normal(self, _patched_sleep): def test__check_lun_status_normal(self, _patched_sleep):
self.common._get_lun_status = ( self.common._get_lun_status = (
mock.Mock(side_effect=[ mock.Mock(side_effect=[
@@ -869,7 +918,7 @@ class SynoCommonTestCase(test.TestCase):
self.common._check_lun_status_normal, self.common._check_lun_status_normal,
VOLUME['name']) VOLUME['name'])
@mock.patch('time.sleep') @mock.patch('eventlet.sleep')
def test__check_snapshot_status_healthy(self, _patched_sleep): def test__check_snapshot_status_healthy(self, _patched_sleep):
self.common._get_snapshot_status = ( self.common._get_snapshot_status = (
mock.Mock(side_effect=[ mock.Mock(side_effect=[
@@ -1180,7 +1229,9 @@ class SynoCommonTestCase(test.TestCase):
self.assertIsNone(result) self.assertIsNone(result)
def test_update_volume_stats(self): def test_update_volume_stats(self):
self.common._get_pool_size = mock.Mock(return_value=(10, 100)) self.common._get_pool_size = mock.Mock(return_value=(10, 100, 50))
self.common._get_pool_lun_provisioned_size = (
mock.Mock(return_value=300))
data = { data = {
'volume_backend_name': 'DiskStation', 'volume_backend_name': 'DiskStation',
@@ -1193,6 +1244,8 @@ class SynoCommonTestCase(test.TestCase):
'reserved_percentage': 0, 'reserved_percentage': 0,
'free_capacity_gb': 10, 'free_capacity_gb': 10,
'total_capacity_gb': 100, 'total_capacity_gb': 100,
'provisioned_capacity_gb': 350,
'max_over_subscription_ratio': 20,
'iscsi_ip_address': '10.0.0.1', 'iscsi_ip_address': '10.0.0.1',
'pool_name': 'volume1', 'pool_name': 'volume1',
'backend_info': 'backend_info':
@@ -1400,7 +1453,7 @@ class SynoCommonTestCase(test.TestCase):
SNAPSHOT) SNAPSHOT)
self.common.exec_webapi = ( self.common.exec_webapi = (
mock.Mock(side_effect=exception.SynoAuthError)) mock.Mock(side_effect=exception.SynoAuthError(reason='dont care')))
self.assertRaises(exception.SynoAuthError, self.assertRaises(exception.SynoAuthError,
self.common.create_snapshot, self.common.create_snapshot,

View File

@@ -17,14 +17,15 @@ import base64
import functools import functools
from hashlib import md5 from hashlib import md5
import json import json
import math
from random import randint from random import randint
import string import string
import time
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_v1_5 from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto import Random from Crypto import Random
import eventlet
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import excutils from oslo_utils import excutils
@@ -162,7 +163,7 @@ class Session(object):
if one_time_pass and not device_id: if one_time_pass and not device_id:
self._did = result['data']['did'] self._did = result['data']['did']
else: else:
raise exception.SynoAuthError(_('Login failed.')) raise exception.SynoAuthError(reason=_('Login failed.'))
def _random_AES_passpharse(self, length): def _random_AES_passpharse(self, length):
available = ('0123456789' available = ('0123456789'
@@ -369,7 +370,8 @@ class APIRequest(object):
if ('error' in result and 'code' in result["error"] if ('error' in result and 'code' in result["error"]
and result['error']['code'] == 105): and result['error']['code'] == 105):
raise exception.SynoAuthError(_('Session might have expired.')) raise exception.SynoAuthError(reason=_('Session might have '
'expired.'))
return result return result
@@ -462,8 +464,39 @@ class SynoCommon(object):
free_capacity_gb = int(int(info['size_free_byte']) / units.Gi) free_capacity_gb = int(int(info['size_free_byte']) / units.Gi)
total_capacity_gb = int(int(info['size_total_byte']) / units.Gi) total_capacity_gb = int(int(info['size_total_byte']) / units.Gi)
other_user_data_gb = int(math.ceil((float(info['size_total_byte']) -
float(info['size_free_byte']) -
float(info['eppool_used_byte'])) /
units.Gi))
return free_capacity_gb, total_capacity_gb return free_capacity_gb, total_capacity_gb, other_user_data_gb
def _get_pool_lun_provisioned_size(self):
pool_name = self.config.pool_name
if not pool_name:
raise exception.InvalidConfigurationValue(option='pool_name',
value=pool_name)
try:
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'list',
1,
location='/' + pool_name)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to _get_pool_lun_provisioned_size.'))
if not self.check_value_valid(out, ['data', 'luns'], list):
raise exception.MalformedResponse(
cmd='_get_pool_lun_provisioned_size',
reason=_('no data found'))
size = 0
for lun in out['data']['luns']:
size += lun['size']
return int(math.ceil(float(size) / units.Gi))
def _get_lun_info(self, lun_name, additional=None): def _get_lun_info(self, lun_name, additional=None):
if not lun_name: if not lun_name:
@@ -503,7 +536,7 @@ class SynoCommon(object):
LOG.exception(_LE('Failed to _get_lun_uuid. [%s]'), lun_name) LOG.exception(_LE('Failed to _get_lun_uuid. [%s]'), lun_name)
if not self.check_value_valid(lun_info, ['uuid'], string_types): if not self.check_value_valid(lun_info, ['uuid'], string_types):
raise exception.MalformedResponse(cmd='_get_lun_info', raise exception.MalformedResponse(cmd='_get_lun_uuid',
reason=_('uuid not found')) reason=_('uuid not found'))
return lun_info['uuid'] return lun_info['uuid']
@@ -719,7 +752,7 @@ class SynoCommon(object):
status, locked = self._get_lun_status(volume_name) status, locked = self._get_lun_status(volume_name)
if not locked: if not locked:
break break
time.sleep(2) eventlet.sleep(2)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to get lun status. [%s]'), LOG.exception(_LE('Failed to get lun status. [%s]'),
@@ -737,7 +770,7 @@ class SynoCommon(object):
status, locked = self._get_snapshot_status(snapshot_uuid) status, locked = self._get_snapshot_status(snapshot_uuid)
if not locked: if not locked:
break break
time.sleep(2) eventlet.sleep(2)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to get snapshot status. [%s]'), LOG.exception(_LE('Failed to get snapshot status. [%s]'),
@@ -760,7 +793,7 @@ class SynoCommon(object):
LUN_NO_SUCH_SNAPSHOT = 18990532 LUN_NO_SUCH_SNAPSHOT = 18990532
if not self.check_value_valid(out, ['error', 'code'], int): if not self.check_value_valid(out, ['error', 'code'], int):
raise exception.MalformedResponse(cmd='exec_webapi', raise exception.MalformedResponse(cmd='_check_iscsi_response',
reason=_('no error code found')) reason=_('no error code found'))
code = out['error']['code'] code = out['error']['code']
@@ -785,7 +818,7 @@ class SynoCommon(object):
def _check_ds_pool_status(self): def _check_ds_pool_status(self):
pool_info = self._get_pool_info() pool_info = self._get_pool_info()
if not self.check_value_valid(pool_info, ['readonly'], bool): if not self.check_value_valid(pool_info, ['readonly'], bool):
raise exception.MalformedResponse(cmd='check_for_setup_error', raise exception.MalformedResponse(cmd='_check_ds_pool_status',
reason=_('no readonly found')) reason=_('no readonly found'))
if pool_info['readonly']: if pool_info['readonly']:
@@ -955,9 +988,22 @@ class SynoCommon(object):
self._check_ds_ability() self._check_ds_ability()
def update_volume_stats(self): def update_volume_stats(self):
"""Update volume statistics.""" """Update volume statistics.
free_capacity_gb, total_capacity_gb = self._get_pool_size() Three kinds of data are stored on the Synology backend pool:
1. Thin volumes (LUNs on the pool),
2. Thick volumes (LUNs on the pool),
3. Other user data.
other_user_data_gb is the size of the 3rd one.
lun_provisioned_gb is the summation of all thin/thick volume
provisioned size.
Only thin type is available for Cinder volumes.
"""
free_gb, total_gb, other_user_data_gb = self._get_pool_size()
lun_provisioned_gb = self._get_pool_lun_provisioned_size()
data = {} data = {}
data['volume_backend_name'] = self.volume_backend_name data['volume_backend_name'] = self.volume_backend_name
@@ -967,10 +1013,14 @@ class SynoCommon(object):
data['QoS_support'] = False data['QoS_support'] = False
data['thin_provisioning_support'] = True data['thin_provisioning_support'] = True
data['thick_provisioning_support'] = False data['thick_provisioning_support'] = False
data['reserved_percentage'] = 0 data['reserved_percentage'] = self.config.reserved_percentage
data['free_capacity_gb'] = free_capacity_gb data['free_capacity_gb'] = free_gb
data['total_capacity_gb'] = total_capacity_gb data['total_capacity_gb'] = total_gb
data['provisioned_capacity_gb'] = (lun_provisioned_gb +
other_user_data_gb)
data['max_over_subscription_ratio'] = (self.config.
max_over_subscription_ratio)
data['iscsi_ip_address'] = self.config.iscsi_ip_address data['iscsi_ip_address'] = self.config.iscsi_ip_address
data['pool_name'] = self.config.pool_name data['pool_name'] = self.config.pool_name
@@ -1091,7 +1141,7 @@ class SynoCommon(object):
if not self.check_value_valid(resp, if not self.check_value_valid(resp,
['data', 'snapshot_uuid'], ['data', 'snapshot_uuid'],
string_types): string_types):
raise exception.MalformedResponse(cmd='take_snapshot', raise exception.MalformedResponse(cmd='create_snapshot',
reason=_('uuid not found')) reason=_('uuid not found'))
snapshot_uuid = resp['data']['snapshot_uuid'] snapshot_uuid = resp['data']['snapshot_uuid']