Merge "Implement create-delete-check for Manila::Share"

This commit is contained in:
Jenkins 2015-06-23 10:58:01 +00:00 committed by Gerrit Code Review
commit 54e530c94f
5 changed files with 588 additions and 0 deletions

View File

@ -12,6 +12,7 @@
# under the License.
from heat.engine.clients import client_plugin
from heat.engine import constraints
from manilaclient import client as manila_client
from manilaclient import exceptions
@ -44,3 +45,84 @@ class ManilaClientPlugin(client_plugin.ClientPlugin):
def is_conflict(self, ex):
return isinstance(ex, exceptions.Conflict)
@staticmethod
def _find_resource_by_id_or_name(id_or_name, resource_list,
resource_type_name):
"""The method is trying to find id or name in item_list
The method searches item with id_or_name in list and returns it.
If there is more than one value or no values then it raises an
exception
:param id_or_name: resource id or name
:param resource_list: list of resources
:param resource_type_name: name of resource type that will be used
for exceptions
:raises NotFound, NoUniqueMatch
:return: resource or generate an exception otherwise
"""
search_result_by_id = [res for res in resource_list
if res.id == id_or_name]
if search_result_by_id:
return search_result_by_id[0]
else:
# try to find resource by name
search_result_by_name = [res for res in resource_list
if res.name == id_or_name]
match_count = len(search_result_by_name)
if match_count > 1:
message = ("Ambiguous {0} name '{1}'. Found more than one "
"{0} for this name in Manila."
).format(resource_type_name, id_or_name)
raise exceptions.NoUniqueMatch(message)
elif match_count == 1:
return search_result_by_name[0]
else:
message = ("{0} '{1}' was not found in Manila. Please "
"use the identity of existing {0} in Heat "
"template.").format(resource_type_name, id_or_name)
raise exceptions.NotFound(message=message)
def get_share_type(self, share_type_identity):
return self._find_resource_by_id_or_name(
share_type_identity,
self.client().share_types.list(),
"share type"
)
def get_share_network(self, share_network_identity):
return self._find_resource_by_id_or_name(
share_network_identity,
self.client().share_networks.list(),
"share network"
)
def get_share_snapshot(self, snapshot_identity):
return self._find_resource_by_id_or_name(
snapshot_identity,
self.client().share_snapshots.list(),
"share snapshot"
)
class ManilaShareBaseConstraint(constraints.BaseCustomConstraint):
# check that exceptions module has been loaded. Without this check
# doc tests on gates will fail
expected_exceptions = (exceptions.NotFound, exceptions.NoUniqueMatch)
def validate_with_client(self, client, resource_id):
getattr(client.client_plugin("manila"), self.resource_getter_name)(
resource_id)
class ManilaShareNetworkConstraint(ManilaShareBaseConstraint):
resource_getter_name = 'get_share_network'
class ManilaShareTypeConstraint(ManilaShareBaseConstraint):
resource_getter_name = 'get_share_type'
class ManilaShareSnapshotConstraint(ManilaShareBaseConstraint):
resource_getter_name = 'get_share_snapshot'

View File

