From 8020d32b078f7c4e2f179413abdb96777509343a Mon Sep 17 00:00:00 2001 From: Arnon Yaari Date: Sun, 26 Jun 2016 11:50:09 +0300 Subject: [PATCH] New cinder driver to support INFINIDAT InfiniBox Add a cinder volume driver for the INFINIDAT InfiniBox storage array. It will include the minimum set of features required by Cinder. This driver supports FC connectivity only. DocImpact Implements: blueprint infinidat-cinder-driver Change-Id: I64ede068d73d4552ce0a3173493f94d1d267e385 --- cinder/opts.py | 2 + .../unit/volume/drivers/test_infinidat.py | 350 ++++++++++++++ cinder/volume/drivers/infinidat.py | 436 ++++++++++++++++++ ...add-infinibox-driver-67cc33fc3fbff1bb.yaml | 3 + 4 files changed, 791 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/test_infinidat.py create mode 100644 cinder/volume/drivers/infinidat.py create mode 100644 releasenotes/notes/infinidat-add-infinibox-driver-67cc33fc3fbff1bb.yaml diff --git a/cinder/opts.py b/cinder/opts.py index f2a675e108c..6fa4f172327 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -133,6 +133,7 @@ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc as \ cinder_volume_drivers_ibm_storwize_svc_storwizesvcfc 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.infortrend.raidcmd_cli import common_cli as \ cinder_volume_drivers_infortrend_raidcmd_cli_commoncli from cinder.volume.drivers.kaminario import kaminario_common as \ @@ -302,6 +303,7 @@ def list_opts(): storwize_svc_fc_opts, cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi. storwize_svc_iscsi_opts, + cinder_volume_drivers_infinidat.infinidat_opts, cinder_volume_drivers_infortrend_raidcmd_cli_commoncli. infortrend_esds_opts, cinder_volume_drivers_infortrend_raidcmd_cli_commoncli. diff --git a/cinder/tests/unit/volume/drivers/test_infinidat.py b/cinder/tests/unit/volume/drivers/test_infinidat.py new file mode 100644 index 00000000000..b23dbb6fca2 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/test_infinidat.py @@ -0,0 +1,350 @@ +# Copyright 2016 Infinidat Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Unit tests for INFINIDAT InfiniBox volume driver.""" + +import copy +import json + +import mock +from oslo_utils import units +import requests +import six + +from cinder import exception +from cinder import test +from cinder.volume import configuration +from cinder.volume.drivers import infinidat + + +BASE_URL = 'http://mockbox/api/rest/' +GET_VOLUME_URL = BASE_URL + 'volumes?name=openstack-vol-1' +GET_SNAP_URL = BASE_URL + 'volumes?name=openstack-snap-2' +GET_CLONE_URL = BASE_URL + 'volumes?name=openstack-vol-3' +GET_INTERNAL_CLONE_URL = BASE_URL + 'volumes?name=openstack-vol-3-internal' +VOLUMES_URL = BASE_URL + 'volumes' +VOLUME_URL = BASE_URL + 'volumes/1' +VOLUME_MAPPING_URL = BASE_URL + 'volumes/1/luns' +SNAPSHOT_URL = BASE_URL + 'volumes/2' +TEST_WWN_1 = '00:11:22:33:44:55:66:77' +TEST_WWN_2 = '11:11:22:33:44:55:66:77' +GET_HOST_URL = BASE_URL + 'hosts?name=openstack-host-0011223344556677' +GET_HOST2_URL = BASE_URL + 'hosts?name=openstack-host-1111223344556677' +HOSTS_URL = BASE_URL + 'hosts' +GET_POOL_URL = BASE_URL + 'pools?name=mockpool' +MAP_URL = BASE_URL + 'hosts/10/luns' +MAP2_URL = BASE_URL + 'hosts/11/luns' +ADD_PORT_URL = BASE_URL + 'hosts/10/ports' +UNMAP_URL = BASE_URL + 'hosts/10/luns/volume_id/1' +FC_PORT_URL = BASE_URL + 'components/nodes?fields=fc_ports' +APPROVAL = '?approved=true' + +VOLUME_RESULT = dict(id=1, + write_protected=False, + has_children=False, + parent_id=0) +VOLUME_RESULT_WP = dict(id=1, + write_protected=True, + has_children=False, + parent_id=0) +SNAPSHOT_RESULT = dict(id=2, + write_protected=True, + has_children=False, + parent_id=0) +HOST_RESULT = dict(id=10, luns=[]) +HOST2_RESULT = dict(id=11, luns=[]) +POOL_RESULT = dict(id=100, + free_physical_space=units.Gi, + physical_capacity=units.Gi) + +GOOD_PATH_RESPONSES = dict(GET={GET_VOLUME_URL: [VOLUME_RESULT], + GET_HOST_URL: [HOST_RESULT], + GET_HOST2_URL: [HOST2_RESULT], + GET_POOL_URL: [POOL_RESULT], + GET_SNAP_URL: [SNAPSHOT_RESULT], + GET_CLONE_URL: [VOLUME_RESULT], + GET_INTERNAL_CLONE_URL: [VOLUME_RESULT], + VOLUME_MAPPING_URL: [], + SNAPSHOT_URL: SNAPSHOT_RESULT, + FC_PORT_URL: [], + MAP_URL: [], + MAP2_URL: []}, + POST={VOLUMES_URL: VOLUME_RESULT, + HOSTS_URL: HOST_RESULT, + MAP_URL + APPROVAL: dict(lun=1), + MAP2_URL + APPROVAL: dict(lun=1), + ADD_PORT_URL: None}, + PUT={VOLUME_URL + APPROVAL: VOLUME_RESULT}, + DELETE={UNMAP_URL + APPROVAL: None, + VOLUME_URL + APPROVAL: None, + SNAPSHOT_URL + APPROVAL: None}) + +test_volume = mock.Mock(id=1, size=1) +test_snapshot = mock.Mock(id=2, volume=test_volume) +test_clone = mock.Mock(id=3, size=1) +test_connector = dict(wwpns=[TEST_WWN_1]) + + +class InfiniboxDriverTestCase(test.TestCase): + def setUp(self): + super(InfiniboxDriverTestCase, self).setUp() + + # create mock configuration + self.configuration = mock.Mock(spec=configuration.Configuration) + self.configuration.san_ip = 'mockbox' + self.configuration.infinidat_pool_name = 'mockpool' + self.configuration.san_thin_provision = 'thin' + self.configuration.san_login = 'user' + self.configuration.san_password = 'pass' + self.configuration.volume_backend_name = 'mock' + self.configuration.volume_dd_blocksize = '1M' + self.configuration.use_multipath_for_image_xfer = False + self.configuration.num_volume_device_scan_tries = 1 + self.configuration.san_is_local = False + + self.driver = infinidat.InfiniboxVolumeDriver( + configuration=self.configuration) + self.driver.do_setup(None) + self.driver._session = mock.Mock(spec=requests.Session) + self.driver._session.request.side_effect = self._request + self._responses = copy.deepcopy(GOOD_PATH_RESPONSES) + + def _request(self, action, url, **kwargs): + result = self._responses[action][url] + response = requests.Response() + if type(result) == int: + # tests set the response to an int of a bad status code if they + # want the api call to fail + response.status_code = result + response.raw = six.BytesIO(six.b(json.dumps(dict()))) + else: + response.status_code = 200 + response.raw = six.BytesIO(six.b(json.dumps(dict(result=result)))) + return response + + def test_get_volume_stats_refreshes(self): + result = self.driver.get_volume_stats() + self.assertEqual(1, result["free_capacity_gb"]) + # change the "free space" in the pool + self._responses["GET"][GET_POOL_URL][0]["free_physical_space"] = 0 + # no refresh - free capacity should stay the same + result = self.driver.get_volume_stats(refresh=False) + self.assertEqual(1, result["free_capacity_gb"]) + # refresh - free capacity should change to 0 + result = self.driver.get_volume_stats(refresh=True) + self.assertEqual(0, result["free_capacity_gb"]) + + def test_get_volume_stats_pool_not_found(self): + self._responses["GET"][GET_POOL_URL] = [] + self.assertRaises(exception.VolumeDriverException, + self.driver.get_volume_stats) + + def test_initialize_connection(self): + self._responses["GET"][GET_HOST_URL] = [] # host doesn't exist yet + result = self.driver.initialize_connection(test_volume, test_connector) + self.assertEqual(1, result["data"]["target_lun"]) + + def test_initialize_connection_host_exists(self): + result = self.driver.initialize_connection(test_volume, test_connector) + self.assertEqual(1, result["data"]["target_lun"]) + + def test_initialize_connection_mapping_exists(self): + self._responses["GET"][MAP_URL] = [{'lun': 888, 'volume_id': 1}] + result = self.driver.initialize_connection(test_volume, test_connector) + self.assertEqual(888, result["data"]["target_lun"]) + + def test_initialize_connection_multiple_hosts(self): + connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]} + result = self.driver.initialize_connection(test_volume, connector) + self.assertEqual(1, result["data"]["target_lun"]) + + def test_initialize_connection_volume_doesnt_exist(self): + self._responses["GET"][GET_VOLUME_URL] = [] + self.assertRaises(exception.InvalidVolume, + self.driver.initialize_connection, + test_volume, test_connector) + + def test_initialize_connection_create_fails(self): + self._responses["GET"][GET_HOST_URL] = [] # host doesn't exist yet + self._responses["POST"][HOSTS_URL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + test_volume, test_connector) + + def test_initialize_connection_map_fails(self): + self._responses["POST"][MAP_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + test_volume, test_connector) + + def test_terminate_connection(self): + self.driver.terminate_connection(test_volume, test_connector) + + def test_terminate_connection_volume_doesnt_exist(self): + self._responses["GET"][GET_VOLUME_URL] = [] + self.assertRaises(exception.InvalidVolume, + self.driver.terminate_connection, + test_volume, test_connector) + + def test_terminate_connection_api_fail(self): + self._responses["DELETE"][UNMAP_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + test_volume, test_connector) + + def test_create_volume(self): + self.driver.create_volume(test_volume) + + def test_create_volume_pool_not_found(self): + self._responses["GET"][GET_POOL_URL] = [] + self.assertRaises(exception.VolumeDriverException, + self.driver.create_volume, test_volume) + + def test_create_volume_api_fail(self): + self._responses["POST"][VOLUMES_URL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, test_volume) + + def test_delete_volume(self): + self.driver.delete_volume(test_volume) + + def test_delete_volume_doesnt_exist(self): + self._responses["GET"][GET_VOLUME_URL] = [] + # should not raise an exception + self.driver.delete_volume(test_volume) + + def test_delete_volume_doesnt_exist_on_delete(self): + self._responses["DELETE"][VOLUME_URL + APPROVAL] = 404 + # due to a possible race condition (get+delete is not atomic) the + # GET may return the volume but it may still be deleted before + # the DELETE request + # In this case we still should not raise an exception + self.driver.delete_volume(test_volume) + + def test_delete_volume_with_children(self): + self._responses["GET"][GET_VOLUME_URL][0]['has_children'] = True + self.assertRaises(exception.VolumeIsBusy, + self.driver.delete_volume, test_volume) + + def test_delete_volume_api_fail(self): + self._responses["DELETE"][VOLUME_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_volume, test_volume) + + def test_extend_volume(self): + self.driver.extend_volume(test_volume, 2) + + def test_extend_volume_api_fail(self): + self._responses["PUT"][VOLUME_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.extend_volume, test_volume, 2) + + def test_create_snapshot(self): + self.driver.create_snapshot(test_snapshot) + + def test_create_snapshot_volume_doesnt_exist(self): + self._responses["GET"][GET_VOLUME_URL] = [] + self.assertRaises(exception.InvalidVolume, + self.driver.create_snapshot, test_snapshot) + + def test_create_snapshot_api_fail(self): + self._responses["POST"][VOLUMES_URL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, test_snapshot) + + @mock.patch("cinder.volume.utils.copy_volume") + @mock.patch("cinder.utils.brick_get_connector") + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + def test_create_volume_from_snapshot(self, *mocks): + self.driver.create_volume_from_snapshot(test_clone, test_snapshot) + + def test_create_volume_from_snapshot_doesnt_exist(self): + self._responses["GET"][GET_SNAP_URL] = [] + self.assertRaises(exception.InvalidSnapshot, + self.driver.create_volume_from_snapshot, + test_clone, test_snapshot) + + def test_create_volume_from_snapshot_create_fails(self): + self._responses["POST"][VOLUMES_URL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + test_clone, test_snapshot) + + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + def test_create_volume_from_snapshot_map_fails(self, *mocks): + self._responses["POST"][MAP_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + test_clone, test_snapshot) + + @mock.patch("cinder.volume.utils.copy_volume") + @mock.patch("cinder.utils.brick_get_connector") + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + def test_create_volume_from_snapshot_delete_clone_fails(self, *mocks): + self._responses["DELETE"][VOLUME_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + test_clone, test_snapshot) + + def test_delete_snapshot(self): + self.driver.delete_snapshot(test_snapshot) + + def test_delete_snapshot_doesnt_exist(self): + self._responses["GET"][GET_SNAP_URL] = [] + # should not raise an exception + self.driver.delete_snapshot(test_snapshot) + + def test_delete_snapshot_doesnt_exist_on_delete(self): + self._responses["DELETE"][SNAPSHOT_URL + APPROVAL] = 404 + # due to a possible race condition (get+delete is not atomic) the + # GET may return the snapshot but it may still be deleted before + # the DELETE request + # In this case we still should not raise an exception + self.driver.delete_snapshot(test_snapshot) + + def test_delete_snapshot_api_fail(self): + self._responses["DELETE"][SNAPSHOT_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, test_snapshot) + + @mock.patch("cinder.volume.utils.copy_volume") + @mock.patch("cinder.utils.brick_get_connector") + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + def test_create_cloned_volume(self, *mocks): + self.driver.create_cloned_volume(test_clone, test_volume) + + def test_create_cloned_volume_volume_already_mapped(self): + test_lun = [{'lun': 888, 'host_id': 10}] + self._responses["GET"][VOLUME_MAPPING_URL] = test_lun + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + test_clone, test_volume) + + def test_create_cloned_volume_create_fails(self): + self._responses["POST"][VOLUMES_URL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + test_clone, test_volume) + + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + def test_create_cloned_volume_map_fails(self, *mocks): + self._responses["POST"][MAP_URL + APPROVAL] = 500 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + test_clone, test_volume) diff --git a/cinder/volume/drivers/infinidat.py b/cinder/volume/drivers/infinidat.py new file mode 100644 index 00000000000..fb41d300435 --- /dev/null +++ b/cinder/volume/drivers/infinidat.py @@ -0,0 +1,436 @@ +# Copyright 2016 Infinidat Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +INFINIDAT InfiniBox Volume Driver +""" + +from contextlib import contextmanager + +import mock +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units +import requests +import six + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils +from cinder.volume import driver +from cinder.volume.drivers.san import san +from cinder.volume import utils as vol_utils +from cinder.zonemanager import utils as fczm_utils + + +LOG = logging.getLogger(__name__) + +VENDOR_NAME = 'INFINIDAT' +DELETE_URI = 'volumes/%s?approved=true' + +infinidat_opts = [ + cfg.StrOpt('infinidat_pool_name', + help='Name of the pool from which volumes are allocated'), +] + +CONF = cfg.CONF +CONF.register_opts(infinidat_opts) + + +@interface.volumedriver +class InfiniboxVolumeDriver(san.SanDriver, + driver.ExtendVD, + driver.SnapshotVD, + driver.TransferVD): + VERSION = '1.0' + + # ThirdPartySystems wiki page + CI_WIKI_NAME = "INFINIDAT_Cinder_CI" + + def __init__(self, *args, **kwargs): + super(InfiniboxVolumeDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(infinidat_opts) + self._lookup_service = fczm_utils.create_lookup_service() + + def do_setup(self, context): + """Driver initialization""" + self._session = requests.Session() + self._session.auth = (self.configuration.san_login, + self.configuration.san_password) + management_address = self.configuration.san_ip + self._base_url = 'http://%s/api/rest/' % management_address + backend_name = self.configuration.safe_get('volume_backend_name') + self._backend_name = backend_name or self.__class__.__name__ + self._volume_stats = None + LOG.debug('setup complete. base url: %s', self._base_url) + + def _request(self, action, uri, data=None): + LOG.debug('--> %(action)s %(uri)s %(data)r', + {'action': action, 'uri': uri, 'data': data}) + response = self._session.request(action, + self._base_url + uri, + json=data) + LOG.debug('<-- %(status_code)s %(response_json)r', + {'status_code': response.status_code, + 'response_json': response.json()}) + try: + response.raise_for_status() + except requests.HTTPError as ex: + # text_type(ex) includes http code and url + msg = _('InfiniBox storage array returned %(exception)s\n' + 'Data: %(data)s\n' + 'Response: %(response_json)s') % { + 'exception': six.text_type(ex), + 'data': repr(data), + 'response_json': repr(response.json())} + LOG.exception(msg) + if response.status_code == 404: + raise exception.NotFound() + else: + raise exception.VolumeBackendAPIException(data=msg) + return response.json()['result'] + + def _get(self, uri): + return self._request('GET', uri) + + def _post(self, uri, data): + return self._request('POST', uri, data) + + def _delete(self, uri): + return self._request('DELETE', uri) + + def _put(self, uri, data): + return self._request('PUT', uri, data) + + def _cleanup_wwpn(self, wwpn): + return wwpn.replace(':', '') + + def _make_volume_name(self, cinder_volume): + return 'openstack-vol-%s' % cinder_volume.id + + def _make_snapshot_name(self, cinder_snapshot): + return 'openstack-snap-%s' % cinder_snapshot.id + + def _make_host_name(self, wwpn): + wwn_for_name = self._cleanup_wwpn(wwpn) + return 'openstack-host-%s' % wwn_for_name + + def _get_infinidat_volume_by_name(self, name): + volumes = self._get('volumes?name=%s' % name) + if len(volumes) != 1: + msg = _('Volume "%s" not found') % name + LOG.error(msg) + raise exception.InvalidVolume(reason=msg) + return volumes[0] + + def _get_infinidat_snapshot_by_name(self, name): + snapshots = self._get('volumes?name=%s' % name) + if len(snapshots) != 1: + msg = _('Snapshot "%s" not found') % name + LOG.error(msg) + raise exception.InvalidSnapshot(reason=msg) + return snapshots[0] + + def _get_infinidat_volume_id(self, cinder_volume): + volume_name = self._make_volume_name(cinder_volume) + return self._get_infinidat_volume_by_name(volume_name)['id'] + + def _get_infinidat_snapshot_id(self, cinder_snapshot): + snap_name = self._make_snapshot_name(cinder_snapshot) + return self._get_infinidat_snapshot_by_name(snap_name)['id'] + + def _get_infinidat_pool(self): + pool_name = self.configuration.infinidat_pool_name + pools = self._get('pools?name=%s' % pool_name) + if len(pools) != 1: + msg = _('Pool "%s" not found') % pool_name + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + return pools[0] + + def _get_host(self, wwpn): + host_name = self._make_host_name(wwpn) + infinidat_hosts = self._get('hosts?name=%s' % host_name) + if len(infinidat_hosts) == 1: + return infinidat_hosts[0] + + def _get_or_create_host(self, wwpn): + host_name = self._make_host_name(wwpn) + infinidat_host = self._get_host(wwpn) + if infinidat_host is None: + # create host + infinidat_host = self._post('hosts', dict(name=host_name)) + # add port to host + self._post('hosts/%s/ports' % infinidat_host['id'], + dict(type='FC', address=self._cleanup_wwpn(wwpn))) + return infinidat_host + + def _get_mapping(self, host_id, volume_id): + existing_mapping = self._get("hosts/%s/luns" % host_id) + for mapping in existing_mapping: + if mapping['volume_id'] == volume_id: + return mapping + + def _get_or_create_mapping(self, host_id, volume_id): + mapping = self._get_mapping(host_id, volume_id) + if mapping: + return mapping + # volume not mapped. map it + uri = 'hosts/%s/luns?approved=true' % host_id + return self._post(uri, dict(volume_id=volume_id)) + + def _get_online_fc_ports(self): + nodes = self._get('components/nodes?fields=fc_ports') + for node in nodes: + for port in node['fc_ports']: + if (port['link_state'].lower() == 'up' + and port['state'] == 'OK'): + yield self._cleanup_wwpn(port['wwpn']) + + @fczm_utils.AddFCZone + def initialize_connection(self, volume, connector): + """Map an InfiniBox volume to the host""" + volume_name = self._make_volume_name(volume) + infinidat_volume = self._get_infinidat_volume_by_name(volume_name) + for wwpn in connector['wwpns']: + infinidat_host = self._get_or_create_host(wwpn) + mapping = self._get_or_create_mapping(infinidat_host['id'], + infinidat_volume['id']) + lun = mapping['lun'] + + # Create initiator-target mapping. + target_wwpns = list(self._get_online_fc_ports()) + target_wwpns, init_target_map = self._build_initiator_target_map( + connector, target_wwpns) + return dict(driver_volume_type='fibre_channel', + data=dict(target_discovered=False, + target_wwn=target_wwpns, + target_lun=lun, + initiator_target_map=init_target_map)) + + @fczm_utils.RemoveFCZone + def terminate_connection(self, volume, connector, **kwargs): + """Unmap an InfiniBox volume from the host""" + volume_id = self._get_infinidat_volume_id(volume) + result_data = dict() + for wwpn in connector['wwpns']: + host_name = self._make_host_name(wwpn) + infinidat_hosts = self._get('hosts?name=%s' % host_name) + if len(infinidat_hosts) != 1: + # not found. ignore. + continue + host_id = infinidat_hosts[0]['id'] + # unmap + uri = ('hosts/%s/luns/volume_id/%s' % (host_id, volume_id) + + '?approved=true') + try: + self._delete(uri) + except (exception.NotFound): + continue # volume mapping not found + # check if the host now doesn't have mappings, to delete host_entry + # if needed + infinidat_hosts = self._get('hosts?name=%s' % host_name) + if len(infinidat_hosts) == 1 and len(infinidat_hosts[0]['luns']) == 0: + # Create initiator-target mapping. + target_wwpns = list(self._get_online_fc_ports()) + target_wwpns, init_target_map = self._build_initiator_target_map( + connector, target_wwpns) + result_data = dict(target_wwn=target_wwpns, + initiator_target_map=init_target_map) + return dict(driver_volume_type='fibre_channel', + data=result_data) + + def get_volume_stats(self, refresh=False): + if self._volume_stats is None or refresh: + pool = self._get_infinidat_pool() + free_capacity_gb = float(pool['free_physical_space']) / units.Gi + total_capacity_gb = float(pool['physical_capacity']) / units.Gi + self._volume_stats = dict(volume_backend_name=self._backend_name, + vendor_name=VENDOR_NAME, + driver_version=self.VERSION, + storage_protocol='FC', + consistencygroup_support='False', + total_capacity_gb=total_capacity_gb, + free_capacity_gb=free_capacity_gb) + return self._volume_stats + + def _create_volume(self, volume): + # get pool id from name + pool = self._get_infinidat_pool() + # create volume + volume_name = self._make_volume_name(volume) + provtype = "THIN" if self.configuration.san_thin_provision else "THICK" + data = dict(pool_id=pool['id'], + provtype=provtype, + name=volume_name, + size=volume.size * units.Gi) + return self._post('volumes', data) + + def create_volume(self, volume): + """Create a new volume on the backend.""" + # this is the same as _create_volume but without the return statement + self._create_volume(volume) + + def delete_volume(self, volume): + """Delete a volume from the backend.""" + try: + volume_name = self._make_volume_name(volume) + volume = self._get_infinidat_volume_by_name(volume_name) + if volume['has_children']: + # can't delete a volume that has a live snapshot + raise exception.VolumeIsBusy(volume_name=volume_name) + self._delete(DELETE_URI % volume['id']) + except (exception.InvalidVolume, exception.NotFound): + return # volume not found + + def extend_volume(self, volume, new_size): + """Extend the size of a volume.""" + volume_id = self._get_infinidat_volume_id(volume) + self._put('volumes/%s?approved=true' % volume_id, + dict(size=new_size * units.Gi)) + + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + volume_id = self._get_infinidat_volume_id(snapshot.volume) + name = self._make_snapshot_name(snapshot) + self._post('volumes', dict(parent_id=volume_id, name=name)) + + @contextmanager + def _device_connect_context(self, volume): + connector = utils.brick_get_connector_properties() + connection = self.initialize_connection(volume, connector) + try: + yield self._connect_device(connection) + finally: + self.terminate_connection(volume, connector) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create volume from snapshot. + + InfiniBox does not yet support detached clone so use dd to copy data. + This could be a lengthy operation. + + - create a clone from snapshot and map it + - create a volume and map it + - copy data from clone to volume + - unmap volume and clone and delete the clone + """ + snapshot_id = self._get_infinidat_snapshot_id(snapshot) + clone_name = self._make_volume_name(volume) + '-internal' + infinidat_clone = self._post('volumes', dict(parent_id=snapshot_id, + name=clone_name)) + # we need a cinder-volume-like object to map the clone by name + # (which is derived from the cinder id) but the clone is internal + # so there is no such object. mock one + clone = mock.Mock(id=str(volume.id) + '-internal') + try: + infinidat_volume = self._create_volume(volume) + try: + src_ctx = self._device_connect_context(clone) + dst_ctx = self._device_connect_context(volume) + with src_ctx as src_dev, dst_ctx as dst_dev: + dd_block_size = self.configuration.volume_dd_blocksize + vol_utils.copy_volume(src_dev['device']['path'], + dst_dev['device']['path'], + snapshot.volume.size * units.Ki, + dd_block_size, + sparse=True) + except Exception: + self._delete(DELETE_URI % infinidat_volume['id']) + raise + finally: + self._delete(DELETE_URI % infinidat_clone['id']) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + try: + snapshot_name = self._make_snapshot_name(snapshot) + snapshot = self._get_infinidat_snapshot_by_name(snapshot_name) + self._delete(DELETE_URI % snapshot['id']) + except (exception.InvalidSnapshot, exception.NotFound): + return # snapshot not found + + def _asssert_volume_not_mapped(self, volume): + # copy is not atomic so we can't clone while the volume is mapped + volume_name = self._make_volume_name(volume) + infinidat_volume = self._get_infinidat_volume_by_name(volume_name) + mappings = self._get("volumes/%s/luns" % infinidat_volume['id']) + if len(mappings) == 0: + return + + # volume has mappings + msg = _("INFINIDAT Cinder driver does not support clone of an " + "attached volume. " + "To get this done, create a snapshot from the attached " + "volume and then create a volume from the snapshot.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def create_cloned_volume(self, volume, src_vref): + """Create a clone from source volume. + + InfiniBox does not yet support detached clone so use dd to copy data. + This could be a lengthy operation. + + * map source volume + * create and map new volume + * copy data from source to new volume + * unmap both volumes + """ + self._asssert_volume_not_mapped(src_vref) + infinidat_volume = self._create_volume(volume) + try: + src_ctx = self._device_connect_context(src_vref) + dst_ctx = self._device_connect_context(volume) + with src_ctx as src_dev, dst_ctx as dst_dev: + dd_block_size = self.configuration.volume_dd_blocksize + vol_utils.copy_volume(src_dev['device']['path'], + dst_dev['device']['path'], + src_vref.size * units.Ki, + dd_block_size, + sparse=True) + except Exception: + self._delete(DELETE_URI % infinidat_volume['id']) + raise + + def _build_initiator_target_map(self, connector, all_target_wwns): + """Build the target_wwns and the initiator target map.""" + target_wwns = [] + init_targ_map = {} + + if self._lookup_service is not None: + # use FC san lookup. + dev_map = self._lookup_service.get_device_mapping_from_network( + connector.get('wwpns'), + all_target_wwns) + + for fabric_name in dev_map: + fabric = dev_map[fabric_name] + target_wwns += fabric['target_port_wwn_list'] + for initiator in fabric['initiator_port_wwn_list']: + if initiator not in init_targ_map: + init_targ_map[initiator] = [] + init_targ_map[initiator] += fabric['target_port_wwn_list'] + init_targ_map[initiator] = list(set( + init_targ_map[initiator])) + target_wwns = list(set(target_wwns)) + else: + initiator_wwns = connector.get('wwpns', []) + target_wwns = all_target_wwns + + for initiator in initiator_wwns: + init_targ_map[initiator] = target_wwns + + return target_wwns, init_targ_map diff --git a/releasenotes/notes/infinidat-add-infinibox-driver-67cc33fc3fbff1bb.yaml b/releasenotes/notes/infinidat-add-infinibox-driver-67cc33fc3fbff1bb.yaml new file mode 100644 index 00000000000..7245ec3ca46 --- /dev/null +++ b/releasenotes/notes/infinidat-add-infinibox-driver-67cc33fc3fbff1bb.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added driver for the InfiniBox storage array.