diff --git a/heat/engine/clients/os/sahara.py b/heat/engine/clients/os/sahara.py index d79f14920f..1db4d9340d 100644 --- a/heat/engine/clients/os/sahara.py +++ b/heat/engine/clients/os/sahara.py @@ -13,10 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging + +from oslo_utils import uuidutils from saharaclient.api import base as sahara_base from saharaclient import client as sahara_client +import six +from heat.common import exception +from heat.common.i18n import _ +from heat.common.i18n import _LI from heat.engine.clients import client_plugin +from heat.engine import constraints + +LOG = logging.getLogger(__name__) class SaharaClientPlugin(client_plugin.ClientPlugin): @@ -49,3 +59,65 @@ class SaharaClientPlugin(client_plugin.ClientPlugin): def is_conflict(self, ex): return (isinstance(ex, sahara_base.APIException) and ex.error_code == 409) + + def is_not_registered(self, ex): + return (isinstance(ex, sahara_base.APIException) and + ex.error_code == 400 and + ex.error_name == 'IMAGE_NOT_REGISTERED') + + def get_image_id(self, image_identifier): + ''' + Return an id for the specified image name or identifier. + + :param image_identifier: image name or a UUID-like identifier + :returns: the id of the requested :image_identifier: + :raises: exception.ImageNotFound, + exception.PhysicalResourceNameAmbiguity + ''' + if uuidutils.is_uuid_like(image_identifier): + try: + image_id = self.client().images.get(image_identifier).id + except sahara_base.APIException as ex: + if self.is_not_registered(ex): + image_id = self.get_image_id_by_name(image_identifier) + else: + image_id = self.get_image_id_by_name(image_identifier) + return image_id + + def get_image_id_by_name(self, image_identifier): + ''' + Return an id for the specified image name. + + :param image_identifier: image name + :returns: the id of the requested :image_identifier: + :raises: exception.ImageNotFound, + exception.PhysicalResourceNameAmbiguity + ''' + try: + filters = {'name': image_identifier} + image_list = self.client().images.find(**filters) + except sahara_base.APIException as ex: + raise exception.Error( + _("Error retrieving image list from sahara: " + "%s") % six.text_type(ex)) + num_matches = len(image_list) + if num_matches == 0: + LOG.info(_LI("Image %s was not found in sahara images"), + image_identifier) + raise exception.ImageNotFound(image_name=image_identifier) + elif num_matches > 1: + LOG.info(_LI("Multiple images %s were found in sahara with name"), + image_identifier) + raise exception.PhysicalResourceNameAmbiguity( + name=image_identifier) + else: + return image_list[0].id + + +class ImageConstraint(constraints.BaseCustomConstraint): + + expected_exceptions = (exception.ImageNotFound, + exception.PhysicalResourceNameAmbiguity,) + + def validate_with_client(self, client, value): + client.client_plugin('sahara').get_image_id(value) diff --git a/heat/engine/resources/sahara_cluster.py b/heat/engine/resources/sahara_cluster.py index 3390ce8edb..6b88f79117 100644 --- a/heat/engine/resources/sahara_cluster.py +++ b/heat/engine/resources/sahara_cluster.py @@ -91,7 +91,7 @@ class SaharaCluster(resource.Resource): properties.Schema.STRING, _('Default name or UUID of the image used to boot Hadoop nodes.'), constraints=[ - constraints.CustomConstraint('glance.image') + constraints.CustomConstraint('sahara.image'), ], ), MANAGEMENT_NETWORK: properties.Schema( diff --git a/heat/engine/resources/sahara_templates.py b/heat/engine/resources/sahara_templates.py index b6542a0bcb..88825433de 100644 --- a/heat/engine/resources/sahara_templates.py +++ b/heat/engine/resources/sahara_templates.py @@ -283,7 +283,7 @@ class SaharaClusterTemplate(resource.Resource): properties.Schema.STRING, _("ID of the default image to use for the template."), constraints=[ - constraints.CustomConstraint('glance.image') + constraints.CustomConstraint('sahara.image'), ], ), MANAGEMENT_NETWORK: properties.Schema( diff --git a/heat/tests/test_sahara_client.py b/heat/tests/test_sahara_client.py new file mode 100644 index 0000000000..a95ba44bd1 --- /dev/null +++ b/heat/tests/test_sahara_client.py @@ -0,0 +1,135 @@ +# +# 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 uuid + +import mock +from saharaclient.api import base as sahara_base +import six + +from heat.common import exception +from heat.engine.clients.os import sahara +from heat.tests import common +from heat.tests import utils + + +class SaharaUtilsTests(common.HeatTestCase): + """ + Basic tests for the helper methods in + :module:'heat.engine.resources.clients.os.sahara'. + """ + + def setUp(self): + super(SaharaUtilsTests, self).setUp() + self.sahara_client = mock.MagicMock() + con = utils.dummy_context() + c = con.clients + self.sahara_plugin = c.client_plugin('sahara') + self.sahara_plugin._client = self.sahara_client + self.my_image = mock.MagicMock() + + def test_get_image_id(self): + """Tests the get_image_id function.""" + img_id = str(uuid.uuid4()) + img_name = 'myfakeimage' + self.my_image.id = img_id + self.my_image.name = img_name + self.sahara_client.images.get.return_value = self.my_image + self.sahara_client.images.find.side_effect = [[self.my_image], []] + + self.assertEqual(img_id, self.sahara_plugin.get_image_id(img_id)) + self.assertEqual(img_id, self.sahara_plugin.get_image_id(img_name)) + self.assertRaises(exception.ImageNotFound, + self.sahara_plugin.get_image_id, 'noimage') + + calls = [mock.call(name=img_name), + mock.call(name='noimage')] + self.sahara_client.images.get.assert_called_once_with(img_id) + self.sahara_client.images.find.assert_has_calls(calls) + + def test_get_image_id_by_name_in_uuid(self): + """Tests the get_image_id function by name in uuid.""" + img_id = str(uuid.uuid4()) + img_name = str(uuid.uuid4()) + self.my_image.id = img_id + self.my_image.name = img_name + self.sahara_client.images.get.side_effect = [ + sahara_base.APIException(error_code=400, + error_name='IMAGE_NOT_REGISTERED')] + + self.sahara_client.images.find.return_value = [self.my_image] + self.assertEqual(img_id, self.sahara_plugin.get_image_id(img_name)) + + self.sahara_client.images.get.assert_called_once_with(img_name) + self.sahara_client.images.find.assert_called_once_with(name=img_name) + + def test_get_image_id_sahara_exception(self): + """Test get_image_id when sahara raises an exception.""" + # Simulate HTTP exception + img_name = str(uuid.uuid4()) + self.sahara_client.images.find.side_effect = [ + sahara_base.APIException(error_message="Error", error_code=404)] + + expected_error = "Error retrieving image list from sahara: Error" + e = self.assertRaises(exception.Error, + self.sahara_plugin.get_image_id_by_name, + img_name) + self.assertEqual(expected_error, six.text_type(e)) + + self.sahara_client.images.find.assert_called_once_with(name=img_name) + + def test_get_image_id_not_found(self): + """Tests the get_image_id function while image is not found.""" + img_name = str(uuid.uuid4()) + self.my_image.name = img_name + self.sahara_client.images.get.side_effect = [ + sahara_base.APIException(error_code=400, + error_name='IMAGE_NOT_REGISTERED')] + self.sahara_client.images.find.return_value = [] + + self.assertRaises(exception.ImageNotFound, + self.sahara_plugin.get_image_id, img_name) + + self.sahara_client.images.get.assert_called_once_with(img_name) + self.sahara_client.images.find.assert_called_once_with(name=img_name) + + def test_get_image_id_name_ambiguity(self): + """Tests the get_image_id function while name ambiguity .""" + img_name = 'ambiguity_name' + self.my_image.name = img_name + + self.sahara_client.images.find.return_value = [self.my_image, + self.my_image] + self.assertRaises(exception.PhysicalResourceNameAmbiguity, + self.sahara_plugin.get_image_id, img_name) + self.sahara_client.images.find.assert_called_once_with(name=img_name) + + +class ImageConstraintTest(common.HeatTestCase): + + def setUp(self): + super(ImageConstraintTest, self).setUp() + self.ctx = utils.dummy_context() + self.mock_get_image = mock.Mock() + self.ctx.clients.client_plugin( + 'sahara').get_image_id = self.mock_get_image + self.constraint = sahara.ImageConstraint() + + def test_validation(self): + self.mock_get_image.return_value = "id1" + self.assertTrue(self.constraint.validate("foo", self.ctx)) + + def test_validation_error(self): + self.mock_get_image.side_effect = exception.ImageNotFound( + image_name='bar') + self.assertFalse(self.constraint.validate("bar", self.ctx)) diff --git a/setup.cfg b/setup.cfg index 1e06ceca92..a879243c3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,6 +70,7 @@ heat.constraints = cinder.volume = heat.engine.clients.os.cinder:VolumeConstraint cinder.snapshot = heat.engine.clients.os.cinder:VolumeSnapshotConstraint cinder.vtype = heat.engine.clients.os.cinder:VolumeTypeConstraint + sahara.image = heat.engine.clients.os.sahara:ImageConstraint heat.stack_lifecycle_plugins =