# 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
import time
import urllib

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
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 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')

    # TODO(alee): This probably needs to be replaced with a version that
    # does not use md5, as this will be disallowed on a FIPS enabled system
    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, usedforsecurity=True).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'] in [105, 119]):
            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'],
                                              str)):
            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'], str):
            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'], str):
            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'], str):
            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
                time.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
                time.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'],
                                      str):
            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('target_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'],
                                      str):
            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('target_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)
