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:
Tom Barron 2015-02-09 19:07:01 -08:00
parent 2a7523cac7
commit 967fef055b
7 changed files with 214 additions and 32 deletions

View File

@ -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

View File

@ -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
"""

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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',

View File

@ -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)