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 <mr.alex.meade@gmail.com> Co-Authored-By: Chuck Fouts <fchuck@netapp.com> DocImpact Implements: blueprint cinder-consistency-groups Change-Id: Ia74c634835958876d97daf6766f2ef110b33ddc4
This commit is contained in:
parent
8ed2d59395
commit
3b2d17a5db
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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']})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -40,6 +40,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
EAPINOTFOUND = '13005'
|
||||
ESIS_CLONE_NOT_LICENSED = '14956'
|
||||
ESNAPSHOTNOTALLOWED = '13023'
|
||||
|
||||
|
||||
class NaServer(object):
|
||||
|
@ -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
|
||||
clone_start = netapp_api.NaElement.create_node_with_children(
|
||||
'clone-start', **{'source-path': path,
|
||||
|
||||
zapi_args = {
|
||||
'source-path': path,
|
||||
'destination-path': clone_path,
|
||||
'no-snap': 'true'})
|
||||
'no-snap': 'true',
|
||||
}
|
||||
if source_snapshot:
|
||||
zapi_args['snapshot-name'] = source_snapshot
|
||||
clone_start = netapp_api.NaElement.create_node_with_children(
|
||||
'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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
clone_create = netapp_api.NaElement.create_node_with_children(
|
||||
'clone-create',
|
||||
**{'volume': volume, 'source-path': name,
|
||||
|
||||
zapi_args = {
|
||||
'volume': volume,
|
||||
'source-path': name,
|
||||
'destination-path': new_name,
|
||||
'space-reserve': space_reserved})
|
||||
'space-reserve': space_reserved,
|
||||
}
|
||||
if source_snapshot:
|
||||
zapi_args['snapshot-name'] = source_snapshot
|
||||
clone_create = netapp_api.NaElement.create_node_with_children(
|
||||
'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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user