From: zhangchao010 Date: Thu, 29 Aug 2013 02:33:52 +0000 (+0800) Subject: Refactor Huawei iSCSI driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=17c4fd71038b0603bf4f0035e46fc0ff1bc517f7;p=openstack-build%2Fcinder-build.git Refactor Huawei iSCSI driver We plan to refactor Huawei iSCSI drivers and add Huawei FC drivers. For that's a huge change, we break the codes into three patches: 1.Refactor T iSCSI driver 2.Refactor Dorado iSCSI driver 3.Add FC drivers for both T and Dorado arrays. This is the first patch, changes as follows: 1.Define a common class for both FC and iSCSI drivers, and also provide a unified class HuaweiVolumeDriver for users. The unified driver will call HuaweiTISCSIDriver according to users' configuration. The HuaweiTISCSIDriver is a subclass of driver.ISCSIDriver, so it could get good inheritance. 2.Support volume type. 3.Refactor unit test to make it more logic clear and add more test cases to get higher coverage rate. Change-Id: I79b7bac7f38f2dcbb22c5db6207d8b55906fdad1 --- diff --git a/cinder/tests/test_huawei.py b/cinder/tests/test_huawei.py deleted file mode 100644 index ab10819d3..000000000 --- a/cinder/tests/test_huawei.py +++ /dev/null @@ -1,862 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 Huawei Technologies Co., Ltd. -# Copyright (c) 2012 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. -""" -Tests for HUAWEI volume driver. -""" -import mox -import os -import shutil -import tempfile -from xml.dom.minidom import Document -from xml.etree import ElementTree as ET - -from cinder import exception -from cinder.openstack.common import log as logging -from cinder import test -from cinder.volume import configuration as conf -from cinder.volume.drivers.huawei import huawei_iscsi - -LOG = logging.getLogger(__name__) - -LUNInfo = {'ID': None, - 'Name': None, - 'Size': None, - 'LUN WWN': None, - 'Status': None, - 'Visible Capacity': None, - 'Stripe Unit Size': None, - 'Disk Pool ID': None, - 'Format Progress': None, - 'Cache Prefetch Strategy': None, - 'LUNType': None, - 'Cache Write Strategy': None, - 'Running Cache Write Strategy': None, - 'Consumed Capacity': None, - 'Pool ID': None, - 'SnapShot ID': None, - 'LunCopy ID': None, - 'Whether Private LUN': None, - 'Remote Replication ID': None, - 'Split mirror ID': None, - 'Owner Controller': None, - 'Worker Controller': None, - 'RAID Group ID': None} - -LUNInfoCopy = {'ID': None, - 'Name': None, - 'Size': None, - 'LUN WWN': None, - 'Status': None, - 'Visible Capacity': None, - 'Stripe Unit Size': None, - 'Disk Pool ID': None, - 'Format Progress': None, - 'Cache Prefetch Strategy': None, - 'LUNType': None, - 'Cache Write Strategy': None, - 'Running Cache Write Strategy': None, - 'Consumed Capacity': None, - 'Pool ID': None, - 'SnapShot ID': None, - 'LunCopy ID': None, - 'Whether Private LUN': None, - 'Remote Replication ID': None, - 'Split mirror ID': None, - 'Owner Controller': None, - 'Worker Controller': None, - 'RAID Group ID': None} - -SnapshotInfo = {'Source LUN ID': None, - 'Source LUN Name': None, - 'ID': None, - 'Name': None, - 'Type': 'Public', - 'Status': None, - 'Time Stamp': '2013-01-15 14:00:00', - 'Rollback Start Time': '--', - 'Rollback End Time': '--', - 'Rollback Speed': '--', - 'Rollback Progress': '--'} - -MapInfo = {'Host Group ID': None, - 'Host Group Name': None, - 'File Engine Cluster': None, - 'Host ID': None, - 'Host Name': None, - 'Os Type': None, - 'INI Port ID': None, - 'INI Port Name': None, - 'INI Port Info': None, - 'Port Type': None, - 'Link Status': None, - 'LUN WWN': None, - 'DEV LUN ID': None, - 'Host LUN ID': None} - -HostPort = {'ID': None, - 'Name': None, - 'Info': None} - -LUNCopy = {'Name': None, - 'ID': None, - 'Type': None, - 'State': None, - 'Status': 'Disable'} - -FakeVolume = {'name': 'Volume-lele34fe-223f-dd33-4423-asdfghjklqwe', - 'size': '2', - 'id': '0', - 'wwn': '630303710030303701094b2b00000031', - 'provider_auth': None} - -FakeVolumeCopy = {'name': 'Volume-jeje34fe-223f-dd33-4423-asdfghjklqwg', - 'size': '3', - 'ID': '1', - 'wwn': '630303710030303701094b2b0000003'} - -FakeLUNCopy = {'ID': '1', - 'Type': 'FULL', - 'State': 'Created', - 'Status': 'Normal'} - -FakeSnapshot = {'name': 'keke34fe-223f-dd33-4423-asdfghjklqwf', - 'volume_name': 'Volume-lele34fe-223f-dd33-4423-asdfghjklqwe', - 'id': '3'} - -FakePoolInfo = {'ID': '2', - 'Level': 'RAID6', - 'Status': 'Normal', - 'Free Capacity': '10240', - 'Disk List': '0,1;0,2;0,3;0,4;0,5;0,6', - 'Name': 'RAID_001', - 'Type': 'Thick'} - -FakeConfInfo = {'HostGroup': 'HostGroup_OpenStack', - 'HostnamePrefix': 'Host_', - 'DefaultTargetIP': '192.168.100.1', - 'TargetIQN': 'iqn.2006-08.com.huawei:oceanspace:2103037:', - 'TargetIQN-T': 'iqn.2006-08.com.huawei:oceanspace:2103037::' - '20001:192.168.100.2', - 'TargetIQN-Dorado5100': 'iqn.2006-08.com.huawei:oceanspace:' - '2103037::192.168.100.2', - 'TargetIQN-Dorado2100G2': 'iqn.2006-08.com.huawei:oceanspace:' - '2103037::192.168.100.2-20001', - 'Initiator Name': 'iqn.1993-08.debian:01:ec2bff7ac3a3', - 'Initiator TargetIP': '192.168.100.2'} - -FakeConnector = {'initiator': "iqn.1993-08.debian:01:ec2bff7ac3a3"} - - -class HuaweiVolumeTestCase(test.TestCase): - - def __init__(self, *args, **kwargs): - super(HuaweiVolumeTestCase, self).__init__(*args, **kwargs) - - def setUp(self): - super(HuaweiVolumeTestCase, self).setUp() - self.tmp_dir = tempfile.mkdtemp() - self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' - self._create_fake_conf_file() - configuration = mox.MockObject(conf.Configuration) - configuration.cinder_huawei_conf_file = self.fake_conf_file - configuration.append_config_values(mox.IgnoreArg()) - self.driver = FakeHuaweiStorage(configuration=configuration) - - self.driver.do_setup({}) - self.driver._test_flg = 'check_for_fail' - self._test_check_for_setup_errors() - - def tearDown(self): - shutil.rmtree(self.tmp_dir) - super(HuaweiVolumeTestCase, self).tearDown() - - def test_create_export_failed(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_export, - {}, FakeVolume) - - def test_delete_volume_failed(self): - self._test_delete_volume() - - def test_create_snapshot_failed(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_snapshot, - FakeSnapshot) - - def test_delete_snapshot_failed(self): - self._test_delete_snapshot() - - def test_create_luncopy_failed(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_volume_from_snapshot, - FakeVolumeCopy, FakeSnapshot) - - def test_initialize_failed(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.initialize_connection, - FakeVolume, FakeConnector) - - def test_terminate_connection_failed(self): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.terminate_connection, - FakeVolume, FakeConnector) - - def test_normal(self): - # test for T Series - self.driver._test_flg = 'check_for_T' - self._test_check_for_setup_errors() - self._test_create_volume() - self._test_create_export() - self._test_create_snapshot() - self._test_create_volume_from_snapshot() - self._test_initialize_connection_for_T() - self._test_terminate_connection() - self._test_delete_snapshot() - self._test_delete_volume() - self._test_get_get_volume_stats() - - # test for Dorado2100 G2 - self.driver._test_flg = 'check_for_Dorado2100G2' - self._test_check_for_setup_errors() - self._test_create_volume() - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_snapshot, - FakeSnapshot) - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_volume_from_snapshot, - FakeVolumeCopy, FakeSnapshot) - self._test_initialize_connection_for_Dorado2100G2() - self._test_terminate_connection() - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.delete_snapshot, - FakeSnapshot) - self._test_delete_volume() - - # test for Dorado5100 - self.driver._test_flg = 'check_for_Dorado5100' - self._test_check_for_setup_errors() - self._test_create_volume() - self._test_create_snapshot() - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_volume_from_snapshot, - FakeVolumeCopy, FakeSnapshot) - self._test_initialize_connection_for_Dorado5100() - self._test_terminate_connection() - self._test_delete_snapshot() - self._test_delete_volume() - - def cleanup(self): - if os.path.exists(self.fake_conf_file): - os.remove(self.fake_conf_file) - shutil.rmtree(self.tmp_dir) - - def _create_fake_conf_file(self): - doc = Document() - - config = doc.createElement('config') - doc.appendChild(config) - - storage = doc.createElement('Storage') - config.appendChild(storage) - controllerip0 = doc.createElement('ControllerIP0') - controllerip0_text = doc.createTextNode('10.10.10.1') - controllerip0.appendChild(controllerip0_text) - storage.appendChild(controllerip0) - controllerip1 = doc.createElement('ControllerIP1') - controllerip1_text = doc.createTextNode('10.10.10.2') - controllerip1.appendChild(controllerip1_text) - storage.appendChild(controllerip1) - username = doc.createElement('UserName') - username_text = doc.createTextNode('admin') - username.appendChild(username_text) - storage.appendChild(username) - userpassword = doc.createElement('UserPassword') - userpassword_text = doc.createTextNode('123456') - userpassword.appendChild(userpassword_text) - storage.appendChild(userpassword) - - lun = doc.createElement('LUN') - config.appendChild(lun) - storagepool = doc.createElement('StoragePool') - storagepool.setAttribute('Name', 'RAID_001') - lun.appendChild(storagepool) - storagepool = doc.createElement('StoragePool') - storagepool.setAttribute('Name', 'RAID_002') - lun.appendChild(storagepool) - - iscsi = doc.createElement('iSCSI') - config.appendChild(iscsi) - defaulttargetip = doc.createElement('DefaultTargetIP') - defaulttargetip_text = doc.createTextNode('192.168.100.1') - defaulttargetip.appendChild(defaulttargetip_text) - iscsi.appendChild(defaulttargetip) - initiator = doc.createElement('Initiator') - initiator.setAttribute('Name', 'iqn.1993-08.debian:01:ec2bff7ac3a3') - initiator.setAttribute('TargetIP', '192.168.100.2') - iscsi.appendChild(initiator) - - file = open(self.fake_conf_file, 'w') - file.write(doc.toprettyxml(indent='')) - file.close() - - def _test_check_for_setup_errors(self): - self.driver.check_for_setup_error() - - def _test_create_volume(self): - self.driver.create_volume(FakeVolume) - self.assertNotEqual(LUNInfo["ID"], None) - self.assertEqual(LUNInfo["RAID Group ID"], FakePoolInfo['ID']) - - def _test_delete_volume(self): - self.driver.delete_volume(FakeVolume) - self.assertEqual(LUNInfo["ID"], None) - - def _test_create_snapshot(self): - self.driver.create_snapshot(FakeSnapshot) - self.assertNotEqual(SnapshotInfo["ID"], None) - self.assertNotEqual(LUNInfo["ID"], None) - self.assertEqual(SnapshotInfo["Status"], 'Active') - self.assertEqual(SnapshotInfo["Source LUN ID"], LUNInfo["ID"]) - - def _test_delete_snapshot(self): - self.driver.delete_snapshot(FakeSnapshot) - self.assertEqual(SnapshotInfo["ID"], None) - - def _test_create_volume_from_snapshot(self): - self.driver.create_volume_from_snapshot(FakeVolumeCopy, FakeSnapshot) - self.assertNotEqual(LUNInfoCopy["ID"], None) - - def _test_create_export(self): - retval = self.driver.create_export({}, FakeVolume) - self.assertNotEqual(retval, FakeVolume["id"]) - - def _test_initialize_connection_for_T(self): - connection_data = self.driver.initialize_connection(FakeVolume, - FakeConnector) - iscsi_properties = connection_data['data'] - - self.assertEquals(iscsi_properties['target_iqn'], - FakeConfInfo['TargetIQN-T']) - self.assertEquals(iscsi_properties['target_portal'], - FakeConfInfo['Initiator TargetIP'] + ':3260') - self.assertEqual(MapInfo["DEV LUN ID"], FakeVolume['id']) - self.assertEqual(MapInfo["INI Port Info"], - FakeConnector['initiator']) - - def _test_initialize_connection_for_Dorado2100G2(self): - connection_data = self.driver.initialize_connection(FakeVolume, - FakeConnector) - iscsi_properties = connection_data['data'] - - self.assertEquals(iscsi_properties['target_iqn'], - FakeConfInfo['TargetIQN-Dorado2100G2']) - self.assertEquals(iscsi_properties['target_portal'], - FakeConfInfo['Initiator TargetIP'] + ':3260') - self.assertEqual(MapInfo["DEV LUN ID"], FakeVolume['id']) - self.assertEqual(MapInfo["INI Port Info"], - FakeConnector['initiator']) - - def _test_initialize_connection_for_Dorado5100(self): - connection_data = self.driver.initialize_connection(FakeVolume, - FakeConnector) - iscsi_properties = connection_data['data'] - - self.assertEquals(iscsi_properties['target_iqn'], - FakeConfInfo['TargetIQN-Dorado5100']) - self.assertEquals(iscsi_properties['target_portal'], - FakeConfInfo['Initiator TargetIP'] + ':3260') - self.assertEqual(MapInfo["DEV LUN ID"], FakeVolume['id']) - self.assertEqual(MapInfo["INI Port Info"], - FakeConnector['initiator']) - - def _test_terminate_connection(self): - self.driver.terminate_connection(FakeVolume, FakeConnector) - self.assertEqual(MapInfo["DEV LUN ID"], None) - self.assertEqual(MapInfo["Host LUN ID"], None) - self.assertEqual(MapInfo["INI Port Info"], None) - - def _test_get_get_volume_stats(self): - stats = self.driver.get_volume_stats(True) - - fakecapacity = float(FakePoolInfo['Free Capacity']) / 1024 - self.assertEqual(stats['free_capacity_gb'], fakecapacity) - - -class FakeHuaweiStorage(huawei_iscsi.HuaweiISCSIDriver): - """Fake Huawei Storage, Rewrite some methods of HuaweiISCSIDriver.""" - - def __init__(self, *args, **kwargs): - super(FakeHuaweiStorage, self).__init__(*args, **kwargs) - self._test_flg = None - - def _execute_cli(self, cmdIn): - cmd = cmdIn.split(' ')[0].lower() - if cmd == 'showsys': - if ((self._test_flg == 'check_for_fail') or - (self._test_flg == 'check_for_T')): - out = """/>showsys -========================================================================== - System Information --------------------------------------------------------------------------- - System Name | SN_S5500T-xu-0123456789 - Device Type | Oceanstor S5500T - Current System Mode | Double Controllers Normal - Mirroring Link Status | Link Up - Location | - Time | 2013-01-01 01:01:01 - Product Version | V100R005C00 -=========================================================================== -""" - elif self._test_flg == 'check_for_Dorado2100G2': - out = """/>showsys -========================================================================== - System Information --------------------------------------------------------------------------- - System Name | SN_Dorado2100_G2 - Device Type | Oceanstor Dorado2100 G2 - Current System Mode | Double Controllers Normal - Mirroring Link Status | Link Up - Location | - Time | 2013-01-01 01:01:01 - Product Version | V100R001C00 -=========================================================================== -""" - elif self._test_flg == 'check_for_Dorado5100': - out = """/>showsys -========================================================================== - System Information --------------------------------------------------------------------------- - System Name | SN_Dorado5100 - Device Type | Oceanstor Dorado5100 - Current System Mode | Double Controllers Normal - Mirroring Link Status | Link Up - Location | - Time | 2013-01-01 01:01:01 - Product Version | V100R001C00 -=========================================================================== -""" - elif cmd == 'addhostmap': - MapInfo['DEV LUN ID'] = LUNInfo['ID'] - MapInfo['LUN WWN'] = LUNInfo['LUN WWN'] - MapInfo['Host LUN ID'] = '0' - out = 'command operates successfully' - - elif cmd == 'showhostmap': - if MapInfo['DEV LUN ID'] is None: - out = 'command operates successfully, but no information.' - else: - out = """/>showhostmap -========================================================================== - Map Information --------------------------------------------------------------------------- - Map ID Working Controller Dev LUN ID LUN WWN Host LUN ID Mapped to \ - RAID ID Dev LUN Cap(MB) Map Type Whether Command LUN Pool ID ---------------------------------------------------------------------------- - 2147483649 %s %s %s %s Host: %s %s %s HOST No -- -=========================================================================== -""" % (LUNInfo['Worker Controller'], LUNInfo['ID'], LUNInfo['LUN WWN'], - MapInfo['Host ID'], MapInfo['Host ID'], LUNInfo['RAID Group ID'], - str(int(LUNInfo['Size']) * 1024)) - - elif cmd == 'delhostmap': - MapInfo['DEV LUN ID'] = None - MapInfo['LUN WWN'] = None - MapInfo['Host LUN ID'] = None - out = 'command operates successfully' - - elif cmd == 'createsnapshot': - SnapshotInfo['Source LUN ID'] = LUNInfo['ID'] - SnapshotInfo['Source LUN Name'] = LUNInfo['Name'] - SnapshotInfo['ID'] = FakeSnapshot['id'] - SnapshotInfo['Name'] = self._name_translate(FakeSnapshot['name']) - SnapshotInfo['Status'] = 'Disable' - out = 'command operates successfully' - - elif cmd == 'actvsnapshot': - SnapshotInfo['Status'] = 'Active' - out = 'command operates successfully' - - elif cmd == 'disablesnapshot': - SnapshotInfo['Status'] = 'Disable' - out = 'command operates successfully' - - elif cmd == 'delsnapshot': - SnapshotInfo['Source LUN ID'] = None - SnapshotInfo['Source LUN Name'] = None - SnapshotInfo['ID'] = None - SnapshotInfo['Name'] = None - SnapshotInfo['Status'] = None - out = 'command operates successfully' - - elif cmd == 'showsnapshot': - if SnapshotInfo['ID'] is None: - out = 'command operates successfully, but no information.' - else: - out = """/>showsnapshot -========================================================================== - Snapshot Information --------------------------------------------------------------------------- - Name ID Type Status Time Stamp --------------------------------------------------------------------------- - %s %s Public %s 2013-01-15 14:21:13 -========================================================================== -""" % (SnapshotInfo['Name'], SnapshotInfo['ID'], SnapshotInfo['Status']) - - elif cmd == 'showlunsnapshot': - if SnapshotInfo['ID'] is None: - out = """Current LUN is not a source LUN""" - else: - out = """/>showlunsnapshot -lun 2 -========================================================================== - Snapshot of LUN --------------------------------------------------------------------------- - Name ID Type Status Time Stamp --------------------------------------------------------------------------- - %s %s Public %s 2013-01-15 14:17:19 -========================================================================== -""" % (SnapshotInfo['Name'], SnapshotInfo['ID'], SnapshotInfo['Status']) - - elif cmd == 'createlun': - if LUNInfo['ID'] is None: - LUNInfo['Name'] = self._name_translate(FakeVolume['name']) - LUNInfo['ID'] = FakeVolume['id'] - LUNInfo['Size'] = FakeVolume['size'] - LUNInfo['LUN WWN'] = FakeVolume['wwn'] - LUNInfo['Owner Controller'] = 'A' - LUNInfo['Worker Controller'] = 'A' - LUNInfo['RAID Group ID'] = FakePoolInfo['ID'] - else: - LUNInfoCopy['Name'] = \ - self._name_translate(FakeVolumeCopy['name']) - LUNInfoCopy['ID'] = FakeVolumeCopy['ID'] - LUNInfoCopy['Size'] = FakeVolumeCopy['size'] - LUNInfoCopy['LUN WWN'] = FakeVolumeCopy['wwn'] - LUNInfoCopy['Owner Controller'] = 'A' - LUNInfoCopy['Worker Controller'] = 'A' - LUNInfoCopy['RAID Group ID'] = FakePoolInfo['ID'] - out = 'command operates successfully' - - elif cmd == 'dellun': - LUNInfo['Name'] = None - LUNInfo['ID'] = None - LUNInfo['Size'] = None - LUNInfo['LUN WWN'] = None - LUNInfo['Owner Controller'] = None - LUNInfo['Worker Controller'] = None - LUNInfo['RAID Group ID'] = None - out = 'command operates successfully' - - elif cmd == 'showlun': - if LUNInfo['ID'] is None: - out = 'command operates successfully, but no information.' - elif LUNInfoCopy['ID'] is None: - if ((self._test_flg == 'check_for_fail') or - (self._test_flg == 'check_for_T')): - out = """/>showlun -=========================================================================== - LUN Information ---------------------------------------------------------------------------- - ID RAID Group ID Disk Pool ID Status Controller Visible Capacity(MB) \ - LUN Name Stripe Unit Size(KB) Lun Type ---------------------------------------------------------------------------- - %s %s -- Normal %s %s %s 64 THICK -=========================================================================== -""" % (LUNInfo['ID'], LUNInfo['RAID Group ID'], LUNInfo['Owner Controller'], - str(int(LUNInfo['Size']) * 1024), LUNInfo['Name']) - elif self._test_flg == 'check_for_Dorado2100G2': - out = """/>showlun -=========================================================================== - LUN Information ---------------------------------------------------------------------------- - ID Status Controller Visible Capacity(MB) LUN Name Lun Type ---------------------------------------------------------------------------- - %s Normal %s %s %s THICK -=========================================================================== -""" % (LUNInfo['ID'], LUNInfo['Owner Controller'], - str(int(LUNInfo['Size']) * 1024), LUNInfo['Name']) - elif self._test_flg == 'check_for_Dorado5100': - out = """/>showlun -=========================================================================== - LUN Information ---------------------------------------------------------------------------- - ID RAIDgroup ID Status Controller Visible Capacity(MB) LUN Name - Strip Unit Size(KB) Lun Type ---------------------------------------------------------------------------- - %s %s Normal %s %s %s 64 THICK -=========================================================================== -""" % (LUNInfo['ID'], LUNInfo['RAID Group ID'], - LUNInfo['Owner Controller'], str(int(LUNInfo['Size']) * 1024), - LUNInfo['Name']) - else: - if ((self._test_flg == 'check_for_fail') or - (self._test_flg == 'check_for_T')): - out = """/>showlun -============================================================================ - LUN Information ----------------------------------------------------------------------------- - ID RAID Group ID Disk Pool ID Status Controller Visible Capacity(MB)\ - LUN Name Stripe Unit Size(KB) Lun Type ----------------------------------------------------------------------------- - %s %s -- Normal %s %s %s 64 THICK - %s %s -- Normal %s %s %s 64 THICK -============================================================================ -""" % (LUNInfo['ID'], LUNInfo['RAID Group ID'], LUNInfo['Owner Controller'], - str(int(LUNInfo['Size']) * 1024), LUNInfo['Name'], LUNInfoCopy['ID'], - LUNInfoCopy['RAID Group ID'], LUNInfoCopy['Owner Controller'], - str(int(LUNInfoCopy['Size']) * 1024), LUNInfoCopy['Name']) - elif self._test_flg == 'check_for_Dorado2100G2': - out = """/>showlun -=========================================================================== - LUN Information ---------------------------------------------------------------------------- - ID Status Controller Visible Capacity(MB) LUN Name Lun Type ---------------------------------------------------------------------------- - %s Normal %s %s %s THICK - %s Normal %s %s %s THICK -=========================================================================== -""" % (LUNInfo['ID'], LUNInfo['Owner Controller'], - str(int(LUNInfo['Size']) * 1024), LUNInfo['Name'], - LUNInfoCopy['ID'], LUNInfoCopy['Owner Controller'], - str(int(LUNInfoCopy['Size']) * 1024), LUNInfoCopy['Name']) - elif self._test_flg == 'check_for_Dorado5100': - out = """/>showlun -=========================================================================== - LUN Information ---------------------------------------------------------------------------- - ID RAIDgroup ID Status Controller Visible Capacity(MB) LUN Name \ - Strip Unit Size(KB) Lun Type ---------------------------------------------------------------------------- - %s %s Normal %s %s %s 64 THICK - %s %s Norma %s %s %s 64 THICK -=========================================================================== -""" % (LUNInfo['ID'], LUNInfo['RAID Group ID'], LUNInfo['Owner Controller'], - str(int(LUNInfo['Size']) * 1024), LUNInfo['Name'], - LUNInfoCopy['ID'], LUNInfoCopy['RAID Group ID'], - LUNInfoCopy['Owner Controller'], str(int(LUNInfoCopy['Size']) * 1024), - LUNInfoCopy['Name']) - - elif cmd == 'createhostgroup': - MapInfo['Host Group ID'] = '1' - MapInfo['Host Group Name'] = FakeConfInfo['HostGroup'] - out = 'command operates successfully' - - elif cmd == 'showhostgroup': - if MapInfo['Host Group ID'] is None: - out = """/>showhostgroup -============================================================ - Host Group Information ------------------------------------------------------------- - Host Group ID Name File Engine Cluster ------------------------------------------------------------- - 0 Default Group NO -============================================================ -""" - else: - out = """/>showhostgroup -============================================================ - Host Group Information ------------------------------------------------------------- - Host Group ID Name File Engine Cluster ------------------------------------------------------------- - 0 Default Group NO - %s %s NO -============================================================ -""" % (MapInfo['Host Group ID'], MapInfo['Host Group Name']) - - elif cmd == 'addhost': - MapInfo['Host ID'] = '1' - MapInfo['Host Name'] = FakeConfInfo['HostnamePrefix'] + \ - str(hash(FakeConnector['initiator'])) - MapInfo['Os Type'] = 'Linux' - out = 'command operates successfully' - - elif cmd == 'delhost': - MapInfo['Host ID'] = None - MapInfo['Host Name'] = None - MapInfo['Os Type'] = None - out = 'command operates successfully' - - elif cmd == 'showhost': - if MapInfo['Host ID'] is None: - out = 'command operates successfully, but no information.' - else: - out = """/>showhost -======================================================= - Host Information -------------------------------------------------------- - Host ID Host Name Host Group ID Os Type -------------------------------------------------------- - %s %s %s Linux -======================================================= -""" % (MapInfo['Host ID'], MapInfo['Host Name'], MapInfo['Host Group ID']) - - elif cmd == 'createluncopy': - LUNCopy['Name'] = LUNInfoCopy['Name'] - LUNCopy['ID'] = FakeLUNCopy['ID'] - LUNCopy['Type'] = FakeLUNCopy['Type'] - LUNCopy['State'] = FakeLUNCopy['State'] - LUNCopy['Status'] = FakeLUNCopy['Status'] - out = 'command operates successfully' - - elif cmd == 'delluncopy': - LUNCopy['Name'] = None - LUNCopy['ID'] = None - LUNCopy['Type'] = None - LUNCopy['State'] = None - LUNCopy['Status'] = None - out = 'command operates successfully' - - elif cmd == 'chgluncopystatus': - LUNCopy['State'] = 'Complete' - out = 'command operates successfully' - - elif cmd == 'showluncopy': - if LUNCopy['ID'] is None: - out = 'command operates successfully, but no information.' - else: - out = """/>showluncopy -============================================================================ - LUN Copy Information ----------------------------------------------------------------------------- - LUN Copy Name LUN Copy ID Type LUN Copy State LUN Copy Status ----------------------------------------------------------------------------- - %s %s %s %s %s -============================================================================ -""" % (LUNCopy['Name'], LUNCopy['ID'], LUNCopy['Type'], - LUNCopy['State'], LUNCopy['Status']) - - elif cmd == 'showiscsitgtname': - if ((self._test_flg == 'check_for_fail') or - (self._test_flg == 'check_for_T')): - out = """/>showiscsitgtname -============================================================================ - ISCSI Name ----------------------------------------------------------------------------- - Iscsi Name | %s -============================================================================ -""" % FakeConfInfo['TargetIQN'] - elif (self._test_flg == 'check_for_Dorado2100G2' or - self._test_flg == 'check_for_Dorado5100'): - out = """/>showiscsitgtname -============================================================================ - ISCSI Name ----------------------------------------------------------------------------- - Iscsi Name | %s -============================================================================ -""" % FakeConfInfo['TargetIQN'] - - elif cmd == 'showiscsiip': - out = """/>showiscsiip -============================================================================ - iSCSI IP Information ----------------------------------------------------------------------------- - Controller ID Interface Module ID Port ID IP Address Mask ----------------------------------------------------------------------------- - A 0 P1 %s 255.255.255.0 -============================================================================ -""" % FakeConfInfo['Initiator TargetIP'] - - elif cmd == 'addhostport': - MapInfo['INI Port ID'] = HostPort['ID'] - MapInfo['INI Port Name'] = HostPort['Name'] - MapInfo['INI Port Info'] = HostPort['Info'] - out = 'command operates successfully' - - elif cmd == 'delhostport': - MapInfo['INI Port ID'] = None - MapInfo['INI Port Name'] = None - MapInfo['INI Port Info'] = None - out = 'command operates successfully' - - elif cmd == 'showhostport': - if MapInfo['INI Port ID'] is None: - out = 'command operates successfully, but no information.' - else: - out = """/>showhostport -host 3 -============================================================================== - Host Port Information ------------------------------------------------------------------------------- -Port ID Port Name Port Information Port Type Host ID \ -Link Status Multipath Type ------------------------------------------------------------------------------- - %s %s %s ISCSITGT %s Unconnected Default -============================================================================== -""" % (MapInfo['INI Port ID'], MapInfo['INI Port Name'], - MapInfo['INI Port Info'], MapInfo['Host ID']) - - elif cmd == 'addiscsiini': - HostPort['ID'] = '1' - HostPort['Name'] = 'iSCSIInitiator001' - HostPort['Info'] = FakeConfInfo['Initiator Name'] - out = 'command operates successfully' - - elif cmd == 'deliscsiini': - HostPort['ID'] = None - HostPort['Name'] = None - HostPort['Info'] = None - out = 'command operates successfully' - - elif cmd == 'showiscsiini': - if HostPort['ID'] is None: - out = 'Error: The parameter is wrong.' - else: - out = """/>showiscsiini -ini iqn.1993-08.org\ -.debian:01:503629a9d3f -======================================================== - Initiator Information --------------------------------------------------------- - Initiator Name Chap Status --------------------------------------------------------- - %s Disable -======================================================== -""" % (HostPort['Info']) - - elif cmd == 'showrg': - out = """/>showrg -===================================================================== - RAID Group Information ---------------------------------------------------------------------- - ID Level Status Free Capacity(MB) Disk List Name ---------------------------------------------------------------------- - 0 RAID6 Normal 1024 0,0;0,2;0,4;0,5;0,6;0,7; RAID003 - %s %s %s %s %s %s -===================================================================== -""" % (FakePoolInfo['ID'], FakePoolInfo['Level'], - FakePoolInfo['Status'], FakePoolInfo['Free Capacity'], - FakePoolInfo['Disk List'], FakePoolInfo['Name']) - - elif cmd == 'showrespool': - out = """/>showrespool -============================================================================ - Resource Pool Information ----------------------------------------------------------------------------- - Pool ID Size(MB) Usage(MB) Valid Size(MB) Alarm Threshold(%) ----------------------------------------------------------------------------- - A 5130.0 0.0 5130.0 80 - B 3082.0 0.0 3082.0 80 -============================================================================ -""" - - elif cmd == 'chglun': - out = 'command operates successfully' - - out = out.replace('\n', '\r\n') - return out - - def _get_lun_controller(self, lunid): - pass diff --git a/cinder/tests/test_huawei_t_dorado.py b/cinder/tests/test_huawei_t_dorado.py new file mode 100644 index 000000000..fdd35289d --- /dev/null +++ b/cinder/tests/test_huawei_t_dorado.py @@ -0,0 +1,1244 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2012 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. +""" +Unit Tests for Huawei T and Dorado volume drivers. +""" + +import mox +import os +import shutil +import socket +import tempfile +import time + +from xml.dom.minidom import Document +from xml.etree import ElementTree as ET + +from cinder import context +from cinder import exception +from cinder import test +from cinder import utils +from cinder.volume import configuration as conf +from cinder.volume.drivers.huawei import HuaweiVolumeDriver +from cinder.volume.drivers.huawei import ssh_common +from cinder.volume import volume_types + + +LUN_INFO = {'ID': None, + 'Name': None, + 'Size': None, + 'LUN WWN': None, + 'Status': None, + 'Visible Capacity': None, + 'Disk Pool ID': None, + 'Cache Prefetch Strategy': None, + 'Lun Type': None, + 'Consumed Capacity': None, + 'Pool ID': None, + 'SnapShot ID': None, + 'LunCopy ID': None, + 'Owner Controller': None, + 'Worker Controller': None, + 'RAID Group ID': None} + +CLONED_LUN_INFO = {'ID': None, + 'Name': None, + 'Size': None, + 'LUN WWN': None, + 'Status': None, + 'Visible Capacity': None, + 'Disk Pool ID': None, + 'Cache Prefetch Strategy': None, + 'Lun Type': None, + 'Consumed Capacity': None, + 'Pool ID': None, + 'SnapShot ID': None, + 'LunCopy ID': None, + 'Owner Controller': None, + 'Worker Controller': None, + 'RAID Group ID': None} + +SNAPSHOT_INFO = {'Source LUN ID': None, + 'Source LUN Name': None, + 'ID': None, + 'Name': None, + 'Type': 'Public', + 'Status': None} + +MAP_INFO = {'Host Group ID': None, + 'Host Group Name': None, + 'Host ID': None, + 'Host Name': None, + 'Os Type': None, + 'INI Port ID': None, + 'INI Port Name': None, + 'INI Port Info': None, + 'INI Port WWN': None, + 'INI Port Type': None, + 'Link Status': None, + 'LUN WWN': None, + 'DEV LUN ID': None, + 'Host LUN ID': None, + 'CHAP status': False} + +HOST_PORT_INFO = {'ID': None, + 'Name': None, + 'Info': None, + 'WWN': None, + 'Type': None} + +LUNCOPY_INFO = {'Name': None, + 'ID': None, + 'Type': None, + 'State': None, + 'Status': None} + +LUNCOPY_SETTING = {'ID': '1', + 'Type': 'FULL', + 'State': 'Created', + 'Status': 'Normal'} + +POOL_SETTING = {'ID': '2', + 'Level': 'RAID6', + 'Status': 'Normal', + 'Free Capacity': '10240', + 'Disk List': '0,1;0,2;0,3;0,4;0,5;0,6', + 'Name': 'RAID_001', + 'Type': 'Thick'} + +INITIATOR_SETTING = {'TargetIQN': 'iqn.2006-08.com.huawei:oceanspace:2103037:', + 'TargetIQN-form': 'iqn.2006-08.com.huawei:oceanspace:' + '2103037::1020001:192.168.100.2', + 'Initiator Name': 'iqn.1993-08.debian:01:ec2bff7ac3a3', + 'Initiator TargetIP': '192.168.100.2'} + +FAKE_VOLUME = {'name': 'Volume-lele34fe-223f-dd33-4423-asdfghjklqwe', + 'id': 'lele34fe-223f-dd33-4423-asdfghjklqwe', + 'size': '2', + 'provider_auth': None, + 'volume_type_id': None, + 'provider_location': None} + +FAKE_CLONED_VOLUME = {'name': 'Volume-jeje34fe-223f-dd33-4423-asdfghjklqwg', + 'id': 'jeje34fe-223f-dd33-4423-asdfghjklqwg', + 'size': '3', + 'provider_auth': None, + 'volume_type_id': None, + 'provider_location': None} + +FAKE_SNAPSHOT = {'name': 'keke34fe-223f-dd33-4423-asdfghjklqwf', + 'id': '223f-dd33-4423-asdfghjklqwf', + 'volume_name': 'Volume-lele34fe-223f-dd33-4423-asdfghjklqwe', + 'provider_location': None} + +FAKE_CONNECTOR = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3', + 'host': 'fakehost'} + +RESPOOL_A_SIM = {'Size': '10240', 'Valid Size': '5120'} +RESPOOL_B_SIM = {'Size': '10240', 'Valid Size': '10240'} +VOLUME_SNAP_ID = {'vol': '0', 'vol_copy': '1', 'snap': '2'} + +cmd_error_list = [] # CLI cmds in this list will run failed +Curr_test = [''] # show current testing driver + + +class FakeChannel(): + def __init__(self): + if Curr_test[0] == 'T': + self.simu = HuaweiTCLIResSimulator() + + def resize_pty(self, width=80, height=24): + pass + + def settimeout(self, time): + pass + + def send(self, s): + self.command = s + + def recv(self, nbytes): + command = self.command.split() + cmd = command[0] + params = command[1:] + if cmd in cmd_error_list: + reset_error_flg(cmd) + out = self.command[:-1] + 'ERROR' + '\nadmin:/>' + return out.replace('\n', '\r\n') + + if cmd == 'showsys': + out = self.simu.cli_showsys(params) + elif cmd == 'createlun': + out = self.simu.cli_createlun(params) + elif cmd == 'showlun': + out = self.simu.cli_showlun(params) + elif cmd == 'dellun': + out = self.simu.cli_dellun(params) + elif cmd == 'showrg': + out = self.simu.cli_showrg(params) + elif cmd == 'showpool': + out = self.simu.cli_showpool(params) + elif cmd == 'createluncopy': + out = self.simu.cli_createluncopy(params) + elif cmd == 'chgluncopystatus': + out = self.simu.cli_chgluncopystatus(params) + elif cmd == 'showluncopy': + out = self.simu.cli_showluncopy(params) + elif cmd == 'delluncopy': + out = self.simu.cli_delluncopy(params) + elif cmd == 'createsnapshot': + out = self.simu.cli_createsnapshot(params) + elif cmd == 'actvsnapshot': + out = self.simu.cli_activesnapshot(params) + elif cmd == 'showsnapshot': + out = self.simu.cli_showsnapshot(params) + elif cmd == 'disablesnapshot': + out = self.simu.cli_disablesnapshot(params) + elif cmd == 'delsnapshot': + out = self.simu.cli_delsnapshot(params) + elif cmd == 'showrespool': + out = self.simu.cli_showrespool(params) + elif cmd == 'showiscsitgtname': + out = self.simu.cli_showiscsitgtname(params) + elif cmd == 'showiscsiip': + out = self.simu.cli_showiscsiip(params) + elif cmd == 'showhostgroup': + out = self.simu.cli_showhostgroup(params) + elif cmd == 'createhostgroup': + out = self.simu.cli_createhostgroup(params) + elif cmd == 'showhost': + out = self.simu.cli_showhost(params) + elif cmd == 'addhost': + out = self.simu.cli_addhost(params) + elif cmd == 'delhost': + out = self.simu.cli_delhost(params) + elif cmd == 'showiscsiini': + out = self.simu.cli_showiscsiini(params) + elif cmd == 'addiscsiini': + out = self.simu.cli_addiscsiini(params) + elif cmd == 'deliscsiini': + out = self.simu.cli_deliscsiini(params) + elif cmd == 'showhostport': + out = self.simu.cli_showhostport(params) + elif cmd == 'addhostport': + out = self.simu.cli_addhostport(params) + elif cmd == 'delhostport': + out = self.simu.cli_delhostport(params) + elif cmd == 'showhostmap': + out = self.simu.cli_showhostmap(params) + elif cmd == 'addhostmap': + out = self.simu.cli_addhostmap(params) + elif cmd == 'delhostmap': + out = self.simu.cli_delhostmap(params) + elif cmd == 'chglun': + out = self.simu.cli_chglun(params) + out = self.command[:-1] + out + '\nadmin:/>' + return out.replace('\n', '\r\n') + + def close(self): + pass + + +class FakeSSHClient(): + def invoke_shell(self): + return FakeChannel() + + def get_transport(self): + + class transport(): + def __init__(self): + self.sock = sock() + + class sock(): + def settimeout(self, time): + pass + + return transport() + + def close(self): + pass + + +class FakeSSHPool(): + def __init__(self, ip, port, conn_timeout, login, password=None, + *args, **kwargs): + self.ip = ip + self.port = port + self.login = login + self.password = password + + def create(self): + return FakeSSHClient() + + def get(self): + return FakeSSHClient() + + def put(self, ssh): + pass + + def remove(self, ssh): + pass + + +def Fake_sleep(time): + pass + + +def Fake_change_file_mode(obj, filepath): + pass + + +def create_fake_conf_file(filename): + doc = Document() + + config = doc.createElement('config') + doc.appendChild(config) + + storage = doc.createElement('Storage') + config.appendChild(storage) + product = doc.createElement('Product') + product_text = doc.createTextNode('T') + product.appendChild(product_text) + storage.appendChild(product) + config.appendChild(storage) + protocol = doc.createElement('Protocol') + protocol_text = doc.createTextNode('iSCSI') + protocol.appendChild(protocol_text) + storage.appendChild(protocol) + controllerip0 = doc.createElement('ControllerIP0') + controllerip0_text = doc.createTextNode('10.10.10.1') + controllerip0.appendChild(controllerip0_text) + storage.appendChild(controllerip0) + controllerip1 = doc.createElement('ControllerIP1') + controllerip1_text = doc.createTextNode('10.10.10.2') + controllerip1.appendChild(controllerip1_text) + storage.appendChild(controllerip1) + username = doc.createElement('UserName') + username_text = doc.createTextNode('admin') + username.appendChild(username_text) + storage.appendChild(username) + userpassword = doc.createElement('UserPassword') + userpassword_text = doc.createTextNode('123456') + userpassword.appendChild(userpassword_text) + storage.appendChild(userpassword) + + lun = doc.createElement('LUN') + config.appendChild(lun) + storagepool = doc.createElement('StoragePool') + storagepool.setAttribute('Name', 'RAID_001') + lun.appendChild(storagepool) + luntype = doc.createElement('LUNType') + luntype_text = doc.createTextNode('Thick') + luntype.appendChild(luntype_text) + lun.appendChild(luntype) + + iscsi = doc.createElement('iSCSI') + config.appendChild(iscsi) + defaulttargetip = doc.createElement('DefaultTargetIP') + defaulttargetip_text = doc.createTextNode('192.168.100.1') + defaulttargetip.appendChild(defaulttargetip_text) + iscsi.appendChild(defaulttargetip) + initiator = doc.createElement('Initiator') + initiator.setAttribute('Name', 'iqn.1993-08.debian:01:ec2bff7ac3a3') + initiator.setAttribute('TargetIP', '192.168.100.2') + iscsi.appendChild(initiator) + + tmp_file = open(filename, 'w') + tmp_file.write(doc.toprettyxml(indent='')) + tmp_file.close() + + +def modify_conf(conf, item, val, attrib=None): + tree = ET.parse(conf) + root = tree.getroot() + conf_item = root.find('%s' % item) + if not attrib: + conf_item.text = '%s' % val + else: + conf_item.attrib['%s' % attrib] = '%s' % val + tree.write(conf, 'UTF-8') + + +def set_error_flg(cmd): + cmd_error_list.append(cmd) + + +def reset_error_flg(cmd): + cmd_error_list.remove(cmd) + + +class HuaweiTCLIResSimulator(): + def _name_translate(self, name): + return 'OpenStack_' + str(hash(name)) + + def cli_showsys(self, params): + pass + + def cli_createlun(self, params): + lun_type = ('THIN' if '-pool' in params else 'THICK') + if LUN_INFO['ID'] is None: + LUN_INFO['Name'] = self._name_translate(FAKE_VOLUME['name']) + LUN_INFO['ID'] = VOLUME_SNAP_ID['vol'] + LUN_INFO['Size'] = FAKE_VOLUME['size'] + LUN_INFO['Lun Type'] = lun_type + LUN_INFO['Owner Controller'] = 'A' + LUN_INFO['Worker Controller'] = 'A' + LUN_INFO['RAID Group ID'] = POOL_SETTING['ID'] + FAKE_VOLUME['provider_location'] = LUN_INFO['ID'] + else: + CLONED_LUN_INFO['Name'] = \ + self._name_translate(FAKE_CLONED_VOLUME['name']) + CLONED_LUN_INFO['ID'] = VOLUME_SNAP_ID['vol_copy'] + CLONED_LUN_INFO['Size'] = FAKE_CLONED_VOLUME['size'] + CLONED_LUN_INFO['Lun Type'] = lun_type + CLONED_LUN_INFO['Owner Controller'] = 'A' + CLONED_LUN_INFO['Worker Controller'] = 'A' + CLONED_LUN_INFO['RAID Group ID'] = POOL_SETTING['ID'] + CLONED_LUN_INFO['provider_location'] = CLONED_LUN_INFO['ID'] + FAKE_CLONED_VOLUME['provider_location'] = CLONED_LUN_INFO['ID'] + out = 'command operates successfully' + return out + + def cli_showlun(self, params): + if '-lun' not in params: + if LUN_INFO['ID'] is None: + out = 'command operates successfully, but no information.' + elif CLONED_LUN_INFO['ID'] is None: + out = """/>showlun +=========================================================================== + LUN Information +--------------------------------------------------------------------------- + ID RAID Group ID Disk Pool ID Status Controller Visible Capacity(MB) \ + LUN Name Stripe Unit Size(KB) Lun Type +--------------------------------------------------------------------------- + %s %s -- Normal %s %s %s 64 THICK +=========================================================================== +""" % (LUN_INFO['ID'], LUN_INFO['RAID Group ID'], LUN_INFO['Owner Controller'], + str(int(LUN_INFO['Size']) * 1024), LUN_INFO['Name']) + else: + out = """/>showlun +============================================================================ + LUN Information +---------------------------------------------------------------------------- + ID RAID Group ID Disk Pool ID Status Controller Visible Capacity(MB)\ + LUN Name Stripe Unit Size(KB) Lun Type +---------------------------------------------------------------------------- + %s %s -- Normal %s %s %s 64 THICK + %s %s -- Normal %s %s %s 64 THICK +============================================================================ +""" % (LUN_INFO['ID'], LUN_INFO['RAID Group ID'], LUN_INFO['Owner Controller'], + str(int(LUN_INFO['Size']) * 1024), LUN_INFO['Name'], + CLONED_LUN_INFO['ID'], CLONED_LUN_INFO['RAID Group ID'], + CLONED_LUN_INFO['Owner Controller'], + str(int(CLONED_LUN_INFO['Size']) * 1024), + CLONED_LUN_INFO['Name']) + + elif params[params.index('-lun') + 1] in VOLUME_SNAP_ID.values(): + out = """/>showlun +================================================ + LUN Information +------------------------------------------------ + ID | %s + Name | %s + LUN WWN | -- + Visible Capacity | %s + RAID GROUP ID | %s + Owning Controller | %s + Workong Controller | %s + Lun Type | %s + SnapShot ID | %s + LunCopy ID | %s +================================================ +""" % ((LUN_INFO['ID'], LUN_INFO['Name'], LUN_INFO['Visible Capacity'], + LUN_INFO['RAID Group ID'], LUN_INFO['Owner Controller'], + LUN_INFO['Worker Controller'], LUN_INFO['Lun Type'], + LUN_INFO['SnapShot ID'], LUN_INFO['LunCopy ID']) + if params[params.index('-lun') + 1] == VOLUME_SNAP_ID['vol'] else + (CLONED_LUN_INFO['ID'], CLONED_LUN_INFO['Name'], + CLONED_LUN_INFO['Visible Capacity'], CLONED_LUN_INFO['RAID Group ID'], + CLONED_LUN_INFO['Owner Controller'], + CLONED_LUN_INFO['Worker Controller'], + CLONED_LUN_INFO['Lun Type'], CLONED_LUN_INFO['SnapShot ID'], + CLONED_LUN_INFO['LunCopy ID'])) + else: + out = 'ERROR: The object does not exist.' + return out + + def cli_dellun(self, params): + if params[params.index('-lun') + 1] == VOLUME_SNAP_ID['vol']: + LUN_INFO['Name'] = None + LUN_INFO['ID'] = None + LUN_INFO['Size'] = None + LUN_INFO['Lun Type'] = None + LUN_INFO['LUN WWN'] = None + LUN_INFO['Owner Controller'] = None + LUN_INFO['Worker Controller'] = None + LUN_INFO['RAID Group ID'] = None + FAKE_VOLUME['provider_location'] = None + else: + CLONED_LUN_INFO['Name'] = None + CLONED_LUN_INFO['ID'] = None + CLONED_LUN_INFO['Size'] = None + CLONED_LUN_INFO['Lun Type'] = None + CLONED_LUN_INFO['LUN WWN'] = None + CLONED_LUN_INFO['Owner Controller'] = None + CLONED_LUN_INFO['Worker Controller'] = None + CLONED_LUN_INFO['RAID Group ID'] = None + CLONED_LUN_INFO['provider_location'] = None + FAKE_CLONED_VOLUME['provider_location'] = None + out = 'command operates successfully' + return out + + def cli_showrg(self, params): + out = """/>showrg +===================================================================== + RAID Group Information +--------------------------------------------------------------------- + ID Level Status Free Capacity(MB) Disk List Name +--------------------------------------------------------------------- + 0 RAID6 Normal 1024 0,0;0,2; RAID003 + %s %s %s %s %s %s +===================================================================== +-""" % (POOL_SETTING['ID'], POOL_SETTING['Level'], + POOL_SETTING['Status'], POOL_SETTING['Free Capacity'], + POOL_SETTING['Disk List'], POOL_SETTING['Name']) + return out + + def cli_showpool(self, params): + out = """/>showpool +===================================================================== + Pool Information +--------------------------------------------------------------------- + Level Status Available Capacity(MB) Disk List +--------------------------------------------------------------------- + RAID6 Normal %s 0,0;0,2;0,4;0,5; +===================================================================== +-""" % POOL_SETTING['Free Capacity'] + return out + + def cli_createluncopy(self, params): + src_id = params[params.index('-slun') + 1] + tgt_id = params[params.index('-tlun') + 1] + LUNCOPY_INFO['Name'] = 'OpenStack_%s_%s' % (src_id, tgt_id) + LUNCOPY_INFO['ID'] = LUNCOPY_SETTING['ID'] + LUNCOPY_INFO['Type'] = LUNCOPY_SETTING['Type'] + LUNCOPY_INFO['State'] = LUNCOPY_SETTING['State'] + LUNCOPY_INFO['Status'] = LUNCOPY_SETTING['Status'] + out = 'command operates successfully' + return out + + def cli_chgluncopystatus(self, params): + LUNCOPY_INFO['State'] = 'Start' + out = 'command operates successfully' + return out + + def cli_showluncopy(self, params): + if LUNCOPY_INFO['State'] == 'Start': + LUNCOPY_INFO['State'] = 'Copying' + elif LUNCOPY_INFO['State'] == 'Copying': + LUNCOPY_INFO['State'] = 'Complete' + out = """/>showluncopy +============================================================================ + LUN Copy Information +---------------------------------------------------------------------------- + LUN Copy Name LUN Copy ID Type LUN Copy State LUN Copy Status +---------------------------------------------------------------------------- + %s %s %s %s %s +============================================================================ +""" % (LUNCOPY_INFO['Name'], LUNCOPY_INFO['ID'], LUNCOPY_INFO['Type'], + LUNCOPY_INFO['State'], LUNCOPY_INFO['Status']) + return out + + def cli_delluncopy(self, params): + LUNCOPY_INFO['Name'] = None + LUNCOPY_INFO['ID'] = None + LUNCOPY_INFO['Type'] = None + LUNCOPY_INFO['State'] = None + LUNCOPY_INFO['Status'] = None + out = 'command operates successfully' + return out + + def cli_createsnapshot(self, params): + SNAPSHOT_INFO['Source LUN ID'] = LUN_INFO['ID'] + SNAPSHOT_INFO['Source LUN Name'] = LUN_INFO['Name'] + SNAPSHOT_INFO['ID'] = VOLUME_SNAP_ID['snap'] + SNAPSHOT_INFO['Name'] =\ + self._name_translate(FAKE_SNAPSHOT['name']) + SNAPSHOT_INFO['Status'] = 'Disable' + out = 'command operates successfully' + return out + + def cli_showsnapshot(self, params): + if SNAPSHOT_INFO['ID'] is None: + out = 'command operates successfully, but no information.' + else: + out = """/>showsnapshot +========================================================================== + Snapshot Information +-------------------------------------------------------------------------- + Name ID Type Status Time Stamp +-------------------------------------------------------------------------- + %s %s Public %s 2013-01-15 14:21:13 +========================================================================== +""" % (SNAPSHOT_INFO['Name'], SNAPSHOT_INFO['ID'], SNAPSHOT_INFO['Status']) + return out + + def cli_activesnapshot(self, params): + SNAPSHOT_INFO['Status'] = 'Active' + FAKE_SNAPSHOT['provider_location'] = SNAPSHOT_INFO['ID'] + out = 'command operates successfully' + return out + + def cli_disablesnapshot(self, params): + SNAPSHOT_INFO['Status'] = 'Disable' + out = 'command operates successfully' + return out + + def cli_delsnapshot(self, params): + SNAPSHOT_INFO['Source LUN ID'] = None + SNAPSHOT_INFO['Source LUN Name'] = None + SNAPSHOT_INFO['ID'] = None + SNAPSHOT_INFO['Name'] = None + SNAPSHOT_INFO['Status'] = None + FAKE_SNAPSHOT['provider_location'] = None + out = 'command operates successfully' + return out + + def cli_showrespool(self, params): + out = """/>showrespool +=========================================================================== + Resource Pool Information +--------------------------------------------------------------------------- + Pool ID Size(MB) Usage(MB) Valid Size(MB) Alarm Threshold +--------------------------------------------------------------------------- + A %s 0.0 %s 80 + B %s 0.0 %s 80 +=========================================================================== +-""" % (RESPOOL_A_SIM['Size'], RESPOOL_A_SIM['Valid Size'], + RESPOOL_B_SIM['Size'], RESPOOL_B_SIM['Valid Size']) + return out + + def cli_showiscsitgtname(self, params): + iqn = INITIATOR_SETTING['TargetIQN'] + out = """/>showiscsitgtname +=================================================================== + ISCSI Name +------------------------------------------------------------------- + Iscsi Name | %s +=================================================================== +""" % iqn + return out + + def cli_showiscsiip(self, params): + out = """/>showiscsiip +============================================================================ + iSCSI IP Information +---------------------------------------------------------------------------- + Controller ID Interface Module ID Port ID IP Address Mask +---------------------------------------------------------------------------- + N 0 P1 %s 255.255.255.0 +============================================================================ +-""" % INITIATOR_SETTING['Initiator TargetIP'] + return out + + def cli_showhostgroup(self, params): + if MAP_INFO['Host Group ID'] is None: + out = """/>showhostgroup +============================================================ + Host Group Information +------------------------------------------------------------ + Host Group ID Name File Engine Cluster +------------------------------------------------------------ + 0 Default Group NO +============================================================ +""" + else: + out = """/>showhostgroup +============================================================ + Host Group Information +------------------------------------------------------------ + Host Group ID Name File Engine Cluster +------------------------------------------------------------ + 0 Default Group NO + %s %s NO +============================================================ +""" % (MAP_INFO['Host Group ID'], MAP_INFO['Host Group Name']) + return out + + def cli_createhostgroup(self, params): + MAP_INFO['Host Group ID'] = '1' + MAP_INFO['Host Group Name'] = 'HostGroup_OpenStack' + out = 'command operates successfully' + return out + + def cli_showhost(self, params): + if MAP_INFO['Host ID'] is None: + out = 'command operates successfully, but no information.' + else: + out = """/>showhost +======================================================= + Host Information +------------------------------------------------------- + Host ID Host Name Host Group ID Os Type +------------------------------------------------------- + %s %s %s Linux +======================================================= +""" % (MAP_INFO['Host ID'], MAP_INFO['Host Name'], MAP_INFO['Host Group ID']) + return out + + def cli_addhost(self, params): + MAP_INFO['Host ID'] = '1' + MAP_INFO['Host Name'] = 'Host_' + FAKE_CONNECTOR['host'] + MAP_INFO['Os Type'] = 'Linux' + out = 'command operates successfully' + return out + + def cli_delhost(self, params): + MAP_INFO['Host ID'] = None + MAP_INFO['Host Name'] = None + MAP_INFO['Os Type'] = None + out = 'command operates successfully' + return out + + def cli_showiscsiini(self, params): + if HOST_PORT_INFO['ID'] is None: + out = 'Error: The parameter is wrong.' + else: + out = """/>showiscsiini +======================================================== + Initiator Information +-------------------------------------------------------- + Initiator Name Chap Status +-------------------------------------------------------- + %s Disable +======================================================== +""" % HOST_PORT_INFO['Info'] + return out + + def cli_addiscsiini(self, params): + HOST_PORT_INFO['ID'] = '1' + HOST_PORT_INFO['Name'] = 'iSCSIInitiator001' + HOST_PORT_INFO['Info'] = INITIATOR_SETTING['Initiator Name'] + HOST_PORT_INFO['Type'] = 'ISCSITGT' + out = 'command operates successfully' + return out + + def cli_deliscsiini(self, params): + HOST_PORT_INFO['ID'] = None + HOST_PORT_INFO['Name'] = None + HOST_PORT_INFO['Info'] = None + HOST_PORT_INFO['Type'] = None + out = 'command operates successfully' + return out + + def cli_showhostport(self, params): + if MAP_INFO['INI Port ID'] is None: + out = 'command operates successfully, but no information.' + else: + out = """/>showhostport +============================================================================ + Host Port Information +---------------------------------------------------------------------------- +Port ID Port Name Port Information Port Type Host ID Link Status \ +Multipath Type +---------------------------------------------------------------------------- + %s %s %s %s %s Unconnected Default +============================================================================ +""" % (MAP_INFO['INI Port ID'], MAP_INFO['INI Port Name'], + MAP_INFO['INI Port Info'], MAP_INFO['INI Port Type'], + MAP_INFO['Host ID']) + return out + + def cli_addhostport(self, params): + MAP_INFO['INI Port ID'] = HOST_PORT_INFO['ID'] + MAP_INFO['INI Port Name'] = HOST_PORT_INFO['Name'] + MAP_INFO['INI Port Info'] = HOST_PORT_INFO['Info'] + MAP_INFO['INI Port Type'] = HOST_PORT_INFO['Type'] + out = 'command operates successfully' + return out + + def cli_delhostport(self, params): + MAP_INFO['INI Port ID'] = None + MAP_INFO['INI Port Name'] = None + MAP_INFO['INI Port Info'] = None + MAP_INFO['INI Port Type'] = None + HOST_PORT_INFO['ID'] = None + HOST_PORT_INFO['Name'] = None + HOST_PORT_INFO['Info'] = None + HOST_PORT_INFO['Type'] = None + out = 'command operates successfully' + return out + + def cli_showhostmap(self, params): + if MAP_INFO['DEV LUN ID'] is None: + out = 'command operates successfully, but no information.' + else: + out = """/>showhostmap +=========================================================================== + Map Information +--------------------------------------------------------------------------- + Map ID Working Controller Dev LUN ID LUN WWN Host LUN ID Mapped to\ + RAID ID Dev LUN Cap(MB) Map Type Whether Command LUN Pool ID +---------------------------------------------------------------------------- + 2147483649 %s %s %s %s Host: %s %s %s HOST No -- +============================================================================ +""" % (LUN_INFO['Worker Controller'], LUN_INFO['ID'], LUN_INFO['LUN WWN'], + MAP_INFO['Host LUN ID'], MAP_INFO['Host ID'], LUN_INFO['RAID Group ID'], + str(int(LUN_INFO['Size']) * 1024)) + return out + + def cli_addhostmap(self, params): + MAP_INFO['DEV LUN ID'] = LUN_INFO['ID'] + MAP_INFO['LUN WWN'] = LUN_INFO['LUN WWN'] + MAP_INFO['Host LUN ID'] = '2' + MAP_INFO['Link Status'] = 'Linked' + out = 'command operates successfully' + return out + + def cli_delhostmap(self, params): + if MAP_INFO['Link Status'] == 'Linked': + MAP_INFO['Link Status'] = 'Deleting' + out = 'there are IOs accessing the system, please try later' + else: + MAP_INFO['Link Status'] = None + MAP_INFO['DEV LUN ID'] = None + MAP_INFO['LUN WWN'] = None + MAP_INFO['Host LUN ID'] = None + out = 'command operates successfully' + return out + + def cli_chglun(self, params): + if params[params.index('-lun') + 1] == VOLUME_SNAP_ID['vol']: + LUN_INFO['Owner Controller'] = 'B' + else: + CLONED_LUN_INFO['Owner Controller'] = 'B' + out = 'command operates successfully' + return out + + +class HuaweiTISCSIDriverTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(HuaweiTISCSIDriverTestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(HuaweiTISCSIDriverTestCase, self).setUp() + + self.tmp_dir = tempfile.mkdtemp() + self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' + create_fake_conf_file(self.fake_conf_file) + self.configuration = mox.MockObject(conf.Configuration) + self.configuration.cinder_huawei_conf_file = self.fake_conf_file + self.configuration.append_config_values(mox.IgnoreArg()) + + self.stubs.Set(time, 'sleep', Fake_sleep) + self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', + Fake_change_file_mode) + self._init_driver() + + def _init_driver(self): + Curr_test[0] = 'T' + self.driver = HuaweiVolumeDriver(configuration=self.configuration) + self.driver.do_setup(None) + + def tearDown(self): + if os.path.exists(self.fake_conf_file): + os.remove(self.fake_conf_file) + shutil.rmtree(self.tmp_dir) + super(HuaweiTISCSIDriverTestCase, self).tearDown() + + def test_conf_invalid(self): + # Test config file not found + tmp_fonf_file = '/xxx/cinder_huawei_conf.xml' + tmp_configuration = mox.MockObject(conf.Configuration) + tmp_configuration.cinder_huawei_conf_file = tmp_fonf_file + tmp_configuration.append_config_values(mox.IgnoreArg()) + self.assertRaises(exception.ConfigNotFound, + HuaweiVolumeDriver, + configuration=tmp_configuration) + # Test Product and Protocol invalid + tmp_dict = {'Storage/Product': 'T', 'Storage/Protocol': 'iSCSI'} + for k, v in tmp_dict.items(): + modify_conf(self.fake_conf_file, k, 'xx') + self.assertRaises(exception.InvalidInput, + HuaweiVolumeDriver, + configuration=self.configuration) + modify_conf(self.fake_conf_file, k, v) + # Test ctr ip, UserName and password unspecified + tmp_dict = {'Storage/ControllerIP0': '10.10.10.1', + 'Storage/ControllerIP1': '10.10.10.2', + 'Storage/UserName': 'admin', + 'Storage/UserPassword': '123456'} + for k, v in tmp_dict.items(): + modify_conf(self.fake_conf_file, k, '') + tmp_driver = HuaweiVolumeDriver(configuration=self.configuration) + self.assertRaises(exception.InvalidInput, + tmp_driver.do_setup, None) + modify_conf(self.fake_conf_file, k, v) + # Test StoragePool unspecified + modify_conf(self.fake_conf_file, 'LUN/StoragePool', '', attrib='Name') + tmp_driver = HuaweiVolumeDriver(configuration=self.configuration) + self.assertRaises(exception.InvalidInput, + tmp_driver.do_setup, None) + modify_conf(self.fake_conf_file, 'LUN/StoragePool', 'RAID_001', + attrib='Name') + # Test LUN type invalid + modify_conf(self.fake_conf_file, 'LUN/LUNType', 'thick') + tmp_driver = HuaweiVolumeDriver(configuration=self.configuration) + tmp_driver.do_setup(None) + self.assertRaises(exception.InvalidInput, + tmp_driver.create_volume, FAKE_VOLUME) + modify_conf(self.fake_conf_file, 'LUN/LUNType', 'Thick') + # Test TargetIP not found + modify_conf(self.fake_conf_file, 'iSCSI/DefaultTargetIP', '') + modify_conf(self.fake_conf_file, 'iSCSI/Initiator', '', attrib='Name') + tmp_driver = HuaweiVolumeDriver(configuration=self.configuration) + tmp_driver.do_setup(None) + tmp_driver.create_volume(FAKE_VOLUME) + self.assertRaises(exception.InvalidInput, + tmp_driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + tmp_driver.delete_volume(FAKE_VOLUME) + modify_conf(self.fake_conf_file, 'iSCSI/DefaultTargetIP', + '192.168.100.1') + modify_conf(self.fake_conf_file, 'iSCSI/Initiator', + 'iqn.1993-08.debian:01:ec2bff7ac3a3', attrib='Name') + + def test_volume_type(self): + ctxt = context.get_admin_context() + extra_specs = {'drivers:LUNType': 'Thin'} + type_ref = volume_types.create(ctxt, 'THIN', extra_specs) + FAKE_VOLUME['volume_type_id'] = type_ref['id'] + self.driver.create_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO["ID"], VOLUME_SNAP_ID['vol']) + self.assertEqual(LUN_INFO['Lun Type'], 'THIN') + self.driver.delete_volume(FAKE_VOLUME) + FAKE_VOLUME['volume_type_id'] = None + + # Test volume type invalid + extra_specs = {'drivers:InvalidLUNType': 'Thin'} + type_ref = volume_types.create(ctxt, 'Invalid_THIN', extra_specs) + FAKE_VOLUME['volume_type_id'] = type_ref['id'] + self.driver.create_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO["ID"], VOLUME_SNAP_ID['vol']) + self.assertNotEqual(LUN_INFO['Lun Type'], 'THIN') + self.driver.delete_volume(FAKE_VOLUME) + FAKE_VOLUME['volume_type_id'] = None + + def test_create_delete_volume(self): + # Test create lun cli exception + set_error_flg('createlun') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, FAKE_VOLUME) + + ret = self.driver.create_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + self.assertEqual(ret['provider_location'], LUN_INFO['ID']) + + # Test delete lun cli exception + set_error_flg('dellun') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_volume, FAKE_VOLUME) + + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + self.assertEqual(FAKE_VOLUME['provider_location'], None) + + def test_create_delete_cloned_volume(self): + # Test no source volume + self.assertRaises(exception.VolumeNotFound, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + + self.driver.create_volume(FAKE_VOLUME) + # Test create luncopy failed + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + set_error_flg('createluncopy') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + # Test start luncopy failed + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + set_error_flg('chgluncopystatus') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + # Test luncopy status abnormal + LUNCOPY_SETTING['Status'] = 'Disable' + self.assertEqual(LUN_INFO['ID'], '0') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + LUNCOPY_SETTING['Status'] = 'Normal' + # Test delete luncopy failed + set_error_flg('delluncopy') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + # need to clean up LUNCopy + LUNCOPY_INFO['Name'] = None + LUNCOPY_INFO['ID'] = None + LUNCOPY_INFO['Type'] = None + LUNCOPY_INFO['State'] = None + LUNCOPY_INFO['Status'] = None + + # Test normal create and delete cloned volume + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + ret = self.driver.create_cloned_volume(FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.assertEqual(ret['provider_location'], CLONED_LUN_INFO['ID']) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(FAKE_CLONED_VOLUME['provider_location'], None) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def test_create_delete_snapshot(self): + # Test no resource pool + RESPOOL_A_SIM['Valid Size'] = '0' + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, FAKE_SNAPSHOT) + RESPOOL_A_SIM['Valid Size'] = '5120' + # Test no source volume + self.assertRaises(exception.VolumeNotFound, + self.driver.create_snapshot, FAKE_SNAPSHOT) + # Test create snapshot cli exception + self.driver.create_volume(FAKE_VOLUME) + set_error_flg('createsnapshot') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, + FAKE_SNAPSHOT) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + # Test active snapshot failed + set_error_flg('actvsnapshot') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, + FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], None) + self.assertEqual(SNAPSHOT_INFO['Status'], None) + # Test disable snapshot failed + set_error_flg('disablesnapshot') + self.driver.create_snapshot(FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], VOLUME_SNAP_ID['snap']) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, + FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['Status'], 'Active') + # Test delsnapshot failed + set_error_flg('delsnapshot') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, + FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['Status'], 'Disable') + + self.driver.delete_snapshot(FAKE_SNAPSHOT) + + # Test normal create and delete snapshot + self.driver.create_volume(FAKE_VOLUME) + ret = self.driver.create_snapshot(FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], VOLUME_SNAP_ID['snap']) + self.assertEqual(SNAPSHOT_INFO['Status'], 'Active') + self.assertEqual(ret['provider_location'], SNAPSHOT_INFO['ID']) + self.driver.delete_snapshot(FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], None) + self.assertEqual(SNAPSHOT_INFO['Status'], None) + + def test_create_delete_snapshot_volume(self): + # Test no source snapshot + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + FAKE_CLONED_VOLUME, FAKE_SNAPSHOT) + # Test normal create and delete snapshot volume + self.driver.create_volume(FAKE_VOLUME) + self.driver.create_snapshot(FAKE_SNAPSHOT) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + self.assertEqual(SNAPSHOT_INFO['ID'], VOLUME_SNAP_ID['snap']) + ret = self.driver.create_volume_from_snapshot(FAKE_CLONED_VOLUME, + FAKE_SNAPSHOT) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.assertEqual(ret['provider_location'], CLONED_LUN_INFO['ID']) + self.driver.delete_snapshot(FAKE_SNAPSHOT) + self.driver.delete_volume(FAKE_VOLUME) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(SNAPSHOT_INFO['ID'], None) + + def test_initialize_connection(self): + # Test can not get iscsi iqn + set_error_flg('showiscsitgtname') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test failed to get iSCSI port info + set_error_flg('showiscsiip') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test create hostgroup failed + set_error_flg('createhostgroup') + MAP_INFO['Host Group ID'] = None + MAP_INFO['Host Group Name'] = None + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test create host failed + set_error_flg('addhost') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test add iSCSI initiator failed + set_error_flg('addiscsiini') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test add hostport failed + set_error_flg('addhostport') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test no volume + FAKE_VOLUME['provider_location'] = '100' + self.assertRaises(exception.VolumeNotFound, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + FAKE_VOLUME['provider_location'] = None + # Test map volume failed + self.driver.create_volume(FAKE_VOLUME) + set_error_flg('addhostmap') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test normal initialize connection + self.assertEqual(FAKE_VOLUME['provider_location'], + VOLUME_SNAP_ID['vol']) + self.assertEqual(LUN_INFO['Owner Controller'], 'A') + ret = self.driver.initialize_connection(FAKE_VOLUME, FAKE_CONNECTOR) + iscsi_propers = ret['data'] + self.assertEquals(iscsi_propers['target_iqn'], + INITIATOR_SETTING['TargetIQN-form']) + self.assertEquals(iscsi_propers['target_portal'], + INITIATOR_SETTING['Initiator TargetIP'] + ':3260') + self.assertEqual(MAP_INFO["DEV LUN ID"], LUN_INFO['ID']) + self.assertEqual(MAP_INFO["INI Port Info"], + FAKE_CONNECTOR['initiator']) + self.assertEqual(LUN_INFO['Owner Controller'], 'B') + self.driver.terminate_connection(FAKE_VOLUME, FAKE_CONNECTOR) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def test_terminate_connection(self): + # Test no host was found + self.assertRaises(exception.HostNotFound, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test no volume was found + self.driver .create_volume(FAKE_VOLUME) + self.driver.initialize_connection(FAKE_VOLUME, FAKE_CONNECTOR) + FAKE_VOLUME['provider_location'] = None + self.assertRaises(exception.VolumeNotFound, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + FAKE_VOLUME['provider_location'] = LUN_INFO['ID'] + # Test delete map failed + set_error_flg('delhostmap') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Delete hostport failed + set_error_flg('delhostport') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test delete initiator failed + set_error_flg('deliscsiini') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test delete host failed + set_error_flg('delhost') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + FAKE_VOLUME, FAKE_CONNECTOR) + # Test normal terminate connection + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + self.driver.initialize_connection(FAKE_VOLUME, FAKE_CONNECTOR) + self.driver.terminate_connection(FAKE_VOLUME, FAKE_CONNECTOR) + self.assertEqual(MAP_INFO["DEV LUN ID"], None) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def test_get_volume_stats(self): + stats = self.driver.get_volume_stats(True) + free_capacity = float(POOL_SETTING['Free Capacity']) / 1024 + self.assertEqual(stats['free_capacity_gb'], free_capacity) + self.assertEqual(stats['storage_protocol'], 'iSCSI') + + +class SSHMethodTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(SSHMethodTestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(SSHMethodTestCase, self).setUp() + + self.tmp_dir = tempfile.mkdtemp() + self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' + create_fake_conf_file(self.fake_conf_file) + self.configuration = mox.MockObject(conf.Configuration) + self.configuration.cinder_huawei_conf_file = self.fake_conf_file + self.configuration.append_config_values(mox.IgnoreArg()) + + self.stubs.Set(time, 'sleep', Fake_sleep) + self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', + Fake_change_file_mode) + Curr_test[0] = 'T' + self.driver = HuaweiVolumeDriver(configuration=self.configuration) + self.driver.do_setup(None) + + def tearDown(self): + if os.path.exists(self.fake_conf_file): + os.remove(self.fake_conf_file) + shutil.rmtree(self.tmp_dir) + super(SSHMethodTestCase, self).tearDown() + + def test_reach_max_connection_limit(self): + self.stubs.Set(FakeChannel, 'recv', self._fake_recv1) + self.assertRaises(exception.CinderException, + self.driver.create_volume, FAKE_VOLUME) + + def test_socket_timeout(self): + self.stubs.Set(FakeChannel, 'recv', self._fake_recv2) + self.assertRaises(exception.CinderException, + self.driver.create_volume, FAKE_VOLUME) + + def _fake_recv1(self, nbytes): + return "No response message" + + def _fake_recv2(self, nBytes): + raise socket.timeout() diff --git a/cinder/volume/drivers/huawei/__init__.py b/cinder/volume/drivers/huawei/__init__.py index 0f4b6d394..d1673f737 100644 --- a/cinder/volume/drivers/huawei/__init__.py +++ b/cinder/volume/drivers/huawei/__init__.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012 Huawei Technologies Co., Ltd. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. # Copyright (c) 2012 OpenStack LLC. # All Rights Reserved. # @@ -13,3 +15,89 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +""" +Provide a unified driver class for users. + +The product type and the protocol should be specified in confige file before. +""" + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import log as logging +from cinder.volume.configuration import Configuration +from cinder.volume import driver +from cinder.volume.drivers.huawei import huawei_t +from cinder.volume.drivers.huawei import ssh_common + +LOG = logging.getLogger(__name__) + +huawei_opt = [ + cfg.StrOpt('cinder_huawei_conf_file', + default='/etc/cinder/cinder_huawei_conf.xml', + help='config data for cinder huawei plugin')] + +CONF = cfg.CONF +CONF.register_opts(huawei_opt) + + +class HuaweiVolumeDriver(object): + """Define an unified driver for Huawei drivers.""" + + def __init__(self, *args, **kwargs): + super(HuaweiVolumeDriver, self).__init__() + self._product = {'T': huawei_t} + self._protocol = {'iSCSI': 'ISCSIDriver'} + + self.driver = self._instantiate_driver(*args, **kwargs) + + def _instantiate_driver(self, *args, **kwargs): + """Instantiate the specified driver.""" + self.configuration = kwargs.get('configuration', None) + if not self.configuration: + msg = (_('_instantiate_driver: configuration not found.')) + raise exception.InvalidInput(reason=msg) + + self.configuration.append_config_values(huawei_opt) + conf_file = self.configuration.cinder_huawei_conf_file + (product, protocol) = self._get_conf_info(conf_file) + + LOG.debug(_('_instantiate_driver: Loading %(protocol)s driver for ' + 'Huawei OceanStor %(product)s series storage arrays.') + % {'protocol': protocol, + 'product': product}) + + driver_module = self._product[product] + driver_class = 'Huawei' + product + self._protocol[protocol] + + driver_class = getattr(driver_module, driver_class) + return driver_class(*args, **kwargs) + + def _get_conf_info(self, filename): + """Get product type and connection protocol from config file.""" + root = ssh_common.parse_xml_file(filename) + product = root.findtext('Storage/Product').strip() + protocol = root.findtext('Storage/Protocol').strip() + if (product in self._product.keys() and + protocol in self._protocol.keys()): + return (product, protocol) + else: + msg = (_('"Product" or "Protocol" is illegal. "Product" should ' + 'be set to T. "Protocol" should be set ' + 'to iSCSI. Product: %(product)s ' + 'Protocol: %(protocol)s') + % {'product': str(product), + 'protocol': str(protocol)}) + raise exception.InvalidInput(reason=msg) + + def __setattr__(self, name, value): + """Set the attribute.""" + if getattr(self, 'driver', None): + self.driver.__setattr__(name, value) + return + object.__setattr__(self, name, value) + + def __getattr__(self, name): + """"Get the attribute.""" + drver = object.__getattribute__(self, 'driver') + return getattr(drver, name) diff --git a/cinder/volume/drivers/huawei/cinder_huawei_conf.xml.sample b/cinder/volume/drivers/huawei/cinder_huawei_conf.xml.sample deleted file mode 100644 index 8d5a577bf..000000000 --- a/cinder/volume/drivers/huawei/cinder_huawei_conf.xml.sample +++ /dev/null @@ -1,34 +0,0 @@ - - - - x.x.x.x - x.x.x.x - xxxxxx - xxxxxx - - - - Thick - - 64 - - - 1 - - 1 - - - - - - - - - - - - x.x.x.x - - - - diff --git a/cinder/volume/drivers/huawei/huawei_iscsi.py b/cinder/volume/drivers/huawei/huawei_iscsi.py deleted file mode 100644 index 42a9fa16d..000000000 --- a/cinder/volume/drivers/huawei/huawei_iscsi.py +++ /dev/null @@ -1,1553 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2012 Huawei Technologies Co., Ltd. -# Copyright (c) 2012 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. -""" -Volume driver for HUAWEI T series and Dorado storage systems. -""" -import base64 -import os -import paramiko -import re -import socket -import threading -import time - -from oslo.config import cfg -from xml.etree import ElementTree as ET - -from cinder import exception -from cinder.openstack.common import excutils -from cinder.openstack.common import log as logging -from cinder import utils -from cinder.volume import driver - -LOG = logging.getLogger(__name__) - -huawei_opt = [ - cfg.StrOpt('cinder_huawei_conf_file', - default='/etc/cinder/cinder_huawei_conf.xml', - help='config data for cinder huawei plugin')] - -HOST_GROUP_NAME = 'HostGroup_OpenStack' -HOST_NAME_PREFIX = 'Host_' -HOST_PORT_PREFIX = 'HostPort_' -VOL_AND_SNAP_NAME_PREFIX = 'OpenStack_' -READBUFFERSIZE = 8192 - - -CONF = cfg.CONF -CONF.register_opts(huawei_opt) - - -class SSHConn(utils.SSHPool): - """Define a new class inherited to SSHPool. - - This class rewrites method create() and defines a private method - ssh_read() which reads results of ssh commands. - """ - - def __init__(self, ip, port, conn_timeout, login, password, - privatekey=None, *args, **kwargs): - - super(SSHConn, self).__init__(ip, port, conn_timeout, login, - password, privatekey=None, - *args, **kwargs) - self.lock = threading.Lock() - - def create(self): - """Create an SSH client. - - Because seting socket timeout to be None will cause client.close() - blocking, here we have to rewrite method create() and use default - socket timeout value 0.1. - """ - try: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - if self.password: - ssh.connect(self.ip, - port=self.port, - username=self.login, - password=self.password, - timeout=self.conn_timeout) - elif self.privatekey: - pkfile = os.path.expanduser(self.privatekey) - privatekey = paramiko.RSAKey.from_private_key_file(pkfile) - ssh.connect(self.ip, - port=self.port, - username=self.login, - pkey=privatekey, - timeout=self.conn_timeout) - else: - msg = _("Specify a password or private_key") - raise exception.CinderException(msg) - - if self.conn_timeout: - transport = ssh.get_transport() - transport.set_keepalive(self.conn_timeout) - return ssh - except Exception as e: - msg = _("Error connecting via ssh: %s") % e - LOG.error(msg) - raise paramiko.SSHException(msg) - - def ssh_read(self, channel, cmd, timeout): - """Get results of CLI commands.""" - result = '' - user = self.login - user_flg = user + ':/>$' - channel.settimeout(timeout) - while True: - try: - result = result + channel.recv(READBUFFERSIZE) - except socket.timeout: - raise exception.VolumeBackendAPIException(_('read timed out')) - else: - if re.search(cmd, result) and re.search(user_flg, result): - if not re.search('Welcome', result): - break - elif re.search(user + ':/>' + cmd, result): - break - elif re.search('(y/n)', result): - break - return '\r\n'.join(result.split('\r\n')[:-1]) - - -class HuaweiISCSIDriver(driver.ISCSIDriver): - """Huawei T series and Dorado iSCSI volume driver.""" - - VERSION = "1.0.0" - - def __init__(self, *args, **kwargs): - super(HuaweiISCSIDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(huawei_opt) - self.device_type = {} - self.login_info = {} - self.hostgroup_id = None - self.ssh_pool = None - - def do_setup(self, context): - """Check config file.""" - LOG.debug(_('do_setup.')) - - self._check_conf_file() - - def check_for_setup_error(self): - """Try to connect with device and get device type.""" - LOG.debug(_('check_for_setup_error.')) - - self.login_info = self._get_login_info() - self.device_type = self._get_device_type() - if not self.device_type['type']: - err_msg = (_('check_for_setup_error: Can not get device type.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - LOG.debug(_('check_for_setup_error: Device type is:%(type)s, ' - 'version is:%(version)s.') - % {'type': self.device_type['type'], - 'version': self.device_type['version']}) - - # Now only version V1 is supported. - if self.device_type['version'] != 'V100R': - err_msg = (_('check_for_setup_error: Product version not right. ' - 'Please make sure the product version is V1.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - # Check whether storage pools are configured. - # Dorado2100 G2 needn't to configure this. - if self.device_type['type'] != 'Dorado2100 G2': - root = self._read_xml() - pool_node = root.findall('LUN/StoragePool') - if not pool_node: - err_msg = (_('_get_device_type: Storage Pool must be ' - 'configured.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def create_volume(self, volume): - """Create a new volume.""" - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('create_volume:volume name: %s.') % volume_name) - - self.login_info = self._get_login_info() - if int(volume['size']) == 0: - volume_size = '100M' - else: - volume_size = '%sG' % volume['size'] - - self._create_volume(volume_name, volume_size) - - def delete_volume(self, volume): - """Delete a volume.""" - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('delete_volume: volume name: %s.') % volume_name) - - self.login_info = self._get_login_info() - volume_id = self._find_lun(volume_name) - if volume_id is not None: - self._delete_volume(volume_name, volume_id) - else: - err_msg = (_('delete_volume:No need to delete volume. ' - 'Volume %(name)s does not exist.') - % {'name': volume['name']}) - LOG.error(err_msg) - - def create_export(self, context, volume): - """Driver entry point to get the export info for a new volume.""" - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('create_export: volume name:%s') % volume['name']) - - lun_id = self._find_lun(volume_name) - if lun_id is None: - err_msg = (_('create_export:Volume %(name)s does not exist.') - % {'name': volume_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - return {'provider_location': lun_id} - - def ensure_export(self, context, volume): - """Driver entry point to get the export info for a existing volume.""" - pass - - def remove_export(self, context, volume_id): - """Driver entry point to remove an export for a volume.""" - pass - - def initialize_connection(self, volume, connector): - """Map a volume to a host and return target iSCSI information.""" - initiator_name = connector['initiator'] - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('initialize_connection: volume name: %(volume)s. ' - 'initiator name: %(ini)s.') - % {'volume': volume_name, - 'ini': initiator_name}) - - self.login_info = self._get_login_info() - # Get target iSCSI iqn. - iscsi_conf = self._get_iscsi_info() - target_ip = None - for ini in iscsi_conf['Initiator']: - if ini['Name'] == initiator_name: - target_ip = ini['TargetIP'] - break - if not target_ip: - if not iscsi_conf['DefaultTargetIP']: - err_msg = (_('initialize_connection:Failed to find target ip ' - 'for initiator:%(initiatorname)s, ' - 'please check config file.') - % {'initiatorname': initiator_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - target_ip = iscsi_conf['DefaultTargetIP'] - - (target_iqn, controller) = self._get_tgt_iqn(target_ip) - if not target_iqn: - err_msg = (_('initialize_connection:Failed to find target iSCSI ' - 'iqn. Target IP:%(ip)s') - % {'ip': target_ip}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - # Create hostgroup and host. - hostgroup_name = HOST_GROUP_NAME - self.hostgroup_id = self._find_hostgroup(hostgroup_name) - if self.hostgroup_id is None: - self._create_hostgroup(hostgroup_name) - self.hostgroup_id = self._find_hostgroup(hostgroup_name) - - host_name = HOST_NAME_PREFIX + str(hash(initiator_name)) - host_id = self._find_host_in_hostgroup(host_name, self.hostgroup_id) - if host_id is None: - self._add_host(host_name, self.hostgroup_id) - host_id = self._find_host_in_hostgroup(host_name, - self.hostgroup_id) - - # Create an initiator. - added = self._check_initiator(initiator_name) - if not added: - self._add_initiator(initiator_name) - - # Add the initiator to host. - port_name = HOST_PORT_PREFIX + str(hash(initiator_name)) - port_info = initiator_name - portadded = False - hostport_info = self._get_hostport_info(host_id) - if hostport_info: - for hostport in hostport_info: - if hostport['info'] == initiator_name: - portadded = True - break - if not portadded: - self._add_hostport(port_name, host_id, port_info) - - LOG.debug(_('initialize_connection:host name: %(host)s, ' - 'initiator name: %(ini)s, ' - 'hostport name: %(port)s') - % {'host': host_name, - 'ini': initiator_name, - 'port': port_name}) - - # Map a LUN to a host if not mapped. - lun_id = self._find_lun(volume_name) - if lun_id is None: - err_msg = (_('initialize_connection:Failed to find the ' - 'given volume. ' - 'volume name:%(volume)s.') - % {'volume': volume_name}) - raise exception.VolumeBackendAPIException(data=err_msg) - - hostlun_id = None - map_info = self._get_map_info(host_id) - # Make sure the hostLUN ID starts from 1. - new_hostlun_id = 1 - new_hostlunid_found = False - if map_info: - for map in map_info: - if map['devlunid'] == lun_id: - hostlun_id = map['hostlunid'] - break - elif not new_hostlunid_found: - if new_hostlun_id < int(map['hostlunid']): - new_hostlunid_found = True - else: - new_hostlun_id = int(map['hostlunid']) + 1 - # The LUN is not mapped to the host. - if not hostlun_id: - self._map_lun(lun_id, host_id, new_hostlun_id) - hostlun_id = self._get_hostlunid(host_id, lun_id) - - # Change lun ownning controller for better performance. - if self._get_lun_controller(lun_id) != controller: - self._change_lun_controller(lun_id, controller) - - # Return iSCSI properties. - properties = {} - properties['target_discovered'] = False - properties['target_portal'] = ('%s:%s' % (target_ip, '3260')) - properties['target_iqn'] = target_iqn - properties['target_lun'] = int(hostlun_id) - properties['volume_id'] = volume['id'] - auth = volume['provider_auth'] - if auth: - (auth_method, auth_username, auth_secret) = auth.split() - - properties['auth_method'] = auth_method - properties['auth_username'] = auth_username - properties['auth_password'] = auth_secret - - return {'driver_volume_type': 'iscsi', 'data': properties} - - def terminate_connection(self, volume, connector, **kwargs): - """Delete map between a volume and a host.""" - initiator_name = connector['initiator'] - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('terminate_connection:volume name: %(volume)s, ' - 'initiator name: %(ini)s.') - % {'volume': volume_name, - 'ini': initiator_name}) - - self.login_info = self._get_login_info() - host_name = HOST_NAME_PREFIX + str(hash(initiator_name)) - host_id = self._find_host_in_hostgroup(host_name, self.hostgroup_id) - if host_id is None: - err_msg = (_('terminate_connection:Host does not exist. ' - 'Host name:%(host)s.') - % {'host': host_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - # Delete host map. - lun_id = self._find_lun(volume_name) - if lun_id is None: - err_msg = (_('terminate_connection:volume does not exist. ' - 'volume name:%(volume)s') - % {'volume': volume_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - map_id = None - mapnum = 0 - map_info = self._get_map_info(host_id) - if map_info: - mapnum = len(map_info) - for map in map_info: - if map['devlunid'] == lun_id: - map_id = map['mapid'] - break - if map_id is not None: - self._delete_map(map_id) - mapnum = mapnum - 1 - else: - LOG.error(_('terminate_connection:No map between host ' - 'and volume. Host name:%(hostname)s, ' - 'volume name:%(volumename)s.') - % {'hostname': host_name, - 'volumename': volume_name}) - - # Delete host initiator when no LUN mapped to it. - portnum = 0 - hostportinfo = self._get_hostport_info(host_id) - if hostportinfo: - portnum = len(hostportinfo) - for hostport in hostportinfo: - if hostport['info'] == initiator_name and mapnum == 0: - self._delete_hostport(hostport['id']) - self._delete_initiator(initiator_name) - portnum = portnum - 1 - break - else: - LOG.error(_('terminate_connection:No initiator is added ' - 'to the host. Host name:%(hostname)s') - % {'hostname': host_name}) - - # Delete host when no initiator added to it. - if portnum == 0: - self._delete_host(host_id) - - def create_snapshot(self, snapshot): - """Create a snapshot.""" - snapshot_name = self._name_translate(snapshot['name']) - volume_name = self._name_translate(snapshot['volume_name']) - - LOG.debug(_('create_snapshot:snapshot name:%(snapshot)s, ' - 'volume name:%(volume)s.') - % {'snapshot': snapshot_name, - 'volume': volume_name}) - - self.login_info = self._get_login_info() - if self.device_type['type'] == 'Dorado2100 G2': - err_msg = (_('create_snapshot:Device does not support snapshot.')) - - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - if self._is_resource_pool_enough() is False: - err_msg = (_('create_snapshot:' - 'Resource pool needs 1GB valid size at least.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - lun_id = self._find_lun(volume_name) - if lun_id is None: - err_msg = (_('create_snapshot:Volume does not exist. ' - 'Volume name:%(name)s') - % {'name': volume_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - self._create_snapshot(snapshot_name, lun_id) - snapshot_id = self._find_snapshot(snapshot_name) - if not snapshot_id: - err_msg = (_('create_snapshot:Snapshot does not exist. ' - 'Snapshot name:%(name)s') - % {'name': snapshot_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - self._active_snapshot(snapshot_id) - - def delete_snapshot(self, snapshot): - """Delete a snapshot.""" - snapshot_name = self._name_translate(snapshot['name']) - volume_name = self._name_translate(snapshot['volume_name']) - - LOG.debug(_('delete_snapshot:snapshot name:%(snapshot)s, ' - 'volume name:%(volume)s.') - % {'snapshot': snapshot_name, - 'volume': volume_name}) - - self.login_info = self._get_login_info() - if self.device_type['type'] == 'Dorado2100 G2': - err_msg = (_('delete_snapshot:Device does not support snapshot.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - snapshot_id = self._find_snapshot(snapshot_name) - if snapshot_id is not None: - self._disable_snapshot(snapshot_id) - self._delete_snapshot(snapshot_id) - else: - err_msg = (_('delete_snapshot:Snapshot does not exist. ' - 'snapshot name:%(snap)s') - % {'snap': snapshot_name}) - LOG.debug(err_msg) - - def create_volume_from_snapshot(self, volume, snapshot): - """Create a volume from a snapshot. - - We use LUNcopy to create a new LUN from snapshot. - """ - snapshot_name = self._name_translate(snapshot['name']) - volume_name = self._name_translate(volume['name']) - - LOG.debug(_('create_volume_from_snapshot:snapshot ' - 'name:%(snapshot)s, ' - 'volume name:%(volume)s.') - % {'snapshot': snapshot_name, - 'volume': volume_name}) - - self.login_info = self._get_login_info() - if self.device_type['type'].find('Dorado') > -1: - err_msg = (_('create_volume_from_snapshot:Device does ' - 'not support create volume from snapshot. ' - 'Volume name:%(volume)s, ' - 'snapshot name:%(snapshot)s.') - % {'volume': volume_name, - 'snapshot': snapshot_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - snapshot_id = self._find_snapshot(snapshot_name) - if snapshot_id is None: - err_msg = (_('create_volume_from_snapshot:Snapshot ' - 'does not exist. Snapshot name:%(name)s') - % {'name': snapshot_name}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - # Create a target LUN. - if int(volume['size']) == 0: - volume_size = '%sG' % snapshot['volume_size'] - else: - volume_size = '%sG' % volume['size'] - - self._create_volume(volume_name, volume_size) - volume_id = self._find_lun(volume_name) - luncopy_name = volume_name - try: - self._create_luncopy(luncopy_name, snapshot_id, volume_id) - luncopy_id = self._find_luncopy(luncopy_name) - self._start_luncopy(luncopy_id) - self._wait_for_luncopy(luncopy_name) - # If LUNcopy failed,we should delete the target volume. - except Exception: - with excutils.save_and_reraise_exception(): - self._delete_luncopy(luncopy_id) - self._delete_volume(volume_name, volume_id) - - self._delete_luncopy(luncopy_id) - - def get_volume_stats(self, refresh=False): - """Get volume stats. - - If 'refresh' is True, run update the stats first. - """ - if refresh: - self._update_volume_stats() - - return self._stats - - def _check_conf_file(self): - """Check the config file, make sure the key elements are set.""" - root = self._read_xml() - try: - IP1 = root.findtext('Storage/ControllerIP0') - IP2 = root.findtext('Storage/ControllerIP1') - username = root.findtext('Storage/UserName') - pwd = root.findtext('Storage/UserPassword') - - isconfwrong = False - if ((not IP1 and not IP2) or - (not username) or - (not pwd)): - err_msg = (_('Config file is wrong. Controler IP, ' - 'UserName and UserPassword must be set.')) - LOG.error(err_msg) - raise exception.InvalidInput(reason=err_msg) - - except Exception as err: - LOG.error(_('_check_conf_file: %s') % str(err)) - raise exception.VolumeBackendAPIException(data=err) - - def _read_xml(self): - """Open xml file.""" - filename = self.configuration.cinder_huawei_conf_file - try: - tree = ET.parse(filename) - root = tree.getroot() - - except Exception as err: - LOG.error(_('_read_xml:%s') % err) - raise exception.VolumeBackendAPIException(data=err) - return root - - def _get_login_info(self): - """Get login IP, username and password from config file.""" - logininfo = {} - try: - filename = self.configuration.cinder_huawei_conf_file - tree = ET.parse(filename) - root = tree.getroot() - logininfo['ControllerIP0'] = root.findtext('Storage/ControllerIP0') - logininfo['ControllerIP1'] = root.findtext('Storage/ControllerIP1') - - need_encode = False - for key in ['UserName', 'UserPassword']: - node = root.find('Storage/%s' % key) - node_text = node.text - if node_text.find('!$$$') == 0: - logininfo[key] = base64.b64decode(node_text[4:]) - else: - logininfo[key] = node_text - node.text = '!$$$' + base64.b64encode(node_text) - need_encode = True - if need_encode: - try: - tree.write(filename, 'UTF-8') - except Exception as err: - LOG.error(_('Write login information to xml error. %s') - % err) - - except Exception as err: - LOG.error(_('_get_login_info error. %s') % err) - raise exception.VolumeBackendAPIException(data=err) - return logininfo - - def _get_lun_set_info(self): - """Get parameters from config file for creating LUN.""" - # Default LUN set information - lunsetinfo = {'LUNType': 'Thick', - 'StripUnitSize': '64', - 'WriteType': '1', - 'MirrorSwitch': '1', - 'PrefetchType': '3', - 'PrefetchValue': '0', - 'PrefetchTimes': '0', - 'StoragePool': 'RAID_001'} - - root = self._read_xml() - try: - luntype = root.findtext('LUN/LUNType') - if luntype in ['Thick', 'Thin']: - lunsetinfo['LUNType'] = luntype - elif luntype: - err_msg = (_('Config file is wrong. LUNType must be "Thin" ' - ' or "Thick". LUNType:%(type)s') - % {'type': luntype}) - raise exception.VolumeBackendAPIException(data=err_msg) - - # Here we do not judge whether the parameters are right. - # CLI will return error responses if the parameters not right. - stripunitsize = root.findtext('LUN/StripUnitSize') - if stripunitsize: - lunsetinfo['StripUnitSize'] = stripunitsize - writetype = root.findtext('LUN/WriteType') - if writetype: - lunsetinfo['WriteType'] = writetype - mirrorswitch = root.findtext('LUN/MirrorSwitch') - if mirrorswitch: - lunsetinfo['MirrorSwitch'] = mirrorswitch - - if self.device_type['type'] == 'Tseries': - pooltype = lunsetinfo['LUNType'] - prefetch = root.find('LUN/Prefetch') - if prefetch and prefetch.attrib['Type']: - lunsetinfo['PrefetchType'] = prefetch.attrib['Type'] - if lunsetinfo['PrefetchType'] == '1': - lunsetinfo['PrefetchValue'] = prefetch.attrib['Value'] - elif lunsetinfo['PrefetchType'] == '2': - lunsetinfo['PrefetchTimes'] = prefetch.attrib['Value'] - else: - LOG.debug(_('_get_lun_set_info:Use default prefetch type. ' - 'Prefetch type:Intelligent.')) - - # No need to set Prefetch type for Dorado. - elif self.device_type['type'] == 'Dorado5100': - pooltype = 'Thick' - elif self.device_type['type'] == 'Dorado2100 G2': - return lunsetinfo - - poolsinfo = self._find_pool_info(pooltype) - if not poolsinfo: - err_msg = (_('_get_lun_set_info:No available pools! ' - 'Please check whether storage pool is created.')) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - pools = root.findall('LUN/StoragePool') - lunsetinfo['StoragePool'] = \ - self._get_maximum_pool(pools, poolsinfo, luntype) - - except Exception as err: - LOG.error(_('_get_lun_set_info:%s') % err) - raise exception.VolumeBackendAPIException(data=err) - - return lunsetinfo - - def _find_pool_info(self, pooltype): - """Return pools information created in storage device.""" - if pooltype == 'Thick': - cli_cmd = ('showrg') - else: - cli_cmd = ('showpool') - - out = self._execute_cli(cli_cmd) - - en = out.split('\r\n') - if len(en) <= 6: - return None - - pools_list = [] - for i in range(6, len(en) - 2): - r = en[i].split() - pools_list.append(r) - return pools_list - - def _get_maximum_pool(self, poolinconf, poolindev, luntype): - """Get the maximum pool from config file. - - According to the given pools' name in config file, - we select the pool of maximum free capacity. - """ - maxpoolid = None - maxpoolsize = 0 - if luntype == 'Thin': - nameindex = 1 - sizeindex = 4 - else: - nameindex = 5 - sizeindex = 3 - - for pool in poolinconf: - poolname = pool.attrib['Name'] - for pooldetail in poolindev: - if pooldetail[nameindex] == poolname: - if int(float(pooldetail[sizeindex])) > maxpoolsize: - maxpoolid = pooldetail[0] - maxpoolsize = int(float(pooldetail[sizeindex])) - break - if maxpoolid is not None: - return maxpoolid - else: - err_msg = (_('_get_maximum_pool:maxpoolid is None. ' - 'Please check config file and make sure ' - 'the "Name" in "StoragePool" is right.')) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _get_iscsi_info(self): - """Get iSCSI info from config file.""" - iscsiinfo = {} - root = self._read_xml() - try: - iscsiinfo['DefaultTargetIP'] = \ - root.findtext('iSCSI/DefaultTargetIP') - initiator_list = [] - for dic in root.findall('iSCSI/Initiator'): - initiator_list.append(dic.attrib) - iscsiinfo['Initiator'] = initiator_list - - except Exception as err: - LOG.error(_('_get_iscsi_info:%s') % str(err)) - - return iscsiinfo - - def _execute_cli(self, cmd): - """Build SSH connection to execute CLI commands. - - If the connection to first controller time out, - try to connect to the other controller. - """ - LOG.debug(_('CLI command:%s') % cmd) - connect_times = 0 - ip0 = self.login_info['ControllerIP0'] - ip1 = self.login_info['ControllerIP1'] - user = self.login_info['UserName'] - pwd = self.login_info['UserPassword'] - if not self.ssh_pool: - self.ssh_pool = SSHConn(ip0, 22, 30, user, pwd) - ssh_client = None - while True: - if connect_times == 1: - # Switch to the other controller. - self.ssh_pool.lock.acquire() - if ssh_client: - if ssh_client.server_ip == self.ssh_pool.ip: - if self.ssh_pool.ip == ip0: - self.ssh_pool.ip = ip1 - else: - self.ssh_pool.ip = ip0 - # Create a new client. - if ssh_client.chan: - ssh_client.chan.close() - ssh_client.chan = None - ssh_client.server_ip = None - ssh_client.close() - ssh_client = None - ssh_client = self.ssh_pool.create() - else: - self.ssh_pool.ip = ip1 - self.ssh_pool.lock.release() - try: - if not ssh_client: - ssh_client = self.ssh_pool.get() - # "server_ip" shows controller connecting with the ssh client. - if ('server_ip' not in ssh_client.__dict__ or - not ssh_client.server_ip): - self.ssh_pool.lock.acquire() - ssh_client.server_ip = self.ssh_pool.ip - self.ssh_pool.lock.release() - # An SSH client owns one "chan". - if ('chan' not in ssh_client.__dict__ or - not ssh_client.chan): - ssh_client.chan =\ - utils.create_channel(ssh_client, 600, 800) - - while True: - ssh_client.chan.send(cmd + '\n') - out = self.ssh_pool.ssh_read(ssh_client.chan, cmd, 20) - if out.find('(y/n)') > -1: - cmd = 'y' - else: - break - self.ssh_pool.put(ssh_client) - - index = out.find(user + ':/>') - if index > -1: - return out[index:] - else: - return out - - except Exception as err: - if connect_times < 1: - connect_times += 1 - continue - else: - if ssh_client: - self.ssh_pool.remove(ssh_client) - LOG.error(_('_execute_cli:%s') % err) - raise exception.VolumeBackendAPIException(data=err) - - def _name_translate(self, name): - """Form new names because of the 32-character limit on names.""" - newname = VOL_AND_SNAP_NAME_PREFIX + str(hash(name)) - - LOG.debug(_('_name_translate:Name in cinder: %(old)s, ' - 'new name in storage system: %(new)s') - % {'old': name, - 'new': newname}) - - return newname - - def _find_lun(self, name): - """Get the ID of a LUN with the given LUN name.""" - cli_cmd = ('showlun') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - if 'Dorado2100 G2' == self.device_type['type']: - d = 2 - elif 'Dorado5100' == self.device_type['type']: - d = 1 - else: - d = 0 - - for i in range(6, len(en) - 2): - r = en[i].replace('Not format', 'Notformat').split() - if r[6 - d] == name: - return r[0] - return None - - def _create_hostgroup(self, hostgroupname): - """Create a host group.""" - cli_cmd = ('createhostgroup -n %(name)s' - % {'name': hostgroupname}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_create_hostgroup:Failed to Create hostgroup. ' - 'Hostgroup name: %(name)s. ' - 'out:%(out)s.') - % {'name': hostgroupname, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _find_hostgroup(self, groupname): - """Get the given hostgroup ID.""" - cli_cmd = ('showhostgroup') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - for i in range(6, len(en) - 2): - r = en[i].split() - if r[1] == groupname: - return r[0] - return None - - def _add_host(self, hostname, hostgroupid): - """Add a new host.""" - cli_cmd = ('addhost -group %(groupid)s -n %(hostname)s -t 0' - % {'groupid': hostgroupid, - 'hostname': hostname}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_add_host:Failed to add host to hostgroup. ' - 'host name:%(host)s ' - 'hostgroup id:%(hostgroup)s ' - 'out:%(out)s') - % {'host': hostname, - 'hostgroup': hostgroupid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _check_initiator(self, ininame): - """Check whether the initiator is already added.""" - cli_cmd = ('showiscsiini -ini %(name)s' - % {'name': ininame}) - out = self._execute_cli(cli_cmd) - if out.find('Initiator Information') > -1: - return True - else: - return False - - def _add_initiator(self, ininame): - """Add a new initiator to storage device.""" - cli_cmd = ('addiscsiini -n %(name)s' - % {'name': ininame}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_add_initiator:Failed to add initiator. ' - 'initiator name:%(name)s ' - 'out:%(out)s') - % {'name': ininame, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _delete_initiator(self, ininame): - """Delete an initiator.""" - cli_cmd = ('deliscsiini -n %(name)s' - % {'name': ininame}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_initiator:ERROE:Failed to delete initiator. ' - 'initiator name:%(name)s ' - 'out:%(out)s') - % {'name': ininame, - 'out': out}) - LOG.error(err_msg) - - def _find_host_in_hostgroup(self, hostname, hostgroupid): - """Get the given host ID.""" - cli_cmd = ('showhost -group %(groupid)s' - % {'groupid': hostgroupid}) - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) < 6: - return None - - for i in range(6, len(en) - 2): - r = en[i].split() - if r[1] == hostname: - return r[0] - return None - - def _get_hostport_info(self, hostid): - """Get hostports details of the given host.""" - cli_cmd = ('showhostport -host %(hostid)s' - % {'hostid': hostid}) - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) < 6: - return None - - hostportinfo = [] - list_key = ['id', 'name', 'info', 'type', 'hostid', - 'linkstatus', 'multioathtype'] - for i in range(6, len(en) - 2): - list_val = en[i].split() - hostport_dic = dict(map(None, list_key, list_val)) - hostportinfo.append(hostport_dic) - return hostportinfo - - def _add_hostport(self, portname, hostid, portinfo, multipathtype=0): - """Add a host port.""" - cli_cmd = ('addhostport -host %(id)s -type 5 ' - '-info %(info)s -n %(name)s -mtype %(mtype)s' - % {'id': hostid, - 'info': portinfo, - 'name': portname, - 'mtype': multipathtype}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_add_hostport:Failed to add hostport. ' - 'port name:%(port)s ' - 'port information:%(info)s ' - 'host id:%(host)s ' - 'out:%(out)s') - % {'port': portname, - 'info': portinfo, - 'host': hostid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _delete_hostport(self, portid): - """Delete a host port.""" - cli_cmd = ('delhostport -force -p %(portid)s' - % {'portid': portid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_hostport:Failed to delete host port. ' - 'port id:%(portid)s') - % {'portid': portid}) - LOG.error(err_msg) - - def _get_tgt_iqn(self, iscsiip): - """Get target iSCSI iqn.""" - LOG.debug(_('_get_tgt_iqn:iSCSI IP is %s.') % iscsiip) - cli_cmd = ('showiscsitgtname') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) < 4: - return (None, None) - - index = en[4].find('iqn') - iqn_prefix = en[4][index:] - iqn_prefix.strip() - iscsiip_info = self._get_iscsi_ip_info(iscsiip) - if iscsiip_info: - if iscsiip_info['ctrid'] == 'A': - ctr = '0' - elif iscsiip_info['ctrid'] == 'B': - ctr = '1' - - interface = '0' + iscsiip_info['interfaceid'] - port = iscsiip_info['portid'].replace('P', '0') - iqn_suffix = ctr + '02' + interface + port - for i in range(0, len(iqn_suffix)): - if iqn_suffix[i] != '0': - iqn_suffix = iqn_suffix[i:] - break - if self.device_type['type'] == 'Tseries': - iqn = iqn_prefix + ':' + iqn_suffix + ':' \ - + iscsiip_info['ipaddress'] - elif self.device_type['type'] == "Dorado2100 G2": - iqn = iqn_prefix + ":" + iscsiip_info['ipaddress'] + "-" \ - + iqn_suffix - else: - iqn = iqn_prefix + ':' + iscsiip_info['ipaddress'] - - LOG.debug(_('_get_tgt_iqn:iSCSI target iqn is:%s') % iqn) - - return (iqn, iscsiip_info['ctrid']) - else: - return (None, None) - - def _get_iscsi_ip_info(self, iscsiip): - """Get iSCSI IP infomation of storage device.""" - cli_cmd = ('showiscsiip') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) < 6: - return None - - iscsiIPinfo = {} - for i in range(6, len(en) - 2): - r = en[i].split() - if r[3] == iscsiip: - iscsiIPinfo['ctrid'] = r[0] - iscsiIPinfo['interfaceid'] = r[1] - iscsiIPinfo['portid'] = r[2] - iscsiIPinfo['ipaddress'] = r[3] - return iscsiIPinfo - return None - - def _map_lun(self, lunid, hostid, new_hostlun_id): - """Map a lun to a host. - - Here we give the hostlun ID which starts from 1. - """ - cli_cmd = ('addhostmap -host %(hostid)s -devlun %(lunid)s ' - '-hostlun %(hostlunid)s' - % {'hostid': hostid, - 'lunid': lunid, - 'hostlunid': new_hostlun_id}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_map_lun:Failed to add hostmap. ' - 'hostid:%(host)s ' - 'lunid:%(lun)s ' - 'hostlunid:%(hostlunid)s ' - 'out:%(out)s') - % {'host': hostid, - 'lun': lunid, - 'hostlunid': new_hostlun_id, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _get_hostlunid(self, hostid, lunid): - """Get the hostLUN ID of a LUN according host ID and LUN ID.""" - mapinfo = self._get_map_info(hostid) - if mapinfo: - for map in mapinfo: - if map['devlunid'] == lunid: - return map['hostlunid'] - return None - - def _delete_map(self, mapid, attempts=1): - """Remove the map.""" - cli_cmd = ('delhostmap -force -map %(mapid)s' - % {'mapid': mapid}) - while attempts >= 0: - attempts -= 1 - out = self._execute_cli(cli_cmd) - - # We retry to delete host map 10s later if there are - # IOs accessing the system. - if re.search('command operates successfully', out): - break - else: - if re.search('there are IOs accessing the system', out): - time.sleep(10) - LOG.debug(_('_delete_map:There are IOs accessing ' - 'the system. Retry to delete host map. ' - 'map id:%(mapid)s') - % {'mapid': mapid}) - continue - else: - err_msg = (_('_delete_map:Failed to delete host map.' - ' mapid:%(mapid)s ' - 'out:%(out)s') - % {'mapid': mapid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _delete_host(self, hostid): - """Delete a host.""" - cli_cmd = ('delhost -force -host %(hostid)s' - % {'hostid': hostid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_host: Failed delete host. ' - 'host id:%(hostid)s ' - 'out:%(out)s') - % {'hostid': hostid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _get_map_info(self, hostid): - """Get map infomation of the given host. - - This method return a map information list. Every item in the list - is a dictionary. The dictionary includes three keys: mapid, - devlunid, hostlunid. These items are sorted by hostlunid value - from small to large. - """ - cli_cmd = ('showhostmap -host %(hostid)s' - % {'hostid': hostid}) - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - mapinfo = [] - list_tmp = [] - list_key = ['mapid', 'devlunid', 'hostlunid'] - for i in range(6, len(en) - 2): - list_tmp = en[i].split() - list_val = [list_tmp[0], list_tmp[2], list_tmp[4]] - dic = dict(map(None, list_key, list_val)) - inserted = False - mapinfo_length = len(mapinfo) - if mapinfo_length == 0: - mapinfo.append(dic) - continue - for index in range(0, mapinfo_length): - if (int(mapinfo[mapinfo_length - index - 1]['hostlunid']) < - int(dic['hostlunid'])): - mapinfo.insert(mapinfo_length - index, dic) - inserted = True - break - if not inserted: - mapinfo.insert(0, dic) - return mapinfo - - def _get_device_type(self): - """Get the storage device type and product version.""" - cli_cmd = ('showsys') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - for line in en: - if re.search('Device Type', line): - if re.search('T$', line): - device_type = 'Tseries' - elif re.search('Dorado2100 G2$', line): - device_type = 'Dorado2100 G2' - elif re.search('Dorado5100$', line): - device_type = 'Dorado5100' - else: - device_type = None - continue - - if re.search('Product Version', line): - if re.search('V100R+', line): - product_version = 'V100R' - else: - product_version = None - break - - r = {'type': device_type, 'version': product_version} - return r - - def _active_snapshot(self, snapshotid): - """Active a snapshot.""" - cli_cmd = ('actvsnapshot -snapshot %(snapshotid)s' - % {'snapshotid': snapshotid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_active_snapshot:Failed to active snapshot. ' - 'snapshot id:%(name)s. ' - 'out:%(out)s') - % {'name': snapshotid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _disable_snapshot(self, snapshotid): - """Disable a snapshot.""" - cli_cmd = ('disablesnapshot -snapshot %(snapshotid)s' - % {'snapshotid': snapshotid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_disable_snapshot:Failed to disable snapshot. ' - 'snapshot id:%(id)s. ' - 'out:%(out)s') - % {'id': snapshotid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _delete_snapshot(self, snapshotid): - """Delete a snapshot.""" - cli_cmd = ('delsnapshot -snapshot %(snapshotid)s' - % {'snapshotid': snapshotid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_snapshot:Failed to delete snapshot. ' - 'snapshot id:%(id)s. ' - 'out:%(out)s') - % {'id': snapshotid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _create_volume(self, name, size): - """Create a new volume with the given name and size.""" - lunsetinfo = self._get_lun_set_info() - cli_cmd = ('createlun -n %(name)s -lunsize %(size)s ' - '-wrtype %(wrtype)s ' - % {'name': name, - 'size': size, - 'wrtype': lunsetinfo['WriteType']}) - - # If write type is "write through", no need to set mirror switch. - if lunsetinfo['WriteType'] != '2': - cli_cmd = cli_cmd + ('-mirrorsw %(mirrorsw)s ' - % {'mirrorsw': lunsetinfo['MirrorSwitch']}) - - # Differences exist between "Thin" and "thick" LUN for CLI commands. - luntype = lunsetinfo['LUNType'] - if luntype == 'Thin': - dorado2100g2_luntype = '2' - Tseries = ('-pool %(pool)s ' - % {'pool': lunsetinfo['StoragePool']}) - else: - dorado2100g2_luntype = '3' - Tseries = ('-rg %(raidgroup)s -susize %(susize)s ' - % {'raidgroup': lunsetinfo['StoragePool'], - 'susize': lunsetinfo['StripUnitSize']}) - - prefetch_value_or_times = '' - pretype = '-pretype %s ' % lunsetinfo['PrefetchType'] - # If constant prefetch, we should set prefetch value. - if lunsetinfo['PrefetchType'] == '1': - prefetch_value_or_times = '-value %s' % lunsetinfo['PrefetchValue'] - # If variable prefetch, we should set prefetch mutiple. - elif lunsetinfo['PrefetchType'] == '2': - prefetch_value_or_times = '-times %s' % lunsetinfo['PrefetchTimes'] - - if self.device_type['type'] == 'Tseries': - cli_cmd = cli_cmd + Tseries + pretype + prefetch_value_or_times - - elif self.device_type['type'] == 'Dorado5100': - cli_cmd = cli_cmd + ('-rg %(raidgroup)s -susize %(susize)s' - % {'raidgroup': lunsetinfo['StoragePool'], - 'susize': lunsetinfo['StripUnitSize']}) - - elif self.device_type['type'] == 'Dorado2100 G2': - cli_cmd = cli_cmd + ('-type %(type)s' - % {'type': dorado2100g2_luntype}) - - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_create_volume:Failed to Create volume. ' - 'volume name:%(name)s. ' - 'out:%(out)s') - % {'name': name, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _delete_volume(self, name, lunid): - """Delete a volume.""" - cli_cmd = ('dellun -force -lun %s' % (lunid)) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_volume:Failed to delete volume. ' - 'Volume name:%(name)s ' - 'out:%(out)s') - % {'name': name, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _create_luncopy(self, luncopyname, srclunid, tgtlunid): - """Create a LUNcopy.""" - cli_cmd = ('createluncopy -n %(name)s -l 4 -slun %(srclunid)s ' - '-tlun %(tgtlunid)s' - % {'name': luncopyname, - 'srclunid': srclunid, - 'tgtlunid': tgtlunid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_create_luncopy:Failed to Create LUNcopy. ' - 'LUNcopy name:%(name)s ' - 'out:%(out)s') - % {'name': luncopyname, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _start_luncopy(self, luncopyid): - """Starte a LUNcopy.""" - cli_cmd = ('chgluncopystatus -luncopy %(luncopyid)s -start' - % {'luncopyid': luncopyid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_start_luncopy:Failed to start LUNcopy. ' - 'LUNcopy id:%(luncopyid)s ' - 'out:%(out)s') - % {'luncopyid': luncopyid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _find_luncopy(self, luncopyname): - """Get the given LUNcopy's ID.""" - cli_cmd = ('showluncopy') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - for i in range(6, len(en) - 2): - r = en[i].split() - if r[0] == luncopyname: - luncopyid = r[1] - return luncopyid - return None - - def _wait_for_luncopy(self, luncopyname): - """Wait for LUNcopy to complete.""" - while True: - luncopy_info = self._get_luncopy_info(luncopyname) - if luncopy_info['state'] == 'Complete': - break - elif luncopy_info['status'] != 'Normal': - err_msg = (_('_wait_for_luncopy:LUNcopy status is not normal. ' - 'LUNcopy name:%(luncopyname)s') - % {'luncopyname': luncopyname}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - time.sleep(10) - - def _get_luncopy_info(self, luncopyname): - """Get LUNcopy information.""" - cli_cmd = ('showluncopy') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - luncopyinfo = {} - for i in range(6, len(en) - 2): - r = en[i].split() - if r[0] == luncopyname: - luncopyinfo['name'] = r[0] - luncopyinfo['id'] = r[1] - luncopyinfo['state'] = r[3] - luncopyinfo['status'] = r[4] - return luncopyinfo - return None - - def _delete_luncopy(self, luncopyid): - """Delete a LUNcopy.""" - cli_cmd = ('delluncopy -luncopy %(id)s' - % {'id': luncopyid}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_delete_luncopy:Failed to delete LUNcopy. ' - 'LUNcopy id:%(luncopyid)s ' - 'out:%(out)s') - % {'luncopyid': luncopyid, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _create_snapshot(self, snapshotname, srclunid): - """Create a snapshot with snapshot name and source LUN ID.""" - cli_cmd = ('createsnapshot -lun %(lunid)s -n %(snapname)s' - % {'lunid': srclunid, - 'snapname': snapshotname}) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_create_snapshot:Failed to Create snapshot. ' - 'Snapshot name:%(name)s ' - 'out:%(out)s') - % {'name': snapshotname, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _find_snapshot(self, snapshotname): - """Get the given snapshot ID.""" - cli_cmd = ('showsnapshot') - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 6: - return None - - for i in range(6, len(en) - 2): - r = en[i].split() - if r[0] == snapshotname: - return r[1] - return None - - def _get_lun_controller(self, lun_id): - cli_cmd = ('showlun -lun %s' % lun_id) - out = self._execute_cli(cli_cmd) - en = out.split('\r\n') - if len(en) <= 4: - return None - - if "Dorado2100 G2" == self.device_type['type']: - return en[10].split()[3] - else: - return en[12].split()[3] - - def _change_lun_controller(self, lun_id, controller): - cli_cmd = ('chglun -lun %s -c %s' % (lun_id, controller)) - out = self._execute_cli(cli_cmd) - if not re.search('command operates successfully', out): - err_msg = (_('_change_lun_controller:Failed to change lun owning ' - 'controller. lun id:%(lunid)s. ' - 'new controller:%(controller)s. ' - 'out:%(out)s') - % {'lunid': lun_id, - 'controller': controller, - 'out': out}) - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - def _is_resource_pool_enough(self): - """Check whether resource pools' valid size is more than 1G.""" - cli_cmd = ('showrespool') - out = self._execute_cli(cli_cmd) - en = re.split('\r\n', out) - if len(en) <= 6: - LOG.error(_('_is_resource_pool_enough:Resource pool for snapshot ' - 'not be added.')) - return False - resource_pools = [] - list_key = ['pool id', 'size', 'usage', 'valid size', - 'alarm threshold'] - for i in range(6, len(en) - 2): - list_val = en[i].split() - dic = dict(map(None, list_key, list_val)) - resource_pools.append(dic) - - for pool in resource_pools: - if float(pool['valid size']) < 1024.0: - return False - return True - - def _update_volume_stats(self): - """Retrieve stats info from volume group.""" - - LOG.debug(_("Updating volume stats")) - data = {} - backend_name = self.configuration.safe_get('volume_backend_name') - data["volume_backend_name"] = backend_name or 'HuaweiISCSIDriver' - data['vendor_name'] = 'Huawei' - data['driver_version'] = self.VERSION - data['storage_protocol'] = 'iSCSI' - - data['total_capacity_gb'] = 'infinite' - data['free_capacity_gb'] = self._get_free_capacity() - data['reserved_percentage'] = 0 - - self._stats = data - - def _get_free_capacity(self): - """Get total free capacity of pools.""" - self.login_info = self._get_login_info() - root = self._read_xml() - lun_type = root.findtext('LUN/LUNType') - if self.device_type['type'] == 'Dorado2100 G2': - lun_type = 'Thin' - elif (self.device_type['type'] == 'Dorado5100' or not lun_type): - lun_type = 'Thick' - poolinfo_dev = self._find_pool_info(lun_type) - pools_conf = root.findall('LUN/StoragePool') - total_free_capacity = 0.0 - for poolinfo in poolinfo_dev: - if self.device_type['type'] == 'Dorado2100 G2': - total_free_capacity += float(poolinfo[2]) - continue - for pool in pools_conf: - if ((self.device_type['type'] == 'Dorado5100') and - (poolinfo[5] == pool.attrib['Name'])): - total_free_capacity += float(poolinfo[3]) - break - else: - if ((lun_type == 'Thick') and - (poolinfo[5] == pool.attrib['Name'])): - total_free_capacity += float(poolinfo[3]) - break - elif poolinfo[1] == pool.attrib['Name']: - total_free_capacity += float(poolinfo[4]) - break - - return total_free_capacity / 1024 diff --git a/cinder/volume/drivers/huawei/huawei_t.py b/cinder/volume/drivers/huawei/huawei_t.py new file mode 100644 index 000000000..ed5dbc3dd --- /dev/null +++ b/cinder/volume/drivers/huawei/huawei_t.py @@ -0,0 +1,361 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2012 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. +""" +Volume Drivers for Huawei OceanStor T series storage arrays. +""" + +import re +import time + +from cinder import exception +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.huawei import ssh_common + + +LOG = logging.getLogger(__name__) + +HOST_PORT_PREFIX = 'HostPort_' + + +class HuaweiTISCSIDriver(driver.ISCSIDriver): + """ISCSI driver for Huawei OceanStor T series storage arrays.""" + + VERSION = '1.1.0' + + def __init__(self, *args, **kwargs): + super(HuaweiTISCSIDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Instantiate common class.""" + self.common = ssh_common.TseriesCommon(configuration= + self.configuration) + self.common.do_setup(context) + self._assert_cli_out = self.common._assert_cli_out + self._assert_cli_operate_out = self.common._assert_cli_operate_out + + def check_for_setup_error(self): + """Check something while starting.""" + self.common.check_for_setup_error() + + def create_volume(self, volume): + """Create a new volume.""" + volume_id = self.common.create_volume(volume) + return {'provider_location': volume_id} + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot.""" + volume_id = self.common.create_volume_from_snapshot(volume, snapshot) + return {'provider_location': volume_id} + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the specified volume.""" + volume_id = self.common.create_cloned_volume(volume, src_vref) + return {'provider_location': volume_id} + + def delete_volume(self, volume): + """Delete a volume.""" + self.common.delete_volume(volume) + + def create_export(self, context, volume): + """Export the volume.""" + pass + + def ensure_export(self, context, volume): + """Synchronously recreate an export for a volume.""" + pass + + def remove_export(self, context, volume): + """Remove an export for a volume.""" + pass + + def create_snapshot(self, snapshot): + """Create a snapshot.""" + snapshot_id = self.common.create_snapshot(snapshot) + return {'provider_location': snapshot_id} + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + self.common.delete_snapshot(snapshot) + + def initialize_connection(self, volume, connector): + """Map a volume to a host and return target iSCSI information.""" + LOG.debug(_('initialize_connection: volume name: %(vol)s ' + 'host: %(host)s initiator: %(ini)s') + % {'vol': volume['name'], + 'host': connector['host'], + 'ini': connector['initiator']}) + + self.common._update_login_info() + (iscsi_iqn, target_ip, port_ctr) =\ + self._get_iscsi_params(connector['initiator']) + + # First, add a host if not added before. + host_id = self.common.add_host(connector['host']) + + # Then, add the iSCSI port to the host. + self._add_iscsi_port_to_host(host_id, connector) + + # Finally, map the volume to the host. + volume_id = volume['provider_location'] + hostlun_id = self.common.map_volume(host_id, volume_id) + + # Change LUN ctr for better performance, just for single path. + lun_details = self.common.get_lun_details(volume_id) + if (lun_details['LunType'] == 'THICK' and + lun_details['OwningController'] != port_ctr): + self.common.change_lun_ctr(volume_id, port_ctr) + + properties = {} + properties['target_discovered'] = False + properties['target_portal'] = ('%s:%s' % (target_ip, '3260')) + properties['target_iqn'] = iscsi_iqn + properties['target_lun'] = int(hostlun_id) + properties['volume_id'] = volume['id'] + auth = volume['provider_auth'] + if auth: + (auth_method, auth_username, auth_secret) = auth.split() + + properties['auth_method'] = auth_method + properties['auth_username'] = auth_username + properties['auth_password'] = auth_secret + + return {'driver_volume_type': 'iscsi', 'data': properties} + + def _get_iscsi_params(self, initiator): + """Get target iSCSI params, including iqn and IP.""" + conf_file = self.common.configuration.cinder_huawei_conf_file + iscsi_conf = self._get_iscsi_conf(conf_file) + target_ip = None + for ini in iscsi_conf['Initiator']: + if ini['Name'] == initiator: + target_ip = ini['TargetIP'] + break + # If didn't specify target IP for some initiator, use default IP. + if not target_ip: + if iscsi_conf['DefaultTargetIP']: + target_ip = iscsi_conf['DefaultTargetIP'] + + else: + msg = (_('_get_iscsi_params: Failed to get target IP ' + 'for initiator %(ini)s, please check config file.') + % {'ini': initiator}) + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + (target_iqn, port_ctr) = self._get_tgt_iqn(target_ip) + return (target_iqn, target_ip, port_ctr) + + def _get_iscsi_conf(self, filename): + """Get iSCSI info from config file. + + This function returns a dict: + {'DefaultTargetIP': '11.11.11.11', + 'Initiator': [{'Name': 'iqn.xxxxxx.1', 'TargetIP': '11.11.11.12'}, + {'Name': 'iqn.xxxxxx.2', 'TargetIP': '11.11.11.13'} + ] + } + + """ + + iscsiinfo = {} + root = ssh_common.parse_xml_file(filename) + + default_ip = root.findtext('iSCSI/DefaultTargetIP') + if default_ip: + iscsiinfo['DefaultTargetIP'] = default_ip.strip() + else: + iscsiinfo['DefaultTargetIP'] = None + initiator_list = [] + tmp_dic = {} + for dic in root.findall('iSCSI/Initiator'): + # Strip the values of dict. + for k, v in dic.items(): + tmp_dic[k] = v.strip() + initiator_list.append(tmp_dic) + iscsiinfo['Initiator'] = initiator_list + return iscsiinfo + + def _get_tgt_iqn(self, port_ip): + """Run CLI command to get target iSCSI iqn. + + The iqn is formed with three parts: + iSCSI target name + iSCSI port info + iSCSI IP + + """ + + LOG.debug(_('_get_tgt_iqn: iSCSI IP is %s.') % port_ip) + + cli_cmd = 'showiscsitgtname' + out = self.common._execute_cli(cli_cmd) + + self._assert_cli_out(re.search('ISCSI Name', out), + '_get_tgt_iqn', + 'Failed to get iSCSI target %s iqn.' % port_ip, + cli_cmd, out) + + lines = out.split('\r\n') + index = lines[4].index('iqn') + iqn_prefix = lines[4][index:].strip() + # Here we make sure port_info won't be None. + port_info = self._get_iscsi_tgt_port_info(port_ip) + ctr = ('0' if port_info[0] == 'A' else '1') + interface = '0' + port_info[1] + port = '0' + port_info[2][1:] + iqn_suffix = ctr + '02' + interface + port + # iqn_suffix should not start with 0 + while(True): + if iqn_suffix.startswith('0'): + iqn_suffix = iqn_suffix[1:] + else: + break + + iqn = iqn_prefix + ':' + iqn_suffix + ':' + port_info[3] + + LOG.debug(_('_get_tgt_iqn: iSCSI target iqn is %s') % iqn) + + return (iqn, port_info[0]) + + def _get_iscsi_tgt_port_info(self, port_ip): + """Get iSCSI Port information of storage device.""" + cli_cmd = 'showiscsiip' + out = self.common._execute_cli(cli_cmd) + if re.search('iSCSI IP Information', out): + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if tmp_line[3] == port_ip: + return tmp_line + + err_msg = _('_get_iscsi_tgt_port_info: Failed to get iSCSI port ' + 'info. Please make sure the iSCSI port IP %s is ' + 'configured in array.') % port_ip + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + def _add_iscsi_port_to_host(self, hostid, connector, multipathtype=0): + """Add an iSCSI port to the given host. + + First, add an initiator if needed, the initiator is equivalent to + an iSCSI port. Then, add the initiator to host if not added before. + + """ + + initiator = connector['initiator'] + # Add an iSCSI initiator. + if not self._initiator_added(initiator): + self._add_initiator(initiator) + # Add the initiator to host if not added before. + port_name = HOST_PORT_PREFIX + str(hash(initiator)) + portadded = False + hostport_info = self.common._get_host_port_info(hostid) + if hostport_info: + for hostport in hostport_info: + if hostport[2] == initiator: + portadded = True + break + if not portadded: + cli_cmd = ('addhostport -host %(id)s -type 5 ' + '-info %(info)s -n %(name)s -mtype %(multype)s' + % {'id': hostid, + 'info': initiator, + 'name': port_name, + 'multype': multipathtype}) + out = self.common._execute_cli(cli_cmd) + + msg = ('Failed to add iSCSI port %(port)s to host %(host)s' + % {'port': port_name, + 'host': hostid}) + self._assert_cli_operate_out('_add_iscsi_port_to_host', + msg, cli_cmd, out) + + def _initiator_added(self, ininame): + """Check whether the initiator is already added.""" + cli_cmd = 'showiscsiini -ini %(name)s' % {'name': ininame} + out = self.common._execute_cli(cli_cmd) + return (True if re.search('Initiator Information', out) else False) + + def _add_initiator(self, ininame): + """Add a new initiator to storage device.""" + cli_cmd = 'addiscsiini -n %(name)s' % {'name': ininame} + out = self.common._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_add_iscsi_host_port', + 'Failed to add initiator %s' % ininame, + cli_cmd, out) + + def _delete_initiator(self, ininame, attempts=2): + """Delete an initiator.""" + cli_cmd = 'deliscsiini -n %(name)s' % {'name': ininame} + while(attempts > 0): + out = self.common._execute_cli(cli_cmd) + if re.search('the port is in use', out): + attempts -= 1 + time.sleep(2) + else: + break + + self._assert_cli_operate_out('_map_lun', + 'Failed to delete initiator %s.' + % ininame, + cli_cmd, out) + + def terminate_connection(self, volume, connector, **kwargs): + """Terminate the map.""" + LOG.debug(_('terminate_connection: volume: %(vol)s host: %(host)s ' + 'connector: %(initiator)s') + % {'vol': volume['name'], + 'host': connector['host'], + 'initiator': connector['initiator']}) + + self.common._update_login_info() + host_id = self.common.remove_map(volume['provider_location'], + connector['host']) + if not self.common._get_host_map_info(host_id): + self._remove_iscsi_port(host_id, connector) + + def _remove_iscsi_port(self, hostid, connector): + """Remove iSCSI ports and delete host.""" + initiator = connector['initiator'] + # Delete the host initiator if no LUN mapped to it. + port_num = 0 + port_info = self.common._get_host_port_info(hostid) + if port_info: + port_num = len(port_info) + for port in port_info: + if port[2] == initiator: + self.common._delete_hostport(port[0]) + self._delete_initiator(initiator) + port_num -= 1 + break + else: + LOG.warn(_('_remove_iscsi_port: iSCSI port was not found ' + 'on host %(hostid)s') % {'hostid': hostid}) + + # Delete host if no initiator added to it. + if port_num == 0: + self.common._delete_host(hostid) + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + self._stats = self.common.get_volume_stats(refresh) + self._stats['storage_protocol'] = 'iSCSI' + self._stats['driver_version'] = self.VERSION + backend_name = self.configuration.safe_get('volume_backend_name') + self._stats['volume_backend_name'] = (backend_name or + self.__class__.__name__) + return self._stats diff --git a/cinder/volume/drivers/huawei/ssh_common.py b/cinder/volume/drivers/huawei/ssh_common.py new file mode 100644 index 000000000..ef507d70e --- /dev/null +++ b/cinder/volume/drivers/huawei/ssh_common.py @@ -0,0 +1,1129 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2012 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. +""" +Common classes for Huawei OceanStor T series and Dorado series storage arrays. + +The common classes provide the drivers command line operation using SSH. +""" + +import base64 +import re +import socket +import threading +import time + +from xml.etree import ElementTree as ET + +from cinder import context +from cinder import exception +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume import volume_types + + +LOG = logging.getLogger(__name__) + +HOST_GROUP_NAME = 'HostGroup_OpenStack' +HOST_NAME_PREFIX = 'Host_' +VOL_AND_SNAP_NAME_PREFIX = 'OpenStack_' + + +def parse_xml_file(filepath): + """Get root of xml file.""" + try: + tree = ET.parse(filepath) + root = tree.getroot() + return root + except IOError as err: + LOG.error(_('parse_xml_file: %s') % err) + raise exception.ConfigNotFound(path=err) + + +def ssh_read(user, channel, cmd, timeout): + """Get results of CLI commands.""" + result = '' + channel.settimeout(timeout) + while True: + try: + result = result + channel.recv(8192) + except socket.timeout: + raise exception.CinderException(_('ssh_read: Read ' + 'SSH timeout')) + else: + # Complete CLI response starts with CLI cmd and + # ends with "username:/>". + if result.startswith(cmd) and result.endswith(user + ':/>'): + if not re.search('Welcome', result): + break + # CLI returns welcome information when first log in. + elif re.search(user + ':/>' + cmd, result): + break + # Some commands need to send 'y'. + elif re.search('(y/n)', result): + break + # Reach maximum limit of SSH connection. + elif re.search('No response message', result): + msg = _('No response message. Please check system status.') + raise exception.CinderException(msg) + # Filter the last line: username:/> . + result = '\r\n'.join(result.split('\r\n')[:-1]) + # Filter welcome information. + index = result.find(user + ':/>') + + return (result[index:] if index > -1 else result) + + +class TseriesCommon(): + """Common class for Huawei T series storage arrays.""" + + def __init__(self, configuration=None): + self.configuration = configuration + self.xml_conf = self.configuration.cinder_huawei_conf_file + self.login_info = {} + self.lun_distribution = [0, 0] + self.hostgroup_id = None + self.ssh_pool = None + self.lock_ip = threading.Lock() + self.luncopy_list = [] # to storage LUNCopy name + + def do_setup(self, context): + """Check config file.""" + LOG.debug(_('do_setup.')) + + self._check_conf_file() + self.login_info = self._get_login_info() + self.lun_distribution = self._get_lun_distribution_info() + self.luncopy_list = self._get_all_luncopy_name() + + def _check_conf_file(self): + """Check config file, make sure essential items are set.""" + root = parse_xml_file(self.xml_conf) + IP1 = root.findtext('Storage/ControllerIP0') + IP2 = root.findtext('Storage/ControllerIP1') + username = root.findtext('Storage/UserName') + pwd = root.findtext('Storage/UserPassword') + pool_node = root.findall('LUN/StoragePool') + + if (not IP1 or not IP2) or (not username) or (not pwd): + err_msg = (_('_check_conf_file: Config file invalid. Controler IP,' + ' UserName and UserPassword must be set.')) + LOG.error(err_msg) + raise exception.InvalidInput(reason=err_msg) + + for pool in pool_node: + if pool.attrib['Name']: + return + # If pool_node is None or pool.attrib['Name'] is None. + err_msg = (_('_check_conf_file: Config file invalid. ' + 'StoragePool must be set.')) + LOG.error(err_msg) + raise exception.InvalidInput(reason=err_msg) + + def _get_login_info(self): + """Get login IP, username and password from config file.""" + logininfo = {} + filename = self.configuration.cinder_huawei_conf_file + tree = ET.parse(filename) + root = tree.getroot() + logininfo['ControllerIP0'] =\ + root.findtext('Storage/ControllerIP0').strip() + logininfo['ControllerIP1'] =\ + root.findtext('Storage/ControllerIP1').strip() + + need_encode = False + for key in ['UserName', 'UserPassword']: + node = root.find('Storage/%s' % key) + node_text = node.text.strip() + # Prefix !$$$ means encoded already. + if node_text.find('!$$$') > -1: + logininfo[key] = base64.b64decode(node_text[4:]) + else: + logininfo[key] = node_text + node.text = '!$$$' + base64.b64encode(node_text) + need_encode = True + if need_encode: + self._change_file_mode(filename) + try: + tree.write(filename, 'UTF-8') + except Exception as err: + LOG.info(_('_get_login_info: %s') % err) + + return logininfo + + def _change_file_mode(self, filepath): + utils.execute('chmod', '777', filepath, run_as_root=True) + + def _get_lun_distribution_info(self): + """Get LUN distribution information. + + For we have two controllers for each array, we want to make all + LUNs(just for Thick LUN) distributed evenly. The driver uses the + LUN distribution info to determine in which controller to create + a new LUN. + + """ + + luns = self._get_all_luns_info() + ctr_info = [0, 0] + for lun in luns: + if (lun[6].startswith(VOL_AND_SNAP_NAME_PREFIX) and + lun[8] == 'THICK'): + if lun[4] == 'A': + ctr_info[0] += 1 + else: + ctr_info[1] += 1 + return ctr_info + + def check_for_setup_error(self): + pass + + def _get_all_luncopy_name(self): + cli_cmd = 'showluncopy' + out = self._execute_cli(cli_cmd) + luncopy_ids = [] + if re.search('LUN Copy Information', out): + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if tmp_line[0].startswith(VOL_AND_SNAP_NAME_PREFIX): + luncopy_ids.append(tmp_line[0]) + return luncopy_ids + + def create_volume(self, volume): + """Create a new volume.""" + volume_name = self._name_translate(volume['name']) + + LOG.debug(_('create_volume: volume name: %s.') % volume_name) + + self._update_login_info() + if int(volume['size']) == 0: + volume_size = '100M' + else: + volume_size = '%sG' % volume['size'] + type_id = volume['volume_type_id'] + parameters = self._parse_volume_type(type_id) + volume_id = self._create_volume(volume_name, volume_size, parameters) + return volume_id + + def _name_translate(self, name): + """Form new names for volume and snapshot because of + 32-character limit on names. + """ + newname = VOL_AND_SNAP_NAME_PREFIX + str(hash(name)) + + LOG.debug(_('_name_translate: Name in cinder: %(old)s, new name in ' + 'storage system: %(new)s') % {'old': name, 'new': newname}) + + return newname + + def _update_login_info(self): + """Update user name and password.""" + self.login_info = self._get_login_info() + + def _parse_volume_type(self, typeid): + """Parse volume type form extra_specs by type id. + + The keys in extra_specs must be consistent with the element in config + file. And the keys can starts with "drivers" to make them distinguished + from capabilities keys, if you like. + + """ + + params = self._get_lun_params() + if typeid is not None: + ctxt = context.get_admin_context() + volume_type = volume_types.get_volume_type(ctxt, typeid) + specs = volume_type.get('extra_specs') + for key, value in specs.iteritems(): + key_split = key.split(':') + if len(key_split) > 1: + if key_split[0] == 'drivers': + key = key_split[1] + else: + continue + else: + key = key_split[0] + + if key in params.keys(): + params[key] = value.strip() + else: + conf = self.configuration.cinder_huawei_conf_file + LOG.warn(_('_parse_volume_type: Unacceptable parameter ' + '%(key)s. Please check this key in extra_specs ' + 'and make it consistent with the element in ' + 'configuration file %(conf)s.') + % {'key': key, + 'conf': conf}) + + return params + + def _create_volume(self, name, size, params): + """Create a new volume with the given name and size.""" + cli_cmd = ('createlun -n %(name)s -lunsize %(size)s ' + '-wrtype %(wrtype)s ' % {'name': name, + 'size': size, + 'wrtype': params['WriteType']}) + + # If write type is "write through", no need to set mirror switch. + if params['WriteType'] != '2': + cli_cmd = cli_cmd + ('-mirrorsw %(mirrorsw)s ' + % {'mirrorsw': params['MirrorSwitch']}) + + # Differences exist between "Thin" and "thick" LUN in CLI commands. + luntype = params['LUNType'] + ctr = None + if luntype == 'Thin': + cli_cmd = cli_cmd + ('-pool %(pool)s ' + % {'pool': params['StoragePool']}) + else: + # Make LUN distributed to A/B controllers evenly, + # just for Thick LUN. + ctr = self._calculate_lun_ctr() + cli_cmd = cli_cmd + ('-rg %(raidgroup)s -susize %(susize)s ' + '-c %(ctr)s ' + % {'raidgroup': params['StoragePool'], + 'susize': params['StripUnitSize'], + 'ctr': ctr}) + + prefetch_value_or_times = '' + pretype = '-pretype %s ' % params['PrefetchType'] + # If constant prefetch, we should specify prefetch value. + if params['PrefetchType'] == '1': + prefetch_value_or_times = '-value %s' % params['PrefetchValue'] + # If variable prefetch, we should specify prefetch mutiple. + elif params['PrefetchType'] == '2': + prefetch_value_or_times = '-times %s' % params['PrefetchTimes'] + + cli_cmd = cli_cmd + pretype + prefetch_value_or_times + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_create_volume', + 'Failed to create volume %s' % name, + cli_cmd, out) + if ctr: + self._update_lun_distribution(ctr) + + return self._get_lun_id(name) + + def _calculate_lun_ctr(self): + return ('a' if self.lun_distribution[0] <= self.lun_distribution[1] + else 'b') + + def _update_lun_distribution(self, ctr): + index = (0 if ctr == 'a' else 1) + self.lun_distribution[index] += 1 + + def _get_lun_params(self): + params_conf = self._parse_conf_lun_params() + # Select a pool with maximum capacity. + pools_dev = self._get_dev_pool_info(params_conf['LUNType']) + params_conf['StoragePool'] = \ + self._get_maximum_capacity_pool_id(params_conf['StoragePool'], + pools_dev, + params_conf['LUNType']) + return params_conf + + def _parse_conf_lun_params(self): + """Get parameters from config file for creating LUN.""" + # Default LUN parameters. + conf_params = {'LUNType': 'Thin', + 'StripUnitSize': '64', + 'WriteType': '1', + 'MirrorSwitch': '1', + 'PrefetchType': '3', + 'PrefetchValue': '0', + 'PrefetchTimes': '0', + 'StoragePool': []} + + root = parse_xml_file(self.xml_conf) + + luntype = root.findtext('LUN/LUNType') + if luntype: + if luntype.strip() in ['Thick', 'Thin']: + conf_params['LUNType'] = luntype.strip() + else: + err_msg = (_('LUNType must be "Thin" or "Thick". ' + 'LUNType:%(type)s') % {'type': luntype}) + raise exception.InvalidInput(reason=err_msg) + + stripunitsize = root.findtext('LUN/StripUnitSize') + if stripunitsize: + conf_params['StripUnitSize'] = stripunitsize.strip() + writetype = root.findtext('LUN/WriteType') + if writetype: + conf_params['WriteType'] = writetype.strip() + mirrorswitch = root.findtext('LUN/MirrorSwitch') + if mirrorswitch: + conf_params['MirrorSwitch'] = mirrorswitch.strip() + prefetch = root.find('LUN/Prefetch') + if prefetch is not None and prefetch.attrib['Type']: + conf_params['PrefetchType'] = prefetch.attrib['Type'].strip() + if conf_params['PrefetchType'] == '1': + conf_params['PrefetchValue'] = prefetch.attrib['Value'].strip() + elif conf_params['PrefetchType'] == '2': + conf_params['PrefetchTimes'] = prefetch.attrib['Value'].strip() + else: + LOG.debug(_('_parse_conf_lun_params: Use default prefetch type. ' + 'Prefetch type: Intelligent.')) + + pools_conf = root.findall('LUN/StoragePool') + for pool in pools_conf: + conf_params['StoragePool'].append(pool.attrib['Name'].strip()) + + return conf_params + + def _get_maximum_capacity_pool_id(self, pools_conf, pools_dev, luntype): + """Get the maximum pool from config file. + + According to the given pools' names in config file, + we select the pool with maximum free capacity. + + """ + + maxpool_id = None + maxpool_size = 0.0 + nameindex, sizeindex = ((1, 4) if luntype == 'Thin' else (5, 3)) + pools_dev = sorted(pools_dev, key=lambda x: int(x[sizeindex])) + while len(pools_dev) > 0: + pool = pools_dev.pop() + if pool[nameindex] in pools_conf: + return pool[0] + + err_msg = (_('_get_maximum_capacity_pool_id: Failed to get pool ' + 'id. Please check config file and make sure ' + 'the StoragePool %s is created in storage ' + 'array.') % pools_conf) + raise exception.InvalidInput(reason=err_msg) + + def _execute_cli(self, cmd): + """Build SSH connection and execute CLI commands. + + If the connection to first controller timeout, + try to connect to the other controller. + + """ + + LOG.debug(_('CLI command: %s') % cmd) + connect_times = 1 + ip0 = self.login_info['ControllerIP0'] + ip1 = self.login_info['ControllerIP1'] + user = self.login_info['UserName'] + pwd = self.login_info['UserPassword'] + if not self.ssh_pool: + self.ssh_pool = utils.SSHPool(ip0, 22, 30, user, pwd, max_size=2) + ssh_client = None + while True: + try: + if connect_times == 2: + # Switch to the other controller. + with self.lock_ip: + if ssh_client: + if ssh_client.server_ip == self.ssh_pool.ip: + self.ssh_pool.ip = (ip1 + if self.ssh_pool.ip == ip0 + else ip0) + old_ip = ssh_client.server_ip + # Create a new client to replace the old one. + if getattr(ssh_client, 'chan', None): + ssh_client.chan.close() + ssh_client.close() + ssh_client = self.ssh_pool.create() + self._reset_transport_timeout(ssh_client, 0.1) + else: + self.ssh_pool.ip = ip1 + old_ip = ip0 + + LOG.info(_('_execute_cli: Can not connect to IP ' + '%(old)s, try to connect to the other ' + 'IP %(new)s.') + % {'old': old_ip, 'new': self.ssh_pool.ip}) + + if not ssh_client: + # Get an SSH client from SSH pool. + ssh_client = self.ssh_pool.get() + self._reset_transport_timeout(ssh_client, 0.1) + # "server_ip" shows the IP of SSH server. + if not getattr(ssh_client, 'server_ip', None): + with self.lock_ip: + setattr(ssh_client, 'server_ip', self.ssh_pool.ip) + # An SSH client owns one "chan". + if not getattr(ssh_client, 'chan', None): + setattr(ssh_client, 'chan', + utils.create_channel(ssh_client, 600, 800)) + + while True: + ssh_client.chan.send(cmd + '\n') + out = ssh_read(user, ssh_client.chan, cmd, 20) + if out.find('(y/n)') > -1: + cmd = 'y' + else: + # Put SSH client back into SSH pool. + self.ssh_pool.put(ssh_client) + return out + + except Exception as err: + if connect_times < 2: + connect_times += 1 + continue + else: + if ssh_client: + self.ssh_pool.remove(ssh_client) + raise err + + def _reset_transport_timeout(self, ssh, time): + transport = ssh.get_transport() + transport.sock.settimeout(time) + + def delete_volume(self, volume): + volume_name = self._name_translate(volume['name']) + + LOG.debug(_('delete_volume: volume name: %s.') % volume_name) + + self._update_login_info() + volume_id = volume.get('provider_location', None) + if (volume_id is not None) and self._check_volume_created(volume_id): + self._delete_volume(volume_id) + else: + err_msg = (_('delete_volume: Volume %(name)s does not exist.') + % {'name': volume['name']}) + LOG.warn(err_msg) + + def _check_volume_created(self, volume_id): + cli_cmd = 'showlun -lun %s' % volume_id + out = self._execute_cli(cli_cmd) + return (True if re.search('LUN Information', out) else False) + + def _delete_volume(self, volumeid): + """Run CLI command to delete volume.""" + cli_cmd = 'dellun -force -lun %s' % volumeid + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_volume', + ('Failed to delete volume. volume id: %s' + % volumeid), + cli_cmd, out) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot. + + We use LUNcopy to copy a new volume from snapshot. + The time needed increases as volume size does. + + """ + + snapshot_name = self._name_translate(snapshot['name']) + volume_name = self._name_translate(volume['name']) + + LOG.debug(_('create_volume_from_snapshot: snapshot ' + 'name: %(snapshot)s, volume name: %(volume)s.') + % {'snapshot': snapshot_name, + 'volume': volume_name}) + + self._update_login_info() + snapshot_id = snapshot.get('provider_location', None) + if not snapshot_id: + snapshot_id = self._get_snapshot_id(snapshot_name) + if snapshot_id is None: + err_msg = (_('create_volume_from_snapshot: Snapshot %(name)s ' + 'does not exist.') + % {'name': snapshot_name}) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + # Create a target LUN. + if int(volume['size']) == 0: + volume_size = '%sG' % snapshot['volume_size'] + else: + volume_size = '%sG' % volume['size'] + type_id = volume['volume_type_id'] + parameters = self._parse_volume_type(type_id) + tgt_vol_id = self._create_volume(volume_name, volume_size, parameters) + self._copy_volume(snapshot_id, tgt_vol_id) + + return tgt_vol_id + + def _copy_volume(self, src_vol_id, tgt_vol_id): + """Copy a volume or snapshot to target volume.""" + luncopy_name = VOL_AND_SNAP_NAME_PREFIX + src_vol_id + '_' + tgt_vol_id + self._create_luncopy(luncopy_name, src_vol_id, tgt_vol_id) + self.luncopy_list.append(luncopy_name) + luncopy_id = self._get_luncopy_info(luncopy_name)[1] + try: + self._start_luncopy(luncopy_id) + self._wait_for_luncopy(luncopy_name) + # Delete the target volume if LUNcopy failed. + except Exception: + with excutils.save_and_reraise_exception(): + # Need to remove the LUNcopy of the volume first. + self._delete_luncopy(luncopy_id) + self.luncopy_list.remove(luncopy_name) + self._delete_volume(tgt_vol_id) + # Need to delete LUNcopy finally. + self._delete_luncopy(luncopy_id) + self.luncopy_list.remove(luncopy_name) + + def _create_luncopy(self, luncopyname, srclunid, tgtlunid): + """Run CLI command to create LUNcopy.""" + cli_cmd = ('createluncopy -n %(name)s -l 4 -slun %(srclunid)s ' + '-tlun %(tgtlunid)s' % {'name': luncopyname, + 'srclunid': srclunid, + 'tgtlunid': tgtlunid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_create_luncopy', + ('Failed to create LUNcopy %s' + % luncopyname), + cli_cmd, out) + + def _start_luncopy(self, luncopyid): + """Run CLI command to start LUNcopy.""" + cli_cmd = ('chgluncopystatus -luncopy %(luncopyid)s -start' + % {'luncopyid': luncopyid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_start_luncopy', + 'Failed to start LUNcopy %s' % luncopyid, + cli_cmd, out) + + def _wait_for_luncopy(self, luncopyname): + """Wait for LUNcopy to complete.""" + while True: + luncopy_info = self._get_luncopy_info(luncopyname) + # If state is complete + if luncopy_info[3] == 'Complete': + break + # If status is not normal + elif luncopy_info[4] != 'Normal': + err_msg = (_('_wait_for_luncopy: LUNcopy %(luncopyname)s ' + 'status is %(status)s.') + % {'luncopyname': luncopyname, + 'status': luncopy_info[4]}) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + time.sleep(10) + + def _get_luncopy_info(self, luncopyname): + """Return a LUNcopy information list.""" + cli_cmd = 'showluncopy' + out = self._execute_cli(cli_cmd) + + self._assert_cli_out(re.search('LUN Copy Information', out), + '_get_luncopy_info', + 'No LUNcopy information was found.', + cli_cmd, out) + + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if tmp_line[0] == luncopyname: + return tmp_line + return None + + def _delete_luncopy(self, luncopyid): + """Run CLI command to delete LUNcopy.""" + cli_cmd = 'delluncopy -luncopy %(id)s' % {'id': luncopyid} + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_luncopy', + 'Failed to delete LUNcopy %s' % luncopyid, + cli_cmd, out) + + def create_cloned_volume(self, tgt_volume, src_volume): + src_vol_name = self._name_translate(src_volume['name']) + tgt_vol_name = self._name_translate(tgt_volume['name']) + + LOG.debug(_('create_cloned_volume: src volume: %(src)s ' + 'tgt volume: %(tgt)s') % {'src': src_vol_name, + 'tgt': tgt_vol_name}) + + self._update_login_info() + src_vol_id = src_volume.get('provider_location', None) + if not src_vol_id: + src_vol_id = self._get_lun_id(src_vol_name) + if src_vol_id is None: + err_msg = (_('Source volume %(name)s does not exist.') + % {'name': src_vol_name}) + LOG.error(err_msg) + raise exception.VolumeNotFound(volume_id=src_vol_name) + + # Create a target volume. + if int(tgt_volume['size']) == 0: + tgt_vol_size = '%sG' % src_vol_name['size'] + else: + tgt_vol_size = '%sG' % tgt_volume['size'] + type_id = tgt_volume['volume_type_id'] + params = self._parse_volume_type(type_id) + tgt_vol_id = self._create_volume(tgt_vol_name, tgt_vol_size, params) + self._copy_volume(src_vol_id, tgt_vol_id) + + return tgt_vol_id + + def _get_all_luns_info(self): + cli_cmd = 'showlun' + out = self._execute_cli(cli_cmd) + luns = [] + if re.search('LUN Information', out): + for line in out.split('\r\n')[6:-2]: + luns.append(line.replace('Not format', 'Notformat').split()) + return luns + + def _get_lun_id(self, lun_name): + luns = self._get_all_luns_info() + if luns: + for lun in luns: + if lun[6] == lun_name: + return lun[0] + return None + + def create_snapshot(self, snapshot): + snapshot_name = self._name_translate(snapshot['name']) + volume_name = self._name_translate(snapshot['volume_name']) + + LOG.debug(_('create_snapshot: snapshot name: %(snapshot)s ' + 'volume name: %(volume)s') + % {'snapshot': snapshot_name, + 'volume': volume_name}) + + if self._resource_pool_enough() is False: + err_msg = (_('create_snapshot: ' + 'Resource pool needs 1GB valid size at least.')) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + lun_id = self._get_lun_id(volume_name) + if lun_id is None: + err_msg = (_('create_snapshot: Volume %(name)s does not exist.') + % {'name': volume_name}) + LOG.error(err_msg) + raise exception.VolumeNotFound(volume_id=volume_name) + + self._create_snapshot(snapshot_name, lun_id) + snapshot_id = self._get_snapshot_id(snapshot_name) + try: + self._active_snapshot(snapshot_id) + except Exception: + with excutils.save_and_reraise_exception(): + self._delete_snapshot(snapshot_id) + + return snapshot_id + + def _resource_pool_enough(self): + """Check whether resource pools' valid size is more than 1GB.""" + cli_cmd = 'showrespool' + out = self._execute_cli(cli_cmd) + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if float(tmp_line[3]) < 1024.0: + return False + + return True + + def _create_snapshot(self, snapshotname, srclunid): + """Create a snapshot with snapshot name and source LUN ID.""" + cli_cmd = ('createsnapshot -lun %(lunid)s -n %(snapname)s' + % {'lunid': srclunid, + 'snapname': snapshotname}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_create_snapshot', + ('Failed to create snapshot %s' + % snapshotname), + cli_cmd, out) + + def _get_snapshot_id(self, snapshotname): + cli_cmd = 'showsnapshot' + out = self._execute_cli(cli_cmd) + if re.search('Snapshot Information', out): + for line in out.split('\r\n')[6:-2]: + emp_line = line.split() + if emp_line[0] == snapshotname: + return emp_line[1] + return None + + def _active_snapshot(self, snapshotid): + """Run CLI command to active snapshot.""" + cli_cmd = ('actvsnapshot -snapshot %(snapshotid)s' + % {'snapshotid': snapshotid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_active_snapshot', + ('Failed to active snapshot %s' + % snapshotid), + cli_cmd, out) + + def delete_snapshot(self, snapshot): + snapshot_name = self._name_translate(snapshot['name']) + volume_name = self._name_translate(snapshot['volume_name']) + + LOG.debug(_('delete_snapshot: snapshot name: %(snapshot)s ' + 'volume name: %(volume)s') % {'snapshot': snapshot_name, + 'volume': volume_name}) + + self._update_login_info() + snapshot_id = snapshot.get('provider_location', None) + if ((snapshot_id is not None) and + self._check_snapshot_created(snapshot_id)): + # Not allow to delete snapshot if it is copying. + if self._snapshot_in_luncopy(snapshot_id): + err_msg = (_('delete_snapshot: Can not delete snapshot %s ' + 'for it is a source LUN of LUNCopy.') + % snapshot_name) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + self._delete_snapshot(snapshot_id) + else: + err_msg = (_('delete_snapshot: Snapshot %(snap)s does not exist.') + % {'snap': snapshot_name}) + LOG.warn(err_msg) + + def _check_snapshot_created(self, snapshot_id): + cli_cmd = 'showsnapshot -snapshot %(snap)s' % {'snap': snapshot_id} + out = self._execute_cli(cli_cmd) + return (True if re.search('Snapshot Information', out) else False) + + def _snapshot_in_luncopy(self, snapshot_id): + for name in self.luncopy_list: + if name.startswith(VOL_AND_SNAP_NAME_PREFIX + snapshot_id): + return True + return False + + def _delete_snapshot(self, snapshotid): + """Send CLI command to delete snapshot. + + Firstly, disable the snapshot, then delete it. + + """ + + cli_cmd = ('disablesnapshot -snapshot %(snapshotid)s' + % {'snapshotid': snapshotid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_snapshot', + ('Failed to disable snapshot %s' + % snapshotid), + cli_cmd, out) + + cli_cmd = ('delsnapshot -snapshot %(snapshotid)s' + % {'snapshotid': snapshotid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_snapshot', + ('Failed to delete snapshot %s' + % snapshotid), + cli_cmd, out) + + def _assert_cli_out(self, condition, func, msg, cmd, cliout): + """Assertion for CLI query out.""" + if not condition: + err_msg = (_('%(func)s: %(msg)s\nCLI command: %(cmd)s\n' + 'CLI out: %(out)s') % {'func': func, + 'msg': msg, + 'cmd': cmd, + 'out': cliout}) + raise exception.VolumeBackendAPIException(data=err_msg) + + def _assert_cli_operate_out(self, func, msg, cmd, cliout): + """Assertion for CLI out string: command operates successfully.""" + condition = re.search('command operates successfully', cliout) + self._assert_cli_out(condition, func, msg, cmd, cliout) + + def map_volume(self, host_id, volume_id): + """Map a volume to a host.""" + # Map a LUN to a host if not mapped. + if not self._check_volume_created(volume_id): + raise exception.VolumeNotFound(volume_id=volume_id) + + hostlun_id = None + map_info = self._get_host_map_info(host_id) + # Make sure the host LUN ID starts from 1. + new_hostlun_id = 1 + new_hostlunid_found = False + if map_info: + for maping in map_info: + if maping[2] == volume_id: + hostlun_id = maping[4] + break + elif not new_hostlunid_found: + if new_hostlun_id < int(maping[4]): + new_hostlunid_found = True + else: + new_hostlun_id = int(maping[4]) + 1 + + if not hostlun_id: + cli_cmd = ('addhostmap -host %(host_id)s -devlun %(lunid)s ' + '-hostlun %(hostlunid)s' + % {'host_id': host_id, + 'lunid': volume_id, + 'hostlunid': new_hostlun_id}) + out = self._execute_cli(cli_cmd) + + msg = ('Failed to map lun %s to host %s. host lun ID: %s' + % (volume_id, host_id, new_hostlun_id)) + self._assert_cli_operate_out('map_volume', msg, cli_cmd, out) + + hostlun_id = new_hostlun_id + + return hostlun_id + + def add_host(self, host_name): + """Create a host and add it to hostgroup.""" + # Create an OpenStack hostgroup if not created before. + hostgroup_name = HOST_GROUP_NAME + self.hostgroup_id = self._get_hostgroup_id(hostgroup_name) + if self.hostgroup_id is None: + self._create_hostgroup(hostgroup_name) + self.hostgroup_id = self._get_hostgroup_id(hostgroup_name) + + # Create a host and add it to the hostgroup. + host_name = HOST_NAME_PREFIX + host_name + host_id = self._get_host_id(host_name, self.hostgroup_id) + if host_id is None: + self._create_host(host_name, self.hostgroup_id) + host_id = self._get_host_id(host_name, self.hostgroup_id) + + return host_id + + def _get_hostgroup_id(self, groupname): + """Get the given hostgroup ID. + + If the hostgroup not found, return None. + + """ + + cli_cmd = 'showhostgroup' + out = self._execute_cli(cli_cmd) + if re.search('Host Group Information', out): + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if tmp_line[1] == groupname: + return tmp_line[0] + return None + + def _create_hostgroup(self, hostgroupname): + """Run CLI command to create host group.""" + cli_cmd = 'createhostgroup -n %(name)s' % {'name': hostgroupname} + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_create_hostgroup', + ('Failed to Create hostgroup %s.' + % hostgroupname), + cli_cmd, out) + + def _get_host_id(self, hostname, hostgroupid): + """Get the given host ID.""" + cli_cmd = 'showhost -group %(groupid)s' % {'groupid': hostgroupid} + out = self._execute_cli(cli_cmd) + if re.search('Host Information', out): + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if tmp_line[1] == hostname: + return tmp_line[0] + return None + + def _create_host(self, hostname, hostgroupid): + """Run CLI command to add host.""" + cli_cmd = ('addhost -group %(groupid)s -n %(hostname)s -t 0' + % {'groupid': hostgroupid, + 'hostname': hostname}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_create_host', + 'Failed to create host %s' % hostname, + cli_cmd, out) + + def _get_host_port_info(self, hostid): + """Run CLI command to get host port information.""" + cli_cmd = ('showhostport -host %(hostid)s' % {'hostid': hostid}) + out = self._execute_cli(cli_cmd) + if re.search('Host Port Information', out): + return [line.split() for line in out.split('\r\n')[6:-2]] + else: + return None + + def _get_host_map_info(self, hostid): + """Get map infomation of the given host.""" + + cli_cmd = 'showhostmap -host %(hostid)s' % {'hostid': hostid} + out = self._execute_cli(cli_cmd) + if re.search('Map Information', out): + mapinfo = [line.split() for line in out.split('\r\n')[6:-2]] + # Sorted by host LUN ID. + return sorted(mapinfo, key=lambda x: int(x[4])) + else: + return None + + def get_lun_details(self, lun_id): + cli_cmd = 'showlun -lun %s' % lun_id + out = self._execute_cli(cli_cmd) + lun_details = {} + if re.search('LUN Information', out): + for line in out.split('\r\n')[4:-2]: + line = line.split('|') + key = ''.join(line[0].strip().split()) + val = line[1].strip() + lun_details[key] = val + return lun_details + + def change_lun_ctr(self, lun_id, ctr): + LOG.debug(_('change_lun_ctr: Changing LUN %(lun)s ctr to %(ctr)s') + % {'lun': lun_id, 'ctr': ctr}) + + cli_cmd = 'chglun -lun %s -c %s' % (lun_id, ctr) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('change_lun_ctr', + 'Failed to change owning controller for ' + 'LUN %s' % lun_id, + cli_cmd, out) + + def remove_map(self, volume_id, host_name): + """Remove host map.""" + host_name = HOST_NAME_PREFIX + host_name + host_id = self._get_host_id(host_name, self.hostgroup_id) + if host_id is None: + LOG.error(_('remove_map: Host %s does not exist.') % host_name) + raise exception.HostNotFound(host=host_name) + + if not self._check_volume_created(volume_id): + LOG.error(_('remove_map: Volume %s does not exist.') % volume_id) + raise exception.VolumeNotFound(volume_id=volume_id) + + map_id = None + map_info = self._get_host_map_info(host_id) + if map_info: + for maping in map_info: + if maping[2] == volume_id: + map_id = maping[0] + break + if map_id is not None: + self._delete_map(map_id) + else: + LOG.warn(_('remove_map: No map between host %(host)s and ' + 'volume %(volume)s.') % {'host': host_name, + 'volume': volume_id}) + return host_id + + def _delete_map(self, mapid, attempts=2): + """Run CLI command to remove map.""" + cli_cmd = 'delhostmap -force -map %(mapid)s' % {'mapid': mapid} + while True: + out = self._execute_cli(cli_cmd) + + # We retry to delete host map 10s later if there are + # IOs accessing the system. + if re.search('command operates successfully', out): + break + else: + if (re.search('there are IOs accessing the system', out) and + (attempts > 0)): + + LOG.debug(_('_delete_map: There are IOs accessing ' + 'the system. Retry to delete host map ' + '%(mapid)s 10s later.') % {'mapid': mapid}) + + time.sleep(10) + attempts -= 1 + continue + else: + err_msg = (_('_delete_map: Failed to delete host map ' + '%(mapid)s.\nCLI out: %(out)s') + % {'mapid': mapid, + 'times': attempts, + 'out': out}) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + def _delete_hostport(self, portid): + """Run CLI command to delete host port.""" + cli_cmd = ('delhostport -force -p %(portid)s' % {'portid': portid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_hostport', + 'Failed to delete host port %s.' % portid, + cli_cmd, out) + + def _delete_host(self, hostid): + """Run CLI command to delete host.""" + cli_cmd = ('delhost -force -host %(hostid)s' % {'hostid': hostid}) + out = self._execute_cli(cli_cmd) + + self._assert_cli_operate_out('_delete_host', + 'Failed to delete host. %s.' % hostid, + cli_cmd, out) + + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If 'refresh' is True, run update the stats first. + """ + if refresh: + self._update_volume_stats() + + return self._stats + + def _update_volume_stats(self): + """Retrieve stats info from volume group.""" + + LOG.debug(_("_update_volume_stats: Updating volume stats")) + data = {} + data['vendor_name'] = 'Huawei' + data['total_capacity_gb'] = 'infinite' + data['free_capacity_gb'] = self._get_free_capacity() + data['reserved_percentage'] = 0 + data['QoS_support'] = False + + self._stats = data + + def _get_free_capacity(self): + """Get total free capacity of pools.""" + self._update_login_info() + params_conf = self._parse_conf_lun_params() + lun_type = params_conf['LUNType'] + pools_conf = params_conf['StoragePool'] + pools_dev = self._get_dev_pool_info(lun_type) + total_free_capacity = 0.0 + for pool_dev in pools_dev: + for pool_conf in pools_conf: + if ((lun_type == 'Thick') and + (pool_dev[5] == pool_conf)): + total_free_capacity += float(pool_dev[3]) + break + elif pool_dev[1] == pool_conf: + total_free_capacity += float(pool_dev[4]) + break + + return total_free_capacity / 1024 + + def _get_dev_pool_info(self, pooltype): + """Get pools information created in storage device. + + Return a list whose elements are also list. + + """ + + cli_cmd = ('showpool' if pooltype == 'Thin' else 'showrg') + out = self._execute_cli(cli_cmd) + + test = (re.search('Pool Information', out) or + re.search('RAID Group Information', out)) + self._assert_cli_out(test, '_get_dev_pool_info', + 'No pools information found.', cli_cmd, out) + + pool = out.split('\r\n')[6:-2] + return [line.split() for line in pool] diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index feb78a021..a1e9e50be 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -124,7 +124,9 @@ MAPPING = { 'cinder.volume.drivers.netapp.nfs.NetAppNFSDriver': 'cinder.volume.drivers.netapp.common.Deprecated', 'cinder.volume.drivers.netapp.nfs.NetAppCmodeNfsDriver': - 'cinder.volume.drivers.netapp.common.Deprecated'} + 'cinder.volume.drivers.netapp.common.Deprecated', + 'cinder.volume.drivers.huawei.HuaweiISCSIDriver': + 'cinder.volume.drivers.huawei.HuaweiVolumeDriver'} class VolumeManager(manager.SchedulerDependentManager): diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 2214d0104..049cc75ab 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1162,7 +1162,7 @@ # -# Options defined in cinder.volume.drivers.huawei.huawei_iscsi +# Options defined in cinder.volume.drivers.huawei # # config data for cinder huawei plugin (string value)