NetApp cDOT: Add NetApp Volume Encryption support

NVE is a software-based technology for encrypting
data at rest one volume at a time. An encryption
key, accessible only to the storage system,
ensures that volume data cannot be read if the
underlying device is repurposed, returned,
misplaced or stolen.

Change-Id: I1bd710b4039320d4daef4a37a5b025164176c23c
Implements: blueprint netapp-cdot-storage-based-encrypted-volume-support
This commit is contained in:
Jose Porrua 2017-01-12 14:15:29 -05:00
parent e080840225
commit ebb62a7ed0
8 changed files with 251 additions and 1 deletions

View File

@ -889,6 +889,47 @@ CLONE_SPLIT_STATUS_NO_DATA_RESPONSE = etree.XML("""
</results>
""")
VOLUME_GET_ITER_ENCRYPTION_SSC_RESPONSE = etree.XML("""
<results status="passed">
<attributes-list>
<volume-attributes>
<encrypt>true</encrypt>
<volume-id-attributes>
<containing-aggregate-name>%(aggr)s</containing-aggregate-name>
<junction-path>/%(volume)s</junction-path>
<name>%(volume)s</name>
<owning-vserver-name>%(vserver)s</owning-vserver-name>
<type>rw</type>
</volume-id-attributes>
<volume-mirror-attributes>
<is-data-protection-mirror>false</is-data-protection-mirror>
<is-replica-volume>false</is-replica-volume>
</volume-mirror-attributes>
<volume-qos-attributes>
<policy-group-name>fake_qos_policy_group_name</policy-group-name>
</volume-qos-attributes>
<volume-space-attributes>
<is-space-guarantee-enabled>true</is-space-guarantee-enabled>
<space-guarantee>none</space-guarantee>
<percentage-snapshot-reserve>5</percentage-snapshot-reserve>
<size>12345</size>
</volume-space-attributes>
<volume-snapshot-attributes>
<snapshot-policy>default</snapshot-policy>
</volume-snapshot-attributes>
<volume-language-attributes>
<language-code>en_US</language-code>
</volume-language-attributes>
</volume-attributes>
</attributes-list>
<num-records>1</num-records>
</results>
""" % {
'aggr': VOLUME_AGGREGATE_NAMES[0],
'volume': VOLUME_NAMES[0],
'vserver': VOLUME_VSERVER_NAME,
})
STORAGE_DISK_GET_ITER_RESPONSE_PAGE_1 = etree.XML("""
<results status="passed">
<attributes-list>

View File

@ -1940,6 +1940,72 @@ class NetAppCmodeClientTestCase(test.TestCase):
self.assertFalse(result)
def test_is_flexvol_encrypted(self):
api_response = netapp_api.NaElement(
fake_client.VOLUME_GET_ITER_ENCRYPTION_SSC_RESPONSE)
self.client.features.add_feature('FLEXVOL_ENCRYPTION')
self.mock_object(self.client,
'send_iter_request',
return_value=api_response)
result = self.client.is_flexvol_encrypted(
fake_client.VOLUME_NAMES[0], fake_client.VOLUME_VSERVER_NAME)
volume_get_iter_args = {
'query': {
'volume-attributes': {
'encrypt': 'true',
'volume-id-attributes': {
'name': fake_client.VOLUME_NAME,
'owning-vserver-name': fake_client.VOLUME_VSERVER_NAME,
}
}
},
'desired-attributes': {
'volume-attributes': {
'encrypt': None,
}
}
}
self.client.send_iter_request.assert_called_once_with(
'volume-get-iter', volume_get_iter_args)
self.assertTrue(result)
def test_is_flexvol_encrypted_unsupported_version(self):
self.client.features.add_feature('FLEXVOL_ENCRYPTION', supported=False)
result = self.client.is_flexvol_encrypted(
fake_client.VOLUME_NAMES[0], fake_client.VOLUME_VSERVER_NAME)
self.assertFalse(result)
def test_is_flexvol_encrypted_no_records_found(self):
api_response = netapp_api.NaElement(
fake_client.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
return_value=api_response)
result = self.client.is_flexvol_encrypted(
fake_client.VOLUME_NAMES[0], fake_client.VOLUME_VSERVER_NAME)
self.assertFalse(result)
def test_is_flexvol_encrypted_api_error(self):
self.mock_object(self.client,
'send_request',
side_effect=self._mock_api_error())
result = self.client.is_flexvol_encrypted(
fake_client.VOLUME_NAMES[0], fake_client.VOLUME_VSERVER_NAME)
self.assertFalse(result)
def test_get_aggregates(self):
api_response = netapp_api.NaElement(

View File

@ -41,6 +41,7 @@ SSC = {
'netapp_raid_type': 'raid_dp',
'netapp_disk_type': ['SSD'],
'netapp_hybrid_aggregate': 'false',
'netapp_flexvol_encryption': 'true',
'pool_name': 'volume1',
},
'volume2': {
@ -54,6 +55,7 @@ SSC = {
'netapp_raid_type': 'raid_dp',
'netapp_disk_type': ['FCAL', 'SSD'],
'netapp_hybrid_aggregate': 'true',
'netapp_flexvol_encryption': 'false',
'pool_name': 'volume2',
},
}
@ -84,6 +86,15 @@ SSC_DEDUPE_INFO = {
},
}
SSC_ENCRYPTION_INFO = {
'volume1': {
'netapp_flexvol_encryption': 'true',
},
'volume2': {
'netapp_flexvol_encryption': 'false',
},
}
SSC_MIRROR_INFO = {
'volume1': {
'netapp_mirrored': 'false',
@ -118,6 +129,19 @@ PROVISIONING_OPTS = {
'size': 20,
}
ENCRYPTED_PROVISIONING_OPTS = {
'aggregate': 'fake_aggregate',
'thin_provisioned': True,
'snapshot_policy': None,
'language': 'en_US',
'dedupe_enabled': False,
'compression_enabled': False,
'snapshot_reserve': '12',
'volume_type': 'rw',
'size': 20,
'encrypt': 'true',
}
def get_fake_cmode_config(backend_name):

View File

@ -134,6 +134,10 @@ class CapabilitiesLibraryTestCase(test.TestCase):
self.ssc_library, '_get_ssc_aggregate_info',
side_effect=[fake.SSC_AGGREGATE_INFO['volume1'],
fake.SSC_AGGREGATE_INFO['volume2']])
mock_get_ssc_encryption_info = self.mock_object(
self.ssc_library, '_get_ssc_encryption_info',
side_effect=[fake.SSC_ENCRYPTION_INFO['volume1'],
fake.SSC_ENCRYPTION_INFO['volume2']])
ordered_ssc = collections.OrderedDict()
ordered_ssc['volume1'] = fake.SSC_VOLUME_MAP['volume1']
ordered_ssc['volume2'] = fake.SSC_VOLUME_MAP['volume2']
@ -150,6 +154,8 @@ class CapabilitiesLibraryTestCase(test.TestCase):
mock.call('volume1'), mock.call('volume2')])
mock_get_ssc_aggregate_info.assert_has_calls([
mock.call('aggr1'), mock.call('aggr2')])
mock_get_ssc_encryption_info.assert_has_calls([
mock.call('volume1'), mock.call('volume2')])
def test__update_for_failover(self):
self.mock_object(self.ssc_library, 'update_ssc')
@ -282,6 +288,22 @@ class CapabilitiesLibraryTestCase(test.TestCase):
self.zapi_client.get_flexvol_dedupe_info.assert_called_once_with(
fake_client.VOLUME_NAMES[0])
def test_get_ssc_encryption_info(self):
self.mock_object(
self.ssc_library.zapi_client, 'is_flexvol_encrypted',
return_value=True)
result = self.ssc_library._get_ssc_encryption_info(
fake_client.VOLUME_NAMES[0])
expected = {
'netapp_flexvol_encryption': 'true',
}
self.assertEqual(expected, result)
self.zapi_client.is_flexvol_encrypted.assert_called_once_with(
fake_client.VOLUME_NAMES[0], fake_client.VOLUME_VSERVER_NAME)
@ddt.data(True, False)
def test_get_ssc_mirror_info(self, mirrored):

View File

@ -543,6 +543,9 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
mock_get_provisioning_opts_call = self.mock_object(
self.mock_src_client, 'get_provisioning_options_from_flexvol',
return_value=provisioning_opts)
mock_is_flexvol_encrypted = self.mock_object(
self.mock_src_client, 'is_flexvol_encrypted',
return_value=False)
self.mock_object(self.dm_mixin, '_get_replication_aggregate_map',
return_value=aggr_map)
mock_client_call = self.mock_object(
@ -560,6 +563,45 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
mock_client_call.assert_called_once_with(
self.dest_flexvol_name, 'aggr01', fakes.PROVISIONING_OPTS['size'],
volume_type='dp', **expected_prov_opts)
mock_is_flexvol_encrypted.assert_called_once_with(
self.src_flexvol_name, self.src_vserver)
def test_create_encrypted_destination_flexvol(self):
aggr_map = {
fakes.ENCRYPTED_PROVISIONING_OPTS['aggregate']: 'aggr01',
'aggr20': 'aggr02',
}
provisioning_opts = copy.deepcopy(fakes.ENCRYPTED_PROVISIONING_OPTS)
expected_prov_opts = copy.deepcopy(fakes.ENCRYPTED_PROVISIONING_OPTS)
expected_prov_opts.pop('volume_type', None)
expected_prov_opts.pop('size', None)
expected_prov_opts.pop('aggregate', None)
mock_get_provisioning_opts_call = self.mock_object(
self.mock_src_client, 'get_provisioning_options_from_flexvol',
return_value=provisioning_opts)
mock_is_flexvol_encrypted = self.mock_object(
self.mock_src_client, 'is_flexvol_encrypted',
return_value=True)
self.mock_object(self.dm_mixin, '_get_replication_aggregate_map',
return_value=aggr_map)
mock_client_call = self.mock_object(
self.mock_dest_client, 'create_flexvol')
retval = self.dm_mixin.create_destination_flexvol(
self.src_backend, self.dest_backend,
self.src_flexvol_name, self.dest_flexvol_name)
self.assertIsNone(retval)
mock_get_provisioning_opts_call.assert_called_once_with(
self.src_flexvol_name)
self.dm_mixin._get_replication_aggregate_map.assert_called_once_with(
self.src_backend, self.dest_backend)
mock_client_call.assert_called_once_with(
self.dest_flexvol_name, 'aggr01',
fakes.ENCRYPTED_PROVISIONING_OPTS['size'],
volume_type='dp', **expected_prov_opts)
mock_is_flexvol_encrypted.assert_called_once_with(
self.src_flexvol_name, self.src_vserver)
def test_ensure_snapmirrors(self):
flexvols = ['nvol1', 'nvol2']

View File

@ -2,6 +2,7 @@
# Copyright (c) 2014 Clinton Knight. All rights reserved.
# Copyright (c) 2015 Tom Barron. All rights reserved.
# Copyright (c) 2016 Mike Rooney. All rights reserved.
# Copyright (c) 2017 Jose Porrua. 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
@ -60,6 +61,7 @@ class Client(client_base.Client):
ontapi_1_2x = (1, 20) <= ontapi_version < (1, 30)
ontapi_1_30 = ontapi_version >= (1, 30)
ontapi_1_100 = ontapi_version >= (1, 100)
ontapi_1_1xx = (1, 100) <= ontapi_version < (1, 200)
self.features.add_feature('SNAPMIRROR_V2', supported=ontapi_1_20)
self.features.add_feature('USER_CAPABILITY_LIST',
@ -73,6 +75,7 @@ class Client(client_base.Client):
supported=ontapi_1_30)
self.features.add_feature('BACKUP_CLONE_PARAM', supported=ontapi_1_100)
self.features.add_feature('CLUSTER_PEER_POLICY', supported=ontapi_1_30)
self.features.add_feature('FLEXVOL_ENCRYPTION', supported=ontapi_1_1xx)
def _invoke_vserver_api(self, na_element, vserver):
server = copy.copy(self.connection)
@ -1163,6 +1166,41 @@ class Client(client_base.Client):
return True
def is_flexvol_encrypted(self, flexvol_name, vserver_name):
"""Check if a flexvol is encrypted."""
if not self.features.FLEXVOL_ENCRYPTION:
return False
api_args = {
'query': {
'volume-attributes': {
'encrypt': 'true',
'volume-id-attributes': {
'name': flexvol_name,
'owning-vserver-name': vserver_name,
},
},
},
'desired-attributes': {
'volume-attributes': {
'encrypt': None,
},
},
}
try:
result = self.send_iter_request('volume-get-iter', api_args)
except netapp_api.NaApiError:
msg = _LE('Failed to get Encryption info for volume %s.')
LOG.exception(msg, flexvol_name)
return False
if not self._has_records(result):
return False
return True
def create_flexvol(self, flexvol_name, aggregate_name, size_gb,
space_guarantee_type=None, snapshot_policy=None,
language=None, dedupe_enabled=False,

View File

@ -1,4 +1,5 @@
# Copyright (c) 2016 Clinton Knight. All rights reserved.
# Copyright (c) 2017 Jose Porrua. 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
@ -44,7 +45,9 @@ SSC_API_MAP = {
'netapp_dedup',
'netapp_compression',
],
('volume', 'show', 'volume-get-iter'): [],
('volume', 'show', 'volume-get-iter'): [
'netapp_flexvol_encryption',
],
}
@ -129,6 +132,7 @@ class CapabilitiesLibrary(object):
ssc_volume.update(self._get_ssc_flexvol_info(flexvol_name))
ssc_volume.update(self._get_ssc_dedupe_info(flexvol_name))
ssc_volume.update(self._get_ssc_mirror_info(flexvol_name))
ssc_volume.update(self._get_ssc_encryption_info(flexvol_name))
# Get aggregate info
aggregate_name = ssc_volume.get('netapp_aggregate')
@ -189,6 +193,13 @@ class CapabilitiesLibrary(object):
'netapp_compression': six.text_type(compression).lower(),
}
def _get_ssc_encryption_info(self, flexvol_name):
"""Gather flexvol encryption info and recast into SSC-style stats."""
encrypted = self.zapi_client.is_flexvol_encrypted(
flexvol_name, self.vserver_name)
return {'netapp_flexvol_encryption': six.text_type(encrypted).lower()}
def _get_ssc_mirror_info(self, flexvol_name):
"""Gather SnapMirror info and recast into SSC-style volume stats."""

View File

@ -417,6 +417,12 @@ class DataMotionMixin(object):
src_flexvol_name)
)
# If the source is encrypted then the destination needs to be
# encrypted too. Using is_flexvol_encrypted because it includes
# a simple check to ensure that the NVE feature is supported.
if src_client.is_flexvol_encrypted(src_flexvol_name, src_vserver):
provisioning_options['encrypt'] = 'true'
# Remove size and volume_type
size = provisioning_options.pop('size', None)
if not size: