From ecfb70cfebed4a40c24bcb874c18eede62a4b378 Mon Sep 17 00:00:00 2001 From: ningwei Date: Mon, 13 Jun 2016 02:57:51 -0400 Subject: [PATCH] Add cinder backend driver for Huawei FusionStorage It will support the minimum set of features required in Cinder: - Volume Create/Delete - Volume Attach/Detach - Snapshot Create/Delete - Create Volume from Snapshot - Get Volume Stats - Copy Image to Volume - Copy Volume to Image - Clone Volume - Extend Volume DocImpact Implements: bp fusionstorage-cinder-driver Co-Authored-By: wangxiyuan Change-Id: I26a809adb7bef4370eb53c634dc9bae0c74b4a8a --- cinder/opts.py | 3 + .../volume/drivers/fusionstorage/__init__.py | 0 .../drivers/fusionstorage/test_dsware.py | 771 ++++++++++++++++++ .../drivers/fusionstorage/test_fspythonapi.py | 447 ++++++++++ .../volume/drivers/fusionstorage/__init__.py | 0 cinder/volume/drivers/fusionstorage/dsware.py | 624 ++++++++++++++ .../drivers/fusionstorage/fspythonapi.py | 499 ++++++++++++ ...torage-cinder-driver-8f3bca98f6e2065a.yaml | 3 + 8 files changed, 2347 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/fusionstorage/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py create mode 100644 cinder/tests/unit/volume/drivers/fusionstorage/test_fspythonapi.py create mode 100644 cinder/volume/drivers/fusionstorage/__init__.py create mode 100644 cinder/volume/drivers/fusionstorage/dsware.py create mode 100644 cinder/volume/drivers/fusionstorage/fspythonapi.py create mode 100644 releasenotes/notes/fusionstorage-cinder-driver-8f3bca98f6e2065a.yaml diff --git a/cinder/opts.py b/cinder/opts.py index 64c6ca5f2..42faf805d 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -87,6 +87,8 @@ from cinder.volume.drivers.emc import xtremio as \ from cinder.volume.drivers import eqlx as cinder_volume_drivers_eqlx from cinder.volume.drivers.fujitsu import eternus_dx_common as \ cinder_volume_drivers_fujitsu_eternusdxcommon +from cinder.volume.drivers.fusionstorage import dsware as \ + cinder_volume_drivers_fusionstorage_dsware from cinder.volume.drivers import glusterfs as cinder_volume_drivers_glusterfs from cinder.volume.drivers import hgst as cinder_volume_drivers_hgst from cinder.volume.drivers.hitachi import hbsd_common as \ @@ -312,6 +314,7 @@ def list_opts(): [cinder_scheduler_scheduleroptions. scheduler_json_config_location_opt], cinder_volume_drivers_zfssa_zfssanfs.ZFSSA_OPTS, + cinder_volume_drivers_fusionstorage_dsware.volume_opts, cinder_volume_drivers_kaminario_kaminariocommon. kaminario1_opts, cinder_volume_drivers_disco_disco.disco_opts, diff --git a/cinder/tests/unit/volume/drivers/fusionstorage/__init__.py b/cinder/tests/unit/volume/drivers/fusionstorage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py b/cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py new file mode 100644 index 000000000..43d971297 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/fusionstorage/test_dsware.py @@ -0,0 +1,771 @@ +# Copyright (c) 2013 - 2016 Huawei Technologies Co., Ltd. +# 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 FusionStorage drivers. +""" + +import mock +from oslo_config import cfg +from oslo_service import loopingcall + +from cinder import exception +from cinder.image import image_utils +from cinder import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.fusionstorage import dsware +from cinder.volume.drivers.fusionstorage import fspythonapi + + +test_volume = {'name': 'test_vol1', + 'size': 4, + 'volume_metadata': '', + 'host': 'host01@dsware', + 'instance_uuid': None, + 'provider_id': '127.0.0.1'} + +test_src_volume = {'name': 'test_vol2', + 'size': 4, + 'status': 'available'} + +test_snapshot = { + 'name': 'test_snapshot1', + 'volume_id': 'vol1', + 'volume_size': '2'} + + +class FakeDSWAREDriver(dsware.DSWAREDriver): + def __init__(self): + configuration = conf.Configuration( + [ + cfg.StrOpt('fake'), + ], + None + ) + super(FakeDSWAREDriver, self).__init__(configuration=configuration) + self.dsware_client = fspythonapi.FSPythonApi() + self.manage_ip = '127.0.0.1' + self.pool_type = '1' + + +class DSwareDriverTestCase(test.TestCase): + def setUp(self): + super(DSwareDriverTestCase, self).setUp() + self.driver = FakeDSWAREDriver() + + def test_private_get_dsware_manage_ip(self): + retval = self.driver._get_dsware_manage_ip(test_volume) + self.assertEqual('127.0.0.1', retval) + + test_volume_fail = {'name': 'test_vol', + 'size': 4, + 'volume_metadata': '', + 'host': 'host01@dsware', + 'provider_id': None} + self.assertRaises(exception.CinderException, + self.driver._get_dsware_manage_ip, + test_volume_fail) + + def test_private_get_poolid_from_host(self): + retval = self.driver._get_poolid_from_host( + 'abc@fusionstorage_sas2copy#0') + self.assertEqual('0', retval) + + retval = self.driver._get_poolid_from_host( + 'abc@fusionstorage_sas2copy@0') + self.assertEqual(self.driver.pool_type, retval) + + retval = self.driver._get_poolid_from_host(None) + self.assertEqual(self.driver.pool_type, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'query_dsware_version') + @mock.patch.object(dsware.DSWAREDriver, '_get_poolid_from_host') + def test_private_create_volume_old_version(self, mock_get_poolid, + mock_query_dsware, + mock_create_volume): + # query_dsware_version return 1, old version + mock_query_dsware.return_value = 1 + mock_create_volume.return_value = 0 + self.driver._create_volume(test_volume['name'], + test_volume['size'], + True, + 'abc@fusionstorage_sas2copy') + mock_create_volume.assert_called_with(test_volume['name'], 0, + test_volume['size'], 1) + + self.driver._create_volume(test_volume['name'], + test_volume['size'], + False, + 'abc@fusionstorage_sas2copy') + mock_create_volume.assert_called_with(test_volume['name'], 0, + test_volume['size'], 0) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'query_dsware_version') + @mock.patch.object(dsware.DSWAREDriver, '_get_poolid_from_host') + def test_private_create_volume_new_version(self, mock_get_poolid, + mock_query_dsware, + mock_create_volume): + # query_dsware_version return 0, new version + mock_query_dsware.return_value = 0 + mock_get_poolid.return_value = 0 + mock_create_volume.return_value = 0 + self.driver._create_volume(test_volume['name'], + test_volume['size'], + True, + 'abcE@fusionstorage_sas2copy#0') + mock_create_volume.assert_called_with(test_volume['name'], 0, + test_volume['size'], 1) + + self.driver._create_volume(test_volume['name'], + test_volume['size'], + False, + 'abc@fusionstorage_sas2copy#0') + mock_create_volume.assert_called_with(test_volume['name'], 0, + test_volume['size'], 0) + + mock_query_dsware.return_value = 0 + mock_get_poolid.return_value = 1 + mock_create_volume.return_value = 0 + self.driver._create_volume(test_volume['name'], + test_volume['size'], + True, + 'abc@fusionstorage_sas2copy#1') + mock_create_volume.assert_called_with(test_volume['name'], 1, + test_volume['size'], 1) + + self.driver._create_volume(test_volume['name'], + test_volume['size'], + False, + 'abc@fusionstorage_sas2copy#1') + mock_create_volume.assert_called_with(test_volume['name'], 1, + test_volume['size'], 0) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'query_dsware_version') + @mock.patch.object(dsware.DSWAREDriver, '_get_poolid_from_host') + def test_private_create_volume_query_version_fail(self, mock_get_poolid, + mock_query_dsware, + mock_create_volume): + # query_dsware_version return 500015, query dsware version failed! + mock_query_dsware.return_value = 500015 + self.assertRaises(exception.CinderException, + self.driver._create_volume, + test_volume['name'], + test_volume['size'], + True, + 'abc@fusionstorage_sas2copy#0') + self.assertRaises(exception.CinderException, + self.driver._create_volume, + test_volume['name'], + test_volume['size'], + False, + 'abc@fusionstorage_sas2copy#0') + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'query_dsware_version') + @mock.patch.object(dsware.DSWAREDriver, '_get_poolid_from_host') + def test_private_create_volume_fail(self, mock_get_poolid, + mock_query_dsware, + mock_create_volume): + mock_query_dsware.return_value = 1 + # create_volume return 1, create volume failed + mock_create_volume.return_value = 1 + self.assertRaises(exception.CinderException, + self.driver._create_volume, + test_volume['name'], + test_volume['size'], + True, + 'abc@fusionstorage_sas2copy#0') + self.assertRaises(exception.CinderException, + self.driver._create_volume, + test_volume['name'], + test_volume['size'], + False, + 'abc@fusionstorage_sas2copy#0') + + @mock.patch.object(dsware.DSWAREDriver, '_create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'get_manage_ip') + def test_create_volume(self, mock_get_manage_ip, mock_create_volume): + # success + mock_get_manage_ip.return_value = self.driver.manage_ip + retval = self.driver.create_volume(test_volume) + self.assertEqual({"provider_id": self.driver.manage_ip}, + retval) + + # failure + mock_create_volume.side_effect = exception.CinderException( + 'DSWARE Create Volume failed!') + + self.assertRaises(exception.CinderException, + self.driver.create_volume, + test_volume) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume_from_snap') + def test_private_create_volume_from_snap(self, mock_create_volume): + mock_create_volume.side_effect = [0, 1] + self.driver._create_volume_from_snap(test_volume['name'], + test_volume['size'], + test_snapshot['name']) + # failure + self.assertRaises(exception.CinderException, + self.driver._create_volume_from_snap, + test_volume['name'], test_volume['size'], + test_snapshot['name']) + + @mock.patch.object(fspythonapi.FSPythonApi, 'extend_volume') + def test_extend_volume(self, mock_extend_volume): + mock_extend_volume.return_value = 0 + self.driver.extend_volume(test_volume, 5) + + mock_extend_volume.return_value = 0 + self.assertRaises(exception.CinderException, + self.driver.extend_volume, + test_volume, + 3) + + mock_extend_volume.return_value = 1 + self.assertRaises(exception.CinderException, + self.driver.extend_volume, + test_volume, + 5) + + @mock.patch.object(dsware.DSWAREDriver, '_create_volume_from_snap') + @mock.patch.object(fspythonapi.FSPythonApi, 'get_manage_ip') + def test_create_volume_from_snap(self, mock_manage_ip, mock_create_vol): + # success + mock_manage_ip.return_value = self.driver.manage_ip + retval = self.driver.create_volume_from_snapshot(test_volume, + test_snapshot) + self.assertEqual({"provider_id": self.driver.manage_ip}, + retval) + + # failure + mock_create_vol.side_effect = exception.CinderException( + 'DSWARE:create volume from snap failed') + self.assertRaises(exception.CinderException, + self.driver.create_volume_from_snapshot, + test_volume, test_snapshot) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume_from_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'get_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, + '_wait_for_create_cloned_volume_finish_timer') + def test_create_cloned_volume(self, mock_wait_finish, + mock_get_manage_ip, mock_create_volume): + # success + mock_create_volume.return_value = None + mock_get_manage_ip.return_value = self.driver.manage_ip + mock_wait_finish.return_value = True + retval = self.driver.create_cloned_volume(test_volume, test_src_volume) + self.assertEqual({"provider_id": "127.0.0.1"}, retval) + + # failure:create exception + mock_create_volume.return_value = 500015 + self.assertRaises(exception.CinderException, + self.driver.create_cloned_volume, + test_volume, test_src_volume) + # failure:wait exception + mock_create_volume.return_value = None + mock_wait_finish.return_value = False + self.assertRaises(exception.CinderException, + self.driver.create_cloned_volume, + test_volume, test_src_volume) + + @mock.patch.object(fspythonapi.FSPythonApi, 'query_volume') + def test_private_check_create_cloned_volume_finish(self, + mock_query_volume): + query_result_done = {'result': 0, 'vol_name': 'vol1', + 'father_name': 'vol1_father', 'status': '0', + 'vol_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'create_time': '01/01/2015'} + + query_result_doing = {'result': 0, 'vol_name': 'vol1', + 'father_name': 'vol1_father', 'status': '6', + 'vol_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'create_time': '01/01/2015'} + + mock_query_volume.side_effect = [ + query_result_done, query_result_doing, query_result_doing] + + # success + self.assertRaises(loopingcall.LoopingCallDone, + self.driver._check_create_cloned_volume_finish, + test_volume['name']) + + # in the process of creating volume + self.driver.count = self.driver.configuration.clone_volume_timeout - 1 + self.driver._check_create_cloned_volume_finish(test_volume['name']) + self.assertEqual(self.driver.configuration.clone_volume_timeout, + self.driver.count) + + # timeout + self.driver.count = self.driver.configuration.clone_volume_timeout + self.assertRaises(loopingcall.LoopingCallDone, + self.driver._check_create_cloned_volume_finish, + test_volume['name']) + + @mock.patch.object(dsware.DSWAREDriver, + '_check_create_cloned_volume_finish') + def test_private_wait_for_create_cloned_volume_finish_timer(self, + mock_check): + mock_check.side_effect = [loopingcall.LoopingCallDone(retvalue=True), + loopingcall.LoopingCallDone(retvalue=False)] + retval = self.driver._wait_for_create_cloned_volume_finish_timer( + test_volume['name']) + self.assertTrue(retval) + + retval = self.driver._wait_for_create_cloned_volume_finish_timer( + test_volume['name']) + self.assertFalse(retval) + + def test_private_analyse_output(self): + out = 'ret_code=10\nret_desc=test\ndev_addr=/sda\n' + retval = self.driver._analyse_output(out) + self.assertEqual({'dev_addr': '/sda', + 'ret_desc': 'test', 'ret_code': '10'}, + retval) + + out = 'abcdefg' + retval = self.driver._analyse_output(out) + self.assertEqual({}, retval) + + def test_private_attach_volume(self): + success = ['ret_code=0\nret_desc=success\ndev_addr=/dev/sdb\n', ''] + failure = ['ret_code=50510011\nret_desc=failed\ndev_addr=/dev/sdb\n', + ''] + mock_execute = self.mock_object(self.driver, '_execute') + mock_execute.side_effect = [success, failure] + # attached successful + retval = self.driver._attach_volume(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'success', 'ret_code': '0'}, + retval) + # attached failure + retval = self.driver._attach_volume(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'failed', 'ret_code': '50510011'}, + retval) + + def test_private_detach_volume(self): + success = ['ret_code=0\nret_desc=success\ndev_addr=/dev/sdb\n', ''] + failure = ['ret_code=50510011\nret_desc=failed\ndev_addr=/dev/sdb\n', + ''] + mock_execute = self.mock_object(self.driver, '_execute') + mock_execute.side_effect = [success, failure] + # detached successful + retval = self.driver._detach_volume(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'success', 'ret_code': '0'}, + retval) + # detached failure + retval = self.driver._detach_volume(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'failed', + 'ret_code': '50510011'}, + retval) + + def test_private_query_volume_attach(self): + success = ['ret_code=0\nret_desc=success\ndev_addr=/dev/sdb\n', ''] + failure = ['ret_code=50510011\nret_desc=failed\ndev_addr=/dev/sdb\n', + ''] + mock_execute = self.mock_object(self.driver, '_execute') + mock_execute.side_effect = [success, failure] + # query successful + retval = self.driver._query_volume_attach(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'success', + 'ret_code': '0'}, + retval) + # query failure + retval = self.driver._query_volume_attach(test_volume['name'], + self.driver.manage_ip) + self.assertEqual({'dev_addr': '/dev/sdb', + 'ret_desc': 'failed', + 'ret_code': '50510011'}, + retval) + + @mock.patch.object(dsware.DSWAREDriver, '_get_dsware_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, '_attach_volume') + @mock.patch.object(image_utils, 'fetch_to_raw') + @mock.patch.object(dsware.DSWAREDriver, '_detach_volume') + def test_copy_image_to_volume(self, mock_detach, mock_fetch, + mock_attach, mock_get_manage_ip): + success = {'ret_code': '0', + 'ret_desc': 'success', + 'dev_addr': '/dev/sdb'} + failure = {'ret_code': '50510011', + 'ret_desc': 'failed', + 'dev_addr': '/dev/sdb'} + context = '' + image_service = '' + image_id = '' + mock_get_manage_ip.return_value = '127.0.0.1' + mock_attach.side_effect = [success, failure, success] + mock_detach.side_effect = [success, failure, failure] + + # success + self.driver.copy_image_to_volume(context, test_volume, image_service, + image_id) + + # failure - attach failure + self.assertRaises(exception.CinderException, + self.driver.copy_image_to_volume, + context, test_volume, image_service, image_id) + + # failure - detach failure + self.assertRaises(exception.CinderException, + self.driver.copy_image_to_volume, + context, test_volume, image_service, image_id) + + @mock.patch.object(dsware.DSWAREDriver, '_get_dsware_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, '_attach_volume') + @mock.patch.object(dsware.DSWAREDriver, '_query_volume_attach') + @mock.patch.object(image_utils, 'upload_volume') + @mock.patch.object(dsware.DSWAREDriver, '_detach_volume') + def test_copy_volume_to_image_success(self, mock_detach, mock_upload, + mock_query, mock_attach, + mock_get_manage_ip): + success = {'ret_code': '0', + 'ret_desc': 'success', + 'dev_addr': '/dev/sdb'} + already_attached = {'ret_code': '50151401', + 'ret_desc': 'already_attached', + 'dev_addr': '/dev/sdb'} + context = '' + image_service = '' + image_meta = '' + + mock_get_manage_ip.return_value = '127.0.0.1' + mock_attach.return_value = success + mock_detach.return_value = success + self.driver.copy_volume_to_image(context, test_volume, image_service, + image_meta) + mock_upload.assert_called_with('', '', '', '/dev/sdb') + + mock_attach.return_value = already_attached + mock_query.return_value = success + mock_detach.return_value = success + self.driver.copy_volume_to_image(context, test_volume, image_service, + image_meta) + mock_upload.assert_called_with('', '', '', '/dev/sdb') + + @mock.patch.object(dsware.DSWAREDriver, '_get_dsware_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, '_attach_volume') + @mock.patch.object(dsware.DSWAREDriver, '_query_volume_attach') + @mock.patch.object(image_utils, 'upload_volume') + @mock.patch.object(dsware.DSWAREDriver, '_detach_volume') + def test_copy_volume_to_image_attach_fail(self, mock_detach, mock_upload, + mock_query, mock_attach, + mock_get_manage_ip): + failure = {'ret_code': '50510011', + 'ret_desc': 'failed', + 'dev_addr': '/dev/sdb'} + context = '' + image_service = '' + image_meta = '' + + mock_get_manage_ip.return_value = '127.0.0.1' + mock_attach.return_value = failure + self.assertRaises(exception.CinderException, + self.driver.copy_volume_to_image, + context, test_volume, image_service, image_meta) + mock_attach.return_value = None + self.assertRaises(exception.CinderException, + self.driver.copy_volume_to_image, + context, test_volume, image_service, image_meta) + + @mock.patch.object(dsware.DSWAREDriver, '_get_dsware_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, '_attach_volume') + @mock.patch.object(dsware.DSWAREDriver, '_query_volume_attach') + @mock.patch.object(image_utils, 'upload_volume') + @mock.patch.object(dsware.DSWAREDriver, '_detach_volume') + def test_copy_volume_to_image_query_attach_fail(self, mock_detach, + mock_upload, mock_query, + mock_attach, + mock_get_manage_ip): + already_attached = {'ret_code': '50151401', + 'ret_desc': 'already_attached', + 'dev_addr': '/dev/sdb'} + failure = {'ret_code': '50510011', + 'ret_desc': 'failed', + 'dev_addr': '/dev/sdb'} + context = '' + image_service = '' + image_meta = '' + + mock_get_manage_ip.return_value = '127.0.0.1' + mock_attach.return_value = already_attached + mock_query.return_value = failure + self.assertRaises(exception.CinderException, + self.driver.copy_volume_to_image, + context, test_volume, image_service, image_meta) + + mock_query.return_value = None + self.assertRaises(exception.CinderException, + self.driver.copy_volume_to_image, + context, test_volume, image_service, image_meta) + + @mock.patch.object(dsware.DSWAREDriver, '_get_dsware_manage_ip') + @mock.patch.object(dsware.DSWAREDriver, '_attach_volume') + @mock.patch.object(dsware.DSWAREDriver, '_query_volume_attach') + @mock.patch.object(image_utils, 'upload_volume') + @mock.patch.object(dsware.DSWAREDriver, '_detach_volume') + def test_copy_volume_to_image_upload_fail(self, mock_detach, mock_upload, + mock_query, mock_attach, + mock_get_manage_ip): + success = {'ret_code': '0', + 'ret_desc': 'success', + 'dev_addr': '/dev/sdb'} + already_attached = {'ret_code': '50151401', + 'ret_desc': 'already_attached', + 'dev_addr': '/dev/sdb'} + context = '' + image_service = '' + image_meta = '' + + mock_get_manage_ip.return_value = '127.0.0.1' + mock_attach.return_value = already_attached + mock_query.return_value = success + mock_upload.side_effect = exception.CinderException( + 'upload_volume error') + self.assertRaises(exception.CinderException, + self.driver.copy_volume_to_image, + context, test_volume, image_service, image_meta) + + @mock.patch.object(fspythonapi.FSPythonApi, 'query_volume') + def test_private_get_volume(self, mock_query): + result_success = {'result': 0} + result_not_exist = {'result': "50150005\n"} + result_exception = {'result': "50510006\n"} + + mock_query.side_effect = [ + result_success, result_not_exist, result_exception] + + retval = self.driver._get_volume(test_volume['name']) + self.assertTrue(retval) + + retval = self.driver._get_volume(test_volume['name']) + self.assertFalse(retval) + + self.assertRaises(exception.CinderException, + self.driver._get_volume, + test_volume['name']) + + @mock.patch.object(fspythonapi.FSPythonApi, 'delete_volume') + def test_private_delete_volume(self, mock_delete): + result_success = 0 + result_not_exist = '50150005\n' + result_being_deleted = '50151002\n' + result_exception = '51050006\n' + + mock_delete.side_effect = [result_success, result_not_exist, + result_being_deleted, result_exception] + + retval = self.driver._delete_volume(test_volume['name']) + self.assertTrue(retval) + + retval = self.driver._delete_volume(test_volume['name']) + self.assertTrue(retval) + + retval = self.driver._delete_volume(test_volume['name']) + self.assertTrue(retval) + + self.assertRaises(exception.CinderException, + self.driver._delete_volume, test_volume['name']) + + @mock.patch.object(dsware.DSWAREDriver, '_get_volume') + @mock.patch.object(dsware.DSWAREDriver, '_delete_volume') + def test_delete_volume(self, mock_delete, mock_get): + mock_get.return_value = False + retval = self.driver.delete_volume(test_volume) + self.assertTrue(retval) + + mock_get.return_value = True + mock_delete.return_value = True + retval = self.driver.delete_volume(test_volume) + self.assertTrue(retval) + + mock_get.return_value = True + mock_delete.side_effect = exception.CinderException( + 'delete volume exception') + self.assertRaises(exception.CinderException, + self.driver.delete_volume, + test_volume) + + mock_get.side_effect = exception.CinderException( + 'get volume exception') + self.assertRaises(exception.CinderException, + self.driver.delete_volume, + test_volume) + + @mock.patch.object(fspythonapi.FSPythonApi, 'query_snap') + def test_private_get_snapshot(self, mock_query): + result_success = {'result': 0} + result_not_found = {'result': "50150006\n"} + result_exception = {'result': "51050007\n"} + mock_query.side_effect = [result_success, result_not_found, + result_exception] + + retval = self.driver._get_snapshot(test_snapshot['name']) + self.assertTrue(retval) + + retval = self.driver._get_snapshot(test_snapshot['name']) + self.assertFalse(retval) + + self.assertRaises(exception.CinderException, + self.driver._get_snapshot, + test_snapshot['name']) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_snapshot') + def test_private_create_snapshot(self, mock_create): + mock_create.side_effect = [0, 1] + + self.driver._create_snapshot(test_snapshot['name'], + test_volume['name']) + + self.assertRaises(exception.CinderException, + self.driver._create_snapshot, + test_snapshot['name'], test_volume['name']) + + @mock.patch.object(fspythonapi.FSPythonApi, 'delete_snapshot') + def test_private_delete_snapshot(self, mock_delete): + mock_delete.side_effect = [0, 1] + + self.driver._delete_snapshot(test_snapshot['name']) + + self.assertRaises(exception.CinderException, + self.driver._delete_snapshot, test_snapshot['name']) + + @mock.patch.object(dsware.DSWAREDriver, '_get_volume') + @mock.patch.object(dsware.DSWAREDriver, '_create_snapshot') + def test_create_snapshot(self, mock_create, mock_get): + mock_get.return_value = True + self.driver.create_snapshot(test_snapshot) + + mock_create.side_effect = exception.CinderException( + 'create snapshot failed') + self.assertRaises(exception.CinderException, + self.driver.create_snapshot, test_snapshot) + + mock_get.side_effect = [ + False, exception.CinderException('get volume failed')] + self.assertRaises(exception.CinderException, + self.driver.create_snapshot, + test_snapshot) + self.assertRaises(exception.CinderException, + self.driver.create_snapshot, + test_snapshot) + + @mock.patch.object(dsware.DSWAREDriver, '_get_snapshot') + @mock.patch.object(dsware.DSWAREDriver, '_delete_snapshot') + def test_delete_snapshot(self, mock_delete, mock_get): + mock_get.side_effect = [True, False, exception.CinderException, True] + self.driver.delete_snapshot(test_snapshot) + self.driver.delete_snapshot(test_snapshot) + + self.assertRaises(exception.CinderException, + self.driver.delete_snapshot, + test_snapshot) + mock_delete.side_effect = exception.CinderException( + 'delete snapshot exception') + self.assertRaises(exception.CinderException, + self.driver.delete_snapshot, + test_snapshot) + + @mock.patch.object(fspythonapi.FSPythonApi, 'query_pool_info') + def test_private_update_single_pool_info_status(self, mock_query): + pool_info = {'result': 0, + 'pool_id': 10, + 'total_capacity': 10240, + 'used_capacity': 5120, + 'alloc_capacity': 7168} + pool_info_none = {'result': 1} + + mock_query.side_effect = [pool_info, pool_info_none] + + self.driver._update_single_pool_info_status() + self.assertEqual({'total_capacity_gb': 10.0, + 'free_capacity_gb': 5.0, + 'volume_backend_name': None, + 'vendor_name': 'Open Source', + 'driver_version': '1.0', + 'storage_protocol': 'dsware', + 'reserved_percentage': 0, + 'QoS_support': False}, + self.driver._stats) + + self.driver._update_single_pool_info_status() + self.assertIsNone(self.driver._stats) + + @mock.patch.object(fspythonapi.FSPythonApi, 'query_pool_type') + def test_private_update_multi_pool_of_same_type_status(self, mock_query): + query_result = (0, [{'result': 0, + 'pool_id': '0', + 'total_capacity': '10240', + 'used_capacity': '5120', + 'alloc_capacity': '7168'}]) + query_result_none = (0, []) + + mock_query.side_effect = [query_result, query_result_none] + + self.driver._update_multi_pool_of_same_type_status() + self.assertEqual({'volume_backend_name': None, + 'vendor_name': 'Open Source', + 'driver_version': '1.0', + 'storage_protocol': 'dsware', + 'pools': [{'pool_name': '0', + 'total_capacity_gb': 10.0, + 'allocated_capacity_gb': 5.0, + 'free_capacity_gb': 5.0, + 'QoS_support': False, + 'reserved_percentage': 0}]}, + self.driver._stats) + + self.driver._update_multi_pool_of_same_type_status() + self.assertIsNone(self.driver._stats) + + def test_private_calculate_pool_info(self): + pool_sets = [{'pool_id': 0, + 'total_capacity': 10240, + 'used_capacity': 5120, + 'QoS_support': False, + 'reserved_percentage': 0}] + retval = self.driver._calculate_pool_info(pool_sets) + self.assertEqual([{'pool_name': 0, + 'total_capacity_gb': 10.0, + 'allocated_capacity_gb': 5.0, + 'free_capacity_gb': 5.0, + 'QoS_support': False, + 'reserved_percentage': 0}], + retval) + + @mock.patch.object(dsware.DSWAREDriver, '_update_single_pool_info_status') + @mock.patch.object(dsware.DSWAREDriver, + '_update_multi_pool_of_same_type_status') + @mock.patch.object(fspythonapi.FSPythonApi, 'query_dsware_version') + def test_get_volume_stats(self, mock_query, mock_type, mock_info): + mock_query.return_value = 1 + + self.driver.get_volume_stats(False) + mock_query.assert_not_called() + + self.driver.get_volume_stats(True) + mock_query.assert_called_once_with() diff --git a/cinder/tests/unit/volume/drivers/fusionstorage/test_fspythonapi.py b/cinder/tests/unit/volume/drivers/fusionstorage/test_fspythonapi.py new file mode 100644 index 000000000..ddf1fac2d --- /dev/null +++ b/cinder/tests/unit/volume/drivers/fusionstorage/test_fspythonapi.py @@ -0,0 +1,447 @@ +# Copyright (c) 2013 - 2016 Huawei Technologies Co., Ltd. +# 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 FusionStorage drivers. +""" + +import mock + +from cinder import test +from cinder import utils +from cinder.volume.drivers.fusionstorage import fspythonapi + + +class FSPythonApiTestCase(test.TestCase): + + def setUp(self): + super(FSPythonApiTestCase, self).setUp() + self.api = fspythonapi.FSPythonApi() + + @mock.patch.object(fspythonapi.FSPythonApi, 'get_ip_port') + @mock.patch.object(fspythonapi.FSPythonApi, 'get_manage_ip') + @mock.patch.object(utils, 'execute') + def test_start_execute_cmd(self, mock_execute, + mock_get_manage_ip, mock_get_ip_port): + result1 = ['result=0\ndesc=success\n', ''] + result2 = ['result=50150007\ndesc=volume does not exist\n', ''] + result3 = ['result=50150008\ndesc=volume is being deleted\n', ''] + result4 = ['result=50\ndesc=exception\n', ''] + cmd = 'abcdef' + + mock_get_ip_port.return_value = ['127.0.0.1', '128.0.0.1'] + mock_get_manage_ip.return_value = '127.0.0.1' + + mock_execute.return_value = result1 + retval = self.api.start_execute_cmd(cmd, 0) + self.assertEqual('result=0', retval) + + mock_execute.return_value = result2 + retval = self.api.start_execute_cmd(cmd, 0) + self.assertEqual('result=0', retval) + + mock_execute.return_value = result3 + retval = self.api.start_execute_cmd(cmd, 0) + self.assertEqual('result=0', retval) + + mock_execute.return_value = result4 + retval = self.api.start_execute_cmd(cmd, 0) + self.assertEqual('result=50', retval) + + mock_execute.return_value = result1 + retval = self.api.start_execute_cmd(cmd, 1) + self.assertEqual(['result=0', 'desc=success', ''], retval) + + mock_execute.return_value = result2 + retval = self.api.start_execute_cmd(cmd, 1) + self.assertEqual('result=0', retval) + + mock_execute.return_value = result3 + retval = self.api.start_execute_cmd(cmd, 1) + self.assertEqual('result=0', retval) + + mock_execute.return_value = result4 + retval = self.api.start_execute_cmd(cmd, 1) + self.assertEqual(['result=50', 'desc=exception', ''], retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_create_volume(self, mock_start_execute): + mock_start_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.create_volume('volume_name', 'pool_id-123', 1024, 0) + self.assertEqual(0, retval) + + retval = self.api.create_volume('volume_name', 'pool_id-123', 1024, 0) + self.assertEqual('50150007\n', retval) + + retval = self.api.create_volume('volume_name', 'pool_id-123', 1024, 0) + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_extend_volume(self, mock_start_execute): + mock_start_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.extend_volume('volume_name', 1024) + self.assertEqual(0, retval) + + retval = self.api.extend_volume('volume_name', 1024) + self.assertEqual('50150007\n', retval) + + retval = self.api.extend_volume('volume_name', 1024) + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_create_volume_from_snap(self, mock_start_execute): + mock_start_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.create_volume_from_snap('volume_name', 1024, + 'snap_name') + self.assertEqual(0, retval) + + retval = self.api.create_volume_from_snap('volume_name', 1024, + 'snap_name') + self.assertEqual('50150007\n', retval) + + retval = self.api.create_volume_from_snap('volume_name', 1024, + 'snap_name') + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_create_fullvol_from_snap(self, mock_start_execute): + mock_start_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.create_fullvol_from_snap('volume_name', 'snap_name') + self.assertEqual(0, retval) + + retval = self.api.create_fullvol_from_snap('volume_name', 'snap_name') + self.assertEqual('50150007\n', retval) + + retval = self.api.create_fullvol_from_snap('volume_name', 'snap_name') + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_snapshot') + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'delete_snapshot') + @mock.patch.object(fspythonapi.FSPythonApi, 'delete_volume') + @mock.patch.object(fspythonapi.FSPythonApi, 'create_fullvol_from_snap') + def test_create_volume_from_volume(self, mock_create_fullvol, + mock_delete_volume, mock_delete_snap, + mock_create_volume, mock_create_snap): + mock_create_snap.return_value = 0 + mock_create_volume.return_value = 0 + mock_create_fullvol.return_value = 0 + + retval = self.api.create_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(0, retval) + + mock_create_snap.return_value = 1 + retval = self.api.create_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(1, retval) + + mock_create_snap.return_value = 0 + mock_create_volume.return_value = 1 + retval = self.api.create_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(1, retval) + + mock_create_volume.return_value = 0 + self.api.create_fullvol_from_snap.return_value = 1 + retval = self.api.create_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'create_snapshot') + @mock.patch.object(fspythonapi.FSPythonApi, 'create_volume_from_snap') + def test_create_clone_volume_from_volume(self, mock_volume, mock_snap): + mock_snap.side_effect = [0, 1] + mock_volume.side_effect = [0, 1] + retval = self.api.create_clone_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(0, retval) + retval = self.api.create_clone_volume_from_volume('vol_name', 1024, + 'src_vol_name') + self.assertEqual(1, retval) + + def test_volume_info_analyze_success(self): + vol_info = ('vol_name=vol1,father_name=vol1_father,' + 'status=available,vol_size=1024,real_size=1024,' + 'pool_id=pool1,create_time=01/01/2015') + vol_info_res = {'result': 0, 'vol_name': 'vol1', + 'father_name': 'vol1_father', + 'status': 'available', 'vol_size': '1024', + 'real_size': '1024', 'pool_id': 'pool1', + 'create_time': '01/01/2015'} + + retval = self.api.volume_info_analyze(vol_info) + self.assertEqual(vol_info_res, retval) + + def test_volume_info_analyze_fail(self): + vol_info = '' + vol_info_res = {'result': 1, 'vol_name': '', 'father_name': '', + 'status': '', 'vol_size': '', 'real_size': '', + 'pool_id': '', 'create_time': ''} + retval = self.api.volume_info_analyze(vol_info) + self.assertEqual(vol_info_res, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + @mock.patch.object(fspythonapi.FSPythonApi, 'volume_info_analyze') + @mock.patch.object(fspythonapi.FSPythonApi, 'delete_snapshot') + def test_query_volume(self, mock_delete, mock_analyze, mock_execute): + exec_result = ['result=0\n', + 'vol_name=vol1,father_name=vol1_father,status=0,' + + 'vol_size=1024,real_size=1024,pool_id=pool1,' + + 'create_time=01/01/2015'] + query_result = {'result': 0, 'vol_name': 'vol1', + 'father_name': 'vol1_father', 'status': '0', + 'vol_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'create_time': '01/01/2015'} + mock_delete.return_value = 0 + mock_execute.return_value = exec_result + mock_analyze.return_value = query_result + retval = self.api.query_volume('vol1') + self.assertEqual(query_result, retval) + + exec_result = ['result=0\n', + 'vol_name=vol1,father_name=vol1_father,status=1,' + + 'vol_size=1024,real_size=1024,pool_id=pool1,' + + 'create_time=01/01/2015'] + query_result = {'result': 0, 'vol_name': 'vol1', + 'father_name': 'vol1_father', 'status': '1', + 'vol_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'create_time': '01/01/2015'} + mock_delete.return_value = 0 + mock_execute.return_value = exec_result + mock_analyze.return_value = query_result + retval = self.api.query_volume('vol1') + self.assertEqual(query_result, retval) + + vol_info_failure = 'result=32500000\n' + failure_res = {'result': 1, 'vol_name': '', 'father_name': '', + 'status': '', 'vol_size': '', 'real_size': '', + 'pool_id': '', 'create_time': ''} + mock_execute.return_value = vol_info_failure + retval = self.api.query_volume('vol1') + self.assertEqual(failure_res, retval) + + vol_info_failure = None + failure_res = {'result': 1, 'vol_name': '', 'father_name': '', + 'status': '', 'vol_size': '', 'real_size': '', + 'pool_id': '', 'create_time': ''} + + mock_execute.return_value = vol_info_failure + retval = self.api.query_volume('vol1') + self.assertEqual(failure_res, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_delete_volume(self, mock_execute): + mock_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.delete_volume('volume_name') + self.assertEqual(0, retval) + + retval = self.api.delete_volume('volume_name') + self.assertEqual('50150007\n', retval) + + retval = self.api.delete_volume('volume_name') + self.assertEqual(1, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_create_snapshot(self, mock_execute): + mock_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.create_snapshot('snap_name', 'vol_name', 0) + self.assertEqual(0, retval) + + retval = self.api.create_snapshot('snap_name', 'vol_name', 0) + self.assertEqual('50150007\n', retval) + + retval = self.api.create_snapshot('snap_name', 'vol_name', 0) + self.assertEqual(1, retval) + + def test_snap_info_analyze_success(self): + snap_info = ('snap_name=snap1,father_name=snap1_father,status=0,' + 'snap_size=1024,real_size=1024,pool_id=pool1,' + 'delete_priority=1,create_time=01/01/2015') + snap_info_res = {'result': 0, 'snap_name': 'snap1', + 'father_name': 'snap1_father', 'status': '0', + 'snap_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'delete_priority': '1', + 'create_time': '01/01/2015'} + + retval = self.api.snap_info_analyze(snap_info) + self.assertEqual(snap_info_res, retval) + + def test_snap_info_analyze_fail(self): + snap_info = '' + snap_info_res = {'result': 1, 'snap_name': '', 'father_name': '', + 'status': '', 'snap_size': '', 'real_size': '', + 'pool_id': '', 'delete_priority': '', + 'create_time': ''} + retval = self.api.snap_info_analyze(snap_info) + self.assertEqual(snap_info_res, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_query_snap(self, mock_execute): + exec_result = ['result=0\n', + 'snap_name=snap1,father_name=snap1_father,status=0,' + + 'snap_size=1024,real_size=1024,pool_id=pool1,' + + 'delete_priority=1,create_time=01/01/2015'] + query_result = {'result': 0, 'snap_name': 'snap1', + 'father_name': 'snap1_father', 'status': '0', + 'snap_size': '1024', 'real_size': '1024', + 'pool_id': 'pool1', 'delete_priority': '1', + 'create_time': '01/01/2015'} + mock_execute.return_value = exec_result + retval = self.api.query_snap('snap1') + self.assertEqual(query_result, retval) + + exec_result = ['result=50150007\n'] + qurey_result = {'result': '50150007\n', 'snap_name': '', + 'father_name': '', 'status': '', 'snap_size': '', + 'real_size': '', 'pool_id': '', + 'delete_priority': '', 'create_time': ''} + mock_execute.return_value = exec_result + retval = self.api.query_snap('snap1') + self.assertEqual(qurey_result, retval) + + exec_result = '' + query_result = {'result': 1, 'snap_name': '', 'father_name': '', + 'status': '', 'snap_size': '', 'real_size': '', + 'pool_id': '', 'delete_priority': '', + 'create_time': ''} + mock_execute.return_value = exec_result + retval = self.api.query_snap('snap1') + self.assertEqual(query_result, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_delete_snapshot(self, mock_execute): + mock_execute.side_effect = ['result=0\n', + 'result=50150007\n', None] + + retval = self.api.delete_snapshot('snap_name') + self.assertEqual(0, retval) + + retval = self.api.delete_snapshot('snap_name') + self.assertEqual('50150007\n', retval) + + retval = self.api.delete_snapshot('snap_name') + self.assertEqual(1, retval) + + def test_pool_info_analyze(self): + pool_info = 'pool_id=pool100,total_capacity=1024,' + \ + 'used_capacity=500,alloc_capacity=500' + analyze_res = {'result': 0, 'pool_id': 'pool100', + 'total_capacity': '1024', 'used_capacity': '500', + 'alloc_capacity': '500'} + + retval = self.api.pool_info_analyze(pool_info) + self.assertEqual(analyze_res, retval) + + pool_info = '' + analyze_res = {'result': 1, 'pool_id': '', 'total_capacity': '', + 'used_capacity': '', 'alloc_capacity': ''} + retval = self.api.pool_info_analyze(pool_info) + self.assertEqual(analyze_res, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_query_pool_info(self, mock_execute): + exec_result = ['result=0\n', + 'pool_id=0,total_capacity=1024,' + + 'used_capacity=500,alloc_capacity=500\n'] + query_result = {'result': 0, 'pool_id': '0', + 'total_capacity': '1024', 'used_capacity': '500', + 'alloc_capacity': '500'} + mock_execute.return_value = exec_result + retval = self.api.query_pool_info('0') + self.assertEqual(query_result, retval) + + exec_result = ['result=51050008\n'] + query_result = {'result': '51050008\n', 'pool_id': '', + 'total_capacity': '', 'used_capacity': '', + 'alloc_capacity': ''} + mock_execute.return_value = exec_result + retval = self.api.query_pool_info('0') + self.assertEqual(query_result, retval) + + exec_result = '' + query_result = {'result': 1, 'pool_id': '', 'total_capacity': '', + 'used_capacity': '', 'alloc_capacity': ''} + mock_execute.return_value = exec_result + retval = self.api.query_pool_info('0') + self.assertEqual(query_result, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_query_pool_type(self, mock_execute): + exec_result = ['result=0\n', + 'pool_id=0,total_capacity=1024,' + + 'used_capacity=500,alloc_capacity=500\n'] + query_result = (0, [{'result': 0, + 'pool_id': '0', 'total_capacity': '1024', + 'used_capacity': '500', 'alloc_capacity': '500'}]) + + mock_execute.return_value = exec_result + retval = self.api.query_pool_type('sata2copy') + self.assertEqual(query_result, retval) + + exec_result = ['result=0\n', + 'pool_id=0,total_capacity=1024,' + + 'used_capacity=500,alloc_capacity=500\n', + 'pool_id=1,total_capacity=2048,' + + 'used_capacity=500,alloc_capacity=500\n'] + query_result = (0, [{'result': 0, 'pool_id': '0', + 'total_capacity': '1024', 'used_capacity': '500', + 'alloc_capacity': '500'}, + {'result': 0, 'pool_id': '1', + 'total_capacity': '2048', 'used_capacity': '500', + 'alloc_capacity': '500'}]) + mock_execute.return_value = exec_result + retval = self.api.query_pool_type('sata2copy') + self.assertEqual(query_result, retval) + + exec_result = ['result=51010015\n'] + query_result = (51010015, []) + mock_execute.return_value = exec_result + retval = self.api.query_pool_type('sata2copy') + self.assertEqual(query_result, retval) + + exec_result = '' + query_result = (0, []) + mock_execute.return_value = exec_result + retval = self.api.query_pool_type('sata2copy') + self.assertEqual(query_result, retval) + + @mock.patch.object(fspythonapi.FSPythonApi, 'start_execute_cmd') + def test_query_dsware_version(self, mock_execute): + mock_execute.side_effect = ['result=0\n', 'result=50500001\n', + 'result=50150007\n', None] + + retval = self.api.query_dsware_version() + self.assertEqual(0, retval) + + retval = self.api.query_dsware_version() + self.assertEqual(1, retval) + + retval = self.api.query_dsware_version() + self.assertEqual('50150007\n', retval) + + retval = self.api.query_dsware_version() + self.assertEqual(2, retval) diff --git a/cinder/volume/drivers/fusionstorage/__init__.py b/cinder/volume/drivers/fusionstorage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/drivers/fusionstorage/dsware.py b/cinder/volume/drivers/fusionstorage/dsware.py new file mode 100644 index 000000000..b7fcc5bec --- /dev/null +++ b/cinder/volume/drivers/fusionstorage/dsware.py @@ -0,0 +1,624 @@ +# Copyright (c) 2013 - 2016 Huawei Technologies Co., Ltd. +# 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. +""" +Driver for Huawei FusionStorage. +""" + +import os +import re + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import loopingcall + +from cinder import exception +from cinder.i18n import _, _LE, _LW +from cinder.image import image_utils +from cinder import interface +from cinder.volume import driver +from cinder.volume.drivers.fusionstorage import fspythonapi + +LOG = logging.getLogger(__name__) + +volume_opts = [ + cfg.BoolOpt('dsware_isthin', + default = False, + help = 'The flag of thin storage allocation.'), + cfg.StrOpt('dsware_manager', + default = '', + help = 'Fusionstorage manager ip addr for cinder-volume.'), + cfg.StrOpt('fusionstorageagent', + default = '', + help = 'Fusionstorage agent ip addr range.'), + cfg.StrOpt('pool_type', + default = 'default', + help = 'Pool type, like sata-2copy.'), + cfg.ListOpt('pool_id_filter', + default = [], + help = 'Pool id permit to use.'), + cfg.IntOpt('clone_volume_timeout', + default = 680, + help = 'Create clone volume timeout.'), +] + +CONF = cfg.CONF +CONF.register_opts(volume_opts) + +OLD_VERSION = 1 +NEW_VERSION = 0 +VOLUME_ALREADY_ATTACHED = 50151401 +VOLUME_NOT_EXIST = '50150005\n' +VOLUME_BEING_DELETED = '50151002\n' +SNAP_NOT_EXIST = '50150006\n' + + +@interface.volumedriver +class DSWAREDriver(driver.VolumeDriver): + """Huawei FusionStorage Driver.""" + VERSION = '1.0' + + DSWARE_VOLUME_CREATE_SUCCESS_STATUS = 0 + DSWARE_VOLUME_DUPLICATE_VOLUME = 6 + DSWARE_VOLUME_CREATING_STATUS = 7 + + def __init__(self, *args, **kwargs): + super(DSWAREDriver, self).__init__(*args, **kwargs) + self.dsware_client = fspythonapi.FSPythonApi() + self.check_cloned_interval = 2 + self.configuration.append_config_values(volume_opts) + + def check_for_setup_error(self): + # lrk: check config file here. + if not os.path.exists(fspythonapi.fsc_conf_file): + msg = _("Dsware config file not exists!") + LOG.error(_LE("Dsware config file: %s not exists!"), + fspythonapi.fsc_conf_file) + raise exception.VolumeBackendAPIException(data=msg) + + def do_setup(self, context): + # lrk: create fsc_conf_file here. + conf_info = ["manage_ip=%s" % self.configuration.dsware_manager, + "\n", + "vbs_url=%s" % self.configuration.fusionstorageagent] + + fsc_dir = os.path.dirname(fspythonapi.fsc_conf_file) + if not os.path.exists(fsc_dir): + os.makedirs(fsc_dir) + + with open(fspythonapi.fsc_conf_file, 'w') as f: + f.writelines(conf_info) + + # Get pool type. + self.pool_type = self.configuration.pool_type + LOG.debug("Dsware Driver do_setup finish.") + + def _get_dsware_manage_ip(self, volume): + dsw_manager_ip = volume['provider_id'] + if dsw_manager_ip is not None: + return dsw_manager_ip + else: + msg = _("Dsware get manager ip failed, " + "volume provider_id is None!") + raise exception.VolumeBackendAPIException(data=msg) + + def _get_poolid_from_host(self, host): + # Host format: 'hostid@backend#poolid'. + # Other formats: return 'default', and the pool id would be zero. + if host: + if len(host.split('#', 1)) == 2: + return host.split('#')[1] + return self.pool_type + + def _create_volume(self, volume_id, volume_size, isThin, volume_host): + pool_id = 0 + result = 1 + + # Query Dsware version. + retcode = self.dsware_client.query_dsware_version() + # Old version. + if retcode == OLD_VERSION: + pool_id = 0 + # New version. + elif retcode == NEW_VERSION: + pool_info = self._get_poolid_from_host(volume_host) + if pool_info != self.pool_type: + pool_id = int(pool_info) + # Query Dsware version failed! + else: + LOG.error(_LE("Query Dsware version fail!")) + msg = (_("Query Dsware version failed! Retcode is %s.") % + retcode) + raise exception.VolumeBackendAPIException(data=msg) + + try: + result = self.dsware_client.create_volume( + volume_id, pool_id, volume_size, int(isThin)) + except Exception as e: + LOG.exception(_LE("Create volume error, details is: %s."), e) + raise + + if result != 0: + msg = _("Dsware create volume failed! Result is: %s.") % result + raise exception.VolumeBackendAPIException(data=msg) + + def create_volume(self, volume): + # Creates a volume in Dsware. + LOG.debug("Begin to create volume %s in Dsware.", volume['name']) + volume_id = volume['name'] + volume_size = volume['size'] + volume_host = volume['host'] + is_thin = self.configuration.dsware_isthin + # Change GB to MB. + volume_size = volume_size * 1024 + self._create_volume(volume_id, volume_size, is_thin, volume_host) + + dsw_manager_ip = self.dsware_client.get_manage_ip() + return {"provider_id": dsw_manager_ip} + + def _create_volume_from_snap(self, volume_id, volume_size, snapshot_name): + result = self.dsware_client.create_volume_from_snap( + volume_id, volume_size, snapshot_name) + if result != 0: + msg = (_("Dsware: create volume from snap failed. Result: %s.") % + result) + raise exception.VolumeBackendAPIException(data=msg) + + def create_volume_from_snapshot(self, volume, snapshot): + # Creates a volume from snapshot. + volume_id = volume['name'] + volume_size = volume['size'] + snapshot_name = snapshot['name'] + if volume_size < int(snapshot['volume_size']): + msg = _("Dsware: volume size can not be less than snapshot size.") + raise exception.VolumeBackendAPIException(data=msg) + + volume_size = volume_size * 1024 + self._create_volume_from_snap(volume_id, volume_size, snapshot_name) + + dsw_manager_ip = self.dsware_client.get_manage_ip() + return {"provider_id": dsw_manager_ip} + + def create_cloned_volume(self, volume, src_volume): + """Dispatcher to Dsware client to create volume from volume. + + Wait volume create finished. + """ + volume_name = volume['name'] + volume_size = volume['size'] + src_volume_name = src_volume['name'] + + volume_size = volume_size * 1024 + result = self.dsware_client.create_volume_from_volume( + volume_name, volume_size, src_volume_name) + if result: + msg = _('Dsware fails to start cloning volume %s.') % volume_name + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug('Dsware create volume %(volume_name)s of size ' + '%(volume_size)s from src volume %(src_volume_name)s start.', + {"volume_name": volume_name, + "volume_size": volume_size, + "src_volume_name": src_volume_name}) + + ret = self._wait_for_create_cloned_volume_finish_timer(volume_name) + if not ret: + msg = (_('Clone volume %s failed while waiting for success.') % + volume_name) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug('Dsware create volume from volume ends.') + + dsw_manager_ip = self.dsware_client.get_manage_ip() + return {"provider_id": dsw_manager_ip} + + def _check_create_cloned_volume_finish(self, new_volume_name): + LOG.debug('Loopcall: _check_create_cloned_volume_finish(), ' + 'volume-name: %s.', new_volume_name) + current_volume = self.dsware_client.query_volume(new_volume_name) + + if current_volume: + status = current_volume['status'] + LOG.debug('Wait clone volume %(volume_name)s, status: %(status)s.', + {"volume_name": new_volume_name, + "status": status}) + if int(status) == self.DSWARE_VOLUME_CREATING_STATUS or int( + status) == self.DSWARE_VOLUME_DUPLICATE_VOLUME: + self.count += 1 + elif int(status) == self.DSWARE_VOLUME_CREATE_SUCCESS_STATUS: + raise loopingcall.LoopingCallDone(retvalue=True) + else: + msg = _('Clone volume %(new_volume_name)s failed, ' + 'volume status is: %(status)s.') + LOG.error(msg, {'new_volume_name': new_volume_name, + 'status': status}) + raise loopingcall.LoopingCallDone(retvalue=False) + if self.count > self.configuration.clone_volume_timeout: + msg = _('Dsware clone volume time out. ' + 'Volume: %(new_volume_name)s, status: %(status)s') + LOG.error(msg, {'new_volume_name': new_volume_name, + 'status': current_volume['status']}) + raise loopingcall.LoopingCallDone(retvalue=False) + else: + LOG.warning(_LW('Can not find volume %s from Dsware.'), + new_volume_name) + self.count += 1 + if self.count > 10: + msg = _("Dsware clone volume failed: volume " + "can not be found from Dsware.") + LOG.error(msg) + raise loopingcall.LoopingCallDone(retvalue=False) + + def _wait_for_create_cloned_volume_finish_timer(self, new_volume_name): + timer = loopingcall.FixedIntervalLoopingCall( + self._check_create_cloned_volume_finish, new_volume_name) + LOG.debug('Call _check_create_cloned_volume_finish: volume-name %s.', + new_volume_name) + self.count = 0 + ret = timer.start(interval=self.check_cloned_interval).wait() + timer.stop() + return ret + + def _analyse_output(self, out): + if out is not None: + analyse_result = {} + out_temp = out.split('\n') + for line in out_temp: + if re.search('^ret_code=', line): + analyse_result['ret_code'] = line[9:] + elif re.search('^ret_desc=', line): + analyse_result['ret_desc'] = line[9:] + elif re.search('^dev_addr=', line): + analyse_result['dev_addr'] = line[9:] + return analyse_result + else: + return None + + def _attach_volume(self, volume_name, dsw_manager_ip): + cmd = ['vbs_cli', '-c', 'attachwithip', '-v', volume_name, '-i', + dsw_manager_ip.replace('\n', ''), '-p', 0] + out, err = self._execute(*cmd, run_as_root=True) + analyse_result = self._analyse_output(out) + LOG.debug("Attach volume result is %s.", analyse_result) + return analyse_result + + def _detach_volume(self, volume_name, dsw_manager_ip): + cmd = ['vbs_cli', '-c', 'detachwithip', '-v', volume_name, '-i', + dsw_manager_ip.replace('\n', ''), '-p', 0] + out, err = self._execute(*cmd, run_as_root=True) + analyse_result = self._analyse_output(out) + LOG.debug("Detach volume result is %s.", analyse_result) + return analyse_result + + def _query_volume_attach(self, volume_name, dsw_manager_ip): + cmd = ['vbs_cli', '-c', 'querydevwithip', '-v', volume_name, '-i', + dsw_manager_ip.replace('\n', ''), '-p', 0] + out, err = self._execute(*cmd, run_as_root=True) + analyse_result = self._analyse_output(out) + LOG.debug("Query volume attach result is %s.", analyse_result) + return analyse_result + + def copy_image_to_volume(self, context, volume, image_service, image_id): + # Copy image to volume. + # Step1: attach volume to host. + LOG.debug("Begin to copy image to volume.") + dsw_manager_ip = self._get_dsware_manage_ip(volume) + volume_attach_result = self._attach_volume(volume['name'], + dsw_manager_ip) + volume_attach_path = '' + if volume_attach_result is not None and int( + volume_attach_result['ret_code']) == 0: + volume_attach_path = volume_attach_result['dev_addr'] + LOG.debug("Volume attach path is %s.", volume_attach_path) + if volume_attach_path == '': + msg = _("Host attach volume failed!") + raise exception.VolumeBackendAPIException(data=msg) + # Step2: fetch the image from image_service and write it to the + # volume. + try: + image_utils.fetch_to_raw(context, + image_service, + image_id, + volume_attach_path, + self.configuration.volume_dd_blocksize) + finally: + # Step3: detach volume from host. + dsw_manager_ip = self._get_dsware_manage_ip(volume) + volume_detach_result = self._detach_volume(volume['name'], + dsw_manager_ip) + if volume_detach_result is not None and int( + volume_detach_result['ret_code']) != 0: + msg = (_("Dsware detach volume from host failed: %s!") % + volume_detach_result) + raise exception.VolumeBackendAPIException(data=msg) + + def copy_volume_to_image(self, context, volume, image_service, image_meta): + # Copy volume to image. + # If volume was not attached, then attach it. + + dsw_manager_ip = self._get_dsware_manage_ip(volume) + + already_attached = False + _attach_result = self._attach_volume(volume['name'], dsw_manager_ip) + if _attach_result: + retcode = _attach_result['ret_code'] + if int(retcode) == VOLUME_ALREADY_ATTACHED: + already_attached = True + result = self._query_volume_attach(volume['name'], + dsw_manager_ip) + if not result or int(result['ret_code']) != 0: + msg = (_("Query volume attach failed, result=%s.") % + result) + raise exception.VolumeBackendAPIException(data=msg) + + elif int(retcode) == 0: + result = _attach_result + else: + msg = (_("Attach volume to host failed " + "in copy volume to image, retcode: %s.") % + retcode) + raise exception.VolumeBackendAPIException(data=msg) + + volume_attach_path = result['dev_addr'] + + else: + msg = _("Attach_volume failed.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + try: + image_utils.upload_volume(context, + image_service, + image_meta, + volume_attach_path) + except Exception as e: + LOG.error(_LE("Upload volume error, details: %s."), e) + raise e + finally: + if not already_attached: + self._detach_volume(volume['name'], dsw_manager_ip) + + def _get_volume(self, volume_name): + result = self.dsware_client.query_volume(volume_name) + LOG.debug("Dsware query volume result is %s.", result['result']) + if result['result'] == VOLUME_NOT_EXIST: + LOG.debug("Dsware volume %s does not exist.", volume_name) + return False + elif result['result'] == 0: + return True + else: + msg = _("Dsware query volume %s failed!") % volume_name + raise exception.VolumeBackendAPIException(data=msg) + + def _delete_volume(self, volume_name): + # Delete volume in Dsware. + result = self.dsware_client.delete_volume(volume_name) + LOG.debug("Dsware delete volume, result is %s.", result) + if result == VOLUME_NOT_EXIST: + LOG.debug("Dsware delete volume, volume does not exist.") + return True + elif result == VOLUME_BEING_DELETED: + LOG.debug("Dsware delete volume, volume is being deleted.") + return True + elif result == 0: + return True + else: + msg = _("Dsware delete volume failed: %s!") % result + raise exception.VolumeBackendAPIException(data=msg) + + def delete_volume(self, volume): + # Delete volume. + # If volume does not exist, then return. + LOG.debug("Begin to delete volume in Dsware: %s.", volume['name']) + if not self._get_volume(volume['name']): + return True + + return self._delete_volume(volume['name']) + + def _get_snapshot(self, snapshot_name): + snapshot_info = self.dsware_client.query_snap(snapshot_name) + LOG.debug("Get snapshot, snapshot_info is : %s.", snapshot_info) + if snapshot_info['result'] == SNAP_NOT_EXIST: + LOG.error(_LE('Snapshot: %s not found!'), snapshot_name) + return False + elif snapshot_info['result'] == 0: + return True + else: + msg = _("Dsware get snapshot failed!") + raise exception.VolumeBackendAPIException(data=msg) + + def _create_snapshot(self, snapshot_id, volume_id): + LOG.debug("Create snapshot %s to Dsware.", snapshot_id) + smart_flag = 0 + res = self.dsware_client.create_snapshot(snapshot_id, + volume_id, + smart_flag) + if res != 0: + msg = _("Dsware Create Snapshot failed! Result: %s.") % res + raise exception.VolumeBackendAPIException(data=msg) + + def _delete_snapshot(self, snapshot_id): + LOG.debug("Delete snapshot %s to Dsware.", snapshot_id) + res = self.dsware_client.delete_snapshot(snapshot_id) + LOG.debug("Ddelete snapshot result is: %s.", res) + if res != 0: + raise exception.SnapshotIsBusy(snapshot_name=snapshot_id) + + def create_snapshot(self, snapshot): + vol_id = 'volume-%s' % snapshot['volume_id'] + snapshot_id = snapshot['name'] + if not self._get_volume(vol_id): + msg = _LE('Create Snapshot, but volume: %s not found!') + LOG.error(msg, vol_id) + raise exception.VolumeNotFound(volume_id=vol_id) + else: + self._create_snapshot(snapshot_id, vol_id) + + def delete_snapshot(self, snapshot): + LOG.debug("Delete snapshot %s.", snapshot['name']) + snapshot_id = snapshot['name'] + if self._get_snapshot(snapshot_id): + self._delete_snapshot(snapshot_id) + + def _calculate_pool_info(self, pool_sets): + filter = False + pools_status = [] + reserved_percentage = self.configuration.reserved_percentage + pool_id_filter = self.configuration.pool_id_filter + LOG.debug("Filtered pool id is %s.", pool_id_filter) + if pool_id_filter == []: + for pool_info in pool_sets: + pool = {} + pool['pool_name'] = pool_info['pool_id'] + pool['total_capacity_gb'] = float( + pool_info['total_capacity']) / 1024 + pool['allocated_capacity_gb'] = float( + pool_info['used_capacity']) / 1024 + pool['free_capacity_gb'] = pool['total_capacity_gb'] - pool[ + 'allocated_capacity_gb'] + pool['QoS_support'] = False + pool['reserved_percentage'] = reserved_percentage + pools_status.append(pool) + else: + for pool_info in pool_sets: + for pool_id in pool_id_filter: + if pool_id == pool_info['pool_id']: + filter = True + break + + if filter: + pool = {} + pool['pool_name'] = pool_info['pool_id'] + pool['total_capacity_gb'] = float( + pool_info['total_capacity']) / 1024 + pool['allocated_capacity_gb'] = float( + pool_info['used_capacity']) / 1024 + pool['free_capacity_gb'] = float( + pool['total_capacity_gb'] - pool[ + 'allocated_capacity_gb']) + pool['QoS_support'] = False + pool['reserved_percentage'] = reserved_percentage + pools_status.append(pool) + + filter = False + + return pools_status + + def _update_single_pool_info_status(self): + """Query pool info when Dsware is single-pool version.""" + status = {} + status['volume_backend_name'] = self.configuration.volume_backend_name + status['vendor_name'] = 'Open Source' + status['driver_version'] = self.VERSION + status['storage_protocol'] = 'dsware' + + status['total_capacity_gb'] = 0 + status['free_capacity_gb'] = 0 + status['reserved_percentage'] = self.configuration.reserved_percentage + status['QoS_support'] = False + pool_id = 0 + pool_info = self.dsware_client.query_pool_info(pool_id) + result = pool_info['result'] + if result == 0: + status['total_capacity_gb'] = float( + pool_info['total_capacity']) / 1024 + status['free_capacity_gb'] = (float( + pool_info['total_capacity']) - float( + pool_info['used_capacity'])) / 1024 + LOG.debug("total_capacity_gb is %s, free_capacity_gb is %s.", + status['total_capacity_gb'], + status['free_capacity_gb']) + self._stats = status + else: + self._stats = None + + def _update_multi_pool_of_same_type_status(self): + """Query info of multiple pools when Dsware is multi-pool version. + + These pools have the same pool type. + """ + status = {} + status['volume_backend_name'] = self.configuration.volume_backend_name + status['vendor_name'] = 'Open Source' + status['driver_version'] = self.VERSION + status['storage_protocol'] = 'dsware' + + (result, pool_sets) = self.dsware_client.query_pool_type( + self.pool_type) + if pool_sets == []: + self._stats = None + else: + pools_status = self._calculate_pool_info(pool_sets) + status['pools'] = pools_status + self._stats = status + + def get_volume_stats(self, refresh=False): + if refresh: + dsware_version = self.dsware_client.query_dsware_version() + # Old version. + if dsware_version == OLD_VERSION: + self._update_single_pool_info_status() + # New version. + elif dsware_version == NEW_VERSION: + self._update_multi_pool_of_same_type_status() + else: + msg = _("Dsware query Dsware version failed!") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + return self._stats + + def extend_volume(self, volume, new_size): + # Extend volume in Dsware. + LOG.debug("Begin to extend volume in Dsware: %s.", volume['name']) + volume_id = volume['name'] + if volume['size'] > new_size: + msg = (_("Dsware extend Volume failed! " + "New size %(new_size)s should be greater than " + "old size %(old_size)s!") + % {'new_size': new_size, + 'old_size': volume['size']}) + raise exception.VolumeBackendAPIException(data=msg) + # Change GB to MB. + volume_size = new_size * 1024 + result = self.dsware_client.extend_volume(volume_id, volume_size) + if result != 0: + msg = _("Dsware extend Volume failed! Result:%s.") % result + raise exception.VolumeBackendAPIException(data=msg) + + def initialize_connection(self, volume, connector): + """Initializes the connection and returns connection info.""" + LOG.debug("Begin initialize connection.") + + properties = {} + properties['volume_name'] = volume['name'] + properties['volume'] = volume + properties['dsw_manager_ip'] = self._get_dsware_manage_ip(volume) + + LOG.debug("End initialize connection with properties:%s.", properties) + + return {'driver_volume_type': 'dsware', + 'data': properties} + + def terminate_connection(self, volume, connector, force=False, **kwargs): + pass + + def create_export(self, context, volume, connector): + pass + + def ensure_export(self, context, volume): + pass + + def remove_export(self, context, volume): + pass diff --git a/cinder/volume/drivers/fusionstorage/fspythonapi.py b/cinder/volume/drivers/fusionstorage/fspythonapi.py new file mode 100644 index 000000000..95739202e --- /dev/null +++ b/cinder/volume/drivers/fusionstorage/fspythonapi.py @@ -0,0 +1,499 @@ +# Copyright (c) 2013 - 2016 Huawei Technologies Co., Ltd. +# 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 api for FusionStorage systems. +""" + +import os +import re +import six + +from oslo_log import log as logging + +from cinder.i18n import _LE +from cinder import utils + +LOG = logging.getLogger(__name__) +fsc_conf_file = "/etc/cinder/volumes/fsc_conf" +fsc_cli = "fsc_cli" +fsc_ip = [] +fsc_port = '10519' +manage_ip = "127.0.0.1" +CMD_BIN = fsc_cli + +volume_info = { + 'result': '', + 'vol_name': '', + 'father_name': '', + 'status': '', + 'vol_size': '', + 'real_size': '', + 'pool_id': '', + 'create_time': ''} + + +snap_info = { + 'result': '', + 'snap_name': '', + 'father_name': '', + 'status': '', + 'snap_size': '', + 'real_size': '', + 'pool_id': '', + 'delete_priority': '', + 'create_time': ''} + + +pool_info = { + 'result': '', + 'pool_id': '', + 'total_capacity': '', + 'used_capacity': '', + 'alloc_capacity': ''} + + +class FSPythonApi(object): + + def __init__(self): + LOG.debug("FSPythonApi init.") + self.get_ip_port() + self.res_idx = len('result=') + + def get_ip_port(self): + LOG.debug("File fsc_conf_file is %s.", fsc_conf_file) + if os.path.exists(fsc_conf_file): + try: + fsc_file = open(fsc_conf_file, 'r') + full_txt = fsc_file.readlines() + LOG.debug("Full_txt is %s.", full_txt) + for line in full_txt: + if re.search('^vbs_url=', line): + tmp_vbs_url = line[8:] + return re.split(',', tmp_vbs_url) + except Exception as e: + LOG.debug("Get fsc ip failed, error=%s.", e) + finally: + fsc_file.close() + else: + LOG.debug("Fsc conf file not exist, file_name=%s.", fsc_conf_file) + + def get_manage_ip(self): + LOG.debug("File fsc_conf_file is %s.", fsc_conf_file) + if os.path.exists(fsc_conf_file): + try: + fsc_file = open(fsc_conf_file, 'r') + full_txt = fsc_file.readlines() + for line in full_txt: + if re.search('^manage_ip=', line): + manage_ip = line[len('manage_ip='):] + manage_ip = manage_ip.strip('\n') + return manage_ip + except Exception as e: + LOG.debug("Get manage ip failed, error=%s.", e) + finally: + fsc_file.close() + else: + LOG.debug("Fsc conf file not exist, file_name=%s.", fsc_conf_file) + + def get_dsw_manage_ip(self): + return manage_ip + + def start_execute_cmd(self, cmd, full_result_flag): + fsc_ip = self.get_ip_port() + manage_ip = self.get_manage_ip() + ip_num = len(fsc_ip) + + LOG.debug("fsc_ip is %s", fsc_ip) + + if ip_num <= 0: + return None + + if ip_num > 3: + ip_num = 3 + + exec_result = '' + result = '' + if full_result_flag: + for ip in fsc_ip: + cmd_args = [CMD_BIN, '--manage_ip', manage_ip.replace( + '\n', ''), '--ip', ip.replace('\n', '')] + cmd.split() + LOG.debug("Dsware cmd_args is %s.", cmd_args) + + exec_result, err = utils.execute(*cmd_args, run_as_root=True) + exec_result = exec_result.split('\n') + LOG.debug("Result is %s.", exec_result) + if exec_result: + for line in exec_result: + if re.search('^result=0', line): + return exec_result + elif re.search('^result=50150007', line): + return 'result=0' + elif re.search('^result=50150008', line): + return 'result=0' + elif re.search('^result=50', line): + return exec_result + return exec_result + else: + for ip in fsc_ip: + cmd_args = [CMD_BIN, '--manage_ip', manage_ip.replace( + '\n', ''), '--ip', ip.replace('\n', '')] + cmd.split() + LOG.debug("Dsware cmd_args is %s.", cmd_args) + + exec_result, err = utils.execute(*cmd_args, run_as_root=True) + LOG.debug("Result is %s.", exec_result) + exec_result = exec_result.split('\n') + if exec_result: + for line in exec_result: + if re.search('^result=', line): + result = line + if re.search('^result=0', line): + return line + elif re.search('^result=50150007', line): + return 'result=0' + elif re.search('^result=50150008', line): + return 'result=0' + elif re.search('^result=50', line): + return line + return result + + def create_volume(self, vol_name, pool_id, vol_size, thin_flag): + cmd = '--op createVolume' + ' ' + '--volName' + ' ' + six.text_type( + vol_name) + ' ' + '--poolId' + ' ' + six.text_type( + pool_id) + ' ' + '--volSize' + ' ' + six.text_type( + vol_size) + ' ' + '--thinFlag' + ' ' + six.text_type(thin_flag) + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def extend_volume(self, vol_name, new_vol_size): + cmd = '' + cmd = '--op expandVolume' + ' ' + '--volName' + ' ' + six.text_type( + vol_name) + ' ' + '--volSize' + ' ' + six.text_type(new_vol_size) + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def create_volume_from_snap(self, vol_name, vol_size, snap_name): + cmd = ('--op createVolumeFromSnap' + ' ') + ( + '--volName' + ' ') + six.text_type( + vol_name) + ' ' + '--snapNameSrc' + ' ' + six.text_type( + snap_name) + ' ' + '--volSize' + ' ' + six.text_type(vol_size) + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def create_fullvol_from_snap(self, vol_name, snap_name): + cmd = ('--op createFullVolumeFromSnap' + ' ') + ( + '--volName' + ' ') + six.text_type( + vol_name) + ' ' + '--snapName' + ' ' + six.text_type(snap_name) + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def create_volume_from_volume(self, vol_name, vol_size, src_vol_name): + retcode = 1 + tmp_snap_name = six.text_type(vol_name) + '_tmp_snap' + + retcode = self.create_snapshot(tmp_snap_name, src_vol_name, 0) + if 0 != retcode: + return retcode + + retcode = self.create_volume(vol_name, 0, vol_size, 0) + if 0 != retcode: + self.delete_snapshot(tmp_snap_name) + return retcode + + retcode = self.create_fullvol_from_snap(vol_name, tmp_snap_name) + if 0 != retcode: + self.delete_snapshot(tmp_snap_name) + self.delete_volume(vol_name) + return retcode + + return 0 + + def create_clone_volume_from_volume(self, vol_name, + vol_size, src_vol_name): + retcode = 1 + tmp_snap_name = six.text_type(src_vol_name) + '_DT_clnoe_snap' + + retcode = self.create_snapshot(tmp_snap_name, src_vol_name, 0) + if 0 != retcode: + return retcode + + retcode = self.create_volume_from_snap( + vol_name, vol_size, tmp_snap_name) + if 0 != retcode: + return retcode + + return 0 + + def volume_info_analyze(self, vol_info): + local_volume_info = volume_info + + if not vol_info: + local_volume_info['result'] = 1 + return local_volume_info + + local_volume_info['result'] = 0 + + vol_info_list = [] + vol_info_list = re.split(',', vol_info) + for line in vol_info_list: + line = line.replace('\n', '') + if re.search('^vol_name=', line): + local_volume_info['vol_name'] = line[len('vol_name='):] + elif re.search('^father_name=', line): + local_volume_info['father_name'] = line[len('father_name='):] + elif re.search('^status=', line): + local_volume_info['status'] = line[len('status='):] + elif re.search('^vol_size=', line): + local_volume_info['vol_size'] = line[len('vol_size='):] + elif re.search('^real_size=', line): + local_volume_info['real_size'] = line[len('real_size='):] + elif re.search('^pool_id=', line): + local_volume_info['pool_id'] = line[len('pool_id='):] + elif re.search('^create_time=', line): + local_volume_info['create_time'] = line[len('create_time='):] + else: + LOG.error(_LE("Analyze key not exist, key=%s."), + six.text_type(line)) + return local_volume_info + + def query_volume(self, vol_name): + tmp_volume_info = volume_info + cmd = '--op queryVolume' + ' ' + '--volName' + ' ' + vol_name + + exec_result = self.start_execute_cmd(cmd, 1) + if exec_result: + for line in exec_result: + if re.search('^result=', line): + if not re.search('^result=0', line): + tmp_volume_info['result'] = line[self.res_idx:] + return tmp_volume_info + for line in exec_result: + if re.search('^vol_name=' + vol_name, line): + tmp_volume_info = self.volume_info_analyze(line) + if six.text_type(0) == tmp_volume_info['status']: + tmp_snap_name = six.text_type( + vol_name) + '_tmp_snap' + self.delete_snapshot(tmp_snap_name) + return tmp_volume_info + + tmp_volume_info['result'] = 1 + return tmp_volume_info + + def delete_volume(self, vol_name): + cmd = '--op deleteVolume' + ' ' + '--volName' + ' ' + vol_name + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def create_snapshot(self, snap_name, vol_name, smart_flag): + cmd = '--op createSnapshot' + ' ' + '--volName' + ' ' + six.text_type( + vol_name) + ' ' + '--snapName' + ' ' + six.text_type( + snap_name) + ' ' + '--smartFlag' + ' ' + six.text_type(smart_flag) + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def snap_info_analyze(self, info): + local_snap_info = snap_info.copy() + + if not info: + local_snap_info['result'] = 1 + return local_snap_info + + local_snap_info['result'] = 0 + + snap_info_list = [] + snap_info_list = re.split(',', info) + for line in snap_info_list: + line = line.replace('\n', '') + if re.search('^snap_name=', line): + local_snap_info['snap_name'] = line[len('snap_name='):] + elif re.search('^father_name=', line): + local_snap_info['father_name'] = line[len('father_name='):] + elif re.search('^status=', line): + local_snap_info['status'] = line[len('status='):] + elif re.search('^snap_size=', line): + local_snap_info['snap_size'] = line[len('snap_size='):] + elif re.search('^real_size=', line): + local_snap_info['real_size'] = line[len('real_size='):] + elif re.search('^pool_id=', line): + local_snap_info['pool_id'] = line[len('pool_id='):] + elif re.search('^delete_priority=', line): + local_snap_info['delete_priority'] = line[ + len('delete_priority='):] + elif re.search('^create_time=', line): + local_snap_info['create_time'] = line[len('create_time='):] + else: + LOG.error(_LE("Analyze key not exist, key=%s."), + line) + + return local_snap_info + + def query_snap(self, snap_name): + tmp_snap_info = snap_info.copy() + cmd = '--op querySnapshot' + ' ' + '--snapName' + ' ' + snap_name + + exec_result = self.start_execute_cmd(cmd, 1) + if exec_result: + for line in exec_result: + if re.search('^result=', line): + if not re.search('^result=0', line): + tmp_snap_info['result'] = line[self.res_idx:] + return tmp_snap_info + for line in exec_result: + if re.search('^snap_name=' + snap_name, line): + tmp_snap_info = self.snap_info_analyze(line) + return tmp_snap_info + + tmp_snap_info['result'] = 1 + return tmp_snap_info + + def delete_snapshot(self, snap_name): + cmd = '--op deleteSnapshot' + ' ' + '--snapName' + ' ' + snap_name + + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + if re.search('^result=0', exec_result): + return 0 + else: + return exec_result[self.res_idx:] + else: + return 1 + + def pool_info_analyze(self, info): + local_pool_info = pool_info.copy() + + if not info: + local_pool_info['result'] = 1 + return local_pool_info + + local_pool_info['result'] = 0 + + pool_info_list = [] + pool_info_list = re.split(',', info) + for line in pool_info_list: + line = line.replace('\n', '') + if re.search('^pool_id=', line): + local_pool_info['pool_id'] = line[len('pool_id='):] + elif re.search('^total_capacity=', line): + local_pool_info['total_capacity'] = line[ + len('total_capacity='):] + elif re.search('^used_capacity=', line): + local_pool_info['used_capacity'] = line[len('used_capacity='):] + elif re.search('^alloc_capacity=', line): + local_pool_info['alloc_capacity'] = line[ + len('alloc_capacity='):] + else: + LOG.error(_LE("Analyze key not exist, key=%s."), + six.text_type(line)) + return local_pool_info + + def query_pool_info(self, pool_id): + tmp_pool_info = pool_info.copy() + cmd = '--op queryPoolInfo' + ' ' + '--poolId' + ' ' + six.text_type( + pool_id) + LOG.debug("Pool id is %s.", pool_id) + exec_result = self.start_execute_cmd(cmd, 1) + if exec_result: + for line in exec_result: + if re.search('^result=', line): + if not re.search('^result=0', line): + tmp_pool_info['result'] = line[self.res_idx:] + return tmp_pool_info + for line in exec_result: + if re.search('^pool_id=' + six.text_type(pool_id), + line): + tmp_pool_info = self.pool_info_analyze(line) + return tmp_pool_info + + tmp_pool_info['result'] = 1 + return tmp_pool_info + + def query_pool_type(self, pool_type): + pool_list = [] + tmp_pool_info = {} + result = 0 + cmd = '' + cmd = '--op queryPoolType --poolType' + ' ' + pool_type + LOG.debug("Query poolType: %s.", pool_type) + exec_result = self.start_execute_cmd(cmd, 1) + if exec_result: + for line in exec_result: + line = line.replace('\n', '') + if re.search('^result=', line): + if not re.search('^result=0', line): + result = int(line[self.res_idx:]) + break + for one_line in exec_result: + if re.search('^pool_id=', one_line): + tmp_pool_info = self.pool_info_analyze(one_line) + pool_list.append(tmp_pool_info) + break + return (result, pool_list) + + def query_dsware_version(self): + retcode = 2 + cmd = '--op getDSwareIdentifier' + exec_result = self.start_execute_cmd(cmd, 0) + if exec_result: + # New version. + if re.search('^result=0', exec_result): + retcode = 0 + # Old version. + elif re.search('^result=50500001', exec_result): + retcode = 1 + # Failed! + else: + retcode = exec_result[self.res_idx:] + return retcode diff --git a/releasenotes/notes/fusionstorage-cinder-driver-8f3bca98f6e2065a.yaml b/releasenotes/notes/fusionstorage-cinder-driver-8f3bca98f6e2065a.yaml new file mode 100644 index 000000000..8cc89cf7d --- /dev/null +++ b/releasenotes/notes/fusionstorage-cinder-driver-8f3bca98f6e2065a.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added backend driver for Huawei FusionStorage.