@ -0,0 +1,305 @@
#
# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
import six
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
LOG = logging.getLogger(__name__)
class ManilaShare(resource.Resource):
"""A resource that creates shared mountable file system.
The resource creates a manila share - shared mountable filesystem that
can be attached to any client(or clients) that has a network access and
permission to mount filesystem. Share is a unit of storage with specific
size that supports pre-defined share protocol and advanced security model
(access lists, share networks and security services).
"""
support_status = support.SupportStatus(version='5.0.0')
_ACCESS_RULE_PROPERTIES = (
ACCESS_TO, ACCESS_TYPE, ACCESS_LEVEL
) = (
'access_to', 'access_type', 'access_level')
_SHARE_STATUSES = (
STATUS_CREATING, STATUS_DELETING, STATUS_ERROR, STATUS_ERROR_DELETING,
STATUS_AVAILABLE
) = (
'creating', 'deleting', 'error', 'error_deleting',
'available'
)
PROPERTIES = (
SHARE_PROTOCOL, SIZE, SHARE_SNAPSHOT, NAME, METADATA,
SHARE_NETWORK, DESCRIPTION, SHARE_TYPE, IS_PUBLIC,
ACCESS_RULES
) = (
'share_protocol', 'size', 'snapshot', 'name', 'metadata',
'share_network', 'description', 'share_type', 'is_public',
'access_rules'
)
ATTRIBUTES = (
AVAILABILITY_ZONE_ATTR, HOST_ATTR, EXPORT_LOCATIONS_ATTR,
SHARE_SERVER_ID_ATTR, CREATED_AT_ATTR, SHARE_STATUS_ATTR,
PROJECT_ID_ATTR
) = (
'availability_zone', 'host', 'export_locations',
'share_server_id', 'created_at', 'status',
'project_id'
)
properties_schema = {
SHARE_PROTOCOL: properties.Schema(
properties.Schema.STRING,
_('Share protocol supported by shared filesystem.'),
required=True,
constraints=[constraints.AllowedValues(
['NFS', 'CIFS', 'GlusterFS', 'HDFS'])]
),
SIZE: properties.Schema(
properties.Schema.INTEGER,
_('Share storage size in GB.'),
required=True
),
SHARE_SNAPSHOT: properties.Schema(
properties.Schema.STRING,
_('Name or ID of shared file system snapshot that will be restored'
' and created as a new share.'),
constraints=[constraints.CustomConstraint('manila.share_snapshot')]
),
NAME: properties.Schema(
properties.Schema.STRING,
_('Share name.'),
update_allowed=True
),
METADATA: properties.Schema(
properties.Schema.MAP,
_('Metadata key-values defined for share.'),
update_allowed=True
),
SHARE_NETWORK: properties.Schema(
properties.Schema.STRING,
_('Name or ID of shared network defined for shared filesystem.'),
constraints=[constraints.CustomConstraint('manila.share_network')]
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Share description.'),
update_allowed=True
),
SHARE_TYPE: properties.Schema(
properties.Schema.STRING,
_('Name or ID of shared filesystem type. Types defines some share '
'filesystem profiles that will be used for share creation.'),
constraints=[constraints.CustomConstraint("manila.share_type")]
),
IS_PUBLIC: properties.Schema(
properties.Schema.BOOLEAN,
_('Defines if shared filesystem is public or private.'),
default=False,
update_allowed=True
),
ACCESS_RULES: properties.Schema(
properties.Schema.LIST,
_('A list of access rules that define access from IP to Share.'),
schema=properties.Schema(
properties.Schema.MAP,
schema={
ACCESS_TO: properties.Schema(
properties.Schema.STRING,
_('IP or other address information about guest that '
'allowed to access to Share.'),
required=True
),
ACCESS_TYPE: properties.Schema(
properties.Schema.STRING,
_('Type of access that should be provided to guest.'),
constraints=[constraints.AllowedValues(
['ip', 'domain'])],
required=True
),
ACCESS_LEVEL: properties.Schema(
properties.Schema.STRING,
_('Level of access that need to be provided for '
'guest.'),
constraints=[constraints.AllowedValues(['ro', 'rw'])]
)
}
),
update_allowed=True,
default=[]
)
}
attributes_schema = {
AVAILABILITY_ZONE_ATTR: attributes.Schema(
_('The availability zone of shared filesystem.'),
type=attributes.Schema.STRING
),
HOST_ATTR: attributes.Schema(
_('Share host.'),
type=attributes.Schema.STRING
),
EXPORT_LOCATIONS_ATTR: attributes.Schema(
_('Export locations of share.'),
type=attributes.Schema.LIST
),
SHARE_SERVER_ID_ATTR: attributes.Schema(
_('ID of server (VM, etc...) on host that is used for '
'exporting network file-system.'),
type=attributes.Schema.STRING
),
CREATED_AT_ATTR: attributes.Schema(
_('Datetime when a share was created.'),
type=attributes.Schema.STRING
),
SHARE_STATUS_ATTR: attributes.Schema(
_('Current share status.'),
type=attributes.Schema.STRING
),
PROJECT_ID_ATTR: attributes.Schema(
_('Share project ID.'),
type=attributes.Schema.STRING
)
}
default_client_name = 'manila'
def _request_share(self):
return self.client().shares.get(self.resource_id)
def _resolve_attribute(self, name):
share = self._request_share()
return six.text_type(getattr(share, name))
def handle_create(self):
# Request IDs of entities from manila
# if name of the entity defined in template
share_net_identity = self.properties[self.SHARE_NETWORK]
if share_net_identity:
share_net_identity = self.client_plugin().get_share_network(
share_net_identity).id
snapshot_identity = self.properties[self.SHARE_SNAPSHOT]
if snapshot_identity:
snapshot_identity = self.client_plugin().get_share_snapshot(
snapshot_identity).id
share_type_identity = self.properties[self.SHARE_TYPE]
if share_type_identity:
share_type_identity = self.client_plugin().get_share_type(
share_type_identity).id
share = self.client().shares.create(
share_proto=self.properties[self.SHARE_PROTOCOL],
size=self.properties[self.SIZE],
snapshot_id=snapshot_identity,
name=self.properties[self.NAME],
description=self.properties[self.DESCRIPTION],
metadata=self.properties[self.METADATA],
share_network=share_net_identity,
share_type=share_type_identity,
is_public=self.properties[self.IS_PUBLIC])
self.resource_id_set(share.id)
def check_create_complete(self, *args):
share_status = self._request_share().status
if share_status == self.STATUS_CREATING:
return False
elif share_status == self.STATUS_AVAILABLE:
LOG.info(_LI('Applying access rules to created Share.'))
# apply access rules to created share. please note that it is not
# possible to define rules for share with share_status = creating
access_rules = self.properties.get(self.ACCESS_RULES)
try:
if access_rules:
for rule in access_rules:
self.client().shares.allow(
share=self.resource_id,
access_type=rule.get(self.ACCESS_TYPE),
access=rule.get(self.ACCESS_TO),
access_level=rule.get(self.ACCESS_LEVEL))
return True
except Exception as ex:
reason = _(
'Error during applying access rules to share "{0}". '
'The root cause of the problem is the following: {1}.'
).format(self.resource_id, ex.message)
raise resource.ResourceInError(status_reason=reason)
elif share_status == self.STATUS_ERROR:
reason = _('Error during creation of share "{0}"').format(
self.resource_id)
raise resource.ResourceInError(status_reason=reason,
resource_status=share_status)
else:
reason = _('Unknown share_status during creation of share "{0}"'
).format(self.resource_id)
raise resource.ResourceUnknownStatus(status_reason=reason,
resource_status=share_status)
def handle_delete(self):
if not self.resource_id:
return
try:
self.client().shares.delete(self.resource_id)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
def check_delete_complete(self, *args):
if not self.resource_id:
return True
try:
share = self._request_share()
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return True
else:
# when share creation is not finished proceed listening
if share.status == self.STATUS_DELETING:
return False
elif share.status in (self.STATUS_ERROR,
self.STATUS_ERROR_DELETING):
raise resource.ResourceInError(
status_reason=_(
'Error during deleting share "{0}".'
).format(self.resource_id),
resource_status=share.status)
else:
reason = _('Unknown status during deleting share '
'"{0}"').format(self.resource_id)
raise resource.ResourceUnknownStatus(
status_reason=reason, resource_status=share.status)
def handle_check(self):
share = self._request_share()
expected_statuses = [self.STATUS_AVAILABLE]
checks = [{'attr': 'status', 'expected': expected_statuses,
'current': share.status}]
self._verify_check_conditions(checks)
def resource_mapping():
return {'OS::Manila::Share': ManilaShare}

