From 3141da1442e9f6ec18194feefc7173ab7798a193 Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Date: Mon, 2 Jan 2023 20:05:26 -0300 Subject: [PATCH] NetApp: Add NVMe/TCP driver Add NVMe/TCP cinder volume driver for the NetApp ONTAP Cluster. Implements: blueprint netapp-volume-driver Change-Id: Ic583131b46d928ef7669fa47e697cce65e7632bc --- .../drivers/netapp/dataontap/client/fakes.py | 90 ++ .../client/test_client_cmode_rest.py | 396 ++++++++ .../volume/drivers/netapp/dataontap/fakes.py | 37 + .../netapp/dataontap/test_nvme_cmode.py | 40 + .../netapp/dataontap/test_nvme_library.py | 930 ++++++++++++++++++ cinder/volume/drivers/netapp/common.py | 3 +- .../drivers/netapp/dataontap/client/api.py | 1 + .../dataontap/client/client_cmode_rest.py | 273 +++++ .../drivers/netapp/dataontap/nvme_cmode.py | 109 ++ .../drivers/netapp/dataontap/nvme_library.py | 765 ++++++++++++++ .../drivers/netapp/dataontap/utils/utils.py | 4 +- cinder/volume/drivers/netapp/options.py | 6 +- .../drivers/netapp-volume-driver.rst | 64 +- .../tables/cinder-netapp_cdot_iscsi.inc | 2 +- doc/source/reference/support-matrix.ini | 2 +- .../add_nvme_tcp_driver-558ff80aa2029e2b.yaml | 4 + 16 files changed, 2718 insertions(+), 8 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_cmode.py create mode 100644 cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_library.py create mode 100644 cinder/volume/drivers/netapp/dataontap/nvme_cmode.py create mode 100644 cinder/volume/drivers/netapp/dataontap/nvme_library.py create mode 100644 releasenotes/notes/add_nvme_tcp_driver-558ff80aa2029e2b.yaml diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py index 8f6a5ad8c35..67c00fcf373 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py @@ -71,6 +71,8 @@ FAKE_NA_SERVER_API_1_20.set_api_version(1, 20) VOLUME_VSERVER_NAME = 'fake_vserver' VOLUME_NAMES = ('volume1', 'volume2') VOLUME_NAME = 'volume1' +NAMESPACE_NAME = '/vol/vol1/namespace1' +HOST_NQN = 'nqn.1992-01.example.com:host' DEST_VOLUME_NAME = 'volume-dest' LUN_NAME = 'fake-lun-name' DEST_LUN_NAME = 'new-fake-lun-name' @@ -3058,3 +3060,91 @@ GET_VSERVER_PEERS_RESPONSE_REST = { "num_records": 1, "records": GET_VSERVER_PEERS_RECORDS_REST } + +GET_NAMESPACE_RESPONSE_REST = { + "records": [ + { + "uuid": "fake_uuid1", + "svm": { + "name": "fake_vserver1" + }, + "name": "/vol/fake_vol_001/test", + "location": { + "volume": { + "name": "fake_vol_001" + } + }, + "os_type": "linux", + "space": { + "block_size": 9999, + "size": 999999, + "guarantee": { + "requested": True + } + }, + }, + { + "uuid": "fake_uuid2", + "svm": { + "name": "fake_vserver2" + }, + "name": "/vol/fake_vol_002/test", + "location": { + "volume": { + "name": "fake_vol_002" + } + }, + "os_type": "linux", + "space": { + "block_size": 8888, + "size": 8888888, + "guarantee": { + "requested": True + } + }, + } + ], + "num_records": 2, +} + +SUBSYSTEM = 'openstack-fake_subsystem' +TARGET_NQN = 'nqn.1992-01.example.com:target' +GET_SUBSYSTEM_RESPONSE_REST = { + "records": [ + { + "uuid": "fake_uuid1", + "name": SUBSYSTEM, + "os_type": "linux", + "target_nqn": TARGET_NQN, + } + ], + "num_records": 1, +} + +GET_SUBSYSTEM_MAP_RESPONSE_REST = { + "records": [ + { + "namespace": { + "uuid": FAKE_UUID, + }, + "subsystem": { + "name": SUBSYSTEM + }, + "svm": { + "name": VSERVER_NAME + }, + }, + ], + "num_records": 1, +} + +GET_INTERFACES_NVME_REST = { + 'records': [ + { + "ip": { + "address": "10.10.10.10", + } + } + ], + 'num_records': 1 +} diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py index 66578c79a7e..d798be911ea 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py @@ -3707,3 +3707,399 @@ class NetAppRestCmodeClientTestCase(test.TestCase): 'patch', body=body) self.client._get_volume_by_args.assert_called_once_with( vol_name=fake_client.VOLUME_NAMES[0]) + + def test_get_namespace_list(self): + response = fake_client.GET_NAMESPACE_RESPONSE_REST + + fake_query = { + 'svm.name': 'fake_vserver', + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid' + } + + expected_result = [ + { + 'Vserver': 'fake_vserver1', + 'Volume': 'fake_vol_001', + 'Size': 999999, + 'Qtree': '', + 'Path': '/vol/fake_vol_001/test', + 'OsType': 'linux', + 'SpaceReserved': True, + 'UUID': 'fake_uuid1' + }, + { + 'Vserver': 'fake_vserver2', + 'Volume': 'fake_vol_002', + 'Size': 8888888, + 'Qtree': '', + 'Path': '/vol/fake_vol_002/test', + 'OsType': 'linux', + 'SpaceReserved': True, + 'UUID': 'fake_uuid2' + }, + ] + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_list() + + self.client.send_request.assert_called_once_with( + '/storage/namespaces/', 'get', query=fake_query) + self.assertEqual(expected_result, result) + + def test_get_namespace_list_no_response(self): + response = fake_client.NO_RECORDS_RESPONSE_REST + fake_query = { + 'svm.name': 'fake_vserver', + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid' + } + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_list() + + self.client.send_request.assert_called_once_with( + '/storage/namespaces/', 'get', query=fake_query) + self.assertEqual([], result) + + def test_destroy_namespace(self): + + fake_query = { + 'name': '/vol/fake_vol_001/test', + 'svm': 'fake_vserver' + } + + self.mock_object(self.client, 'send_request') + + self.client.destroy_namespace('/vol/fake_vol_001/test', force=False) + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'delete', query=fake_query) + + def test_destroy_namespace_force_true(self): + + fake_query = { + 'name': '/vol/fake_vol_001/test', + 'svm': 'fake_vserver', + 'allow_delete_while_mapped': 'true' + } + + self.mock_object(self.client, 'send_request') + + self.client.destroy_namespace('/vol/fake_vol_001/test', force=True) + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'delete', query=fake_query) + + def test_clone_namespace(self): + + fake_body = { + 'svm': { + 'name': 'fake_vserver' + }, + 'name': '/vol/fake_volume/fake_new_name', + 'clone': { + 'source': { + 'name': '/vol/fake_volume/fake_name', + } + } + } + + self.mock_object(self.client, 'send_request') + + self.client.clone_namespace('fake_volume', + 'fake_name', + 'fake_new_name') + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'post', body=fake_body) + + def test_get_namespace_by_args(self): + response = fake_client.GET_NAMESPACE_RESPONSE_REST + + lun_info_args = { + 'vserver': fake.VSERVER_NAME, + 'path': fake.LUN_PATH, + 'uuid': fake.UUID1} + + fake_query = { + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid,space.block_size', + 'svm.name': fake.VSERVER_NAME, + 'name': fake.LUN_PATH, + 'uuid': fake.UUID1, + } + + expected_result = [ + { + 'Vserver': 'fake_vserver1', + 'Volume': 'fake_vol_001', + 'Size': 999999, + 'Qtree': '', + 'Path': '/vol/fake_vol_001/test', + 'OsType': 'linux', + 'SpaceReserved': True, + 'UUID': 'fake_uuid1', + 'BlockSize': 9999 + }, + { + 'Vserver': 'fake_vserver2', + 'Volume': 'fake_vol_002', + 'Size': 8888888, + 'Qtree': '', + 'Path': '/vol/fake_vol_002/test', + 'OsType': 'linux', + 'SpaceReserved': True, + 'UUID': 'fake_uuid2', + 'BlockSize': 8888 + }, + ] + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_by_args(**lun_info_args) + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'get', query=fake_query) + self.assertEqual(expected_result, result) + + def test_get_namespace_by_args_no_response(self): + response = fake_client.NO_RECORDS_RESPONSE_REST + + lun_info_args = { + 'vserver': fake.VSERVER_NAME, + 'path': fake.LUN_PATH, + 'uuid': fake.UUID1} + + fake_query = { + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid,space.block_size', + 'svm.name': fake.VSERVER_NAME, + 'name': fake.LUN_PATH, + 'uuid': fake.UUID1, + } + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_by_args(**lun_info_args) + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'get', query=fake_query) + self.assertEqual([], result) + + def test_namespace_resize(self): + fake_body = {'space.size': 9999} + fake_query = {'name': fake.LUN_PATH} + + self.mock_object(self.client, 'send_request') + + self.client.namespace_resize(fake.LUN_PATH, 9999) + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'patch', body=fake_body, query=fake_query) + + def test_get_namespace_sizes_by_volume(self): + response = fake_client.GET_NAMESPACE_RESPONSE_REST + + fake_query = { + 'location.volume.name': 'fake_volume', + 'fields': 'space.size,name' + } + + expected_result = [ + { + 'path': '/vol/fake_vol_001/test', + 'size': 999999, + }, + { + 'path': '/vol/fake_vol_002/test', + 'size': 8888888, + }, + ] + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_sizes_by_volume('fake_volume') + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'get', query=fake_query) + self.assertEqual(expected_result, result) + + def test_get_namespace_sizes_by_volume_no_response(self): + response = fake_client.NO_RECORDS_RESPONSE_REST + + fake_query = { + 'location.volume.name': 'fake_volume', + 'fields': 'space.size,name' + } + + self.mock_object(self.client, 'send_request', return_value=response) + + result = self.client.get_namespace_sizes_by_volume('fake_volume') + + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'get', query=fake_query) + self.assertEqual([], result) + + def test_create_namespace(self): + """Issues API request for creating namespace on volume.""" + self.mock_object(self.client, 'send_request') + + self.client.create_namespace( + fake_client.VOLUME_NAME, fake_client.NAMESPACE_NAME, + fake_client.VOLUME_SIZE_TOTAL, {'OsType': 'linux'}) + + path = f'/vol/{fake_client.VOLUME_NAME}/{fake_client.NAMESPACE_NAME}' + body = { + 'name': path, + 'space.size': str(fake_client.VOLUME_SIZE_TOTAL), + 'os_type': 'linux', + } + self.client.send_request.assert_called_once_with( + '/storage/namespaces', 'post', body=body) + + def test_create_namespace_error(self): + api_error = netapp_api.NaApiError(code=0) + self.mock_object(self.client, 'send_request', side_effect=api_error) + + self.assertRaises( + netapp_api.NaApiError, + self.client.create_namespace, + fake_client.VOLUME_NAME, fake_client.NAMESPACE_NAME, + fake_client.VOLUME_SIZE_TOTAL, {'OsType': 'linux'}) + + def test_get_subsystem_by_host(self): + response = fake_client.GET_SUBSYSTEM_RESPONSE_REST + self.mock_object(self.client, 'send_request', + return_value=response) + + res = self.client.get_subsystem_by_host(fake_client.HOST_NQN) + + expected_res = [ + {'name': fake_client.SUBSYSTEM, 'os_type': 'linux'}] + self.assertEqual(expected_res, res) + query = { + 'svm.name': self.client.vserver, + 'hosts.nqn': fake_client.HOST_NQN, + 'fields': 'name,os_type', + 'name': 'openstack-*', + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystems', 'get', query=query) + + def test_create_subsystem(self): + self.mock_object(self.client, 'send_request') + + self.client.create_subsystem(fake_client.SUBSYSTEM, 'linux', + fake_client.HOST_NQN) + + body = { + 'svm.name': self.client.vserver, + 'name': fake_client.SUBSYSTEM, + 'os_type': 'linux', + 'hosts': [{'nqn': fake_client.HOST_NQN}] + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystems', 'post', body=body) + + def test_get_namespace_map(self): + response = fake_client.GET_SUBSYSTEM_MAP_RESPONSE_REST + self.mock_object(self.client, 'send_request', + return_value=response) + + res = self.client.get_namespace_map(fake_client.NAMESPACE_NAME) + + expected_res = [ + {'subsystem': fake_client.SUBSYSTEM, + 'uuid': fake_client.FAKE_UUID, + 'vserver': fake_client.VSERVER_NAME}] + self.assertEqual(expected_res, res) + query = { + 'namespace.name': fake_client.NAMESPACE_NAME, + 'fields': 'subsystem.name,namespace.uuid,svm.name', + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystem-maps', 'get', query=query) + + def test_map_namespace(self): + response = fake_client.GET_SUBSYSTEM_MAP_RESPONSE_REST + self.mock_object(self.client, 'send_request', + return_value=response) + + res = self.client.map_namespace(fake_client.NAMESPACE_NAME, + fake_client.SUBSYSTEM) + + self.assertEqual(fake_client.FAKE_UUID, res) + body = { + 'namespace.name': fake_client.NAMESPACE_NAME, + 'subsystem.name': fake_client.SUBSYSTEM + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystem-maps', 'post', body=body, + query={'return_records': 'true'}) + + def test_map_namespace_error(self): + api_error = netapp_api.NaApiError(code=0) + self.mock_object(self.client, 'send_request', side_effect=api_error) + + self.assertRaises( + netapp_api.NaApiError, + self.client.map_namespace, + fake_client.VOLUME_NAME, fake_client.SUBSYSTEM) + + @ddt.data( + {'response': fake_client.GET_SUBSYSTEM_RESPONSE_REST, + 'expected': fake_client.TARGET_NQN}, + {'response': fake_client.NO_RECORDS_RESPONSE_REST, + 'expected': None}) + @ddt.unpack + def test_get_nvme_subsystem_nqn(self, response, expected): + self.mock_object(self.client, 'send_request', + return_value=response) + + res = self.client.get_nvme_subsystem_nqn(fake_client.SUBSYSTEM) + + self.assertEqual(expected, res) + query = { + 'fields': 'target_nqn', + 'name': fake_client.SUBSYSTEM, + 'svm.name': self.client.vserver + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystems', 'get', query=query) + + def test_get_nvme_target_portals(self): + response = fake_client.GET_INTERFACES_NVME_REST + self.mock_object(self.client, 'send_request', + return_value=response) + + res = self.client.get_nvme_target_portals() + + expected = ["10.10.10.10"] + self.assertEqual(expected, res) + query = { + 'services': 'data_nvme_tcp', + 'fields': 'ip.address', + 'enabled': 'true', + } + self.client.send_request.assert_called_once_with( + '/network/ip/interfaces', 'get', query=query) + + def test_unmap_namespace(self): + self.mock_object(self.client, 'send_request') + + self.client.unmap_namespace(fake_client.NAMESPACE_NAME, + fake_client.SUBSYSTEM) + + query = { + 'subsystem.name': fake_client.SUBSYSTEM, + 'namespace.name': fake_client.NAMESPACE_NAME, + } + self.client.send_request.assert_called_once_with( + '/protocols/nvme/subsystem-maps', 'delete', query=query) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 2b3d0726553..f5e50047682 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -22,7 +22,9 @@ from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api VOLUME_ID = 'f10d1a84-9b7b-427e-8fec-63c48b509a56' LUN_ID = 'ee6b4cc7-477b-4016-aa0c-7127b4e3af86' LUN_HANDLE = 'fake_lun_handle' +NAMESPACE_HANDLE = 'fake_namespace_handle' LUN_NAME = 'lun1' +NAMESPACE_NAME = 'namespace1' LUN_SIZE = 3 LUN_TABLE = {LUN_NAME: None} SIZE = 1024 @@ -40,6 +42,7 @@ AGGREGATE = 'aggr1' FLEXVOL = 'openstack-flexvol' NFS_FILE_PATH = 'nfsvol' PATH = '/vol/%s/%s' % (POOL_NAME, LUN_NAME) +PATH_NAMESPACE = '/vol/%s/%s' % (POOL_NAME, NAMESPACE_NAME) IMAGE_FILE_ID = 'img-cache-imgid' PROVIDER_LOCATION = 'fake_provider_location' NFS_HOST = 'nfs-host1' @@ -65,6 +68,12 @@ LUN_METADATA = { 'Qtree': None, 'Volume': POOL_NAME, } +NAMESPACE_METADATA = { + 'OsType': None, + 'Path': PATH_NAMESPACE, + 'Qtree': None, + 'Volume': POOL_NAME, +} VOLUME = { 'name': LUN_NAME, 'size': SIZE, @@ -72,6 +81,13 @@ VOLUME = { 'host': HOST_STRING, 'attach_status': DETACHED, } +NAMESPACE_VOLUME = { + 'name': NAMESPACE_NAME, + 'size': SIZE, + 'id': VOLUME_ID, + 'host': HOST_STRING, + 'attach_status': DETACHED, +} NFS_VOLUME = { 'name': NFS_FILE_PATH, 'size': SIZE, @@ -349,6 +365,8 @@ VOLUME_PATH = '/vol/%s/%s' % (NETAPP_VOLUME, VOLUME_NAME) MOUNT_PATH = '168.10.16.11:/' + VOLUME_ID SNAPSHOT_NAME = 'fake_snapshot_name' SNAPSHOT_LUN_HANDLE = 'fake_snapshot_lun_handle' +SNAPSHOT_NAMESPACE_HANDLE = 'fake_snapshot_namespace_handle' + SNAPSHOT_MOUNT = '/fake/mount/path' SNAPSHOT = { @@ -381,6 +399,21 @@ LUN_WITH_METADATA = { } } +NAMESPACE_WITH_METADATA = { + 'handle': 'vserver_fake:/vol/fake_flexvol/volume-fake-uuid', + 'name': 'volume-fake-uuid', + 'size': 20971520, + 'metadata': { + 'Vserver': 'vserver_fake', + 'Volume': 'fake_flexvol', + 'Qtree': None, + 'Path': '/vol/fake_flexvol/volume-fake-uuid', + 'OsType': 'linux', + 'SpaceReserved': 'false', + 'UUID': 'fake-uuid' + } +} + VOLUME_REF = {'name': 'fake_vref_name', 'size': 42} FAKE_CMODE_VOLUMES = ['open123', 'mixed', 'open321'] @@ -723,6 +756,7 @@ test_volume.state = { test_volume.qos = {'qos_policy_group': None} test_volume.host = 'fakehost@backbackend#fakepool' test_volume.name = 'fakename' +test_volume.size = SIZE class test_snapshot(object): @@ -975,3 +1009,6 @@ ADAPTIVE_QOS_POLICY_GROUP_INFO_REST = { } REST_FIELDS = 'uuid,name,style' +SUBSYSTEM = 'openstack-fake-subsystem' +HOST_NQN = 'nqn.1992-01.example.com:string' +TARGET_NQN = 'nqn.1992-01.example.com:target' diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_cmode.py new file mode 100644 index 00000000000..5e63748e1ec --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_cmode.py @@ -0,0 +1,40 @@ +# 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. +"""Mock unit tests for NetApp Data ONTAP FibreChannel storage systems.""" + +from unittest import mock + +from cinder import context +from cinder.tests.unit import test +import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes +from cinder.volume.drivers.netapp.dataontap import nvme_cmode + + +class NetAppCmodeNVMeDriverTestCase(test.TestCase): + + def setUp(self): + super(NetAppCmodeNVMeDriverTestCase, self).setUp() + + kwargs = { + 'configuration': self.get_config_base(), + 'host': 'openstack@netappblock', + } + self.library = nvme_cmode.NetAppCmodeNVMeDriver(**kwargs) + self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client + self.mock_request = mock.Mock() + self.ctxt = context.RequestContext('fake', 'fake', auth_token=True) + + def get_config_base(self): + return na_fakes.create_configuration() diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_library.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_library.py new file mode 100644 index 00000000000..367c974d3c9 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nvme_library.py @@ -0,0 +1,930 @@ +# Copyright (c) 2023 NetApp, Inc. 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. +"""Mock unit tests for the NetApp block storage library""" + +import copy +from unittest import mock +import uuid + +import ddt +from oslo_utils import units + +from cinder import context +from cinder import exception +from cinder.tests.unit import test +from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake +import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap import nvme_library +from cinder.volume.drivers.netapp.dataontap.performance import perf_cmode +from cinder.volume.drivers.netapp.dataontap.utils import capabilities +from cinder.volume.drivers.netapp.dataontap.utils import loopingcalls +from cinder.volume.drivers.netapp.dataontap.utils import utils as dot_utils +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume import volume_utils + + +@ddt.ddt +class NetAppNVMeStorageLibraryTestCase(test.TestCase): + + def setUp(self): + super(NetAppNVMeStorageLibraryTestCase, self).setUp() + + config = na_fakes.create_configuration_cmode() + config.netapp_storage_protocol = 'nvme' + config.netapp_login = 'admin' + config.netapp_password = 'pass' + config.netapp_server_hostname = '127.0.0.1' + config.netapp_transport_type = 'https' + config.netapp_server_port = '443' + config.netapp_vserver = 'openstack' + config.netapp_api_trace_pattern = 'fake_regex' + + kwargs = { + 'configuration': config, + 'host': 'openstack@netappnvme', + } + + self.library = nvme_library.NetAppNVMeStorageLibrary( + 'driver', 'protocol', **kwargs) + self.library.client = mock.Mock() + self.client = self.library.client + self.mock_request = mock.Mock() + self.ctxt = context.RequestContext('fake', 'fake', auth_token=True) + self.vserver = fake.VSERVER_NAME + + self.library.perf_library = mock.Mock() + self.library.ssc_library = mock.Mock() + self.library.vserver = mock.Mock() + + # fakes objects. + self.fake_namespace = nvme_library.NetAppNamespace( + fake.NAMESPACE_HANDLE, fake.NAMESPACE_NAME, fake.SIZE, + fake.NAMESPACE_METADATA) + self.fake_snapshot_namespace = nvme_library.NetAppNamespace( + fake.SNAPSHOT_NAMESPACE_HANDLE, fake.SNAPSHOT_NAME, fake.SIZE, + None) + self.mock_object(self.library, 'namespace_table') + self.library.namespace_table = { + fake.NAMESPACE_NAME: self.fake_namespace, + fake.SNAPSHOT_NAME: self.fake_snapshot_namespace, + } + + @mock.patch.object(perf_cmode, 'PerformanceCmodeLibrary', mock.Mock()) + @mock.patch.object(capabilities.CapabilitiesLibrary, + 'cluster_user_supported') + @mock.patch.object(capabilities.CapabilitiesLibrary, + 'check_api_permissions') + @mock.patch.object(na_utils, 'check_flags') + def test_do_setup_san_unconfigured(self, mock_check_flags, + mock_check_api_permissions, + mock_cluster_user_supported): + self.library.configuration.netapp_namespace_ostype = None + self.library.configuration.netapp_host_type = None + self.library.backend_name = 'fake_backend' + fake_client = mock.Mock() + fake_client.vserver = 'fake_vserver' + self.mock_object(dot_utils, 'get_client_for_backend', + return_value=fake_client) + + self.library.do_setup(mock.Mock()) + + self.assertTrue(mock_check_flags.called) + mock_check_api_permissions.assert_called_once_with() + mock_cluster_user_supported.assert_called_once_with() + self.assertEqual('linux', self.library.namespace_ostype) + self.assertEqual('linux', self.library.host_type) + dot_utils.get_client_for_backend.assert_called_once_with( + 'fake_backend', force_rest=True) + + def test_check_for_setup_error(self): + self.mock_object(self.library, '_get_flexvol_to_pool_map', + return_value=fake.POOL_NAME) + self.mock_object(self.library, '_add_looping_tasks') + self.library.namespace_ostype = 'linux' + self.library.host_type = 'linux' + self.mock_object(self.library.client, 'get_namespace_list', + return_value='fake_namespace_list') + self.mock_object(self.library, '_extract_and_populate_namespaces') + self.mock_object(self.library.loopingcalls, 'start_tasks') + + self.library.check_for_setup_error() + + self.library._get_flexvol_to_pool_map.assert_called_once_with() + self.library._add_looping_tasks.assert_called_once_with() + self.library.client.get_namespace_list.assert_called_once_with() + self.library._extract_and_populate_namespaces.assert_called_once_with( + 'fake_namespace_list') + self.library.loopingcalls.start_tasks.assert_called_once_with() + + @ddt.data( + {'pool_map': None, 'namespace': 'linux', 'host': 'linux'}, + {'pool_map': 'fake_map', 'namespace': 'fake', 'host': 'linux'}, + {'pool_map': 'fake_map', 'namespace': 'linux', 'host': 'fake'}) + @ddt.unpack + def test_check_for_setup_error_error(self, pool_map, namespace, host): + self.mock_object(self.library, '_get_flexvol_to_pool_map', + return_value=pool_map) + self.library.namespace_ostype = namespace + self.library.host_type = host + self.mock_object(self.library, '_add_looping_tasks') + + self.assertRaises( + na_utils.NetAppDriverException, + self.library.check_for_setup_error) + + def test_create_volume(self): + volume_size_in_bytes = int(fake.SIZE) * units.Gi + self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) + self.mock_object(self.library.client, 'create_namespace') + self.mock_object(self.library, '_create_namespace_handle') + self.mock_object(self.library, '_add_namespace_to_table') + + volume1 = copy.deepcopy(fake.test_volume) + self.library.create_volume(volume1) + + fake_metadata = { + 'OsType': self.library.namespace_ostype, + 'Path': '/vol/aggr1/fakename', + 'Volume': 'aggr1', + 'Qtree': None + } + self.library.client.create_namespace.assert_called_once_with( + fake.POOL_NAME, 'fakename', volume_size_in_bytes, fake_metadata) + self.library._create_namespace_handle.assert_called_once_with( + fake_metadata) + + def test_create_namespace_handle(self): + self.library.vserver = fake.VSERVER_NAME + res = self.library._create_namespace_handle(fake.NAMESPACE_METADATA) + + self.assertEqual(f'{fake.VSERVER_NAME}:{fake.PATH_NAMESPACE}', res) + + def test__extract_namespace_info(self): + self.mock_object(self.library, '_create_namespace_handle', + return_value=fake.NAMESPACE_HANDLE) + + namespace = {'Path': fake.PATH_NAMESPACE, 'Size': fake.SIZE} + res = self.library._extract_namespace_info(namespace) + + self.assertEqual(fake.NAMESPACE_NAME, res.name) + self.library._create_namespace_handle.assert_called_once_with( + namespace) + + def test__extract_and_populate_namespaces(self): + self.mock_object(self.library, '_extract_namespace_info', + return_value='fake_namespace') + self.mock_object(self.library, '_add_namespace_to_table') + + self.library._extract_and_populate_namespaces([fake.NAMESPACE_NAME]) + + self.library._extract_namespace_info.assert_called_once_with( + fake.NAMESPACE_NAME) + self.library._add_namespace_to_table.assert_called_once_with( + 'fake_namespace') + + def test__add_namespace_to_table(self): + namespace = nvme_library.NetAppNamespace( + fake.NAMESPACE_HANDLE, 'fake_namespace2', fake.SIZE, None) + self.library._add_namespace_to_table(namespace) + + has_namespace = 'fake_namespace2' in self.library.namespace_table + self.assertTrue(has_namespace) + self.assertEqual(namespace, + self.library.namespace_table['fake_namespace2']) + + def test__add_namespace_to_table_error(self): + self.assertRaises( + exception.VolumeBackendAPIException, + self.library._add_namespace_to_table, + 'fake' + ) + + def test__get_namespace_from_table_error(self): + self.mock_object(self.library.client, 'get_namespace_list', + return_value='fake_list') + self.mock_object(self.library, '_extract_and_populate_namespaces') + + self.assertRaises( + exception.VolumeNotFound, + self.library._get_namespace_from_table, + 'fake') + + self.library.client.get_namespace_list.assert_called_once_with() + self.library._extract_and_populate_namespaces.assert_called_once_with( + 'fake_list') + + def test__get_namespace_from_table(self): + + res = self.library._get_namespace_from_table(fake.NAMESPACE_NAME) + + self.assertEqual(self.fake_namespace, res) + + @ddt.data(exception.VolumeNotFound, netapp_api.NaApiError) + def test__get_namespace_attr_error(self, error_obj): + self.mock_object(self.library, '_get_namespace_from_table', + side_effect=error_obj) + + res = self.library._get_namespace_attr('namespace', 'name') + + self.assertIsNone(res) + + def test__get_namespace_attr(self): + self.mock_object(self.library, '_get_namespace_from_table', + return_value=self.fake_namespace) + + res = self.library._get_namespace_attr('namespace', 'name') + + self.assertEqual(fake.NAMESPACE_NAME, res) + + def test_create_volume_error(self): + self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) + self.mock_object(self.library.client, 'create_namespace', + side_effect=exception.VolumeBackendAPIException) + self.mock_object(self.library, '_create_namespace_handle') + self.mock_object(self.library, '_add_namespace_to_table') + + self.assertRaises( + exception.VolumeBackendAPIException, + self.library.create_volume, + copy.deepcopy(fake.test_volume)) + + def test__update_ssc(self): + mock_get_flexvol = self.mock_object( + self.library, '_get_flexvol_to_pool_map', + return_value='fake_pool_map') + self.library.ssc_library.update_ssc = mock.Mock() + + self.library._update_ssc() + + mock_get_flexvol.assert_called_once_with() + self.library.ssc_library.update_ssc.assert_called_once_with( + 'fake_pool_map') + + def test__find_mapped_namespace_subsystem(self): + self.mock_object(self.library.client, 'get_subsystem_by_host', + return_value=[{'name': fake.SUBSYSTEM}]) + self.mock_object( + self.library.client, 'get_namespace_map', + return_value=[{ + 'subsystem': fake.SUBSYSTEM, + 'uuid': fake.UUID1 + }]) + + subsystem, n_uuid = self.library._find_mapped_namespace_subsystem( + fake.NAMESPACE_NAME, fake.HOST_NQN) + + self.assertEqual(fake.SUBSYSTEM, subsystem) + self.assertEqual(fake.UUID1, n_uuid) + self.library.client.get_subsystem_by_host.assert_called_once_with( + fake.HOST_NQN) + self.library.client.get_namespace_map.assert_called_once_with( + fake.NAMESPACE_NAME) + + def test_delete_volume(self): + self.mock_object(self.library, '_delete_namespace') + + self.library.delete_volume(fake.NAMESPACE_VOLUME) + + self.library._delete_namespace.assert_called_once_with( + fake.NAMESPACE_NAME) + + def test__delete_namespace(self): + namespace = copy.deepcopy(fake.NAMESPACE_WITH_METADATA) + self.mock_object(self.library, '_get_namespace_attr', + return_value=namespace['metadata']) + self.mock_object(self.library.client, 'destroy_namespace') + + self.library._delete_namespace(fake.NAMESPACE_NAME) + + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'metadata') + self.library.client.destroy_namespace.assert_called_once_with( + namespace['metadata']['Path']) + has_namespace = fake.NAMESPACE_NAME in self.library.namespace_table + self.assertFalse(has_namespace) + + def test__delete_namespace_not_found(self): + namespace = copy.deepcopy(fake.NAMESPACE_WITH_METADATA) + self.mock_object(self.library, '_get_namespace_attr', + return_value=namespace['metadata']) + error = netapp_api.NaApiError( + code=netapp_api.REST_NAMESPACE_EOBJECTNOTFOUND[0]) + self.mock_object(self.library.client, 'destroy_namespace', + side_effect=error) + + self.library._delete_namespace(fake.NAMESPACE_NAME) + + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'metadata') + self.library.client.destroy_namespace.assert_called_once_with( + namespace['metadata']['Path']) + has_namespace = fake.NAMESPACE_NAME in self.library.namespace_table + self.assertFalse(has_namespace) + + def test__delete_namespace_error(self): + namespace = copy.deepcopy(fake.NAMESPACE_WITH_METADATA) + self.mock_object(self.library, '_get_namespace_attr', + return_value=namespace['metadata']) + self.mock_object(self.library.client, 'destroy_namespace', + side_effect=netapp_api.NaApiError) + + self.assertRaises(na_utils.NetAppDriverException, + self.library._delete_namespace, + fake.NAMESPACE_NAME) + + def test__delete_namespace_no_metadata(self): + self.mock_object(self.library, '_get_namespace_attr', + return_value=None) + self.mock_object(self.library.client, 'destroy_namespace') + + self.library._delete_namespace(fake.NAMESPACE_NAME) + + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'metadata') + self.library.client.destroy_namespace.assert_not_called() + + def test_add_looping_tasks(self): + mock_add_task = self.mock_object(self.library.loopingcalls, 'add_task') + self.mock_object(self.library, '_update_ssc') + + self.library._add_looping_tasks() + + self.library._update_ssc.assert_called_once_with() + mock_add_task.assert_has_calls([ + mock.call(self.library._update_ssc, loopingcalls.ONE_HOUR, + loopingcalls.ONE_HOUR), + mock.call(self.library._handle_ems_logging, + loopingcalls.ONE_HOUR)]) + + def test_handle_ems_logging(self): + volume_list = ['vol0', 'vol1', 'vol2'] + self.mock_object( + self.library.ssc_library, 'get_ssc_flexvol_names', + return_value=volume_list) + self.mock_object( + dot_utils, 'build_ems_log_message_0', + return_value='fake_base_ems_log_message') + self.mock_object( + dot_utils, 'build_ems_log_message_1', + return_value='fake_pool_ems_log_message') + mock_send_ems_log_message = self.mock_object( + self.client, 'send_ems_log_message') + + self.library._handle_ems_logging() + + mock_send_ems_log_message.assert_has_calls([ + mock.call('fake_base_ems_log_message'), + mock.call('fake_pool_ems_log_message'), + ]) + dot_utils.build_ems_log_message_0.assert_called_once_with( + self.library.driver_name, self.library.app_version) + dot_utils.build_ems_log_message_1.assert_called_once_with( + self.library.driver_name, self.library.app_version, + self.library.vserver, volume_list, []) + + def test_get_pool(self): + namespace = copy.deepcopy(fake.NAMESPACE_WITH_METADATA) + self.mock_object(self.library, '_get_namespace_attr', + return_value=namespace['metadata']) + + res = self.library.get_pool(fake.VOLUME) + + self.assertEqual('fake_flexvol', res) + self.library._get_namespace_attr.assert_called_once_with( + fake.LUN_NAME, 'metadata') + + def test_delete_snapshot(self): + mock__delete = self.mock_object(self.library, '_delete_namespace') + + self.library.delete_snapshot(fake.SNAPSHOT) + + mock__delete.assert_called_once_with(fake.SNAPSHOT_NAME) + + def test_create_volume_from_snapshot(self): + self.mock_object(self.library, '_clone_source_to_destination') + + self.library.create_volume_from_snapshot(fake.NAMESPACE_VOLUME, + fake.SNAPSHOT) + + self.library._clone_source_to_destination.assert_called_once_with( + {'name': fake.SNAPSHOT_NAME, 'size': fake.SIZE}, + fake.NAMESPACE_VOLUME) + + def test_create_cloned_volume(self): + self.mock_object(self.library, '_get_namespace_from_table', + return_value=self.fake_namespace) + self.mock_object(self.library, '_clone_source_to_destination') + + src_volume = {'size': fake.SIZE, 'name': 'fake_name'} + self.library.create_cloned_volume(fake.NAMESPACE_VOLUME, src_volume) + + self.library._get_namespace_from_table.assert_called_once_with( + 'fake_name') + self.library._clone_source_to_destination.assert_called_once_with( + {'name': fake.NAMESPACE_NAME, 'size': fake.SIZE}, + fake.NAMESPACE_VOLUME) + + def test_clone_source_to_destination(self): + self.mock_object(self.library, '_clone_namespace') + self.mock_object(self.library, '_extend_volume') + self.mock_object(self.library, 'delete_volume') + + source_vol = {'size': fake.SIZE, 'name': 'fake_source'} + dest_size = fake.SIZE + 12 + dest_vol = {'size': dest_size, 'name': 'fake_dest'} + self.library._clone_source_to_destination(source_vol, dest_vol) + + self.library._clone_namespace.assert_called_once_with( + 'fake_source', 'fake_dest') + self.library._extend_volume.assert_called_once_with( + dest_vol, dest_size) + self.library.delete_volume.assert_not_called() + + def test_clone_source_to_destination_clone_error(self): + self.mock_object(self.library, '_clone_namespace', + side_effect=exception.VolumeBackendAPIException) + self.mock_object(self.library, '_extend_volume') + self.mock_object(self.library, 'delete_volume') + + source_vol = {'size': fake.SIZE, 'name': 'fake_source'} + dest_size = fake.SIZE + 12 + dest_vol = {'size': dest_size, 'name': 'fake_dest'} + self.assertRaises( + exception.VolumeBackendAPIException, + self.library._clone_source_to_destination, + source_vol, dest_vol) + + def test_clone_source_to_destination_extend_error(self): + self.mock_object(self.library, '_clone_namespace') + self.mock_object(self.library, '_extend_volume', + side_effect=exception.VolumeBackendAPIException) + self.mock_object(self.library, 'delete_volume') + + source_vol = {'size': fake.SIZE, 'name': 'fake_source'} + dest_size = fake.SIZE + 12 + dest_vol = {'size': dest_size, 'name': 'fake_dest'} + self.assertRaises( + exception.VolumeBackendAPIException, + self.library._clone_source_to_destination, + source_vol, dest_vol) + + @ddt.data(True, False) + def test_get_volume_stats(self, refresh): + self.library._stats = 'fake_stats' + self.mock_object(self.library, '_update_volume_stats') + + res = self.library.get_volume_stats(refresh, filter_function='filter', + goodness_function='good') + + self.assertEqual('fake_stats', res) + if refresh: + self.library._update_volume_stats.assert_called_once_with( + filter_function='filter', goodness_function='good') + else: + self.library._update_volume_stats.assert_not_called() + + def test__update_volume_stats(self): + + self.library.VERSION = '1.0.0' + self.library.driver_protocol = 'nvme' + self.mock_object(self.library, '_get_pool_stats', + return_value='fake_pools') + self.library._update_volume_stats(filter_function='filter', + goodness_function='good') + + expected_ssc = { + 'volume_backend_name': 'driver', + 'vendor_name': 'NetApp', + 'driver_version': '1.0.0', + 'pools': 'fake_pools', + 'sparse_copy_volume': True, + 'replication_enabled': False, + 'storage_protocol': 'nvme', + } + self.assertEqual(expected_ssc, self.library._stats) + + @ddt.data({'cluster_credentials': False, + 'report_provisioned_capacity': False}, + {'cluster_credentials': True, + 'report_provisioned_capacity': True}) + @ddt.unpack + def test_get_pool_stats(self, cluster_credentials, + report_provisioned_capacity): + self.library.using_cluster_credentials = cluster_credentials + conf = self.library.configuration + conf.netapp_driver_reports_provisioned_capacity = ( + report_provisioned_capacity) + + ssc = { + 'vola': { + 'pool_name': 'vola', + 'thick_provisioning_support': True, + 'thin_provisioning_support': False, + 'netapp_thin_provisioned': 'false', + 'netapp_compression': 'false', + 'netapp_mirrored': 'false', + 'netapp_dedup': 'true', + 'netapp_aggregate': 'aggr1', + 'netapp_raid_type': 'raid_dp', + 'netapp_disk_type': 'SSD', + 'netapp_is_flexgroup': 'false', + }, + } + mock_get_ssc = self.mock_object(self.library.ssc_library, + 'get_ssc', + return_value=ssc) + mock_get_aggrs = self.mock_object(self.library.ssc_library, + 'get_ssc_aggregates', + return_value=['aggr1']) + + self.library.reserved_percentage = 5 + self.library.max_over_subscription_ratio = 10 + self.library.perf_library.get_node_utilization_for_pool = ( + mock.Mock(return_value=30.0)) + mock_capacities = { + 'size-total': 10737418240.0, + 'size-available': 2147483648.0, + } + namespaces_provisioned_cap = [{ + 'path': '/vol/volume-ae947c9b-2392-4956-b373-aaac4521f37e', + 'size': 5368709120.0 # 5GB + }, { + 'path': '/vol/snapshot-527eedad-a431-483d-b0ca-18995dd65b66', + 'size': 1073741824.0 # 1GB + }] + self.mock_object(self.client, + 'get_flexvol_capacity', + return_value=mock_capacities) + self.mock_object(self.client, + 'get_namespace_sizes_by_volume', + return_value=namespaces_provisioned_cap) + self.mock_object(self.client, + 'get_flexvol_dedupe_used_percent', + return_value=55.0) + + aggr_capacities = { + 'aggr1': { + 'percent-used': 45, + 'size-available': 59055800320.0, + 'size-total': 107374182400.0, + }, + } + mock_get_aggr_capacities = self.mock_object( + self.client, 'get_aggregate_capacities', + return_value=aggr_capacities) + + result = self.library._get_pool_stats(filter_function='filter', + goodness_function='goodness') + + expected = [{ + 'pool_name': 'vola', + 'QoS_support': False, + 'consistencygroup_support': False, + 'consistent_group_snapshot_enabled': False, + 'reserved_percentage': 5, + 'max_over_subscription_ratio': 10, + 'multiattach': False, + 'total_capacity_gb': 10.0, + 'free_capacity_gb': 2.0, + 'netapp_dedupe_used_percent': 55.0, + 'netapp_aggregate_used_percent': 45, + 'utilization': 30.0, + 'filter_function': 'filter', + 'goodness_function': 'goodness', + 'thick_provisioning_support': True, + 'thin_provisioning_support': False, + 'netapp_thin_provisioned': 'false', + 'netapp_compression': 'false', + 'netapp_mirrored': 'false', + 'netapp_dedup': 'true', + 'netapp_aggregate': 'aggr1', + 'netapp_raid_type': 'raid_dp', + 'netapp_disk_type': 'SSD', + 'online_extend_support': False, + 'netapp_is_flexgroup': 'false', + }] + if report_provisioned_capacity: + expected[0].update({'provisioned_capacity_gb': 5.0}) + + if not cluster_credentials: + expected[0].update({ + 'netapp_aggregate_used_percent': 0, + 'netapp_dedupe_used_percent': 0.0 + }) + + self.assertEqual(expected, result) + mock_get_ssc.assert_called_once_with() + if cluster_credentials: + mock_get_aggrs.assert_called_once_with() + mock_get_aggr_capacities.assert_called_once_with(['aggr1']) + + @ddt.data({}, None) + def test_get_pool_stats_no_ssc_vols(self, ssc): + + mock_get_ssc = self.mock_object(self.library.ssc_library, + 'get_ssc', + return_value=ssc) + + pools = self.library._get_pool_stats() + + self.assertListEqual([], pools) + mock_get_ssc.assert_called_once_with() + + @ddt.data(r'open+|demix+', 'open.+', r'.+\d', '^((?!mix+).)*$', + 'open123, open321') + def test_get_pool_map_match_selected_pools(self, patterns): + + self.library.configuration.netapp_pool_name_search_pattern = patterns + mock_list_flexvols = self.mock_object( + self.library.client, 'list_flexvols', + return_value=fake.FAKE_CMODE_VOLUMES) + + result = self.library._get_flexvol_to_pool_map() + + expected = { + 'open123': { + 'pool_name': 'open123', + }, + 'open321': { + 'pool_name': 'open321', + }, + } + self.assertEqual(expected, result) + mock_list_flexvols.assert_called_once_with() + + @ddt.data('', 'mix.+|open.+', '.+', 'open123, mixed, open321', + '.*?') + def test_get_pool_map_match_all_pools(self, patterns): + + self.library.configuration.netapp_pool_name_search_pattern = patterns + mock_list_flexvols = self.mock_object( + self.library.client, 'list_flexvols', + return_value=fake.FAKE_CMODE_VOLUMES) + + result = self.library._get_flexvol_to_pool_map() + + self.assertEqual(fake.FAKE_CMODE_POOL_MAP, result) + mock_list_flexvols.assert_called_once_with() + + def test_get_pool_map_invalid_conf(self): + """Verify an exception is raised if the regex pattern is invalid""" + self.library.configuration.netapp_pool_name_search_pattern = '(.+' + + self.assertRaises(exception.InvalidConfigurationValue, + self.library._get_flexvol_to_pool_map) + + @ddt.data('abc|stackopen|openstack|abc*', 'abc', 'stackopen', 'openstack', + 'abc*', '^$') + def test_get_pool_map_non_matching_patterns(self, patterns): + + self.library.configuration.netapp_pool_name_search_pattern = patterns + mock_list_flexvols = self.mock_object( + self.library.client, 'list_flexvols', + return_value=fake.FAKE_CMODE_VOLUMES) + + result = self.library._get_flexvol_to_pool_map() + + self.assertEqual({}, result) + mock_list_flexvols.assert_called_once_with() + + def test_create_snapshot(self): + self.mock_object(self.library, '_create_snapshot') + + self.library.create_snapshot('fake_snap') + + self.library._create_snapshot.assert_called_once_with('fake_snap') + + def test__create_snapshot(self): + self.mock_object(self.library, '_get_namespace_from_table', + return_value=self.fake_namespace) + self.mock_object(self.library, '_clone_namespace') + + self.library._create_snapshot(fake.SNAPSHOT) + + self.library._get_namespace_from_table.assert_called_once_with( + fake.VOLUME_NAME) + self.library._clone_namespace.assert_called_once_with( + fake.NAMESPACE_NAME, fake.SNAPSHOT_NAME) + + def test__clone_namespace_error(self): + self.mock_object(self.library, '_get_namespace_attr', + return_value=fake.NAMESPACE_METADATA) + self.mock_object(self.library.client, 'clone_namespace') + self.mock_object(self.library.client, 'get_namespace_by_args', + return_value=[]) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.library._clone_namespace, + fake.NAMESPACE_NAME, + 'fake_new_name') + + def test__clone_namespace(self): + self.mock_object(self.library, '_get_namespace_attr', + return_value=fake.NAMESPACE_METADATA) + self.mock_object(self.library.client, 'clone_namespace') + fake_namespace_res = { + 'Vserver': fake.VSERVER_NAME, + 'Path': fake.NAMESPACE_NAME, + 'Size': 1024 + } + self.mock_object(self.library.client, 'get_namespace_by_args', + return_value=[fake_namespace_res]) + self.mock_object(self.library, '_add_namespace_to_table') + + self.library._clone_namespace(fake.NAMESPACE_NAME, 'fake_new_name') + + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'metadata') + self.library.client.clone_namespace.assert_called_once_with( + fake.POOL_NAME, fake.NAMESPACE_NAME, 'fake_new_name') + self.library.client.get_namespace_by_args.assert_called_once() + self.library._add_namespace_to_table.assert_called_once() + + def test_ensure_export(self): + self.mock_object(self.library, '_get_namespace_attr', + return_value='fake_handle') + + res = self.library.ensure_export(mock.Mock(), fake.NAMESPACE_VOLUME) + + self.assertEqual({'provider_location': 'fake_handle'}, res) + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'handle') + + def test_create_export(self): + self.mock_object(self.library, '_get_namespace_attr', + return_value='fake_handle') + + res = self.library.create_export(mock.Mock(), fake.NAMESPACE_VOLUME) + + self.assertEqual({'provider_location': 'fake_handle'}, res) + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'handle') + + def test__extend_volume(self): + self.mock_object(self.library, '_get_namespace_from_table', + return_value=self.fake_namespace) + self.mock_object(self.library.client, 'namespace_resize') + + self.library._extend_volume(fake.NAMESPACE_VOLUME, fake.SIZE) + + new_bytes = str(int(fake.SIZE) * units.Gi) + self.assertEqual(new_bytes, self.fake_namespace.size) + self.library._get_namespace_from_table.assert_called_once_with( + fake.NAMESPACE_NAME) + self.library.client.namespace_resize.assert_called_once_with( + fake.PATH_NAMESPACE, new_bytes) + + @ddt.data([{'name': fake.SUBSYSTEM, 'os_type': 'linux'}], []) + def test__get_or_create_subsystem(self, subs): + self.mock_object(self.library.client, 'get_subsystem_by_host', + return_value=subs) + self.mock_object(self.library.client, 'create_subsystem') + self.mock_object(uuid, 'uuid4', return_value='fake_uuid') + + sub, os = self.library._get_or_create_subsystem(fake.HOST_NQN, 'linux') + + self.library.client.get_subsystem_by_host.assert_called_once_with( + fake.HOST_NQN) + self.assertEqual('linux', os) + if subs: + self.assertEqual(fake.SUBSYSTEM, sub) + else: + self.library.client.create_subsystem.assert_called_once_with( + sub, 'linux', fake.HOST_NQN) + expected_sub = 'openstack-fake_uuid' + self.assertEqual(expected_sub, sub) + + def test__map_namespace(self): + self.library.host_type = 'win' + self.mock_object(self.library, '_get_or_create_subsystem', + return_value=(fake.SUBSYSTEM, 'linux')) + self.mock_object(self.library, '_get_namespace_attr', + return_value=fake.NAMESPACE_METADATA) + self.mock_object(self.library.client, 'map_namespace', + return_value=fake.UUID1) + + sub, n_uuid = self.library._map_namespace( + fake.NAMESPACE_NAME, fake.HOST_NQN) + + self.assertEqual(fake.SUBSYSTEM, sub) + self.assertEqual(fake.UUID1, n_uuid) + self.library._get_or_create_subsystem.assert_called_once_with( + fake.HOST_NQN, 'win') + self.library.client.map_namespace.assert_called_once_with( + fake.PATH_NAMESPACE, fake.SUBSYSTEM) + + def test_initialize_connection(self): + self.mock_object(self.library, '_map_namespace', + return_value=(fake.SUBSYSTEM, fake.UUID1)) + self.mock_object(self.library.client, 'get_nvme_subsystem_nqn', + return_value=fake.TARGET_NQN) + self.mock_object(self.library.client, 'get_nvme_target_portals', + return_value=['fake_ip']) + + res = self.library.initialize_connection( + fake.NAMESPACE_VOLUME, {'nqn': fake.HOST_NQN}) + + expected_conn_info = { + "driver_volume_type": "nvmeof", + "data": { + "target_nqn": fake.TARGET_NQN, + "host_nqn": fake.HOST_NQN, + "portals": [('fake_ip', 4420, 'tcp')], + "vol_uuid": fake.UUID1 + } + } + self.assertEqual(expected_conn_info, res) + self.library._map_namespace.assert_called_once_with( + fake.NAMESPACE_NAME, fake.HOST_NQN) + self.library.client.get_nvme_subsystem_nqn.assert_called_once_with( + fake.SUBSYSTEM) + self.library.client.get_nvme_target_portals.assert_called_once_with() + + def test_initialize_connection_error_no_host(self): + self.mock_object(self.library, '_map_namespace', + return_value=(fake.SUBSYSTEM, fake.UUID1)) + self.mock_object(self.library.client, 'get_nvme_subsystem_nqn', + return_value=fake.TARGET_NQN) + self.mock_object(self.library.client, 'get_nvme_target_portals', + return_value=['fake_ip']) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.library.initialize_connection, + fake.NAMESPACE_VOLUME, {}) + + def test_initialize_connection_error_no_target(self): + self.mock_object(self.library, '_map_namespace', + return_value=(fake.SUBSYSTEM, fake.UUID1)) + self.mock_object(self.library.client, 'get_nvme_subsystem_nqn', + return_value=None) + self.mock_object(self.library.client, 'get_nvme_target_portals', + return_value=['fake_ip']) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.library.initialize_connection, + fake.NAMESPACE_VOLUME, {'nqn': fake.HOST_NQN}) + + def test_initialize_connection_error_no_portals(self): + self.mock_object(self.library, '_map_namespace', + return_value=(fake.SUBSYSTEM, fake.UUID1)) + self.mock_object(self.library.client, 'get_nvme_subsystem_nqn', + return_value=fake.TARGET_NQN) + self.mock_object(self.library.client, 'get_nvme_target_portals', + return_value=[]) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.library.initialize_connection, + fake.NAMESPACE_VOLUME, {'nqn': fake.HOST_NQN}) + + @ddt.data(fake.HOST_NQN, None) + def test__unmap_namespace(self, host_nqn): + mock_find = self.mock_object( + self.library, '_find_mapped_namespace_subsystem', + return_value=(fake.SUBSYSTEM, 'fake')) + self.mock_object(self.library.client, 'get_namespace_map', + return_value=[{'subsystem': fake.SUBSYSTEM}]) + self.mock_object(self.library.client, 'unmap_namespace') + + self.library._unmap_namespace(fake.PATH_NAMESPACE, host_nqn) + + if host_nqn: + mock_find.assert_called_once_with(fake.PATH_NAMESPACE, + fake.HOST_NQN) + self.library.client.get_namespace_map.assert_not_called() + else: + self.library._find_mapped_namespace_subsystem.assert_not_called() + self.library.client.get_namespace_map.assert_called_once_with( + fake.PATH_NAMESPACE) + self.library.client.unmap_namespace.assert_called_once_with( + fake.PATH_NAMESPACE, fake.SUBSYSTEM) + + @ddt.data(None, {'nqn': fake.HOST_NQN}) + def test_terminate_connection(self, connector): + self.mock_object(self.library, '_get_namespace_attr', + return_value=fake.NAMESPACE_METADATA) + self.mock_object(self.library, '_unmap_namespace') + + self.library.terminate_connection(fake.NAMESPACE_VOLUME, connector) + + self.library._get_namespace_attr.assert_called_once_with( + fake.NAMESPACE_NAME, 'metadata') + host = connector['nqn'] if connector else None + self.library._unmap_namespace(fake.PATH_NAMESPACE, host) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 55b312f735d..a22ae9734d4 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -39,7 +39,8 @@ NETAPP_UNIFIED_DRIVER_REGISTRY = { { 'iscsi': DATAONTAP_PATH + '.iscsi_cmode.NetAppCmodeISCSIDriver', 'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver', - 'fc': DATAONTAP_PATH + '.fc_cmode.NetAppCmodeFibreChannelDriver' + 'fc': DATAONTAP_PATH + '.fc_cmode.NetAppCmodeFibreChannelDriver', + 'nvme': DATAONTAP_PATH + '.nvme_cmode.NetAppCmodeNVMeDriver' }} diff --git a/cinder/volume/drivers/netapp/dataontap/client/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py index bfe60449caa..dd3cce84dc1 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/api.py +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -651,6 +651,7 @@ REST_SNAPMIRROR_IN_PROGRESS = '13303810' REST_UPDATE_SNAPMIRROR_FAILED = '13303844' REST_NO_SUCH_LUN_MAP = '5374922' REST_NO_SUCH_FILE = '6684674' +REST_NAMESPACE_EOBJECTNOTFOUND = ('72090006', '72090006') class RestNaServer(object): diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py index 4a3154d6c86..c6c55a30695 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -2519,3 +2519,276 @@ class RestClient(object): self.send_request( f'/storage/volumes/{unique_volume["uuid"]}/files/{orig_file_name}', 'patch', body=body) + + def get_namespace_list(self): + """Gets the list of namespaces on filer. + + Gets the namespaces from cluster with vserver. + """ + + query = { + 'svm.name': self.vserver, + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid' + } + + response = self.send_request( + '/storage/namespaces/', 'get', query=query) + + namespace_list = [] + for namespace in response.get('records', []): + namespace_info = {} + namespace_info['Vserver'] = namespace['svm']['name'] + namespace_info['Volume'] = namespace['location']['volume']['name'] + namespace_info['Size'] = namespace['space']['size'] + namespace_info['Qtree'] = ( + namespace['location'].get('qtree', {}).get('name', '')) + namespace_info['Path'] = namespace['name'] + namespace_info['OsType'] = namespace['os_type'] + namespace_info['SpaceReserved'] = ( + namespace['space']['guarantee']['requested']) + namespace_info['UUID'] = namespace['uuid'] + + namespace_list.append(namespace_info) + + return namespace_list + + def create_namespace(self, volume_name, namespace_name, size, metadata): + """Issues API request for creating namespace on volume.""" + + path = f'/vol/{volume_name}/{namespace_name}' + initial_size = size + + body = { + 'name': path, + 'space.size': str(initial_size), + 'os_type': metadata['OsType'], + } + + try: + self.send_request('/storage/namespaces', 'post', body=body) + except netapp_api.NaApiError as ex: + with excutils.save_and_reraise_exception(): + LOG.error('Error provisioning volume %(namespace_name)s on ' + '%(volume_name)s. Details: %(ex)s', + { + 'namespace_name': namespace_name, + 'volume_name': volume_name, + 'ex': ex, + }) + + def destroy_namespace(self, path, force=True): + """Destroys the namespace at the path.""" + query = { + 'name': path, + 'svm': self.vserver + } + + if force: + query['allow_delete_while_mapped'] = 'true' + + self.send_request('/storage/namespaces', 'delete', query=query) + + def clone_namespace(self, volume, name, new_name): + """Clones namespace on vserver.""" + LOG.debug('Cloning namespace - volume: %(volume)s, name: %(name)s, ' + 'new_name: %(new_name)s', + { + 'volume': volume, + 'name': name, + 'new_name': new_name, + }) + + source_path = f'/vol/{volume}/{name}' + body = { + 'svm': { + 'name': self.vserver + }, + 'name': f'/vol/{volume}/{new_name}', + 'clone': { + 'source': { + 'name': source_path, + } + } + } + self.send_request('/storage/namespaces', 'post', body=body) + + def get_namespace_by_args(self, **namespace_info_args): + """Retrieves namespace with specified args.""" + + query = { + 'fields': 'svm.name,location.volume.name,space.size,' + 'location.qtree.name,name,os_type,' + 'space.guarantee.requested,uuid,space.block_size' + } + + if namespace_info_args: + if 'vserver' in namespace_info_args: + query['svm.name'] = namespace_info_args['vserver'] + if 'path' in namespace_info_args: + query['name'] = namespace_info_args['path'] + if 'uuid' in namespace_info_args: + query['uuid'] = namespace_info_args['uuid'] + + response = self.send_request('/storage/namespaces', 'get', query=query) + + namespace_list = [] + for namespace in response.get('records', []): + namespace_info = {} + namespace_info['Vserver'] = namespace['svm']['name'] + namespace_info['Volume'] = namespace['location']['volume']['name'] + namespace_info['Size'] = namespace['space']['size'] + namespace_info['Qtree'] = ( + namespace['location'].get('qtree', {}).get('name', '')) + namespace_info['Path'] = namespace['name'] + namespace_info['OsType'] = namespace['os_type'] + namespace_info['SpaceReserved'] = ( + namespace['space']['guarantee']['requested']) + namespace_info['UUID'] = namespace['uuid'] + namespace_info['BlockSize'] = namespace['space']['block_size'] + + namespace_list.append(namespace_info) + + return namespace_list + + def namespace_resize(self, path, new_size_bytes): + """Resize the namespace.""" + seg = path.split("/") + LOG.info('Resizing namespace %s to new size.', seg[-1]) + + body = {'space.size': new_size_bytes} + query = {'name': path} + self.send_request('/storage/namespaces', 'patch', body=body, + query=query) + + def get_namespace_sizes_by_volume(self, volume_name): + """"Gets the list of namespace and their sizes from a given volume.""" + + query = { + 'location.volume.name': volume_name, + 'fields': 'space.size,name' + } + response = self.send_request('/storage/namespaces', 'get', query=query) + + namespaces = [] + for namespace_info in response.get('records', []): + namespaces.append({ + 'path': namespace_info.get('name', ''), + 'size': float(namespace_info.get('space', {}).get('size', 0)) + }) + + return namespaces + + def get_subsystem_by_host(self, host_nqn): + """Get subsystem exactly matching the initiator host.""" + query = { + 'svm.name': self.vserver, + 'hosts.nqn': host_nqn, + 'fields': 'name,os_type', + 'name': f'{na_utils.OPENSTACK_PREFIX}*', + } + response = self.send_request('/protocols/nvme/subsystems', 'get', + query=query) + + records = response.get('records', []) + + return [{'name': subsystem['name'], 'os_type': subsystem['os_type']} + for subsystem in records] + + def create_subsystem(self, subsystem_name, os_type, host_nqn): + """Creates subsystem with specified args.""" + body = { + 'svm.name': self.vserver, + 'name': subsystem_name, + 'os_type': os_type, + 'hosts': [{'nqn': host_nqn}] + } + self.send_request('/protocols/nvme/subsystems', 'post', body=body) + + def get_namespace_map(self, path): + """Gets the namespace map using its path.""" + query = { + 'namespace.name': path, + 'fields': 'subsystem.name,namespace.uuid,svm.name', + } + response = self.send_request('/protocols/nvme/subsystem-maps', + 'get', + query=query) + + records = response.get('records', []) + map_list = [] + for map in records: + map_subsystem = {} + map_subsystem['subsystem'] = map['subsystem']['name'] + map_subsystem['uuid'] = map['namespace']['uuid'] + map_subsystem['vserver'] = map['svm']['name'] + + map_list.append(map_subsystem) + + return map_list + + def map_namespace(self, path, subsystem_name): + """Maps namespace to the host nqn and returns namespace uuid.""" + + body_post = { + 'namespace.name': path, + 'subsystem.name': subsystem_name + } + try: + result = self.send_request('/protocols/nvme/subsystem-maps', + 'post', + body=body_post, + query={'return_records': 'true'}) + records = result.get('records') + namespace_uuid = records[0]['namespace']['uuid'] + return namespace_uuid + except netapp_api.NaApiError as e: + code = e.code + message = e.message + LOG.warning('Error mapping namespace. Code :%(code)s, Message: ' + '%(message)s', {'code': code, 'message': message}) + raise + + def get_nvme_subsystem_nqn(self, subsystem): + """Returns target subsystem nqn.""" + query = { + 'fields': 'target_nqn', + 'name': subsystem, + 'svm.name': self.vserver + } + response = self.send_request( + '/protocols/nvme/subsystems', 'get', query=query) + + records = response.get('records', []) + if records: + return records[0]['target_nqn'] + + LOG.debug('No %(subsystem)s NVMe subsystem found for vserver ' + '%(vserver)s', + {'subsystem': subsystem, 'vserver': self.vserver}) + return None + + def get_nvme_target_portals(self): + """Gets the NVMe target portal details.""" + query = { + 'services': 'data_nvme_tcp', + 'fields': 'ip.address', + 'enabled': 'true', + } + + response = self.send_request('/network/ip/interfaces', 'get', + query=query) + + interfaces = response.get('records', []) + return [record['ip']['address'] for record in interfaces] + + def unmap_namespace(self, path, subsystem): + """Unmaps a namespace from given subsystem.""" + + query = { + 'subsystem.name': subsystem, + 'namespace.name': path + } + self.send_request('/protocols/nvme/subsystem-maps', 'delete', + query=query) diff --git a/cinder/volume/drivers/netapp/dataontap/nvme_cmode.py b/cinder/volume/drivers/netapp/dataontap/nvme_cmode.py new file mode 100644 index 00000000000..eaae8979013 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/nvme_cmode.py @@ -0,0 +1,109 @@ +# Copyright (c) 2023 NetApp, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver for NetApp Data ONTAP NVMe storage systems. +""" + +from cinder import interface +from cinder.volume import driver +from cinder.volume.drivers.netapp.dataontap import nvme_library +from cinder.volume.drivers.netapp import options as na_opts + + +@interface.volumedriver +class NetAppCmodeNVMeDriver(driver.BaseVD): + """NetApp C-mode NVMe volume driver. + + Version history: + + .. code-block:: none + + 1.0.0 - Initial driver + + """ + + VERSION = "1.0.0" + + DRIVER_NAME = 'NetApp_NVMe_Cluster_direct' + + # ThirdPartySystems wiki page + CI_WIKI_NAME = "NetApp_CI" + + def __init__(self, *args, **kwargs): + super(NetAppCmodeNVMeDriver, self).__init__(*args, **kwargs) + self.library = nvme_library.NetAppNVMeStorageLibrary( + self.DRIVER_NAME, 'NVMe', **kwargs) + + @staticmethod + def get_driver_options(): + return na_opts.netapp_cluster_opts + + def do_setup(self, context): + self.library.do_setup(context) + + def check_for_setup_error(self): + self.library.check_for_setup_error() + + def create_volume(self, volume): + return self.library.create_volume(volume) + + def create_volume_from_snapshot(self, volume, snapshot): + return self.library.create_volume_from_snapshot(volume, snapshot) + + def create_cloned_volume(self, volume, src_vref): + return self.library.create_cloned_volume(volume, src_vref) + + def delete_volume(self, volume): + self.library.delete_volume(volume) + + def create_snapshot(self, snapshot): + self.library.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + self.library.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + return self.library.get_volume_stats(refresh, + self.get_filter_function(), + self.get_goodness_function()) + + def get_default_filter_function(self): + return self.library.get_default_filter_function() + + def get_default_goodness_function(self): + return self.library.get_default_goodness_function() + + def extend_volume(self, volume, new_size): + self.library.extend_volume(volume, new_size) + + def ensure_export(self, context, volume): + return self.library.ensure_export(context, volume) + + def create_export(self, context, volume, connector): + return self.library.create_export(context, volume) + + def remove_export(self, context, volume): + self.library.remove_export(context, volume) + + def initialize_connection(self, volume, connector): + conn_info = self.library.initialize_connection(volume, connector) + return conn_info + + def terminate_connection(self, volume, connector, **kwargs): + conn_info = self.library.terminate_connection(volume, connector, + **kwargs) + return conn_info + + def get_pool(self, volume): + return self.library.get_pool(volume) diff --git a/cinder/volume/drivers/netapp/dataontap/nvme_library.py b/cinder/volume/drivers/netapp/dataontap/nvme_library.py new file mode 100644 index 00000000000..5c853f80fa0 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/nvme_library.py @@ -0,0 +1,765 @@ +# Copyright (c) 2023 NetApp, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Volume driver library for NetApp C-mode NVMe storage systems. +""" + + +import sys +import uuid + +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import units +import six + +from cinder import exception +from cinder.i18n import _ +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api +from cinder.volume.drivers.netapp.dataontap.performance import perf_cmode +from cinder.volume.drivers.netapp.dataontap.utils import capabilities +from cinder.volume.drivers.netapp.dataontap.utils import loopingcalls +from cinder.volume.drivers.netapp.dataontap.utils import utils as dot_utils +from cinder.volume.drivers.netapp import options as na_opts +from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume import volume_utils + +LOG = logging.getLogger(__name__) + + +class NetAppNamespace(object): + """Represents a namespace on NetApp storage.""" + + def __init__(self, handle, name, size, metadata_dict): + self.handle = handle + self.name = name + self.size = size + self.metadata = metadata_dict or {} + + def get_metadata_property(self, prop): + """Get the metadata property of a namespace.""" + if prop in self.metadata: + return self.metadata[prop] + name = self.name + LOG.debug("No metadata property %(prop)s defined for the namespace " + "%(name)s", {'prop': prop, 'name': name}) + + def __str__(self, *args, **kwargs): + return ('NetApp namespace [handle:%s, name:%s, size:%s, metadata:%s]' + % (self.handle, self.name, self.size, self.metadata)) + + +@six.add_metaclass(volume_utils.TraceWrapperMetaclass) +class NetAppNVMeStorageLibrary(object): + """NetApp NVMe storage library for Data ONTAP.""" + + # do not increment this as it may be used in volume type definitions. + VERSION = "1.0.0" + REQUIRED_FLAGS = ['netapp_login', 'netapp_password', + 'netapp_server_hostname'] + ALLOWED_NAMESPACE_OS_TYPES = ['aix', 'linux', 'vmware', 'windows'] + ALLOWED_SUBSYSTEM_HOST_TYPES = ['aix', 'linux', 'vmware', 'windows'] + DEFAULT_NAMESPACE_OS = 'linux' + DEFAULT_HOST_TYPE = 'linux' + DEFAULT_FILTER_FUNCTION = 'capabilities.utilization < 70' + DEFAULT_GOODNESS_FUNCTION = '100 - capabilities.utilization' + REQUIRED_CMODE_FLAGS = ['netapp_vserver'] + NVME_PORT = 4420 + NVME_TRANSPORT = "tcp" + + def __init__(self, driver_name, driver_protocol, **kwargs): + + na_utils.validate_instantiation(**kwargs) + + self.driver_name = driver_name + self.driver_protocol = driver_protocol + self.rest_client = None + self._stats = {} + self.namespace_table = {} + self.namespace_ostype = None + self.host_type = None + self.app_version = kwargs.get("app_version", "unknown") + self.host = kwargs.get('host') + self.backend_name = self.host.split('@')[1] + + self.configuration = kwargs['configuration'] + self.configuration.append_config_values(na_opts.netapp_connection_opts) + self.configuration.append_config_values(na_opts.netapp_basicauth_opts) + self.configuration.append_config_values(na_opts.netapp_transport_opts) + self.configuration.append_config_values( + na_opts.netapp_provisioning_opts) + self.configuration.append_config_values(na_opts.netapp_san_opts) + self.configuration.append_config_values(na_opts.netapp_cluster_opts) + + self.max_over_subscription_ratio = ( + volume_utils.get_max_over_subscription_ratio( + self.configuration.max_over_subscription_ratio, + supports_auto=True)) + self.reserved_percentage = self.configuration.reserved_percentage + self.loopingcalls = loopingcalls.LoopingCalls() + + def do_setup(self, context): + na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) + self.namespace_ostype = (self.configuration.netapp_namespace_ostype + or self.DEFAULT_NAMESPACE_OS) + self.host_type = (self.configuration.netapp_host_type + or self.DEFAULT_HOST_TYPE) + + na_utils.check_flags(self.REQUIRED_CMODE_FLAGS, self.configuration) + + # NOTE(felipe_rodrigues): NVMe driver is only available with + # REST client. + self.client = dot_utils.get_client_for_backend( + self.backend_name, force_rest=True) + self.vserver = self.client.vserver + + # Storage service catalog. + self.ssc_library = capabilities.CapabilitiesLibrary( + self.driver_protocol, self.vserver, self.client, + self.configuration) + + self.ssc_library.check_api_permissions() + + self.using_cluster_credentials = ( + self.ssc_library.cluster_user_supported()) + + # Performance monitoring library. + self.perf_library = perf_cmode.PerformanceCmodeLibrary( + self.client) + + def _update_ssc(self): + """Refresh the storage service catalog with the latest set of pools.""" + + self.ssc_library.update_ssc(self._get_flexvol_to_pool_map()) + + def _get_flexvol_to_pool_map(self): + """Get the flexvols that match the pool name search pattern. + + The map is of the format suitable for seeding the storage service + catalog: { : {'pool_name': }} + """ + + pool_regex = na_utils.get_pool_name_filter_regex(self.configuration) + + pools = {} + flexvol_names = self.client.list_flexvols() + + for flexvol_name in flexvol_names: + + msg_args = { + 'flexvol': flexvol_name, + 'vol_pattern': pool_regex.pattern, + } + + if pool_regex.match(flexvol_name): + msg = "Volume '%(flexvol)s' matches %(vol_pattern)s" + LOG.debug(msg, msg_args) + pools[flexvol_name] = {'pool_name': flexvol_name} + else: + msg = "Volume '%(flexvol)s' does not match %(vol_pattern)s" + LOG.debug(msg, msg_args) + + return pools + + def check_for_setup_error(self): + """Check that the driver is working and can communicate. + + Discovers the namespaces on the NetApp server. + """ + if not self._get_flexvol_to_pool_map(): + msg = _('No pools are available for provisioning volumes. ' + 'Ensure that the configuration option ' + 'netapp_pool_name_search_pattern is set correctly.') + raise na_utils.NetAppDriverException(msg) + self._add_looping_tasks() + + if self.namespace_ostype not in self.ALLOWED_NAMESPACE_OS_TYPES: + msg = _("Invalid value for NetApp configuration" + " option netapp_namespace_ostype.") + LOG.error(msg) + raise na_utils.NetAppDriverException(msg) + if self.host_type not in self.ALLOWED_SUBSYSTEM_HOST_TYPES: + msg = _("Invalid value for NetApp configuration" + " option netapp_host_type.") + LOG.error(msg) + raise na_utils.NetAppDriverException(msg) + + namespace_list = self.client.get_namespace_list() + self._extract_and_populate_namespaces(namespace_list) + LOG.debug("Success getting list of namespace from server.") + + self.loopingcalls.start_tasks() + + def _add_looping_tasks(self): + """Add tasks that need to be executed at a fixed interval. + + Inheriting class overrides and then explicitly calls this method. + """ + # Note(cknight): Run the update once in the current thread to prevent a + # race with the first invocation of _update_volume_stats. + self._update_ssc() + + # Add the task that updates the slow-changing storage service catalog. + self.loopingcalls.add_task(self._update_ssc, + loopingcalls.ONE_HOUR, + loopingcalls.ONE_HOUR) + + # Add the task that logs EMS messages. + self.loopingcalls.add_task( + self._handle_ems_logging, + loopingcalls.ONE_HOUR) + + def _handle_ems_logging(self): + """Log autosupport messages.""" + + base_ems_message = dot_utils.build_ems_log_message_0( + self.driver_name, self.app_version) + self.client.send_ems_log_message(base_ems_message) + + pool_ems_message = dot_utils.build_ems_log_message_1( + self.driver_name, self.app_version, self.vserver, + self.ssc_library.get_ssc_flexvol_names(), []) + self.client.send_ems_log_message(pool_ems_message) + + def get_pool(self, volume): + """Return pool name where volume resides. + + :param volume: The volume hosted by the driver. + :return: Name of the pool where given volume is hosted. + """ + name = volume['name'] + metadata = self._get_namespace_attr(name, 'metadata') or dict() + return metadata.get('Volume', None) + + def create_volume(self, volume): + """Driver entry point for creating a new volume (ONTAP namespace).""" + + LOG.debug('create_volume on %s', volume['host']) + + # get Data ONTAP volume name as pool name. + pool_name = volume_utils.extract_host(volume['host'], level='pool') + if pool_name is None: + msg = _("Pool is not available in the volume host field.") + raise exception.InvalidHost(reason=msg) + + namespace = volume.name + size = int(volume['size']) * units.Gi + metadata = {'OsType': self.namespace_ostype, + 'Path': '/vol/%s/%s' % (pool_name, namespace)} + + try: + self.client.create_namespace(pool_name, namespace, size, metadata) + except Exception: + LOG.exception("Exception creating namespace %(name)s in pool " + "%(pool)s.", {'name': namespace, 'pool': pool_name}) + msg = _("Volume %s could not be created.") + raise exception.VolumeBackendAPIException(data=msg % namespace) + LOG.debug('Created namespace with name %(name)s.', {'name': namespace}) + + metadata['Volume'] = pool_name + metadata['Qtree'] = None + handle = self._create_namespace_handle(metadata) + self._add_namespace_to_table( + NetAppNamespace(handle, namespace, size, metadata)) + + return + + def delete_volume(self, volume): + """Driver entry point for destroying existing volumes.""" + self._delete_namespace(volume['name']) + + def _delete_namespace(self, namespace_name): + """Helper method to delete namespace backing a volume or snapshot.""" + + metadata = self._get_namespace_attr(namespace_name, 'metadata') + if metadata: + try: + self.client.destroy_namespace(metadata['Path']) + except netapp_api.NaApiError as e: + if e.code in netapp_api.REST_NAMESPACE_EOBJECTNOTFOUND: + LOG.warning("Failure deleting namespace %(name)s. " + "%(message)s", + {'name': namespace_name, 'message': e}) + else: + error_message = (_('A NetApp Api Error occurred: %s') % e) + raise na_utils.NetAppDriverException(error_message) + self.namespace_table.pop(namespace_name) + else: + LOG.warning("No entry in namespace table for volume/snapshot" + " %(name)s.", {'name': namespace_name}) + + def ensure_export(self, context, volume): + """Driver entry point to get the export info for an existing volume.""" + handle = self._get_namespace_attr(volume['name'], 'handle') + return {'provider_location': handle} + + def create_export(self, context, volume): + """Driver entry point to get the export info for a new volume.""" + handle = self._get_namespace_attr(volume['name'], 'handle') + return {'provider_location': handle} + + def remove_export(self, context, volume): + """Driver entry point to remove an export for a volume. + + Since exporting is idempotent in this driver, we have nothing + to do for unexporting. + """ + + pass + + def create_snapshot(self, snapshot): + """Driver entry point for creating a snapshot. + + This driver implements snapshots by using efficient single-file + (namespace) cloning. + """ + self._create_snapshot(snapshot) + + def _create_snapshot(self, snapshot): + vol_name = snapshot['volume_name'] + snapshot_name = snapshot['name'] + namespace = self._get_namespace_from_table(vol_name) + self._clone_namespace(namespace.name, snapshot_name) + + def _clone_namespace(self, name, new_name): + """Clone namespace with the given handle to the new name.""" + metadata = self._get_namespace_attr(name, 'metadata') + volume = metadata['Volume'] + + self.client.clone_namespace(volume, name, new_name) + + LOG.debug("Cloned namespace with new name %s", new_name) + namespace = self.client.get_namespace_by_args( + vserver=self.vserver, path=f'/vol/{volume}/{new_name}') + if len(namespace) == 0: + msg = _("No cloned namespace named %s found on the filer.") + raise exception.VolumeBackendAPIException(data=msg % new_name) + + cloned_namespace = namespace[0] + self._add_namespace_to_table( + NetAppNamespace( + f"{cloned_namespace['Vserver']}:{cloned_namespace['Path']}", + new_name, + cloned_namespace['Size'], + cloned_namespace)) + + def delete_snapshot(self, snapshot): + """Driver entry point for deleting a snapshot.""" + self._delete_namespace(snapshot['name']) + LOG.debug("Snapshot %s deletion successful.", snapshot['name']) + + def create_volume_from_snapshot(self, volume, snapshot): + source = {'name': snapshot['name'], 'size': snapshot['volume_size']} + self._clone_source_to_destination(source, volume) + + def create_cloned_volume(self, volume, src_vref): + src_namespace = self._get_namespace_from_table(src_vref['name']) + source = {'name': src_namespace.name, 'size': src_vref['size']} + self._clone_source_to_destination(source, volume) + + def _clone_source_to_destination(self, source, destination_volume): + source_size = source['size'] + destination_size = destination_volume['size'] + + source_name = source['name'] + destination_name = destination_volume['name'] + + try: + self._clone_namespace(source_name, destination_name) + + if destination_size != source_size: + + try: + self._extend_volume(destination_volume, destination_size) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Resizing %s failed. Cleaning volume.", + destination_volume['id']) + self.delete_volume(destination_volume) + + except Exception: + LOG.exception("Exception cloning volume %(name)s from source " + "volume %(source)s.", + {'name': destination_name, 'source': source_name}) + + msg = _("Volume %s could not be created from source volume.") + raise exception.VolumeBackendAPIException( + data=msg % destination_name) + + def _create_namespace_handle(self, metadata): + """Returns namespace handle based on filer type.""" + return '%s:%s' % (self.vserver, metadata['Path']) + + def _extract_namespace_info(self, namespace): + """Extracts the namespace from API and populates the table.""" + + path = namespace['Path'] + (_rest, _splitter, name) = path.rpartition('/') + handle = self._create_namespace_handle(namespace) + size = namespace['Size'] + return NetAppNamespace(handle, name, size, namespace) + + def _extract_and_populate_namespaces(self, api_namespaces): + """Extracts the namespaces from API and populates the table.""" + + for namespace in api_namespaces: + discovered_namespace = self._extract_namespace_info(namespace) + self._add_namespace_to_table(discovered_namespace) + + def _add_namespace_to_table(self, namespace): + """Adds namespace to cache table.""" + if not isinstance(namespace, NetAppNamespace): + msg = _("Object is not a NetApp namespace.") + raise exception.VolumeBackendAPIException(data=msg) + self.namespace_table[namespace.name] = namespace + + def _get_namespace_from_table(self, name): + """Gets namespace from cache table. + + Refreshes cache if namespace not found in cache. + """ + namespace = self.namespace_table.get(name) + if namespace is None: + namespace_list = self.client.get_namespace_list() + self._extract_and_populate_namespaces(namespace_list) + namespace = self.namespace_table.get(name) + if namespace is None: + raise exception.VolumeNotFound(volume_id=name) + return namespace + + def _get_namespace_attr(self, name, attr): + """Get the namespace attribute if found else None.""" + try: + attr = getattr(self._get_namespace_from_table(name), attr) + return attr + except exception.VolumeNotFound as e: + LOG.error("Message: %s", e.msg) + except Exception as e: + LOG.error("Error getting namespace attribute. Exception: %s", e) + return None + + def get_volume_stats(self, refresh=False, filter_function=None, + goodness_function=None): + """Get volume stats. + + If 'refresh' is True, update the stats first. + """ + + if refresh: + self._update_volume_stats(filter_function=filter_function, + goodness_function=goodness_function) + return self._stats + + def _update_volume_stats(self, filter_function=None, + goodness_function=None): + """Retrieve backend stats.""" + + LOG.debug('Updating volume stats') + data = {} + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = backend_name or self.driver_name + data['vendor_name'] = 'NetApp' + data['driver_version'] = self.VERSION + data['storage_protocol'] = self.driver_protocol + data['pools'] = self._get_pool_stats( + filter_function=filter_function, + goodness_function=goodness_function) + data['sparse_copy_volume'] = True + data['replication_enabled'] = False + + self._stats = data + + def _get_pool_stats(self, filter_function=None, goodness_function=None): + """Retrieve pool (Data ONTAP flexvol) stats. + + Pool statistics are assembled from static driver capabilities, the + Storage Service Catalog of flexvol attributes, and real-time capacity + and controller utilization metrics. The pool name is the flexvol name. + """ + + pools = [] + + ssc = self.ssc_library.get_ssc() + if not ssc: + return pools + + # Utilization and performance metrics require cluster-scoped + # credentials + if self.using_cluster_credentials: + # Get up-to-date node utilization metrics just once + self.perf_library.update_performance_cache(ssc) + + # Get up-to-date aggregate capacities just once + aggregates = self.ssc_library.get_ssc_aggregates() + aggr_capacities = self.client.get_aggregate_capacities( + aggregates) + else: + aggr_capacities = {} + + for ssc_vol_name, ssc_vol_info in ssc.items(): + + pool = dict() + + # Add storage service catalog data + pool.update(ssc_vol_info) + + # Add driver capabilities and config info + pool['QoS_support'] = False + pool['multiattach'] = False + pool['online_extend_support'] = False + pool['consistencygroup_support'] = False + pool['consistent_group_snapshot_enabled'] = False + pool['reserved_percentage'] = self.reserved_percentage + pool['max_over_subscription_ratio'] = ( + self.max_over_subscription_ratio) + + # Add up-to-date capacity info + capacity = self.client.get_flexvol_capacity( + flexvol_name=ssc_vol_name) + + size_total_gb = capacity['size-total'] / units.Gi + pool['total_capacity_gb'] = na_utils.round_down(size_total_gb) + + size_available_gb = capacity['size-available'] / units.Gi + pool['free_capacity_gb'] = na_utils.round_down(size_available_gb) + + if self.configuration.netapp_driver_reports_provisioned_capacity: + namespaces = self.client.get_namespace_sizes_by_volume( + ssc_vol_name) + provisioned_cap = 0 + for namespace in namespaces: + namespace_name = namespace['path'].split('/')[-1] + # Filtering namespaces that matches the volume name + # template to exclude snapshots. + if volume_utils.extract_id_from_volume_name( + namespace_name): + provisioned_cap = provisioned_cap + namespace['size'] + pool['provisioned_capacity_gb'] = na_utils.round_down( + float(provisioned_cap) / units.Gi) + + if self.using_cluster_credentials: + dedupe_used = self.client.get_flexvol_dedupe_used_percent( + ssc_vol_name) + else: + dedupe_used = 0.0 + pool['netapp_dedupe_used_percent'] = na_utils.round_down( + dedupe_used) + + aggregate_name = ssc_vol_info.get('netapp_aggregate') + aggr_capacity = aggr_capacities.get(aggregate_name, {}) + pool['netapp_aggregate_used_percent'] = aggr_capacity.get( + 'percent-used', 0) + + # Add utilization data + utilization = self.perf_library.get_node_utilization_for_pool( + ssc_vol_name) + pool['utilization'] = na_utils.round_down(utilization) + pool['filter_function'] = filter_function + pool['goodness_function'] = goodness_function + + pools.append(pool) + + return pools + + def get_default_filter_function(self): + """Get the default filter_function string.""" + return self.DEFAULT_FILTER_FUNCTION + + def get_default_goodness_function(self): + """Get the default goodness_function string.""" + return self.DEFAULT_GOODNESS_FUNCTION + + def extend_volume(self, volume, new_size): + """Driver entry point to increase the size of a volume.""" + self._extend_volume(volume, new_size) + + def _extend_volume(self, volume, new_size): + """Extend an existing volume to the new size.""" + name = volume['name'] + namespace = self._get_namespace_from_table(name) + path = namespace.metadata['Path'] + curr_size_bytes = str(namespace.size) + new_size_bytes = str(int(new_size) * units.Gi) + # Reused by clone scenarios. + # Hence comparing the stored size. + if curr_size_bytes == new_size_bytes: + LOG.info("No need to extend volume %s" + " as it is already the requested new size.", name) + return + + self.client.namespace_resize(path, new_size_bytes) + + self.namespace_table[name].size = new_size_bytes + + def _get_or_create_subsystem(self, host_nqn, host_os_type): + """Checks for an subsystem for a host. + + Creates subsystem if not already present with given host os type and + adds the host. + """ + # Backend supports different subsystems with the same hosts, so + # instead of reusing non OpenStack subsystem, we make sure we only use + # our own, thus being compatible with custom subsystem. + subsystems = self.client.get_subsystem_by_host( + host_nqn) + if subsystems: + subsystem_name = subsystems[0]['name'] + host_os_type = subsystems[0]['os_type'] + else: + subsystem_name = na_utils.OPENSTACK_PREFIX + str(uuid.uuid4()) + self.client.create_subsystem(subsystem_name, host_os_type, + host_nqn) + + return subsystem_name, host_os_type + + def _find_mapped_namespace_subsystem(self, path, host_nqn): + """Find an subsystem for a namespace mapped to the given host.""" + subsystems = [subsystem['name'] for subsystem in + self.client.get_subsystem_by_host(host_nqn)] + + # Map subsystem name to namespace-id for the requested host. + namespace_map = {v['subsystem']: v['uuid'] + for v in self.client.get_namespace_map(path) + if v['subsystem'] in subsystems} + + subsystem_name = n_uuid = None + # Give preference to OpenStack subsystems, just use the last one if not + # present to allow unmapping old mappings that used a custom subsystem. + for subsystem_name, n_uuid in namespace_map.items(): + if subsystem_name.startswith(na_utils.OPENSTACK_PREFIX): + break + + return subsystem_name, n_uuid + + def _map_namespace(self, name, host_nqn): + """Maps namespace to the host nqn and returns its ID assigned.""" + + subsystem_name, subsystem_host_os = self._get_or_create_subsystem( + host_nqn, self.host_type) + if subsystem_host_os != self.host_type: + LOG.warning("Namespace misalignment may occur for current" + " subsystem %(sub_name)s with host OS type" + " %(sub_os)s. Please configure subsystem manually" + " according to the type of the host OS.", + {'sub_name': subsystem_name, + 'sub_os': subsystem_host_os}) + + metadata = self._get_namespace_attr(name, 'metadata') + path = metadata['Path'] + try: + ns_uuid = self.client.map_namespace( + path, subsystem_name,) + return subsystem_name, ns_uuid + except netapp_api.NaApiError: + exc_info = sys.exc_info() + (subsystem_name, ns_uuid) = self._find_mapped_namespace_subsystem( + path, host_nqn) + if ns_uuid is not None and subsystem_name: + return subsystem_name, ns_uuid + else: + six.reraise(*exc_info) + + def initialize_connection(self, volume, connector): + """Initializes the connection and returns connection info. + + Assign any created volume to a compute node/host so that it can be + used from that host. Example return values: + + .. code-block:: default + + { + 'driver_volume_type': 'nvmeof', + 'data': { + 'target_nqn' 'nqn.1992-01.example.com:subsystem', + 'host_nqn': 'nqn.1992-01.example.com:string', + 'portals': [ + ('10.10.10.10', '4420', 'tcp') + ], + 'uuid': 'a1129e6f-8497-4c0c-be01-3eab1ba684ed' + } + } + + """ + host_nqn = connector.get("nqn") + if not host_nqn: + raise exception.VolumeBackendAPIException( + data=_("Initialize connection error: no host nqn available!")) + + name = volume['name'] + subsystem, namespace_uuid = self._map_namespace(name, host_nqn) + + LOG.debug("Mapped namespace %(name)s to the host NQN %(host_nqn)s", + {'name': name, 'host_nqn': host_nqn}) + + target_nqn = self.client.get_nvme_subsystem_nqn(subsystem) + if not target_nqn: + msg = _('Failed to get subsystem %(subsystem)s target NQN for the ' + 'namespace %(name)s') + msg_args = {'subsystem': subsystem, 'name': name} + raise exception.VolumeBackendAPIException(data=msg % msg_args) + + target_portals = self.client.get_nvme_target_portals() + if not target_portals: + msg = _('Failed to get target portals for the namespace %s') + raise exception.VolumeBackendAPIException( + data=msg % name) + + portal = (target_portals[0], self.NVME_PORT, self.NVME_TRANSPORT) + data = { + "target_nqn": str(target_nqn), + "host_nqn": host_nqn, + "portals": [portal], + "vol_uuid": namespace_uuid + } + conn_info = {"driver_volume_type": "nvmeof", "data": data} + LOG.debug("Initialize connection info: %s", conn_info) + + return conn_info + + def _unmap_namespace(self, path, host_nqn): + """Unmaps a namespace from given host.""" + + namespace_unmap_list = [] + if host_nqn: + (subsystem, _) = self._find_mapped_namespace_subsystem( + path, host_nqn) + namespace_unmap_list.append((path, subsystem)) + else: + namespace_maps = self.client.get_namespace_map(path) + namespace_unmap_list = [ + (path, m['subsystem']) for m in namespace_maps] + + for _path, _subsystem in namespace_unmap_list: + self.client.unmap_namespace(_path, _subsystem) + + def terminate_connection(self, volume, connector, **kwargs): + """Driver entry point to unattach a volume from an instance. + + Unmask the namespace on the storage system so the given initiator can + no longer access it. + """ + + name = volume['name'] + host_nqn = None + if connector is None: + LOG.debug('Unmapping namespace %(name)s from all hosts.', + {'name': name}) + else: + host_nqn = connector.get("nqn") + LOG.debug("Unmapping namespace %(name)s from the host " + "%(host_nqn)s", {'name': name, 'host_nqn': host_nqn}) + + metadata = self._get_namespace_attr(name, 'metadata') + path = metadata['Path'] + self._unmap_namespace(path, host_nqn) diff --git a/cinder/volume/drivers/netapp/dataontap/utils/utils.py b/cinder/volume/drivers/netapp/dataontap/utils/utils.py index 314863cab46..481b3f20121 100644 --- a/cinder/volume/drivers/netapp/dataontap/utils/utils.py +++ b/cinder/volume/drivers/netapp/dataontap/utils/utils.py @@ -62,11 +62,11 @@ def get_backend_configuration(backend_name): return config -def get_client_for_backend(backend_name, vserver_name=None): +def get_client_for_backend(backend_name, vserver_name=None, force_rest=False): """Get a cDOT API client for a specific backend.""" config = get_backend_configuration(backend_name) - if config.netapp_use_legacy_client: + if config.netapp_use_legacy_client and not force_rest: client = client_cmode.Client( transport_type=config.netapp_transport_type, username=config.netapp_login, diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index a92d38c7768..07dbdaf7591 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -39,7 +39,7 @@ netapp_proxy_opts = [ 'the only valid value is ontap_cluster for using ' 'clustered Data ONTAP.')), cfg.StrOpt('netapp_storage_protocol', - choices=['iscsi', 'fc', 'nfs'], + choices=['iscsi', 'fc', 'nfs', 'nvme'], help=('The storage protocol to be used on the data path with ' 'the storage system.')), ] @@ -158,6 +158,10 @@ netapp_san_opts = [ help=('This option defines the type of operating system that' ' will access a LUN exported from Data ONTAP; it is' ' assigned to the LUN at the time it is created.')), + cfg.StrOpt('netapp_namespace_ostype', + help=('This option defines the type of operating system that' + ' will access a namespace exported from Data ONTAP; it is' + ' assigned to the namespace at the time it is created.')), cfg.StrOpt('netapp_host_type', help=('This option defines the type of operating system for' ' all initiators that can access a LUN. This information' diff --git a/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst b/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst index f28f1a35b8f..d0b0e78cd5c 100644 --- a/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst +++ b/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst @@ -6,7 +6,7 @@ The NetApp unified driver is a Block Storage driver that supports multiple storage families and protocols. Currently, the only storage family supported by this driver is the clustered Data ONTAP. The storage protocol refers to the protocol used to initiate data storage and -access operations on those storage systems like iSCSI and NFS. The NetApp +access operations on those storage systems like NVMe, iSCSI and NFS. The NetApp unified driver can be configured to provision and manage OpenStack volumes on a given storage family using a specified storage protocol. @@ -44,7 +44,7 @@ NetApp clustered Data ONTAP storage family The NetApp clustered Data ONTAP storage family represents a configuration group which provides Compute instances access to clustered Data ONTAP storage systems. At present it can be configured in -Block Storage to work with iSCSI and NFS storage protocols. +Block Storage to work with NVme, iSCSI and NFS storage protocols. NetApp iSCSI configuration for clustered Data ONTAP --------------------------------------------------- @@ -107,6 +107,66 @@ setting the ``volume_driver``, ``netapp_storage_family`` and operational scenarios, visit the `NetApp OpenStack website `_. +NetApp NVMe/TCP configuration for clustered Data ONTAP +------------------------------------------------------ + +The NetApp NVMe/TCP configuration for clustered Data ONTAP is an interface +from OpenStack to clustered Data ONTAP storage systems. It provisions +and manages the SAN block storage entity, which is a NetApp namespace that +can be accessed using the NVMe/TCP protocol. + +The NVMe/TCP configuration for clustered Data ONTAP is a direct interface +from Block Storage to the clustered Data ONTAP instance and as +such does not require additional management software to achieve the +desired functionality. It uses NetApp APIs to interact with the +clustered Data ONTAP instance. + +**Configuration options** + +Configure the volume driver, storage family, and storage protocol to the +NetApp unified driver, clustered Data ONTAP, and NVMe respectively by +setting the ``volume_driver``, ``netapp_storage_family`` and +``netapp_storage_protocol`` options in the ``cinder.conf`` file as follows: + +.. code-block:: ini + + volume_driver = cinder.volume.drivers.netapp.common.NetAppDriver + netapp_storage_family = ontap_cluster + netapp_storage_protocol = nvme + netapp_vserver = openstack-vserver + netapp_server_hostname = myhostname + netapp_server_port = port + netapp_login = username + netapp_password = password + +.. note:: + + To use the NVMe/TCP protocol, you must override the default value of + ``netapp_storage_protocol`` with ``nvme``. + Note that this is not the same value that is reported by the driver + to the scheduler as `storage_protocol`, which is always + ``NVMe`` (case sensitive). + +.. note:: + + If you specify an account in the ``netapp_login`` that only has + virtual storage server (Vserver) administration privileges (rather + than cluster-wide administration privileges), some advanced features + of the NetApp unified driver will not work and you may see warnings + in the Block Storage logs. + +.. note:: + + The driver only supports the minimal Cinder driver features: create/delete + volume and snapshots, extend volume, attack/detach volume, create volume + from volume and create volume from image/snapshot. + +.. tip:: + + For more information on these options and other deployment and + operational scenarios, visit the `NetApp OpenStack website + `_. + NetApp NFS configuration for clustered Data ONTAP ------------------------------------------------- diff --git a/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc b/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc index e4d83d87072..2c50bfc40c4 100644 --- a/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc +++ b/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc @@ -17,7 +17,7 @@ * - ``netapp_password`` = ``None`` - (String) Password for the administrative user account specified in the netapp_login option. * - ``netapp_pool_name_search_pattern`` = ``(.+)`` - - (String) This option is used to restrict provisioning to the specified pools. Specify the value of this option to be a regular expression which will be applied to the names of objects from the storage backend which represent pools in Cinder. This option is only utilized when the storage protocol is configured to use iSCSI or FC. + - (String) This option is used to restrict provisioning to the specified pools. Specify the value of this option to be a regular expression which will be applied to the names of objects from the storage backend which represent pools in Cinder. This option is only utilized when the storage protocol is configured to use NVMe, iSCSI or FC. * - ``netapp_replication_aggregate_map`` = ``None`` - (Unknown) Multi opt of dictionaries to represent the aggregate mapping between source and destination back ends when using whole back end replication. For every source aggregate associated with a cinder pool (NetApp FlexVol), you would need to specify the destination aggregate on the replication target device. A replication target device is configured with the configuration option replication_device. Specify this option as many times as you have replication devices. Each entry takes the standard dict config form: netapp_replication_aggregate_map = backend_id:,src_aggr_name1:dest_aggr_name1,src_aggr_name2:dest_aggr_name2,... * - ``netapp_server_hostname`` = ``None`` diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 384a633daa5..8e25c0a5eb7 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -151,7 +151,7 @@ title=NEC Storage M Series Driver (iSCSI, FC) title=NEC Storage V Series Driver (iSCSI, FC) [driver.netapp_ontap] -title=NetApp Data ONTAP Driver (iSCSI, NFS, FC) +title=NetApp Data ONTAP Driver (iSCSI, NFS, FC, NVMe/TCP) [driver.netapp_solidfire] title=NetApp Solidfire Driver (iSCSI) diff --git a/releasenotes/notes/add_nvme_tcp_driver-558ff80aa2029e2b.yaml b/releasenotes/notes/add_nvme_tcp_driver-558ff80aa2029e2b.yaml new file mode 100644 index 00000000000..54201f8c0e8 --- /dev/null +++ b/releasenotes/notes/add_nvme_tcp_driver-558ff80aa2029e2b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added NVMe/TCP volume driver for NetApp ONTAP Storage Cluster.