cinder/cinder/volume/drivers/synology/synology_common.py

1328 lines
49 KiB
Python

# 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.
import base64
import functools
import hashlib
import json
import math
from os import urandom
from random import randint
import re
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import modes
import eventlet
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import units
import requests
from six.moves import urllib
from six import string_types
from cinder import exception
from cinder.i18n import _
from cinder.objects import snapshot
from cinder.objects import volume
from cinder import utils
from cinder.volume import configuration
from cinder.volume import volume_utils
cinder_opts = [
cfg.StrOpt('synology_pool_name',
default='',
help='Volume on Synology storage to be used for creating lun.'),
cfg.PortOpt('synology_admin_port',
default=5000,
help='Management port for Synology storage.'),
cfg.StrOpt('synology_username',
default='admin',
help='Administrator of Synology storage.'),
cfg.StrOpt('synology_password',
default='',
help='Password of administrator for logging in '
'Synology storage.',
secret=True),
cfg.BoolOpt('synology_ssl_verify',
default=True,
help='Do certificate validation or not if '
'$driver_use_ssl is True'),
cfg.StrOpt('synology_one_time_pass',
default=None,
help='One time password of administrator for logging in '
'Synology storage if OTP is enabled.',
secret=True),
cfg.StrOpt('synology_device_id',
default=None,
help='Device id for skip one time password check for '
'logging in Synology storage if OTP is enabled.'),
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(cinder_opts, group=configuration.SHARED_CONF_GROUP)
class SynoAPIHTTPError(exception.VolumeDriverException):
message = _("HTTP exit code: [%(code)s]")
class SynoAuthError(exception.VolumeDriverException):
message = _("Synology driver authentication failed: %(reason)s.")
class SynoLUNNotExist(exception.VolumeDriverException):
message = _("LUN not found by UUID: %(uuid)s.")
class AESCipher(object):
"""Encrypt with OpenSSL-compatible way"""
SALT_MAGIC = b'Salted__'
def __init__(self, password, key_length=32):
self._bs = 16
self._salt = urandom(self._bs - len(self.SALT_MAGIC))
self._key, self._iv = self._derive_key_and_iv(password,
self._salt,
key_length,
self._bs)
def _pad(self, s):
bs = self._bs
return (s + (bs - len(s) % bs) * chr(bs - len(s) % bs)).encode('utf-8')
def _derive_key_and_iv(self, password, salt, key_length, iv_length):
d = d_i = b''
while len(d) < key_length + iv_length:
md5_str = d_i + password + salt
d_i = hashlib.md5(md5_str).digest()
d += d_i
return d[:key_length], d[key_length:key_length + iv_length]
def encrypt(self, text):
cipher = Cipher(
algorithms.AES(self._key),
modes.CBC(self._iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(self._pad(text)) + encryptor.finalize()
return self.SALT_MAGIC + self._salt + ciphertext
class Session(object):
def __init__(self,
host,
port,
username,
password,
https=False,
ssl_verify=True,
one_time_pass=None,
device_id=None):
self._proto = 'https' if https else 'http'
self._host = host
self._port = port
self._sess = 'dsm'
self._https = https
self._url_prefix = self._proto + '://' + host + ':' + str(port)
self._url = self._url_prefix + '/webapi/auth.cgi'
self._ssl_verify = ssl_verify
self._sid = None
self._did = device_id
data = {'api': 'SYNO.API.Auth',
'method': 'login',
'version': 6}
params = {'account': username,
'passwd': password,
'session': self._sess,
'format': 'sid'}
if one_time_pass:
if device_id:
params.update(device_id=device_id)
else:
params.update(otp_code=one_time_pass,
enable_device_token='yes')
if not https:
params = self._encrypt_params(params)
data.update(params)
resp = requests.post(self._url,
data=data,
verify=self._ssl_verify)
result = resp.json()
if result and result['success']:
self._sid = result['data']['sid']
if one_time_pass and not device_id:
self._did = result['data']['did']
else:
raise SynoAuthError(reason=_('Login failed.'))
def _random_AES_passphrase(self, length):
available = ('0123456789'
'abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'~!@#$%^&*()_+-/')
key = b''
while length > 0:
key += available[randint(0, len(available) - 1)].encode('utf-8')
length -= 1
return key
def _get_enc_info(self):
url = self.url_prefix() + '/webapi/encryption.cgi'
data = {"api": "SYNO.API.Encryption",
"method": "getinfo",
"version": 1,
"format": "module"}
resp = requests.post(url, data=data, verify=self._ssl_verify)
result = resp.json()
return result["data"]
def _encrypt_RSA(self, modulus, passphrase, text):
public_numbers = rsa.RSAPublicNumbers(passphrase, modulus)
public_key = public_numbers.public_key(default_backend())
if isinstance(text, str):
text = text.encode('utf-8')
ciphertext = public_key.encrypt(
text,
padding.PKCS1v15()
)
return ciphertext
def _encrypt_AES(self, passphrase, text):
cipher = AESCipher(passphrase)
return cipher.encrypt(text)
def _encrypt_params(self, params):
enc_info = self._get_enc_info()
public_key = enc_info["public_key"]
cipher_key = enc_info["cipherkey"]
cipher_token = enc_info["ciphertoken"]
server_time = enc_info["server_time"]
random_passphrase = self._random_AES_passphrase(501)
params[cipher_token] = server_time
encrypted_passphrase = self._encrypt_RSA(int(public_key, 16),
int("10001", 16),
random_passphrase)
encrypted_params = self._encrypt_AES(random_passphrase,
urllib.parse.urlencode(params))
enc_params = {
"rsa": base64.b64encode(encrypted_passphrase).decode("ascii"),
"aes": base64.b64encode(encrypted_params).decode("ascii")
}
return {cipher_key: json.dumps(enc_params)}
def sid(self):
return self._sid
def did(self):
return self._did
def url_prefix(self):
return self._url_prefix
def query(self, api):
url = self._url_prefix + '/webapi/query.cgi'
data = {'api': 'SYNO.API.Info',
'version': 1,
'method': 'query',
'query': api}
resp = requests.post(url,
data=data,
verify=self._ssl_verify)
result = resp.json()
if 'success' in result and result['success']:
return result['data'][api]
else:
return None
def __del__(self):
if not hasattr(self, '_sid'):
return
data = {'api': 'SYNO.API.Auth',
'version': 1,
'method': 'logout',
'session': self._sess,
'_sid': self._sid}
requests.post(self._url, data=data, verify=self._ssl_verify)
def _connection_checker(func):
"""Decorator to check session has expired or not."""
@functools.wraps(func)
def inner_connection_checker(self, *args, **kwargs):
LOG.debug('in _connection_checker')
for attempts in range(2):
try:
return func(self, *args, **kwargs)
except SynoAuthError as e:
if attempts < 1:
LOG.debug('Session might have expired.'
' Trying to relogin')
self.new_session()
continue
else:
LOG.error('Try to renew session: [%s]', e)
raise
return inner_connection_checker
class APIRequest(object):
def __init__(self,
host,
port,
username,
password,
https=False,
ssl_verify=True,
one_time_pass=None,
device_id=None):
self._host = host
self._port = port
self._username = username
self._password = password
self._https = https
self._ssl_verify = ssl_verify
self._one_time_pass = one_time_pass
self._device_id = device_id
self.new_session()
def new_session(self):
self.__session = Session(self._host,
self._port,
self._username,
self._password,
self._https,
self._ssl_verify,
self._one_time_pass,
self._device_id)
if not self._device_id:
self._device_id = self.__session.did()
def _start(self, api, version):
apiInfo = self.__session.query(api)
self._jsonFormat = apiInfo['requestFormat'] == 'JSON'
if (apiInfo and (apiInfo['minVersion'] <= version)
and (apiInfo['maxVersion'] >= version)):
return apiInfo['path']
else:
raise exception.APIException(service=api)
def _encode_param(self, params):
# Json encode
if self._jsonFormat:
for key, value in params.items():
params[key] = json.dumps(value)
# url encode
return urllib.parse.urlencode(params)
@utils.synchronized('Synology')
@_connection_checker
def request(self, api, method, version, **params):
cgi_path = self._start(api, version)
s = self.__session
url = s.url_prefix() + '/webapi/' + cgi_path
data = {'api': api,
'version': version,
'method': method,
'_sid': s.sid()
}
data.update(params)
LOG.debug('[%s]', url)
LOG.debug('%s', json.dumps(data, indent=4))
# Send HTTP Post Request
resp = requests.post(url,
data=self._encode_param(data),
verify=self._ssl_verify)
http_status = resp.status_code
result = resp.json()
LOG.debug('%s', json.dumps(result, indent=4))
# Check for status code
if (200 != http_status):
result = {'http_status': http_status}
elif 'success' not in result:
reason = _("'success' not found")
raise exception.MalformedResponse(cmd=json.dumps(data, indent=4),
reason=reason)
if ('error' in result and 'code' in result["error"]
and result['error']['code'] == 105):
raise SynoAuthError(reason=_('Session might have expired.'))
return result
class SynoCommon(object):
"""Manage Cinder volumes on Synology storage"""
TARGET_NAME_PREFIX = 'Cinder-Target-'
CINDER_LUN = 'CINDER'
METADATA_DS_SNAPSHOT_UUID = 'ds_snapshot_UUID'
def __init__(self, config, driver_type):
if not config.safe_get('target_ip_address'):
raise exception.InvalidConfigurationValue(
option='target_ip_address',
value='')
if not config.safe_get('synology_pool_name'):
raise exception.InvalidConfigurationValue(
option='synology_pool_name',
value='')
self.config = config
self.vendor_name = 'Synology'
self.driver_type = driver_type
self.volume_backend_name = self._get_backend_name()
self.target_port = self.config.safe_get('target_port')
api = APIRequest(self.config.target_ip_address,
self.config.synology_admin_port,
self.config.synology_username,
self.config.synology_password,
self.config.safe_get('driver_use_ssl'),
self.config.safe_get('synology_ssl_verify'),
self.config.safe_get('synology_one_time_pass'),
self.config.safe_get('synology_device_id'),)
self.synoexec = api.request
self.host_uuid = self._get_node_uuid()
def _get_node_uuid(self):
try:
out = self.exec_webapi('SYNO.Core.ISCSI.Node',
'list',
1)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_node_uuid.')
if (not self.check_value_valid(out, ['data', 'nodes'], list)
or 0 >= len(out['data']['nodes'])
or not self.check_value_valid(out['data']['nodes'][0],
['uuid'],
string_types)):
msg = _('Failed to _get_node_uuid.')
raise exception.VolumeDriverException(message=msg)
return out['data']['nodes'][0]['uuid']
def _get_pool_info(self):
pool_name = self.config.synology_pool_name
if not pool_name:
raise exception.InvalidConfigurationValue(option='pool_name',
value='')
try:
out = self.exec_webapi('SYNO.Core.Storage.Volume',
'get',
1,
volume_path='/' + pool_name)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_pool_status.')
if not self.check_value_valid(out, ['data', 'volume'], object):
raise exception.MalformedResponse(cmd='_get_pool_info',
reason=_('no data found'))
return out['data']['volume']
def _get_pool_size(self):
info = self._get_pool_info()
if 'size_free_byte' not in info or 'size_total_byte' not in info:
raise exception.MalformedResponse(cmd='_get_pool_size',
reason=_('size not found'))
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, other_user_data_gb
def _get_pool_lun_provisioned_size(self):
pool_name = self.config.synology_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('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:
err = _('Param [lun_name] is invalid.')
raise exception.InvalidParameterValue(err=err)
params = {'uuid': lun_name}
if additional is not None:
params['additional'] = additional
try:
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'get',
1,
**params)
self.check_response(out, uuid=lun_name)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_lun_info. [%s]', lun_name)
if not self.check_value_valid(out, ['data', 'lun'], object):
raise exception.MalformedResponse(cmd='_get_lun_info',
reason=_('lun info not found'))
return out['data']['lun']
def _get_lun_uuid(self, lun_name):
if not lun_name:
err = _('Param [lun_name] is invalid.')
raise exception.InvalidParameterValue(err=err)
try:
lun_info = self._get_lun_info(lun_name)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('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_uuid',
reason=_('uuid not found'))
return lun_info['uuid']
def _get_lun_status(self, lun_name):
if not lun_name:
err = _('Param [lun_name] is invalid.')
raise exception.InvalidParameterValue(err=err)
try:
lun_info = self._get_lun_info(lun_name,
['status', 'is_action_locked'])
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_lun_status. [%s]', lun_name)
if not self.check_value_valid(lun_info, ['status'], string_types):
raise exception.MalformedResponse(cmd='_get_lun_status',
reason=_('status not found'))
if not self.check_value_valid(lun_info, ['is_action_locked'], bool):
raise exception.MalformedResponse(cmd='_get_lun_status',
reason=_('action_locked '
'not found'))
return lun_info['status'], lun_info['is_action_locked']
def _get_snapshot_info(self, snapshot_uuid, additional=None):
if not snapshot_uuid:
err = _('Param [snapshot_uuid] is invalid.')
raise exception.InvalidParameterValue(err=err)
params = {'snapshot_uuid': snapshot_uuid}
if additional is not None:
params['additional'] = additional
try:
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'get_snapshot',
1,
**params)
self.check_response(out, snapshot_id=snapshot_uuid)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_snapshot_info. [%s]',
snapshot_uuid)
if not self.check_value_valid(out, ['data', 'snapshot'], object):
raise exception.MalformedResponse(cmd='_get_snapshot_info',
reason=_('snapshot info not '
'found'))
return out['data']['snapshot']
def _get_snapshot_status(self, snapshot_uuid):
if not snapshot_uuid:
err = _('Param [snapshot_uuid] is invalid.')
raise exception.InvalidParameterValue(err=err)
try:
snapshot_info = self._get_snapshot_info(snapshot_uuid,
['status',
'is_action_locked'])
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _get_snapshot_info. [%s]',
snapshot_uuid)
if not self.check_value_valid(snapshot_info, ['status'], string_types):
raise exception.MalformedResponse(cmd='_get_snapshot_status',
reason=_('status not found'))
if not self.check_value_valid(snapshot_info,
['is_action_locked'],
bool):
raise exception.MalformedResponse(cmd='_get_snapshot_status',
reason=_('action_locked '
'not found'))
return snapshot_info['status'], snapshot_info['is_action_locked']
def _get_metadata_value(self, obj, key):
if key not in obj['metadata']:
if isinstance(obj, volume.Volume):
raise exception.VolumeMetadataNotFound(
volume_id=obj['id'],
metadata_key=key)
elif isinstance(obj, snapshot.Snapshot):
raise exception.SnapshotMetadataNotFound(
snapshot_id=obj['id'],
metadata_key=key)
else:
raise exception.MetadataAbsent()
return obj['metadata'][key]
def _get_backend_name(self):
return self.config.safe_get('volume_backend_name') or 'Synology'
def _target_create(self, identifier):
if not identifier:
err = _('Param [identifier] is invalid.')
raise exception.InvalidParameterValue(err=err)
# 0 for no auth, 1 for single chap, 2 for mutual chap
auth_type = 0
chap_username = ''
chap_password = ''
provider_auth = ''
if self.config.safe_get('use_chap_auth') and self.config.use_chap_auth:
auth_type = 1
chap_username = (self.config.safe_get('chap_username') or
volume_utils.generate_username(12))
chap_password = (self.config.safe_get('chap_password') or
volume_utils.generate_password())
provider_auth = ' '.join(('CHAP', chap_username, chap_password))
trg_prefix = self.config.safe_get('target_prefix')
trg_name = (self.TARGET_NAME_PREFIX + '%s') % identifier
iqn = trg_prefix + trg_name
try:
out = self.exec_webapi('SYNO.Core.ISCSI.Target',
'create',
1,
name=trg_name,
iqn=iqn,
auth_type=auth_type,
user=chap_username,
password=chap_password,
max_sessions=0)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _target_create. [%s]',
identifier)
if not self.check_value_valid(out, ['data', 'target_id']):
msg = _('Failed to get target_id of target [%s]') % trg_name
raise exception.VolumeDriverException(message=msg)
trg_id = out['data']['target_id']
return iqn, trg_id, provider_auth
def _target_delete(self, trg_id):
if 0 > trg_id:
err = _('trg_id is invalid: %d.') % trg_id
raise exception.InvalidParameterValue(err=err)
try:
out = self.exec_webapi('SYNO.Core.ISCSI.Target',
'delete',
1,
target_id=('%d' % trg_id))
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _target_delete. [%d]', trg_id)
# is_map True for map, False for ummap
def _lun_map_unmap_target(self, volume_name, is_map, trg_id):
if 0 > trg_id:
err = _('trg_id is invalid: %d.') % trg_id
raise exception.InvalidParameterValue(err=err)
try:
lun_uuid = self._get_lun_uuid(volume_name)
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'map_target' if is_map else 'unmap_target',
1,
uuid=lun_uuid,
target_ids=['%d' % trg_id])
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _lun_map_unmap_target. '
'[%(action)s][%(vol)s].',
{'action': ('map_target' if is_map
else 'unmap_target'),
'vol': volume_name})
def _lun_map_target(self, volume_name, trg_id):
self._lun_map_unmap_target(volume_name, True, trg_id)
def _lun_unmap_target(self, volume_name, trg_id):
self._lun_map_unmap_target(volume_name, False, trg_id)
def _modify_lun_name(self, name, new_name):
try:
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'set',
1,
uuid=name,
new_name=new_name)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _modify_lun_name [%s].', name)
def _check_lun_status_normal(self, volume_name):
status = ''
try:
while True:
status, locked = self._get_lun_status(volume_name)
if not locked:
break
eventlet.sleep(2)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to get lun status. [%s]',
volume_name)
LOG.debug('Lun [%(vol)s], status [%(status)s].',
{'vol': volume_name,
'status': status})
return status == 'normal'
def _check_snapshot_status_healthy(self, snapshot_uuid):
status = ''
try:
while True:
status, locked = self._get_snapshot_status(snapshot_uuid)
if not locked:
break
eventlet.sleep(2)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to get snapshot status. [%s]',
snapshot_uuid)
LOG.debug('Lun [%(snapshot)s], status [%(status)s].',
{'snapshot': snapshot_uuid,
'status': status})
return status == 'Healthy'
def _check_storage_response(self, out, **kwargs):
data = 'internal error'
exc = exception.VolumeBackendAPIException(data=data)
message = 'Internal error'
return (message, exc)
def _check_iscsi_response(self, out, **kwargs):
LUN_BAD_LUN_UUID = 18990505
LUN_NO_SUCH_SNAPSHOT = 18990532
if not self.check_value_valid(out, ['error', 'code'], int):
raise exception.MalformedResponse(cmd='_check_iscsi_response',
reason=_('no error code found'))
code = out['error']['code']
exc = None
message = ''
if code == LUN_BAD_LUN_UUID:
exc = SynoLUNNotExist(**kwargs)
message = 'Bad LUN UUID'
elif code == LUN_NO_SUCH_SNAPSHOT:
exc = exception.SnapshotNotFound(**kwargs)
message = 'No such snapshot'
else:
data = 'internal error'
exc = exception.VolumeBackendAPIException(data=data)
message = 'Internal error'
message = '%s [%d]' % (message, code)
return (message, exc)
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_ds_pool_status',
reason=_('no readonly found'))
if pool_info['readonly']:
message = (_('pool [%s] is not writable') %
self.config.synology_pool_name)
raise exception.VolumeDriverException(message=message)
def _check_ds_version(self):
try:
out = self.exec_webapi('SYNO.Core.System',
'info',
1,
type='firmware')
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _check_ds_version')
if not self.check_value_valid(out,
['data', 'firmware_ver'],
string_types):
raise exception.MalformedResponse(cmd='_check_ds_version',
reason=_('data not found'))
firmware_version = out['data']['firmware_ver']
# e.g. 'DSM 6.1-7610', 'DSM 6.0.1-7321 update 3', 'DSM UC 1.0-6789'
pattern = re.compile(r"^(.*) (\d+)\.(\d+)(?:\.(\d+))?-(\d+)"
r"(?: [uU]pdate (\d+))?$")
matches = pattern.match(firmware_version)
if not matches:
m = (_('DS version %s is not supported') %
firmware_version)
raise exception.VolumeDriverException(message=m)
os_name = matches.group(1)
major = int(matches.group(2))
minor = int(matches.group(3))
hotfix = int(matches.group(4)) if matches.group(4) else 0
if os_name == 'DSM UC':
return
elif (os_name == 'DSM' and
((6 > major) or (major == 6 and minor == 0 and hotfix < 2))):
m = (_('DS version %s is not supported') %
firmware_version)
raise exception.VolumeDriverException(message=m)
def _check_ds_ability(self):
try:
out = self.exec_webapi('SYNO.Core.System',
'info',
1,
type='define')
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _check_ds_ability')
if not self.check_value_valid(out, ['data'], dict):
raise exception.MalformedResponse(cmd='_check_ds_ability',
reason=_('data not found'))
define = out['data']
if 'usbstation' in define and define['usbstation'] == 'yes':
m = _('usbstation is not supported')
raise exception.VolumeDriverException(message=m)
if ('support_storage_mgr' not in define
or define['support_storage_mgr'] != 'yes'):
m = _('Storage Manager is not supported in DS')
raise exception.VolumeDriverException(message=m)
if ('support_iscsi_target' not in define
or define['support_iscsi_target'] != 'yes'):
m = _('iSCSI target feature is not supported in DS')
raise exception.VolumeDriverException(message=m)
if ('support_vaai' not in define
or define['support_vaai'] != 'yes'):
m = _('VAAI feature is not supported in DS')
raise exception.VolumeDriverException(message=m)
if ('supportsnapshot' not in define
or define['supportsnapshot'] != 'yes'):
m = _('Snapshot feature is not supported in DS')
raise exception.VolumeDriverException(message=m)
def check_response(self, out, **kwargs):
if out['success']:
return
data = 'internal error'
exc = exception.VolumeBackendAPIException(data=data)
message = 'Internal error'
api = out['api_info']['api']
if (api.startswith('SYNO.Core.ISCSI.')):
message, exc = self._check_iscsi_response(out, **kwargs)
elif (api.startswith('SYNO.Core.Storage.')):
message, exc = self._check_storage_response(out, **kwargs)
LOG.exception('%(message)s', {'message': message})
raise exc
def exec_webapi(self, api, method, version, **kwargs):
result = self.synoexec(api, method, version, **kwargs)
if 'http_status' in result and 200 != result['http_status']:
raise SynoAPIHTTPError(code=result['http_status'])
result['api_info'] = {'api': api,
'method': method,
'version': version}
return result
def check_value_valid(self, obj, key_array, value_type=None):
curr_obj = obj
for key in key_array:
if key not in curr_obj:
LOG.error('key [%(key)s] is not in %(obj)s',
{'key': key,
'obj': curr_obj})
return False
curr_obj = curr_obj[key]
if value_type and not isinstance(curr_obj, value_type):
LOG.error('[%(obj)s] is %(type)s, not %(value_type)s',
{'obj': curr_obj,
'type': type(curr_obj),
'value_type': value_type})
return False
return True
def get_ip(self):
return self.config.target_ip_address
def get_provider_location(self, iqn, trg_id):
portals = ['%(ip)s:%(port)d' % {'ip': self.get_ip(),
'port': self.target_port}]
sec_ips = self.config.safe_get('iscsi_secondary_ip_addresses')
for ip in sec_ips:
portals.append('%(ip)s:%(port)d' %
{'ip': ip,
'port': self.target_port})
return '%s,%d %s 0' % (
';'.join(portals),
trg_id,
iqn)
def is_lun_mapped(self, lun_name):
if not lun_name:
err = _('Param [lun_name] is invalid.')
raise exception.InvalidParameterValue(err=err)
try:
lun_info = self._get_lun_info(lun_name, ['is_mapped'])
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to _is_lun_mapped. [%s]', lun_name)
if not self.check_value_valid(lun_info, ['is_mapped'], bool):
raise exception.MalformedResponse(cmd='_is_lun_mapped',
reason=_('is_mapped not found'))
return lun_info['is_mapped']
def check_for_setup_error(self):
self._check_ds_pool_status()
self._check_ds_version()
self._check_ds_ability()
def update_volume_stats(self):
"""Update volume statistics.
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
data['vendor_name'] = self.vendor_name
data['storage_protocol'] = self.config.target_protocol
data['consistencygroup_support'] = False
data['QoS_support'] = False
data['thin_provisioning_support'] = True
data['thick_provisioning_support'] = False
data['reserved_percentage'] = self.config.reserved_percentage
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['target_ip_address'] = self.config.target_ip_address
data['pool_name'] = self.config.synology_pool_name
data['backend_info'] = ('%s:%s:%s' %
(self.vendor_name,
self.driver_type,
self.host_uuid))
return data
def create_volume(self, volume):
try:
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'create',
1,
name=volume['name'],
type=self.CINDER_LUN,
location=('/' +
self.config.synology_pool_name),
size=volume['size'] * units.Gi)
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to create_volume. [%s]',
volume['name'])
if not self._check_lun_status_normal(volume['name']):
message = _('Lun [%s] status is not normal') % volume['name']
raise exception.VolumeDriverException(message=message)
def delete_volume(self, volume):
try:
lun_uuid = self._get_lun_uuid(volume['name'])
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'delete',
1,
uuid=lun_uuid)
self.check_response(out)
except SynoLUNNotExist:
LOG.warning('LUN does not exist')
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to delete_volume. [%s]',
volume['name'])
def create_cloned_volume(self, volume, src_vref):
try:
src_lun_uuid = self._get_lun_uuid(src_vref['name'])
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'clone',
1,
src_lun_uuid=src_lun_uuid,
dst_lun_name=volume['name'],
is_same_pool=True,
clone_type='CINDER')
self.check_response(out)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to create_cloned_volume. [%s]',
volume['name'])
if not self._check_lun_status_normal(volume['name']):
message = _('Lun [%s] status is not normal.') % volume['name']
raise exception.VolumeDriverException(message=message)
if src_vref['size'] < volume['size']:
self.extend_volume(volume, volume['size'])
def extend_volume(self, volume, new_size):
try:
lun_uuid = self._get_lun_uuid(volume['name'])
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'set',
1,
uuid=lun_uuid,
new_size=new_size * units.Gi)
self.check_response(out)
except Exception as e:
LOG.exception('Failed to extend_volume. [%s]',
volume['name'])
raise exception.ExtendVolumeError(reason=e.msg)
def update_migrated_volume(self, volume, new_volume):
try:
self._modify_lun_name(new_volume['name'], volume['name'])
except Exception:
reason = _('Failed to _modify_lun_name [%s].') % new_volume['name']
raise exception.VolumeMigrationFailed(reason=reason)
return {'_name_id': None}
def create_snapshot(self, snapshot):
desc = '(Cinder) ' + (snapshot['id'] or '')
try:
resp = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'take_snapshot',
1,
src_lun_uuid=snapshot['volume']['name'],
is_app_consistent=False,
is_locked=False,
taken_by='Cinder',
description=desc)
self.check_response(resp)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to create_snapshot. [%s]',
snapshot['volume']['name'])
if not self.check_value_valid(resp,
['data', 'snapshot_uuid'],
string_types):
raise exception.MalformedResponse(cmd='create_snapshot',
reason=_('uuid not found'))
snapshot_uuid = resp['data']['snapshot_uuid']
if not self._check_snapshot_status_healthy(snapshot_uuid):
message = (_('Volume [%(vol)s] snapshot [%(snapshot)s] status '
'is not healthy.') %
{'vol': snapshot['volume']['name'],
'snapshot': snapshot_uuid})
raise exception.VolumeDriverException(message=message)
metadata = snapshot['metadata']
metadata.update({
self.METADATA_DS_SNAPSHOT_UUID: snapshot_uuid
})
return {'metadata': metadata}
def delete_snapshot(self, snapshot):
try:
ds_snapshot_uuid = (self._get_metadata_value
(snapshot, self.METADATA_DS_SNAPSHOT_UUID))
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'delete_snapshot',
1,
snapshot_uuid=ds_snapshot_uuid,
deleted_by='Cinder')
self.check_response(out, snapshot_id=snapshot['id'])
except (exception.SnapshotNotFound,
exception.SnapshotMetadataNotFound):
return
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to delete_snapshot. [%s]',
snapshot['id'])
def create_volume_from_snapshot(self, volume, snapshot):
try:
ds_snapshot_uuid = (self._get_metadata_value
(snapshot, self.METADATA_DS_SNAPSHOT_UUID))
out = self.exec_webapi('SYNO.Core.ISCSI.LUN',
'clone_snapshot',
1,
src_lun_uuid=snapshot['volume']['name'],
snapshot_uuid=ds_snapshot_uuid,
cloned_lun_name=volume['name'],
clone_type='CINDER')
self.check_response(out)
except exception.SnapshotMetadataNotFound:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to get snapshot UUID. [%s]',
snapshot['id'])
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception('Failed to create_volume_from_snapshot. [%s]',
snapshot['id'])
if not self._check_lun_status_normal(volume['name']):
message = (_('Volume [%(vol)s] snapshot [%(snapshot)s] status '
'is not healthy.') %
{'vol': snapshot['volume']['name'],
'snapshot': ds_snapshot_uuid})
raise exception.VolumeDriverException(message=message)
if snapshot['volume_size'] < volume['size']:
self.extend_volume(volume, volume['size'])
def get_iqn_and_trgid(self, location):
if not location:
err = _('Param [location] is invalid.')
raise exception.InvalidParameterValue(err=err)
result = location.split(' ')
if len(result) < 2:
raise exception.InvalidInput(reason=location)
data = result[0].split(',')
if len(data) < 2:
raise exception.InvalidInput(reason=location)
iqn = result[1]
trg_id = data[1]
return iqn, int(trg_id, 10)
def get_iscsi_properties(self, volume):
if not volume['provider_location']:
err = _("Param volume['provider_location'] is invalid.")
raise exception.InvalidParameterValue(err=err)
iqn, trg_id = self.get_iqn_and_trgid(volume['provider_location'])
iscsi_properties = {
'target_discovered': False,
'target_iqn': iqn,
'target_portal': '%(ip)s:%(port)d' % {'ip': self.get_ip(),
'port': self.target_port},
'volume_id': volume['id'],
'access_mode': 'rw',
'discard': False
}
ips = self.config.safe_get('iscsi_secondary_ip_addresses')
if ips:
target_portals = [iscsi_properties['target_portal']]
for ip in ips:
target_portals.append('%(ip)s:%(port)d' %
{'ip': ip,
'port': self.target_port})
iscsi_properties.update(target_portals=target_portals)
count = len(target_portals)
iscsi_properties.update(target_iqns=[
iscsi_properties['target_iqn']
] * count)
iscsi_properties.update(target_lun=0)
iscsi_properties.update(target_luns=[
iscsi_properties['target_lun']
] * count)
if 'provider_auth' in volume:
auth = volume['provider_auth']
if auth:
try:
(auth_method, auth_username, auth_password) = auth.split()
iscsi_properties['auth_method'] = auth_method
iscsi_properties['auth_username'] = auth_username
iscsi_properties['auth_password'] = auth_password
except Exception:
LOG.error('Invalid provider_auth: %s', auth)
return iscsi_properties
def create_iscsi_export(self, volume_name, identifier):
iqn, trg_id, provider_auth = self._target_create(identifier)
self._lun_map_target(volume_name, trg_id)
return iqn, trg_id, provider_auth
def remove_iscsi_export(self, volume_name, trg_id):
self._lun_unmap_target(volume_name, trg_id)
self._target_delete(trg_id)