1237 lines
45 KiB
Python
1237 lines
45 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
|
|
from hashlib import md5
|
|
import json
|
|
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
|
|
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 import utils
|
|
from cinder.i18n import _, _LE, _LW
|
|
from cinder.objects import snapshot
|
|
from cinder.objects import volume
|
|
from cinder.volume import utils as volutils
|
|
|
|
|
|
cinder_opts = [
|
|
cfg.StrOpt('pool_name',
|
|
default='',
|
|
help='Volume on Synology storage to be used for creating lun.'),
|
|
cfg.PortOpt('admin_port',
|
|
default=5000,
|
|
help='Management port for Synology storage.'),
|
|
cfg.StrOpt('username',
|
|
default='admin',
|
|
help='Administrator of Synology storage.'),
|
|
cfg.StrOpt('password',
|
|
default='',
|
|
help='Password of administator for logging in '
|
|
'Synology storage.',
|
|
secret=True),
|
|
cfg.BoolOpt('ssl_verify',
|
|
default=True,
|
|
help='Do certificate validation or not if '
|
|
'$driver_use_ssl is True'),
|
|
cfg.StrOpt('one_time_pass',
|
|
default=None,
|
|
help='One time password of administator for logging in '
|
|
'Synology storage if OTP is enabled.',
|
|
secret=True),
|
|
cfg.StrOpt('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)
|
|
|
|
|
|
class AESCipher(object):
|
|
"""Encrypt with OpenSSL-compatible way"""
|
|
|
|
SALT_MAGIC = 'Salted__'
|
|
|
|
def __init__(self, password, key_length=32):
|
|
self._bs = AES.block_size
|
|
self._salt = Random.new().read(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)
|
|
|
|
def _derive_key_and_iv(self, password, salt, key_length, iv_length):
|
|
d = d_i = ''
|
|
while len(d) < key_length + iv_length:
|
|
md5_str = d_i + password + salt
|
|
d_i = md5(md5_str).digest()
|
|
d += d_i
|
|
return d[:key_length], d[key_length:key_length + iv_length]
|
|
|
|
def encrypt(self, text):
|
|
cipher = AES.new(self._key, AES.MODE_CBC, self._iv)
|
|
ciphertext = cipher.encrypt(self._pad(text))
|
|
|
|
return "%s%s%s" % (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 exception.SynoAuthError(_('Login failed.'))
|
|
|
|
def _random_AES_passpharse(self, length):
|
|
available = ('0123456789'
|
|
'abcdefghijklmnopqrstuvwxyz'
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
'~!@#$%^&*()_+-/')
|
|
key = ''
|
|
|
|
while length > 0:
|
|
key += available[randint(0, len(available) - 1)]
|
|
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):
|
|
key = RSA.construct((modulus, passphrase))
|
|
cipher = PKCS1_v1_5.new(key)
|
|
ciphertext = cipher.encrypt(text)
|
|
|
|
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_passpharse(501)
|
|
|
|
params[cipher_token] = server_time
|
|
|
|
encrypted_passphrase = self._encrypt_RSA(string.atol(public_key, 16),
|
|
string.atol("10001", 16),
|
|
random_passphrase)
|
|
|
|
encrypted_params = self._encrypt_AES(random_passphrase,
|
|
urllib.parse.urlencode(params))
|
|
|
|
enc_params = {"rsa": base64.b64encode(encrypted_passphrase),
|
|
"aes": base64.b64encode(encrypted_params)}
|
|
|
|
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 exception.SynoAuthError as e:
|
|
if attempts < 1:
|
|
LOG.debug('Session might have expired.'
|
|
' Trying to relogin')
|
|
self.new_session()
|
|
continue
|
|
else:
|
|
LOG.error(_LE('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 exception.SynoAuthError(_('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('iscsi_ip_address'):
|
|
raise exception.InvalidConfigurationValue(
|
|
option='iscsi_ip_address',
|
|
value='')
|
|
if not config.safe_get('pool_name'):
|
|
raise exception.InvalidConfigurationValue(
|
|
option='pool_name',
|
|
value='')
|
|
|
|
self.config = config
|
|
self.vendor_name = 'Synology'
|
|
self.driver_type = driver_type
|
|
self.volume_backend_name = self._get_backend_name()
|
|
self.iscsi_port = self.config.safe_get('iscsi_port')
|
|
|
|
api = APIRequest(self.config.iscsi_ip_address,
|
|
self.config.admin_port,
|
|
self.config.username,
|
|
self.config.password,
|
|
self.config.safe_get('driver_use_ssl'),
|
|
self.config.safe_get('ssl_verify'),
|
|
self.config.safe_get('one_time_pass'),
|
|
self.config.safe_get('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(_LE('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.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(_LE('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)
|
|
|
|
return free_capacity_gb, total_capacity_gb
|
|
|
|
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(_LE('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(_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',
|
|
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(_LE('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(_LE('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(_LE('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
|
|
volutils.generate_username(12))
|
|
chap_password = (self.config.safe_get('chap_password') or
|
|
volutils.generate_password())
|
|
provider_auth = ' '.join(('CHAP', chap_username, chap_password))
|
|
|
|
trg_prefix = self.config.safe_get('iscsi_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(_LE('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(_LE('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(_LE('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(_LE('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
|
|
time.sleep(2)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception(_LE('Failed to get lun status. [%s]'),
|
|
volume_name)
|
|
|
|
LOG.debug(_LE('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
|
|
time.sleep(2)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception(_LE('Failed to get snapshot status. [%s]'),
|
|
snapshot_uuid)
|
|
|
|
LOG.debug(_LE('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='exec_webapi',
|
|
reason=_('no error code found'))
|
|
|
|
code = out['error']['code']
|
|
exc = None
|
|
message = ''
|
|
|
|
if code == LUN_BAD_LUN_UUID:
|
|
exc = exception.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_for_setup_error',
|
|
reason=_('no readonly found'))
|
|
|
|
if pool_info['readonly']:
|
|
message = _('pool [%s] is not writable') % self.config.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(_LE('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-7370', 'DSM 6.0-7321 update 3'
|
|
version = firmware_version.split()[1].split('-')[0]
|
|
versions = version.split('.')
|
|
major, minor, hotfix = (versions[0],
|
|
versions[1],
|
|
versions[2] if len(versions) is 3 else '0')
|
|
|
|
major, minor, hotfix = (int(major), int(minor), int(hotfix))
|
|
|
|
if (6 > major) or (major is 6 and minor is 0 and hotfix < 2):
|
|
m = (_('DS version %s is not supperted') %
|
|
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(_LE('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(_LE('%(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 exception.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(_LE('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(_LE('[%(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.iscsi_ip_address
|
|
|
|
def get_provider_location(self, iqn, trg_id):
|
|
portals = ['%(ip)s:%(port)d' % {'ip': self.get_ip(),
|
|
'port': self.iscsi_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.iscsi_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(_LE('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."""
|
|
|
|
free_capacity_gb, total_capacity_gb = self._get_pool_size()
|
|
|
|
data = {}
|
|
data['volume_backend_name'] = self.volume_backend_name
|
|
data['vendor_name'] = self.vendor_name
|
|
data['storage_protocol'] = self.config.iscsi_protocol
|
|
data['consistencygroup_support'] = False
|
|
data['QoS_support'] = False
|
|
data['thin_provisioning_support'] = True
|
|
data['thick_provisioning_support'] = False
|
|
data['reserved_percentage'] = 0
|
|
|
|
data['free_capacity_gb'] = free_capacity_gb
|
|
data['total_capacity_gb'] = total_capacity_gb
|
|
|
|
data['iscsi_ip_address'] = self.config.iscsi_ip_address
|
|
data['pool_name'] = self.config.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.pool_name,
|
|
size=volume['size'] * units.Gi)
|
|
|
|
self.check_response(out)
|
|
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception(_LE('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 exception.SynoLUNNotExist:
|
|
LOG.warning(_LW('LUN does not exist'))
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception(_LE('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(_LE('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(_LE('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(_LE('Failed to create_snapshot. [%s]'),
|
|
snapshot['volume']['name'])
|
|
|
|
if not self.check_value_valid(resp,
|
|
['data', 'snapshot_uuid'],
|
|
string_types):
|
|
raise exception.MalformedResponse(cmd='take_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)
|
|
|
|
return {
|
|
'metadata': {
|
|
self.METADATA_DS_SNAPSHOT_UUID: snapshot_uuid
|
|
}}
|
|
|
|
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,
|
|
delete_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(_LE('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(_LE('Failed to get snapshot UUID. [%s]'),
|
|
snapshot['id'])
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception(_LE('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.iscsi_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.iscsi_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(_LE('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)
|