Merge "Implement create-delete-check for Manila::Share"
This commit is contained in:
commit
54e530c94f
@ -12,6 +12,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from heat.engine.clients import client_plugin
|
from heat.engine.clients import client_plugin
|
||||||
|
from heat.engine import constraints
|
||||||
from manilaclient import client as manila_client
|
from manilaclient import client as manila_client
|
||||||
from manilaclient import exceptions
|
from manilaclient import exceptions
|
||||||
|
|
||||||
@ -44,3 +45,84 @@ class ManilaClientPlugin(client_plugin.ClientPlugin):
|
|||||||
|
|
||||||
def is_conflict(self, ex):
|
def is_conflict(self, ex):
|
||||||
return isinstance(ex, exceptions.Conflict)
|
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'
|
||||||
|
305
heat/engine/resources/openstack/manila/share.py
Normal file
305
heat/engine/resources/openstack/manila/share.py
Normal 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}
|
@ -10,12 +10,43 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from manilaclient import exceptions
|
||||||
|
import mock
|
||||||
|
|
||||||
from heat.tests import common
|
from heat.tests import common
|
||||||
from heat.tests import utils
|
from heat.tests import utils
|
||||||
|
|
||||||
|
|
||||||
class ManilaClientPluginTests(common.HeatTestCase):
|
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):
|
def test_create(self):
|
||||||
context = utils.dummy_context()
|
context = utils.dummy_context()
|
||||||
@ -23,3 +54,14 @@ class ManilaClientPluginTests(common.HeatTestCase):
|
|||||||
client = plugin.client()
|
client = plugin.client()
|
||||||
self.assertIsNotNone(client.security_services)
|
self.assertIsNotNone(client.security_services)
|
||||||
self.assertEqual('http://server.test:5000/v3', client.client.base_url)
|
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")
|
||||||
|
156
heat/tests/test_manila_share.py
Normal file
156
heat/tests/test_manila_share.py
Normal 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))
|
@ -86,6 +86,9 @@ heat.constraints =
|
|||||||
keystone.project = heat.engine.clients.os.keystone:KeystoneProjectConstraint
|
keystone.project = heat.engine.clients.os.keystone:KeystoneProjectConstraint
|
||||||
keystone.group = heat.engine.clients.os.keystone:KeystoneGroupConstraint
|
keystone.group = heat.engine.clients.os.keystone:KeystoneGroupConstraint
|
||||||
keystone.service = heat.engine.clients.os.keystone:KeystoneServiceConstraint
|
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 =
|
heat.stack_lifecycle_plugins =
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user