From de89f6c370ea278386842b5a866c1ca48b1e9b12 Mon Sep 17 00:00:00 2001 From: jiaohaolin Date: Fri, 20 Apr 2018 16:02:39 +0800 Subject: [PATCH] Cinder volume driver for Inspur AS13000 series Features that Inspur AS13000 Driver support: Create, list, delete, attach (map), and detach (unmap) volumes Create, list, and delete volume snapshots Copy an image to a volume Copy a volume to an image Clone a volume Extend a volume Create a volume from a snapshot ThirdPartySystems: INSPUR CI Change-Id: Ib18ffb38f87747805a3aaf0c3837d5b8bb71b101 Implements: Blueprint inspur-as13000-driver --- cinder/opts.py | 4 + .../volume/drivers/inspur/as13000/__init__.py | 0 .../inspur/as13000/test_as13000_driver.py | 1347 +++++++++++++++++ .../volume/drivers/inspur/as13000/__init__.py | 0 .../drivers/inspur/as13000/as13000_driver.py | 873 +++++++++++ .../drivers/inspur-as13000-driver.rst | 78 + .../block-storage/volume-drivers.rst | 1 + doc/source/reference/support-matrix.ini | 12 + ...s13000-cinder-driver-bfa5cc17683d87a9.yaml | 4 + 9 files changed, 2319 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/inspur/as13000/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/as13000/test_as13000_driver.py create mode 100644 cinder/volume/drivers/inspur/as13000/__init__.py create mode 100644 cinder/volume/drivers/inspur/as13000/as13000_driver.py create mode 100644 doc/source/configuration/block-storage/drivers/inspur-as13000-driver.rst create mode 100644 releasenotes/notes/inspur-as13000-cinder-driver-bfa5cc17683d87a9.yaml diff --git a/cinder/opts.py b/cinder/opts.py index cf25d618c84..1cccb508e1a 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -119,6 +119,8 @@ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc as \ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi as \ cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi from cinder.volume.drivers import infinidat as cinder_volume_drivers_infinidat +from cinder.volume.drivers.inspur.as13000 import as13000_driver as \ + cinder_volume_drivers_inspur_as13000_as13000driver from cinder.volume.drivers.inspur.instorage import instorage_common as \ cinder_volume_drivers_inspur_instorage_instoragecommon from cinder.volume.drivers.inspur.instorage import instorage_iscsi as \ @@ -247,6 +249,8 @@ def list_opts(): cinder_volume_driver.nvmet_opts, cinder_volume_drivers_datacore_driver.datacore_opts, cinder_volume_drivers_datacore_iscsi.datacore_iscsi_opts, + cinder_volume_drivers_inspur_as13000_as13000driver. + inspur_as13000_opts, cinder_volume_drivers_inspur_instorage_instoragecommon. instorage_mcs_opts, cinder_volume_drivers_inspur_instorage_instorageiscsi. diff --git a/cinder/tests/unit/volume/drivers/inspur/as13000/__init__.py b/cinder/tests/unit/volume/drivers/inspur/as13000/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/inspur/as13000/test_as13000_driver.py b/cinder/tests/unit/volume/drivers/inspur/as13000/test_as13000_driver.py new file mode 100644 index 00000000000..01b6cf1da4f --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/as13000/test_as13000_driver.py @@ -0,0 +1,1347 @@ +# Copyright 2018 Inspur Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Volume driver test for Inspur AS13000 +""" + +import json +import mock +import random +import time + +import ddt +import eventlet +from oslo_config import cfg +import requests + +from cinder import context +from cinder import exception +from cinder import test +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.volume import configuration +from cinder.volume.drivers.inspur.as13000 import as13000_driver +from cinder.volume import utils as volume_utils + + +CONF = cfg.CONF + +test_config = configuration.Configuration(None) +test_config.san_ip = 'some_ip' +test_config.san_api_port = 'as13000_api_port' +test_config.san_login = 'username' +test_config.san_password = 'password' +test_config.as13000_ipsan_pools = ['fakepool'] +test_config.as13000_meta_pool = 'meta_pool' +test_config.use_chap_auth = True +test_config.chap_username = 'fakeuser' +test_config.chap_password = 'fakepass' + + +class FakeResponse(object): + def __init__(self, status, output): + self.status_code = status + self.text = 'return message' + self._json = output + + def json(self): + return self._json + + def close(self): + pass + + +@ddt.ddt +class RestAPIExecutorTestCase(test.TestCase): + def setUp(self): + self.rest_api = as13000_driver.RestAPIExecutor( + test_config.san_ip, + test_config.san_api_port, + test_config.san_login, + test_config.san_password) + super(RestAPIExecutorTestCase, self).setUp() + + def test_login(self): + mock__login = self.mock_object(self.rest_api, '_login', + mock.Mock(return_value='fake_token')) + self.rest_api.login() + mock__login.assert_called_once() + self.assertEqual('fake_token', self.rest_api._token) + + def test__login(self): + response = {'token': 'fake_token', 'expireTime': '7200', 'type': 0} + mock_sra = self.mock_object(self.rest_api, 'send_rest_api', + mock.Mock(return_value=response)) + + result = self.rest_api._login() + + self.assertEqual('fake_token', result) + + login_params = {'name': test_config.san_login, + 'password': test_config.san_password} + mock_sra.assert_called_once_with(method='security/token', + params=login_params, + request_type='post') + + def test_send_rest_api(self): + expected = {'value': 'abc'} + mock_sa = self.mock_object(self.rest_api, 'send_api', + mock.Mock(return_value=expected)) + result = self.rest_api.send_rest_api( + method='fake_method', + params='fake_params', + request_type='fake_type') + self.assertEqual(expected, result) + mock_sa.assert_called_once_with( + 'fake_method', + 'fake_params', + 'fake_type') + + def test_send_rest_api_retry(self): + expected = {'value': 'abc'} + mock_sa = self.mock_object( + self.rest_api, + 'send_api', + mock.Mock(side_effect=(exception.VolumeDriverException, expected))) + mock_login = self.mock_object(self.rest_api, 'login', mock.Mock()) + result = self.rest_api.send_rest_api( + method='fake_method', + params='fake_params', + request_type='fake_type' + ) + self.assertEqual(expected, result) + + mock_sa.assert_called_with( + 'fake_method', + 'fake_params', + 'fake_type') + mock_login.assert_called_once() + + def test_send_rest_api_3times_fail(self): + mock_sa = self.mock_object( + self.rest_api, 'send_api', mock.Mock( + side_effect=(exception.VolumeDriverException))) + mock_login = self.mock_object(self.rest_api, 'login', mock.Mock()) + self.assertRaises( + exception.VolumeDriverException, + self.rest_api.send_rest_api, + method='fake_method', + params='fake_params', + request_type='fake_type') + mock_sa.assert_called_with('fake_method', + 'fake_params', + 'fake_type') + mock_login.assert_called() + + def test_send_rest_api_backend_error_fail(self): + side_effect = exception.VolumeBackendAPIException('fake_err_msg') + mock_sa = self.mock_object(self.rest_api, + 'send_api', + mock.Mock(side_effect=side_effect)) + mock_login = self.mock_object(self.rest_api, 'login') + + self.assertRaises(exception.VolumeBackendAPIException, + self.rest_api.send_rest_api, + method='fake_method', + params='fake_params', + request_type='fake_type') + mock_sa.assert_called_with('fake_method', + 'fake_params', + 'fake_type') + mock_login.assert_not_called() + + @ddt.data( + {'method': 'fake_method', 'request_type': 'post', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'get', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'delete', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'put', 'params': + {'fake_param': 'fake_value'}}, ) + @ddt.unpack + def test_send_api(self, method, params, request_type): + self.rest_api._token = 'fake_token' + if request_type in ('post', 'delete', 'put'): + fake_output = {'code': 0, 'message': 'success'} + elif request_type == 'get': + fake_output = {'code': 0, 'data': 'fake_date'} + mock_request = self.mock_object( + requests, request_type, mock.Mock( + return_value=FakeResponse( + 200, fake_output))) + self.rest_api.send_api( + method, + params=params, + request_type=request_type) + mock_request.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + method), + data=json.dumps(params), + headers={'X-Auth-Token': 'fake_token'}) + + @ddt.data({'method': r'security/token', + 'params': {'name': test_config.san_login, + 'password': test_config.san_password}, + 'request_type': 'post'}, + {'method': r'security/token', + 'params': None, + 'request_type': 'delete'}) + @ddt.unpack + def test_send_api_access_success(self, method, params, request_type): + if request_type == 'post': + fake_value = {'code': 0, 'data': { + 'token': 'fake_token', + 'expireTime': '7200', + 'type': 0}} + mock_requests = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + result = self.rest_api.send_api(method, params, request_type) + self.assertEqual(fake_value['data'], result) + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + method), + data=json.dumps(params), + headers=None) + if request_type == 'delete': + fake_value = {'code': 0, 'message': 'Success!'} + self.rest_api._token = 'fake_token' + mock_requests = self.mock_object( + requests, 'delete', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + self.rest_api.send_api(method, params, request_type) + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + method), + data=None, + headers={'X-Auth-Token': 'fake_token'}) + + def test_send_api_wrong_access_fail(self): + req_params = {'method': r'security/token', + 'params': {'name': test_config.san_login, + 'password': 'fake_password'}, + 'request_type': 'post'} + fake_value = {'message': ' User name or password error.', 'code': 400} + mock_request = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + self.assertRaises( + exception.VolumeBackendAPIException, + self.rest_api.send_api, + method=req_params['method'], + params=req_params['params'], + request_type=req_params['request_type']) + mock_request.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + req_params['method']), + data=json.dumps( + req_params['params']), + headers=None) + + def test_send_api_token_overtime_fail(self): + self.rest_api._token = 'fake_token' + fake_value = {'method': 'fake_url', + 'params': 'fake_params', + 'reuest_type': 'post'} + fake_out_put = {'message': 'Unauthorized access!', 'code': 301} + mock_requests = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_out_put))) + self.assertRaises(exception.VolumeDriverException, + self.rest_api.send_api, + method='fake_url', + params='fake_params', + request_type='post') + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + fake_value['method']), + data=json.dumps('fake_params'), + headers={ + 'X-Auth-Token': 'fake_token'}) + + def test_send_api_fail(self): + self.rest_api._token = 'fake_token' + fake_output = {'code': 999, 'message': 'fake_message'} + mock_request = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_output))) + self.assertRaises( + exception.VolumeBackendAPIException, + self.rest_api.send_api, + method='fake_method', + params='fake_params', + request_type='post') + mock_request.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.san_ip, + test_config.san_api_port, + 'fake_method'), + data=json.dumps('fake_params'), + headers={'X-Auth-Token': 'fake_token'} + ) + + +@ddt.ddt +class AS13000DriverTestCase(test.TestCase): + def __init__(self, *args, **kwds): + super(AS13000DriverTestCase, self).__init__(*args, **kwds) + self._ctxt = context.get_admin_context() + self.configuration = test_config + + def setUp(self): + self.rest_api = as13000_driver.RestAPIExecutor( + test_config.san_ip, + test_config.san_api_port, + test_config.san_login, + test_config.san_password) + self.as13000_san = as13000_driver.AS13000Driver( + configuration=self.configuration) + super(AS13000DriverTestCase, self).setUp() + + @ddt.data(None, 'pool1') + def test_do_setup(self, meta_pool): + mock_login = self.mock_object(as13000_driver.RestAPIExecutor, + 'login', mock.Mock()) + fake_nodes = [{'healthStatus': 1, 'ip': 'fakeip1'}, + {'healthStatus': 1, 'ip': 'fakeip2'}, + {'healthStatus': 1, 'ip': 'fakeip3'}] + mock_gcs = self.mock_object(self.as13000_san, + '_get_cluster_status', + mock.Mock(return_value=fake_nodes)) + fake_pools = { + 'pool1': {'name': 'pool1', 'type': '1'}, + 'pool2': {'name': 'pool2', 'type': 2} + } + mock_gpi = self.mock_object(self.as13000_san, + '_get_pools_info', + mock.Mock(return_value=fake_pools)) + mock_cp = self.mock_object(self.as13000_san, + '_check_pools', + mock.Mock()) + mock_cmp = self.mock_object(self.as13000_san, + '_check_meta_pool', + mock.Mock()) + + self.as13000_san.meta_pool = meta_pool + self.as13000_san.pools = ['pool1', 'pool2'] + self.as13000_san.do_setup(self._ctxt) + + mock_login.assert_called_once() + mock_gcs.assert_called_once() + if meta_pool: + mock_gpi.assert_called_with(['pool1', 'pool2', 'pool1']) + else: + mock_gpi.assert_called_with(['pool1', 'pool2']) + self.assertEqual('pool1', self.as13000_san.meta_pool) + mock_cp.assert_called_once() + mock_cmp.assert_called_once() + + def test_check_for_setup_error(self): + mock_sg = self.mock_object(configuration.Configuration, 'safe_get', + mock.Mock(return_value='fake_config')) + self.as13000_san.nodes = [{'fakenode': 'fake_name'}] + self.as13000_san.check_for_setup_error() + mock_sg.assert_called() + + def test_check_for_setup_error_no_healthy_node_fail(self): + mock_sg = self.mock_object(configuration.Configuration, 'safe_get', + mock.Mock(return_value='fake_config')) + self.as13000_san.nodes = [] + self.assertRaises(exception.VolumeDriverException, + self.as13000_san.check_for_setup_error) + mock_sg.assert_called() + + def test_check_for_setup_error_no_config_fail(self): + mock_sg = self.mock_object(configuration.Configuration, 'safe_get', + mock.Mock(return_value=None)) + self.as13000_san.nodes = [] + self.assertRaises(exception.InvalidConfigurationValue, + self.as13000_san.check_for_setup_error) + mock_sg.assert_called() + + def test__check_pools(self): + fake_pools_info = { + 'pool1': {'name': 'pool1', 'type': '1'}, + 'pool2': {'name': 'pool2', 'type': 1} + } + self.as13000_san.pools = ['pool1'] + self.as13000_san.pools_info = fake_pools_info + self.as13000_san._check_pools() + + def test__check_pools_fail(self): + fake_pools_info = { + 'pool1': {'name': 'pool1', 'type': '1'}, + 'pool2': {'name': 'pool2', 'type': 1} + } + self.as13000_san.pools = ['pool0, pool1'] + self.as13000_san.pools_info = fake_pools_info + self.assertRaises(exception.InvalidInput, + self.as13000_san._check_pools) + + def test__check_meta_pool(self): + fake_pools_info = { + 'pool1': {'name': 'pool1', 'type': 2}, + 'pool2': {'name': 'pool2', 'type': 1} + } + self.as13000_san.meta_pool = 'pool2' + self.as13000_san.pools_info = fake_pools_info + self.as13000_san._check_meta_pool() + + @ddt.data(None, 'pool0', 'pool1') + def test__check_meta_pool_failed(self, meta_pool): + fake_pools_info = { + 'pool1': {'name': 'pool1', 'type': 2}, + 'pool2': {'name': 'pool2', 'type': 1} + } + + self.as13000_san.meta_pool = meta_pool + self.as13000_san.pools_info = fake_pools_info + self.assertRaises(exception.InvalidInput, + self.as13000_san._check_meta_pool) + + @mock.patch.object(as13000_driver.RestAPIExecutor, + 'send_rest_api') + def test_create_volume(self, mock_rest): + volume = fake_volume.fake_volume_obj(self._ctxt, host='H@B#P') + self.as13000_san.pools_info = {'P': {'name': 'P', 'type': 1}} + self.as13000_san.meta_pool = 'meta_pool' + self.as13000_san.create_volume(volume) + + mock_rest.assert_called_once_with( + method='block/lvm', + params={ + "name": volume.name.replace('-', '_'), + "capacity": volume.size * 1024, + "dataPool": 'P', + "dataPoolType": 1, + "metaPool": 'meta_pool' + }, + request_type='post') + + @ddt.data(1, 2) + def test_create_volume_from_snapshot(self, size): + volume = fake_volume.fake_volume_obj(self._ctxt, size=size) + volume2 = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume=volume2) + + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + _tnd_mock = mock.Mock(side_effect=('source_volume', + 'dest_volume', + 'snapshot')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + _tnd_mock) + mock_lock_op = self.mock_object(self.as13000_san, + '_snapshot_lock_op', + mock.Mock()) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + mock_fv = self.mock_object(self.as13000_san, + '_filling_volume', + mock.Mock()) + mock_wvf = self.mock_object(self.as13000_san, + '_wait_volume_filled', + mock.Mock(side_effect=(False, True))) + mock_ev = self.mock_object(self.as13000_san, 'extend_volume', + mock.Mock()) + + self.as13000_san.create_volume_from_snapshot(volume, snapshot) + + lock_op_calls = [ + mock.call('lock', 'source_volume', 'snapshot', 'fake_pool'), + mock.call('unlock', 'source_volume', 'snapshot', 'fake_pool') + ] + mock_lock_op.assert_has_calls(lock_op_calls) + mock_fv.assert_called_once_with('dest_volume', 'fake_pool') + wait_volume_filled_calls = [ + mock.call('dest_volume', 'fake_pool', 10, 5), + mock.call('dest_volume', 'fake_pool', 10, 5), + ] + mock_wvf.assert_has_calls(wait_volume_filled_calls) + + mock_eh.assert_called() + mock_tnd.assert_called() + params = { + 'originalLvm': 'source_volume', + 'originalPool': 'fake_pool', + 'originalSnap': 'snapshot', + 'name': 'dest_volume', + 'pool': 'fake_pool'} + mock_rest.assert_called_once_with(method='snapshot/volume/cloneLvm', + params=params, + request_type='post') + if size == 2: + mock_ev.assert_called_once_with(volume, size) + + def test_create_volume_from_snapshot_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume_size=10) + self.assertRaises( + exception.InvalidInput, + self.as13000_san.create_volume_from_snapshot, volume, snapshot) + + @ddt.data(1, 2) + def test_create_cloned_volume(self, size): + volume = fake_volume.fake_volume_obj(self._ctxt, size=size) + volume_src = fake_volume.fake_volume_obj(self._ctxt) + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + side_effect=('fake_name1', 'fake_name2'))) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + mock_ev = self.mock_object(self.as13000_san, + 'extend_volume', + mock.Mock()) + self.as13000_san.create_cloned_volume(volume, volume_src) + mock_eh.assert_called() + mock_tnd.assert_called() + method = 'block/lvm/clone' + params = { + 'srcVolumeName': 'fake_name2', + 'srcPoolName': 'fake_pool', + 'destVolumeName': 'fake_name1', + 'destPoolName': 'fake_pool'} + request_type = 'post' + mock_rest.assert_called_once_with( + method=method, params=params, request_type=request_type) + if size == 2: + mock_ev.assert_called_once_with(volume, size) + + def test_create_clone_volume_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + volume_source = fake_volume.fake_volume_obj(self._ctxt, size=2) + self.assertRaises( + exception.InvalidInput, + self.as13000_san.create_cloned_volume, volume, volume_source) + + def test_extend_volume(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + return_value='fake_name')) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=True)) + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + self.as13000_san.extend_volume(volume, 10) + mock_tnd.assert_called_once_with(volume.name) + mock_cv.assert_called_once_with(volume) + mock_eh.assert_called_once_with(volume.host, level='pool') + method = 'block/lvm' + request_type = 'put' + params = {'pool': 'fake_pool', + 'name': 'fake_name', + 'newCapacity': 10240} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test_extend_volume_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + return_value='fake_name')) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=False)) + self.assertRaises(exception.VolumeDriverException, + self.as13000_san.extend_volume, volume, 10) + mock_tnd.assert_called_once_with(volume.name) + mock_cv.assert_called_once_with(volume) + + @ddt.data(True, False) + def test_delete_volume(self, volume_exist): + volume = fake_volume.fake_volume_obj(self._ctxt) + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + return_value='fake_name')) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=volume_exist)) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + self.as13000_san.delete_volume(volume) + mock_tnd.assert_called_once_with(volume.name) + mock_cv.assert_called_once_with(volume) + + if volume_exist: + mock_eh.assert_called_once_with(volume.host, level='pool') + + method = 'block/lvm?pool=%s&lvm=%s' % ('fake_pool', 'fake_name') + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test_create_snapshot(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume=volume) + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=True)) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + side_effect=('fake_name', 'fake_snap'))) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + self.as13000_san.create_snapshot(snapshot) + + mock_eh.assert_called_once_with(volume.host, level='pool') + mock_tnd.assert_called() + mock_cv.assert_called_once_with(snapshot.volume) + method = 'snapshot/volume' + params = {'snapName': 'fake_snap', + 'volumeName': 'fake_name', + 'poolName': 'fake_pool', + 'snapType': 'r'} + request_type = 'post' + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test_create_snapshot_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume=volume) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=False)) + self.assertRaises(exception.VolumeDriverException, + self.as13000_san.create_snapshot, snapshot) + mock_cv.assert_called_once_with(snapshot.volume) + + def test_delete_snapshot(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume=volume) + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=True)) + mock_tnd = self.mock_object( + self.as13000_san, '_trans_name_down', mock.Mock( + side_effect=('fake_name', 'fake_snap'))) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + self.as13000_san.delete_snapshot(snapshot) + + mock_eh.assert_called_once_with(volume.host, level='pool') + mock_tnd.assert_called() + mock_cv.assert_called_once_with(snapshot.volume) + + method = ('snapshot/volume?snapName=%s&volumeName=%s&poolName=%s' + % ('fake_snap', 'fake_name', 'fake_pool')) + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test_delete_snapshot_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt) + snapshot = fake_snapshot.fake_snapshot_obj(self._ctxt, volume=volume) + mock_cv = self.mock_object(self.as13000_san, + '_check_volume', + mock.Mock(return_value=False)) + self.assertRaises(exception.VolumeDriverException, + self.as13000_san.delete_snapshot, snapshot) + mock_cv.assert_called_once_with(snapshot.volume) + + @ddt.data((time.time() - 3000), (time.time() - 4000)) + def test__update_volume_stats(self, time_token): + self.as13000_san.VENDOR = 'INSPUR' + self.as13000_san.VERSION = 'V1.3.1' + self.as13000_san.PROTOCOL = 'iSCSI' + mock_sg = self.mock_object(configuration.Configuration, 'safe_get', + mock.Mock(return_value='fake_backend_name')) + fake_pool_backend = [{'pool_name': 'fake_pool'}, + {'pool_name': 'fake_pool1'}] + self.as13000_san.pools = ['fake_pool'] + mock_gps = self.mock_object(self.as13000_san, '_get_pools_stats', + mock.Mock(return_value=fake_pool_backend)) + self.as13000_san._stats = None + self.as13000_san._token_time = time_token + self.as13000_san.token_available_time = 3600 + mock_login = self.mock_object(as13000_driver.RestAPIExecutor, + 'login') + + self.as13000_san._update_volume_stats() + backend_data = {'driver_version': 'V1.3.1', + 'pools': [{'pool_name': 'fake_pool'}], + 'storage_protocol': 'iSCSI', + 'vendor_name': 'INSPUR', + 'volume_backend_name': 'fake_backend_name'} + + self.assertEqual(backend_data, self.as13000_san._stats) + mock_sg.assert_called_once_with('volume_backend_name') + mock_gps.assert_called_once() + if (time.time() - time_token) > 3600: + mock_login.assert_called_once() + else: + mock_login.assert_not_called() + + @ddt.data((4, u'127.0.0.1', '3260'), + (6, u'FF01::101', '3260')) + @ddt.unpack + def test__build_target_portal(self, version, ip, port): + portal = self.as13000_san._build_target_portal(ip, port) + if version == 4: + self.assertEqual(portal, '127.0.0.1:3260') + else: + self.assertEqual(portal, '[FF01::101]:3260') + + @ddt.data((True, True, True), + (True, True, False), + (False, True, True), + (False, True, False), + (False, False, True), + (False, False, False), + (True, False, True), + (True, False, False)) + @ddt.unpack + def test_initialize_connection(self, host_exist, multipath, chap_enabled): + volume = fake_volume.fake_volume_obj(self._ctxt) + connector = {'multipath': multipath, + 'ip': 'fake_ip', + 'host': 'fake_host'} + self.as13000_san.configuration.use_chap_auth = chap_enabled + fakenode = [{'name': 'fake_name1', 'ip': 'node_ip1'}, + {'name': 'fake_name2', 'ip': 'node_ip2'}, + {'name': 'fake_name3', 'ip': 'node_ip3'}] + self.as13000_san.nodes = fakenode + if multipath: + mock_gtfc = self.mock_object( + self.as13000_san, + '_get_target_from_conn', + mock.Mock(return_value=(host_exist, + 'target_name', + ['fake_name1', 'fake_name2']))) + else: + mock_gtfc = self.mock_object( + self.as13000_san, + '_get_target_from_conn', + mock.Mock(return_value=(host_exist, + 'target_name', + ['fake_name1']))) + + mock_altt = self.mock_object(self.as13000_san, + '_add_lun_to_target', + mock.Mock()) + mock_ct = self.mock_object(self.as13000_san, + '_create_target', + mock.Mock()) + mock_ahtt = self.mock_object(self.as13000_san, + '_add_host_to_target', + mock.Mock()) + mock_actt = self.mock_object(self.as13000_san, + '_add_chap_to_target', + mock.Mock()) + mock_gli = self.mock_object(self.as13000_san, + '_get_lun_id', + mock.Mock(return_value='1')) + mock_rr = self.mock_object(random, 'randint', + mock.Mock(return_value='12345678')) + mock_btp = self.mock_object(self.as13000_san, + '_build_target_portal', + mock.Mock(side_effect=['node_ip1:3260', + 'node_ip2:3260', + 'node_ip3:3260'])) + + connect_info = self.as13000_san.initialize_connection( + volume, connector) + + expect_conn_data = { + 'target_discovered': True, + 'volume_id': volume.id, + } + if host_exist: + if multipath: + expect_conn_data.update({ + 'target_portals': ['node_ip1:3260', 'node_ip2:3260'], + 'target_luns': [1] * 2, + 'target_iqns': ['target_name'] * 2 + }) + else: + expect_conn_data.update({ + 'target_portal': 'node_ip1:3260', + 'target_lun': 1, + 'target_iqn': 'target_name' + }) + else: + target_name = 'target.inspur.fake_host-12345678' + if multipath: + expect_conn_data.update({ + 'target_portals': ['node_ip1:3260', + 'node_ip2:3260', + 'node_ip3:3260'], + 'target_luns': [1] * 3, + 'target_iqns': [target_name] * 3 + }) + else: + expect_conn_data.update({ + 'target_portal': 'node_ip1:3260', + 'target_lun': 1, + 'target_iqn': target_name + }) + + if chap_enabled: + expect_conn_data['auth_method'] = 'CHAP' + expect_conn_data['auth_username'] = 'fakeuser' + expect_conn_data['auth_password'] = 'fakepass' + + expect_datas = { + 'driver_volume_type': 'iscsi', + 'data': expect_conn_data + } + + self.assertEqual(expect_datas, connect_info) + mock_gtfc.assert_called_once_with('fake_ip') + mock_altt.assert_called_once() + if not host_exist: + mock_ct.assert_called_once() + mock_ahtt.assert_called_once() + mock_rr.assert_called_once() + if chap_enabled: + mock_actt.assert_called_once() + + mock_gli.assert_called_once() + mock_btp.assert_called() + + @ddt.data(True, False) + def test_terminate_connection(self, delete_target): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + connector = {'multipath': False, + 'ip': 'fake_ip', + 'host': 'fake_host'} + mock_tnd = self.mock_object(self.as13000_san, '_trans_name_down', + mock.Mock(return_value='fake_volume')) + fake_target_list = [{'hostIp': ['fake_ip'], + 'name': 'target_name', + 'lun': [ + {'lvm': 'fake_volume', 'lunID': 'fake_id'}]}] + mock_gtl = self.mock_object(self.as13000_san, '_get_target_list', + mock.Mock(return_value=fake_target_list)) + mock_dlft = self.mock_object(self.as13000_san, + '_delete_lun_from_target', + mock.Mock()) + if delete_target: + mock_gll = self.mock_object(self.as13000_san, '_get_lun_list', + mock.Mock(return_value=[])) + else: + mock_gll = self.mock_object(self.as13000_san, '_get_lun_list', + mock.Mock(return_value=[1, 2])) + mock_dt = self.mock_object(self.as13000_san, '_delete_target', + mock.Mock()) + self.as13000_san.terminate_connection(volume, connector) + mock_tnd.assert_called_once_with(volume.name) + mock_gtl.assert_called_once() + mock_dlft.assert_called_once_with(lun_id='fake_id', + target_name='target_name') + mock_gll.assert_called_once_with('target_name') + if delete_target: + mock_dt.assert_called_once_with('target_name') + else: + mock_dt.assert_not_called() + + @ddt.data(True, False) + def test_terminate_connection_force(self, delete_target): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + connector = {} + mock_tnd = self.mock_object(self.as13000_san, '_trans_name_down', + mock.Mock(return_value='fake_volume')) + fake_target_list = [{'hostIp': ['fake_hostIp'], + 'name':'target_name', + 'lun':[{'lvm': 'fake_volume', + 'lunID': 'fake_id'}]}] + mock_gtl = self.mock_object(self.as13000_san, '_get_target_list', + mock.Mock(return_value=fake_target_list)) + mock_dlft = self.mock_object(self.as13000_san, + '_delete_lun_from_target', + mock.Mock()) + if delete_target: + mock_gll = self.mock_object(self.as13000_san, '_get_lun_list', + mock.Mock(return_value=[])) + else: + mock_gll = self.mock_object(self.as13000_san, '_get_lun_list', + mock.Mock(return_value=[1, 2])) + mock_dt = self.mock_object(self.as13000_san, '_delete_target', + mock.Mock()) + + self.as13000_san.terminate_connection(volume, connector) + + mock_tnd.assert_called_once_with(volume.name) + mock_gtl.assert_called_once() + mock_dlft.assert_called_once_with(lun_id='fake_id', + target_name='target_name') + mock_gll.assert_called_once_with('target_name') + if delete_target: + mock_dt.assert_called_once_with('target_name') + else: + mock_dt.assert_not_called() + + @mock.patch.object(as13000_driver.RestAPIExecutor, + 'send_rest_api') + def test__get_pools_info(self, mock_rest): + fake_pools_data = [{'name': 'pool1', 'type': 1}, + {'name': 'pool2', 'type': 2}] + mock_rest.return_value = fake_pools_data + + # get a partial of pools + result_pools_info = self.as13000_san._get_pools_info(['pool1']) + self.assertEqual(result_pools_info, + {'pool1': {'name': 'pool1', 'type': 1}}) + + # get both exist pools + result_pools_info = self.as13000_san._get_pools_info(['pool1', + 'pool2']) + self.assertEqual(result_pools_info, + {'pool1': {'name': 'pool1', 'type': 1}, + 'pool2': {'name': 'pool2', 'type': 2}}) + + # get pools not exist + result_pools_info = self.as13000_san._get_pools_info(['pool1', + 'pool2', + 'pool3']) + self.assertEqual(result_pools_info, + {'pool1': {'name': 'pool1', 'type': 1}, + 'pool2': {'name': 'pool2', 'type': 2}}) + + def test__get_pools_stats(self): + pool_date = [{'ID': 'fake_id', + 'name': 'fake_name', + 'totalCapacity': '2t', + 'usedCapacity': '300g'}] + self.as13000_san.pools = ['fake_name'] + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=pool_date)) + mock_uc = self.mock_object(self.as13000_san, '_unit_convert', + mock.Mock(side_effect=(2000, 300))) + pool_info = { + 'pool_name': 'fake_name', + 'total_capacity_gb': 2000, + 'free_capacity_gb': 1700, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + } + + result_pools = self.as13000_san._get_pools_stats() + + expect_pools = [pool_info] + self.assertEqual(expect_pools, result_pools) + mock_rest.assert_called_once_with(method='block/pool?type=2', + request_type='get') + mock_uc.assert_called() + + @ddt.data('fake_ip3', 'fake_ip5') + def test__get_target_from_conn(self, host_ip): + target_list = [ + { + 'hostIp': ['fake_ip1', 'fake_ip2'], + 'name':'fake_target_1', + 'node':['fake_node1', 'fake_node2'] + }, + { + 'hostIp': ['fake_ip3', 'fake_ip4'], + 'name': 'fake_target_2', + 'node': ['fake_node4', 'fake_node3'] + } + ] + mock_gtl = self.mock_object(self.as13000_san, + '_get_target_list', + mock.Mock(return_value=target_list)) + + host_exist, target_name, node = ( + self.as13000_san._get_target_from_conn(host_ip)) + + if host_ip is 'fake_ip3': + self.assertEqual((True, 'fake_target_2', + ['fake_node4', 'fake_node3']), + (host_exist, target_name, node)) + else: + self.assertEqual((False, None, None), + (host_exist, target_name, node)) + mock_gtl.assert_called_once() + + def test__get_target_list(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value='fake_date')) + method = 'block/target/detail' + request_type = 'get' + result = self.as13000_san._get_target_list() + self.assertEqual('fake_date', result) + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__create_target(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + target_name = 'fake_name' + target_node = 'fake_node' + method = 'block/target' + params = {'name': target_name, 'nodeName': target_node} + request_type = 'post' + self.as13000_san._create_target(target_name, target_node) + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test__delete_target(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + target_name = 'fake_name' + method = 'block/target?name=%s' % target_name + request_type = 'delete' + self.as13000_san._delete_target(target_name) + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__add_chap_to_target(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + target_name = 'fake_name' + chap_username = 'fake_user' + chap_password = 'fake_pass' + self.as13000_san._add_chap_to_target(target_name, + chap_username, + chap_password) + + method = 'block/chap/bond' + params = {'target': target_name, + 'user': chap_username, + 'password': chap_password} + request_type = 'post' + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test__add_host_to_target(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + target_name = 'fake_name' + host_ip = 'fake_ip' + method = 'block/host' + params = {'name': target_name, 'hostIp': host_ip} + request_type = 'post' + self.as13000_san._add_host_to_target(host_ip, target_name) + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test__add_lun_to_target(self): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + mock.Mock(return_value='fake_name')) + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + + target_name = 'fake_target' + self.as13000_san._add_lun_to_target(target_name, volume) + method = 'block/lun' + params = {'name': target_name, + 'pool': 'fake_pool', + 'lvm': 'fake_name'} + request_type = 'post' + mock_eh.assert_called_once_with(volume.host, level='pool') + mock_tnd.assert_called_once_with(volume.name) + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test__add_lun_to_target_retry_3times(self): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + mock.Mock(return_value='fake_name')) + mock_rest = self.mock_object( + as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(side_effect=(exception.VolumeDriverException, + mock.MagicMock()))) + + target_name = 'fake_target' + self.as13000_san._add_lun_to_target(target_name, volume) + method = 'block/lun' + params = {'name': target_name, + 'pool': 'fake_pool', + 'lvm': 'fake_name'} + request_type = 'post' + mock_eh.assert_called_with(volume.host, level='pool') + mock_tnd.assert_called_with(volume.name) + mock_rest.assert_called_with(method=method, + params=params, + request_type=request_type) + + def test__add_lun_to_target_fail(self): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + mock.Mock(return_value='fake_name')) + mock_rest = self.mock_object( + as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(side_effect=exception.VolumeDriverException)) + + target_name = 'fake_target' + self.assertRaises(exception.VolumeDriverException, + self.as13000_san._add_lun_to_target, + target_name=target_name, + volume=volume) + method = 'block/lun' + params = {'name': target_name, + 'pool': 'fake_pool', + 'lvm': 'fake_name'} + request_type = 'post' + mock_eh.assert_called_with(volume.host, level='pool') + mock_tnd.assert_called_with(volume.name) + mock_rest.assert_called_with(method=method, + params=params, + request_type=request_type) + + def test__delete_lun_from_target(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + target_name = 'fake_target' + lun_id = 'fake_id' + self.as13000_san._delete_lun_from_target(target_name, lun_id) + method = 'block/lun?name=%s&id=%s&force=1' % (target_name, lun_id) + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + @ddt.data('lock', 'unlock') + def test__snapshot_lock_op(self, operation): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + vol_name = 'fake_volume' + snap_name = 'fake_snapshot' + pool_name = "fake_pool" + self.as13000_san._snapshot_lock_op(operation, + vol_name, + snap_name, + pool_name) + + method = 'snapshot/volume/' + operation + request_type = 'post' + params = {'snapName': snap_name, + 'volumeName': vol_name, + 'poolName': pool_name} + mock_rest.assert_called_once_with(method=method, + params=params, + request_type=request_type) + + def test__filling_volume(self): + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock()) + vol_name = 'fake_volume' + pool_name = 'fake_pool' + self.as13000_san._filling_volume(vol_name, pool_name) + params = {'pool': 'fake_pool', 'name': 'fake_volume'} + mock_rest.assert_called_once_with(method='block/lvm/filling', + params=params, + request_type='post') + + @ddt.data(2, 3) + def test__wait_volume_filled(self, attempts): + mock_gv = self.mock_object(self.as13000_san, '_get_volumes', + mock.Mock(side_effect=( + [{'name': 'fake_v1', 'lvmType': 2}], + [{'name': 'fake_v1', 'lvmType': 2}], + [{'name': 'fake_v1', 'lvmType': 1}] + ))) + mock_el = self.mock_object(eventlet, 'sleep', + mock.Mock(return_value=None)) + + ret = self.as13000_san._wait_volume_filled('fake_v1', 'fake_pool', + attempts, 1) + if attempts == 2: + self.assertEqual(ret, False) + else: + self.assertEqual(ret, True) + mock_gv.assert_called_with('fake_pool') + mock_el.assert_called_with(1) + + def test__get_lun_list(self): + target_name = 'fake_name' + lun_list = ['lun_1', 'lun_2'] + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=lun_list)) + lun_result = self.as13000_san._get_lun_list(target_name) + self.assertEqual(lun_list, lun_result) + method = 'block/lun?name=%s' % target_name + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + @ddt.data(True, False) + def test__check_volume(self, exist): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + mock.Mock(return_value='fake_name')) + mock_el = self.mock_object(eventlet, 'sleep', + mock.Mock(return_value=None)) + if exist: + mock_gv = self.mock_object(self.as13000_san, '_get_volumes', + mock.Mock(return_value=[ + {'name': 'fake_name'}, + {'name': 'fake_name2'}])) + else: + mock_gv = self.mock_object(self.as13000_san, '_get_volumes', + mock.Mock(return_value=[ + {'name': 'fake_name2'}, + {'name': 'fake_name3'}])) + expect = self.as13000_san._check_volume(volume) + self.assertEqual(exist, expect) + mock_eh.assert_called_once_with(volume.host, 'pool') + mock_tnd.assert_called_once_with(volume.name) + if exist: + mock_gv.assert_called_once_with('fake_pool') + else: + mock_gv.assert_called() + mock_el.assert_called() + + def test__get_volumes(self): + volumes = [{'name': 'fake_name1'}, + {'name': 'fake_name2'}, + {'name': 'fake_name3'}] + pool = 'fake_pool' + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=volumes)) + result = self.as13000_san._get_volumes(pool) + method = 'block/lvm?pool=%s' % pool + request_type = 'get' + self.assertEqual(volumes, result) + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__get_cluster_status(self): + method = 'cluster/node' + request_type = 'get' + cluster = 'fake_cluster' + mock_rest = self.mock_object(as13000_driver.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=cluster)) + result = self.as13000_san._get_cluster_status() + self.assertEqual(cluster, result) + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + @ddt.data(True, False) + def test__get_lun_id(self, lun_exist): + volume = fake_volume.fake_volume_obj(self._ctxt, host='fakehost') + if lun_exist: + lun_list = [{'id': '01', 'mappingLvm': r'fake_pool/fake_volume1'}, + {'id': '02', 'mappingLvm': r'fake_pool/fake_volume2'}] + else: + lun_list = [{'id': '01', 'mappingLvm': r'fake_pool/fake_volume1'}, + {'id': '02', 'mappingLvm': r'fake_pool/fake_volume0'}] + + mock_eh = self.mock_object(volume_utils, + 'extract_host', + mock.Mock(return_value='fake_pool')) + mock_tnd = self.mock_object(self.as13000_san, + '_trans_name_down', + mock.Mock(return_value='fake_volume2')) + mock_gll = self.mock_object(self.as13000_san, '_get_lun_list', + mock.Mock(return_value=lun_list)) + + lun_id = self.as13000_san._get_lun_id(volume, 'fake_target') + if lun_exist: + self.assertEqual('02', lun_id) + else: + self.assertIsNone(lun_id) + + mock_eh.assert_called_once_with(volume.host, level='pool') + mock_tnd.assert_called_once_with(volume.name) + mock_gll.assert_called_once_with('fake_target') + + def test__trans_name_down(self): + fake_name = 'test-abcd-1234_1234-234' + expect = 'test_abcd_1234_1234_234' + result = self.as13000_san._trans_name_down(fake_name) + self.assertEqual(expect, result) + + @ddt.data('5000000000', '5000000k', '5000mb', '50G', '5TB', '5PB', '5EB') + def test__unit_convert(self, capacity): + trans = {'5000000000': '%.0f' % (float(5000000000) / (1024 ** 3)), + '5000000k': '%.0f' % (float(5000000) / (1024 ** 2)), + '5000mb': '%.0f' % (float(5000) / 1024), + '50G': '%.0f' % float(50), + '5TB': '%.0f' % (float(5) * 1024), + '5PB': '%.0f' % (float(5) * (1024 ** 2)), + '5EB': '%.0f' % (float(5) * (1024 ** 3))} + expect = float(trans[capacity]) + result = self.as13000_san._unit_convert(capacity) + self.assertEqual(expect, result) diff --git a/cinder/volume/drivers/inspur/as13000/__init__.py b/cinder/volume/drivers/inspur/as13000/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/inspur/as13000/as13000_driver.py b/cinder/volume/drivers/inspur/as13000/as13000_driver.py new file mode 100644 index 00000000000..21f15d4fee1 --- /dev/null +++ b/cinder/volume/drivers/inspur/as13000/as13000_driver.py @@ -0,0 +1,873 @@ +# Copyright 2017 Inspur Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Volume driver for Inspur AS13000 +""" + +import ipaddress +import json +import random +import re +import time + +import eventlet +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units +import requests + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils +from cinder.volume.drivers.san import san +from cinder.volume import utils as volume_utils + +LOG = logging.getLogger(__name__) + +inspur_as13000_opts = [ + cfg.ListOpt( + 'as13000_ipsan_pools', + default=['Pool0'], + help='The Storage Pools Cinder should use, a comma separated list.'), + cfg.IntOpt( + 'as13000_token_available_time', + default=3300, + min=600, max=3600, + help='The effective time of token validity in seconds.'), + cfg.StrOpt( + 'as13000_meta_pool', + help='The pool which is used as a meta pool when creating a volume, ' + 'and it should be a replication pool at present. ' + 'If not set, the driver will choose a replication pool ' + 'from the value of as13000_ipsan_pools.'), +] + +CONF = cfg.CONF +CONF.register_opts(inspur_as13000_opts) + + +class RestAPIExecutor(object): + def __init__(self, hostname, port, username, password): + self._username = username + self._password = password + self._token = None + self._baseurl = 'http://%s:%s/rest' % (hostname, port) + + def login(self): + """Login the AS13000 and store the token.""" + self._token = self._login() + LOG.debug('Login the AS13000.') + + def _login(self): + """Do request to login the AS13000 and get the token.""" + method = 'security/token' + params = {'name': self._username, 'password': self._password} + token = self.send_rest_api(method=method, params=params, + request_type='post').get('token') + return token + + @utils.retry(exception.VolumeDriverException, interval=1, retries=3) + def send_rest_api(self, method, params=None, request_type='post'): + try: + return self.send_api(method, params, request_type) + except exception.VolumeDriverException: + self.login() + raise + + @staticmethod + @utils.trace_method + def do_request(cmd, url, header, data): + """Send request to the storage and handle the response.""" + if cmd in ['post', 'get', 'put', 'delete']: + req = getattr(requests, cmd)(url, data=data, headers=header) + else: + msg = (_('Unsupported cmd: %s.') % cmd) + raise exception.VolumeBackendAPIException(msg) + + response = req.json() + code = req.status_code + LOG.debug('CODE: %(code)s, RESPONSE: %(response)s.', + {'code': code, 'response': response}) + + if code != 200: + msg = (_('Code: %(code)s, URL: %(url)s, Message: %(msg)s.') + % {'code': req.status_code, + 'url': req.url, + 'msg': req.text}) + LOG.error(msg) + raise exception.VolumeDriverException(msg) + + return response + + @utils.trace + def send_api(self, method, params=None, request_type='post'): + if params: + params = json.dumps(params) + + url = '%s/%s' % (self._baseurl, method) + + # header is not needed when the driver login the backend + if method == 'security/token': + if request_type == 'delete': + header = {'X-Auth-Token': self._token} + else: + header = None + else: + if not self._token: + self.login() + header = {'X-Auth-Token': self._token} + + response = self.do_request(request_type, url, header, params) + + try: + code = response.get('code') + if code == 0: + if request_type == 'get': + data = response.get('data') + else: + if method == 'security/token': + data = response.get('data') + else: + data = response.get('message') + data = str(data).lower() + if hasattr(data, 'success'): + return + elif code == 301: + msg = _('Token is expired.') + LOG.error(msg) + raise exception.VolumeDriverException(msg) + else: + message = response.get('message') + msg = (_('Unexpected RestAPI response: %(code)d %(msg)s.') % { + 'code': code, 'msg': message}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(msg) + except ValueError: + msg = _("Deal with response failed.") + raise exception.VolumeDriverException(msg) + + return data + + +@interface.volumedriver +class AS13000Driver(san.SanISCSIDriver): + """Driver for Inspur AS13000 storage. + + Version history: + + .. code-block:: none + + 1.0.0 - Initial driver + """ + + VENDOR = 'INSPUR' + VERSION = '1.0.0' + PROTOCOL = 'iSCSI' + + # ThirdPartySystems wiki page + CI_WIKI_NAME = 'INSPUR_CI' + + def __init__(self, *args, **kwargs): + super(AS13000Driver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(inspur_as13000_opts) + self.hostname = self.configuration.san_ip + self.port = self.configuration.safe_get('san_api_port') or 8088 + self.username = self.configuration.san_login + self.password = self.configuration.san_password + self.token_available_time = (self.configuration. + as13000_token_available_time) + self.pools = self.configuration.as13000_ipsan_pools + self.meta_pool = self.configuration.as13000_meta_pool + self.pools_info = {} + self.nodes = [] + self._token_time = 0 + # get the RestAPIExecutor + self._rest = RestAPIExecutor(self.hostname, + self.port, + self.username, + self.password) + + @utils.trace + def do_setup(self, context): + # get tokens for the driver + self._rest.login() + self._token_time = time.time() + + # get available nodes in the backend + for node in self._get_cluster_status(): + if node.get('healthStatus') == 1 and node.get('ip'): + self.nodes.append(node) + + # collect pools info + meta_pools = [self.meta_pool] if self.meta_pool else [] + self.pools_info = self._get_pools_info(self.pools + meta_pools) + + # setup the meta pool if it is not setted + if not self.meta_pool: + for pool_info in self.pools_info.values(): + if pool_info['type'] in (1, '1'): + self.meta_pool = pool_info['name'] + break + + self._check_pools() + + self._check_meta_pool() + + @utils.trace + def check_for_setup_error(self): + """Do check to make sure service is available.""" + # check the required flags in conf + required_flags = ['san_ip', 'san_login', 'san_password', + 'as13000_ipsan_pools'] + for flag in required_flags: + value = self.configuration.safe_get(flag) + if not value: + msg = (_('Required flag %s is not set.') % flag) + LOG.error(msg) + raise exception.InvalidConfigurationValue(option=flag, + value=value) + + # make sure at least one node can + if not self.nodes: + msg = _('No healthy nodes are available!') + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + def _check_pools(self): + """Check the pool in conf exist in the AS13000.""" + if not set(self.pools).issubset(self.pools_info): + pools = set(self.pools) - set(self.pools_info) + msg = _('Pools %s do not exist.') % pools + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + def _check_meta_pool(self): + """Check whether the meta pool is valid.""" + if not self.meta_pool: + msg = _('Meta pool is not set.') + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + if self.meta_pool not in self.pools_info: + msg = _('Meta pool %s does not exist.') % self.meta_pool + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + if self.pools_info[self.meta_pool]['type'] not in (1, '1'): + msg = _('Meta pool %s is not a replication pool.') % self.meta_pool + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + @utils.trace + def create_volume(self, volume): + """Create volume in the backend.""" + pool = volume_utils.extract_host(volume.host, level='pool') + size = volume.size * units.Ki + name = self._trans_name_down(volume.name) + + method = 'block/lvm' + request_type = "post" + params = { + "name": name, + "capacity": size, + "dataPool": pool, + "dataPoolType": self.pools_info[pool]['type'], + "metaPool": self.meta_pool + } + self._rest.send_rest_api(method=method, params=params, + request_type=request_type) + + @utils.trace + def create_volume_from_snapshot(self, volume, snapshot): + """Create a new volume base on a specific snapshot.""" + if snapshot.volume_size > volume.size: + msg = (_("create_volume_from_snapshot: snapshot %(snapshot_name)s " + "size is %(snapshot_size)dGB and doesn't fit in target " + "volume %(volume_name)s of size %(volume_size)dGB.") % + {'snapshot_name': snapshot.name, + 'snapshot_size': snapshot.volume_size, + 'volume_name': volume.name, + 'volume_size': volume.size}) + LOG.error(msg) + raise exception.InvalidInput(message=msg) + src_vol_name = self._trans_name_down(snapshot.volume_name) + source_vol = snapshot.volume + src_pool = volume_utils.extract_host(source_vol['host'], + level='pool') + dest_name = self._trans_name_down(volume.name) + dest_pool = volume_utils.extract_host(volume.host, level='pool') + snap_name = self._trans_name_down(snapshot.name) + + # lock the snapshot before clone from it + self._snapshot_lock_op('lock', src_vol_name, snap_name, src_pool) + + # do clone from snap to a volume + method = 'snapshot/volume/cloneLvm' + request_type = 'post' + params = {'originalLvm': src_vol_name, + 'originalPool': src_pool, + 'originalSnap': snap_name, + 'name': dest_name, + 'pool': dest_pool} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + # do filling the cloned volume + self._filling_volume(dest_name, dest_pool) + + # wait until the cloned volume has been filled + while True: + if self._wait_volume_filled(dest_name, dest_pool, 10, 5): + break + + # unlock the original snapshot + self._snapshot_lock_op('unlock', src_vol_name, snap_name, src_pool) + + if volume.size > snapshot.volume_size: + self.extend_volume(volume, volume.size) + + @utils.trace + def create_cloned_volume(self, volume, src_vref): + """Clone a volume.""" + if src_vref.size > volume.size: + msg = (_("create_cloned_volume: source volume %(src_vol)s " + "size is %(src_size)dGB and doesn't fit in target " + "volume %(tgt_vol)s of size %(tgt_size)dGB.") % + {'src_vol': src_vref.name, + 'src_size': src_vref.size, + 'tgt_vol': volume.name, + 'tgt_size': volume.size}) + LOG.error(msg) + raise exception.InvalidInput(message=msg) + dest_pool = volume_utils.extract_host(volume.host, level='pool') + dest_vol_name = self._trans_name_down(volume.name) + src_pool = volume_utils.extract_host(src_vref.host, level='pool') + src_vol_name = self._trans_name_down(src_vref.name) + + method = 'block/lvm/clone' + request_type = 'post' + params = {'srcVolumeName': src_vol_name, + 'srcPoolName': src_pool, + 'destVolumeName': dest_vol_name, + 'destPoolName': dest_pool} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + if volume.size > src_vref.size: + self.extend_volume(volume, volume.size) + + @utils.trace + def extend_volume(self, volume, new_size): + """Extend volume to new size.""" + name = self._trans_name_down(volume.name) + if not self._check_volume(volume): + msg = _('Extend Volume Failed: Volume %s does not exist.') % name + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + size = new_size * units.Ki + pool = volume_utils.extract_host(volume.host, level='pool') + + method = 'block/lvm' + request_type = 'put' + params = {'pool': pool, + 'name': name, + 'newCapacity': size} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def delete_volume(self, volume): + """Delete volume from AS13000.""" + name = self._trans_name_down(volume.name) + if not self._check_volume(volume): + # if volume is not exist in backend, the driver will do + # nothing but log it + LOG.info('Tried to delete non-existent volume %(name)s.', + {'name': name}) + return + + pool = volume_utils.extract_host(volume.host, level='pool') + + method = 'block/lvm?pool=%s&lvm=%s' % (pool, name) + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @utils.trace + def create_snapshot(self, snapshot): + """Create snapshot of volume in backend. + + The snapshot type of AS13000 is copy-on-write. + """ + source_volume = snapshot.volume + volume_name = self._trans_name_down(source_volume.name) + if not self._check_volume(source_volume): + msg = (_('create_snapshot: Source_volume %s does not exist.') + % volume_name) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + pool = volume_utils.extract_host(source_volume.host, level='pool') + snapshot_name = self._trans_name_down(snapshot.name) + + method = 'snapshot/volume' + request_type = 'post' + params = {'snapName': snapshot_name, + 'volumeName': volume_name, + 'poolName': pool, + 'snapType': 'r'} + self._rest.send_rest_api(method=method, params=params, + request_type=request_type) + + @utils.trace + def delete_snapshot(self, snapshot): + """Delete snapshot of volume.""" + source_volume = snapshot.volume + volume_name = self._trans_name_down(source_volume.name) + if self._check_volume(source_volume) is False: + msg = (_('delete_snapshot: Source_volume %s does not exist.') + % volume_name) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + pool = volume_utils.extract_host(source_volume.host, level='pool') + snapshot_name = self._trans_name_down(snapshot.name) + + method = ('snapshot/volume?snapName=%s&volumeName=%s&poolName=%s' + % (snapshot_name, volume_name, pool)) + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @utils.trace + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If we haven't gotten stats yet or 'refresh' is True, + run update the stats first. + """ + if not self._stats or refresh: + self._update_volume_stats() + return self._stats + + @utils.trace + def _update_volume_stats(self): + """Update the backend stats including driver info and pools info.""" + + # As _update_volume_stats runs periodically, + # so we can do a check and refresh the token each time it runs. + time_difference = time.time() - self._token_time + if time_difference > self.token_available_time: + self._rest.login() + self._token_time = time.time() + LOG.debug('Token of the Driver has been refreshed.') + + # update the backend stats + data = {} + backend_name = self.configuration.safe_get('volume_backend_name') + data['vendor_name'] = self.VENDOR + data['driver_version'] = self.VERSION + data['storage_protocol'] = self.PROTOCOL + data['volume_backend_name'] = backend_name + data['pools'] = self._get_pools_stats() + + self._stats = data + LOG.debug('Update volume stats : %(stats)s.', {'stats': self._stats}) + + def _build_target_portal(self, ip, port): + """Build iSCSI portal for both IPV4 and IPV6.""" + addr = ipaddress.ip_address(ip) + if addr.version == 4: + ipaddr = ip + else: + ipaddr = '[%s]' % ip + return '%(ip)s:%(port)s' % {'ip': ipaddr, 'port': port} + + @utils.trace + def initialize_connection(self, volume, connector, **kwargs): + """Initialize connection steps: + + 1. check if the host exist in targets. + 2.1 if there is target that has the host, add the volume to the target. + 2.2 if not, create an target add host to host add volume to host. + 3. return the target info. + """ + host_ip = connector['ip'] + multipath = connector.get("multipath", False) + # Check if there host exist in targets + host_exist, target_name, node_of_target = self._get_target_from_conn( + host_ip) + if not host_exist: + # host doesn't exist, need create target and bind the host, + + # generate the target name + _TARGET_NAME_PATTERN = 'target.inspur.%(host)s-%(padding)s' + _padding = str(random.randint(0, 99999999)).zfill(8) + target_name = _TARGET_NAME_PATTERN % {'host': connector['host'], + 'padding': _padding} + + # decide the nodes to be used + if multipath: + node_of_target = [node['name'] for node in self.nodes] + else: + # single node + node_of_target = [self.nodes[0]['name']] + + # create the target + nodes = ','.join(node_of_target) + self._create_target(target_node=nodes, + target_name=target_name) + self._add_host_to_target(host_ip=host_ip, + target_name=target_name) + + self._add_lun_to_target(target_name=target_name, volume=volume) + if self.configuration.use_chap_auth: + self._add_chap_to_target(target_name, + self.configuration.chap_username, + self.configuration.chap_password) + + lun_id = self._get_lun_id(volume, target_name) + connection_data = { + 'target_discovered': True, + 'volume_id': volume.id, + } + + portals = [] + for node_name in node_of_target: + for node in self.nodes: + if node['name'] == node_name: + portal = self._build_target_portal(node.get('ip'), '3260') + portals.append(portal) + + if multipath: + connection_data.update({ + 'target_portals': portals, + 'target_luns': [int(lun_id)] * len(portals), + 'target_iqns': [target_name] * len(portals) + }) + else: + # single node + connection_data.update({ + 'target_portal': portals[0], + 'target_lun': int(lun_id), + 'target_iqn': target_name + }) + + if self.configuration.use_chap_auth: + connection_data['auth_method'] = 'CHAP' + connection_data['auth_username'] = self.configuration.chap_username + connection_data['auth_password'] = self.configuration.chap_password + + return {'driver_volume_type': 'iscsi', 'data': connection_data} + + @utils.trace + def terminate_connection(self, volume, connector, **kwargs): + """Delete lun from target. + + If target has no any lun, driver will delete the target. + """ + volume_name = self._trans_name_down(volume.name) + target_name = None + lun_id = None + + host_ip = None + if connector and 'ip' in connector: + host_ip = connector['ip'] + + target_list = self._get_target_list() + for target in target_list: + if not host_ip or host_ip in target['hostIp']: + for lun in target['lun']: + if volume_name == lun['lvm']: + target_name = target['name'] + lun_id = lun['lunID'] + break + if lun_id is not None: + break + if lun_id is None: + return + + self._delete_lun_from_target(target_name=target_name, + lun_id=lun_id) + luns = self._get_lun_list(target_name) + if not luns: + self._delete_target(target_name) + + def _get_pools_info(self, pools): + """Get the pools info.""" + method = 'block/pool?type=2' + requests_type = 'get' + pools_data = self._rest.send_rest_api(method=method, + request_type=requests_type) + pools_info = {} + for pool_data in pools_data: + if pool_data['name'] in pools: + pools_info[pool_data['name']] = pool_data + + return pools_info + + @utils.trace + def _get_pools_stats(self): + """Generate the pool stat information.""" + pools_info = self._get_pools_info(self.pools) + + pools = [] + for pool_info in pools_info.values(): + total_capacity = pool_info.get('totalCapacity') + total_capacity_gb = self._unit_convert(total_capacity) + used_capacity = pool_info.get('usedCapacity') + used_capacity_gb = self._unit_convert(used_capacity) + free_capacity_gb = total_capacity_gb - used_capacity_gb + + pool = { + 'pool_name': pool_info.get('name'), + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + } + pools.append(pool) + + return pools + + @utils.trace + def _get_target_from_conn(self, host_ip): + """Get target information base on the host ip.""" + host_exist = False + target_name = None + node = None + + target_list = self._get_target_list() + for target in target_list: + if host_ip in target['hostIp']: + host_exist = True + target_name = target['name'] + node = target['node'] + break + + return host_exist, target_name, node + + @utils.trace + def _get_target_list(self): + """Get a list of all targets in the backend.""" + method = 'block/target/detail' + request_type = 'get' + data = self._rest.send_rest_api(method=method, + request_type=request_type) + return data + + @utils.trace + def _create_target(self, target_name, target_node): + """Create a target on the specified node.""" + method = 'block/target' + request_type = 'post' + params = {'name': target_name, 'nodeName': target_node} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def _delete_target(self, target_name): + """Delete all target of all the node.""" + method = 'block/target?name=%s' % target_name + request_type = 'delete' + self._rest.send_rest_api(method=method, + request_type=request_type) + + @utils.trace + def _add_chap_to_target(self, target_name, chap_username, chap_password): + """Add CHAP to target.""" + method = 'block/chap/bond' + request_type = 'post' + params = {'target': target_name, + 'user': chap_username, + 'password': chap_password} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def _add_host_to_target(self, host_ip, target_name): + """Add the authority of host to target.""" + method = 'block/host' + request_type = 'post' + params = {'name': target_name, 'hostIp': host_ip} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + @utils.retry(exceptions=exception.VolumeDriverException, + interval=1, + retries=3) + def _add_lun_to_target(self, target_name, volume): + """Add volume to target.""" + pool = volume_utils.extract_host(volume.host, level='pool') + volume_name = self._trans_name_down(volume.name) + + method = 'block/lun' + request_type = 'post' + params = {'name': target_name, + 'pool': pool, + 'lvm': volume_name} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def _delete_lun_from_target(self, target_name, lun_id): + """Delete lun from target_name.""" + method = 'block/lun?name=%s&id=%s&force=1' % (target_name, lun_id) + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @utils.trace + def _get_lun_list(self, target_name): + """Get all lun list of the target.""" + method = 'block/lun?name=%s' % target_name + request_type = 'get' + return self._rest.send_rest_api(method=method, + request_type=request_type) + + @utils.trace + def _snapshot_lock_op(self, op, vol_name, snap_name, pool_name): + """Lock or unlock a snapshot to protect the snapshot. + + op is 'lock' for lock and 'unlock' for unlock + """ + method = 'snapshot/volume/%s' % op + request_type = 'post' + params = {'snapName': snap_name, + 'volumeName': vol_name, + 'poolName': pool_name} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def _filling_volume(self, name, pool): + """Filling a volume so that make it independently.""" + method = 'block/lvm/filling' + request_type = 'post' + params = {'pool': pool, 'name': name} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @utils.trace + def _wait_volume_filled(self, name, pool, attempts, interval): + """Wait until the volume is filled.""" + try_num = 0 + while try_num < attempts: + volumes = self._get_volumes(pool) + for vol in volumes: + if name == vol['name']: + if vol['lvmType'] == 1: + return True + else: + break + eventlet.sleep(interval) + try_num += 1 + return False + + @utils.trace + def _check_volume(self, volume): + """Check if the volume exists in the backend.""" + pool = volume_utils.extract_host(volume.host, 'pool') + volume_name = self._trans_name_down(volume.name) + attempts = 3 + while attempts > 0: + volumes = self._get_volumes(pool) + attempts -= 1 + for vol in volumes: + if volume_name == vol.get('name'): + return True + eventlet.sleep(1) + return False + + @utils.trace + def _get_volumes(self, pool): + """Get all the volumes in the pool.""" + method = 'block/lvm?pool=%s' % pool + request_type = 'get' + return self._rest.send_rest_api(method=method, + request_type=request_type) + + @utils.trace + def _get_cluster_status(self): + """Get all nodes of the backend.""" + method = 'cluster/node' + request_type = 'get' + return self._rest.send_rest_api(method=method, + request_type=request_type) + + @utils.trace + def _get_lun_id(self, volume, target_name): + """Get lun id of the voluem in a target.""" + pool = volume_utils.extract_host(volume.host, level='pool') + volume_name = self._trans_name_down(volume.name) + + lun_id = None + luns = self._get_lun_list(target_name) + for lun in luns: + mappinglvm = lun.get('mappingLvm') + lun_name = mappinglvm.replace(r'%s/' % pool, '') + if lun_name == volume_name: + lun_id = lun.get('id') + return lun_id + + def _trans_name_down(self, name): + """Legitimize the name. + + Because AS13000 volume name is only allowed letters, numbers, and '_'. + """ + return name.replace('-', '_') + + @utils.trace + def _unit_convert(self, capacity): + """Convert all units to GB. + + The capacity is a string in form like 100GB, 20TB, 100B, + this routine will convert to GB unit. + """ + capacity = capacity.upper() + try: + unit = re.findall(r'[A-Z]+', capacity)[0] + except BaseException: + unit = '' + capacity = float(capacity.replace(unit, '')) + + size_gb = 0.0 + + if unit in ['B', '']: + size_gb = capacity / units.Gi + elif unit in ['K', 'KB']: + size_gb = capacity / units.Mi + elif unit in ['M', 'MB']: + size_gb = capacity / units.Ki + elif unit in ['G', 'GB']: + size_gb = capacity + elif unit in ['T', 'TB']: + size_gb = capacity * units.Ki + elif unit in ['P', 'PB']: + size_gb = capacity * units.Mi + elif unit in ['E', 'EB']: + size_gb = capacity * units.Gi + + return float('%.0f' % size_gb) diff --git a/doc/source/configuration/block-storage/drivers/inspur-as13000-driver.rst b/doc/source/configuration/block-storage/drivers/inspur-as13000-driver.rst new file mode 100644 index 00000000000..6769e4d9185 --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/inspur-as13000-driver.rst @@ -0,0 +1,78 @@ +=================================== +Inspur AS13000 series volume driver +=================================== + +Inspur AS13000 series volume driver provides OpenStack Compute instances +with access to Inspur AS13000 series storage system. + +Inspur AS13000 storage can be used with iSCSI connection. + +This documentation explains how to configure and connect the block storage +nodes to Inspur AS13000 series storage. + +Driver options +~~~~~~~~~~~~~~ + +The following table contains the configuration options supported by the +Inspur AS13000 iSCSI driver. + +.. config-table:: + :config-target: Inspur AS13000 + + cinder.volume.drivers.inspur.as13000.as13000_driver + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create, list, delete, attach (map), and detach (unmap) volumes. +- Create, list and delete volume snapshots. +- Create a volume from a snapshot. +- Copy an image to a volume. +- Copy a volume to an image. +- Clone a volume. +- Extend a volume. + +Configure Inspur AS13000 iSCSI backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section details the steps required to configure the Inspur AS13000 +storage cinder driver. + +#. In the ``cinder.conf`` configuration file under the ``[DEFAULT]`` + section, set the enabled_backends parameter. + + .. code-block:: ini + + [DEFAULT] + enabled_backends = AS13000-1 + + +#. Add a backend group section for backend group specified + in the enabled_backends parameter. + +#. In the newly created backend group section, set the + following configuration options: + + .. code-block:: ini + + [AS13000-1] + # The driver path + volume_driver = cinder.volume.drivers.inspur.as13000.as13000_driver.AS13000Driver + # Management IP of Inspur AS13000 storage array + san_ip = 10.0.0.10 + # The Rest API port + san_api_port = 8088 + # Management username of Inspur AS13000 storage array + san_login = root + # Management password of Inspur AS13000 storage array + san_password = passw0rd + # The Pool used to allocated volumes + as13000_ipsan_pools = Pool0 + # The Meta Pool to use, should be a replication Pool + as13000_meta_pool = Pool_Rep + # Backend name + volume_backend_name = AS13000 + + +#. Save the changes to the ``/etc/cinder/cinder.conf`` file and + restart the ``cinder-volume`` service. diff --git a/doc/source/configuration/block-storage/volume-drivers.rst b/doc/source/configuration/block-storage/volume-drivers.rst index 313034de689..bfe74eac3f0 100644 --- a/doc/source/configuration/block-storage/volume-drivers.rst +++ b/doc/source/configuration/block-storage/volume-drivers.rst @@ -51,6 +51,7 @@ Driver Configuration Reference drivers/ibm-storage-volume-driver drivers/ibm-storwize-svc-driver drivers/infinidat-volume-driver + drivers/inspur-as13000-driver drivers/inspur-instorage-driver drivers/kaminario-driver drivers/lenovo-driver diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 6cfddc84eb2..0f2db482e76 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -102,6 +102,9 @@ title=Infinidat Storage Driver (iSCSI, FC) [driver.inspur] title=Inspur G2 Storage Driver (iSCSI, FC) +[driver.inspur_as13000] +title=Inspur AS13000 Storage Driver (iSCSI) + [driver.kaminario] title=Kaminario Storage Driver (iSCSI, FC) @@ -228,6 +231,7 @@ driver.ibm_gpfs=complete driver.ibm_storwize=complete driver.ibm_xiv=complete driver.inspur=complete +driver.inspur_as13000=complete driver.kaminario=complete driver.lenovo=complete driver.linbit_drbd=complete @@ -292,6 +296,7 @@ driver.ibm_gpfs=complete driver.ibm_storwize=complete driver.ibm_xiv=complete driver.inspur=complete +driver.inspur_as13000=complete driver.kaminario=complete driver.lenovo=complete driver.linbit_drbd=complete @@ -356,6 +361,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=complete driver.ibm_xiv=missing driver.inspur=complete +driver.inspur_as13000=missing driver.kaminario=missing driver.lenovo=missing driver.linbit_drbd=missing @@ -421,6 +427,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=complete driver.ibm_xiv=missing driver.inspur=complete +driver.inspur_as13000=missing driver.kaminario=missing driver.lenovo=missing driver.linbit_drbd=missing @@ -487,6 +494,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=complete driver.ibm_xiv=complete driver.inspur=complete +driver.inspur_as13000=missing driver.kaminario=complete driver.lenovo=missing driver.linbit_drbd=missing @@ -554,6 +562,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=complete driver.ibm_xiv=complete driver.inspur=complete +driver.inspur_as13000=missing driver.kaminario=missing driver.lenovo=missing driver.linbit_drbd=missing @@ -620,6 +629,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=missing driver.ibm_xiv=missing driver.inspur=missing +driver.inspur_as13000=complete driver.kaminario=complete driver.lenovo=missing driver.linbit_drbd=missing @@ -687,6 +697,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=missing driver.ibm_xiv=missing driver.inspur=missing +driver.inspur_as13000=missing driver.kaminario=missing driver.lenovo=missing driver.linbit_drbd=missing @@ -754,6 +765,7 @@ driver.ibm_gpfs=missing driver.ibm_storwize=complete driver.ibm_xiv=complete driver.inspur=missing +driver.inspur_as13000=complete driver.kaminario=missing driver.lenovo=missing driver.linbit_drbd=missing diff --git a/releasenotes/notes/inspur-as13000-cinder-driver-bfa5cc17683d87a9.yaml b/releasenotes/notes/inspur-as13000-cinder-driver-bfa5cc17683d87a9.yaml new file mode 100644 index 00000000000..ffe633d4d5b --- /dev/null +++ b/releasenotes/notes/inspur-as13000-cinder-driver-bfa5cc17683d87a9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + New Cinder volume driver for Inspur AS13000 series.