]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp: Implement CGs for ONTAP Drivers
authorMike Rooney <rooneym@netapp.com>
Thu, 17 Dec 2015 22:01:05 +0000 (17:01 -0500)
committerMike Rooney <rooneym@netapp.com>
Wed, 24 Feb 2016 20:31:22 +0000 (20:31 +0000)
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 <mr.alex.meade@gmail.com>
Co-Authored-By: Chuck Fouts <fchuck@netapp.com>
DocImpact
Implements: blueprint cinder-consistency-groups
Change-Id: Ia74c634835958876d97daf6766f2ef110b33ddc4

20 files changed:
cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py
cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py
cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py
cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py
cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py
cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py
cinder/volume/drivers/netapp/dataontap/block_7mode.py
cinder/volume/drivers/netapp/dataontap/block_base.py
cinder/volume/drivers/netapp/dataontap/block_cmode.py
cinder/volume/drivers/netapp/dataontap/client/api.py
cinder/volume/drivers/netapp/dataontap/client/client_7mode.py
cinder/volume/drivers/netapp/dataontap/client/client_base.py
cinder/volume/drivers/netapp/dataontap/client/client_cmode.py
cinder/volume/drivers/netapp/dataontap/fc_7mode.py
cinder/volume/drivers/netapp/dataontap/fc_cmode.py
cinder/volume/drivers/netapp/dataontap/iscsi_7mode.py
cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py
releasenotes/notes/NetApp-ONTAP-full-cg-support-cfdc91bf0acf9fe1.yaml [new file with mode: 0644]

index b2c56627203a02c05fa301d9f627efc9709020c9..76bf92fc9cbd205d688fdc13805a25f4b503011c 100644 (file)
@@ -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("""
   </results>
 """)
 
+SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_CMODE = etree.XML("""
+    <results status="passed">
+    <attributes-list>
+      <snapshot-info>
+        <name>%(snapshot_name)s</name>
+        <busy>False</busy>
+        <volume>%(vol_name)s</volume>
+      </snapshot-info>
+    </attributes-list>
+    <num-records>1</num-records>
+    </results>
+""" % {
+    'snapshot_name': fake.SNAPSHOT['name'],
+    'vol_name': fake.SNAPSHOT['volume_id'],
+})
+
+SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_CMODE = etree.XML("""
+    <results status="passed">
+    <attributes-list>
+      <snapshot-info>
+        <name>%(snapshot_name)s</name>
+        <busy>True</busy>
+        <volume>%(vol_name)s</volume>
+      </snapshot-info>
+    </attributes-list>
+    <num-records>1</num-records>
+    </results>
+""" % {
+    'snapshot_name': fake.SNAPSHOT['name'],
+    'vol_name': fake.SNAPSHOT['volume_id'],
+})
+
+SNAPSHOT_INFO_FOR_PRESENT_NOT_BUSY_SNAPSHOT_7MODE = etree.XML("""
+    <results status="passed">
+    <snapshots>
+      <snapshot-info>
+        <name>%(snapshot_name)s</name>
+        <busy>False</busy>
+        <volume>%(vol_name)s</volume>
+      </snapshot-info>
+    </snapshots>
+    </results>
+""" % {
+    'snapshot_name': fake.SNAPSHOT['name'],
+    'vol_name': fake.SNAPSHOT['volume_id'],
+})
+
+SNAPSHOT_INFO_FOR_PRESENT_BUSY_SNAPSHOT_7MODE = etree.XML("""
+    <results status="passed">
+    <snapshots>
+      <snapshot-info>
+        <name>%(snapshot_name)s</name>
+        <busy>True</busy>
+        <volume>%(vol_name)s</volume>
+      </snapshot-info>
+    </snapshots>
+    </results>
+""" % {
+    'snapshot_name': fake.SNAPSHOT['name'],
+    'vol_name': fake.SNAPSHOT['volume_id'],
+})
+
+SNAPSHOT_NOT_PRESENT_7MODE = etree.XML("""
+    <results status="passed">
+    <snapshots>
+      <snapshot-info>
+        <name>NOT_THE_RIGHT_SNAPSHOT</name>
+        <busy>false</busy>
+        <volume>%(vol_name)s</volume>
+      </snapshot-info>
+    </snapshots>
+    </results>
+""" % {'vol_name': fake.SNAPSHOT['volume_id']})
+
 NO_RECORDS_RESPONSE = etree.XML("""
   <results status="passed">
     <num-records>0</num-records>
index 2b072cc8feebe507a2bb6746f6b6d0c4bdd92195..aed35b32353db1bb8c6805a47700569ff1f6df13 100644 (file)
@@ -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)
index b492828cb342d6d3c2390a34652f5b807a8f3e62..f6db0a7318b42c5bcb50a82c58add18b30c6c5ef 100644 (file)
@@ -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']})
index 13f2959722b367d45ec346db9e31b865e2e02930..4ed9933c0b35084d1a7e1bc1680f61a94d1f4fcc 100644 (file)
@@ -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)
index 4df62ad338e414ced3d1cb3cc6799c36217d6e9e..6a2f91082f6728774ad2c8cdd22c457fd37ddde8 100644 (file)
@@ -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
index 60668a699ac88339d2372522cb1a3eada6391c07..68b87c988a0ac46b2e056ff9d2e435a31977b5e2 100644 (file)
@@ -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,
index 17e3eab888fd1e721611a18d257a0432322591bc..1437be8a8e5b7f9eb12ae8afaf3f55b909f2b666 100644 (file)
@@ -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)
index a4c79094c2906aebc6a2ce8021b61d43ce14f064..c60219f21289e223903c7f8de2091338146c92b9 100644 (file)
@@ -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,
index 8012fe1ae19ebc75f3be7d475e2275b9bc633eb6..0c184c1dbd21b2b031d8823776b51f5911fa622c 100644 (file)
@@ -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
index d28705c00f9f3b2927e2ffa21b3dc62c6aec4179..5028d497f508762a8e48eca3f8093d01666f6a48 100644 (file)
@@ -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
index 0e59b0cbbd954aefb96ea4d123c1a2bc4374f5b5..acb9b54f74957b69b7f4705ee4c36b8626179722 100644 (file)
@@ -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
index 2ba813b3094afb1ff8b86fa61751eda3f3238a93..f5848f9d5f33aa91b38d60d2b888424899c785c5 100644 (file)
@@ -40,6 +40,7 @@ LOG = logging.getLogger(__name__)
 
 EAPINOTFOUND = '13005'
 ESIS_CLONE_NOT_LICENSED = '14956'
+ESNAPSHOTNOTALLOWED = '13023'
 
 
 class NaServer(object):
index 7e099bbc270399f428abff3a6332a2fe49729d4a..8711d0c73e2f18a28895a186ae0de950921180df 100644 (file)
@@ -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
index 265aa3bf3e9566ae21a2872dc711d2f939d6aeec..36766269577cbafe837e155dc5b600a99cec37ef 100644 (file)
@@ -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)
index db177cdc59e296c0548dfa6d782f01b369f9cc2c..a6c8513086a6139ef5ce324a0abaa0993bc77008 100644 (file)
@@ -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)
index 9efe27c6922571bdce8147b1701b5f6ecc16f4bd..7209e8c82e3925f148294c99de17791825c1794d 100644 (file)
@@ -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)
index 391fff1585a7b720bc2e845b07c2227781b21adf..efd9795f2de18e8eb54aa5c12467894ab388ba0f 100644 (file)
@@ -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)
index aa32886493987fd17fa97432c81c10753631aeaf..d3e09ef33a83451b7e00bf958e39f550cb47f260 100644 (file)
@@ -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)
index 9ce71ad7fb3c91e99f471b7d80ff73c0de87bf8b..e11e6d960d44a9b61fba26db5bd9d828ab421d10 100644 (file)
@@ -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 (file)
index 0000000..1937429
--- /dev/null
@@ -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.