Add sahara image constraint

Sahara uses images with specific tags. These images should be registered
in Sahara, otherwise you will get error during creation.
This patch adds constraint for sahara images.

Change-Id: Idce140ca65cd396cd1febcc052898717d378f88d
This commit is contained in:
Sergey Kraynev 2015-01-23 20:17:25 -05:00
parent 5ded08aa19
commit 8ac934471a
5 changed files with 210 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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