View File

@ -10,12 +10,43 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
from manilaclient import exceptions
import mock
from heat.tests import common
from heat.tests import utils
class ManilaClientPluginTests(common.HeatTestCase):
scenarios = [
('share_type',
dict(manager_name="share_types",
method_name="get_share_type")),
('share_network',
dict(manager_name="share_networks",
method_name="get_share_network")),
('share_snapshot',
dict(manager_name="share_snapshots",
method_name="get_share_snapshot")),
]
def setUp(self):
super(ManilaClientPluginTests, self).setUp()
# mock client and plugin
self.manila_client = mock.MagicMock()
con = utils.dummy_context()
c = con.clients
self.manila_plugin = c.client_plugin('manila')
self.manila_plugin._client = self.manila_client
# prepare list of items to test search
Item = collections.namedtuple('Item', ['id', 'name'])
self.item_list = [
Item(name="unique_name", id="unique_id"),
Item(name="unique_id", id="i_am_checking_that_id_prior"),
Item(name="duplicated_name", id="duplicate_test_one"),
Item(name="duplicated_name", id="duplicate_test_second")]
def test_create(self):
context = utils.dummy_context()
@ -23,3 +54,14 @@ class ManilaClientPluginTests(common.HeatTestCase):
client = plugin.client()
self.assertIsNotNone(client.security_services)
self.assertEqual('http://server.test:5000/v3', client.client.base_url)
def test_manila_get_method(self):
# set item list as client output
manager = getattr(self.manila_client, self.manager_name)
manager.list.return_value = self.item_list
# test that get_<method_name> is searching correctly
get_method = getattr(self.manila_plugin, self.method_name)
self.assertEqual(get_method("unique_id").name, "unique_name")
self.assertRaises(exceptions.NotFound, get_method, "non_exist")
self.assertRaises(exceptions.NoUniqueMatch, get_method,
"duplicated_name")

View File

