From 3b2d17a5db07dfba5d20a1697025706dda6f0a0a Mon Sep 17 00:00:00 2001 From: Mike Rooney Date: Thu, 17 Dec 2015 17:01:05 -0500 Subject: [PATCH] NetApp: Implement CGs for ONTAP Drivers This patch includes the driver changes necessary for NetApp 7mode and CDOT backends to support all consistency group and cgsnapshot functionality. Co-Authored-By: Alex Meade Co-Authored-By: Chuck Fouts DocImpact Implements: blueprint cinder-consistency-groups Change-Id: Ia74c634835958876d97daf6766f2ef110b33ddc4 --- .../drivers/netapp/dataontap/client/fakes.py | 76 +++++++++ .../dataontap/client/test_client_7mode.py | 41 +++++ .../dataontap/client/test_client_base.py | 52 ++++++ .../dataontap/client/test_client_cmode.py | 40 +++++ .../volume/drivers/netapp/dataontap/fakes.py | 72 +++++++++ .../netapp/dataontap/test_block_7mode.py | 11 +- .../netapp/dataontap/test_block_base.py | 153 ++++++++++++++++++ .../netapp/dataontap/test_block_cmode.py | 11 +- .../drivers/netapp/dataontap/block_7mode.py | 8 +- .../drivers/netapp/dataontap/block_base.py | 145 ++++++++++++++++- .../drivers/netapp/dataontap/block_cmode.py | 10 +- .../drivers/netapp/dataontap/client/api.py | 1 + .../netapp/dataontap/client/client_7mode.py | 55 ++++++- .../netapp/dataontap/client/client_base.py | 35 ++++ .../netapp/dataontap/client/client_cmode.py | 98 ++++++++++- .../drivers/netapp/dataontap/fc_7mode.py | 26 +++ .../drivers/netapp/dataontap/fc_cmode.py | 26 +++ .../drivers/netapp/dataontap/iscsi_7mode.py | 26 +++ .../drivers/netapp/dataontap/iscsi_cmode.py | 26 +++ ...NTAP-full-cg-support-cfdc91bf0acf9fe1.yaml | 6 + 20 files changed, 897 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/NetApp-ONTAP-full-cg-support-cfdc91bf0acf9fe1.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 b2c566272..76bf92fc9 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py @@ -1,4 +1,5 @@ # Copyright (c) - 2015, Tom Barron. All rights reserved. +# Copyright (c) - 2016 Mike Rooney. 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 @@ -17,6 +18,7 @@ from lxml import etree import mock from six.moves import urllib +from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake import cinder.volume.drivers.netapp.dataontap.client.api as netapp_api @@ -204,6 +206,80 @@ VOLUME_LIST_INFO_RESPONSE = etree.XML(""" """) +SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_CMODE = etree.XML(""" + + + + %(snapshot_name)s + False + %(vol_name)s + + + 1 + +""" % { + 'snapshot_name': fake.SNAPSHOT['name'], + 'vol_name': fake.SNAPSHOT['volume_id'], +}) + +SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_CMODE = etree.XML(""" + + + + %(snapshot_name)s + True + %(vol_name)s + + + 1 + +""" % { + 'snapshot_name': fake.SNAPSHOT['name'], + 'vol_name': fake.SNAPSHOT['volume_id'], +}) + +SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_7MODE = etree.XML(""" + + + + %(snapshot_name)s + False + %(vol_name)s + + + +""" % { + 'snapshot_name': fake.SNAPSHOT['name'], + 'vol_name': fake.SNAPSHOT['volume_id'], +}) + +SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_7MODE = etree.XML(""" + + + + %(snapshot_name)s + True + %(vol_name)s + + + +""" % { + 'snapshot_name': fake.SNAPSHOT['name'], + 'vol_name': fake.SNAPSHOT['volume_id'], +}) + +SNAPSHOT_NOT_PRESENT_7MODE = etree.XML(""" + + + + NOT_THE_RIGHT_SNAPSHOT + false + %(vol_name)s + + + +""" % {'vol_name': fake.SNAPSHOT['volume_id']}) + NO_RECORDS_RESPONSE = etree.XML(""" 0 diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py index 2b072cc8f..aed35b323 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py @@ -1,5 +1,6 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2015 Dustin Schoenbrun. All rights reserved. +# Copyright (c) 2016 Mike Rooney. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,6 +22,7 @@ import mock import paramiko import six +from cinder import exception from cinder import ssh_utils from cinder import test from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( @@ -768,3 +770,42 @@ class NetApp7modeClientTestCase(test.TestCase): self.client.ssh_client.execute_command.assert_has_calls( [mock.call(ssh, command)] ) + + def test_get_snapshot_if_snapshot_present_not_busy(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement( + fake_client.SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_7MODE) + self.connection.invoke_successfully.return_value = response + + snapshot = self.client.get_snapshot(expected_vol_name, + expected_snapshot_name) + + self.assertEqual(expected_vol_name, snapshot['volume']) + self.assertEqual(expected_snapshot_name, snapshot['name']) + self.assertEqual(set([]), snapshot['owners']) + self.assertFalse(snapshot['busy']) + + def test_get_snapshot_if_snapshot_present_busy(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement( + fake_client.SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_7MODE) + self.connection.invoke_successfully.return_value = response + + snapshot = self.client.get_snapshot(expected_vol_name, + expected_snapshot_name) + + self.assertEqual(expected_vol_name, snapshot['volume']) + self.assertEqual(expected_snapshot_name, snapshot['name']) + self.assertEqual(set([]), snapshot['owners']) + self.assertTrue(snapshot['busy']) + + def test_get_snapshot_if_snapshot_not_present(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement(fake_client.SNAPSHOT_NOT_PRESENT_7MODE) + self.connection.invoke_successfully.return_value = response + + self.assertRaises(exception.SnapshotNotFound, self.client.get_snapshot, + expected_vol_name, expected_snapshot_name) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py index b492828cb..f6db0a731 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -1,5 +1,6 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -507,3 +508,54 @@ class NetAppBaseClientTestCase(test.TestCase): self.client.get_performance_counter_info, 'wafl', 'invalid') + + def test_delete_snapshot(self): + api_args = { + 'volume': fake.SNAPSHOT['volume_id'], + 'snapshot': fake.SNAPSHOT['name'], + } + self.mock_object(self.client, 'send_request') + + self.client.delete_snapshot(api_args['volume'], + api_args['snapshot']) + + asserted_api_args = { + 'volume': api_args['volume'], + 'snapshot': api_args['snapshot'], + } + self.client.send_request.assert_called_once_with('snapshot-delete', + asserted_api_args) + + def test_create_cg_snapshot(self): + self.mock_object(self.client, '_start_cg_snapshot', mock.Mock( + return_value=fake.CONSISTENCY_GROUP_ID)) + self.mock_object(self.client, '_commit_cg_snapshot') + + self.client.create_cg_snapshot([fake.CG_VOLUME_NAME], + fake.CG_SNAPSHOT_NAME) + + self.client._commit_cg_snapshot.assert_called_once_with( + fake.CONSISTENCY_GROUP_ID) + + def test_start_cg_snapshot(self): + snapshot_init = { + 'snapshot': fake.CG_SNAPSHOT_NAME, + 'timeout': 'relaxed', + 'volumes': [{'volume-name': fake.CG_VOLUME_NAME}], + } + self.mock_object(self.client, 'send_request') + + self.client._start_cg_snapshot([fake.CG_VOLUME_NAME], + snapshot_init['snapshot']) + + self.client.send_request.assert_called_once_with('cg-start', + snapshot_init) + + def test_commit_cg_snapshot(self): + snapshot_commit = {'cg-id': fake.CG_VOLUME_ID} + self.mock_object(self.client, 'send_request') + + self.client._commit_cg_snapshot(snapshot_commit['cg-id']) + + self.client.send_request.assert_called_once_with( + 'cg-commit', {'cg-id': snapshot_commit['cg-id']}) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py index 13f295972..4ed9933c0 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -1,6 +1,7 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2015 Dustin Schoenbrun. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -1246,3 +1247,42 @@ class NetAppCmodeClientTestCase(test.TestCase): fake_client.INITIATOR_IQN, fake_client.USER_NAME, fake_client.PASSWORD) + + def test_get_snapshot_if_snapshot_present_not_busy(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement( + fake_client.SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_CMODE) + self.mock_send_request.return_value = response + + snapshot = self.client.get_snapshot(expected_vol_name, + expected_snapshot_name) + + self.assertEqual(expected_vol_name, snapshot['volume']) + self.assertEqual(expected_snapshot_name, snapshot['name']) + self.assertEqual(set([]), snapshot['owners']) + self.assertFalse(snapshot['busy']) + + def test_get_snapshot_if_snapshot_present_busy(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement( + fake_client.SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_CMODE) + self.mock_send_request.return_value = response + + snapshot = self.client.get_snapshot(expected_vol_name, + expected_snapshot_name) + + self.assertEqual(expected_vol_name, snapshot['volume']) + self.assertEqual(expected_snapshot_name, snapshot['name']) + self.assertEqual(set([]), snapshot['owners']) + self.assertTrue(snapshot['busy']) + + def test_get_snapshot_if_snapshot_not_present(self): + expected_vol_name = fake.SNAPSHOT['volume_id'] + expected_snapshot_name = fake.SNAPSHOT['name'] + response = netapp_api.NaElement(fake_client.NO_RECORDS_RESPONSE) + self.mock_send_request.return_value = response + + self.assertRaises(exception.SnapshotNotFound, self.client.get_snapshot, + expected_vol_name, expected_snapshot_name) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 4df62ad33..6a2f91082 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -1,5 +1,6 @@ # Copyright (c) - 2014, Clinton Knight. All rights reserved. # Copyright (c) - 2015, Tom Barron. All rights reserved. +# Copyright (c) - 2016 Chuck Fouts. 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 @@ -216,6 +217,7 @@ SNAPSHOT = { 'name': SNAPSHOT_NAME, 'volume_size': SIZE, 'volume_id': 'fake_volume_id', + 'busy': False, } VOLUME_REF = {'name': 'fake_vref_name', 'size': 42} @@ -223,6 +225,7 @@ VOLUME_REF = {'name': 'fake_vref_name', 'size': 42} FAKE_CMODE_POOLS = [ { 'QoS_support': True, + 'consistencygroup_support': True, 'free_capacity_gb': 3.72, 'netapp_compression': u'true', 'netapp_dedup': u'true', @@ -338,6 +341,7 @@ FAKE_7MODE_VOL1 = [netapp_api.NaElement( FAKE_7MODE_POOLS = [ { 'pool_name': 'open123', + 'consistencygroup_support': True, 'QoS_support': False, 'reserved_percentage': 0, 'total_capacity_gb': 0.0, @@ -352,6 +356,74 @@ FAKE_7MODE_POOLS = [ } ] +CG_VOLUME_NAME = 'fake_cg_volume' +CG_GROUP_NAME = 'fake_consistency_group' +SOURCE_CG_VOLUME_NAME = 'fake_source_cg_volume' +CG_VOLUME_ID = 'fake_cg_volume_id' +CG_VOLUME_SIZE = 100 +SOURCE_CG_VOLUME_ID = 'fake_source_cg_volume_id' +CONSISTENCY_GROUP_NAME = 'fake_cg' +SOURCE_CONSISTENCY_GROUP_ID = 'fake_source_cg_id' +CONSISTENCY_GROUP_ID = 'fake_cg_id' +CG_SNAPSHOT_ID = 'fake_cg_snapshot_id' +CG_SNAPSHOT_NAME = 'snapshot-' + CG_SNAPSHOT_ID +CG_VOLUME_SNAPSHOT_ID = 'fake_cg_volume_snapshot_id' + +CG_LUN_METADATA = { + 'OsType': None, + 'Path': '/vol/aggr1/fake_cg_volume', + 'SpaceReserved': 'true', + 'Qtree': None, + 'Volume': POOL_NAME, +} + +SOURCE_CG_VOLUME = { + 'name': SOURCE_CG_VOLUME_NAME, + 'size': CG_VOLUME_SIZE, + 'id': SOURCE_CG_VOLUME_ID, + 'host': 'hostname@backend#cdot', + 'consistencygroup_id': None, + 'status': 'fake_status', +} + +CG_VOLUME = { + 'name': CG_VOLUME_NAME, + 'size': 100, + 'id': CG_VOLUME_ID, + 'host': 'hostname@backend#cdot', + 'consistencygroup_id': CONSISTENCY_GROUP_ID, + 'status': 'fake_status', +} + +SOURCE_CONSISTENCY_GROUP = { + 'id': SOURCE_CONSISTENCY_GROUP_ID, + 'status': 'fake_status', +} + +CONSISTENCY_GROUP = { + 'id': CONSISTENCY_GROUP_ID, + 'status': 'fake_status', + 'name': CG_GROUP_NAME, +} + +CG_SNAPSHOT = { + 'id': CG_SNAPSHOT_ID, + 'name': CG_SNAPSHOT_NAME, + 'volume_size': CG_VOLUME_SIZE, + 'consistencygroup_id': CONSISTENCY_GROUP_ID, + 'status': 'fake_status', + 'volume_id': 'fake_source_volume_id', +} + +CG_VOLUME_SNAPSHOT = { + 'name': CG_SNAPSHOT_NAME, + 'volume_size': CG_VOLUME_SIZE, + 'cgsnapshot_id': CG_SNAPSHOT_ID, + 'id': CG_VOLUME_SNAPSHOT_ID, + 'status': 'fake_status', + 'volume_id': CG_VOLUME_ID, +} + class test_volume(object): pass diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py index 60668a699..68b87c988 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py @@ -2,6 +2,7 @@ # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -302,7 +303,8 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', - 'newFakeLUN', 'false', block_count=0, dest_block=0, src_block=0) + 'newFakeLUN', 'false', block_count=0, dest_block=0, + source_snapshot=None, src_block=0) def test_clone_lun_blocks(self): """Test for when clone lun is passed block information.""" @@ -322,7 +324,8 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', 'newFakeLUN', 'false', block_count=block_count, - dest_block=dest_block, src_block=src_block) + dest_block=dest_block, src_block=src_block, + source_snapshot=None) def test_clone_lun_no_space_reservation(self): """Test for when space_reservation is not passed.""" @@ -337,7 +340,8 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( '/vol/fake/fakeLUN', '/vol/fake/newFakeLUN', 'fakeLUN', - 'newFakeLUN', 'false', block_count=0, dest_block=0, src_block=0) + 'newFakeLUN', 'false', block_count=0, dest_block=0, src_block=0, + source_snapshot=None) def test_clone_lun_qos_supplied(self): """Test for qos supplied in clone lun invocation.""" @@ -526,6 +530,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): expected = [{ 'pool_name': 'vol1', + 'consistencygroup_support': True, 'QoS_support': False, 'thin_provisioning_support': not thick, 'thick_provisioning_support': thick, diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py index 17e3eab88..1437be8a8 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py @@ -4,6 +4,7 @@ # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved. # Copyright (c) 2015 Dustin Schoenbrun. All rights reserved. +# Copyright (c) 2016 Chuck Fouts. All rights reserved. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -1047,3 +1048,155 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): self.assertEqual('CHAP', data['discovery_auth_method']) self.assertEqual('user1', data['discovery_auth_username']) self.assertEqual('pass1', data['discovery_auth_password']) + + def test_create_cgsnapshot(self): + snapshot = fake.CG_SNAPSHOT + snapshot['volume'] = fake.CG_VOLUME + + mock_extract_host = self.mock_object( + volume_utils, 'extract_host', + mock.Mock(return_value=fake.POOL_NAME)) + + mock_clone_lun = self.mock_object(self.library, '_clone_lun') + mock_busy = self.mock_object(self.library, '_handle_busy_snapshot') + + self.library.create_cgsnapshot(fake.CG_SNAPSHOT, [snapshot]) + + mock_extract_host.assert_called_once_with(fake.CG_VOLUME['host'], + level='pool') + self.zapi_client.create_cg_snapshot.assert_called_once_with( + set([fake.POOL_NAME]), fake.CG_SNAPSHOT_ID) + mock_clone_lun.assert_called_once_with( + fake.CG_VOLUME_NAME, fake.CG_SNAPSHOT_NAME, + source_snapshot=fake.CG_SNAPSHOT_ID) + mock_busy.assert_called_once_with(fake.POOL_NAME, fake.CG_SNAPSHOT_ID) + + def test_delete_cgsnapshot(self): + + mock_delete_snapshot = self.mock_object( + self.library, '_delete_lun') + + self.library.delete_cgsnapshot(fake.CG_SNAPSHOT, [fake.CG_SNAPSHOT]) + + mock_delete_snapshot.assert_called_once_with(fake.CG_SNAPSHOT['name']) + + def test_delete_cgsnapshot_not_found(self): + self.mock_object(block_base, 'LOG') + self.mock_object(self.library, '_get_lun_attr', + mock.Mock(return_value=None)) + + self.library.delete_cgsnapshot(fake.CG_SNAPSHOT, [fake.CG_SNAPSHOT]) + + self.assertEqual(0, block_base.LOG.error.call_count) + self.assertEqual(1, block_base.LOG.warning.call_count) + self.assertEqual(0, block_base.LOG.info.call_count) + + def test_create_volume_with_cg(self): + volume_size_in_bytes = int(fake.CG_VOLUME_SIZE) * units.Gi + self._create_volume_test_helper() + + self.library.create_volume(fake.CG_VOLUME) + + self.library._create_lun.assert_called_once_with( + fake.POOL_NAME, fake.CG_VOLUME_NAME, volume_size_in_bytes, + fake.CG_LUN_METADATA, None) + self.assertEqual(0, self.library. + _mark_qos_policy_group_for_deletion.call_count) + self.assertEqual(0, block_base.LOG.error.call_count) + + def _create_volume_test_helper(self): + self.mock_object(na_utils, 'get_volume_extra_specs') + self.mock_object(na_utils, 'log_extra_spec_warnings') + self.mock_object(block_base, 'LOG') + self.mock_object(volume_utils, 'extract_host', + mock.Mock(return_value=fake.POOL_NAME)) + self.mock_object(self.library, '_setup_qos_for_volume', + mock.Mock(return_value=None)) + self.mock_object(self.library, '_create_lun') + self.mock_object(self.library, '_create_lun_handle') + self.mock_object(self.library, '_add_lun_to_table') + self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') + + def test_create_consistency_group(self): + model_update = self.library.create_consistencygroup( + fake.CONSISTENCY_GROUP) + self.assertEqual('available', model_update['status']) + + def test_delete_consistencygroup_volume_delete_failure(self): + self.mock_object(block_base, 'LOG') + self.mock_object(self.library, '_delete_lun', + mock.Mock(side_effect=Exception)) + + model_update, volumes = self.library.delete_consistencygroup( + fake.CONSISTENCY_GROUP, [fake.CG_VOLUME]) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('error_deleting', volumes[0]['status']) + self.assertEqual(1, block_base.LOG.exception.call_count) + + def test_delete_consistencygroup_not_found(self): + self.mock_object(block_base, 'LOG') + self.mock_object(self.library, '_get_lun_attr', + mock.Mock(return_value=None)) + + model_update, volumes = self.library.delete_consistencygroup( + fake.CONSISTENCY_GROUP, [fake.CG_VOLUME]) + + self.assertEqual(0, block_base.LOG.error.call_count) + self.assertEqual(1, block_base.LOG.warning.call_count) + self.assertEqual(0, block_base.LOG.info.call_count) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('deleted', volumes[0]['status']) + + def test_create_consistencygroup_from_src_cg_snapshot(self): + + mock_clone_source_to_destination = self.mock_object( + self.library, '_clone_source_to_destination') + + self.library.create_consistencygroup_from_src( + fake.CONSISTENCY_GROUP, [fake.VOLUME], cgsnapshot=fake.CG_SNAPSHOT, + snapshots=[fake.CG_VOLUME_SNAPSHOT]) + + clone_source_to_destination_args = { + 'name': fake.CG_SNAPSHOT['name'], + 'size': fake.CG_SNAPSHOT['volume_size'], + } + mock_clone_source_to_destination.assert_called_once_with( + clone_source_to_destination_args, fake.VOLUME) + + def test_create_consistencygroup_from_src_cg(self): + class fake_lun_name(object): + pass + fake_lun_name_instance = fake_lun_name() + fake_lun_name_instance.name = fake.SOURCE_CG_VOLUME['name'] + self.mock_object(self.library, '_get_lun_from_table', mock.Mock( + return_value=fake_lun_name_instance) + ) + mock_clone_source_to_destination = self.mock_object( + self.library, '_clone_source_to_destination') + + self.library.create_consistencygroup_from_src( + fake.CONSISTENCY_GROUP, [fake.VOLUME], + source_cg=fake.SOURCE_CONSISTENCY_GROUP, + source_vols=[fake.SOURCE_CG_VOLUME]) + + clone_source_to_destination_args = { + 'name': fake.SOURCE_CG_VOLUME['name'], + 'size': fake.SOURCE_CG_VOLUME['size'], + } + mock_clone_source_to_destination.assert_called_once_with( + clone_source_to_destination_args, fake.VOLUME) + + def test_handle_busy_snapshot(self): + self.mock_object(block_base, 'LOG') + mock_get_snapshot = self.mock_object( + self.zapi_client, 'get_snapshot', + mock.Mock(return_value=fake.SNAPSHOT) + ) + + self.library._handle_busy_snapshot(fake.FLEXVOL, fake.SNAPSHOT_NAME) + + self.assertEqual(1, block_base.LOG.info.call_count) + mock_get_snapshot.assert_called_once_with(fake.FLEXVOL, + fake.SNAPSHOT_NAME) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py index a4c79094c..c60219f21 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -1,6 +1,7 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -199,7 +200,8 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'false', block_count=0, - dest_block=0, src_block=0, qos_policy_group_name=None) + dest_block=0, src_block=0, qos_policy_group_name=None, + source_snapshot=None) def test_clone_lun_blocks(self): """Test for when clone lun is passed block information.""" @@ -224,7 +226,8 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'false', block_count=block_count, dest_block=dest_block, - src_block=src_block, qos_policy_group_name=None) + src_block=src_block, qos_policy_group_name=None, + source_snapshot=None) def test_clone_lun_no_space_reservation(self): """Test for when space_reservation is not passed.""" @@ -244,7 +247,8 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.library.zapi_client.clone_lun.assert_called_once_with( 'fakeLUN', 'fakeLUN', 'newFakeLUN', 'false', block_count=0, - dest_block=0, src_block=0, qos_policy_group_name=None) + dest_block=0, src_block=0, qos_policy_group_name=None, + source_snapshot=None) def test_get_fc_target_wwpns(self): ports = [fake.FC_FORMATTED_TARGET_WWPNS[0], @@ -372,6 +376,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): goodness_function='goodness') expected = [{'pool_name': 'vola', + 'consistencygroup_support': True, 'netapp_unmirrored': 'true', 'QoS_support': True, 'thin_provisioning_support': not thick, diff --git a/cinder/volume/drivers/netapp/dataontap/block_7mode.py b/cinder/volume/drivers/netapp/dataontap/block_7mode.py index 8012fe1ae..0c184c1db 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_7mode.py @@ -7,6 +7,7 @@ # Copyright (c) 2014 Jeff Applewhite. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -193,7 +194,7 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary): def _clone_lun(self, name, new_name, space_reserved=None, qos_policy_group_name=None, src_block=0, dest_block=0, - block_count=0): + block_count=0, source_snapshot=None): """Clone LUN with the given handle to the new name.""" if not space_reserved: space_reserved = self.lun_space_reservation @@ -210,7 +211,8 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary): self.zapi_client.clone_lun(path, clone_path, name, new_name, space_reserved, src_block=src_block, dest_block=dest_block, - block_count=block_count) + block_count=block_count, + source_snapshot=source_snapshot) self.vol_refresh_voluntary = True luns = self.zapi_client.get_lun_by_args(path=clone_path) @@ -322,6 +324,8 @@ class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary): pool['filter_function'] = filter_function pool['goodness_function'] = goodness_function + pool['consistencygroup_support'] = True + pools.append(pool) return pools diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index d28705c00..5028d497f 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -6,8 +6,9 @@ # Copyright (c) 2014 Andrew Kerr. All rights reserved. # Copyright (c) 2014 Jeff Applewhite. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. -# Copyright (c) 2015 Chuck Fouts. All rights reserved. # Copyright (c) 2015 Dustin Schoenbrun. All rights reserved. +# Copyright (c) 2016 Chuck Fouts. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -452,7 +453,7 @@ class NetAppBlockStorageLibrary(object): def _clone_lun(self, name, new_name, space_reserved='true', qos_policy_group_name=None, src_block=0, dest_block=0, - block_count=0): + block_count=0, source_snapshot=None): """Clone LUN with the given name to the new name.""" raise NotImplementedError() @@ -983,3 +984,143 @@ class NetAppBlockStorageLibrary(object): init_targ_map[initiator] = target_wwpns return target_wwpns, init_targ_map, num_paths + + def create_consistencygroup(self, group): + """Driver entry point for creating a consistency group. + + ONTAP does not maintain an actual CG construct. As a result, no + communication to the backend is necessary for consistency group + creation. + + :return: Hard-coded model update for consistency group model. + """ + model_update = {'status': 'available'} + return model_update + + def delete_consistencygroup(self, group, volumes): + """Driver entry point for deleting a consistency group. + + :return: Updated consistency group model and list of volume models + for the volumes that were deleted. + """ + model_update = {'status': 'deleted'} + volumes_model_update = [] + for volume in volumes: + try: + self._delete_lun(volume['name']) + volumes_model_update.append( + {'id': volume['id'], 'status': 'deleted'}) + except Exception: + volumes_model_update.append( + {'id': volume['id'], 'status': 'error_deleting'}) + LOG.exception(_LE("Volume %(vol) in the consistency group " + "could not be deleted."), {'vol': volume}) + return model_update, volumes_model_update + + def update_consistencygroup(self, group, add_volumes=None, + remove_volumes=None): + """Driver entry point for updating a consistency group. + + Since no actual CG construct is ever created in ONTAP, it is not + necessary to update any metadata on the backend. Since this is a NO-OP, + there is guaranteed to be no change in any of the volumes' statuses. + """ + return None, None, None + + def create_cgsnapshot(self, cgsnapshot, snapshots): + """Creates a Cinder cgsnapshot object. + + The Cinder cgsnapshot object is created by making use of an + ephemeral ONTAP CG in order to provide write-order consistency for a + set of flexvol snapshots. First, a list of the flexvols backing the + given Cinder CG must be gathered. An ONTAP cg-snapshot of these + flexvols will create a snapshot copy of all the Cinder volumes in the + CG group. For each Cinder volume in the CG, it is then necessary to + clone its backing LUN from the ONTAP cg-snapshot. The naming convention + used for the clones is what indicates the clone's role as a Cinder + snapshot and its inclusion in a Cinder CG. The ONTAP CG-snapshot of + the flexvols is no longer required after having cloned the LUNs + backing the Cinder volumes in the Cinder CG. + + :return: An implicit update for cgsnapshot and snapshots models that + is interpreted by the manager to set their models to available. + """ + flexvols = set() + for snapshot in snapshots: + flexvols.add(volume_utils.extract_host(snapshot['volume']['host'], + level='pool')) + + self.zapi_client.create_cg_snapshot(flexvols, cgsnapshot['id']) + + for snapshot in snapshots: + self._clone_lun(snapshot['volume']['name'], snapshot['name'], + source_snapshot=cgsnapshot['id']) + + for flexvol in flexvols: + self._handle_busy_snapshot(flexvol, cgsnapshot['id']) + self.zapi_client.delete_snapshot(flexvol, cgsnapshot['id']) + + return None, None + + @utils.retry(exception.SnapshotIsBusy) + def _handle_busy_snapshot(self, flexvol, snapshot_name): + """Checks for and handles a busy snapshot. + + If a snapshot is not busy, take no action. If a snapshot is busy for + reasons other than a clone dependency, raise immediately. Otherwise, + since we always start a clone split operation after cloning a share, + wait up to a minute for a clone dependency to clear before giving up. + """ + snapshot = self.zapi_client.get_snapshot(flexvol, snapshot_name) + if not snapshot['busy']: + LOG.info(_LI("Backing consistency group snapshot %s " + "available for deletion"), snapshot_name) + return + else: + LOG.debug('Snapshot %(snap)s for vol %(vol)s is busy, waiting ' + 'for volume clone dependency to clear.', + {'snap': snapshot_name, 'vol': flexvol}) + + raise exception.SnapshotIsBusy(snapshot_name=snapshot_name) + + def delete_cgsnapshot(self, cgsnapshot, snapshots): + """Delete LUNs backing each snapshot in the cgsnapshot. + + :return: An implicit update for snapshots models that is interpreted + by the manager to set their models to deleted. + """ + for snapshot in snapshots: + self._delete_lun(snapshot['name']) + LOG.debug("Snapshot %s deletion successful", snapshot['name']) + + return None, None + + def create_consistencygroup_from_src(self, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + """Creates a CG from a either a cgsnapshot or group of cinder vols. + + :return: An implicit update for the volumes model that is + interpreted by the manager as a successful operation. + """ + LOG.debug("VOLUMES %s ", [dict(vol) for vol in volumes]) + + if cgsnapshot: + vols = zip(volumes, snapshots) + + for volume, snapshot in vols: + source = { + 'name': snapshot['name'], + 'size': snapshot['volume_size'], + } + self._clone_source_to_destination(source, volume) + + else: + vols = zip(volumes, source_vols) + + for volume, old_src_vref in vols: + src_lun = self._get_lun_from_table(old_src_vref['name']) + source = {'name': src_lun.name, 'size': old_src_vref['size']} + self._clone_source_to_destination(source, volume) + + return None, None diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py index 0e59b0cbb..acb9b54f7 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -7,6 +7,7 @@ # Copyright (c) 2014 Jeff Applewhite. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -128,16 +129,19 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary): def _clone_lun(self, name, new_name, space_reserved=None, qos_policy_group_name=None, src_block=0, dest_block=0, - block_count=0): + block_count=0, source_snapshot=None): """Clone LUN with the given handle to the new name.""" if not space_reserved: space_reserved = self.lun_space_reservation metadata = self._get_lun_attr(name, 'metadata') volume = metadata['Volume'] + self.zapi_client.clone_lun(volume, name, new_name, space_reserved, qos_policy_group_name=qos_policy_group_name, src_block=src_block, dest_block=dest_block, - block_count=block_count) + block_count=block_count, + source_snapshot=source_snapshot) + LOG.debug("Cloned LUN with new name %s", new_name) lun = self.zapi_client.get_lun_by_args(vserver=self.vserver, path='/vol/%s/%s' @@ -267,6 +271,8 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary): pool['filter_function'] = filter_function pool['goodness_function'] = goodness_function + pool['consistencygroup_support'] = True + pools.append(pool) return pools diff --git a/cinder/volume/drivers/netapp/dataontap/client/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py index 2ba813b30..f5848f9d5 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/api.py +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -40,6 +40,7 @@ LOG = logging.getLogger(__name__) EAPINOTFOUND = '13005' ESIS_CLONE_NOT_LICENSED = '14956' +ESNAPSHOTNOTALLOWED = '13023' class NaServer(object): diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py index 7e099bbc2..8711d0c73 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py @@ -1,5 +1,6 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -27,6 +28,7 @@ from cinder import utils from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base +from oslo_utils import strutils LOG = logging.getLogger(__name__) @@ -228,7 +230,7 @@ class Client(client_base.Client): def clone_lun(self, path, clone_path, name, new_name, space_reserved='true', src_block=0, - dest_block=0, block_count=0): + dest_block=0, block_count=0, source_snapshot=None): # zAPI can only handle 2^24 blocks per range bc_limit = 2 ** 24 # 8GB # zAPI can only handle 32 block ranges per call @@ -244,10 +246,16 @@ class Client(client_base.Client): zbc -= z_limit else: block_count = zbc + + zapi_args = { + 'source-path': path, + 'destination-path': clone_path, + 'no-snap': 'true', + } + if source_snapshot: + zapi_args['snapshot-name'] = source_snapshot clone_start = netapp_api.NaElement.create_node_with_children( - 'clone-start', **{'source-path': path, - 'destination-path': clone_path, - 'no-snap': 'true'}) + 'clone-start', **zapi_args) if block_count > 0: block_ranges = netapp_api.NaElement("block-ranges") # zAPI can only handle 2^24 block ranges @@ -536,3 +544,42 @@ class Client(client_base.Client): system_info = result.get_child_by_name('system-info') system_name = system_info.get_child_content('system-name') return system_name + + def get_snapshot(self, volume_name, snapshot_name): + """Gets a single snapshot.""" + snapshot_list_info = netapp_api.NaElement('snapshot-list-info') + snapshot_list_info.add_new_child('volume', volume_name) + result = self.connection.invoke_successfully(snapshot_list_info, + enable_tunneling=True) + + snapshots = result.get_child_by_name('snapshots') + if not snapshots: + msg = _('No snapshots could be found on volume %s.') + raise exception.VolumeBackendAPIException(data=msg % volume_name) + snapshot_list = snapshots.get_children() + snapshot = None + for s in snapshot_list: + if (snapshot_name == s.get_child_content('name')) and (snapshot + is None): + snapshot = { + 'name': s.get_child_content('name'), + 'volume': s.get_child_content('volume'), + 'busy': strutils.bool_from_string( + s.get_child_content('busy')), + } + snapshot_owners_list = s.get_child_by_name( + 'snapshot-owners-list') or netapp_api.NaElement('none') + snapshot_owners = set([snapshot_owner.get_child_content( + 'owner') for snapshot_owner in + snapshot_owners_list.get_children()]) + snapshot['owners'] = snapshot_owners + elif (snapshot_name == s.get_child_content('name')) and ( + snapshot is not None): + msg = _('Could not find unique snapshot %(snap)s on ' + 'volume %(vol)s.') + msg_args = {'snap': snapshot_name, 'vol': volume_name} + raise exception.VolumeBackendAPIException(data=msg % msg_args) + if not snapshot: + raise exception.SnapshotNotFound(snapshot_id=snapshot_name) + + return snapshot diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index 265aa3bf3..367662695 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -1,6 +1,7 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -396,3 +397,37 @@ class Client(object): LOG.warning(_LW("Failed to invoke ems. Message : %s"), e) finally: requester.last_ems = timeutils.utcnow() + + def delete_snapshot(self, volume_name, snapshot_name): + """Deletes a volume snapshot.""" + api_args = {'volume': volume_name, 'snapshot': snapshot_name} + self.send_request('snapshot-delete', api_args) + + def create_cg_snapshot(self, volume_names, snapshot_name): + """Creates a consistency group snapshot out of one or more flexvols. + + ONTAP requires an invocation of cg-start to first fence off the + flexvols to be included in the snapshot. If cg-start returns + success, a cg-commit must be executed to finalized the snapshot and + unfence the flexvols. + """ + cg_id = self._start_cg_snapshot(volume_names, snapshot_name) + if not cg_id: + msg = _('Could not start consistency group snapshot %s.') + raise exception.VolumeBackendAPIException(data=msg % snapshot_name) + self._commit_cg_snapshot(cg_id) + + def _start_cg_snapshot(self, volume_names, snapshot_name): + snapshot_init = { + 'snapshot': snapshot_name, + 'timeout': 'relaxed', + 'volumes': [ + {'volume-name': volume_name} for volume_name in volume_names + ], + } + result = self.send_request('cg-start', snapshot_init) + return result.get_child_content('cg-id') + + def _commit_cg_snapshot(self, cg_id): + snapshot_commit = {'cg-id': cg_id} + self.send_request('cg-commit', snapshot_commit) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index db177cdc5..a6c851308 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -1,6 +1,7 @@ # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -28,6 +29,8 @@ from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp import utils as na_utils +from oslo_utils import strutils + LOG = logging.getLogger(__name__) DELETED_PREFIX = 'deleted_cinder_' @@ -314,7 +317,7 @@ class Client(client_base.Client): def clone_lun(self, volume, name, new_name, space_reserved='true', qos_policy_group_name=None, src_block=0, dest_block=0, - block_count=0): + block_count=0, source_snapshot=None): # zAPI can only handle 2^24 blocks per range bc_limit = 2 ** 24 # 8GB # zAPI can only handle 32 block ranges per call @@ -330,11 +333,17 @@ class Client(client_base.Client): zbc -= z_limit else: block_count = zbc + + zapi_args = { + 'volume': volume, + 'source-path': name, + 'destination-path': new_name, + 'space-reserve': space_reserved, + } + if source_snapshot: + zapi_args['snapshot-name'] = source_snapshot clone_create = netapp_api.NaElement.create_node_with_children( - 'clone-create', - **{'volume': volume, 'source-path': name, - 'destination-path': new_name, - 'space-reserve': space_reserved}) + 'clone-create', **zapi_args) if qos_policy_group_name is not None: clone_create.add_new_child('qos-policy-group-name', qos_policy_group_name) @@ -860,3 +869,82 @@ class Client(client_base.Client): }) return counter_data + + def get_snapshot(self, volume_name, snapshot_name): + """Gets a single snapshot.""" + api_args = { + 'query': { + 'snapshot-info': { + 'name': snapshot_name, + 'volume': volume_name, + }, + }, + 'desired-attributes': { + 'snapshot-info': { + 'name': None, + 'volume': None, + 'busy': None, + 'snapshot-owners-list': { + 'snapshot-owner': None, + } + }, + }, + } + result = self.send_request('snapshot-get-iter', api_args) + + self._handle_get_snapshot_return_failure(result, snapshot_name) + + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + snapshot_info_list = attributes_list.get_children() + + self._handle_snapshot_not_found(result, snapshot_info_list, + snapshot_name, volume_name) + + snapshot_info = snapshot_info_list[0] + snapshot = { + 'name': snapshot_info.get_child_content('name'), + 'volume': snapshot_info.get_child_content('volume'), + 'busy': strutils.bool_from_string( + snapshot_info.get_child_content('busy')), + } + + snapshot_owners_list = snapshot_info.get_child_by_name( + 'snapshot-owners-list') or netapp_api.NaElement('none') + snapshot_owners = set([ + snapshot_owner.get_child_content('owner') + for snapshot_owner in snapshot_owners_list.get_children()]) + snapshot['owners'] = snapshot_owners + + return snapshot + + def _handle_get_snapshot_return_failure(self, result, snapshot_name): + error_record_list = result.get_child_by_name( + 'volume-errors') or netapp_api.NaElement('none') + errors = error_record_list.get_children() + + if errors: + error = errors[0] + error_code = error.get_child_content('errno') + error_reason = error.get_child_content('reason') + msg = _('Could not read information for snapshot %(name)s. ' + 'Code: %(code)s. Reason: %(reason)s') + msg_args = { + 'name': snapshot_name, + 'code': error_code, + 'reason': error_reason, + } + if error_code == netapp_api.ESNAPSHOTNOTALLOWED: + raise exception.SnapshotUnavailable(msg % msg_args) + else: + raise exception.VolumeBackendAPIException(data=msg % msg_args) + + def _handle_snapshot_not_found(self, result, snapshot_info_list, + snapshot_name, volume_name): + if not self._has_records(result): + raise exception.SnapshotNotFound(snapshot_id=snapshot_name) + elif len(snapshot_info_list) > 1: + msg = _('Could not find unique snapshot %(snap)s on ' + 'volume %(vol)s.') + msg_args = {'snap': snapshot_name, 'vol': volume_name} + raise exception.VolumeBackendAPIException(data=msg % msg_args) diff --git a/cinder/volume/drivers/netapp/dataontap/fc_7mode.py b/cinder/volume/drivers/netapp/dataontap/fc_7mode.py index 9efe27c69..7209e8c82 100644 --- a/cinder/volume/drivers/netapp/dataontap/fc_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/fc_7mode.py @@ -1,4 +1,5 @@ # Copyright (c) - 2014, Clinton Knight. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -26,6 +27,7 @@ LOG = logging.getLogger(__name__) class NetApp7modeFibreChannelDriver(driver.BaseVD, + driver.ConsistencyGroupVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, @@ -106,3 +108,27 @@ class NetApp7modeFibreChannelDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup(group, add_volumes=None, + remove_volumes=None) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot=cgsnapshot, snapshots=snapshots, + source_cg=source_cg, source_vols=source_vols) diff --git a/cinder/volume/drivers/netapp/dataontap/fc_cmode.py b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py index 391fff158..efd9795f2 100644 --- a/cinder/volume/drivers/netapp/dataontap/fc_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py @@ -1,4 +1,5 @@ # Copyright (c) - 2014, Clinton Knight. All rights reserved. +# Copyright (c) - 2016 Mike Rooney. 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 @@ -26,6 +27,7 @@ LOG = logging.getLogger(__name__) class NetAppCmodeFibreChannelDriver(driver.BaseVD, + driver.ConsistencyGroupVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, @@ -106,3 +108,27 @@ class NetAppCmodeFibreChannelDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup(group, add_volumes=None, + remove_volumes=None) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot=cgsnapshot, snapshots=snapshots, + source_cg=source_cg, source_vols=source_vols) diff --git a/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py b/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py index aa3288649..d3e09ef33 100644 --- a/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -25,6 +26,7 @@ LOG = logging.getLogger(__name__) class NetApp7modeISCSIDriver(driver.BaseVD, + driver.ConsistencyGroupVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, @@ -103,3 +105,27 @@ class NetApp7modeISCSIDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup(group, add_volumes=None, + remove_volumes=None) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot=cgsnapshot, snapshots=snapshots, + source_cg=source_cg, source_vols=source_vols) diff --git a/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py index 9ce71ad7f..e11e6d960 100644 --- a/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2016 Mike Rooney. 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 @@ -25,6 +26,7 @@ LOG = logging.getLogger(__name__) class NetAppCmodeISCSIDriver(driver.BaseVD, + driver.ConsistencyGroupVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, @@ -103,3 +105,27 @@ class NetAppCmodeISCSIDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup(group, add_volumes=None, + remove_volumes=None) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot=cgsnapshot, snapshots=snapshots, + source_cg=source_cg, source_vols=source_vols) diff --git a/releasenotes/notes/NetApp-ONTAP-full-cg-support-cfdc91bf0acf9fe1.yaml b/releasenotes/notes/NetApp-ONTAP-full-cg-support-cfdc91bf0acf9fe1.yaml new file mode 100644 index 000000000..19374293d --- /dev/null +++ b/releasenotes/notes/NetApp-ONTAP-full-cg-support-cfdc91bf0acf9fe1.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added support for creating, deleting, and updating consistency groups for + NetApp 7mode and CDOT backends. + - Added support for taking, deleting, and restoring a cgsnapshot for NetApp + 7mode and CDOT backends.