HDS Cinder Driver. Rev #1
blueprint hds-hus-iscsi-cinder-driver This is the first rev of Hitachi Data Systems Cinder iSCSI driver. This driver works with HUS (df850) array. This driver contains all the base-line features specified for Havana release. Amended into this submission are changes from code-reviews. Docimpact: Bug #1180648 Change-Id: Ia27d076443b10da2c653456f9292dd192362b853
This commit is contained in:
parent
77a77456af
commit
da00b6bcca
@ -426,6 +426,10 @@ class ConfigNotFound(NotFound):
|
||||
message = _("Could not find config at %(path)s")
|
||||
|
||||
|
||||
class ParameterNotFound(NotFound):
|
||||
message = _("Could not find parameter %(param)s")
|
||||
|
||||
|
||||
class PasteAppNotFound(NotFound):
|
||||
message = _("Could not load paste app '%(name)s' from %(path)s")
|
||||
|
||||
|
254
cinder/tests/test_hds.py
Normal file
254
cinder/tests/test_hds.py
Normal file
@ -0,0 +1,254 @@
|
||||
# Copyright (c) 2013 Hitachi Data Systems, Inc.
|
||||
# Copyright (c) 2013 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Self test for Hitachi Unified Storage (HUS) platform.
|
||||
"""
|
||||
|
||||
import mox
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.hds import hds
|
||||
|
||||
|
||||
CONF = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<config>
|
||||
<mgmt_ip0>172.17.44.16</mgmt_ip0>
|
||||
<mgmt_ip1>172.17.44.17</mgmt_ip1>
|
||||
<username>system</username>
|
||||
<password>manager</password>
|
||||
<svc_0>
|
||||
<volume_type>default</volume_type>
|
||||
<iscsi_ip>172.17.39.132</iscsi_ip>
|
||||
<hdp>9</hdp>
|
||||
</svc_0>
|
||||
<svc_1>
|
||||
<volume_type>silver</volume_type>
|
||||
<iscsi_ip>172.17.39.133</iscsi_ip>
|
||||
<hdp>9</hdp>
|
||||
</svc_1>
|
||||
<svc_2>
|
||||
<volume_type>gold</volume_type>
|
||||
<iscsi_ip>172.17.39.134</iscsi_ip>
|
||||
<hdp>9</hdp>
|
||||
</svc_2>
|
||||
<svc_3>
|
||||
<volume_type>platinum</volume_type>
|
||||
<iscsi_ip>172.17.39.135</iscsi_ip>
|
||||
<hdp>9</hdp>
|
||||
</svc_3>
|
||||
<snapshot>
|
||||
<hdp>9</hdp>
|
||||
</snapshot>
|
||||
<lun_start>
|
||||
3300
|
||||
</lun_start>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
class SimulatedHusBackend:
|
||||
"""Simulation Back end. Talks to HUS."""
|
||||
|
||||
alloc_lun = [] # allocated LUs
|
||||
connections = [] # iSCSI connections
|
||||
|
||||
def __init__(self):
|
||||
self.start_lun = 0
|
||||
|
||||
def get_version(self, cmd, ip0, ip1, user, pw):
|
||||
out = ("Array_ID: 92210013 (HUS130) version: 0920/B-S LU: 4096"
|
||||
" RG: 75 RG_LU: 1024 Utility_version: 1.0.0")
|
||||
return out
|
||||
|
||||
def get_iscsi_info(self, cmd, ip0, ip1, user, pw):
|
||||
out = """CTL: 0 Port: 4 IP: 172.17.39.132 Port: 3260 Link: Up
|
||||
CTL: 0 Port: 5 IP: 172.17.39.133 Port: 3260 Link: Up
|
||||
CTL: 1 Port: 4 IP: 172.17.39.134 Port: 3260 Link: Up
|
||||
CTL: 1 Port: 5 IP: 172.17.39.135 Port: 3260 Link: Up"""
|
||||
return out
|
||||
|
||||
def get_hdp_info(self, cmd, ip0, ip1, user, pw):
|
||||
out = """HDP: 2 272384 MB 33792 MB 12 % LUs: 70 Normal Normal
|
||||
HDP: 9 546816 MB 73728 MB 13 % LUs: 194 Normal Normal"""
|
||||
return out
|
||||
|
||||
def create_lu(self, cmd, ip0, ip1, user, pw, id, hdp, start, end, size):
|
||||
if self.start_lun < int(start): # initialize first time
|
||||
self.start_lun = int(start)
|
||||
out = ("LUN: %d HDP: 9 size: %s MB, is successfully created" %
|
||||
(self.start_lun, size))
|
||||
self.alloc_lun.append(str(self.start_lun))
|
||||
self.start_lun += 1
|
||||
return out
|
||||
|
||||
def delete_lu(self, cmd, ip0, ip1, user, pw, id, lun):
|
||||
out = ""
|
||||
if lun in self.alloc_lun:
|
||||
out = "LUN: %s is successfully deleted" % (lun)
|
||||
self.alloc_lun.remove(lun)
|
||||
return out
|
||||
|
||||
def create_dup(self, cmd, ip0, ip1, user, pw, id, src_lun,
|
||||
hdp, start, end, size):
|
||||
out = ("LUN: %s HDP: 9 size: %s MB, is successfully created" %
|
||||
(self.start_lun, size))
|
||||
self.alloc_lun.append(str(self.start_lun))
|
||||
self.start_lun += 1
|
||||
return out
|
||||
|
||||
def add_iscsi_conn(self, cmd, ip0, ip1, user, pw, id, lun, ctl, port, iqn,
|
||||
tgt_alias, initiator, init_alias):
|
||||
conn = (initiator, iqn, ctl, port)
|
||||
out = ("iSCSI Initiator: %s, index: 26, and Target: %s, index 8 is \
|
||||
successfully paired @ CTL: %s, Port: %s" % conn)
|
||||
SimulatedHusBackend.connections.append(conn)
|
||||
return out
|
||||
|
||||
def del_iscsi_conn(self, cmd, ip0, ip1, user, pw, id, lun, ctl, port, iqn,
|
||||
initiator, force):
|
||||
conn = (initiator, iqn, ctl, port)
|
||||
out = ("iSCSI Initiator: %s, index: 26, and Target: %s, index 8 is \
|
||||
successfully un-paired @ CTL: %s, Port: %s" % conn)
|
||||
if conn in SimulatedHusBackend.connections:
|
||||
SimulatedHusBackend.connections.remove(conn)
|
||||
return out
|
||||
|
||||
|
||||
# The following information is passed on to tests, when creating a volume
|
||||
|
||||
_VOLUME = {'volume_id': '1234567890', 'size': 128,
|
||||
'volume_type': None, 'provider_location': None, 'id': 'abcdefg'}
|
||||
|
||||
|
||||
class HUSiSCSIDriverTest(test.TestCase):
|
||||
"""Test HUS iSCSI volume driver."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HUSiSCSIDriverTest, self).__init__(*args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
super(HUSiSCSIDriverTest, self).setUp()
|
||||
(handle, self.config_file) = tempfile.mkstemp('.xml')
|
||||
os.write(handle, CONF)
|
||||
os.close(handle)
|
||||
SimulatedHusBackend.alloc_lun = []
|
||||
SimulatedHusBackend.connections = []
|
||||
self.mox = mox.Mox()
|
||||
self.mox.StubOutWithMock(hds, 'factory_bend')
|
||||
hds.factory_bend().AndReturn(SimulatedHusBackend())
|
||||
self.mox.ReplayAll()
|
||||
self.configuration = mox.MockObject(conf.Configuration)
|
||||
self.configuration.hds_cinder_config_file = self.config_file
|
||||
self.driver = hds.HUSDriver(configuration=self.configuration)
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(self.config_file)
|
||||
self.mox.UnsetStubs()
|
||||
super(HUSiSCSIDriverTest, self).tearDown()
|
||||
|
||||
def test_get_volume_stats(self):
|
||||
stats = self.driver.get_volume_stats(True)
|
||||
self.assertEqual(stats["vendor_name"], "HDS")
|
||||
self.assertEqual(stats["storage_protocol"], "iSCSI")
|
||||
self.assertTrue(stats["total_capacity_gb"] > 0)
|
||||
|
||||
def test_create_volume(self):
|
||||
loc = self.driver.create_volume(_VOLUME)
|
||||
self.assertNotEqual(loc, None)
|
||||
vol = _VOLUME.copy()
|
||||
vol['provider_location'] = loc['provider_location']
|
||||
self.assertNotEqual(loc['provider_location'], None)
|
||||
return vol
|
||||
|
||||
def test_delete_volume(self):
|
||||
"""Delete a volume (test).
|
||||
|
||||
Note: this API call should not expect any exception:
|
||||
This driver will silently accept a delete request, because
|
||||
the DB can be out of sync, and Cinder manager will keep trying
|
||||
to delete, even though the volume has been wiped out of the
|
||||
Array. We don't want to have a dangling volume entry in the
|
||||
customer dashboard.
|
||||
"""
|
||||
vol = self.test_create_volume()
|
||||
self.assertTrue(SimulatedHusBackend.alloc_lun)
|
||||
num_luns_before = len(SimulatedHusBackend.alloc_lun)
|
||||
self.driver.delete_volume(vol)
|
||||
num_luns_after = len(SimulatedHusBackend.alloc_lun)
|
||||
self.assertTrue(num_luns_before > num_luns_after)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
vol = self.test_create_volume()
|
||||
self.mox.StubOutWithMock(self.driver, '_id_to_vol')
|
||||
self.driver._id_to_vol(vol['volume_id']).AndReturn(vol)
|
||||
self.mox.ReplayAll()
|
||||
svol = vol.copy()
|
||||
svol['volume_size'] = svol['size']
|
||||
loc = self.driver.create_snapshot(svol)
|
||||
self.assertNotEqual(loc, None)
|
||||
svol['provider_location'] = loc['provider_location']
|
||||
return svol
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
"""Delete a snapshot (test).
|
||||
|
||||
Note: this API call should not expect any exception:
|
||||
This driver will silently accept a delete request, because
|
||||
the DB can be out of sync, and Cinder manager will keep trying
|
||||
to delete, even though the snapshot has been wiped out of the
|
||||
Array. We don't want to have a dangling snapshot entry in the
|
||||
customer dashboard.
|
||||
"""
|
||||
svol = self.test_create_snapshot()
|
||||
num_luns_before = len(SimulatedHusBackend.alloc_lun)
|
||||
self.driver.delete_snapshot(svol)
|
||||
num_luns_after = len(SimulatedHusBackend.alloc_lun)
|
||||
self.assertTrue(num_luns_before > num_luns_after)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
svol = self.test_create_snapshot()
|
||||
vol = self.driver.create_volume_from_snapshot(_VOLUME, svol)
|
||||
self.assertNotEqual(vol, None)
|
||||
return vol
|
||||
|
||||
def test_initialize_connection(self):
|
||||
connector = {}
|
||||
connector['initiator'] = 'iqn.1993-08.org.debian:01:11f90746eb2'
|
||||
connector['host'] = 'dut_1.lab.hds.com'
|
||||
vol = self.test_create_volume()
|
||||
conn = self.driver.initialize_connection(vol, connector)
|
||||
self.assertTrue('hitachi' in conn['data']['target_iqn'])
|
||||
self.assertTrue('3260' in conn['data']['target_portal'])
|
||||
return (vol, connector)
|
||||
|
||||
def test_terminate_connection(self):
|
||||
"""Terminate a connection (test).
|
||||
|
||||
Note: this API call should not expect any exception:
|
||||
This driver will silently accept a terminate_connection request
|
||||
because an error/exception return will only jeopardize the
|
||||
connection tear down at a host.
|
||||
"""
|
||||
(vol, conn) = self.test_initialize_connection()
|
||||
num_conn_before = len(SimulatedHusBackend.connections)
|
||||
self.driver.terminate_connection(vol, conn)
|
||||
num_conn_after = len(SimulatedHusBackend.connections)
|
||||
self.assertTrue(num_conn_before > num_conn_after)
|
16
cinder/volume/drivers/hds/__init__.py
Normal file
16
cinder/volume/drivers/hds/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2013 Hitachi Data Systems, Inc.
|
||||
# Copyright (c) 2013 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
454
cinder/volume/drivers/hds/hds.py
Normal file
454
cinder/volume/drivers/hds/hds.py
Normal file
@ -0,0 +1,454 @@
|
||||
# Copyright (c) 2013 Hitachi Data Systems, Inc.
|
||||
# Copyright (c) 2013 OpenStack LLC.
|
||||
# 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 oslo.config import cfg
|
||||
from xml.etree import ElementTree as ETree
|
||||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
from cinder.volume import driver
|
||||
|
||||
from cinder.volume.drivers.hds.hus_backend import HusBackend
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
HUS_OPTS = [
|
||||
cfg.StrOpt('hds_cinder_config_file',
|
||||
default='/opt/hds/hus/cinder_hus_conf.xml',
|
||||
help='configuration file for HDS cinder plugin for HUS'), ]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.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 HusBackend()
|
||||
|
||||
|
||||
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 uppper (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(_("%(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 as e:
|
||||
if check:
|
||||
LOG.error(_("XML exception reading parameter: %s") % element)
|
||||
raise e
|
||||
else:
|
||||
LOG.info(_("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."""
|
||||
|
||||
def _array_info_get(self):
|
||||
"""Get array parameters."""
|
||||
out = self.bend.get_version(self.config['hus_cmd'],
|
||||
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'],
|
||||
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(_("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'],
|
||||
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"] = '1.0'
|
||||
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'],
|
||||
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(_("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 __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(_("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'],
|
||||
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 delete_volume(self, volume):
|
||||
"""Delete an LU on HUS."""
|
||||
loc = volume['provider_location']
|
||||
if loc is None: # to take care of spurious input
|
||||
return # which could cause exception.
|
||||
(arid, lun) = loc.split('.')
|
||||
myid = self.arid
|
||||
if arid != myid:
|
||||
LOG.error(_("Array Mismatch %(myid)s vs %(arid)s")
|
||||
% {'myid': myid,
|
||||
'arid': arid})
|
||||
msg = 'Array id mismatch in volume delete'
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
name = self.hus_name
|
||||
LOG.debug(_("delete lun %(lun)s on %(name)s")
|
||||
% {'lun': lun,
|
||||
'name': name})
|
||||
_out = self.bend.delete_lu(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.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
|
||||
loc = volume['provider_location']
|
||||
(_array_id, lun) = loc.split('.')
|
||||
iqn = HI_IQN + loc
|
||||
tgt_alias = 'cinder.' + loc
|
||||
init_alias = connector['host'][:(31 - len(loc))] + '.' + loc
|
||||
_out = self.bend.add_iscsi_conn(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.arid, lun, ctl, port, iqn,
|
||||
tgt_alias, connector['initiator'],
|
||||
init_alias)
|
||||
hus_portal = ip + ':' + ipp
|
||||
tgt = hus_portal + ',' + iqn + ',' + loc + ',' + ctl + ',' + port
|
||||
properties = {}
|
||||
properties['provider_location'] = tgt
|
||||
properties['target_discovered'] = False
|
||||
properties['target_portal'] = hus_portal
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = 0 # for now !
|
||||
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."""
|
||||
loc = volume['provider_location']
|
||||
(_array_id, lun) = loc.split('.')
|
||||
iqn = HI_IQN + loc
|
||||
service = self._get_service(volume)
|
||||
(_ip, _ipp, ctl, port, _hdp) = service
|
||||
_out = self.bend.del_iscsi_conn(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.arid, lun, ctl, port, iqn,
|
||||
connector['initiator'], 1)
|
||||
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) = snapshot['provider_location'].split('.')
|
||||
service = self._get_service(volume)
|
||||
(_ip, _ipp, _ctl, _port, hdp) = service
|
||||
out = self.bend.create_dup(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.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) = source_vol['provider_location'].split('.')
|
||||
out = self.bend.create_dup(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.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.")
|
||||
% {'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('.')
|
||||
myid = self.arid
|
||||
if arid != myid:
|
||||
LOG.error(_('Array mismatch %(myid)s vs %(arid)s')
|
||||
% {'myid': myid,
|
||||
'arid': arid})
|
||||
msg = 'Array id mismatch in delete snapshot'
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
_out = self.bend.delete_lu(self.config['hus_cmd'],
|
||||
self.config['mgmt_ip0'],
|
||||
self.config['mgmt_ip1'],
|
||||
self.config['username'],
|
||||
self.config['password'],
|
||||
self.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
|
148
cinder/volume/drivers/hds/hus_backend.py
Normal file
148
cinder/volume/drivers/hds/hus_backend.py
Normal file
@ -0,0 +1,148 @@
|
||||
# Copyright (c) 2013 Hitachi Data Systems, Inc.
|
||||
# Copyright (c) 2013 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Hitachi Unified Storage (HUS) platform. Backend operations.
|
||||
"""
|
||||
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
|
||||
LOG = logging.getLogger("cinder.volume.driver")
|
||||
|
||||
|
||||
class HusBackend:
|
||||
"""Back end. Talks to HUS."""
|
||||
def get_version(self, cmd, ip0, ip1, user, pw):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--version', '1',
|
||||
run_as_root=True,
|
||||
check_exit_code=True)
|
||||
LOG.debug('get_version: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def get_iscsi_info(self, cmd, ip0, ip1, user, pw):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--iscsi', '1',
|
||||
check_exit_code=True)
|
||||
LOG.debug('get_iscsi_info: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def get_hdp_info(self, cmd, ip0, ip1, user, pw):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--hdp', '1',
|
||||
check_exit_code=True)
|
||||
LOG.debug('get_hdp_info: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def create_lu(self, cmd, ip0, ip1, user, pw, id, hdp, start, end, size):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--create_lun', '1',
|
||||
'--array_id', id,
|
||||
'--hdp', hdp,
|
||||
'--start', start,
|
||||
'--end', end,
|
||||
'--size', size,
|
||||
check_exit_code=True)
|
||||
LOG.debug('create_lu: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def delete_lu(self, cmd, ip0, ip1, user, pw, id, lun):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--delete_lun', '1',
|
||||
'--array_id', id,
|
||||
'--lun', lun,
|
||||
check_exit_code=True)
|
||||
LOG.debug('delete_lu: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def create_dup(self, cmd, ip0, ip1, user, pw, id, src_lun,
|
||||
hdp, start, end, size):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--create_dup', '1',
|
||||
'--array_id', id,
|
||||
'--pvol', src_lun,
|
||||
'--hdp', hdp,
|
||||
'--start', start,
|
||||
'--end', end,
|
||||
'--size', size,
|
||||
check_exit_code=True)
|
||||
LOG.debug('create_dup: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def add_iscsi_conn(self, cmd, ip0, ip1, user, pw, id, lun, ctl, port, iqn,
|
||||
tgt_alias, initiator, init_alias):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--add_iscsi_connection', '1',
|
||||
'--array_id', id,
|
||||
'--lun', lun,
|
||||
'--ctl', ctl,
|
||||
'--port', port,
|
||||
'--target', iqn,
|
||||
'--target_alias', tgt_alias,
|
||||
'--initiator', initiator,
|
||||
'--initiator_alias', init_alias,
|
||||
check_exit_code=True)
|
||||
LOG.debug('add_iscsi_conn: ' + out + ' -- ' + err)
|
||||
return out
|
||||
|
||||
def del_iscsi_conn(self, cmd, ip0, ip1, user, pw, id, lun, ctl, port, iqn,
|
||||
initiator, force):
|
||||
out, err = utils.execute(cmd,
|
||||
'--ip0', ip0,
|
||||
'--ip1', ip1,
|
||||
'--user', user,
|
||||
'--password', pw,
|
||||
'--delete_iscsi_connection', '1',
|
||||
'--array_id', id,
|
||||
'--lun', lun,
|
||||
'--ctl', ctl,
|
||||
'--port', port,
|
||||
'--target', iqn,
|
||||
'--initiator', initiator,
|
||||
'--force', force,
|
||||
check_exit_code=True)
|
||||
LOG.debug('del_iscsi_conn: ' + out + ' -- ' + err)
|
||||
return out
|
@ -1314,6 +1314,14 @@
|
||||
#zadara_vpsa_allow_nonexistent_delete=true
|
||||
|
||||
|
||||
#
|
||||
# options for cinder.volumes.drivers.hds.hds.HUSDriver
|
||||
#
|
||||
|
||||
# default configuration file location/name is (string) :
|
||||
# hds_cinder_config_file=/opt/hds/hus/cinder_hds_conf.xml
|
||||
|
||||
|
||||
#
|
||||
# Options defined in cinder.volume.iscsi
|
||||
#
|
||||
@ -1344,5 +1352,4 @@
|
||||
# Driver to use for volume creation (string value)
|
||||
#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
|
||||
|
||||
|
||||
# Total option count: 299
|
||||
# Total option count: 300
|
||||
|
@ -10,7 +10,7 @@ filters_path=/etc/cinder/rootwrap.d,/usr/share/cinder/rootwrap
|
||||
# explicitely specify a full path (separated by ',')
|
||||
# If not specified, defaults to system PATH environment variable.
|
||||
# These directories MUST all be only writeable by root !
|
||||
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
|
||||
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin
|
||||
|
||||
# Enable logging to syslog
|
||||
# Default value is False
|
||||
|
@ -54,6 +54,5 @@ chmod: CommandFilter, chmod, root
|
||||
rm: CommandFilter, rm, root
|
||||
lvs: CommandFilter, lvs, root
|
||||
|
||||
# cinder/volume/scality.py
|
||||
mount: CommandFilter, mount, root
|
||||
dd: CommandFilter, dd, root
|
||||
# cinder/volumes/drivers/hds/hds.py:
|
||||
hus_cmd: CommandFilter, hus_cmd, root
|
||||
|
Loading…
x
Reference in New Issue
Block a user