@ -0,0 +1,156 @@
#
# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
import six
from heat.common import exception
from heat.common import template_format
from heat.engine.resources.openstack.manila import share as mshare
from heat.engine import scheduler
from heat.tests import common
from heat.tests import utils
manila_template = """
heat_template_version: 2015-04-30
resources:
test_share:
type: OS::Manila::Share
properties:
share_protocol: NFS
size: 1
access_rules:
- access_to: 127.0.0.1
access_type: ip
access_level: ro
name: basic_test_share
description: basic test share
is_public: True
metadata: {"key": "value"}
"""
class ManilaShareTest(common.HeatTestCase):
def setUp(self):
super(ManilaShareTest, self).setUp()
utils.setup_dummy_db()
self.ctx = utils.dummy_context()
self.fake_share = mock.MagicMock(id="test_share_id")
self.available_share = mock.MagicMock(
id="test_share_id",
status=mshare.ManilaShare.STATUS_AVAILABLE)
self.failed_share = mock.MagicMock(
id="test_share_id",
status=mshare.ManilaShare.STATUS_ERROR)
self.deleting_share = mock.MagicMock(
id="test_share_id",
status=mshare.ManilaShare.STATUS_DELETING)
def _init_share(self, stack_name):
tmp = template_format.parse(manila_template)
self.stack = utils.parse_stack(tmp, stack_name=stack_name)
res_def = self.stack.t.resource_definitions(self.stack)["test_share"]
share = mshare.ManilaShare("test_share", res_def, self.stack)
# replace clients and plugins with mocks
mock_client = mock.MagicMock()
client = mock.MagicMock(return_value=mock_client)
share.client = client
mock_plugin = mock.MagicMock()
client_plugin = mock.MagicMock(return_value=mock_plugin)
share.client_plugin = client_plugin
return share
def _create_share(self, stack_name):
share = self._init_share(stack_name)
share.client().shares.create.return_value = self.fake_share
share.client().shares.get.return_value = self.available_share
scheduler.TaskRunner(share.create)()
return share
def test_share_create(self):
share = self._create_share("stack_share_create")
expected_state = (share.CREATE, share.COMPLETE)
self.assertEqual(expected_state, share.state,
"Share is not in expected state")
self.assertEqual(self.fake_share.id, share.resource_id,
"Expected share ID was not propagated to share")
share.client().shares.allow.assert_called_once_with(
access="127.0.0.1", access_level="ro",
share=share.resource_id, access_type="ip")
args, kwargs = share.client().shares.create.call_args
message_end = " parameter was not passed to manila client"
self.assertEqual(u"NFS", kwargs["share_proto"],
"Share protocol" + message_end)
self.assertEqual(1, kwargs["size"], "Share size" + message_end)
self.assertEqual("basic_test_share", kwargs["name"],
"Share name" + message_end)
self.assertEqual("basic test share", kwargs["description"],
"Share description" + message_end)
self.assertEqual({u"key": u"value"}, kwargs["metadata"],
"Metadata" + message_end)
self.assertTrue(kwargs["is_public"])
share.client().shares.get.assert_called_once_with(self.fake_share.id)
def test_share_create_fail(self):
share = self._init_share("stack_share_create_fail")
share.client().shares.create.return_value = self.fake_share
share.client().shares.get.return_value = self.failed_share
exc = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(share.create))
self.assertIn("Error during creation", six.text_type(exc))
def test_share_create_unknown_status(self):
share = self._init_share("stack_share_create_unknown")
share.client().shares.create.return_value = self.fake_share
share.client().shares.get.return_value = self.deleting_share
exc = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(share.create))
self.assertIn("Unknown status", six.text_type(exc))
def test_share_delete(self):
share = self._create_share("stack_share_delete")
share.client().shares.get.side_effect = exception.NotFound()
share.client_plugin().ignore_not_found.return_value = None
scheduler.TaskRunner(share.delete)()
share.client().shares.delete.assert_called_once_with(
self.fake_share.id)
def test_share_delete_fail(self):
share = self._create_share("stack_share_delete_fail")
share.client().shares.delete.return_value = None
share.client().shares.get.return_value = self.failed_share
exc = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(share.delete))
self.assertIn("Error during deleting share", six.text_type(exc))
def test_share_check(self):
share = self._create_share("stack_share_check")
scheduler.TaskRunner(share.check)()
expected_state = (share.CHECK, share.COMPLETE)
self.assertEqual(expected_state, share.state,
"Share is not in expected state")
def test_share_check_fail(self):
share = self._create_share("stack_share_check_fail")
share.client().shares.get.return_value = self.failed_share
exc = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(share.check))
self.assertIn("Error: 'status': expected '['available']'",
six.text_type(exc))

View File

@ -86,6 +86,9 @@ heat.constraints =
keystone.project = heat.engine.clients.os.keystone:KeystoneProjectConstraint
keystone.group = heat.engine.clients.os.keystone:KeystoneGroupConstraint
keystone.service = heat.engine.clients.os.keystone:KeystoneServiceConstraint
manila.share_snapshot = heat.engine.clients.os.manila:ManilaShareSnapshotConstraint
manila.share_network = heat.engine.clients.os.manila:ManilaShareNetworkConstraint
manila.share_type = heat.engine.clients.os.manila:ManilaShareTypeConstraint
heat.stack_lifecycle_plugins =