From a9fad35a20570e6ecd3757ea50e794a0592c3921 Mon Sep 17 00:00:00 2001 From: mayurindalkar Date: Tue, 30 Jan 2018 20:22:21 +0530 Subject: [PATCH] Add ISCSI driver for Veritas Access This driver implements all the minimum required features for cinder. Change-Id: I48b59a2ecab8b856547a4ae17e6e0fd04094475a Implement: blueprint veritas-access-cinder-iscsi-support --- cinder/opts.py | 3 + .../volume/drivers/veritas_access/__init__.py | 0 .../veritas_access/test_veritas_iscsi.py | 674 +++++++++++++ .../volume/drivers/veritas_access/__init__.py | 0 .../drivers/veritas_access/veritas_iscsi.py | 886 ++++++++++++++++++ .../drivers/veritas-access-iscsi-driver.rst | 89 ++ .../block-storage/volume-drivers.rst | 1 + ..._access_iscsi_driver-de642dad9e7d0890.yaml | 3 + 8 files changed, 1656 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/veritas_access/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/veritas_access/test_veritas_iscsi.py create mode 100644 cinder/volume/drivers/veritas_access/__init__.py create mode 100644 cinder/volume/drivers/veritas_access/veritas_iscsi.py create mode 100644 doc/source/configuration/block-storage/drivers/veritas-access-iscsi-driver.rst create mode 100644 releasenotes/notes/veritas_access_iscsi_driver-de642dad9e7d0890.yaml diff --git a/cinder/opts.py b/cinder/opts.py index 4a5e0685de2..7445b63b0d1 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -159,6 +159,8 @@ from cinder.volume.drivers import storpool as cinder_volume_drivers_storpool from cinder.volume.drivers.synology import synology_common as \ cinder_volume_drivers_synology_synologycommon from cinder.volume.drivers import tintri as cinder_volume_drivers_tintri +from cinder.volume.drivers.veritas_access import veritas_iscsi as \ + cinder_volume_drivers_veritas_access_veritasiscsi from cinder.volume.drivers.vmware import vmdk as \ cinder_volume_drivers_vmware_vmdk from cinder.volume.drivers import vzstorage as cinder_volume_drivers_vzstorage @@ -259,6 +261,7 @@ def list_opts(): cinder_volume_drivers_inspur_instorage_instorageiscsi. instorage_mcs_iscsi_opts, cinder_volume_drivers_storpool.storpool_opts, + cinder_volume_drivers_veritas_access_veritasiscsi.VA_VOL_OPTS, cinder_volume_manager.volume_manager_opts, cinder_wsgi_eventletserver.socket_opts, )), diff --git a/cinder/tests/unit/volume/drivers/veritas_access/__init__.py b/cinder/tests/unit/volume/drivers/veritas_access/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/veritas_access/test_veritas_iscsi.py b/cinder/tests/unit/volume/drivers/veritas_access/test_veritas_iscsi.py new file mode 100644 index 00000000000..e3def8ca905 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/veritas_access/test_veritas_iscsi.py @@ -0,0 +1,674 @@ +# Copyright 2017 Veritas Technologies LLC. +# +# 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 Veritas Access cinder driver. +""" +import hashlib +import json +import tempfile +from xml.dom.minidom import Document + +import mock +from oslo_config import cfg +import requests + +from cinder import context +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.veritas_access import veritas_iscsi + +CONF = cfg.CONF +FAKE_BACKEND = 'fake_backend' + + +class MockResponse(object): + def __init__(self): + self.status_code = 200 + + def json(self): + data = {'fake_key': 'fake_val'} + return json.dumps(data) + + +class FakeXML(object): + + def __init__(self): + self.tempdir = tempfile.mkdtemp() + + def create_vrts_fake_config_file(self): + + target = 'iqn.2017-02.com.veritas:faketarget' + portal = '1.1.1.1' + auth_detail = '0' + doc = Document() + + vrts_node = doc.createElement("VRTS") + doc.appendChild(vrts_node) + + vrts_target_node = doc.createElement("VrtsTargets") + vrts_node.appendChild(vrts_target_node) + + target_node = doc.createElement("Target") + + vrts_target_node.appendChild(target_node) + + name_ele = doc.createElement("Name") + portal_ele = doc.createElement("PortalIP") + auth_ele = doc.createElement("Authentication") + + name_ele.appendChild(doc.createTextNode(target)) + portal_ele.appendChild(doc.createTextNode(portal)) + auth_ele.appendChild(doc.createTextNode(auth_detail)) + + target_node.appendChild(name_ele) + target_node.appendChild(portal_ele) + target_node.appendChild(auth_ele) + + filename = 'vrts_config.xml' + config_file_path = self.tempdir + '/' + filename + + f = open(config_file_path, 'w') + doc.writexml(f) + f.close() + return config_file_path + + +class fake_volume(object): + def __init__(self): + self.id = 'fakeid' + self.name = 'fakename' + self.size = 1 + self.snapshot_id = False + self.metadata = {'dense': True} + + +class fake_volume2(object): + def __init__(self): + self.id = 'fakeid2' + self.name = 'fakename2' + self.size = 2 + self.snapshot_id = False + self.metadata = {'dense': True} + + +class fake_clone_volume(object): + def __init__(self): + self.id = 'fakecloneid' + self.name = 'fakeclonename' + self.size = 1 + self.snapshot_id = False + + +class fake_clone_volume2(object): + def __init__(self): + self.id = 'fakecloneid2' + self.name = 'fakeclonename' + self.size = 2 + self.snapshot_id = False + + +class fake_snapshot(object): + def __init__(self): + self.id = 'fakeid' + self.volume_id = 'fakevolumeid' + self.volume_size = 1 + + +class ACCESSIscsiDriverTestCase(test.TestCase): + """Tests ACCESSShareDriver.""" + + volume = fake_volume() + volume2 = fake_volume2() + snapshot = fake_snapshot() + clone_volume = fake_clone_volume() + clone_volume2 = fake_clone_volume2() + connector = { + 'initiator': 'iqn.1994-05.com.fakeinitiator' + } + + def setUp(self): + super(ACCESSIscsiDriverTestCase, self).setUp() + self._create_fake_config() + lcfg = self.configuration + self._context = context.get_admin_context() + self._driver = veritas_iscsi.ACCESSIscsiDriver(configuration=lcfg) + self._driver.do_setup(self._context) + + def _create_fake_config(self): + self.mock_object(veritas_iscsi.ACCESSIscsiDriver, + '_authenticate_access') + self.configuration = mock.Mock(spec=conf.Configuration) + self.configuration.safe_get = self.fake_safe_get + self.configuration.san_ip = '1.1.1.1' + self.configuration.san_login = 'user' + self.configuration.san_password = 'passwd' + self.configuration.san_api_port = 14161 + self.configuration.vrts_lun_sparse = True + self.configuration.vrts_target_config = ( + FakeXML().create_vrts_fake_config_file()) + self.configuration.target_port = 3260 + self.configuration.volume_backend_name = FAKE_BACKEND + + def fake_safe_get(self, value): + try: + val = getattr(self.configuration, value) + except AttributeError: + val = None + return val + + def test_create_volume(self): + self.mock_object(self._driver, '_vrts_get_suitable_target') + self.mock_object(self._driver, '_vrts_get_targets_store') + self.mock_object(self._driver, '_access_api') + + mylist = [] + target = {} + target['name'] = 'iqn.2017-02.com.veritas:faketarget' + target['portal_ip'] = '1.1.1.1' + target['auth'] = '0' + mylist.append(target) + + self._driver._vrts_get_suitable_target.return_value = ( + 'iqn.2017-02.com.veritas:faketarget') + self._driver._access_api.return_value = True + return_list = self._driver._vrts_parse_xml_file( + self.configuration.vrts_target_config) + + self._driver.create_volume(self.volume) + + self.assertEqual(mylist, return_list) + self.assertEqual(2, self._driver._access_api.call_count) + + def test_create_volume_negative(self): + self.mock_object(self._driver, '_vrts_get_suitable_target') + self.mock_object(self._driver, '_vrts_get_targets_store') + self.mock_object(self._driver, '_access_api') + + self._driver._vrts_get_suitable_target.return_value = ( + 'iqn.2017-02.com.veritas:faketarget') + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_volume, + self.volume) + + def test_create_volume_negative_no_suitable_target_found(self): + self.mock_object(self._driver, '_vrts_get_suitable_target') + self.mock_object(self._driver, '_access_api') + + self._driver._vrts_get_suitable_target.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_volume, + self.volume) + self.assertEqual(0, self._driver._access_api.call_count) + + def test_delete_volume(self): + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_access_api') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + length = len(self.volume.id) + index = int(length / 2) + name1 = self.volume.id[:index] + name2 = self.volume.id[index:] + crc1 = hashlib.md5(name1.encode('utf-8')).hexdigest()[:8] + crc2 = hashlib.md5(name2.encode('utf-8')).hexdigest()[:8] + + volume_name_to_ret = crc1 + '-' + crc2 + + lun = {} + lun['lun_name'] = va_lun_name + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + self._driver._get_vrts_lun_list.return_value = lun_list + + self._driver._access_api.return_value = True + + self._driver.delete_volume(self.volume) + self.assertEqual(volume_name_to_ret, va_lun_name) + self.assertEqual(2, self._driver._access_api.call_count) + + def test_delete_volume_negative(self): + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_access_api') + + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.delete_volume, + self.volume) + + def test_create_snapshot(self): + self.mock_object(self._driver, '_access_api') + + self._driver._access_api.return_value = True + + self._driver.create_snapshot(self.snapshot) + self.assertEqual(2, self._driver._access_api.call_count) + + def test_create_snapshot_negative(self): + self.mock_object(self._driver, '_access_api') + + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_snapshot, + self.snapshot) + + self.assertEqual(1, self._driver._access_api.call_count) + + def test_delete_snapshot(self): + self.mock_object(self._driver, '_access_api') + + self._driver._access_api.return_value = True + + self._driver.delete_snapshot(self.snapshot) + self.assertEqual(2, self._driver._access_api.call_count) + + def test_delete_snapshot_negative(self): + self.mock_object(self._driver, '_access_api') + + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.delete_snapshot, + self.snapshot) + + self.assertEqual(1, self._driver._access_api.call_count) + + def test_create_cloned_volume(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_vrts_extend_lun') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver._access_api.return_value = True + + self._driver.create_cloned_volume(self.clone_volume, self.volume) + self.assertEqual(2, self._driver._access_api.call_count) + self.assertEqual(0, self._driver._vrts_extend_lun.call_count) + + def test_create_cloned_volume_of_greater_size(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_vrts_extend_lun') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver._access_api.return_value = True + + self._driver.create_cloned_volume(self.clone_volume2, self.volume) + self.assertEqual(2, self._driver._access_api.call_count) + self.assertEqual(1, self._driver._vrts_extend_lun.call_count) + + def test_create_cloned_volume_negative(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_cloned_volume, + self.clone_volume, self.volume) + + self.assertEqual(1, self._driver._access_api.call_count) + + def test_create_volume_from_snapshot(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_vrts_extend_lun') + self.mock_object(self._driver, '_vrts_get_targets_store') + self.mock_object(self._driver, '_vrts_get_assigned_store') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + snap_name = self._driver._get_va_lun_name(self.snapshot.id) + + snap = {} + snap['snapshot_name'] = snap_name + snap['target_name'] = 'fake_target' + + snapshots = [] + snapshots.append(snap) + + snap_info = {} + snap_info['output'] = {'output': {'snapshots': snapshots}} + + self._driver._access_api.return_value = snap_info + + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver.create_volume_from_snapshot(self.volume, self.snapshot) + self.assertEqual(3, self._driver._access_api.call_count) + self.assertEqual(0, self._driver._vrts_extend_lun.call_count) + + def test_create_volume_from_snapshot_of_greater_size(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_vrts_extend_lun') + self.mock_object(self._driver, '_vrts_get_targets_store') + self.mock_object(self._driver, '_vrts_get_assigned_store') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + snap_name = self._driver._get_va_lun_name(self.snapshot.id) + + snap = {} + snap['snapshot_name'] = snap_name + snap['target_name'] = 'fake_target' + + snapshots = [] + snapshots.append(snap) + + snap_info = {} + snap_info['output'] = {'output': {'snapshots': snapshots}} + + self._driver._access_api.return_value = snap_info + + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver.create_volume_from_snapshot(self.volume2, self.snapshot) + self.assertEqual(3, self._driver._access_api.call_count) + self.assertEqual(1, self._driver._vrts_extend_lun.call_count) + + def test_create_volume_from_snapshot_negative(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_vrts_get_targets_store') + + snap = {} + snap['snapshot_name'] = 'fake_snap_name' + snap['target_name'] = 'fake_target' + + snapshots = [] + snapshots.append(snap) + + snap_info = {} + snap_info['output'] = {'output': {'snapshots': snapshots}} + + self._driver._access_api.return_value = snap_info + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.create_volume_from_snapshot, + self.volume, self.snapshot) + + self.assertEqual(1, self._driver._access_api.call_count) + self.assertEqual(0, self._driver._vrts_get_targets_store.call_count) + + def test_extend_volume(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver._access_api.return_value = True + + self._driver.extend_volume(self.volume, 2) + self.assertEqual(1, self._driver._access_api.call_count) + + def test_extend_volume_negative(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + self.mock_object(self._driver, '_vrts_is_space_available_in_store') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + self._driver._vrts_is_space_available_in_store.return_value = True + + self._driver._access_api.return_value = False + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.extend_volume, self.volume, 2) + self.assertEqual(1, self._driver._vrts_get_fs_list.call_count) + self.assertEqual(1, self._driver._access_api.call_count) + + def test_extend_volume_negative_not_volume_found(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_get_fs_list') + + lun = {} + lun['lun_name'] = 'fake_lun' + lun['fs_name'] = 'fake_fs' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.extend_volume, self.volume, 2) + + self.assertEqual(0, self._driver._vrts_get_fs_list.call_count) + self.assertEqual(0, self._driver._access_api.call_count) + + def test_initialize_connection(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_target_initiator_mapping') + self.mock_object(self._driver, '_vrts_get_iscsi_properties') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + + lun = {} + lun['lun_name'] = va_lun_name + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + + self._driver._get_vrts_lun_list.return_value = lun_list + self._driver._access_api.return_value = True + self._driver.initialize_connection(self.volume, self.connector) + self.assertEqual(1, self._driver._vrts_get_iscsi_properties.call_count) + + def test_initialize_connection_negative(self): + self.mock_object(self._driver, '_access_api') + self.mock_object(self._driver, '_get_vrts_lun_list') + self.mock_object(self._driver, '_vrts_target_initiator_mapping') + self.mock_object(self._driver, '_vrts_get_iscsi_properties') + + lun = {} + lun['lun_name'] = 'fakelun' + lun['target_name'] = 'iqn.2017-02.com.veritas:faketarget' + lun_list = {'output': {'output': {'luns': [lun]}}} + self._driver.LUN_FOUND_INTERVAL = 5 + + self._driver._get_vrts_lun_list.return_value = lun_list + self._driver._access_api.return_value = True + + self.assertRaises(exception.VolumeBackendAPIException, + self._driver.initialize_connection, self.volume, + self.connector) + + self.assertEqual( + 0, self._driver._vrts_target_initiator_mapping.call_count) + self.assertEqual(0, self._driver._vrts_get_iscsi_properties.call_count) + + def test___vrts_get_iscsi_properties(self): + self.mock_object(self._driver, '_access_api') + + va_lun_name = self._driver._get_va_lun_name(self.volume.id) + storage_object = "'/fakestores/fakeio/" + va_lun_name + "'" + + lun_id_list = {} + lun_id_list['output'] = ("[{'storage_object': " + + storage_object + ", 'index': '1'}]") + + target_name = 'iqn.2017-02.com.veritas:faketarget' + + self._driver._access_api.return_value = lun_id_list + + iscsi_properties_ret_value = {} + iscsi_properties_ret_value['target_discovered'] = True + iscsi_properties_ret_value['target_iqn'] = target_name + iscsi_properties_ret_value['target_portal'] = '1.1.1.1:3260' + iscsi_properties_ret_value['target_lun'] = 1 + iscsi_properties_ret_value['volume_id'] = 'fakeid' + iscsi_properties = self._driver._vrts_get_iscsi_properties(self.volume, + target_name) + + self.assertEqual(iscsi_properties_ret_value, iscsi_properties) + + def test__access_api(self): + self.mock_object(requests, 'session') + + provider = '%s:%s' % (self._driver._va_ip, self._driver._port) + path = '/fake/path' + input_data = {} + mock_response = MockResponse() + session = requests.session + + data = {'fake_key': 'fake_val'} + json_data = json.dumps(data) + + session.request.return_value = mock_response + ret_value = self._driver._access_api(session, provider, path, + json.dumps(input_data), 'GET') + + self.assertEqual(json_data, ret_value) + + def test__access_api_ret_for_update_object(self): + self.mock_object(requests, 'session') + + provider = '%s:%s' % (self._driver._va_ip, self._driver._port) + path = self._driver._update_object + input_data = None + mock_response = MockResponse() + session = requests.session + + session.request.return_value = mock_response + ret = self._driver._access_api(session, provider, path, + input_data, 'GET') + + self.assertTrue(ret) + + def test__access_api_negative(self): + session = self._driver.session + provider = '%s:%s' % (self._driver._va_ip, self._driver._port) + path = '/fake/path' + input_data = {} + ret_value = self._driver._access_api(session, provider, path, + json.dumps(input_data), 'GET') + self.assertEqual(False, ret_value) + + def test__get_api(self): + provider = '%s:%s' % (self._driver._va_ip, self._driver._port) + tail = '/fake/path' + ret = self._driver._get_api(provider, tail) + + api_root = 'https://%s/api/access' % (provider) + to_be_ret = api_root + tail + self.assertEqual(to_be_ret, ret) + + def test__vrts_target_initiator_mapping_negative(self): + self.mock_object(self._driver, '_access_api') + target_name = 'fake_target' + initiator_name = 'fake_initiator' + + self._driver._access_api.return_value = False + self.assertRaises(exception.VolumeBackendAPIException, + self._driver._vrts_target_initiator_mapping, + target_name, initiator_name) + + def test_get_volume_stats(self): + self.mock_object(self._driver, '_authenticate_access') + self.mock_object(self._driver, '_vrts_get_targets_store') + self.mock_object(self._driver, '_vrts_get_fs_list') + + target_list = [] + target_details = {} + target_details['fs_list'] = ['fs1'] + target_details['wwn'] = 'iqn.2017-02.com.veritas:faketarget' + target_list.append(target_details) + + self._driver._vrts_get_targets_store.return_value = target_list + + fs_list = [] + fs_dict = {} + fs_dict['name'] = 'fs1' + fs_dict['file_storage_capacity'] = 10737418240 + fs_dict['file_storage_used'] = 1073741824 + fs_list.append(fs_dict) + + self._driver._vrts_get_fs_list.return_value = fs_list + + self._driver.get_volume_stats() + data = { + 'volume_backend_name': FAKE_BACKEND, + 'vendor_name': 'Veritas', + 'driver_version': '1.0', + 'storage_protocol': 'iSCSI', + 'total_capacity_gb': 10, + 'free_capacity_gb': 9, + 'reserved_percentage': 0, + 'thin_provisioning_support': True + } + + self.assertEqual(data, self._driver._stats) diff --git a/cinder/volume/drivers/veritas_access/__init__.py b/cinder/volume/drivers/veritas_access/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/veritas_access/veritas_iscsi.py b/cinder/volume/drivers/veritas_access/veritas_iscsi.py new file mode 100644 index 00000000000..e6dfcc08f78 --- /dev/null +++ b/cinder/volume/drivers/veritas_access/veritas_iscsi.py @@ -0,0 +1,886 @@ +# Copyright 2017 Veritas Technologies LLC. +# +# 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. +""" +Veritas Access Driver for ISCSI. + +""" +import hashlib +import json +from random import randint + +from defusedxml import minidom +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import loopingcall +from oslo_utils import netutils +from oslo_utils import strutils +from oslo_utils import units +import requests +import requests.auth +from six.moves import http_client + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder.volume import driver +from cinder.volume.drivers.san import san + +LOG = logging.getLogger(__name__) + + +VA_VOL_OPTS = [ + cfg.BoolOpt('vrts_lun_sparse', + default=True, + help='Create sparse Lun.'), + cfg.StrOpt('vrts_target_config', + default='/etc/cinder/vrts_target.xml', + help='VA config file.') +] + + +CONF = cfg.CONF +CONF.register_opts(VA_VOL_OPTS) + + +class NoAuth(requests.auth.AuthBase): + """This is a 'authentication' handler. + + It exists for use with custom authentication systems, such as the + one for the Access API, it simply passes the Authorization header as-is. + + The default authentication handler for requests will clobber the + Authorization header. + """ + + def __call__(self, r): + return r + + +@interface.volumedriver +class ACCESSIscsiDriver(driver.ISCSIDriver): + """ACCESS Share Driver. + + Executes commands relating to ACCESS ISCSI. + Supports creation of volumes on ACCESS. + + API version history: + + 1.0 - Initial version. + """ + + VERSION = "1.0" + # ThirdPartySytems wiki page + CI_WIKI_NAME = "Veritas_Access_CI" + DRIVER_VOLUME_TYPE = 'iSCSI' + LUN_FOUND_INTERVAL = 30 # seconds + + def __init__(self, *args, **kwargs): + # Parent sets db, host, _execute and base config + super(ACCESSIscsiDriver, self).__init__(*args, **kwargs) + + self._va_ip = None + self._port = None + self._user = None + self._pwd = None + self.iscsi_port = None + self._fs_list_str = '/fs' + self._target_list_str = '/iscsi/target/list' + self._target_status = '/iscsi/target/status' + self._lun_create_str = '/iscsi/lun/create' + self._lun_destroy_str = '/iscsi/lun/destroy' + self._lun_list_str = '/iscsi/lun/list' + self._lun_create_from_snap_str = '/iscsi/lun_from_snap/create' + self._snapshot_create_str = '/iscsi/lun/snapshot/create' + self._snapshot_destroy_str = '/iscsi/lun/snapshot/destroy' + self._snapshot_list_str = '/iscsi/lun/snapshot/list' + self._lun_clone_create_str = '/iscsi/lun/clone/create' + self._lun_extend_str = '/iscsi/lun/growto' + self._lun_shrink_str = '/iscsi/lun/shrinkto' + self._lun_getid_str = '/iscsi/lun/getlunid' + self._target_map_str = '/iscsi/target/map/add' + self._update_object = '/objecttags' + + self.configuration.append_config_values(VA_VOL_OPTS) + self.configuration.append_config_values(san.san_opts) + self.backend_name = (self.configuration.safe_get('volume_backend_name') + or 'ACCESS_ISCSI') + self.verify = (self.configuration. + safe_get('driver_ssl_cert_verify') or False) + + if self.verify: + verify_path = (self.configuration. + safe_get('driver_ssl_cert_path') or None) + if verify_path: + self.verify = verify_path + + def do_setup(self, context): + """Any initialization the volume driver does while starting.""" + super(ACCESSIscsiDriver, self).do_setup(context) + + required_config = ['san_ip', + 'san_login', + 'san_password', + 'san_api_port'] + + for attr in required_config: + if not getattr(self.configuration, attr, None): + message = (_('config option %s is not set.') % attr) + raise exception.InvalidInput(message=message) + + self._va_ip = self.configuration.san_ip + self._user = self.configuration.san_login + self._pwd = self.configuration.san_password + self._port = self.configuration.san_api_port + self._sparse_lun_support = self.configuration.vrts_lun_sparse + self.target_info_file = self.configuration.vrts_target_config + self.iscsi_port = self.configuration.target_port + self.session = self._authenticate_access(self._va_ip, self._user, + self._pwd) + + def _get_va_lun_name(self, name): + length = len(name) + index = int(length / 2) + name1 = name[:index] + name2 = name[index:] + crc1 = hashlib.md5(name1.encode('utf-8')).hexdigest()[:8] + crc2 = hashlib.md5(name2.encode('utf-8')).hexdigest()[:8] + return crc1 + '-' + crc2 + + def check_for_setup_error(self): + """Check if veritas access target is online.""" + target_list = self._vrts_parse_xml_file(self.target_info_file) + + path = self._target_status + provider = '%s:%s' % (self._va_ip, self._port) + + for target in target_list: + target_name = target['name'] + data = {} + data["name"] = target_name + + output = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + + target_status = output['output']['header']['messages'][0] + + if 'ONLINE' not in target_status: + message = (_('ACCESSIscsiDriver setup error as %s ' + 'target is offline') % target_name) + raise exception.VolumeBackendAPIException(message=message) + + def create_export(self, context, volume, connector): + """Driver entry point to get the export info for a new volume.""" + pass + + def remove_export(self, context, volume): + """Driver entry point to remove an export for a volume.""" + pass + + def ensure_export(self, context, volume): + """Driver entry point to get the export info for an existing volume.""" + pass + + def _vrts_get_iscsi_properties(self, volume, target_name): + """Get target and LUN details.""" + lun_name = self._get_va_lun_name(volume.id) + + data = {} + path = self._lun_getid_str + provider = '%s:%s' % (self._va_ip, self._port) + + lun_id_list = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + + for lun in eval(lun_id_list['output']): + vrts_lun_name = lun['storage_object'].split('/')[3] + if vrts_lun_name == lun_name: + lun_id = int(lun['index']) + + target_list = self._vrts_parse_xml_file(self.target_info_file) + authentication = False + portal_ip = "" + + for target in target_list: + if target_name == target['name']: + portal_ip = target['portal_ip'] + if target['auth'] == '1': + auth_user = target['auth_user'] + auth_password = target['auth_password'] + authentication = True + break + + if portal_ip == "": + message = (_('ACCESSIscsiDriver initialize_connection ' + 'failed for %s as no portal ip was found') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + portal_list = portal_ip.split(',') + + target_portal_list = [] + for ip in portal_list: + if netutils.is_valid_ipv6(ip): + target_portal_list.append('[%s]:%s' % (ip, + str(self.iscsi_port))) + else: + target_portal_list.append('%s:%s' % (ip, str(self.iscsi_port))) + + iscsi_properties = {} + iscsi_properties['target_discovered'] = True + iscsi_properties['target_iqn'] = target_name + iscsi_properties['target_portal'] = target_portal_list[0] + if len(target_portal_list) > 1: + iscsi_properties['target_portals'] = target_portal_list + iscsi_properties['target_lun'] = lun_id + iscsi_properties['volume_id'] = volume.id + if authentication: + iscsi_properties['auth_username'] = auth_user + iscsi_properties['auth_password'] = auth_password + iscsi_properties['auth_method'] = 'CHAP' + + return iscsi_properties + + def _get_vrts_lun_list(self): + """Get Lun list.""" + data = {} + path = self._lun_list_str + provider = '%s:%s' % (self._va_ip, self._port) + + lun_list = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + + return lun_list + + def _vrts_target_initiator_mapping(self, target_name, initiator_name): + """Map target to initiator.""" + path = self._target_map_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["target_name"] = target_name + data["initiator_name"] = initiator_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver target-initiator mapping ' + 'failed for target %s') + % target_name) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + def initialize_connection(self, volume, connector, initiator_data=None): + """Initializes the connection and returns connection info. + + The iscsi driver returns a driver_volume_type of 'iscsi'. + the format of the driver data is defined in _vrts_get_iscsi_properties. + Example return value:: + + { + 'driver_volume_type': 'iscsi' + 'data': { + 'target_discovered': True, + 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001', + 'target_portal': '127.0.0.0.1:3260', + 'target_lun': 1, + 'volume_id': '12345678-1234-4321-1234-123456789012', + } + } + """ + lun_name = self._get_va_lun_name(volume.id) + target = {'target_name': ''} + + def _inner(): + lun_list = self._get_vrts_lun_list() + for lun in lun_list['output']['output']['luns']: + if lun['lun_name'] == lun_name: + target['target_name'] = lun['target_name'] + raise loopingcall.LoopingCallDone() + + timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(_inner) + try: + timer.start(interval=5, timeout=self.LUN_FOUND_INTERVAL).wait() + except loopingcall.LoopingCallTimeOut: + message = (_('ACCESSIscsiDriver initialize_connection ' + 'failed for %s as no target was found') + % volume.id) + + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + self._vrts_target_initiator_mapping(target['target_name'], + connector['initiator']) + + iscsi_properties = self._vrts_get_iscsi_properties( + volume, target['target_name']) + + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties + } + + def terminate_connection(self, volume, connector, **kwargs): + """Disallow connection from connector.""" + pass + + def _vrts_parse_xml_file(self, filename): + """VRTS target info. + + + + + iqn.2017-02.com.veritas:target03 + 10.182.174.188 + + + iqn.2017-02.com.veritas:target04 + 10.182.174.189 + + + + + :param filename: the configuration file + :returns: list + """ + myfile = open(filename, 'r') + data = myfile.read() + myfile.close() + dom = minidom.parseString(data) + + mylist = [] + target = {} + + try: + for trg in dom.getElementsByTagName('Target'): + target['name'] = (trg.getElementsByTagName('Name')[0] + .childNodes[0].nodeValue) + target['portal_ip'] = (trg.getElementsByTagName('PortalIP')[0] + .childNodes[0].nodeValue) + target['auth'] = (trg.getElementsByTagName('Authentication')[0] + .childNodes[0].nodeValue) + if target['auth'] == '1': + target['auth_user'] = (trg.getElementsByTagName + ('Auth_username')[0] + .childNodes[0].nodeValue) + target['auth_password'] = (trg.getElementsByTagName + ('Auth_password')[0] + .childNodes[0].nodeValue) + + mylist.append(target) + target = {} + except IndexError: + pass + + return mylist + + def _vrts_get_fs_list(self): + """Get FS list.""" + path = self._fs_list_str + provider = '%s:%s' % (self._va_ip, self._port) + data = {} + fs_list = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + return fs_list + + def _vrts_get_targets_store(self): + """Get target and its store list.""" + path = self._target_list_str + provider = '%s:%s' % (self._va_ip, self._port) + data = {} + target_list = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + + return target_list['output']['output']['targets'] + + def _vrts_get_assigned_store(self, target, vrts_target_list): + """Get the store mapped to given target.""" + for vrts_target in vrts_target_list: + if vrts_target['wwn'] == target: + return vrts_target['fs_list'][0] + + def _vrts_is_space_available_in_store(self, vol_size, store_name, fs_list): + """Check whether space is available on store.""" + if self._sparse_lun_support: + return True + + for fs in fs_list: + if fs['name'] == store_name: + fs_avilable_space = (int(fs['file_storage_capacity']) - + int(fs['file_storage_used'])) + free_space = fs_avilable_space / units.Gi + + if free_space > vol_size: + return True + break + return False + + def _vrts_get_suitable_target(self, target_list, vol_size): + """Get a suitable target for lun creation. + + Picking random target at first, if space is not available + in first selected target then check each target one by one + for suitable one. + """ + + target_count = len(target_list) + + incrmnt_pointer = 0 + target_index = randint(0, (target_count - 1)) + + fs_list = self._vrts_get_fs_list() + + vrts_target_list = self._vrts_get_targets_store() + + store_name = self._vrts_get_assigned_store( + target_list[target_index]['name'], + vrts_target_list + ) + + if not self._vrts_is_space_available_in_store( + vol_size, store_name, fs_list): + while (incrmnt_pointer != target_count - 1): + target_index = (target_index + 1) % target_count + store_name = self._vrts_get_assigned_store( + target_list[target_index]['name'], + vrts_target_list + ) + if self._vrts_is_space_available_in_store( + vol_size, store_name, fs_list): + return target_list[target_index]['name'] + incrmnt_pointer = incrmnt_pointer + 1 + else: + return target_list[target_index]['name'] + + return False + + def create_volume(self, volume): + """Creates a Veritas Access Iscsi LUN.""" + create_dense = False + if 'dense' in volume.metadata.keys(): + create_dense = strutils.bool_from_string( + volume.metadata['dense']) + + lun_name = self._get_va_lun_name(volume.id) + lun_size = '%sg' % volume.size + path = self._lun_create_str + provider = '%s:%s' % (self._va_ip, self._port) + + target_list = self._vrts_parse_xml_file(self.target_info_file) + + target_name = self._vrts_get_suitable_target(target_list, volume.size) + + if not target_name: + message = (_('ACCESSIscsiDriver create volume failed %s ' + 'as no space is available') % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + data = {} + data["lun_name"] = lun_name + data["target_name"] = target_name + data["size"] = lun_size + if not self._sparse_lun_support or create_dense: + data["option"] = "option=dense" + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver create volume failed %s') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + data2 = {"type": "LUN", "key": "cinder_iscsi"} + data2["id"] = lun_name + data2["value"] = 'cinder_lun' + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'POST') + + def delete_volume(self, volume): + """Deletes a Veritas Access Iscsi LUN.""" + lun_name = self._get_va_lun_name(volume.id) + lun_list = self._get_vrts_lun_list() + target_name = "" + + for lun in lun_list['output']['output']['luns']: + if lun['lun_name'] == lun_name: + target_name = lun['target_name'] + + path = self._lun_destroy_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["target_name"] = target_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver delete volume failed %s') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + data2 = {"type": "LUN", "key": "cinder_iscsi"} + data2["id"] = lun_name + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'DELETE') + + def create_snapshot(self, snapshot): + """Creates a snapshot of LUN.""" + lun_name = self._get_va_lun_name(snapshot.volume_id) + snap_name = self._get_va_lun_name(snapshot.id) + path = self._snapshot_create_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["snap_name"] = snap_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver create snapshot failed for %s') + % snapshot.volume_id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + data2 = {"type": "LUN_SNAP", "key": "cinder_iscsi"} + data2["id"] = snap_name + data2["value"] = 'cinder_lun_snap' + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'POST') + + def delete_snapshot(self, snapshot): + """Deletes a snapshot of LUN.""" + lun_name = self._get_va_lun_name(snapshot.volume_id) + snap_name = self._get_va_lun_name(snapshot.id) + path = self._snapshot_destroy_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["snap_name"] = snap_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver delete snapshot failed for %s') + % snapshot.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + data2 = {"type": "LUN_SNAP", "key": "cinder_iscsi"} + data2["id"] = snap_name + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'DELETE') + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the volume.""" + lun_name = self._get_va_lun_name(src_vref.id) + cloned_lun_name = self._get_va_lun_name(volume.id) + + lun_found = False + + lun_list = self._get_vrts_lun_list() + for lun in lun_list['output']['output']['luns']: + if lun['lun_name'] == lun_name: + store_name = lun['fs_name'] + lun_found = True + break + + if not lun_found: + message = (_('ACCESSIscsiDriver create cloned volume ' + 'failed %s as no source volume found') % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + fs_list = self._vrts_get_fs_list() + + if not self._vrts_is_space_available_in_store(volume.size, store_name, + fs_list): + message = (_('ACCESSIscsiDriver create cloned volume ' + 'failed %s as no space is available') % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + path = self._lun_clone_create_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["clone_name"] = cloned_lun_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver create cloned ' + 'volume failed for %s') + % src_vref.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + if volume.size > src_vref.size: + self._vrts_extend_lun(volume, volume.size) + + data2 = {"type": "LUN", "key": "cinder_iscsi"} + data2["id"] = cloned_lun_name + data2["value"] = 'cinder_lun' + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'POST') + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from snapshot.""" + LOG.debug('ACCESSIscsiDriver create_volume_from_snapshot called') + + lun_name = self._get_va_lun_name(volume.id) + snap_name = self._get_va_lun_name(snapshot.id) + + path = self._snapshot_list_str + provider = '%s:%s' % (self._va_ip, self._port) + data = {} + data["snap_name"] = snap_name + snap_info = self._access_api(self.session, provider, path, + json.dumps(data), 'GET') + + target_name = "" + for snap in snap_info['output']['output']['snapshots']: + if snap['snapshot_name'] == snap_name: + target_name = snap['target_name'] + + if target_name == "": + message = (_('ACCESSIscsiDriver create volume from snapshot ' + 'failed for volume %s as failed to gather ' + 'snapshot details') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + vrts_target_list = self._vrts_get_targets_store() + store_name = self._vrts_get_assigned_store( + target_name, vrts_target_list) + + fs_list = self._vrts_get_fs_list() + + if not self._vrts_is_space_available_in_store(volume.size, store_name, + fs_list): + message = (_('ACCESSIscsiDriver create volume from snapshot ' + 'failed %s as no space is available') % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + path = self._lun_create_from_snap_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["snap_name"] = snap_name + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + + if not result: + message = (_('ACCESSIscsiDriver create volume from snapshot ' + 'failed for volume %s') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + if volume.size > snapshot.volume_size: + self._vrts_extend_lun(volume, volume.size) + + data2 = {"type": "LUN", "key": "cinder_iscsi"} + data2["id"] = lun_name + data2["value"] = 'cinder_lun' + path = self._update_object + result = self._access_api(self.session, provider, path, + json.dumps(data2), 'POST') + + def _vrts_extend_lun(self, volume, size): + """Extend vrts LUN to given size.""" + lun_name = self._get_va_lun_name(volume.id) + target = {'target_name': ''} + + def _inner(): + lun_list = self._get_vrts_lun_list() + for lun in lun_list['output']['output']['luns']: + if lun['lun_name'] == lun_name: + target['target_name'] = lun['target_name'] + raise loopingcall.LoopingCallDone() + + timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(_inner) + + try: + timer.start(interval=5, timeout=self.LUN_FOUND_INTERVAL).wait() + except loopingcall.LoopingCallTimeOut: + return False + + lun_size = '%sg' % size + path = self._lun_extend_str + provider = '%s:%s' % (self._va_ip, self._port) + + data = {} + data["lun_name"] = lun_name + data["target_name"] = target['target_name'] + data["size"] = lun_size + + result = self._access_api(self.session, provider, path, + json.dumps(data), 'POST') + return result + + def extend_volume(self, volume, size): + """Extend the volume to new size""" + lun_name = self._get_va_lun_name(volume.id) + lun_found = False + + lun_list = self._get_vrts_lun_list() + for lun in lun_list['output']['output']['luns']: + if lun['lun_name'] == lun_name: + store_name = lun['fs_name'] + lun_found = True + break + + if not lun_found: + message = (_('ACCESSIscsiDriver extend volume ' + 'failed %s as no volume found at backend') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + fs_list = self._vrts_get_fs_list() + + if not self._vrts_is_space_available_in_store(size, store_name, + fs_list): + message = (_('ACCESSIscsiDriver extend volume ' + 'failed %s as no space is available') % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + result = self._vrts_extend_lun(volume, size) + + if not result: + message = (_('ACCESSIscsiDriver extend ' + 'volume failed for %s') + % volume.id) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + def _get_api(self, provider, tail): + api_root = 'https://%s/api/access' % (provider) + if tail == self._fs_list_str or tail == self._update_object: + api_root = 'https://%s/api' % (provider) + + return api_root + tail + + def _access_api(self, session, provider, path, input_data, method): + """Returns False if failure occurs.""" + kwargs = {'data': input_data} + if not isinstance(input_data, dict): + kwargs['headers'] = {'Content-Type': 'application/json'} + full_url = self._get_api(provider, path) + response = session.request(method, full_url, **kwargs) + if path == self._update_object: + return True + if response.status_code != http_client.OK: + LOG.error('Access API operation failed with HTTP error code %s.', + str(response.status_code)) + return False + result = response.json() + return result + + def _authenticate_access(self, address, username, password): + session = requests.session() + session.verify = self.verify + session.auth = NoAuth() + + # Here 'address' will be only IPv4. + response = session.post('https://%s:%s/api/rest/authenticate' + % (address, self._port), + data={'username': username, + 'password': password}) + if response.status_code != http_client.OK: + LOG.error('Failed to authenticate to remote cluster at %s as %s.', + address, username) + raise exception.NotAuthorized(_('Authentication failure.')) + result = response.json() + session.headers.update({'Authorization': 'Bearer {}' + .format(result['token'])}) + session.headers.update({'Content-Type': 'application/json'}) + + return session + + def _get_va_backend_capacity(self): + """Get VA backend total and free capacity.""" + target_list = self._vrts_parse_xml_file(self.target_info_file) + fs_list = self._vrts_get_fs_list() + vrts_target_list = self._vrts_get_targets_store() + + total_space = 0 + free_space = 0 + + target_name = [] + target_store = [] + + for target in target_list: + target_name.append(target['name']) + + for target in vrts_target_list: + if target['wwn'] in target_name: + target_store.append(target['fs_list'][0]) + + for store in target_store: + for fs in fs_list: + if fs['name'] == store: + total_space = total_space + fs['file_storage_capacity'] + fs_free_space = (fs['file_storage_capacity'] - + fs['file_storage_used']) + + if fs_free_space > free_space: + free_space = fs_free_space + + total_capacity = int(total_space) / units.Gi + free_capacity = int(free_space) / units.Gi + + return (total_capacity, free_capacity) + + def get_volume_stats(self, refresh=False): + """Retrieve status info from share volume group.""" + total_capacity, free_capacity = self._get_va_backend_capacity() + self.session = self._authenticate_access(self._va_ip, + self._user, self._pwd) + + backend_name = self.configuration.safe_get('volume_backend_name') + res_percentage = self.configuration.safe_get('reserved_percentage') + self._stats["volume_backend_name"] = backend_name or 'VeritasISCSI' + self._stats["vendor_name"] = 'Veritas' + self._stats["reserved_percentage"] = res_percentage or 0 + self._stats["driver_version"] = self.VERSION + self._stats["storage_protocol"] = self.DRIVER_VOLUME_TYPE + self._stats['total_capacity_gb'] = total_capacity + self._stats['free_capacity_gb'] = free_capacity + self._stats['thin_provisioning_support'] = True + + return self._stats diff --git a/doc/source/configuration/block-storage/drivers/veritas-access-iscsi-driver.rst b/doc/source/configuration/block-storage/drivers/veritas-access-iscsi-driver.rst new file mode 100644 index 00000000000..24a8667ea27 --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/veritas-access-iscsi-driver.rst @@ -0,0 +1,89 @@ +=========================== +Veritas ACCESS iSCSI driver +=========================== + +Veritas Access is a software-defined scale-out network-attached +storage (NAS) solution for unstructured data that works on commodity +hardware and takes advantage of placing data on premise or in the +cloud based on intelligent policies. Through Veritas Access iSCSI +Driver, OpenStack Block Storage can use Veritas Access backend as a +block storage resource. The driver enables you to create iSCSI volumes +that an OpenStack Block Storage server can allocate to any virtual machine +running on a compute host. + +Requirements +~~~~~~~~~~~~ + +The Veritas ACCESS iSCSI Driver, version ``1.0.0`` and later, supports +Veritas ACCESS release ``7.4`` and later. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create and delete volumes. +- Create and delete snapshots. +- Create volume from snapshot. +- Extend a volume. +- Attach and detach volumes. +- Clone volumes. + +Configuration +~~~~~~~~~~~~~ + +#. Enable RESTful service on the Veritas Access Backend. + +#. Create Veritas Access iSCSI target, add store and portal IP to it. + + You can create target and add portal IP, store to it as follows: + + .. code-block:: console + + Target> iscsi target create iqn.2018-02.com.veritas:target02 + Target> iscsi target store add target_fs iqn.2018-02.com.veritas:target02 + Target> iscsi target portal add iqn.2018-02.com.veritas:target02 10.10.10.1 + ... + + You can add authentication to target as follows: + + .. code-block:: console + + Target> iscsi target auth incominguser add iqn.2018-02.com.veritas:target02 user1 + ... + +#. Ensure that the Veritas Access iSCSI target service is online. If the Veritas Access + iSCSI target service is not online, enable the service by using the CLI or REST API. + + .. code-block:: console + + Target> iscsi service start + Target> iscsi service status + ... + + Define the following required properties in the ``cinder.conf`` file: + + .. code-block:: ini + + volume_driver = cinder.volume.drivers.veritas_access.veritas_iscsi.ACCESSIscsiDriver + san_ip = va_console_ip + san_api_port = 14161 + san_login = master + san_password = password + target_port = 3260 + vrts_lun_sparse = True + vrts_target_config = /etc/cinder/vrts_target.xml + +#. Define Veritas Access Target details in ``/etc/cinder/vrts_target.xml``: + + .. code-block:: console + + + + + + iqn.2018-02.com.veritas:target02 + 10.10.10.1 + 0 + + + + ... diff --git a/doc/source/configuration/block-storage/volume-drivers.rst b/doc/source/configuration/block-storage/volume-drivers.rst index a1430e043b4..355801e9ddd 100644 --- a/doc/source/configuration/block-storage/volume-drivers.rst +++ b/doc/source/configuration/block-storage/volume-drivers.rst @@ -66,6 +66,7 @@ Driver Configuration Reference drivers/storpool-volume-driver drivers/synology-dsm-driver drivers/tintri-volume-driver + drivers/veritas-access-iscsi-driver drivers/vzstorage-driver drivers/vmware-vmdk-driver drivers/windows-iscsi-volume-driver diff --git a/releasenotes/notes/veritas_access_iscsi_driver-de642dad9e7d0890.yaml b/releasenotes/notes/veritas_access_iscsi_driver-de642dad9e7d0890.yaml new file mode 100644 index 00000000000..47bffae39d5 --- /dev/null +++ b/releasenotes/notes/veritas_access_iscsi_driver-de642dad9e7d0890.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added ISCSI based driver for Veritas Access.