Revise Synology DSM storage driver

This patch addressed last comments from
https://review.openstack.org/#/c/335870/, including:
1. Add an exception message for SynoAuthError
2. Change time.sleep to eventlet.sleep
3. Report reserved_percentage from cinder.conf
4. Report provisioned_capacity_gb and max_over_subscription_ratio in
update_volume_stats

Closes-Bug: #1602942
Co-Authored-By: Taylor Huang<taylorh@synology.com>

Change-Id: I0ab0b8ab651830412bad87a0fe5d9ac8a601aacb
This commit is contained in:
arsenc 2016-07-13 15:53:37 +08:00
parent 15d2229d72
commit 5564f2277d
3 changed files with 124 additions and 21 deletions

View File

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

View File

@ -16,6 +16,7 @@
"""Tests for the Synology iSCSI volume driver."""
import copy
import math
import mock
from oslo_utils import units
@ -109,6 +110,7 @@ POOL_INFO = {
'readonly': False,
'fs_type': 'ext4',
'location': 'internal',
'eppool_used_byte': '139177984',
'size_total_byte': '487262806016',
'volume_id': 1,
'size_free_byte': '486521139200',
@ -367,6 +369,8 @@ class SynoCommonTestCase(test.TestCase):
config.pool_name = POOL_NAME
config.chap_username = 'abcd'
config.chap_password = 'qwerty'
config.reserved_percentage = 0
config.max_over_subscription_ratio = 20
return config
@ -457,13 +461,58 @@ class SynoCommonTestCase(test.TestCase):
result = self.common._get_pool_size()
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)
del pool_info['size_free_byte']
self.assertRaises(exception.MalformedResponse,
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):
out = {
'data': {
@ -847,7 +896,7 @@ class SynoCommonTestCase(test.TestCase):
VOLUME['name'],
NEW_VOLUME['name'])
@mock.patch('time.sleep')
@mock.patch('eventlet.sleep')
def test__check_lun_status_normal(self, _patched_sleep):
self.common._get_lun_status = (
mock.Mock(side_effect=[
@ -869,7 +918,7 @@ class SynoCommonTestCase(test.TestCase):
self.common._check_lun_status_normal,
VOLUME['name'])
@mock.patch('time.sleep')
@mock.patch('eventlet.sleep')
def test__check_snapshot_status_healthy(self, _patched_sleep):
self.common._get_snapshot_status = (
mock.Mock(side_effect=[
@ -1180,7 +1229,9 @@ class SynoCommonTestCase(test.TestCase):
self.assertIsNone(result)
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 = {
'volume_backend_name': 'DiskStation',
@ -1193,6 +1244,8 @@ class SynoCommonTestCase(test.TestCase):
'reserved_percentage': 0,
'free_capacity_gb': 10,
'total_capacity_gb': 100,
'provisioned_capacity_gb': 350,
'max_over_subscription_ratio': 20,
'iscsi_ip_address': '10.0.0.1',
'pool_name': 'volume1',
'backend_info':
@ -1400,7 +1453,7 @@ class SynoCommonTestCase(test.TestCase):
SNAPSHOT)
self.common.exec_webapi = (
mock.Mock(side_effect=exception.SynoAuthError))
mock.Mock(side_effect=exception.SynoAuthError(reason='dont care')))
self.assertRaises(exception.SynoAuthError,
self.common.create_snapshot,

View File

@ -17,14 +17,15 @@ import base64
import functools
from hashlib import md5
import json
import math
from random import randint
import string
import time
from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto import Random
import eventlet
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
@ -162,7 +163,7 @@ class Session(object):
if one_time_pass and not device_id:
self._did = result['data']['did']
else:
raise exception.SynoAuthError(_('Login failed.'))
raise exception.SynoAuthError(reason=_('Login failed.'))
def _random_AES_passpharse(self, length):
available = ('0123456789'
@ -369,7 +370,8 @@ class APIRequest(object):
if ('error' in result and 'code' in result["error"]
and result['error']['code'] == 105):
raise exception.SynoAuthError(_('Session might have expired.'))
raise exception.SynoAuthError(reason=_('Session might have '
'expired.'))
return result
@ -462,8 +464,39 @@ class SynoCommon(object):
free_capacity_gb = int(int(info['size_free_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):
if not lun_name:
@ -503,7 +536,7 @@ class SynoCommon(object):
LOG.exception(_LE('Failed to _get_lun_uuid. [%s]'), lun_name)
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'))
return lun_info['uuid']
@ -719,7 +752,7 @@ class SynoCommon(object):
status, locked = self._get_lun_status(volume_name)
if not locked:
break
time.sleep(2)
eventlet.sleep(2)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to get lun status. [%s]'),
@ -737,7 +770,7 @@ class SynoCommon(object):
status, locked = self._get_snapshot_status(snapshot_uuid)
if not locked:
break
time.sleep(2)
eventlet.sleep(2)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to get snapshot status. [%s]'),
@ -760,7 +793,7 @@ class SynoCommon(object):
LUN_NO_SUCH_SNAPSHOT = 18990532
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'))
code = out['error']['code']
@ -785,7 +818,7 @@ class SynoCommon(object):
def _check_ds_pool_status(self):
pool_info = self._get_pool_info()
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'))
if pool_info['readonly']:
@ -955,9 +988,22 @@ class SynoCommon(object):
self._check_ds_ability()
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['volume_backend_name'] = self.volume_backend_name
@ -967,10 +1013,14 @@ class SynoCommon(object):
data['QoS_support'] = False
data['thin_provisioning_support'] = True
data['thick_provisioning_support'] = False
data['reserved_percentage'] = 0
data['reserved_percentage'] = self.config.reserved_percentage
data['free_capacity_gb'] = free_capacity_gb
data['total_capacity_gb'] = total_capacity_gb
data['free_capacity_gb'] = free_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['pool_name'] = self.config.pool_name
@ -1091,7 +1141,7 @@ class SynoCommon(object):
if not self.check_value_valid(resp,
['data', 'snapshot_uuid'],
string_types):
raise exception.MalformedResponse(cmd='take_snapshot',
raise exception.MalformedResponse(cmd='create_snapshot',
reason=_('uuid not found'))
snapshot_uuid = resp['data']['snapshot_uuid']