873 lines
32 KiB
Python
873 lines
32 KiB
Python
# Copyright 2017 Inspur Corp.
|
|
# 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.
|
|
|
|
"""
|
|
Volume driver for Inspur AS13000
|
|
"""
|
|
|
|
import ipaddress
|
|
import json
|
|
import random
|
|
import re
|
|
import time
|
|
|
|
import eventlet
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import units
|
|
import requests
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import interface
|
|
from cinder import utils
|
|
from cinder.volume.drivers.san import san
|
|
from cinder.volume import utils as volume_utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
inspur_as13000_opts = [
|
|
cfg.ListOpt(
|
|
'as13000_ipsan_pools',
|
|
default=['Pool0'],
|
|
help='The Storage Pools Cinder should use, a comma separated list.'),
|
|
cfg.IntOpt(
|
|
'as13000_token_available_time',
|
|
default=3300,
|
|
min=600, max=3600,
|
|
help='The effective time of token validity in seconds.'),
|
|
cfg.StrOpt(
|
|
'as13000_meta_pool',
|
|
help='The pool which is used as a meta pool when creating a volume, '
|
|
'and it should be a replication pool at present. '
|
|
'If not set, the driver will choose a replication pool '
|
|
'from the value of as13000_ipsan_pools.'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(inspur_as13000_opts)
|
|
|
|
|
|
class RestAPIExecutor(object):
|
|
def __init__(self, hostname, port, username, password):
|
|
self._username = username
|
|
self._password = password
|
|
self._token = None
|
|
self._baseurl = 'http://%s:%s/rest' % (hostname, port)
|
|
|
|
def login(self):
|
|
"""Login the AS13000 and store the token."""
|
|
self._token = self._login()
|
|
LOG.debug('Login the AS13000.')
|
|
|
|
def _login(self):
|
|
"""Do request to login the AS13000 and get the token."""
|
|
method = 'security/token'
|
|
params = {'name': self._username, 'password': self._password}
|
|
token = self.send_rest_api(method=method, params=params,
|
|
request_type='post').get('token')
|
|
return token
|
|
|
|
@utils.retry(exception.VolumeDriverException, interval=1, retries=3)
|
|
def send_rest_api(self, method, params=None, request_type='post'):
|
|
try:
|
|
return self.send_api(method, params, request_type)
|
|
except exception.VolumeDriverException:
|
|
self.login()
|
|
raise
|
|
|
|
@staticmethod
|
|
@utils.trace_method
|
|
def do_request(cmd, url, header, data):
|
|
"""Send request to the storage and handle the response."""
|
|
if cmd in ['post', 'get', 'put', 'delete']:
|
|
req = getattr(requests, cmd)(url, data=data, headers=header)
|
|
else:
|
|
msg = (_('Unsupported cmd: %s.') % cmd)
|
|
raise exception.VolumeBackendAPIException(msg)
|
|
|
|
response = req.json()
|
|
code = req.status_code
|
|
LOG.debug('CODE: %(code)s, RESPONSE: %(response)s.',
|
|
{'code': code, 'response': response})
|
|
|
|
if code != 200:
|
|
msg = (_('Code: %(code)s, URL: %(url)s, Message: %(msg)s.')
|
|
% {'code': req.status_code,
|
|
'url': req.url,
|
|
'msg': req.text})
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(msg)
|
|
|
|
return response
|
|
|
|
@utils.trace
|
|
def send_api(self, method, params=None, request_type='post'):
|
|
if params:
|
|
params = json.dumps(params)
|
|
|
|
url = '%s/%s' % (self._baseurl, method)
|
|
|
|
# header is not needed when the driver login the backend
|
|
if method == 'security/token':
|
|
if request_type == 'delete':
|
|
header = {'X-Auth-Token': self._token}
|
|
else:
|
|
header = None
|
|
else:
|
|
if not self._token:
|
|
self.login()
|
|
header = {'X-Auth-Token': self._token}
|
|
|
|
response = self.do_request(request_type, url, header, params)
|
|
|
|
try:
|
|
code = response.get('code')
|
|
if code == 0:
|
|
if request_type == 'get':
|
|
data = response.get('data')
|
|
else:
|
|
if method == 'security/token':
|
|
data = response.get('data')
|
|
else:
|
|
data = response.get('message')
|
|
data = str(data).lower()
|
|
if hasattr(data, 'success'):
|
|
return
|
|
elif code == 301:
|
|
msg = _('Token is expired.')
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(msg)
|
|
else:
|
|
message = response.get('message')
|
|
msg = (_('Unexpected RestAPI response: %(code)d %(msg)s.') % {
|
|
'code': code, 'msg': message})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(msg)
|
|
except ValueError:
|
|
msg = _("Deal with response failed.")
|
|
raise exception.VolumeDriverException(msg)
|
|
|
|
return data
|
|
|
|
|
|
@interface.volumedriver
|
|
class AS13000Driver(san.SanISCSIDriver):
|
|
"""Driver for Inspur AS13000 storage.
|
|
|
|
.. code-block:: none
|
|
|
|
Version history:
|
|
1.0.0 - Initial driver
|
|
|
|
"""
|
|
|
|
VENDOR = 'INSPUR'
|
|
VERSION = '1.0.0'
|
|
PROTOCOL = 'iSCSI'
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = 'INSPUR_CI'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(AS13000Driver, self).__init__(*args, **kwargs)
|
|
self.configuration.append_config_values(inspur_as13000_opts)
|
|
self.hostname = self.configuration.san_ip
|
|
self.port = self.configuration.safe_get('san_api_port') or 8088
|
|
self.username = self.configuration.san_login
|
|
self.password = self.configuration.san_password
|
|
self.token_available_time = (self.configuration.
|
|
as13000_token_available_time)
|
|
self.pools = self.configuration.as13000_ipsan_pools
|
|
self.meta_pool = self.configuration.as13000_meta_pool
|
|
self.pools_info = {}
|
|
self.nodes = []
|
|
self._token_time = 0
|
|
# get the RestAPIExecutor
|
|
self._rest = RestAPIExecutor(self.hostname,
|
|
self.port,
|
|
self.username,
|
|
self.password)
|
|
|
|
@staticmethod
|
|
def get_driver_options():
|
|
return inspur_as13000_opts
|
|
|
|
@utils.trace
|
|
def do_setup(self, context):
|
|
# get tokens for the driver
|
|
self._rest.login()
|
|
self._token_time = time.time()
|
|
|
|
# get available nodes in the backend
|
|
for node in self._get_cluster_status():
|
|
if node.get('healthStatus') == 1 and node.get('ip'):
|
|
self.nodes.append(node)
|
|
|
|
# collect pools info
|
|
meta_pools = [self.meta_pool] if self.meta_pool else []
|
|
self.pools_info = self._get_pools_info(self.pools + meta_pools)
|
|
|
|
# setup the meta pool if it is not setted
|
|
if not self.meta_pool:
|
|
for pool_info in self.pools_info.values():
|
|
if pool_info['type'] in (1, '1'):
|
|
self.meta_pool = pool_info['name']
|
|
break
|
|
|
|
self._check_pools()
|
|
|
|
self._check_meta_pool()
|
|
|
|
@utils.trace
|
|
def check_for_setup_error(self):
|
|
"""Do check to make sure service is available."""
|
|
# check the required flags in conf
|
|
required_flags = ['san_ip', 'san_login', 'san_password',
|
|
'as13000_ipsan_pools']
|
|
for flag in required_flags:
|
|
value = self.configuration.safe_get(flag)
|
|
if not value:
|
|
msg = (_('Required flag %s is not set.') % flag)
|
|
LOG.error(msg)
|
|
raise exception.InvalidConfigurationValue(option=flag,
|
|
value=value)
|
|
|
|
# make sure at least one node can
|
|
if not self.nodes:
|
|
msg = _('No healthy nodes are available!')
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(message=msg)
|
|
|
|
def _check_pools(self):
|
|
"""Check the pool in conf exist in the AS13000."""
|
|
if not set(self.pools).issubset(self.pools_info):
|
|
pools = set(self.pools) - set(self.pools_info)
|
|
msg = _('Pools %s do not exist.') % pools
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
def _check_meta_pool(self):
|
|
"""Check whether the meta pool is valid."""
|
|
if not self.meta_pool:
|
|
msg = _('Meta pool is not set.')
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
if self.meta_pool not in self.pools_info:
|
|
msg = _('Meta pool %s does not exist.') % self.meta_pool
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
if self.pools_info[self.meta_pool]['type'] not in (1, '1'):
|
|
msg = _('Meta pool %s is not a replication pool.') % self.meta_pool
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
@utils.trace
|
|
def create_volume(self, volume):
|
|
"""Create volume in the backend."""
|
|
pool = volume_utils.extract_host(volume.host, level='pool')
|
|
size = volume.size * units.Ki
|
|
name = self._trans_name_down(volume.name)
|
|
|
|
method = 'block/lvm'
|
|
request_type = "post"
|
|
params = {
|
|
"name": name,
|
|
"capacity": size,
|
|
"dataPool": pool,
|
|
"dataPoolType": self.pools_info[pool]['type'],
|
|
"metaPool": self.meta_pool
|
|
}
|
|
self._rest.send_rest_api(method=method, params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Create a new volume base on a specific snapshot."""
|
|
if snapshot.volume_size > volume.size:
|
|
msg = (_("create_volume_from_snapshot: snapshot %(snapshot_name)s "
|
|
"size is %(snapshot_size)dGB and doesn't fit in target "
|
|
"volume %(volume_name)s of size %(volume_size)dGB.") %
|
|
{'snapshot_name': snapshot.name,
|
|
'snapshot_size': snapshot.volume_size,
|
|
'volume_name': volume.name,
|
|
'volume_size': volume.size})
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(message=msg)
|
|
src_vol_name = self._trans_name_down(snapshot.volume_name)
|
|
source_vol = snapshot.volume
|
|
src_pool = volume_utils.extract_host(source_vol['host'],
|
|
level='pool')
|
|
dest_name = self._trans_name_down(volume.name)
|
|
dest_pool = volume_utils.extract_host(volume.host, level='pool')
|
|
snap_name = self._trans_name_down(snapshot.name)
|
|
|
|
# lock the snapshot before clone from it
|
|
self._snapshot_lock_op('lock', src_vol_name, snap_name, src_pool)
|
|
|
|
# do clone from snap to a volume
|
|
method = 'snapshot/volume/cloneLvm'
|
|
request_type = 'post'
|
|
params = {'originalLvm': src_vol_name,
|
|
'originalPool': src_pool,
|
|
'originalSnap': snap_name,
|
|
'name': dest_name,
|
|
'pool': dest_pool}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
# do filling the cloned volume
|
|
self._filling_volume(dest_name, dest_pool)
|
|
|
|
# wait until the cloned volume has been filled
|
|
self._wait_volume_filled(dest_name, dest_pool)
|
|
|
|
# unlock the original snapshot
|
|
self._snapshot_lock_op('unlock', src_vol_name, snap_name, src_pool)
|
|
|
|
if volume.size > snapshot.volume_size:
|
|
self.extend_volume(volume, volume.size)
|
|
|
|
@utils.trace
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Clone a volume."""
|
|
if src_vref.size > volume.size:
|
|
msg = (_("create_cloned_volume: source volume %(src_vol)s "
|
|
"size is %(src_size)dGB and doesn't fit in target "
|
|
"volume %(tgt_vol)s of size %(tgt_size)dGB.") %
|
|
{'src_vol': src_vref.name,
|
|
'src_size': src_vref.size,
|
|
'tgt_vol': volume.name,
|
|
'tgt_size': volume.size})
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(message=msg)
|
|
dest_pool = volume_utils.extract_host(volume.host, level='pool')
|
|
dest_vol_name = self._trans_name_down(volume.name)
|
|
src_pool = volume_utils.extract_host(src_vref.host, level='pool')
|
|
src_vol_name = self._trans_name_down(src_vref.name)
|
|
|
|
method = 'block/lvm/clone'
|
|
request_type = 'post'
|
|
params = {'srcVolumeName': src_vol_name,
|
|
'srcPoolName': src_pool,
|
|
'destVolumeName': dest_vol_name,
|
|
'destPoolName': dest_pool}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
if volume.size > src_vref.size:
|
|
self.extend_volume(volume, volume.size)
|
|
|
|
@utils.trace
|
|
def extend_volume(self, volume, new_size):
|
|
"""Extend volume to new size."""
|
|
name = self._trans_name_down(volume.name)
|
|
if not self._check_volume(volume):
|
|
msg = _('Extend Volume Failed: Volume %s does not exist.') % name
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(message=msg)
|
|
|
|
size = new_size * units.Ki
|
|
pool = volume_utils.extract_host(volume.host, level='pool')
|
|
|
|
method = 'block/lvm'
|
|
request_type = 'put'
|
|
params = {'pool': pool,
|
|
'name': name,
|
|
'newCapacity': size}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def delete_volume(self, volume):
|
|
"""Delete volume from AS13000."""
|
|
name = self._trans_name_down(volume.name)
|
|
if not self._check_volume(volume):
|
|
# if volume is not exist in backend, the driver will do
|
|
# nothing but log it
|
|
LOG.info('Tried to delete non-existent volume %(name)s.',
|
|
{'name': name})
|
|
return
|
|
|
|
pool = volume_utils.extract_host(volume.host, level='pool')
|
|
|
|
method = 'block/lvm?pool=%s&lvm=%s' % (pool, name)
|
|
request_type = 'delete'
|
|
self._rest.send_rest_api(method=method, request_type=request_type)
|
|
|
|
@utils.trace
|
|
def create_snapshot(self, snapshot):
|
|
"""Create snapshot of volume in backend.
|
|
|
|
The snapshot type of AS13000 is copy-on-write.
|
|
"""
|
|
source_volume = snapshot.volume
|
|
volume_name = self._trans_name_down(source_volume.name)
|
|
if not self._check_volume(source_volume):
|
|
msg = (_('create_snapshot: Source_volume %s does not exist.')
|
|
% volume_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(message=msg)
|
|
|
|
pool = volume_utils.extract_host(source_volume.host, level='pool')
|
|
snapshot_name = self._trans_name_down(snapshot.name)
|
|
|
|
method = 'snapshot/volume'
|
|
request_type = 'post'
|
|
params = {'snapName': snapshot_name,
|
|
'volumeName': volume_name,
|
|
'poolName': pool,
|
|
'snapType': 'r'}
|
|
self._rest.send_rest_api(method=method, params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def delete_snapshot(self, snapshot):
|
|
"""Delete snapshot of volume."""
|
|
source_volume = snapshot.volume
|
|
volume_name = self._trans_name_down(source_volume.name)
|
|
if self._check_volume(source_volume) is False:
|
|
msg = (_('delete_snapshot: Source_volume %s does not exist.')
|
|
% volume_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(message=msg)
|
|
|
|
pool = volume_utils.extract_host(source_volume.host, level='pool')
|
|
snapshot_name = self._trans_name_down(snapshot.name)
|
|
|
|
method = ('snapshot/volume?snapName=%s&volumeName=%s&poolName=%s'
|
|
% (snapshot_name, volume_name, pool))
|
|
request_type = 'delete'
|
|
self._rest.send_rest_api(method=method, request_type=request_type)
|
|
|
|
@utils.trace
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Get volume stats.
|
|
|
|
If we haven't gotten stats yet or 'refresh' is True,
|
|
run update the stats first.
|
|
"""
|
|
if not self._stats or refresh:
|
|
self._update_volume_stats()
|
|
return self._stats
|
|
|
|
@utils.trace
|
|
def _update_volume_stats(self):
|
|
"""Update the backend stats including driver info and pools info."""
|
|
|
|
# As _update_volume_stats runs periodically,
|
|
# so we can do a check and refresh the token each time it runs.
|
|
time_difference = time.time() - self._token_time
|
|
if time_difference > self.token_available_time:
|
|
self._rest.login()
|
|
self._token_time = time.time()
|
|
LOG.debug('Token of the Driver has been refreshed.')
|
|
|
|
# update the backend stats
|
|
data = {}
|
|
backend_name = self.configuration.safe_get('volume_backend_name')
|
|
data['vendor_name'] = self.VENDOR
|
|
data['driver_version'] = self.VERSION
|
|
data['storage_protocol'] = self.PROTOCOL
|
|
data['volume_backend_name'] = backend_name
|
|
data['pools'] = self._get_pools_stats()
|
|
|
|
self._stats = data
|
|
LOG.debug('Update volume stats : %(stats)s.', {'stats': self._stats})
|
|
|
|
def _build_target_portal(self, ip, port):
|
|
"""Build iSCSI portal for both IPV4 and IPV6."""
|
|
addr = ipaddress.ip_address(ip)
|
|
if addr.version == 4:
|
|
ipaddr = ip
|
|
else:
|
|
ipaddr = '[%s]' % ip
|
|
return '%(ip)s:%(port)s' % {'ip': ipaddr, 'port': port}
|
|
|
|
@utils.trace
|
|
def initialize_connection(self, volume, connector, **kwargs):
|
|
"""Initialize connection steps:
|
|
|
|
1. check if the host exist in targets.
|
|
2.1 if there is target that has the host, add the volume to the target.
|
|
2.2 if not, create an target add host to host add volume to host.
|
|
3. return the target info.
|
|
"""
|
|
host_ip = connector['ip']
|
|
multipath = connector.get("multipath", False)
|
|
# Check if there host exist in targets
|
|
host_exist, target_name, node_of_target = self._get_target_from_conn(
|
|
host_ip)
|
|
if not host_exist:
|
|
# host doesn't exist, need create target and bind the host,
|
|
|
|
# generate the target name
|
|
_TARGET_NAME_PATTERN = 'target.inspur.%(host)s-%(padding)s'
|
|
_padding = str(random.randint(0, 99999999)).zfill(8)
|
|
target_name = _TARGET_NAME_PATTERN % {'host': connector['host'],
|
|
'padding': _padding}
|
|
|
|
# decide the nodes to be used
|
|
if multipath:
|
|
node_of_target = [node['name'] for node in self.nodes]
|
|
else:
|
|
# single node
|
|
node_of_target = [self.nodes[0]['name']]
|
|
|
|
# create the target
|
|
nodes = ','.join(node_of_target)
|
|
self._create_target(target_node=nodes,
|
|
target_name=target_name)
|
|
self._add_host_to_target(host_ip=host_ip,
|
|
target_name=target_name)
|
|
|
|
self._add_lun_to_target(target_name=target_name, volume=volume)
|
|
if self.configuration.use_chap_auth:
|
|
self._add_chap_to_target(target_name,
|
|
self.configuration.chap_username,
|
|
self.configuration.chap_password)
|
|
|
|
lun_id = self._get_lun_id(volume, target_name)
|
|
connection_data = {
|
|
'target_discovered': True,
|
|
'volume_id': volume.id,
|
|
}
|
|
|
|
portals = []
|
|
for node_name in node_of_target:
|
|
for node in self.nodes:
|
|
if node['name'] == node_name:
|
|
portal = self._build_target_portal(node.get('ip'), '3260')
|
|
portals.append(portal)
|
|
|
|
if multipath:
|
|
connection_data.update({
|
|
'target_portals': portals,
|
|
'target_luns': [int(lun_id)] * len(portals),
|
|
'target_iqns': [target_name] * len(portals)
|
|
})
|
|
else:
|
|
# single node
|
|
connection_data.update({
|
|
'target_portal': portals[0],
|
|
'target_lun': int(lun_id),
|
|
'target_iqn': target_name
|
|
})
|
|
|
|
if self.configuration.use_chap_auth:
|
|
connection_data['auth_method'] = 'CHAP'
|
|
connection_data['auth_username'] = self.configuration.chap_username
|
|
connection_data['auth_password'] = self.configuration.chap_password
|
|
|
|
return {'driver_volume_type': 'iscsi', 'data': connection_data}
|
|
|
|
@utils.trace
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
"""Delete lun from target.
|
|
|
|
If target has no any lun, driver will delete the target.
|
|
"""
|
|
volume_name = self._trans_name_down(volume.name)
|
|
target_name = None
|
|
lun_id = None
|
|
|
|
host_ip = None
|
|
if connector and 'ip' in connector:
|
|
host_ip = connector['ip']
|
|
|
|
target_list = self._get_target_list()
|
|
for target in target_list:
|
|
if not host_ip or host_ip in target['hostIp']:
|
|
for lun in target['lun']:
|
|
if volume_name == lun['lvm']:
|
|
target_name = target['name']
|
|
lun_id = lun['lunID']
|
|
break
|
|
if lun_id is not None:
|
|
break
|
|
if lun_id is None:
|
|
return
|
|
|
|
self._delete_lun_from_target(target_name=target_name,
|
|
lun_id=lun_id)
|
|
luns = self._get_lun_list(target_name)
|
|
if not luns:
|
|
self._delete_target(target_name)
|
|
|
|
def _get_pools_info(self, pools):
|
|
"""Get the pools info."""
|
|
method = 'block/pool?type=2'
|
|
requests_type = 'get'
|
|
pools_data = self._rest.send_rest_api(method=method,
|
|
request_type=requests_type)
|
|
pools_info = {}
|
|
for pool_data in pools_data:
|
|
if pool_data['name'] in pools:
|
|
pools_info[pool_data['name']] = pool_data
|
|
|
|
return pools_info
|
|
|
|
@utils.trace
|
|
def _get_pools_stats(self):
|
|
"""Generate the pool stat information."""
|
|
pools_info = self._get_pools_info(self.pools)
|
|
|
|
pools = []
|
|
for pool_info in pools_info.values():
|
|
total_capacity = pool_info.get('totalCapacity')
|
|
total_capacity_gb = self._unit_convert(total_capacity)
|
|
used_capacity = pool_info.get('usedCapacity')
|
|
used_capacity_gb = self._unit_convert(used_capacity)
|
|
free_capacity_gb = total_capacity_gb - used_capacity_gb
|
|
|
|
pool = {
|
|
'pool_name': pool_info.get('name'),
|
|
'total_capacity_gb': total_capacity_gb,
|
|
'free_capacity_gb': free_capacity_gb,
|
|
'thin_provisioning_support': True,
|
|
'thick_provisioning_support': False,
|
|
}
|
|
pools.append(pool)
|
|
|
|
return pools
|
|
|
|
@utils.trace
|
|
def _get_target_from_conn(self, host_ip):
|
|
"""Get target information base on the host ip."""
|
|
host_exist = False
|
|
target_name = None
|
|
node = None
|
|
|
|
target_list = self._get_target_list()
|
|
for target in target_list:
|
|
if host_ip in target['hostIp']:
|
|
host_exist = True
|
|
target_name = target['name']
|
|
node = target['node']
|
|
break
|
|
|
|
return host_exist, target_name, node
|
|
|
|
@utils.trace
|
|
def _get_target_list(self):
|
|
"""Get a list of all targets in the backend."""
|
|
method = 'block/target/detail'
|
|
request_type = 'get'
|
|
data = self._rest.send_rest_api(method=method,
|
|
request_type=request_type)
|
|
return data
|
|
|
|
@utils.trace
|
|
def _create_target(self, target_name, target_node):
|
|
"""Create a target on the specified node."""
|
|
method = 'block/target'
|
|
request_type = 'post'
|
|
params = {'name': target_name, 'nodeName': target_node}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _delete_target(self, target_name):
|
|
"""Delete all target of all the node."""
|
|
method = 'block/target?name=%s' % target_name
|
|
request_type = 'delete'
|
|
self._rest.send_rest_api(method=method,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _add_chap_to_target(self, target_name, chap_username, chap_password):
|
|
"""Add CHAP to target."""
|
|
method = 'block/chap/bond'
|
|
request_type = 'post'
|
|
params = {'target': target_name,
|
|
'user': chap_username,
|
|
'password': chap_password}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _add_host_to_target(self, host_ip, target_name):
|
|
"""Add the authority of host to target."""
|
|
method = 'block/host'
|
|
request_type = 'post'
|
|
params = {'name': target_name, 'hostIp': host_ip}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
@utils.retry(exceptions=exception.VolumeDriverException,
|
|
interval=1,
|
|
retries=3)
|
|
def _add_lun_to_target(self, target_name, volume):
|
|
"""Add volume to target."""
|
|
pool = volume_utils.extract_host(volume.host, level='pool')
|
|
volume_name = self._trans_name_down(volume.name)
|
|
|
|
method = 'block/lun'
|
|
request_type = 'post'
|
|
params = {'name': target_name,
|
|
'pool': pool,
|
|
'lvm': volume_name}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _delete_lun_from_target(self, target_name, lun_id):
|
|
"""Delete lun from target_name."""
|
|
method = 'block/lun?name=%s&id=%s&force=1' % (target_name, lun_id)
|
|
request_type = 'delete'
|
|
self._rest.send_rest_api(method=method, request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _get_lun_list(self, target_name):
|
|
"""Get all lun list of the target."""
|
|
method = 'block/lun?name=%s' % target_name
|
|
request_type = 'get'
|
|
return self._rest.send_rest_api(method=method,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _snapshot_lock_op(self, op, vol_name, snap_name, pool_name):
|
|
"""Lock or unlock a snapshot to protect the snapshot.
|
|
|
|
op is 'lock' for lock and 'unlock' for unlock
|
|
"""
|
|
method = 'snapshot/volume/%s' % op
|
|
request_type = 'post'
|
|
params = {'snapName': snap_name,
|
|
'volumeName': vol_name,
|
|
'poolName': pool_name}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _filling_volume(self, name, pool):
|
|
"""Filling a volume so that make it independently."""
|
|
method = 'block/lvm/filling'
|
|
request_type = 'post'
|
|
params = {'pool': pool, 'name': name}
|
|
self._rest.send_rest_api(method=method,
|
|
params=params,
|
|
request_type=request_type)
|
|
|
|
@utils.retry(exception.VolumeDriverException, interval=5, retries=36)
|
|
def _wait_volume_filled(self, name, pool):
|
|
"""Wait until the volume is filled."""
|
|
volumes = self._get_volumes(pool)
|
|
for vol in volumes:
|
|
if name == vol['name']:
|
|
if vol['lvmType'] == 1:
|
|
return
|
|
else:
|
|
break
|
|
msg = (_('Volume %s is not filled.') % name)
|
|
raise exception.VolumeDriverException(msg)
|
|
|
|
@utils.trace
|
|
def _check_volume(self, volume):
|
|
"""Check if the volume exists in the backend."""
|
|
pool = volume_utils.extract_host(volume.host, 'pool')
|
|
volume_name = self._trans_name_down(volume.name)
|
|
attempts = 3
|
|
while attempts > 0:
|
|
volumes = self._get_volumes(pool)
|
|
attempts -= 1
|
|
for vol in volumes:
|
|
if volume_name == vol.get('name'):
|
|
return True
|
|
eventlet.sleep(1)
|
|
return False
|
|
|
|
@utils.trace
|
|
def _get_volumes(self, pool):
|
|
"""Get all the volumes in the pool."""
|
|
method = 'block/lvm?pool=%s' % pool
|
|
request_type = 'get'
|
|
return self._rest.send_rest_api(method=method,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _get_cluster_status(self):
|
|
"""Get all nodes of the backend."""
|
|
method = 'cluster/node'
|
|
request_type = 'get'
|
|
return self._rest.send_rest_api(method=method,
|
|
request_type=request_type)
|
|
|
|
@utils.trace
|
|
def _get_lun_id(self, volume, target_name):
|
|
"""Get lun id of the voluem in a target."""
|
|
pool = volume_utils.extract_host(volume.host, level='pool')
|
|
volume_name = self._trans_name_down(volume.name)
|
|
|
|
lun_id = None
|
|
luns = self._get_lun_list(target_name)
|
|
for lun in luns:
|
|
mappinglvm = lun.get('mappingLvm')
|
|
lun_name = mappinglvm.replace(r'%s/' % pool, '')
|
|
if lun_name == volume_name:
|
|
lun_id = lun.get('id')
|
|
return lun_id
|
|
|
|
def _trans_name_down(self, name):
|
|
"""Legitimize the name.
|
|
|
|
Because AS13000 volume name is only allowed letters, numbers, and '_'.
|
|
"""
|
|
return name.replace('-', '_')
|
|
|
|
@utils.trace
|
|
def _unit_convert(self, capacity):
|
|
"""Convert all units to GB.
|
|
|
|
The capacity is a string in form like 100GB, 20TB, 100B,
|
|
this routine will convert to GB unit.
|
|
"""
|
|
capacity = capacity.upper()
|
|
try:
|
|
unit = re.findall(r'[A-Z]+', capacity)[0]
|
|
except BaseException:
|
|
unit = ''
|
|
capacity = float(capacity.replace(unit, ''))
|
|
|
|
size_gb = 0.0
|
|
|
|
if unit in ['B', '']:
|
|
size_gb = capacity / units.Gi
|
|
elif unit in ['K', 'KB']:
|
|
size_gb = capacity / units.Mi
|
|
elif unit in ['M', 'MB']:
|
|
size_gb = capacity / units.Ki
|
|
elif unit in ['G', 'GB']:
|
|
size_gb = capacity
|
|
elif unit in ['T', 'TB']:
|
|
size_gb = capacity * units.Ki
|
|
elif unit in ['P', 'PB']:
|
|
size_gb = capacity * units.Mi
|
|
elif unit in ['E', 'EB']:
|
|
size_gb = capacity * units.Gi
|
|
|
|
return float('%.0f' % size_gb)
|