Manila cDOT netapp:thin_provisioned qualified extra spec
Manila has "share types", analogous to Cinder "volume types", that may contain qualified extra-specs - extra-specs that have significance only for the backend driver and which are not reported back to the scheduler. This commit implements the first of several qualified extra-specs: * netapp:thin_provisioned This extra spec determines whether the backing NetApp flexvol for a Manila share will be "thick" or "thin" provisioned, i.e. with or without space guarantees. This is a boolean extra spec, 'netapp:thin_provisioned=true' or 'netapp:thin_provisioned=false'. If this qualified extra spec is not supplied, then the default action is taken for the backing flexvol, namely to provision it as "thick". Partially implements blueprint cdot-qualified-specs Change-Id: I9096ead88ce94afc84a0c8bc8315f07368241ec2
This commit is contained in:
parent
2a7523cac7
commit
967fef055b
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2015 Clinton Knight. All rights reserved.
|
||||
# Copyright (c) 2015 Tom Barron. 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
|
||||
|
@ -715,7 +716,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
|||
raise exception.NetAppException(msg % e.message)
|
||||
|
||||
@na_utils.trace
|
||||
def create_volume(self, aggregate_name, volume_name, size_gb):
|
||||
def create_volume(self, aggregate_name, volume_name, size_gb,
|
||||
thin_provisioned=False):
|
||||
"""Creates a volume."""
|
||||
api_args = {
|
||||
'containing-aggr-name': aggregate_name,
|
||||
|
@ -723,6 +725,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
|||
'volume': volume_name,
|
||||
'junction-path': '/%s' % volume_name,
|
||||
}
|
||||
if thin_provisioned:
|
||||
api_args['space-reserve'] = 'none'
|
||||
self.send_request('volume-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright (c) 2015 Clinton Knight. All rights reserved.
|
||||
# Copyright (c) 2015 Tom Barron. 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
|
||||
|
@ -34,9 +35,9 @@ from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode
|
|||
from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode
|
||||
from manila.share.drivers.netapp import options as na_opts
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila.share import share_types
|
||||
from manila.share import utils as share_utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -45,6 +46,13 @@ class NetAppCmodeFileStorageLibrary(object):
|
|||
AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly
|
||||
SSC_UPDATE_INTERVAL_SECONDS = 3600 # hourly
|
||||
|
||||
# Maps NetApp qualified extra specs keys to corresponding backend API
|
||||
# client library argument keywords. When we expose more backend
|
||||
# capabilities here, we will add them to this map.
|
||||
BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP = {
|
||||
'netapp:thin_provisioned': 'thin_provisioned'
|
||||
}
|
||||
|
||||
def __init__(self, db, driver_name, **kwargs):
|
||||
na_utils.validate_driver_instantiation(**kwargs)
|
||||
|
||||
|
@ -280,14 +288,14 @@ class NetAppCmodeFileStorageLibrary(object):
|
|||
if pool:
|
||||
return pool
|
||||
|
||||
volume_name = self._get_valid_share_name(share['id'])
|
||||
return self._client.get_aggregate_for_volume(volume_name)
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
return self._client.get_aggregate_for_volume(share_name)
|
||||
|
||||
@na_utils.trace
|
||||
def create_share(self, context, share, share_server):
|
||||
"""Creates new share."""
|
||||
vserver, vserver_client = self._get_vserver(share_server=share_server)
|
||||
self._allocate_container(share, vserver, vserver_client)
|
||||
self._allocate_container(share, vserver_client)
|
||||
return self._create_export(share, vserver, vserver_client)
|
||||
|
||||
@na_utils.trace
|
||||
|
@ -299,20 +307,72 @@ class NetAppCmodeFileStorageLibrary(object):
|
|||
return self._create_export(share, vserver, vserver_client)
|
||||
|
||||
@na_utils.trace
|
||||
def _allocate_container(self, share, vserver, vserver_client):
|
||||
def _allocate_container(self, share, vserver_client):
|
||||
"""Create new share on aggregate."""
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
|
||||
# Get Data ONTAP aggregate name as pool name.
|
||||
aggregate_name = share_utils.extract_host(share['host'], level='pool')
|
||||
|
||||
if aggregate_name is None:
|
||||
pool_name = share_utils.extract_host(share['host'], level='pool')
|
||||
if pool_name is None:
|
||||
msg = _("Pool is not available in the share host field.")
|
||||
raise exception.InvalidHost(reason=msg)
|
||||
|
||||
LOG.debug('Creating volume %(share_name)s on aggregate %(aggregate)s',
|
||||
{'share_name': share_name, 'aggregate': aggregate_name})
|
||||
vserver_client.create_volume(aggregate_name, share_name, share['size'])
|
||||
extra_specs = share_types.get_extra_specs_from_share(share)
|
||||
self._check_boolean_extra_specs_validity(
|
||||
share, extra_specs, list(self.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP))
|
||||
provisioning_options = self._get_boolean_provisioning_options(
|
||||
extra_specs, self.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP)
|
||||
|
||||
LOG.debug('Creating share %(share)s on pool %(pool)s with '
|
||||
'provisioning options %(options)s',
|
||||
{'share': share_name, 'pool': pool_name,
|
||||
'options': provisioning_options})
|
||||
vserver_client.create_volume(pool_name, share_name,
|
||||
share['size'],
|
||||
**provisioning_options)
|
||||
|
||||
@na_utils.trace
|
||||
def _check_boolean_extra_specs_validity(self, share, specs,
|
||||
keys_of_interest):
|
||||
# Boolean extra spec values must be (ignoring case) 'true' or 'false'.
|
||||
for key in keys_of_interest:
|
||||
value = specs.get(key)
|
||||
if value is not None and value.lower() not in ['true', 'false']:
|
||||
type_id = share['share_type_id']
|
||||
share_id = share['id']
|
||||
arg_map = {'value': value, 'key': key, 'type_id': type_id,
|
||||
'share_id': share_id}
|
||||
msg = _('Invalid value "%(value)s" for extra_spec "%(key)s" '
|
||||
'in share_type %(type_id)s for share %(share_id)s.')
|
||||
raise exception.Invalid(msg % arg_map)
|
||||
|
||||
@na_utils.trace
|
||||
def _get_boolean_provisioning_options(self, specs, boolean_specs_map):
|
||||
"""Given extra specs, return corresponding client library kwargs.
|
||||
|
||||
Build a full set of client library provisioning kwargs, filling in a
|
||||
default value if an explicit value has not been supplied via a
|
||||
corresponding extra spec. Boolean extra spec values are "true" or
|
||||
"false", with missing specs treated as "false". Provisioning kwarg
|
||||
values are True or False.
|
||||
"""
|
||||
# Extract the extra spec keys of concern and their corresponding
|
||||
# kwarg keys as lists.
|
||||
keys_of_interest = list(boolean_specs_map)
|
||||
provisioning_args = [boolean_specs_map[key]
|
||||
for key in keys_of_interest]
|
||||
# Set missing spec values to 'false'
|
||||
for key in keys_of_interest:
|
||||
if key not in specs:
|
||||
specs[key] = 'false'
|
||||
# Build a list of Boolean provisioning arguments from the string
|
||||
# equivalents in the spec values.
|
||||
provisioning_values = [specs[key].lower() == 'true' for key in
|
||||
keys_of_interest]
|
||||
# Combine the list of provisioning args and the list of provisioning
|
||||
# values into a dictionary suitable for use as kwargs when invoking
|
||||
# provisioning methods from the client API library.
|
||||
return dict(zip(provisioning_args, provisioning_values))
|
||||
|
||||
@na_utils.trace
|
||||
def _allocate_container_from_snapshot(self, share, snapshot,
|
||||
|
@ -322,7 +382,7 @@ class NetAppCmodeFileStorageLibrary(object):
|
|||
parent_share_name = self._get_valid_share_name(snapshot['share_id'])
|
||||
parent_snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
|
||||
|
||||
LOG.debug('Creating volume from snapshot %s', snapshot['id'])
|
||||
LOG.debug('Creating share from snapshot %s', snapshot['id'])
|
||||
vserver_client.create_volume_clone(share_name, parent_share_name,
|
||||
parent_snapshot_name)
|
||||
|
||||
|
@ -456,7 +516,7 @@ class NetAppCmodeFileStorageLibrary(object):
|
|||
def _update_ssc_aggr_info(self, aggregate_names, ssc_stats):
|
||||
"""Updates the given SSC dictionary with new disk type information.
|
||||
|
||||
:param volume_groups: The volume groups this driver cares about
|
||||
:param aggregate_names: The aggregates this driver cares about
|
||||
:param ssc_stats: The dictionary to update
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright (c) 2014 Openstack Foundation.
|
||||
# Copyright (c) 2015 Tom Barron. 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
|
||||
|
@ -185,3 +186,8 @@ def share_types_diff(context, share_type_id1, share_type_id2):
|
|||
all_equal = False
|
||||
|
||||
return (diff, all_equal)
|
||||
|
||||
|
||||
def get_extra_specs_from_share(share):
|
||||
type_id = share.get('share_type_id', None)
|
||||
return get_share_type_extra_specs(type_id)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2015 Clinton Knight. All rights reserved.
|
||||
# Copyright (c) 2015 Tom Barron. 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
|
||||
|
@ -1290,11 +1291,29 @@ class NetAppClientCmodeTestCase(test.TestCase):
|
|||
'containing-aggr-name': fake.SHARE_AGGREGATE_NAME,
|
||||
'size': '100g',
|
||||
'volume': fake.SHARE_NAME,
|
||||
'junction-path': '/%s' % fake.SHARE_NAME
|
||||
'junction-path': '/%s' % fake.SHARE_NAME,
|
||||
}
|
||||
|
||||
self.client.send_request.assert_has_calls([
|
||||
mock.call('volume-create', volume_create_args)])
|
||||
self.client.send_request.assert_called_once_with('volume-create',
|
||||
volume_create_args)
|
||||
|
||||
def test_create_thin_volume(self):
|
||||
self.mock_object(self.client, 'send_request')
|
||||
|
||||
self.client.create_volume(
|
||||
fake.SHARE_AGGREGATE_NAME, fake.SHARE_NAME, 100,
|
||||
thin_provisioned=True)
|
||||
|
||||
volume_create_args = {
|
||||
'containing-aggr-name': fake.SHARE_AGGREGATE_NAME,
|
||||
'size': '100g',
|
||||
'volume': fake.SHARE_NAME,
|
||||
'junction-path': '/%s' % fake.SHARE_NAME,
|
||||
'space-reserve': 'none'
|
||||
}
|
||||
|
||||
self.client.send_request.assert_called_once_with('volume-create',
|
||||
volume_create_args)
|
||||
|
||||
def test_volume_exists(self):
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright (c) 2015 Clinton Knight. All rights reserved.
|
||||
# Copyright (c) 2015 Tom Barron. 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
|
||||
|
@ -30,6 +31,8 @@ from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base
|
|||
from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode
|
||||
from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila.share import share_types
|
||||
from manila.share import utils as share_utils
|
||||
from manila import test
|
||||
from manila.tests.share.drivers.netapp.dataontap import fakes as fake
|
||||
|
||||
|
@ -498,7 +501,6 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
|||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
mock_allocate_container.assert_called_once_with(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
mock_create_export.assert_called_once_with(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
|
@ -536,17 +538,61 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
|||
self.assertEqual('fake_export_location', result)
|
||||
|
||||
def test_allocate_container(self):
|
||||
|
||||
self.mock_object(self.library, '_get_valid_share_name', mock.Mock(
|
||||
return_value=fake.SHARE_NAME))
|
||||
self.mock_object(share_utils, 'extract_host', mock.Mock(
|
||||
return_value=fake.POOL_NAME))
|
||||
self.mock_object(share_types, 'get_extra_specs_from_share',
|
||||
mock.Mock(return_value=fake.EXTRA_SPEC))
|
||||
self.mock_object(self.library, '_check_boolean_extra_specs_validity')
|
||||
self.mock_object(self.library, '_get_boolean_provisioning_options',
|
||||
mock.Mock(return_value=fake.PROVISIONING_OPTIONS))
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.library._allocate_container(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
self.library._allocate_container(fake.EXTRA_SPEC_SHARE,
|
||||
vserver_client)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
vserver_client.create_volume.assert_called_with(fake.POOL_NAME,
|
||||
share_name,
|
||||
fake.SHARE['size'])
|
||||
vserver_client.create_volume.assert_called_once_with(
|
||||
fake.POOL_NAME, fake.SHARE_NAME, fake.SHARE['size'],
|
||||
thin_provisioned=True)
|
||||
|
||||
def test_check_boolean_extra_specs_validity(self):
|
||||
self.library._check_boolean_extra_specs_validity(
|
||||
fake.EXTRA_SPEC_SHARE, fake.EXTRA_SPEC,
|
||||
list(self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP))
|
||||
|
||||
def test_check_boolean_extra_specs_validity_empty_spec(self):
|
||||
self.library._check_boolean_extra_specs_validity(
|
||||
fake.EXTRA_SPEC_SHARE, fake.EMPTY_EXTRA_SPEC,
|
||||
list(self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP))
|
||||
|
||||
def test_check_boolean_extra_specs_validity_invalid_value(self):
|
||||
self.assertRaises(
|
||||
exception.Invalid,
|
||||
self.library._check_boolean_extra_specs_validity,
|
||||
fake.EXTRA_SPEC_SHARE, fake.INVALID_EXTRA_SPEC,
|
||||
list(self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP))
|
||||
|
||||
def test_get_boolean_provisioning_options(self):
|
||||
result = self.library._get_boolean_provisioning_options(
|
||||
fake.EXTRA_SPEC,
|
||||
self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP)
|
||||
|
||||
self.assertEqual(fake.PROVISIONING_OPTIONS, result)
|
||||
|
||||
def test_get_boolean_provisioning_options_missing_spec(self):
|
||||
result = self.library._get_boolean_provisioning_options(
|
||||
fake.SHORT_EXTRA_SPEC,
|
||||
self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP)
|
||||
|
||||
self.assertEqual(fake.PROVISIONING_OPTIONS, result)
|
||||
|
||||
def test_get_boolean_provisioning_options_implicit_false(self):
|
||||
result = self.library._get_boolean_provisioning_options(
|
||||
fake.EMPTY_EXTRA_SPEC,
|
||||
self.library.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP)
|
||||
|
||||
self.assertEqual({'thin_provisioned': False}, result)
|
||||
|
||||
def test_allocate_container_no_pool(self):
|
||||
|
||||
|
@ -557,7 +603,6 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
|||
self.assertRaises(exception.InvalidHost,
|
||||
self.library._allocate_container,
|
||||
fake_share,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
|
||||
def test_allocate_container_from_snapshot(self):
|
||||
|
@ -573,7 +618,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
|||
fake.SNAPSHOT['share_id'])
|
||||
parent_snapshot_name = self.library._get_valid_snapshot_name(
|
||||
fake.SNAPSHOT['id'])
|
||||
vserver_client.create_volume_clone.assert_called_with(
|
||||
vserver_client.create_volume_clone.assert_called_once_with(
|
||||
share_name,
|
||||
parent_share_name,
|
||||
parent_snapshot_name)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright (c) 2015 Clinton Knight All rights reserved.
|
||||
# Copyright (c) 2015 Tom Barron 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
|
||||
|
@ -12,6 +13,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
|
||||
import manila.tests.share.drivers.netapp.fakes as na_fakes
|
||||
|
||||
|
||||
|
@ -41,6 +44,8 @@ ROOT_VOLUME = 'root'
|
|||
CLUSTER_NODES = ('cluster1_01', 'cluster1_02')
|
||||
NODE_DATA_PORT = 'e0c'
|
||||
LIF_NAME_TEMPLATE = 'os_%(net_allocation_id)s'
|
||||
SHARE_TYPE_ID = '26e89a5b-960b-46bb-a8cf-0778e653098f'
|
||||
SHARE_TYPE_NAME = 'fake_share_type'
|
||||
|
||||
CLIENT_KWARGS = {
|
||||
'username': 'admin',
|
||||
|
@ -67,6 +72,34 @@ SHARE = {
|
|||
}
|
||||
}
|
||||
|
||||
EXTRA_SPEC = {
|
||||
'netapp:thin_provisioned': 'true',
|
||||
}
|
||||
|
||||
PROVISIONING_OPTIONS = {
|
||||
'thin_provisioned': True,
|
||||
}
|
||||
|
||||
SHORT_EXTRA_SPEC = {
|
||||
'netapp:thin_provisioned': 'true',
|
||||
}
|
||||
|
||||
INVALID_EXTRA_SPEC = {
|
||||
'netapp:thin_provisioned': 'ture',
|
||||
}
|
||||
|
||||
|
||||
EMPTY_EXTRA_SPEC = {}
|
||||
|
||||
SHARE_TYPE = {
|
||||
'id': SHARE_TYPE_ID,
|
||||
'name': SHARE_TYPE_NAME,
|
||||
'extra_specs': EXTRA_SPEC
|
||||
}
|
||||
|
||||
EXTRA_SPEC_SHARE = copy.deepcopy(SHARE)
|
||||
EXTRA_SPEC_SHARE['share_type_id'] = SHARE_TYPE_ID
|
||||
|
||||
NETWORK_INFO = {
|
||||
'server_id': '56aafd02-4d44-43d7-b784-57fc88167224',
|
||||
'cidr': '10.0.0.0/24',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright 2015 Deutsche Telekom AG
|
||||
# All Rights Reserved.
|
||||
# Copyright 2015 Deutsche Telekom AG. All rights reserved.
|
||||
# Copyright 2015 Tom Barron. 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
|
||||
|
@ -14,7 +14,7 @@
|
|||
# under the License.
|
||||
|
||||
|
||||
"""Test of Volume Test Manager for Manila."""
|
||||
"""Test of Share Type methods for Manila."""
|
||||
import datetime
|
||||
|
||||
import ddt
|
||||
|
@ -40,13 +40,15 @@ class ShareTypesTestCase(test.TestCase):
|
|||
'updated_at': None
|
||||
}
|
||||
}
|
||||
fake_extra_specs = {u'gold': u'True'}
|
||||
fake_share_type_id = u'fooid-2'
|
||||
fake_type_w_extra = {
|
||||
'test_with_extra': {
|
||||
'created_at': datetime.datetime(2015, 1, 22, 11, 45, 31),
|
||||
'deleted': '0',
|
||||
'deleted_at': None,
|
||||
'extra_specs': {u'gold': u'True'},
|
||||
'id': u'fooid-2',
|
||||
'extra_specs': fake_extra_specs,
|
||||
'id': fake_share_type_id,
|
||||
'name': u'test_with_extra',
|
||||
'updated_at': None
|
||||
}
|
||||
|
@ -54,6 +56,8 @@ class ShareTypesTestCase(test.TestCase):
|
|||
|
||||
fake_types = dict(fake_type.items() + fake_type_w_extra.items())
|
||||
|
||||
fake_share = {'id': u'fooid-1', 'share_type_id': fake_share_type_id}
|
||||
|
||||
def setUp(self):
|
||||
super(ShareTypesTestCase, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
|
@ -113,3 +117,14 @@ class ShareTypesTestCase(test.TestCase):
|
|||
share_type['id'],
|
||||
share_type['id'])
|
||||
self.assertTrue(equal)
|
||||
|
||||
def test_get_extra_specs_from_share(self):
|
||||
expected = self.fake_extra_specs
|
||||
self.mock_object(share_types, 'get_share_type_extra_specs',
|
||||
mock.Mock(return_value=expected))
|
||||
|
||||
spec_value = share_types.get_extra_specs_from_share(self.fake_share)
|
||||
|
||||
self.assertEqual(expected, spec_value)
|
||||
share_types.get_share_type_extra_specs.assert_called_once_with(
|
||||
self.fake_share_type_id)
|
||||
|
|
Loading…
Reference in New Issue