text: # 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
  
  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 utils as volutils
  
  
  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 AESCipher(object):
      """Encrypt with OpenSSL-compatible way"""
  
      SALT_MAGIC = '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)
  
      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 = 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 "%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(reason=_('Login failed.'))
  
      def _random_AES_passphrase(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):
          public_numbers = rsa.RSAPublicNumbers(passphrase, modulus)
          public_key = public_numbers.public_key(default_backend())
          ciphertext = public_key.encrypt(
              text.encode('ascii'),
              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 exception.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 exception.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('iscsi_ip_address'):
              raise exception.InvalidConfigurationValue(
                  option='iscsi_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.iscsi_port = self.config.safe_get('iscsi_port')
  
          api = APIRequest(self.config.iscsi_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
                               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('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 = 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_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-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('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 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('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.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('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.iscsi_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['iscsi_ip_address'] = self.config.iscsi_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 exception.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.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('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)
  