From a2dca1ffc3e6ba81862c0eff625b620f3f175690 Mon Sep 17 00:00:00 2001 From: zhangchao010 Date: Tue, 3 Sep 2013 19:17:26 +0800 Subject: [PATCH] Add volume driver for Huawei HVS storage system Huawei OceanStor HVS-series enterprise storage system is an optimum storage platform for next-generation data centers that feature virtualization, hybrid cloud, simplified IT, and low carbon footprints. This patch add an iSCSI driver and a FC driver for Huawei HVS storage system, using REST. We define a common module for both iSCSI driver and FC driver. The drivers support volume type, QoS. Implements: blueprint huawei-hvs-volume-driver Change-Id: Ibdfed7df6d347e00f498694898c88dfa641559eb --- cinder/tests/test_huawei_hvs.py | 846 ++++++++++++ cinder/volume/drivers/huawei/__init__.py | 8 +- cinder/volume/drivers/huawei/huawei_hvs.py | 165 +++ cinder/volume/drivers/huawei/rest_common.py | 1273 +++++++++++++++++++ 4 files changed, 2289 insertions(+), 3 deletions(-) create mode 100644 cinder/tests/test_huawei_hvs.py create mode 100644 cinder/volume/drivers/huawei/huawei_hvs.py create mode 100644 cinder/volume/drivers/huawei/rest_common.py diff --git a/cinder/tests/test_huawei_hvs.py b/cinder/tests/test_huawei_hvs.py new file mode 100644 index 00000000000..7b1a45c2c9b --- /dev/null +++ b/cinder/tests/test_huawei_hvs.py @@ -0,0 +1,846 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2013 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Unit Tests for Huawei HVS volume drivers. +""" + +import json +import mox +import os +import shutil +import tempfile +import time + +from xml.dom.minidom import Document + +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 huawei_hvs +from cinder.volume.drivers.huawei import rest_common + + +test_volume = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', + 'size': 2, + 'volume_name': 'vol1', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'vol1', + 'display_description': 'test volume', + 'volume_type_id': None} + +test_snap = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', + 'size': 1, + 'volume_name': 'vol1', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'vol1', + 'display_description': 'test volume', + 'volume_type_id': None} + +FakeConnector = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3', + 'wwpns': ['10000090fa0d6754'], + 'wwnns': ['10000090fa0d6755'], + 'host': 'fakehost'} + + +def Fake_sleep(time): + pass + + +class FakeHVSCommon(rest_common.HVSCommon): + + def __init__(self, configuration): + rest_common.HVSCommon.__init__(self, configuration) + self.test_normal = True + self.other_flag = True + self.deviceid = None + self.lun_id = None + self.snapshot_id = None + self.luncopy_id = None + self.termin_flag = False + + def _parse_volume_type(self, volume): + self._get_lun_conf_params() + poolinfo = self._find_pool_info() + volume_size = self._get_volume_size(poolinfo, volume) + + params = {'LUNType': 0, + 'WriteType': '1', + 'PrefetchType': '3', + 'qos_level': 'Qos-high', + 'StripUnitSize': '64', + 'PrefetchValue': '0', + 'PrefetchTimes': '0', + 'qos': 'OpenStack_Qos_High', + 'MirrorSwitch': '1', + 'tier': 'Tier_high'} + + params['volume_size'] = volume_size + params['pool_id'] = poolinfo['ID'] + return params + + def _change_file_mode(self, filepath): + utils.execute('chmod', '777', filepath) + + def call(self, url=False, data=None, method=None): + + url = url.replace('http://100.115.10.69:8082/deviceManager/rest', '') + url = url.replace('/210235G7J20000000000/', '') + data = None + + if self.test_normal: + if url == "/xx/sessions": + data = """{"error":{"code":0}, + "data":{"username":"admin", + "deviceid":"210235G7J20000000000" + }}""" + if url == "sessions": + data = """{"error":{"code":0}, + "data":{"ID":11}}""" + + if url == "storagepool": + data = """{"error":{"code":0}, + "data":[{"ID":"0", + "NAME":"OpenStack_Pool", + "USERFREECAPACITY":"985661440", + "USERTOTALCAPACITY":"985661440" + }]}""" + + if url == "lun": + if method is None: + data = """{"error":{"code":0}, + "data":{"ID":"1", + "NAME":"5mFHcBv4RkCcD+JyrWc0SA"}}""" + self.lun_id = "0" + + if method == 'GET': + data = """{"error":{"code":0}, + "data":[{"ID":"1", + "NAME":"IexzQZJWSXuX2e9I7c8GNQ"}]}""" + + if url == "lungroup": + if method is None: + data = """{"error":{"code":0}, + "data":{"NAME":"5mFHcBv4RkCcD+JyrWc0SA", + "DESCRIPTION":"5mFHcBv4RkCcD", + "ID":"11", + "TYPE":256}}""" + + if method == "GET": + data = """{"error":{"code":0}, + "data":[{"NAME":"IexzQZJWSXuX2e9I7c8GNQ", + "DESCRIPTION":"5mFHcBv4RkCcD", + "ID":"11", + "TYPE":256}]}""" + + if method == "DELETE": + data = """{"error":{"code":0}, + "data":[{"NAME":"IexzQZJWSXuX2e9I7c8GNQ", + "DESCRIPTION":"5mFHcBv4RkCcD+JyrWc0SA", + "ID":"11", + "TYPE":256}]}""" + + if url == "lungroup/associate": + data = """{"error":{"code":0}, + "data":{"NAME":"5mFHcBv4RkCcD+JyrWc0SA", + "DESCRIPTION":"5mFHcBv4RkCcD+JyrWc0SA", + "ID":"11", + "TYPE":256}}""" + + if url == "snapshot": + if method is None: + data = """{"error":{"code":0}, + "data":{"ID":11}}""" + self.snapshot_id = "3" + + if method == "GET": + data = """{"error":{"code":0}, + "data":[{"ID":11,"NAME":"SDFAJSDFLKJ"}, + {"ID":12,"NAME":"SDFAJSDFLKJ"}]}""" + + if url == "snapshot/activate": + data = """{"error":{"code":0}}""" + + if url == ("lungroup/associate?ID=11" + "&ASSOCIATEOBJTYPE=11&ASSOCIATEOBJID=1"): + data = """{"error":{"code":0}}""" + + if url == "LUNGroup/11": + data = """{"error":{"code":0}}""" + + if url == 'lun/1': + data = """{"error":{"code":0}}""" + self.lun_id = None + + if url == 'snapshot': + if method == "GET": + data = """{"error":{"code":0}, + "data":[{"PARENTTYPE":11, + "NAME":"IexzQZJWSXuX2e9I7c8GNQ", + "WWN":"60022a11000a2a3907ce96cb00000b", + "ID":"11", + "CONSUMEDCAPACITY":"0"}]}""" + + if url == "snapshot/stop": + data = """{"error":{"code":0}}""" + + if url == "snapshot/11": + data = """{"error":{"code":0}}""" + self.snapshot_id = None + + if url == "luncopy": + data = """{"error":{"code":0}, + "data":{"COPYSTOPTIME":"-1", + "HEALTHSTATUS":"1", + "NAME":"w1PSNvu6RumcZMmSh4/l+Q==", + "RUNNINGSTATUS":"36", + "DESCRIPTION":"w1PSNvu6RumcZMmSh4/l+Q==", + "ID":"0","LUNCOPYTYPE":"1", + "COPYPROGRESS":"0","COPYSPEED":"2", + "TYPE":219,"COPYSTARTTIME":"-1"}}""" + self.luncopy_id = "7" + + if url == "LUNCOPY/start": + data = """{"error":{"code":0}}""" + + if url == "LUNCOPY?range=[0-100000]": + data = """{"error":{"code":0}, + "data":[{"COPYSTOPTIME":"1372209335", + "HEALTHSTATUS":"1", + "NAME":"w1PSNvu6RumcZMmSh4/l+Q==", + "RUNNINGSTATUS":"40", + "DESCRIPTION":"w1PSNvu6RumcZMmSh4/l+Q==", + "ID":"0","LUNCOPYTYPE":"1", + "COPYPROGRESS":"100", + "COPYSPEED":"2", + "TYPE":219, + "COPYSTARTTIME":"1372209329"}]}""" + + if url == "LUNCOPY/0": + data = '{"error":{"code":0}}' + + if url == "eth_port": + data = """{"error":{"code":0}, + "data":[{"PARENTTYPE":209, + "MACADDRESS":"00:22:a1:0a:79:57", + "ETHNEGOTIATE":"-1","ERRORPACKETS":"0", + "IPV4ADDR":"100.115.10.68", + "IPV6GATEWAY":"","IPV6MASK":"0", + "OVERFLOWEDPACKETS":"0","ISCSINAME":"P0", + "HEALTHSTATUS":"1","ETHDUPLEX":"2", + "ID":"16909568","LOSTPACKETS":"0", + "TYPE":213,"NAME":"P0","INIORTGT":"4", + "RUNNINGSTATUS":"10","IPV4GATEWAY":"", + "BONDNAME":"","STARTTIME":"1371684218", + "SPEED":"1000","ISCSITCPPORT":"0", + "IPV4MASK":"255.255.0.0","IPV6ADDR":"", + "LOGICTYPE":"0","LOCATION":"ENG0.B5.P0", + "MTU":"1500","PARENTID":"1.5"}]}""" + + if url == "iscsidevicename": + data = """{"error":{"code":0}, +"data":[{"CMO_ISCSI_DEVICE_NAME": +"iqn.2006-08.com.huawei:oceanstor:21000022a10a2a39:iscsinametest"}]}""" + + if url == "hostgroup": + if method is None: + data = """{"error":{"code":0}, + "data":{"NAME":"ubuntuc", + "DESCRIPTION":"", + "ID":"0", + "TYPE":14}}""" + + if method == "GET": + data = """{"error":{"code":0}, + "data":[{"NAME":"ubuntuc", + "DESCRIPTION":"", + "ID":"0", + "TYPE":14}]}""" + + if url == "host": + if method is None: + data = """{"error":{"code":0}, + "data":{"PARENTTYPE":245, + "NAME":"Default Host", + "DESCRIPTION":"", + "RUNNINGSTATUS":"1", + "IP":"","PARENTNAME":"0", + "OPERATIONSYSTEM":"1","LOCATION":"", + "HEALTHSTATUS":"1","MODEL":"", + "ID":"0","PARENTID":"0", + "NETWORKNAME":"","TYPE":21}} """ + + if method == "GET": + data = """{"error":{"code":0}, + "data":[{"PARENTTYPE":245, + "NAME":"ubuntuc", + "DESCRIPTION":"", + "RUNNINGSTATUS":"1", + "IP":"","PARENTNAME":"", + "OPERATIONSYSTEM":"0", + "LOCATION":"", + "HEALTHSTATUS":"1", + "MODEL":"", + "ID":"1","PARENTID":"", + "NETWORKNAME":"","TYPE":21}, + {"PARENTTYPE":245, + "NAME":"ubuntu", + "DESCRIPTION":"", + "RUNNINGSTATUS":"1", + "IP":"","PARENTNAME":"", + "OPERATIONSYSTEM":"0", + "LOCATION":"", + "HEALTHSTATUS":"1", + "MODEL":"","ID":"2", + "PARENTID":"", + "NETWORKNAME":"","TYPE":21}]} """ + + if url == "host/associate": + if method is None: + data = """{"error":{"code":0}}""" + if method == "GET": + data = """{"error":{"code":0}}""" + + if url == "iscsi_initiator/iqn.1993-08.debian:01:ec2bff7ac3a3": + data = """{"error":{"code":0}, + "data":{"ID":"iqn.1993-08.win:01:ec2bff7ac3a3", + "NAME":"iqn.1993-08.win:01:ec2bff7ac3a3", + "ISFREE":"True"}}""" + + if url == "iscsi_initiator/": + data = """{"error":{"code":0}}""" + + if url == "mappingview": + self.termin_flag = True + if method is None: + data = """{"error":{"code":0}, + "data":{"WORKMODE":"255", + "HEALTHSTATUS":"1", + "NAME":"mOWtSXnaQKi3hpB3tdFRIQ", + "RUNNINGSTATUS":"27","DESCRIPTION":"", + "ENABLEINBANDCOMMAND":"true", + "ID":"1","INBANDLUNWWN":"", + "TYPE":245}}""" + + if method == "GET": + if self.other_flag: + data = """{"error":{"code":0}, + "data":[{"WORKMODE":"255", + "HEALTHSTATUS":"1", + "NAME":"mOWtSXnaQKi3hpB3tdFRIQ", + "RUNNINGSTATUS":"27", + "DESCRIPTION":"", + "ENABLEINBANDCOMMAND": + "true","ID":"1", + "INBANDLUNWWN":"", + "TYPE":245}, + {"WORKMODE":"255", + "HEALTHSTATUS":"1", + "NAME":"YheUoRwbSX2BxN767nvLSw", + "RUNNINGSTATUS":"27", + "DESCRIPTION":"", + "ENABLEINBANDCOMMAND":"true", + "ID":"2", + "INBANDLUNWWN":"", + "TYPE":245}]}""" + else: + data = """{"error":{"code":0}, + "data":[{"WORKMODE":"255", + "HEALTHSTATUS":"1", + "NAME":"IexzQZJWSXuX2e9I7c8GNQ", + "RUNNINGSTATUS":"27", + "DESCRIPTION":"", + "ENABLEINBANDCOMMAND":"true", + "ID":"1", + "INBANDLUNWWN":"", + "TYPE":245}, + {"WORKMODE":"255", + "HEALTHSTATUS":"1", + "NAME":"YheUoRwbSX2BxN767nvLSw", + "RUNNINGSTATUS":"27", + "DESCRIPTION":"", + "ENABLEINBANDCOMMAND":"true", + "ID":"2", + "INBANDLUNWWN":"", + "TYPE":245}]}""" + + if url == "MAPPINGVIEW/CREATE_ASSOCIATE": + data = """{"error":{"code":0}}""" + + if url == ("lun/associate?TYPE=11&" + "ASSOCIATEOBJTYPE=21&ASSOCIATEOBJID=0"): + data = """{"error":{"code":0}}""" + + if url == "fc_initiator?ISFREE=true&range=[0-1000]": + data = """{"error":{"code":0}, + "data":[{"HEALTHSTATUS":"1", + "NAME":"", + "MULTIPATHTYPE":"1", + "ISFREE":"true", + "RUNNINGSTATUS":"27", + "ID":"10000090fa0d6754", + "OPERATIONSYSTEM":"255", + "TYPE":223}, + {"HEALTHSTATUS":"1", + "NAME":"", + "MULTIPATHTYPE":"1", + "ISFREE":"true", + "RUNNINGSTATUS":"27", + "ID":"10000090fa0d6755", + "OPERATIONSYSTEM":"255", + "TYPE":223}]}""" + + if url == "host_link?INITIATOR_TYPE=223&INITIATOR_PORT_WWN="\ + "10000090fa0d6754": + + data = """{"error":{"code":0}, + "data":[{"PARENTTYPE":21, + "TARGET_ID":"0000000000000000", + "INITIATOR_NODE_WWN":"20000090fa0d6754", + "INITIATOR_TYPE":"223", + "RUNNINGSTATUS":"27", + "PARENTNAME":"ubuntuc", + "INITIATOR_ID":"10000090fa0d6754", + "TARGET_PORT_WWN":"24000022a10a2a39", + "HEALTHSTATUS":"1", + "INITIATOR_PORT_WWN":"10000090fa0d6754", + "ID":"010000090fa0d675-0000000000110400", + "TARGET_NODE_WWN":"21000022a10a2a39", + "PARENTID":"1","CTRL_ID":"0", + "TYPE":255,"TARGET_TYPE":"212"}]}""" + + if url == ("mappingview/associate?TYPE=245&" + "ASSOCIATEOBJTYPE=14&ASSOCIATEOBJID=0"): + + data = """{"error":{"code":0}, + "data":[{"ID":11,"NAME":"test"}]}""" + + if url == ("mappingview/associate?TYPE=245&" + "ASSOCIATEOBJTYPE=256&ASSOCIATEOBJID=11"): + + data = """{"error":{"code":0}, + "data":[{"ID":11,"NAME":"test"}]}""" + + if url == "fc_initiator/10000090fa0d6754": + data = """{"error":{"code":0}}""" + + if url == "mappingview/REMOVE_ASSOCIATE": + data = """{"error":{"code":0}}""" + self.termin_flag = True + + if url == "mappingview/1": + data = """{"error":{"code":0}}""" + + if url == "ioclass": + data = """{"error":{"code":0}, + "data":[{"NAME":"OpenStack_Qos_High", + "ID":"0", + "LUNLIST":"[]", + "TYPE":230}]}""" + + if url == "ioclass/0": + data = """{"error":{"code":0}}""" + + else: + data = """{"error":{"code":31755596}}""" + + res_json = json.loads(data) + return res_json + + +class FakeHVSiSCSIStorage(huawei_hvs.HuaweiHVSISCSIDriver): + + def __init__(self, configuration): + super(FakeHVSiSCSIStorage, self).__init__(configuration) + self.configuration = configuration + + def do_setup(self, context): + self.common = FakeHVSCommon(configuration=self.configuration) + + +class FakeHVSFCStorage(huawei_hvs.HuaweiHVSFCDriver): + + def __init__(self, configuration): + super(FakeHVSFCStorage, self).__init__(configuration) + self.configuration = configuration + + def do_setup(self, context): + self.common = FakeHVSCommon(configuration=self.configuration) + + +class HVSRESTiSCSIDriverTestCase(test.TestCase): + def setUp(self): + super(HVSRESTiSCSIDriverTestCase, self).setUp() + self.tmp_dir = tempfile.mkdtemp() + self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' + self.create_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.driver = FakeHVSiSCSIStorage(configuration=self.configuration) + self.driver.do_setup({}) + self.driver.common.test_normal = True + + def tearDown(self): + if os.path.exists(self.fake_conf_file): + os.remove(self.fake_conf_file) + shutil.rmtree(self.tmp_dir) + super(HVSRESTiSCSIDriverTestCase, self).tearDown() + + def test_log_in_success(self): + deviceid = self.driver.common.login() + self.assertNotEqual(deviceid, None) + + def test_log_out_success(self): + self.driver.common.login() + self.driver.common.login_out() + + def test_create_volume_success(self): + self.driver.common.login() + self.driver.create_volume(test_volume) + self.assertEqual(self.driver.common.lun_id, "0") + + def test_create_snapshot_success(self): + self.driver.common.login() + self.driver.create_snapshot(test_volume) + self.assertEqual(self.driver.common.snapshot_id, "3") + + def test_delete_volume_success(self): + self.driver.common.login() + self.driver.delete_volume(test_volume) + self.assertEqual(self.driver.common.lun_id, None) + + def test_delete_snapshot_success(self): + self.driver.common.login() + self.driver.delete_snapshot(test_snap) + self.assertEqual(self.driver.common.snapshot_id, None) + + def test_colone_volume_success(self): + self.driver.common.login() + self.driver.create_cloned_volume(test_volume, test_volume) + self.assertEqual(self.driver.common.luncopy_id, "7") + + def test_create_volume_from_snapshot_success(self): + self.driver.common.login() + self.driver.create_volume_from_snapshot(test_volume, test_volume) + self.assertEqual(self.driver.common.luncopy_id, "7") + + def test_initialize_connection_success(self): + self.driver.common.login() + conn = self.driver.initialize_connection(test_volume, FakeConnector) + self.assertEqual(conn['data']['target_lun'], 1) + + def test_terminate_connection_success(self): + self.driver.common.login() + self.driver.terminate_connection(test_volume, FakeConnector) + self.assertEqual(self.driver.common.termin_flag, True) + + def test_initialize_connection_no_view_success(self): + self.driver.common.login() + self.driver.common.other_flag = False + conn = self.driver.initialize_connection(test_volume, FakeConnector) + self.assertEqual(conn['data']['target_lun'], 1) + + def test_terminate_connectio_no_view_success(self): + self.driver.common.login() + self.driver.common.other_flag = False + self.driver.terminate_connection(test_volume, FakeConnector) + self.assertEqual(self.driver.common.termin_flag, True) + + def test_get_volume_stats(self): + self.driver.common.login() + status = self.driver.get_volume_stats() + self.assertNotEqual(status['free_capacity_gb'], None) + + def test_create_snapshot_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.create_snapshot, test_volume) + + def test_create_volume_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.create_volume, test_volume) + + def test_delete_volume_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.delete_volume, test_volume) + + def test_delete_snapshot_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.delete_snapshot, test_volume) + + def test_initialize_connection_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.initialize_connection, + test_volume, FakeConnector) + + def create_fake_conf_file(self): + doc = Document() + + config = doc.createElement('config') + doc.appendChild(config) + + storage = doc.createElement('Storage') + config.appendChild(storage) + + product = doc.createElement('Product') + product_text = doc.createTextNode('HVS') + product.appendChild(product_text) + storage.appendChild(product) + + protocal = doc.createElement('Protocol') + protocal_text = doc.createTextNode('iSCSI') + protocal.appendChild(protocal_text) + storage.appendChild(protocal) + + username = doc.createElement('UserName') + username_text = doc.createTextNode('admin') + username.appendChild(username_text) + storage.appendChild(username) + userpassword = doc.createElement('UserPassword') + userpassword_text = doc.createTextNode('Admin@storage') + userpassword.appendChild(userpassword_text) + storage.appendChild(userpassword) + url = doc.createElement('HVSURL') + url_text = doc.createTextNode('http://100.115.10.69:8082/' + 'deviceManager/rest/') + url.appendChild(url_text) + storage.appendChild(url) + lun = doc.createElement('LUN') + config.appendChild(lun) + storagepool = doc.createElement('StoragePool') + pool_text = doc.createTextNode('OpenStack_Pool') + storagepool.appendChild(pool_text) + lun.appendChild(storagepool) + + luntype = doc.createElement('LUNType') + luntype_text = doc.createTextNode('Thick') + luntype.appendChild(luntype_text) + lun.appendChild(luntype) + + writetype = doc.createElement('WriteType') + writetype_text = doc.createTextNode('1') + writetype.appendChild(writetype_text) + lun.appendChild(writetype) + + prefetchType = doc.createElement('Prefetch') + prefetchType.setAttribute('Type', '2') + prefetchType.setAttribute('Value', '20') + lun.appendChild(prefetchType) + + iscsi = doc.createElement('iSCSI') + config.appendChild(iscsi) + defaulttargetip = doc.createElement('DefaultTargetIP') + defaulttargetip_text = doc.createTextNode('100.115.10.68') + defaulttargetip.appendChild(defaulttargetip_text) + iscsi.appendChild(defaulttargetip) + + initiator = doc.createElement('Initiator') + initiator.setAttribute('Name', 'iqn.1993-08.debian:01:ec2bff7ac3a3') + initiator.setAttribute('TargetIP', '100.115.10.68') + iscsi.appendChild(initiator) + + newefile = open(self.fake_conf_file, 'w') + newefile.write(doc.toprettyxml(indent='')) + newefile.close() + + +class HVSRESTFCDriverTestCase(test.TestCase): + def setUp(self): + super(HVSRESTFCDriverTestCase, self).setUp() + self.tmp_dir = tempfile.mkdtemp() + self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' + self.create_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.driver = FakeHVSFCStorage(configuration=self.configuration) + self.driver.do_setup({}) + self.driver.common.test_normal = True + + def tearDown(self): + if os.path.exists(self.fake_conf_file): + os.remove(self.fake_conf_file) + shutil.rmtree(self.tmp_dir) + super(HVSRESTFCDriverTestCase, self).tearDown() + + def test_log_in_Success(self): + deviceid = self.driver.common.login() + self.assertNotEqual(deviceid, None) + + def test_create_volume_success(self): + self.driver.common.login() + self.driver.create_volume(test_volume) + self.assertEqual(self.driver.common.lun_id, "0") + + def test_create_snapshot_success(self): + self.driver.common.login() + self.driver.create_snapshot(test_volume) + self.assertEqual(self.driver.common.snapshot_id, "3") + + def test_delete_volume_success(self): + self.driver.common.login() + self.driver.delete_volume(test_volume) + self.assertEqual(self.driver.common.lun_id, None) + + def test_delete_snapshot_success(self): + self.driver.common.login() + self.driver.delete_snapshot(test_snap) + self.assertEqual(self.driver.common.snapshot_id, None) + + def test_colone_volume_success(self): + self.driver.common.login() + self.driver.create_cloned_volume(test_volume, test_volume) + self.assertEqual(self.driver.common.luncopy_id, "7") + + def test_create_volume_from_snapshot_success(self): + self.driver.common.login() + self.driver.create_volume_from_snapshot(test_volume, test_volume) + self.assertEqual(self.driver.common.luncopy_id, "7") + + def test_initialize_connection_success(self): + self.driver.common.login() + conn = self.driver.initialize_connection(test_volume, FakeConnector) + self.assertEqual(conn['data']['target_lun'], 1) + + def test_terminate_connection_success(self): + self.driver.common.login() + self.driver.terminate_connection(test_volume, FakeConnector) + self.assertEqual(self.driver.common.termin_flag, True) + + def test_initialize_connection_no_view_success(self): + self.driver.common.login() + self.driver.common.other_flag = False + conn = self.driver.initialize_connection(test_volume, FakeConnector) + self.assertEqual(conn['data']['target_lun'], 1) + + def test_terminate_connection_no_viewn_success(self): + self.driver.common.login() + self.driver.common.other_flag = False + self.driver.terminate_connection(test_volume, FakeConnector) + self.assertEqual(self.driver.common.termin_flag, True) + + def test_get_volume_stats(self): + self.driver.common.login() + status = self.driver.get_volume_stats() + self.assertNotEqual(status['free_capacity_gb'], None) + + def test_create_snapshot_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.create_snapshot, test_volume) + + def test_create_volume_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.create_volume, test_volume) + + def test_delete_volume_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.delete_volume, test_volume) + + def test_delete_snapshot_fail(self): + self.driver.common.login() + self.driver.common.test_normal = False + self.assertRaises(exception.CinderException, + self.driver.delete_snapshot, test_volume) + + def create_fake_conf_file(self): + doc = Document() + + config = doc.createElement('config') + doc.appendChild(config) + + storage = doc.createElement('Storage') + config.appendChild(storage) + + product = doc.createElement('Product') + product_text = doc.createTextNode('HVS') + product.appendChild(product_text) + storage.appendChild(product) + + protocal = doc.createElement('Protocol') + protocal_text = doc.createTextNode('FC') + protocal.appendChild(protocal_text) + storage.appendChild(protocal) + + username = doc.createElement('UserName') + username_text = doc.createTextNode('admin') + username.appendChild(username_text) + storage.appendChild(username) + + userpassword = doc.createElement('UserPassword') + userpassword_text = doc.createTextNode('Admin@storage') + userpassword.appendChild(userpassword_text) + storage.appendChild(userpassword) + url = doc.createElement('HVSURL') + url_text = doc.createTextNode('http://100.115.10.69:8082/' + 'deviceManager/rest/') + url.appendChild(url_text) + storage.appendChild(url) + + lun = doc.createElement('LUN') + config.appendChild(lun) + storagepool = doc.createElement('StoragePool') + pool_text = doc.createTextNode('OpenStack_Pool') + storagepool.appendChild(pool_text) + lun.appendChild(storagepool) + + luntype = doc.createElement('LUNType') + luntype_text = doc.createTextNode('Thick') + luntype.appendChild(luntype_text) + lun.appendChild(luntype) + + writetype = doc.createElement('WriteType') + writetype_text = doc.createTextNode('1') + writetype.appendChild(writetype_text) + lun.appendChild(writetype) + + prefetchType = doc.createElement('Prefetch') + prefetchType.setAttribute('Type', '2') + prefetchType.setAttribute('Value', '20') + lun.appendChild(prefetchType) + + newfile = open(self.fake_conf_file, 'w') + newfile.write(doc.toprettyxml(indent='')) + newfile.close() diff --git a/cinder/volume/drivers/huawei/__init__.py b/cinder/volume/drivers/huawei/__init__.py index f1f5777bf8e..37948aa478a 100644 --- a/cinder/volume/drivers/huawei/__init__.py +++ b/cinder/volume/drivers/huawei/__init__.py @@ -28,6 +28,7 @@ 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_dorado +from cinder.volume.drivers.huawei import huawei_hvs from cinder.volume.drivers.huawei import huawei_t from cinder.volume.drivers.huawei import ssh_common @@ -47,7 +48,8 @@ class HuaweiVolumeDriver(object): def __init__(self, *args, **kwargs): super(HuaweiVolumeDriver, self).__init__() - self._product = {'T': huawei_t, 'Dorado': huawei_dorado} + self._product = {'T': huawei_t, 'Dorado': huawei_dorado, + 'HVS': huawei_hvs} self._protocol = {'iSCSI': 'ISCSIDriver', 'FC': 'FCDriver'} self.driver = self._instantiate_driver(*args, **kwargs) @@ -84,8 +86,8 @@ class HuaweiVolumeDriver(object): return (product, protocol) else: msg = (_('"Product" or "Protocol" is illegal. "Product" should ' - 'be set to either T or Dorado. "Protocol" should be set ' - 'to either iSCSI or FC. Product: %(product)s ' + 'be set to either T, Dorado or HVS. "Protocol" should ' + 'be set to either iSCSI or FC. Product: %(product)s ' 'Protocol: %(protocol)s') % {'product': str(product), 'protocol': str(protocol)}) diff --git a/cinder/volume/drivers/huawei/huawei_hvs.py b/cinder/volume/drivers/huawei/huawei_hvs.py new file mode 100644 index 00000000000..8682acac2f2 --- /dev/null +++ b/cinder/volume/drivers/huawei/huawei_hvs.py @@ -0,0 +1,165 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2013 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume Drivers for Huawei OceanStor HVS storage arrays. +""" + +from cinder.volume import driver +from cinder.volume.drivers.huawei.rest_common import HVSCommon + + +class HuaweiHVSISCSIDriver(driver.ISCSIDriver): + """ISCSI driver for Huawei OceanStor HVS storage arrays.""" + + VERSION = '1.0.0' + + def __init__(self, *args, **kwargs): + super(HuaweiHVSISCSIDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Instantiate common class and log in storage system.""" + self.common = HVSCommon(configuration=self.configuration) + self.common.login() + + def check_for_setup_error(self): + """Check configuration file.""" + self.common._check_conf_file() + + def create_volume(self, volume): + """Create a volume.""" + self.common.create_volume(volume) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot.""" + self.common.create_volume_from_snapshot(volume, snapshot) + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the specified volume.""" + self.common.create_cloned_volume(volume, src_vref) + + def delete_volume(self, volume): + """Delete a volume.""" + self.common.delete_volume(volume) + + def create_snapshot(self, snapshot): + """Create a snapshot.""" + self.common.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + self.common.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + data = self.common.update_volume_stats(refresh) + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or self.__class__.__name__ + data['storage_protocol'] = 'iSCSI' + data['driver_version'] = self.VERSION + return data + + def initialize_connection(self, volume, connector): + """Map a volume to a host.""" + return self.common.initialize_connection_iscsi(volume, connector) + + def terminate_connection(self, volume, connector, **kwargs): + """Terminate the map.""" + self.common.terminate_connection(volume, connector, **kwargs) + + 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 + + +class HuaweiHVSFCDriver(driver.FibreChannelDriver): + """FC driver for Huawei OceanStor HVS storage arrays.""" + + VERSION = '1.0.0' + + def __init__(self, *args, **kwargs): + super(HuaweiHVSFCDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Instantiate common class and log in storage system.""" + self.common = HVSCommon(configuration=self.configuration) + self.common.login() + + def check_for_setup_error(self): + """Check configuration file.""" + self.common._check_conf_file() + + def create_volume(self, volume): + """Create a volume.""" + self.common.create_volume(volume) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot.""" + self.common.create_volume_from_snapshot(volume, snapshot) + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the specified volume.""" + self.common.create_cloned_volume(volume, src_vref) + + def delete_volume(self, volume): + """Delete a volume.""" + self.common.delete_volume(volume) + + def create_snapshot(self, snapshot): + """Create a snapshot.""" + self.common.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + self.common.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + data = self.common.update_volume_stats(refresh) + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or self.__class__.__name__ + data['storage_protocol'] = 'FC' + data['driver_version'] = self.VERSION + return data + + def initialize_connection(self, volume, connector): + """Map a volume to a host.""" + return self.common.initialize_connection_fc(volume, connector) + + def terminate_connection(self, volume, connector, **kwargs): + """Terminate the map.""" + self.common.terminate_connection(volume, connector, **kwargs) + + 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 diff --git a/cinder/volume/drivers/huawei/rest_common.py b/cinder/volume/drivers/huawei/rest_common.py new file mode 100644 index 00000000000..8d46d48e7db --- /dev/null +++ b/cinder/volume/drivers/huawei/rest_common.py @@ -0,0 +1,1273 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Huawei Technologies Co., Ltd. +# Copyright (c) 2013 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Common class for Huawei HVS storage drivers.""" + +import base64 +import cookielib +import json +import time +import urllib2 +import uuid + +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 units +from cinder import utils +from cinder.volume import volume_types + + +LOG = logging.getLogger(__name__) + +QOS_KEY = ["Qos-high", "Qos-normal", "Qos-low"] +TIER_KEY = ["Tier-high", "Tier-normal", "Tier-low"] + + +class HVSCommon(): + """Common class for Huawei OceanStor HVS storage system.""" + + def __init__(self, configuration): + self.configuration = configuration + self.cookie = cookielib.CookieJar() + self.url = None + + def call(self, url=False, data=None, method=None): + """Send requests to HVS server. + + Send HTTPS call, get response in JSON. + Convert response into Python Object and return it. + """ + + LOG.debug(_('HVS Request URL: %(url)s') % {'url': url}) + LOG.debug(_('HVS Request Data: %(data)s') % {'data': data}) + + headers = {"Connection": "keep-alive", + "Content-Type": "application/json"} + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie)) + urllib2.install_opener(opener) + + try: + urllib2.socket.setdefaulttimeout(720) + req = urllib2.Request(url, data, headers) + if method: + req.get_method = lambda: method + res = urllib2.urlopen(req).read().decode("utf-8") + LOG.debug(_('HVS Response Data: %(res)s') % {'res': res}) + except Exception as err: + err_msg = _('Bad reponse from server: %s') % err + LOG.error(err_msg) + raise err + + try: + res_json = json.loads(res) + except Exception: + raise exception.CinderException(_('JSON transfer Error')) + + return res_json + + def login(self): + """Log in HVS array. + + If login failed, the driver will sleep 30's to avoid frequent + connection to the server. + """ + + login_info = self._get_login_info() + url = login_info['HVSURL'] + "xx/sessions" + data = json.dumps({"username": login_info['UserName'], + "password": login_info['UserPassword'], + "scope": "0"}) + result = self.call(url, data) + if (result['error']['code'] != 0) or ("data" not in result): + time.sleep(30) + msg = _("Login error, reason is %s") % result + raise exception.CinderException(msg) + + deviceid = result['data']['deviceid'] + self.url = login_info['HVSURL'] + deviceid + return deviceid + + def _init_tier_parameters(self, parameters, lunparam): + """Init the LUN parameters through the volume type "performance".""" + if "tier" in parameters: + smart_tier = parameters['tier'] + if smart_tier == 'Tier_high': + lunparam['INITIALDISTRIBUTEPOLICY'] = "1" + elif smart_tier == 'Tier_normal': + lunparam['INITIALDISTRIBUTEPOLICY'] = "2" + elif smart_tier == 'Tier_low': + lunparam['INITIALDISTRIBUTEPOLICY'] = "3" + else: + lunparam['INITIALDISTRIBUTEPOLICY'] = "2" + + def _init_lun_parameters(self, name, parameters): + """Init basic LUN parameters """ + lunparam = {"TYPE": "11", + "NAME": name, + "PARENTTYPE": "216", + "PARENTID": parameters['pool_id'], + "DESCRIPTION": "", + "ALLOCTYPE": parameters['LUNType'], + "CAPACITY": parameters['volume_size'], + "WRITEPOLICY": parameters['WriteType'], + "MIRRORPOLICY": parameters['MirrorSwitch'], + "PREFETCHPOLICY": parameters['PrefetchType'], + "PREFETCHVALUE": parameters['PrefetchValue'], + "DATATRANSFERPOLICY": "1", + "INITIALDISTRIBUTEPOLICY": "0"} + + return lunparam + + def _init_qos_parameters(self, parameters, lun_param): + """Init the LUN parameters through the volume type "Qos-xxx".""" + policy_id = None + policy_info = None + if "qos" in parameters: + policy_info = self._find_qos_policy_info(parameters['qos']) + if policy_info: + policy_id = policy_info['ID'] + + lun_param['IOClASSID'] = policy_info['ID'] + qos_level = parameters['qos_level'] + if qos_level == 'Qos-high': + lun_param['IOPRIORITY'] = "3" + elif qos_level == 'Qos-normal': + lun_param['IOPRIORITY'] = "2" + elif qos_level == 'Qos-low': + lun_param['IOPRIORITY'] = "1" + else: + lun_param['IOPRIORITY'] = "2" + + return (policy_info, policy_id) + + def _assert_rest_result(self, result, err_str): + error_code = result['error']['code'] + if error_code != 0: + msg = _('%(err)s\nresult: %(res)s') % {'err': err_str, + 'res': result} + LOG.error(msg) + raise exception.CinderException(msg) + + def _create_volume(self, lun_param): + url = self.url + "/lun" + data = json.dumps(lun_param) + result = self.call(url, data) + self._assert_rest_result(result, 'create volume error') + + if "data" in result: + return result['data']['ID'] + else: + msg = _('create volume error: %(err)s') % {'err': result} + raise exception.CinderException(msg) + + def create_volume(self, volume): + volume_name = self._encode_name(volume['id']) + config_params = self._parse_volume_type(volume) + + # Prepare lun parameters, including qos parameter and tier parameter. + lun_param = self._init_lun_parameters(volume_name, config_params) + self._init_tier_parameters(config_params, lun_param) + policy_info, policy_id = self._init_qos_parameters(config_params, + lun_param) + + # Create LUN in array + lunid = self._create_volume(lun_param) + + # Enable qos, need to add lun into qos policy + if "qos" in config_params: + lun_list = policy_info['LUNLIST'] + lun_list.append(lunid) + if policy_id: + self._update_qos_policy_lunlist(lun_list, policy_id) + else: + LOG.warn(_("Can't find the Qos policy in array")) + + # Create lun group and add LUN into to lun group + lungroup_id = self._create_lungroup(volume_name) + self._associate_lun_to_lungroup(lungroup_id, lunid) + + return lunid + + def _get_volume_size(self, poolinfo, volume): + """Calculate the volume size. + + We should devide the given volume size by 512 for the HVS system + caculates volume size with sectors, which is 512 bytes. + """ + + volume_size = units.GiB / 512 # 1G + if int(volume['size']) != 0: + volume_size = int(volume['size']) * units.GiB / 512 + + return volume_size + + def delete_volume(self, volume): + """Delete a volume. + + Three steps: first, remove associate from lun group. + Second, remove associate from qos policy. Third, remove the lun. + """ + + name = self._encode_name(volume['id']) + lun_id = self._get_volume_by_name(name) + lungroup_id = self._find_lungroup(name) + + if lun_id and lungroup_id: + self._delete_lun_from_qos_policy(volume, lun_id) + self._delete_associated_lun_from_lungroup(lungroup_id, lun_id) + self._delete_lungroup(lungroup_id) + self._delete_lun(lun_id) + else: + LOG.warn(_("Can't find lun or lun goup in array")) + + def _delete_lun_from_qos_policy(self, volume, lun_id): + """Remove lun from qos policy.""" + parameters = self._parse_volume_type(volume) + + if "qos" in parameters: + qos = parameters['qos'] + policy_info = self._find_qos_policy_info(qos) + if policy_info: + lun_list = policy_info['LUNLIST'] + for item in lun_list: + if lun_id == item: + lun_list.remove(item) + self._update_qos_policy_lunlist(lun_list, policy_info['ID']) + + def _delete_lun(self, lun_id): + url = self.url + "/lun/" + lun_id + data = json.dumps({"TYPE": "11", + "ID": lun_id}) + result = self.call(url, data, "DELETE") + self._assert_rest_result(result, 'delete lun error') + + def _read_xml(self): + """Open xml file and parse the content.""" + 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 _encode_name(self, name): + uuid_str = name.replace("-", "") + vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str) + vol_encoded = base64.urlsafe_b64encode(vol_uuid.bytes) + newuuid = vol_encoded.replace("=", "") + return newuuid + + def _find_pool_info(self): + root = self._read_xml() + pool_name = root.findtext('LUN/StoragePool') + if not pool_name: + err_msg = _("Invalid resource pool: %s") % pool_name + raise exception.InvalidInput(err_msg) + + url = self.url + "/storagepool" + result = self.call(url, None) + self._assert_rest_result(result, 'Query resource pool error') + + poolinfo = {} + if "data" in result: + for item in result['data']: + if pool_name.strip() == item['NAME']: + poolinfo['ID'] = item['ID'] + poolinfo['CAPACITY'] = item['USERFREECAPACITY'] + poolinfo['TOTALCAPACITY'] = item['USERTOTALCAPACITY'] + break + + if not poolinfo: + msg = (_('Get pool info error, pool name is:%s') % pool_name) + raise exception.CinderException(msg) + + return poolinfo + + def _get_volume_by_name(self, name): + url = self.url + "/lun" + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Get volume by name error!') + + volume_id = None + if "data" in result: + for item in result['data']: + if name == item['NAME']: + volume_id = item['ID'] + break + return volume_id + + def _active_snapshot(self, snapshot_id): + activeurl = self.url + "/snapshot/activate" + data = json.dumps({"SNAPSHOTLIST": [snapshot_id]}) + result = self.call(activeurl, data) + self._assert_rest_result(result, 'Active snapshot error.') + + def _create_snapshot(self, snapshot): + snapshot_name = self._encode_name(snapshot['id']) + volume_name = self._encode_name(snapshot['volume_id']) + + LOG.debug(_('create_snapshot:snapshot name:%(snapshot)s, ' + 'volume name:%(volume)s.') + % {'snapshot': snapshot_name, + 'volume': volume_name}) + + lun_id = self._get_volume_by_name(volume_name) + url = self.url + "/snapshot" + data = json.dumps({"TYPE": "27", + "NAME": snapshot_name, + "PARENTTYPE": "11", + "PARENTID": lun_id}) + result = self.call(url, data) + self._assert_rest_result(result, 'Create snapshot error.') + + if 'data' not in result: + raise exception.CinderException(_('Create snapshot error.')) + + return result['data']['ID'] + + def create_snapshot(self, snapshot): + snapshot_id = self._create_snapshot(snapshot) + self._active_snapshot(snapshot_id) + + def _stop_snapshot(self, snapshot): + snapshot_name = self._encode_name(snapshot['id']) + volume_name = self._encode_name(snapshot['volume_id']) + + LOG.debug(_('_stop_snapshot:snapshot name:%(snapshot)s, ' + 'volume name:%(volume)s.') + % {'snapshot': snapshot_name, + 'volume': volume_name}) + + snapshotid = self._get_snapshotid_by_name(snapshot_name) + stopdata = json.dumps({"ID": snapshotid}) + url = self.url + "/snapshot/stop" + result = self.call(url, stopdata, "PUT") + self._assert_rest_result(result, 'Stop snapshot error.') + + return snapshotid + + def _delete_snapshot(self, snapshotid): + url = self.url + "/snapshot/%s" % snapshotid + data = json.dumps({"TYPE": "27", "ID": snapshotid}) + result = self.call(url, data, "DELETE") + self._assert_rest_result(result, 'Delete snapshot error.') + + def delete_snapshot(self, snapshot): + snapshotid = self._stop_snapshot(snapshot) + self._delete_snapshot(snapshotid) + + def _get_snapshotid_by_name(self, name): + url = self.url + "/snapshot" + data = json.dumps({"TYPE": "27"}) + result = self.call(url, data, "GET") + self._assert_rest_result(result, 'Get snapshot id error.') + + snapshot_id = None + if "data" in result: + for item in result['data']: + if name == item['NAME']: + snapshot_id = item['ID'] + break + return snapshot_id + + def _copy_volume(self, volume, copy_name, src_lun, tgt_lun): + luncopy_id = self._create_luncopy(copy_name, + src_lun, tgt_lun) + try: + self._start_luncopy(luncopy_id) + self._wait_for_luncopy(luncopy_id) + except Exception: + with excutils.save_and_reraise_exception(): + self._delete_luncopy(luncopy_id) + self.delete_volume(volume) + + self._delete_luncopy(luncopy_id) + + 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._encode_name(snapshot['id']) + src_lun_id = self._get_snapshotid_by_name(snapshot_name) + tgt_lun_id = self.create_volume(volume) + luncopy_name = self._encode_name(volume['id']) + + self._copy_volume(volume, luncopy_name, src_lun_id, tgt_lun_id) + + def create_cloned_volume(self, volume, src_vref): + """Clone a new volume from an existing volume.""" + volume_name = self._encode_name(src_vref['id']) + src_lun_id = self._get_volume_by_name(volume_name) + tgt_lun_id = self.create_volume(volume) + luncopy_name = self._encode_name(volume['id']) + + self._copy_volume(volume, luncopy_name, src_lun_id, tgt_lun_id) + + def _create_luncopy(self, luncopyname, srclunid, tgtlunid): + """Create a luncopy.""" + url = self.url + "/luncopy" + data = json.dumps({"TYPE": "219", + "NAME": luncopyname, + "DESCRIPTION": luncopyname, + "COPYSPEED": "2", + "LUNCOPYTYPE": "1", + "SOURCELUN": ("INVALID;%s;INVALID;INVALID;INVALID" + % srclunid), + "TARGETLUN": ("INVALID;%s;INVALID;INVALID;INVALID" + % tgtlunid)}) + result = self.call(url, data) + self._assert_rest_result(result, 'Create lun copy error.') + + if "data" not in result: + raise exception.CinderException(_('Create luncopy error.')) + + return result['data']['ID'] + + def _add_host_into_hostgroup(self, host_name): + """Associate host to hostgroup. + + If host group doesn't exist, create one. + + """ + + hostgroup_id = self._find_hostgroup(host_name) + if hostgroup_id is None: + hostgroup_id = self._create_hostgroup(host_name) + + hostid = self._find_host(host_name) + if hostid is None: + hostid = self._add_host(host_name) + self._associate_host_to_hostgroup(hostgroup_id, hostid) + + return hostid, hostgroup_id + + def _mapping_hostgroup_and_lungroup(self, volume_name, + hostgroup_id, host_id): + """Add hostgroup and lungroup to view.""" + lungroup_id = self._find_lungroup(volume_name) + lun_id = self._get_volume_by_name(volume_name) + view_id = self._find_mapping_view(volume_name) + + LOG.debug(_('_mapping_hostgroup_and_lungroup: lun_group: %(lun_group)s' + 'view_id: %(view_id)s') + % {'lun_group': str(lungroup_id), + 'view_id': str(view_id)}) + + try: + if view_id is None: + view_id = self._add_mapping_view(volume_name, host_id) + self._associate_hostgroup_to_view(view_id, hostgroup_id) + self._associate_lungroup_to_view(view_id, lungroup_id) + else: + if not self._hostgroup_associated(view_id, hostgroup_id): + self._associate_hostgroup_to_view(view_id, hostgroup_id) + if not self._lungroup_associated(view_id, lungroup_id): + self._associate_lungroup_to_view(view_id, lungroup_id) + + except Exception: + with excutils.save_and_reraise_exception(): + self._delete_hostgoup_mapping_view(view_id, hostgroup_id) + self._delete_lungroup_mapping_view(view_id, lungroup_id) + self._delete_mapping_view(view_id) + + return lun_id + + def _ensure_initiator_added(self, initiator_name, hostid): + added = self._initiator_is_added_to_array(initiator_name) + if not added: + self._add_initiator_to_array(initiator_name) + else: + if self._is_initiator_associated_to_host(initiator_name) is False: + self._associate_initiator_to_host(initiator_name, hostid) + + def initialize_connection_iscsi(self, volume, connector): + """Map a volume to a host and return target iSCSI information.""" + initiator_name = connector['initiator'] + host_name = connector['host'] + volume_name = self._encode_name(volume['id']) + + LOG.debug(_('initiator name:%(initiator_name)s, ' + 'volume name:%(volume)s.') + % {'initiator_name': initiator_name, + 'volume': volume_name}) + (iscsi_iqn, target_ip) = self._get_iscsi_params(connector) + + #create host_goup if not exist + hostid, hostgroup_id = self._add_host_into_hostgroup(host_name) + self._ensure_initiator_added(initiator_name, hostid) + + # Mapping lungooup and hostgoup to view + lun_id = self._mapping_hostgroup_and_lungroup(volume_name, + hostgroup_id, hostid) + hostlunid = self._find_host_lun_id(hostid, lun_id) + LOG.debug(_("host lun id is %s") % hostlunid) + + # Return iSCSI properties. + properties = {} + properties['target_discovered'] = False + properties['target_portal'] = ('%s:%s' % (target_ip, '3260')) + properties['target_iqn'] = iscsi_iqn + properties['target_lun'] = int(hostlunid) + properties['volume_id'] = volume['id'] + + return {'driver_volume_type': 'iscsi', 'data': properties} + + def initialize_connection_fc(self, volume, connector): + wwns = connector['wwpns'] + host_name = connector['host'] + volume_name = self._encode_name(volume['id']) + + LOG.debug(_('initiator name:%(initiator_name)s, ' + 'volume name:%(volume)s.') + % {'initiator_name': wwns, + 'volume': volume_name}) + + # Create host goup if not exist + hostid, hostgroup_id = self._add_host_into_hostgroup(host_name) + + free_wwns = self._get_connected_free_wwns() + LOG.debug(_("the free wwns %s") % free_wwns) + for wwn in wwns: + if wwn in free_wwns: + self._add_fc_port_to_host(hostid, wwn) + + lun_id = self._mapping_hostgroup_and_lungroup(volume_name, + hostgroup_id, hostid) + host_lun_id = self._find_host_lun_id(hostid, lun_id) + + tgt_port_wwns = [] + for wwn in wwns: + tgtwwpns = self._get_fc_target_wwpns(wwn) + if tgtwwpns: + tgt_port_wwns.append(tgtwwpns) + + # Return FC properties. + properties = {} + properties['target_discovered'] = False + properties['target_wwn'] = tgt_port_wwns + properties['target_lun'] = int(host_lun_id) + properties['volume_id'] = volume['id'] + LOG.debug(_("the fc server properties is:%s") % properties) + + return {'driver_volume_type': 'fibre_channel', + 'data': properties} + + def _get_iscsi_tgt_port(self): + url = self.url + "/iscsidevicename" + result = self.call(url, None) + msg = 'Get iSCSI target port error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + return result['data'][0]['CMO_ISCSI_DEVICE_NAME'] + + def _find_hostgroup(self, groupname): + """Get the given hostgroup id.""" + url = self.url + "/hostgroup" + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Get host group information error.') + + host_group_id = None + if "data" in result: + for item in result['data']: + if groupname == item['NAME']: + host_group_id = item['ID'] + break + return host_group_id + + def _find_lungroup(self, lungroupname): + """Get the given hostgroup id.""" + url = self.url + "/lungroup" + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Get lun group information error.') + + lun_group_id = None + if 'data' in result: + for item in result['data']: + if lungroupname == item['NAME']: + lun_group_id = item['ID'] + break + return lun_group_id + + def _create_hostgroup(self, hostgroupname): + url = self.url + "/hostgroup" + data = json.dumps({"TYPE": "14", "NAME": hostgroupname}) + result = self.call(url, data) + msg = 'Create host group error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + return result['data']['ID'] + + def _create_lungroup(self, lungroupname): + url = self.url + "/lungroup" + data = json.dumps({"DESCRIPTION": lungroupname, + "NAME": lungroupname}) + result = self.call(url, data) + msg = 'Create lun group error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + return result['data']['ID'] + + def _delete_lungroup(self, lungroupid): + url = self.url + "/LUNGroup/" + lungroupid + result = self.call(url, None, "DELETE") + self._assert_rest_result(result, 'Delete lun group error.') + + def _lungroup_associated(self, viewid, lungroupid): + url_subfix = ("/mappingview/associate?TYPE=245&" + "ASSOCIATEOBJTYPE=256&ASSOCIATEOBJID=%s" % lungroupid) + url = self.url + url_subfix + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Check lun group associated error.') + + if "data" in result: + for item in result['data']: + if viewid == item['ID']: + return True + return False + + def _hostgroup_associated(self, viewid, hostgroupid): + url_subfix = ("/mappingview/associate?TYPE=245&" + "ASSOCIATEOBJTYPE=14&ASSOCIATEOBJID=%s" % hostgroupid) + url = self.url + url_subfix + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Check host group associated error.') + + if "data" in result: + for item in result['data']: + if viewid == item['ID']: + return True + return False + + def _find_host_lun_id(self, hostid, lunid): + time.sleep(2) + url = self.url + ("/lun/associate?TYPE=11&ASSOCIATEOBJTYPE=21" + "&ASSOCIATEOBJID=%s" % (hostid)) + result = self.call(url, None, "GET") + self._assert_rest_result(result, 'Find host lun id error.') + + host_lun_id = 1 + if "data" in result: + for item in result['data']: + if lunid == item['ID']: + associate_data = result['data'][0]['ASSOCIATEMETADATA'] + try: + hostassoinfo = json.loads(associate_data) + host_lun_id = hostassoinfo['HostLUNID'] + break + except Exception: + msg = _("_find_host_lun_id transfer data error! ") + raise exception.CinderException(msg) + return host_lun_id + + def _find_host(self, hostname): + """Get the given host ID.""" + url = self.url + "/host" + data = json.dumps({"TYPE": "21"}) + result = self.call(url, data, "GET") + self._assert_rest_result(result, 'Find host in host group error.') + + host_id = None + if "data" in result: + for item in result['data']: + if hostname == item['NAME']: + host_id = item['ID'] + break + return host_id + + def _add_host(self, hostname): + """Add a new host.""" + url = self.url + "/host" + data = json.dumps({"TYPE": "21", + "NAME": hostname, + "OPERATIONSYSTEM": "0"}) + result = self.call(url, data) + self._assert_rest_result(result, 'Add new host error.') + + if "data" in result: + return result['data']['ID'] + else: + return None + + def _associate_host_to_hostgroup(self, hostgroupid, hostid): + url = self.url + "/host/associate" + data = json.dumps({"ID": hostgroupid, + "ASSOCIATEOBJTYPE": "21", + "ASSOCIATEOBJID": hostid}) + + result = self.call(url, data) + self._assert_rest_result(result, 'Associate host to host group error.') + + def _associate_lun_to_lungroup(self, lungroupid, lunid): + """Associate lun to lun group.""" + url = self.url + "/lungroup/associate" + data = json.dumps({"ID": lungroupid, + "ASSOCIATEOBJTYPE": "11", + "ASSOCIATEOBJID": lunid}) + result = self.call(url, data) + self._assert_rest_result(result, 'Associate lun to lun group error.') + + def _delete_associated_lun_from_lungroup(self, lungroupid, lunid): + """Remove lun from lun group.""" + + url = self.url + ("/lungroup/associate?ID=%s" + "&ASSOCIATEOBJTYPE=11&ASSOCIATEOBJID=%s" + % (lungroupid, lunid)) + + result = self.call(url, None, 'DELETE') + self._assert_rest_result(result, + 'Delete associated lun from lun group error') + + def _initiator_is_added_to_array(self, ininame): + """Check whether the initiator is already added in array.""" + url = self.url + "/iscsi_initiator/" + ininame + data = json.dumps({"TYPE": "222", "ID": ininame}) + result = self.call(url, data, "GET") + self._assert_rest_result(result, + 'Check initiator added to array error.') + + if "data" in result and result['data']['ID']: + return True + else: + return False + + def _is_initiator_associated_to_host(self, ininame): + """Check whether the initiator is associated to the host.""" + url = self.url + "/iscsi_initiator/" + ininame + data = json.dumps({"TYPE": "222", "ID": ininame}) + result = self.call(url, data, "GET") + self._assert_rest_result(result, + 'Check initiator associated to host error.') + + if "data" in result and result['data']['ISFREE'] == "true": + return True + else: + return False + + def _add_initiator_to_array(self, ininame): + """Add a new initiator to storage device.""" + url = self.url + "/iscsi_initiator/" + data = json.dumps({"TYPE": "222", + "ID": ininame, + "USECHAP": "False"}) + result = self.call(url, data) + self._assert_rest_result(result, 'Add initiator to array error.') + + def _associate_initiator_to_host(self, ininame, hostid): + """Associate initiator with the host.""" + url = self.url + "/iscsi_initiator/" + ininame + data = json.dumps({"TYPE": "222", + "ID": ininame, + "USECHAP": "False", + "PARENTTYPE": "21", + "PARENTID": hostid}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Associate initiator to host error.') + + def _find_mapping_view(self, name): + """Find mapping view.""" + url = self.url + "/mappingview" + data = json.dumps({"TYPE": "245"}) + result = self.call(url, data, "GET") + msg = 'Find map view error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + viewid = None + for item in result['data']: + if name == item['NAME']: + viewid = item['ID'] + break + return viewid + + def _add_mapping_view(self, name, host_id): + url = self.url + "/mappingview" + data = json.dumps({"NAME": name, "TYPE": "245"}) + result = self.call(url, data) + self._assert_rest_result(result, 'Add map view error.') + + return result['data']['ID'] + + def _associate_hostgroup_to_view(self, viewID, hostGroupID): + url = self.url + "/MAPPINGVIEW/CREATE_ASSOCIATE" + data = json.dumps({"ASSOCIATEOBJTYPE": "14", + "ASSOCIATEOBJID": hostGroupID, + "TYPE": "245", + "ID": viewID}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Associate host to view error.') + + def _associate_lungroup_to_view(self, viewID, lunGroupID): + url = self.url + "/MAPPINGVIEW/CREATE_ASSOCIATE" + data = json.dumps({"ASSOCIATEOBJTYPE": "256", + "ASSOCIATEOBJID": lunGroupID, + "TYPE": "245", + "ID": viewID}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Associate lun group to view error.') + + def _delete_lungroup_mapping_view(self, view_id, lungroup_id): + """remove lun group associate from the mapping view.""" + url = self.url + "/mappingview/REMOVE_ASSOCIATE" + data = json.dumps({"ASSOCIATEOBJTYPE": "256", + "ASSOCIATEOBJID": lungroup_id, + "TYPE": "245", + "ID": view_id}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Delete lun group from view error.') + + def _delete_hostgoup_mapping_view(self, view_id, hostgroup_id): + """remove host group associate from the mapping view.""" + url = self.url + "/mappingview/REMOVE_ASSOCIATE" + data = json.dumps({"ASSOCIATEOBJTYPE": "14", + "ASSOCIATEOBJID": hostgroup_id, + "TYPE": "245", + "ID": view_id}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Delete host group from view error.') + + def _delete_mapping_view(self, view_id): + """remove mapping view from the storage.""" + url = self.url + "/mappingview/" + view_id + result = self.call(url, None, "DELETE") + self._assert_rest_result(result, 'Delete map view error.') + + def terminate_connection(self, volume, connector, **kwargs): + """Delete map between a volume and a host.""" + initiator_name = connector['initiator'] + volume_name = self._encode_name(volume['id']) + host_name = connector['host'] + + LOG.debug(_('terminate_connection:volume name: %(volume)s, ' + 'initiator name: %(ini)s.') + % {'volume': volume_name, + 'ini': initiator_name}) + + view_id = self._find_mapping_view(volume_name) + hostgroup_id = self._find_hostgroup(host_name) + lungroup_id = self._find_lungroup(volume_name) + + if view_id is not None: + self._delete_hostgoup_mapping_view(view_id, hostgroup_id) + self._delete_lungroup_mapping_view(view_id, lungroup_id) + self._delete_mapping_view(view_id) + + def login_out(self): + """logout the session.""" + url = self.url + "/sessions" + result = self.call(url, None, "DELETE") + self._assert_rest_result(result, 'Log out of session error.') + + def _start_luncopy(self, luncopyid): + """Starte a LUNcopy.""" + url = self.url + "/LUNCOPY/start" + data = json.dumps({"TYPE": "219", "ID": luncopyid}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Start lun copy error.') + + def _get_capacity(self): + """Get free capacity and total capacity of the pools.""" + poolinfo = self._find_pool_info() + pool_capacity = {'total_capacity': 0.0, + 'CAPACITY': 0.0} + + if poolinfo: + total = int(poolinfo['TOTALCAPACITY']) / 1024.0 / 1024.0 / 2 + free = int(poolinfo['CAPACITY']) / 1024.0 / 1024.0 / 2 + pool_capacity['total_capacity'] = total + pool_capacity['free_capacity'] = free + + return pool_capacity + + def _get_lun_conf_params(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'} + + root = self._read_xml() + luntype = root.findtext('LUN/LUNType') + if luntype: + if luntype.strip() in ['Thick', 'Thin']: + lunsetinfo['LUNType'] = luntype.strip() + if luntype.strip() == 'Thick': + lunsetinfo['LUNType'] = 0 + if luntype.strip() == 'Thin': + lunsetinfo['LUNType'] = 1 + + elif luntype is not '' and luntype is not None: + err_msg = (_('Config file is wrong. LUNType must be "Thin"' + ' or "Thick". LUNType:%(fetchtype)s') + % {'fetchtype': luntype}) + raise exception.VolumeBackendAPIException(data=err_msg) + + stripunitsize = root.findtext('LUN/StripUnitSize') + if stripunitsize is not None: + lunsetinfo['StripUnitSize'] = stripunitsize.strip() + writetype = root.findtext('LUN/WriteType') + if writetype is not None: + lunsetinfo['WriteType'] = writetype.strip() + mirrorswitch = root.findtext('LUN/MirrorSwitch') + if mirrorswitch is not None: + lunsetinfo['MirrorSwitch'] = mirrorswitch.strip() + + prefetch = root.find('LUN/Prefetch') + fetchtype = prefetch.attrib['Type'] + if prefetch is not None and prefetch.attrib['Type']: + if fetchtype in ['0', '1', '2', '3']: + lunsetinfo['PrefetchType'] = fetchtype.strip() + typevalue = prefetch.attrib['Value'].strip() + if lunsetinfo['PrefetchType'] == '1': + lunsetinfo['PrefetchValue'] = typevalue + elif lunsetinfo['PrefetchType'] == '2': + lunsetinfo['PrefetchValue'] = typevalue + else: + err_msg = (_('PrefetchType config is wrong. PrefetchType' + ' must in 1,2,3,4. fetchtype is:%(fetchtype)s') + % {'fetchtype': fetchtype}) + raise exception.CinderException(err_msg) + else: + LOG.debug(_('Use default prefetch fetchtype. ' + 'Prefetch fetchtype:Intelligent.')) + + return lunsetinfo + + def _wait_for_luncopy(self, luncopyid): + """Wait for LUNcopy to complete.""" + while True: + luncopy_info = self._get_luncopy_info(luncopyid) + if luncopy_info['status'] == '40': + break + elif luncopy_info['state'] != '1': + err_msg = (_('_wait_for_luncopy:LUNcopy status is not normal.' + 'LUNcopy name: %(luncopyname)s') + % {'luncopyname': luncopyid}) + raise exception.VolumeBackendAPIException(data=err_msg) + time.sleep(10) + + def _get_luncopy_info(self, luncopyid): + """Get LUNcopy information.""" + url = self.url + "/LUNCOPY?range=[0-100000]" + data = json.dumps({"TYPE": "219", }) + result = self.call(url, data, "GET") + self._assert_rest_result(result, 'Get lun copy information error.') + + luncopyinfo = {} + if "data" in result: + for item in result['data']: + if luncopyid == item['ID']: + luncopyinfo['name'] = item['NAME'] + luncopyinfo['id'] = item['ID'] + luncopyinfo['state'] = item['HEALTHSTATUS'] + luncopyinfo['status'] = item['RUNNINGSTATUS'] + break + return luncopyinfo + + def _delete_luncopy(self, luncopyid): + """Delete a LUNcopy.""" + url = self.url + "/LUNCOPY/%s" % luncopyid + result = self.call(url, None, "DELETE") + self._assert_rest_result(result, 'Delete lun copy error.') + + def _get_connected_free_wwns(self): + """Get free connected FC port WWNs. + + If no new ports connected, return an empty list. + """ + url = self.url + "/fc_initiator?ISFREE=true&range=[0-1000]" + result = self.call(url, None, "GET") + msg = 'Get connected free FC wwn error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + wwns = [] + for item in result['data']: + wwns.append(item['ID']) + return wwns + + def _add_fc_port_to_host(self, hostid, wwn, multipathtype=0): + """Add a FC port to the host.""" + url = self.url + "/fc_initiator/" + wwn + data = json.dumps({"TYPE": "223", + "ID": wwn, + "PARENTTYPE": 21, + "PARENTID": hostid}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Add FC port to host error.') + + def _get_iscsi_port_info(self, ip): + """Get iscsi port info in order to build the iscsi target iqn.""" + url = self.url + "/eth_port" + result = self.call(url, None, "GET") + msg = 'Get iSCSI port information error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + iscsi_port_info = None + for item in result['data']: + if ip == item['IPV4ADDR']: + iscsi_port_info = item['LOCATION'] + break + + return iscsi_port_info + + def _get_iscsi_conf(self): + """Get iSCSI info from config file.""" + iscsiinfo = {} + root = self._read_xml() + iscsiinfo['DefaultTargetIP'] = \ + root.findtext('iSCSI/DefaultTargetIP').strip() + initiator_list = [] + tmp_dic = {} + for dic in root.findall('iSCSI/Initiator'): + # Strip values of dic + 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, iscsiip): + """Get target iSCSI iqn.""" + LOG.debug(_('_get_tgt_iqn: iSCSI IP is %s.') % iscsiip) + ip_info = self._get_iscsi_port_info(iscsiip) + iqn_prefix = self._get_iscsi_tgt_port() + LOG.debug(_('request ip info is %s.') % ip_info) + split_list = ip_info.split(".") + newstr = split_list[1] + split_list[2] + LOG.debug(_('new str info is %s.') % newstr) + + if ip_info: + if newstr[0] == 'A': + ctr = "0" + elif newstr[0] == 'B': + ctr = "1" + interface = '0' + newstr[1] + port = '0' + newstr[3] + 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 + iqn = iqn_prefix + ':' + iqn_suffix + ':' + iscsiip + LOG.debug(_('_get_tgt_iqn: iSCSI target iqn is %s') % iqn) + return iqn + else: + return None + + def _get_fc_target_wwpns(self, wwn): + url = (self.url + + "/host_link?INITIATOR_TYPE=223&INITIATOR_PORT_WWN=" + wwn) + result = self.call(url, None, "GET") + msg = 'Get FC target wwpn error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + fc_wwpns = None + for item in result['data']: + if wwn == item['INITIATOR_PORT_WWN']: + fc_wwpns = item['TARGET_PORT_WWN'] + break + + return fc_wwpns + + def _parse_volume_type(self, volume): + type_id = volume['volume_type_id'] + params = self._get_lun_conf_params() + LOG.debug(_('_parse_volume_type: type id: %(type_id)s ' + 'config parameter is: %(params)s') + % {'type_id': type_id, + 'params': params}) + + poolinfo = self._find_pool_info() + volume_size = self._get_volume_size(poolinfo, volume) + params['volume_size'] = volume_size + params['pool_id'] = poolinfo['ID'] + + if type_id is not None: + ctxt = context.get_admin_context() + volume_type = volume_types.get_volume_type(ctxt, type_id) + 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 QOS_KEY: + params["qos"] = value.strip() + params["qos_level"] = key + elif key in TIER_KEY: + params["tier"] = value.strip() + elif key in params.keys(): + params[key] = value.strip() + else: + conf = self.configuration.cinder_huawei_conf_file + LOG.warn(_('_parse_volume_type: Unacceptable paramater ' + '%(key)s. Please check this key in extra_specs ' + 'and make it consistent with the configuration ' + 'file %(conf)s.') % {'key': key, 'conf': conf}) + + LOG.debug(_("The config parameters are: %s") % params) + return params + + def update_volume_stats(self, refresh=False): + capacity = self._get_capacity() + data = {} + data['vendor_name'] = 'Huawei' + data['total_capacity_gb'] = capacity['total_capacity'] + data['free_capacity_gb'] = capacity['free_capacity'] + data['reserved_percentage'] = 0 + data['QoS_support'] = True + data['Tier_support'] = True + return data + + def _find_qos_policy_info(self, policy_name): + url = self.url + "/ioclass" + result = self.call(url, None, "GET") + msg = 'Get qos policy error.' + self._assert_rest_result(result, msg) + + if "data" not in result: + raise exception.CinderException(_('%s') % msg) + + qos_info = {} + for item in result['data']: + if policy_name == item['NAME']: + qos_info['ID'] = item['ID'] + lun_list = json.loads(item['LUNLIST']) + qos_info['LUNLIST'] = lun_list + break + return qos_info + + def _update_qos_policy_lunlist(self, lunlist, policy_id): + url = self.url + "/ioclass/" + policy_id + data = json.dumps({"TYPE": "230", + "ID": policy_id, + "LUNLIST": lunlist}) + result = self.call(url, data, "PUT") + self._assert_rest_result(result, 'Up date qos policy error.') + + 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['HVSURL'] = root.findtext('Storage/HVSURL').strip() + + need_encode = False + for key in ['UserName', 'UserPassword']: + node = root.find('Storage/%s' % key) + node_text = node.text + # 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.warn(_('%s') % err) + + return logininfo + + def _change_file_mode(self, filepath): + utils.execute('chmod', '777', filepath, run_as_root=True) + + def _check_conf_file(self): + """Check the config file, make sure the essential items are set.""" + root = self._read_xml() + hvsurl = root.findtext('Storage/HVSURL') + username = root.findtext('Storage/UserName') + pwd = root.findtext('Storage/UserPassword') + pool_node = root.findall('LUN/StoragePool') + + if (not hvsurl) or (not username) or (not pwd): + err_msg = (_('_check_conf_file: Config file invalid. HVSURL,' + ' UserName and UserPassword must be set.')) + LOG.error(err_msg) + raise exception.InvalidInput(reason=err_msg) + + if not pool_node: + err_msg = (_('_check_conf_file: Config file invalid. ' + 'StoragePool must be set.')) + raise exception.InvalidInput(reason=err_msg) + + def _get_iscsi_params(self, connector): + """Get target iSCSI params, including iqn, IP.""" + initiator = connector['initiator'] + iscsi_conf = self._get_iscsi_conf() + 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 = self._get_tgt_iqn(target_ip) + + return (target_iqn, target_ip)