diff options
-rw-r--r-- | heat/common/exception.py | 2 | ||||
-rw-r--r-- | heat/common/heat_keystoneclient.py | 26 | ||||
-rw-r--r-- | heat/db/sqlalchemy/models.py | 41 | ||||
-rw-r--r-- | heat/engine/resources/nova_floatingip.py | 3 | ||||
-rw-r--r-- | heat/engine/resources/nova_utils.py | 3 | ||||
-rw-r--r-- | heat/engine/resources/server.py | 41 | ||||
-rw-r--r-- | heat/engine/resources/wait_condition.py | 9 | ||||
-rw-r--r-- | heat/tests/test_api_openstack_v1.py | 111 | ||||
-rw-r--r-- | heat/tests/test_heatclient.py | 31 | ||||
-rw-r--r-- | heat/tests/test_nova_floatingip.py | 23 | ||||
-rw-r--r-- | heat/tests/test_server.py | 9 | ||||
-rw-r--r-- | heat/tests/test_sqlalchemy_api.py | 16 | ||||
-rw-r--r-- | requirements.txt | 2 |
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 |