From: Lakhinder Walia Date: Tue, 7 May 2013 23:05:46 +0000 (-0700) Subject: HDS Cinder Driver. Rev #1 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=da00b6bcca92c5ca773ee5ef779cd59d641214c0;p=openstack-build%2Fcinder-build.git 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 --- diff --git a/cinder/exception.py b/cinder/exception.py index 4c840ecb7..b3ae8ccb7 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -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") diff --git a/cinder/tests/test_hds.py b/cinder/tests/test_hds.py new file mode 100644 index 000000000..d9cea2662 --- /dev/null +++ b/cinder/tests/test_hds.py @@ -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 = """ + + 172.17.44.16 + 172.17.44.17 + system + manager + + default + 172.17.39.132 + 9 + + + silver + 172.17.39.133 + 9 + + + gold + 172.17.39.134 + 9 + + + platinum + 172.17.39.135 + 9 + + + 9 + + + 3300 + + +""" + + +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) diff --git a/cinder/volume/drivers/hds/__init__.py b/cinder/volume/drivers/hds/__init__.py new file mode 100644 index 000000000..8998fc930 --- /dev/null +++ b/cinder/volume/drivers/hds/__init__.py @@ -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. +# diff --git a/cinder/volume/drivers/hds/hds.py b/cinder/volume/drivers/hds/hds.py new file mode 100644 index 000000000..45c944bbd --- /dev/null +++ b/cinder/volume/drivers/hds/hds.py @@ -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 diff --git a/cinder/volume/drivers/hds/hus_backend.py b/cinder/volume/drivers/hds/hus_backend.py new file mode 100644 index 000000000..52d6882b8 --- /dev/null +++ b/cinder/volume/drivers/hds/hus_backend.py @@ -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 diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 055d698f7..4e8b127d6 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -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 diff --git a/etc/cinder/rootwrap.conf b/etc/cinder/rootwrap.conf index dfa8a99cc..ff58e1b35 100644 --- a/etc/cinder/rootwrap.conf +++ b/etc/cinder/rootwrap.conf @@ -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 diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 9aca2742b..11224ceda 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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