cinder/cinder/volume/drivers/hds/hds.py

535 lines
22 KiB
Python

# Copyright (c) 2013 Hitachi Data Systems, Inc.
# Copyright (c) 2013 OpenStack Foundation
# 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.
#
"""
iSCSI Cinder Volume driver for Hitachi Unified Storage (HUS) platform.
"""
from xml.etree import ElementTree as ETree
from oslo_config import cfg
from oslo_utils import excutils
from cinder import exception
from cinder.i18n import _, _LE, _LI
from cinder.openstack.common import log as logging
from cinder import utils
from cinder.volume import driver
from cinder.volume.drivers.hds import hus_backend
HDS_VERSION = '1.0.2'
LOG = logging.getLogger(__name__)
HUS_OPTS = [
cfg.StrOpt('hds_cinder_config_file',
default='/opt/hds/hus/cinder_hus_conf.xml',
help='The configuration file for the Cinder HDS driver '
'for HUS'), ]
CONF = cfg.CONF
CONF.register_opts(HUS_OPTS)
HI_IQN = 'iqn.1994-04.jp.co.hitachi:' # fixed string, for now.
HUS_DEFAULT_CONFIG = {'hus_cmd': 'hus-cmd',
'lun_start': '0',
'lun_end': '8192'}
def factory_bend():
"""Factory over-ride in self-tests."""
return hus_backend.HusBackend()
def _loc_info(loc):
"""Parse info from location string."""
info = {}
tup = loc.split(',')
if len(tup) < 5:
info['id_lu'] = tup[0].split('.')
return info
info['id_lu'] = tup[2].split('.')
info['tgt'] = tup
return info
def _do_lu_range_check(start, end, maxlun):
"""Validate array allocation range."""
LOG.debug("Range: start LU: %(start)s, end LU: %(end)s"
% {'start': start,
'end': end})
if int(start) < 0:
msg = 'start LU limit too low: ' + start
raise exception.InvalidInput(reason=msg)
if int(start) >= int(maxlun):
msg = 'start LU limit high: ' + start + ' max: ' + maxlun
raise exception.InvalidInput(reason=msg)
if int(end) <= int(start):
msg = 'LU end limit too low: ' + end
raise exception.InvalidInput(reason=msg)
if int(end) > int(maxlun):
end = maxlun
LOG.debug("setting LU upper (end) limit to %s" % maxlun)
return (start, end)
def _xml_read(root, element, check=None):
"""Read an xml element."""
try:
val = root.findtext(element)
LOG.info(_LI("%(element)s: %(val)s")
% {'element': element,
'val': val})
if val:
return val.strip()
if check:
raise exception.ParameterNotFound(param=element)
return None
except ETree.ParseError:
if check:
with excutils.save_and_reraise_exception():
LOG.error(_LE("XML exception reading parameter: %s") % element)
else:
LOG.info(_LI("XML exception reading parameter: %s") % element)
return None
def _read_config(xml_config_file):
"""Read hds driver specific xml config file."""
try:
root = ETree.parse(xml_config_file).getroot()
except Exception:
raise exception.NotFound(message='config file not found: '
+ xml_config_file)
config = {}
arg_prereqs = ['mgmt_ip0', 'mgmt_ip1', 'username', 'password']
for req in arg_prereqs:
config[req] = _xml_read(root, req, 'check')
config['hdp'] = {}
config['services'] = {}
for svc in ['svc_0', 'svc_1', 'svc_2', 'svc_3']: # min one needed
if _xml_read(root, svc) is None:
continue
service = {}
service['label'] = svc
for arg in ['volume_type', 'hdp', 'iscsi_ip']: # none optional
service[arg] = _xml_read(root, svc + '/' + arg, 'check')
config['services'][service['volume_type']] = service
config['hdp'][service['hdp']] = service['hdp']
if config['services'].keys() is None: # at least one service required!
raise exception.ParameterNotFound(param="No service found")
config['snapshot_hdp'] = _xml_read(root, 'snapshot/hdp', 'check')
for arg in ['hus_cmd', 'lun_start', 'lun_end']: # optional
config[arg] = _xml_read(root, arg) or HUS_DEFAULT_CONFIG[arg]
return config
class HUSDriver(driver.ISCSIDriver):
"""HDS HUS volume driver."""
VERSION = HDS_VERSION
def _array_info_get(self):
"""Get array parameters."""
out = self.bend.get_version(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'])
inf = out.split()
return(inf[1], 'hus_' + inf[1], inf[6])
def _get_iscsi_info(self):
"""Validate array iscsi parameters."""
out = self.bend.get_iscsi_info(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'])
lines = out.split('\n')
conf = {} # dict based on iSCSI portal ip addresses
for line in lines:
if 'CTL' in line:
inf = line.split()
(ctl, port, ip, ipp) = (inf[1], inf[3], inf[5], inf[7])
conf[ip] = {}
conf[ip]['ctl'] = ctl
conf[ip]['port'] = port
conf[ip]['iscsi_port'] = ipp # HUS default: 3260
msg = _('portal: %(ip)s:%(ipp)s, CTL: %(ctl)s, port: %(port)s')
LOG.debug(msg
% {'ip': ip,
'ipp': ipp,
'ctl': ctl,
'port': port})
return conf
def _get_service(self, volume):
"""Get the available service parameters for a given volume type."""
label = None
if volume['volume_type']:
label = volume['volume_type']['name']
label = label or 'default'
if label in self.config['services'].keys():
svc = self.config['services'][label]
service = (svc['iscsi_ip'], svc['iscsi_port'], svc['ctl'],
svc['port'], svc['hdp']) # ip, ipp, ctl, port, hdp
else:
LOG.error(_LE("No configuration found for service: %s") % label)
raise exception.ParameterNotFound(param=label)
return service
def _get_stats(self):
"""Get HDP stats from HUS."""
total_cap = 0
total_used = 0
out = self.bend.get_hdp_info(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'])
for line in out.split('\n'):
if 'HDP' in line:
(hdp, size, _ign, used) = line.split()[1:5] # in MB
if hdp in self.config['hdp'].keys():
total_cap += int(size)
total_used += int(used)
hus_stat = {}
hus_stat['total_capacity_gb'] = int(total_cap / 1024) # in GB
hus_stat['free_capacity_gb'] = int((total_cap - total_used) / 1024)
be_name = self.configuration.safe_get('volume_backend_name')
hus_stat["volume_backend_name"] = be_name or 'HUSDriver'
hus_stat["vendor_name"] = 'HDS'
hus_stat["driver_version"] = HDS_VERSION
hus_stat["storage_protocol"] = 'iSCSI'
hus_stat['QoS_support'] = False
hus_stat['reserved_percentage'] = 0
return hus_stat
def _get_hdp_list(self):
"""Get HDPs from HUS."""
out = self.bend.get_hdp_info(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'])
hdp_list = []
for line in out.split('\n'):
if 'HDP' in line:
hdp_list.extend(line.split()[1:2])
return hdp_list
def _check_hdp_list(self):
"""Verify all HDPs specified in the configuration exist."""
hdpl = self._get_hdp_list()
lst = self.config['hdp'].keys()
lst.extend([self.config['snapshot_hdp'], ])
for hdp in lst:
if hdp not in hdpl:
LOG.error(_LE("HDP not found: %s") % hdp)
err = "HDP not found: " + hdp
raise exception.ParameterNotFound(param=err)
def _id_to_vol(self, idd):
"""Given the volume id, retrieve the volume object from database."""
vol = self.db.volume_get(self.context, idd)
return vol
def _update_vol_location(self, id, loc):
"""Update the provider location."""
update = {}
update['provider_location'] = loc
self.db.volume_update(self.context, id, update)
def __init__(self, *args, **kwargs):
"""Initialize, read different config parameters."""
super(HUSDriver, self).__init__(*args, **kwargs)
self.driver_stats = {}
self.context = {}
self.bend = factory_bend()
self.configuration.append_config_values(HUS_OPTS)
self.config = _read_config(self.configuration.hds_cinder_config_file)
(self.arid, self.hus_name, self.lumax) = self._array_info_get()
self._check_hdp_list()
start = self.config['lun_start']
end = self.config['lun_end']
maxlun = self.lumax
(self.start, self.end) = _do_lu_range_check(start, end, maxlun)
iscsi_info = self._get_iscsi_info()
for svc in self.config['services'].keys():
svc_ip = self.config['services'][svc]['iscsi_ip']
if svc_ip in iscsi_info.keys():
self.config['services'][svc]['port'] = (
iscsi_info[svc_ip]['port'])
self.config['services'][svc]['ctl'] = iscsi_info[svc_ip]['ctl']
self.config['services'][svc]['iscsi_port'] = (
iscsi_info[svc_ip]['iscsi_port'])
else: # config iscsi address not found on device!
LOG.error(_LE("iSCSI portal not found "
"for service: %s") % svc_ip)
raise exception.ParameterNotFound(param=svc_ip)
return
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
return
def do_setup(self, context):
"""do_setup.
Setup and verify HDS HUS storage connection. But moved it to
__init__ as (setup/errors) could became an infinite loop.
"""
self.context = context
def ensure_export(self, context, volume):
return
def create_export(self, context, volume):
"""Create an export. Moved to initialize_connection."""
return
@utils.synchronized('hds_hus', external=True)
def create_volume(self, volume):
"""Create a LU on HUS."""
service = self._get_service(volume)
(_ip, _ipp, _ctl, _port, hdp) = service
out = self.bend.create_lu(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
self.arid, hdp, self.start, self.end,
'%s' % (int(volume['size']) * 1024))
lun = self.arid + '.' + out.split()[1]
sz = int(out.split()[5])
LOG.debug("LUN %(lun)s of size %(sz)s MB is created."
% {'lun': lun,
'sz': sz})
return {'provider_location': lun}
@utils.synchronized('hds_hus', external=True)
def create_cloned_volume(self, dst, src):
"""Create a clone of a volume."""
if src['size'] != dst['size']:
msg = 'clone volume size mismatch'
raise exception.VolumeBackendAPIException(data=msg)
service = self._get_service(dst)
(_ip, _ipp, _ctl, _port, hdp) = service
size = int(src['size']) * 1024
source_vol = self._id_to_vol(src['id'])
(arid, slun) = _loc_info(source_vol['provider_location'])['id_lu']
out = self.bend.create_dup(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, slun,
hdp,
self.start, self.end,
'%s' % (size))
lun = self.arid + '.' + out.split()[1]
size = int(out.split()[5])
LOG.debug("LUN %(lun)s of size %(size)s MB is cloned."
% {'lun': lun,
'size': size})
return {'provider_location': lun}
@utils.synchronized('hds_hus', external=True)
def extend_volume(self, volume, new_size):
"""Extend an existing volume."""
(arid, lun) = _loc_info(volume['provider_location'])['id_lu']
self.bend.extend_vol(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun,
'%s' % (new_size * 1024))
LOG.debug("LUN %(lun)s extended to %(size)s GB."
% {'lun': lun,
'size': new_size})
@utils.synchronized('hds_hus', external=True)
def delete_volume(self, volume):
"""Delete an LU on HUS."""
prov_loc = volume['provider_location']
if prov_loc is None:
return
info = _loc_info(prov_loc)
(arid, lun) = info['id_lu']
if 'tgt' in info.keys(): # connected?
(_portal, iqn, _loc, ctl, port) = info['tgt']
self.bend.del_iscsi_conn(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun, ctl, port, iqn,
'')
name = self.hus_name
LOG.debug("delete lun %(lun)s on %(name)s"
% {'lun': lun,
'name': name})
self.bend.delete_lu(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun)
def remove_export(self, context, volume):
"""Disconnect a volume from an attached instance."""
return
@utils.synchronized('hds_hus', external=True)
def initialize_connection(self, volume, connector):
"""Map the created volume to connector['initiator']."""
service = self._get_service(volume)
(ip, ipp, ctl, port, _hdp) = service
info = _loc_info(volume['provider_location'])
if 'tgt' in info.keys(): # spurious repeat connection
return
(arid, lun) = info['id_lu']
loc = arid + '.' + lun
iqn = HI_IQN + connector['host']
out = self.bend.add_iscsi_conn(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun, ctl, port, iqn,
connector['initiator'])
hus_portal = ip + ':' + ipp
tgt = hus_portal + ',' + iqn + ',' + loc + ',' + ctl + ',' + port
properties = {}
hlun = out.split()[1]
properties['provider_location'] = tgt
self._update_vol_location(volume['id'], tgt)
properties['target_discovered'] = False
properties['target_portal'] = hus_portal
properties['target_iqn'] = iqn
properties['target_lun'] = hlun
properties['volume_id'] = volume['id']
return {'driver_volume_type': 'iscsi', 'data': properties}
@utils.synchronized('hds_hus', external=True)
def terminate_connection(self, volume, connector, **kwargs):
"""Terminate a connection to a volume."""
info = _loc_info(volume['provider_location'])
if 'tgt' not in info.keys(): # spurious disconnection
return
(arid, lun) = info['id_lu']
(_portal, iqn, loc, ctl, port) = info['tgt']
self.bend.del_iscsi_conn(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun, ctl, port, iqn,
connector['initiator'])
self._update_vol_location(volume['id'], loc)
return {'provider_location': loc}
@utils.synchronized('hds_hus', external=True)
def create_volume_from_snapshot(self, volume, snapshot):
"""Create a volume from a snapshot."""
size = int(snapshot['volume_size']) * 1024
(arid, slun) = _loc_info(snapshot['provider_location'])['id_lu']
service = self._get_service(volume)
(_ip, _ipp, _ctl, _port, hdp) = service
out = self.bend.create_dup(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, slun, hdp,
self.start, self.end,
'%s' % (size))
lun = self.arid + '.' + out.split()[1]
sz = int(out.split()[5])
LOG.debug("LUN %(lun)s of size %(sz)s MB is created from snapshot."
% {'lun': lun,
'sz': sz})
return {'provider_location': lun}
@utils.synchronized('hds_hus', external=True)
def create_snapshot(self, snapshot):
"""Create a snapshot."""
source_vol = self._id_to_vol(snapshot['volume_id'])
size = int(snapshot['volume_size']) * 1024
(arid, slun) = _loc_info(source_vol['provider_location'])['id_lu']
out = self.bend.create_dup(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, slun,
self.config['snapshot_hdp'],
self.start, self.end,
'%s' % (size))
lun = self.arid + '.' + out.split()[1]
size = int(out.split()[5])
LOG.debug("LUN %(lun)s of size %(size)s MB is created as snapshot."
% {'lun': lun,
'size': size})
return {'provider_location': lun}
@utils.synchronized('hds_hus', external=True)
def delete_snapshot(self, snapshot):
"""Delete a snapshot."""
loc = snapshot['provider_location']
if loc is None: # to take care of spurious input
return # which could cause exception.
(arid, lun) = loc.split('.')
self.bend.delete_lu(self.config['hus_cmd'],
HDS_VERSION,
self.config['mgmt_ip0'],
self.config['mgmt_ip1'],
self.config['username'],
self.config['password'],
arid, lun)
LOG.debug("LUN %s is deleted." % lun)
return
@utils.synchronized('hds_hus', external=True)
def get_volume_stats(self, refresh=False):
"""Get volume stats. If 'refresh', run update the stats first."""
if refresh:
self.driver_stats = self._get_stats()
return self.driver_stats