From 2406d5fef94d6828d4b247114ce0583c896ff163 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 16 Oct 2014 16:11:32 -0400 Subject: [PATCH] NetApp E-series: Allow scheduling by disk This patch allows volume scheduling by disk type via the netapp_disk_type volume type extra spec and disk encryption via the netapp_disk_encryption volume type extra spec Partially Implements bp: netapp-eseries-extra-specs Change-Id: I6e11073838c1b63b6a0680d991f8f1289ca63704 --- cinder/tests/test_netapp_eseries_iscsi.py | 4 +- .../drivers/netapp/eseries/test_iscsi.py | 143 ++++++++++++++++++ cinder/tests/volume/drivers/netapp/fakes.py | 52 ++++--- .../volume/drivers/netapp/eseries/client.py | 7 + cinder/volume/drivers/netapp/eseries/iscsi.py | 89 ++++++++++- 5 files changed, 267 insertions(+), 28 deletions(-) diff --git a/cinder/tests/test_netapp_eseries_iscsi.py b/cinder/tests/test_netapp_eseries_iscsi.py index 9ba083500..bc94020b3 100644 --- a/cinder/tests/test_netapp_eseries_iscsi.py +++ b/cinder/tests/test_netapp_eseries_iscsi.py @@ -1,4 +1,6 @@ # Copyright (c) 2014 NetApp, Inc. +# Copyright (c) 2015 Alex Meade. All Rights Reserved. +# Copyright (c) 2015 Rushil Chugh. All Rights Reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -771,7 +773,7 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): self.assertIsNotNone(properties, 'Target portal is none') def test_vol_stats(self): - self.driver.get_volume_stats(refresh=True) + self.driver.get_volume_stats(refresh=False) def test_create_vol_snapshot_diff_size_resize(self): self.driver.db = mock.Mock( diff --git a/cinder/tests/volume/drivers/netapp/eseries/test_iscsi.py b/cinder/tests/volume/drivers/netapp/eseries/test_iscsi.py index 2b5e2ebf7..193deade4 100644 --- a/cinder/tests/volume/drivers/netapp/eseries/test_iscsi.py +++ b/cinder/tests/volume/drivers/netapp/eseries/test_iscsi.py @@ -1,4 +1,6 @@ # Copyright (c) 2014 Andrew Kerr. All rights reserved. +# Copyright (c) 2015 Alex Meade. All rights reserved. +# Copyright (c) 2015 Rushil Chugh. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -32,6 +34,7 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): kwargs = {'configuration': self.get_config_eseries()} self.driver = es_iscsi.NetAppEseriesISCSIDriver(**kwargs) + self.driver._client = mock.Mock() def get_config_eseries(self): config = na_fakes.create_configuration_eseries() @@ -56,3 +59,143 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): self.driver.do_setup(mock.Mock()) self.assertTrue(mock_check_flags.called) + + def test_update_ssc_info(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'driveMediaType': 'ssd'}] + + self.driver._objects["disk_pool_refs"] = ['test_vg1'] + self.driver._client.list_storage_pools = mock.Mock(return_value=[]) + self.driver._client.list_drives = mock.Mock(return_value=drives) + + self.driver._update_ssc_info() + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'SSD'}}, + self.driver._ssc_stats) + + def test_update_ssc_disk_types_ssd(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'driveMediaType': 'ssd'}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'SSD'}}, + ssc_stats) + + def test_update_ssc_disk_types_scsi(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': 'scsi'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'SCSI'}}, + ssc_stats) + + def test_update_ssc_disk_types_fcal(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': 'fibre'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'FCAL'}}, + ssc_stats) + + def test_update_ssc_disk_types_sata(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': 'sata'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'SATA'}}, + ssc_stats) + + def test_update_ssc_disk_types_sas(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': 'sas'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'SAS'}}, + ssc_stats) + + def test_update_ssc_disk_types_unknown(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': 'unknown'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'unknown'}}, + ssc_stats) + + def test_update_ssc_disk_types_undefined(self): + drives = [{'currentVolumeGroupRef': 'test_vg1', + 'interfaceType': {'driveType': '__UNDEFINED'}}] + self.driver._client.list_drives = mock.Mock(return_value=drives) + + ssc_stats = self.driver._update_ssc_disk_types(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_type': 'unknown'}}, + ssc_stats) + + def test_update_ssc_disk_encryption_SecType_enabled(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'enabled'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'true'}}, + ssc_stats) + + def test_update_ssc_disk_encryption_SecType_unknown(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'unknown'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'false'}}, + ssc_stats) + + def test_update_ssc_disk_encryption_SecType_none(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'none'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'false'}}, + ssc_stats) + + def test_update_ssc_disk_encryption_SecType_capable(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'capable'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1']) + + self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'false'}}, + ssc_stats) + + def test_update_ssc_disk_encryption_SecType_garbage(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'garbage'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1']) + + self.assertRaises(TypeError, 'test_vg1', + {'netapp_disk_encryption': 'false'}, ssc_stats) + + def test_update_ssc_disk_encryption_multiple(self): + pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'none'}, + {'volumeGroupRef': 'test_vg2', 'securityType': 'enabled'}] + self.driver._client.list_storage_pools = mock.Mock(return_value=pools) + + ssc_stats = self.driver._update_ssc_disk_encryption(['test_vg1', + 'test_vg2']) + + self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'false'}, + 'test_vg2': {'netapp_disk_encryption': 'true'}}, + ssc_stats) diff --git a/cinder/tests/volume/drivers/netapp/fakes.py b/cinder/tests/volume/drivers/netapp/fakes.py index b797613c3..aef1d4d2d 100644 --- a/cinder/tests/volume/drivers/netapp/fakes.py +++ b/cinder/tests/volume/drivers/netapp/fakes.py @@ -1,5 +1,6 @@ # Copyright (c) - 2014, Clinton Knight All rights reserved. -# Copyright (c) - 2014, Rushil Chugh All rights reserved. +# Copyright (c) - 2015, Alex Meade. All Rights Reserved. +# Copyright (c) - 2015, Rushil Chugh. 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 @@ -18,6 +19,31 @@ from cinder.volume import configuration as conf import cinder.volume.drivers.netapp.options as na_opts +ISCSI_FAKE_LUN_ID = 1 + +ISCSI_FAKE_IQN = 'iqn.1993-08.org.debian:01:10' + +ISCSI_FAKE_ADDRESS = '10.63.165.216' + +ISCSI_FAKE_PORT = '2232' + +ISCSI_FAKE_VOLUME = {'id': 'fake_id'} + +ISCSI_FAKE_TARGET = {} +ISCSI_FAKE_TARGET['address'] = ISCSI_FAKE_ADDRESS +ISCSI_FAKE_TARGET['port'] = ISCSI_FAKE_PORT + +ISCSI_FAKE_VOLUME = {'id': 'fake_id', 'provider_auth': 'None stack password'} + +FC_ISCSI_TARGET_INFO_DICT = {'target_discovered': False, + 'target_portal': '10.63.165.216:2232', + 'target_iqn': ISCSI_FAKE_IQN, + 'target_lun': ISCSI_FAKE_LUN_ID, + 'volume_id': ISCSI_FAKE_VOLUME['id'], + 'auth_method': 'None', 'auth_username': 'stack', + 'auth_password': 'password'} + + def create_configuration(): config = conf.Configuration(None) config.append_config_values(na_opts.netapp_connection_opts) @@ -43,27 +69,3 @@ def create_configuration_eseries(): config = create_configuration() config.append_config_values(na_opts.netapp_eseries_opts) return config - -ISCSI_FAKE_LUN_ID = 1 - -ISCSI_FAKE_IQN = 'iqn.1993-08.org.debian:01:10' - -ISCSI_FAKE_ADDRESS = '10.63.165.216' - -ISCSI_FAKE_PORT = '2232' - -ISCSI_FAKE_VOLUME = {'id': 'fake_id'} - -ISCSI_FAKE_TARGET = {} -ISCSI_FAKE_TARGET['address'] = ISCSI_FAKE_ADDRESS -ISCSI_FAKE_TARGET['port'] = ISCSI_FAKE_PORT - -ISCSI_FAKE_VOLUME = {'id': 'fake_id', 'provider_auth': 'None stack password'} - -FC_ISCSI_TARGET_INFO_DICT = {'target_discovered': False, - 'target_portal': '10.63.165.216:2232', - 'target_iqn': ISCSI_FAKE_IQN, - 'target_lun': ISCSI_FAKE_LUN_ID, - 'volume_id': ISCSI_FAKE_VOLUME['id'], - 'auth_method': 'None', 'auth_username': 'stack', - 'auth_password': 'password'} diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index af87856e8..268fb85a4 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -1,5 +1,7 @@ # Copyright (c) 2014 NetApp, Inc. All rights reserved. # Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2015 Alex Meade. All Rights Reserved. +# Copyright (c) 2015 Rushil Chugh. 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 @@ -298,6 +300,11 @@ class RestClient(WebserviceClient): path = "/storage-systems/{system-id}/storage-pools" return self._invoke('GET', path) + def list_drives(self): + """Lists drives in the array.""" + path = "/storage-systems/{system-id}/drives" + return self._invoke('GET', path) + def list_storage_systems(self): """Lists managed storage systems registered with web service.""" path = "/storage-systems" diff --git a/cinder/volume/drivers/netapp/eseries/iscsi.py b/cinder/volume/drivers/netapp/eseries/iscsi.py index 952d2bd36..53623f92c 100644 --- a/cinder/volume/drivers/netapp/eseries/iscsi.py +++ b/cinder/volume/drivers/netapp/eseries/iscsi.py @@ -1,4 +1,6 @@ # Copyright (c) 2014 NetApp, Inc. All Rights Reserved. +# Copyright (c) 2015 Alex Meade. All Rights Reserved. +# Copyright (c) 2015 Rushil Chugh. 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 @@ -15,6 +17,7 @@ iSCSI driver for NetApp E-series storage systems. """ +import copy import socket import time import uuid @@ -27,6 +30,7 @@ import six from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder.openstack.common import log as logging +from cinder.openstack.common import loopingcall from cinder import utils as cinder_utils from cinder.volume import driver from cinder.volume.drivers.netapp.eseries import client @@ -78,6 +82,16 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): 'windows_clustered': 'Windows 2000/Server 2003/Server 2008 Clustered' } + # NOTE(ameade): This maps what is reported by the e-series api to a + # consistent set of values that are reported by all NetApp drivers + # to the cinder scheduler. + SSC_DISK_TYPE_MAPPING = { + 'scsi': 'SCSI', + 'fibre': 'FCAL', + 'sas': 'SAS', + 'sata': 'SATA', + } + SSC_UPDATE_INTERVAL = 60 # seconds def __init__(self, *args, **kwargs): super(NetAppEseriesISCSIDriver, self).__init__(*args, **kwargs) @@ -91,6 +105,7 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): self._objects = {'disk_pool_refs': [], 'pools': [], 'volumes': {'label_ref': {}, 'ref_vol': {}}, 'snapshots': {'label_ref': {}, 'ref_snap': {}}} + self._ssc_stats = {} def do_setup(self, context): """Any initialization the volume driver does while starting.""" @@ -114,11 +129,17 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): password=self.configuration.netapp_password) self._check_mode_get_or_register_storage_system() + def _start_periodic_tasks(self): + ssc_periodic_task = loopingcall.FixedIntervalLoopingCall( + self._update_ssc_info) + ssc_periodic_task.start(interval=self.SSC_UPDATE_INTERVAL) + def check_for_setup_error(self): self._check_host_type() self._check_multipath() self._check_storage_system() self._populate_system_objects() + self._start_periodic_tasks() def _check_host_type(self): self.host_type =\ @@ -170,8 +191,8 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): system = self._client.list_storage_system() except exception.NetAppDriverException: with excutils.save_and_reraise_exception(): - msg = _("System with controller addresses [%s] is not" - " registered with web service.") + msg = _LI("System with controller addresses [%s] is not" + " registered with web service.") LOG.info(msg % self.configuration.netapp_controller_ips) password_not_in_sync = False if system.get('status', '').lower() == 'passwordoutofsync': @@ -723,7 +744,10 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): def get_volume_stats(self, refresh=False): """Return the current state of the volume service.""" if refresh: + if not self._ssc_stats: + self._update_ssc_info() self._update_volume_stats() + return self._stats def _update_volume_stats(self): @@ -748,11 +772,72 @@ class NetAppEseriesISCSIDriver(driver.ISCSIDriver): cinder_pool["free_capacity_gb"] = ((tot_bytes - used_bytes) / units.Gi) cinder_pool["total_capacity_gb"] = tot_bytes / units.Gi + + pool_ssc_stats = self._ssc_stats.get(pool["volumeGroupRef"]) + + if pool_ssc_stats: + cinder_pool.update(pool_ssc_stats) data["pools"].append(cinder_pool) self._stats = data self._garbage_collect_tmp_vols() + @cinder_utils.synchronized("netapp_update_ssc_info", external=False) + def _update_ssc_info(self): + """Periodically runs to update ssc information from the backend. + + The self._ssc_stats attribute is updated with the following format. + { : {: }} + """ + LOG.info(_LI("Updating storage service catalog information for " + "backend '%s'") % self._backend_name) + self._ssc_stats = \ + self._update_ssc_disk_encryption(self._objects["disk_pool_refs"]) + self._ssc_stats = \ + self._update_ssc_disk_types(self._objects["disk_pool_refs"]) + + def _update_ssc_disk_types(self, volume_groups): + """Updates the given ssc dictionary with new disk type information. + + :param volume_groups: The volume groups this driver cares about + """ + ssc_stats = copy.deepcopy(self._ssc_stats) + all_disks = self._client.list_drives() + relevant_disks = filter(lambda x: x.get('currentVolumeGroupRef') in + volume_groups, all_disks) + for drive in relevant_disks: + current_vol_group = drive.get('currentVolumeGroupRef') + if current_vol_group not in ssc_stats: + ssc_stats[current_vol_group] = {} + + if drive.get("driveMediaType") == 'ssd': + ssc_stats[current_vol_group]['netapp_disk_type'] = 'SSD' + else: + disk_type = drive.get('interfaceType').get('driveType') + ssc_stats[current_vol_group]['netapp_disk_type'] = \ + self.SSC_DISK_TYPE_MAPPING.get(disk_type, 'unknown') + + return ssc_stats + + def _update_ssc_disk_encryption(self, volume_groups): + """Updates the given ssc dictionary with new disk encryption information. + + :param volume_groups: The volume groups this driver cares about + """ + ssc_stats = copy.deepcopy(self._ssc_stats) + all_pools = self._client.list_storage_pools() + relevant_pools = filter(lambda x: x.get('volumeGroupRef') in + volume_groups, all_pools) + for pool in relevant_pools: + current_vol_group = pool.get('volumeGroupRef') + if current_vol_group not in ssc_stats: + ssc_stats[current_vol_group] = {} + + ssc_stats[current_vol_group]['netapp_disk_encryption'] = 'true' \ + if pool['securityType'] == 'enabled' else 'false' + + return ssc_stats + def _get_sorted_avl_storage_pools(self, size_gb): """Returns storage pools sorted on available capacity.""" size = size_gb * units.Gi -- 2.45.2