summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--heat/common/exception.py2
-rw-r--r--heat/common/heat_keystoneclient.py26
-rw-r--r--heat/db/sqlalchemy/models.py41
-rw-r--r--heat/engine/resources/nova_floatingip.py3
-rw-r--r--heat/engine/resources/nova_utils.py3
-rw-r--r--heat/engine/resources/server.py41
-rw-r--r--heat/engine/resources/wait_condition.py9
-rw-r--r--heat/tests/test_api_openstack_v1.py111
-rw-r--r--heat/tests/test_heatclient.py31
-rw-r--r--heat/tests/test_nova_floatingip.py23
-rw-r--r--heat/tests/test_server.py9
-rw-r--r--heat/tests/test_sqlalchemy_api.py16
-rw-r--r--requirements.txt2
13 files changed, 262 insertions, 55 deletions
diff --git a/heat/common/exception.py b/heat/common/exception.py
index 7bc204421..0879b3020 100644
--- a/heat/common/exception.py
+++ b/heat/common/exception.py
@@ -317,7 +317,7 @@ class Error(HeatException):
class NotFound(HeatException):
- def __init__(self, msg_fmt):
+ def __init__(self, msg_fmt=_('Not found')):
self.msg_fmt = msg_fmt
super(NotFound, self).__init__()
diff --git a/heat/common/heat_keystoneclient.py b/heat/common/heat_keystoneclient.py
index b4d16e144..f50746ac6 100644
--- a/heat/common/heat_keystoneclient.py
+++ b/heat/common/heat_keystoneclient.py
@@ -362,11 +362,18 @@ class KeystoneClientV3(object):
logger.warning(_('Falling back to legacy non-domain user delete, '
'configure domain in heat.conf'))
return self.delete_stack_user(user_id)
- self._check_stack_domain_user(user_id, project_id, 'delete')
- self.domain_admin_client.users.delete(user_id)
+
+ try:
+ self._check_stack_domain_user(user_id, project_id, 'delete')
+ self.domain_admin_client.users.delete(user_id)
+ except kc_exception.NotFound:
+ pass
def delete_stack_user(self, user_id):
- self.client.users.delete(user=user_id)
+ try:
+ self.client.users.delete(user=user_id)
+ except kc_exception.NotFound:
+ pass
def create_stack_domain_project(self, stack_id):
'''Creates a project in the heat stack-user domain.'''
@@ -393,7 +400,10 @@ class KeystoneClientV3(object):
logger.warning(_('Falling back to legacy non-domain project, '
'configure domain in heat.conf'))
return
- self.domain_admin_client.projects.delete(project=project_id)
+ try:
+ self.domain_admin_client.projects.delete(project=project_id)
+ except kc_exception.NotFound:
+ pass
def _find_ec2_keypair(self, access, user_id=None):
'''Lookup an ec2 keypair by access ID.'''
@@ -411,10 +421,14 @@ class KeystoneClientV3(object):
user_id=None):
'''Delete credential containing ec2 keypair.'''
if credential_id:
- self.client.credentials.delete(credential_id)
+ try:
+ self.client.credentials.delete(credential_id)
+ except kc_exception.NotFound:
+ pass
elif access:
cred = self._find_ec2_keypair(access=access, user_id=user_id)
- self.client.credentials.delete(cred.id)
+ if cred:
+ self.client.credentials.delete(cred.id)
else:
raise ValueError("Must specify either credential_id or access")
diff --git a/heat/db/sqlalchemy/models.py b/heat/db/sqlalchemy/models.py
index cf9d5ea77..5fe3b225d 100644
--- a/heat/db/sqlalchemy/models.py
+++ b/heat/db/sqlalchemy/models.py
@@ -79,6 +79,21 @@ class SoftDelete(object):
session=session)
+class StateAware(object):
+
+ action = sqlalchemy.Column('action', sqlalchemy.String(255))
+ status = sqlalchemy.Column('status', sqlalchemy.String(255))
+ _status_reason = sqlalchemy.Column('status_reason', sqlalchemy.String(255))
+
+ @property
+ def status_reason(self):
+ return self._status_reason
+
+ @status_reason.setter
+ def status_reason(self, reason):
+ self._status_reason = reason and reason[:255] or ''
+
+
class RawTemplate(BASE, HeatBase):
"""Represents an unparsed template which should be in JSON format."""
@@ -88,7 +103,7 @@ class RawTemplate(BASE, HeatBase):
files = sqlalchemy.Column(Json)
-class Stack(BASE, HeatBase, SoftDelete):
+class Stack(BASE, HeatBase, SoftDelete, StateAware):
"""Represents a stack created by the heat engine."""
__tablename__ = 'stack'
@@ -103,9 +118,6 @@ class Stack(BASE, HeatBase, SoftDelete):
raw_template = relationship(RawTemplate, backref=backref('stack'))
username = sqlalchemy.Column(sqlalchemy.String(256))
tenant = sqlalchemy.Column(sqlalchemy.String(256))
- action = sqlalchemy.Column('action', sqlalchemy.String(255))
- status = sqlalchemy.Column('status', sqlalchemy.String(255))
- status_reason = sqlalchemy.Column('status_reason', sqlalchemy.String(255))
parameters = sqlalchemy.Column('parameters', Json)
user_creds_id = sqlalchemy.Column(
sqlalchemy.Integer,
@@ -172,10 +184,19 @@ class Event(BASE, HeatBase):
resource_status = sqlalchemy.Column(sqlalchemy.String(255))
resource_name = sqlalchemy.Column(sqlalchemy.String(255))
physical_resource_id = sqlalchemy.Column(sqlalchemy.String(255))
- resource_status_reason = sqlalchemy.Column(sqlalchemy.String(255))
+ _resource_status_reason = sqlalchemy.Column(
+ 'resource_status_reason', sqlalchemy.String(255))
resource_type = sqlalchemy.Column(sqlalchemy.String(255))
resource_properties = sqlalchemy.Column(sqlalchemy.PickleType)
+ @property
+ def resource_status_reason(self):
+ return self._resource_status_reason
+
+ @resource_status_reason.setter
+ def resource_status_reason(self, reason):
+ self._resource_status_reason = reason and reason[:255] or ''
+
class ResourceData(BASE, HeatBase):
"""Key/value store of arbitrary, resource-specific data."""
@@ -196,7 +217,7 @@ class ResourceData(BASE, HeatBase):
nullable=False)
-class Resource(BASE, HeatBase):
+class Resource(BASE, HeatBase, StateAware):
"""Represents a resource created by the heat engine."""
__tablename__ = 'resource'
@@ -204,11 +225,8 @@ class Resource(BASE, HeatBase):
id = sqlalchemy.Column(sqlalchemy.String(36),
primary_key=True,
default=lambda: str(uuid.uuid4()))
- action = sqlalchemy.Column('action', sqlalchemy.String(255))
- status = sqlalchemy.Column('status', sqlalchemy.String(255))
name = sqlalchemy.Column('name', sqlalchemy.String(255), nullable=True)
nova_instance = sqlalchemy.Column('nova_instance', sqlalchemy.String(255))
- status_reason = sqlalchemy.Column('status_reason', sqlalchemy.String(255))
# odd name as "metadata" is reserved
rsrc_metadata = sqlalchemy.Column('rsrc_metadata', Json)
@@ -277,7 +295,7 @@ class SoftwareConfig(BASE, HeatBase):
'tenant', sqlalchemy.String(256), nullable=False)
-class SoftwareDeployment(BASE, HeatBase):
+class SoftwareDeployment(BASE, HeatBase, StateAware):
"""
Represents applying a software configuration resource to a
single server resource.
@@ -301,6 +319,3 @@ class SoftwareDeployment(BASE, HeatBase):
'tenant', sqlalchemy.String(256), nullable=False)
stack_user_project_id = sqlalchemy.Column(sqlalchemy.String(64),
nullable=True)
- action = sqlalchemy.Column('action', sqlalchemy.String(255))
- status = sqlalchemy.Column('status', sqlalchemy.String(255))
- status_reason = sqlalchemy.Column('status_reason', sqlalchemy.String(255))
diff --git a/heat/engine/resources/nova_floatingip.py b/heat/engine/resources/nova_floatingip.py
index 21ab5e446..85f859775 100644
--- a/heat/engine/resources/nova_floatingip.py
+++ b/heat/engine/resources/nova_floatingip.py
@@ -107,6 +107,9 @@ class NovaFloatingIpAssociation(resource.Resource):
self.resource_id_set('%s-%s' % (fl_ip.id, fl_ip.ip))
def handle_delete(self):
+ if self.resource_id is None:
+ return
+
try:
server = self.nova().servers.get(self.properties[self.SERVER])
if server:
diff --git a/heat/engine/resources/nova_utils.py b/heat/engine/resources/nova_utils.py
index d011d0db2..b197ba1e1 100644
--- a/heat/engine/resources/nova_utils.py
+++ b/heat/engine/resources/nova_utils.py
@@ -60,7 +60,8 @@ def refresh_server(server):
'id': server.id,
'exception': str(exc)})
except clients.novaclient.exceptions.ClientException as exc:
- if exc.code in (500, 503):
+ if ((getattr(exc, 'http_status', getattr(exc, 'code', None)) in
+ (500, 503))):
msg = _('Server "%(name)s" (%(id)s) received the following '
'exception during server.get(): %(exception)s')
logger.warning(msg % {'name': server.name,
diff --git a/heat/engine/resources/server.py b/heat/engine/resources/server.py
index 5c36e5c94..4f5f45ced 100644
--- a/heat/engine/resources/server.py
+++ b/heat/engine/resources/server.py
@@ -856,6 +856,15 @@ class Server(stack_user.StackUser):
if new_metadata is None:
self.metadata = self.parsed_template('Metadata')
+ @staticmethod
+ def _check_maximum(count, maximum, msg):
+ '''
+ Check a count against a maximum, unless maximum is -1 which indicates
+ that there is no limit
+ '''
+ if maximum != -1 and count > maximum:
+ raise exception.StackValidationFailed(message=msg)
+
def validate(self):
'''
Validate any of the provided params
@@ -934,28 +943,28 @@ class Server(stack_user.StackUser):
# than the maximum number allowed in the provider's absolute
# limits
if metadata is not None:
- if len(metadata) > limits['maxServerMeta']:
- msg = _('Instance metadata must not contain greater than %s '
- 'entries. This is the maximum number allowed by your '
- 'service provider') % limits['maxServerMeta']
- raise exception.StackValidationFailed(message=msg)
+ msg = _('Instance metadata must not contain greater than %s '
+ 'entries. This is the maximum number allowed by your '
+ 'service provider') % limits['maxServerMeta']
+ self._check_maximum(len(metadata),
+ limits['maxServerMeta'], msg)
# verify the number of personality files and the size of each
# personality file against the provider's absolute limits
if personality is not None:
- if len(personality) > limits['maxPersonality']:
- msg = _("The personality property may not contain "
- "greater than %s entries.") % limits['maxPersonality']
- raise exception.StackValidationFailed(message=msg)
+ msg = _("The personality property may not contain "
+ "greater than %s entries.") % limits['maxPersonality']
+ self._check_maximum(len(personality),
+ limits['maxPersonality'], msg)
for path, contents in personality.items():
- if len(bytes(contents)) > limits['maxPersonalitySize']:
- msg = (_("The contents of personality file \"%(path)s\" "
- "is larger than the maximum allowed personality "
- "file size (%(max_size)s bytes).") %
- {'path': path,
- 'max_size': limits['maxPersonalitySize']})
- raise exception.StackValidationFailed(message=msg)
+ msg = (_("The contents of personality file \"%(path)s\" "
+ "is larger than the maximum allowed personality "
+ "file size (%(max_size)s bytes).") %
+ {'path': path,
+ 'max_size': limits['maxPersonalitySize']})
+ self._check_maximum(len(bytes(contents)),
+ limits['maxPersonalitySize'], msg)
def handle_delete(self):
'''
diff --git a/heat/engine/resources/wait_condition.py b/heat/engine/resources/wait_condition.py
index 05d66a4ff..a3528b9b2 100644
--- a/heat/engine/resources/wait_condition.py
+++ b/heat/engine/resources/wait_condition.py
@@ -91,8 +91,7 @@ class WaitConditionHandle(signal_responder.SignalResponder):
'''
Return a list of the Status values for the handle signals
'''
- return [self.metadata[s]['Status']
- for s in self.metadata]
+ return [v['Status'] for v in self.metadata.values()]
def get_status_reason(self, status):
'''
@@ -100,9 +99,9 @@ class WaitConditionHandle(signal_responder.SignalResponder):
If there is more than one handle signal matching the specified status
then return a semicolon delimited string containing all reasons
'''
- return ';'.join([self.metadata[s]['Reason']
- for s in self.metadata
- if self.metadata[s]['Status'] == status])
+ return ';'.join([v['Reason']
+ for v in self.metadata.values()
+ if v['Status'] == status])
WAIT_STATUSES = (
diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py
index 763beafb6..948284166 100644
--- a/heat/tests/test_api_openstack_v1.py
+++ b/heat/tests/test_api_openstack_v1.py
@@ -3304,6 +3304,24 @@ class SoftwareConfigControllerTest(ControllerTest, HeatTestCase):
self.assertEqual(expected, resp)
@mock.patch.object(policy.Enforcer, 'enforce')
+ def test_show_not_found(self, mock_enforce):
+ self._mock_enforce_setup(mock_enforce, 'show')
+ config_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
+ req = self._get('/software_configs/%s' % config_id)
+
+ error = heat_exc.NotFound('Not found %s' % config_id)
+ with mock.patch.object(
+ self.controller.rpc_client,
+ 'show_software_config',
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(fault.FaultWrapper,
+ self.controller.show,
+ req, config_id=config_id,
+ tenant_id=self.tenant)
+ self.assertEqual(404, resp.json['code'])
+ self.assertEqual('NotFound', resp.json['error']['type'])
+
+ @mock.patch.object(policy.Enforcer, 'enforce')
def test_create(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'create')
body = {
@@ -3346,15 +3364,35 @@ class SoftwareConfigControllerTest(ControllerTest, HeatTestCase):
self._mock_enforce_setup(mock_enforce, 'delete')
config_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
req = self._delete('/software_configs/%s' % config_id)
- return_value = {'Error': 'something wrong'}
+ error = Exception('something wrong')
with mock.patch.object(
self.controller.rpc_client,
'delete_software_config',
- return_value=return_value):
- self.assertRaises(
- webob.exc.HTTPBadRequest, self.controller.delete,
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.delete,
req, config_id=config_id, tenant_id=self.tenant)
+ self.assertEqual(500, resp.json['code'])
+ self.assertEqual('Exception', resp.json['error']['type'])
+
+ @mock.patch.object(policy.Enforcer, 'enforce')
+ def test_delete_not_found(self, mock_enforce):
+ self._mock_enforce_setup(mock_enforce, 'delete')
+ config_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
+ req = self._delete('/software_configs/%s' % config_id)
+ error = heat_exc.NotFound('Not found %s' % config_id)
+ with mock.patch.object(
+ self.controller.rpc_client,
+ 'delete_software_config',
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.delete,
+ req, config_id=config_id, tenant_id=self.tenant)
+
+ self.assertEqual(404, resp.json['code'])
+ self.assertEqual('NotFound', resp.json['error']['type'])
+
class SoftwareDeploymentControllerTest(ControllerTest, HeatTestCase):
@@ -3426,6 +3464,24 @@ class SoftwareDeploymentControllerTest(ControllerTest, HeatTestCase):
self.assertEqual(expected, resp)
@mock.patch.object(policy.Enforcer, 'enforce')
+ def test_show_not_found(self, mock_enforce):
+ self._mock_enforce_setup(mock_enforce, 'show')
+ deployment_id = '38eccf10-97e5-4ae8-9d37-b577c9801750'
+ req = self._get('/software_deployments/%s' % deployment_id)
+
+ error = heat_exc.NotFound('Not found %s' % deployment_id)
+ with mock.patch.object(
+ self.controller.rpc_client,
+ 'show_software_deployment',
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.show,
+ req, deployment_id=deployment_id, tenant_id=self.tenant)
+
+ self.assertEqual(404, resp.json['code'])
+ self.assertEqual('NotFound', resp.json['error']['type'])
+
+ @mock.patch.object(policy.Enforcer, 'enforce')
def test_create(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'create')
config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107'
@@ -3465,7 +3521,8 @@ class SoftwareDeploymentControllerTest(ControllerTest, HeatTestCase):
return_value = body.copy()
deployment_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
return_value['id'] = deployment_id
- req = self._put('/software_deployments/%s', json.dumps(body))
+ req = self._put('/software_deployments/%s' % deployment_id,
+ json.dumps(body))
return_value['server_id'] = server_id
expected = {'software_deployment': return_value}
with mock.patch.object(
@@ -3478,6 +3535,24 @@ class SoftwareDeploymentControllerTest(ControllerTest, HeatTestCase):
self.assertEqual(expected, resp)
@mock.patch.object(policy.Enforcer, 'enforce')
+ def test_update_not_found(self, mock_enforce):
+ self._mock_enforce_setup(mock_enforce, 'update')
+ deployment_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
+ req = self._put('/software_deployments/%s' % deployment_id,
+ '{}')
+ error = heat_exc.NotFound('Not found %s' % deployment_id)
+ with mock.patch.object(
+ self.controller.rpc_client,
+ 'update_software_deployment',
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.update,
+ req, deployment_id=deployment_id,
+ body={}, tenant_id=self.tenant)
+ self.assertEqual(404, resp.json['code'])
+ self.assertEqual('NotFound', resp.json['error']['type'])
+
+ @mock.patch.object(policy.Enforcer, 'enforce')
def test_delete(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'delete')
deployment_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
@@ -3496,11 +3571,29 @@ class SoftwareDeploymentControllerTest(ControllerTest, HeatTestCase):
self._mock_enforce_setup(mock_enforce, 'delete')
deployment_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
req = self._delete('/software_deployments/%s' % deployment_id)
- return_value = {'Error': 'something wrong'}
+ error = Exception('something wrong')
with mock.patch.object(
self.controller.rpc_client,
'delete_software_deployment',
- return_value=return_value):
- self.assertRaises(
- webob.exc.HTTPBadRequest, self.controller.delete,
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.delete,
+ req, deployment_id=deployment_id, tenant_id=self.tenant)
+ self.assertEqual(500, resp.json['code'])
+ self.assertEqual('Exception', resp.json['error']['type'])
+
+ @mock.patch.object(policy.Enforcer, 'enforce')
+ def test_delete_not_found(self, mock_enforce):
+ self._mock_enforce_setup(mock_enforce, 'delete')
+ deployment_id = 'a45559cd-8736-4375-bc39-d6a7bb62ade2'
+ req = self._delete('/software_deployments/%s' % deployment_id)
+ error = heat_exc.NotFound('Not Found %s' % deployment_id)
+ with mock.patch.object(
+ self.controller.rpc_client,
+ 'delete_software_deployment',
+ side_effect=to_remote_error(error)):
+ resp = request_with_middleware(
+ fault.FaultWrapper, self.controller.delete,
req, deployment_id=deployment_id, tenant_id=self.tenant)
+ self.assertEqual(404, resp.json['code'])
+ self.assertEqual('NotFound', resp.json['error']['type'])
diff --git a/heat/tests/test_heatclient.py b/heat/tests/test_heatclient.py
index 854914e90..c219c4097 100644
--- a/heat/tests/test_heatclient.py
+++ b/heat/tests/test_heatclient.py
@@ -279,11 +279,17 @@ class KeystoneClientTest(HeatTestCase):
mock_user.default_project_id = 'aproject'
self.mock_admin_client.users.get('duser123').AndReturn(mock_user)
self.mock_admin_client.users.delete('duser123').AndReturn(None)
+ self.mock_admin_client.users.get('duser123').AndRaise(
+ kc_exception.NotFound)
+
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_domain_user(user_id='duser123',
project_id='aproject')
+ # Second delete will raise ignored NotFound
+ heat_ks_client.delete_stack_domain_user(user_id='duser123',
+ project_id='aproject')
def test_delete_stack_domain_user_legacy_fallback(self):
"""Test deleting a stack domain user, fallback path."""
@@ -358,9 +364,14 @@ class KeystoneClientTest(HeatTestCase):
# mock keystone client delete function
self.mock_ks_v3_client.users = self.m.CreateMockAnything()
self.mock_ks_v3_client.users.delete(user='atestuser').AndReturn(None)
+ self.mock_ks_v3_client.users.delete(user='atestuser').AndRaise(
+ kc_exception.NotFound)
+
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_user('atestuser')
+ # Second delete will raise ignored NotFound
+ heat_ks_client.delete_stack_user('atestuser')
def test_init_v3_token(self):
@@ -671,6 +682,7 @@ class KeystoneClientTest(HeatTestCase):
mock_user.domain_id = domain_id
mock_user.default_project_id = project_id
self.mock_admin_client.users.get(user_id).AndReturn(mock_user)
+ return mock_user
def test_enable_stack_domain_user(self):
"""Test enabling a stack domain user."""
@@ -808,16 +820,24 @@ class KeystoneClientTest(HeatTestCase):
# mock keystone client functions
self._stub_domain_admin_client()
- self._stub_admin_user_get('duser123', 'adomain123', 'aproject')
+ user = self._stub_admin_user_get('duser123', 'adomain123', 'aproject')
self.mock_admin_client.credentials = self.m.CreateMockAnything()
self.mock_admin_client.credentials.delete(
'acredentialid').AndReturn(None)
+
+ self.mock_admin_client.users.get('duser123').AndReturn(user)
+ self.mock_admin_client.credentials.delete(
+ 'acredentialid').AndRaise(kc_exception.NotFound)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_domain_user_keypair(
user_id='duser123', project_id='aproject',
credential_id='acredentialid')
+ # Second delete will raise ignored NotFound
+ heat_ks_client.delete_stack_domain_user_keypair(
+ user_id='duser123', project_id='aproject',
+ credential_id='acredentialid')
def test_delete_stack_domain_user_keypair_legacy_fallback(self):
cfg.CONF.clear_override('stack_user_domain')
@@ -1078,10 +1098,15 @@ class KeystoneClientTest(HeatTestCase):
# mock keystone client delete function
self.mock_ks_v3_client.credentials = self.m.CreateMockAnything()
self.mock_ks_v3_client.credentials.delete(credential_id)
+ self.mock_ks_v3_client.credentials.delete(credential_id).AndRaise(
+ kc_exception.NotFound)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
self.assertIsNone(heat_ks_client.delete_ec2_keypair(
credential_id=credential_id))
+ # Second delete will raise ignored NotFound
+ self.assertIsNone(heat_ks_client.delete_ec2_keypair(
+ credential_id=credential_id))
def test_delete_ec2_keypair_access(self):
@@ -1153,12 +1178,16 @@ class KeystoneClientTest(HeatTestCase):
self._stub_domain_admin_client()
self.mock_admin_client.projects = self.m.CreateMockAnything()
self.mock_admin_client.projects.delete(project='aprojectid')
+ self.mock_admin_client.projects.delete(project='aprojectid').AndRaise(
+ kc_exception.NotFound)
self.m.ReplayAll()
ctx = utils.dummy_context()
ctx.trust_id = None
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_domain_project(project_id='aprojectid')
+ # Second delete will raise ignored NotFound
+ heat_ks_client.delete_stack_domain_project(project_id='aprojectid')
def test_delete_stack_domain_project_legacy_fallback(self):
"""Test the delete_stack_domain_project function, fallback path."""
diff --git a/heat/tests/test_nova_floatingip.py b/heat/tests/test_nova_floatingip.py
index 4240d143f..da83f467b 100644
--- a/heat/tests/test_nova_floatingip.py
+++ b/heat/tests/test_nova_floatingip.py
@@ -10,8 +10,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+from novaclient import exceptions as ncli_ex
from novaclient.v1_1 import client as novaclient
+from heat.common import exception as heat_ex
from heat.common import template_format
from heat.engine import clients
from heat.engine.resources.nova_floatingip import NovaFloatingIp
@@ -101,7 +103,6 @@ class NovaFloatingIPTest(HeatTestCase):
'pool': 'public'
})
)
- self.novaclient.servers.add_floating_ip(None, '11.0.0.1')
template = template_format.parse(floating_ip_template_with_assoc)
stack = utils.parse_stack(template)
@@ -140,8 +141,26 @@ class NovaFloatingIPTest(HeatTestCase):
self.m.VerifyAll()
+ def test_delete_floating_ip_assoc_successful_if_create_failed(self):
+ rsrc = self.prepare_floating_ip_assoc()
+ self.novaclient.servers.add_floating_ip(None, '11.0.0.1').AndRaise(
+ ncli_ex.BadRequest(400))
+
+ self.m.ReplayAll()
+
+ rsrc.validate()
+
+ self.assertRaises(heat_ex.ResourceFailure,
+ scheduler.TaskRunner(rsrc.create))
+ self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
+ scheduler.TaskRunner(rsrc.delete)()
+ self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
+
+ self.m.VerifyAll()
+
def test_floating_ip_assoc_create(self):
rsrc = self.prepare_floating_ip_assoc()
+ self.novaclient.servers.add_floating_ip(None, '11.0.0.1')
self.m.ReplayAll()
rsrc.validate()
@@ -153,7 +172,7 @@ class NovaFloatingIPTest(HeatTestCase):
def test_floating_ip_assoc_delete(self):
rsrc = self.prepare_floating_ip_assoc()
-
+ self.novaclient.servers.add_floating_ip(None, '11.0.0.1')
self.novaclient.servers.get(
'67dc62f9-efde-4c8b-94af-013e00f5dc57').AndReturn('server')
self.novaclient.floating_ips.get('1').AndReturn(
diff --git a/heat/tests/test_server.py b/heat/tests/test_server.py
index 5fe4dbdd6..aad04dbfd 100644
--- a/heat/tests/test_server.py
+++ b/heat/tests/test_server.py
@@ -711,6 +711,15 @@ class ServersTest(HeatTestCase):
disk_config=None, reservation_id=None,
files={}, admin_pass='foo')
+ def test_check_maximum(self):
+ msg = 'test_check_maximum'
+ self.assertIsNone(servers.Server._check_maximum(1, 1, msg))
+ self.assertIsNone(servers.Server._check_maximum(1000, -1, msg))
+ error = self.assertRaises(exception.StackValidationFailed,
+ servers.Server._check_maximum,
+ 2, 1, msg)
+ self.assertEqual(msg, str(error))
+
def test_server_validate(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
diff --git a/heat/tests/test_sqlalchemy_api.py b/heat/tests/test_sqlalchemy_api.py
index 829ad228a..a91273c8e 100644
--- a/heat/tests/test_sqlalchemy_api.py
+++ b/heat/tests/test_sqlalchemy_api.py
@@ -1221,6 +1221,11 @@ class DBAPIStackTest(HeatTestCase):
self.assertIsNone(db_api.stack_get(ctx, stacks[s].id,
show_deleted=True))
+ def test_stack_status_reason_truncate(self):
+ stack = create_stack(self.ctx, self.template, self.user_creds,
+ status_reason='a' * 1024)
+ self.assertEqual('a' * 255, stack.status_reason)
+
class DBAPIResourceTest(HeatTestCase):
def setUp(self):
@@ -1307,6 +1312,12 @@ class DBAPIResourceTest(HeatTestCase):
self.assertRaises(exception.NotFound, db_api.resource_get_all_by_stack,
self.ctx, self.stack2.id)
+ def test_resource_status_reason_truncate(self):
+ res = create_resource(self.ctx, self.stack,
+ status_reason='a' * 1024)
+ ret_res = db_api.resource_get(self.ctx, res.id)
+ self.assertEqual('a' * 255, ret_res.status_reason)
+
class DBAPIStackLockTest(HeatTestCase):
def setUp(self):
@@ -1511,6 +1522,11 @@ class DBAPIEventTest(HeatTestCase):
self.assertEqual(1, db_api.event_count_all_by_stack(self.ctx,
self.stack2.id))
+ def test_event_resource_status_reason_truncate(self):
+ event = create_event(self.ctx, resource_status_reason='a' * 1024)
+ ret_event = db_api.event_get(self.ctx, event.id)
+ self.assertEqual('a' * 255, ret_event.resource_status_reason)
+
class DBAPIWatchRuleTest(HeatTestCase):
def setUp(self):
diff --git a/requirements.txt b/requirements.txt
index d93023a34..4b6298d53 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,7 @@ kombu>=2.4.8
argparse
lxml>=2.3
netaddr>=0.7.6
-six>=1.5.2
+six>=1.6.0
sqlalchemy-migrate>=0.8.2,!=0.8.4
python-novaclient>=2.17.0
PasteDeploy>=1.5.0