diff -Nru heat-10.0.1/AUTHORS heat-10.0.2/AUTHORS --- heat-10.0.1/AUTHORS 2018-05-08 04:30:36.000000000 +0000 +++ heat-10.0.2/AUTHORS 2018-09-04 20:06:15.000000000 +0000 @@ -115,9 +115,11 @@ Eoghan Glynn Eoghan Glynn Eric Brown +Erik Olof Gunnar Andersson Ethan Lynn Fabien Boucher FeihuJiang +Feilong Wang Flavio Percoco Gary Kotton Gauvain Pocentek @@ -194,6 +196,7 @@ Joshua Harlow JuPing Juan Antonio Osorio Robles +Julia Kreger Julia Varlamova Julian Sy Julien Danjou @@ -324,6 +327,7 @@ Sergey Reshetnyak Sergey Skripnick Sergey Vilgelm +Seyeong Kim Shane Wang ShaoHe Feng Sharmin Choksey diff -Nru heat-10.0.1/ChangeLog heat-10.0.2/ChangeLog --- heat-10.0.1/ChangeLog 2018-05-08 04:30:36.000000000 +0000 +++ heat-10.0.2/ChangeLog 2018-09-04 20:06:14.000000000 +0000 @@ -1,10 +1,31 @@ CHANGES ======= +10.0.2 +------ + +* Eliminate client races in legacy operations +* Eliminate client race condition in convergence delete +* Delete snapshots using contemporary resources +* Ignore RESOLVE translation errors when translating before\_props +* Ignore NotFound error in prepare\_for\_replace +* Fix multi region issue for software deployment +* Support region\_name for software deployment +* Ignore errors in purging events +* Replace deprecated nova calls for floatingip +* Don't allow nested or stacks in FAILED state to be migrated +* Download octavia image in tests +* Reset resource replaced\_by field for rollback +* Delete internal ports for ERROR-ed nodes +* Fixing Senlin incompatibility with openstacksdk 0.11.x +* Retry resource check if atomic key incremented +* Fixing unicode issue when to\_dict is called on py2.7 env + 10.0.1 ------ * Resolve race in providing deployment data to Swift +* Persist external resources on update * Generate user passwords with special characters * Fix entropy problems with OS::Random::String * Fix races in conditionals tests diff -Nru heat-10.0.1/debian/changelog heat-10.0.2/debian/changelog --- heat-10.0.1/debian/changelog 2018-04-24 07:26:21.000000000 +0000 +++ heat-10.0.2/debian/changelog 2018-10-01 15:36:52.000000000 +0000 @@ -1,3 +1,11 @@ +heat (1:10.0.2-0ubuntu1) bionic; urgency=medium + + * New stable point release for OpenStack Queens (LP: #1795424). + - d/p/0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch: + Dropped. Included in upstream stable point release. + + -- Corey Bryant Mon, 01 Oct 2018 11:36:52 -0400 + heat (1:10.0.1-0ubuntu2) bionic; urgency=medium * Fixing heat error with unicode (LP: #1761629) diff -Nru heat-10.0.1/debian/patches/0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch heat-10.0.2/debian/patches/0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch --- heat-10.0.1/debian/patches/0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch 2018-04-24 07:26:21.000000000 +0000 +++ heat-10.0.2/debian/patches/0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -From 4d71926b3afc50c3f16378de260b86a85e8d721d Mon Sep 17 00:00:00 2001 -From: Seyeong Kim -Date: Thu, 5 Apr 2018 15:10:01 -0700 -Subject: [PATCH] Fixing unicode issue when to_dict is called on py2.7 env -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -When using non-unicode old style user id such as Gāo -Unicode error popup on py2.7 environment -Fixing it on common/context.py - -Change-Id: I95e49f359410049ff5b254cd1b8ee16402c8719d -Closes-Bug: #1761629 ---- - heat/common/context.py | 4 ++-- - heat/tests/test_common_context.py | 46 +++++++++++++++++++++++++++++++++++++++ - 2 files changed, 48 insertions(+), 2 deletions(-) - -Bug: upstream, https://github.com/openstack/heat/commit/44fb52f0652a634540b8fcac07bd4ee7df03d3b2 -Bug-Ubuntu: https://bugs.launchpad.net/bugs/1761629 -Index: heat-10.0.0/heat/common/context.py -=================================================================== ---- heat-10.0.0.orig/heat/common/context.py 2018-04-24 00:26:18.505148074 -0700 -+++ heat-10.0.0/heat/common/context.py 2018-04-24 00:26:18.501148202 -0700 -@@ -156,8 +156,8 @@ - return self._clients - - def to_dict(self): -- user_idt = '{user} {tenant}'.format(user=self.user_id or '-', -- tenant=self.tenant_id or '-') -+ user_idt = u'{user} {tenant}'.format(user=self.user_id or '-', -+ tenant=self.tenant_id or '-') - - return {'auth_token': self.auth_token, - 'username': self.username, -Index: heat-10.0.0/heat/tests/test_common_context.py -=================================================================== ---- heat-10.0.0.orig/heat/tests/test_common_context.py 2018-04-24 00:26:18.505148074 -0700 -+++ heat-10.0.0/heat/tests/test_common_context.py 2018-04-24 00:26:18.501148202 -0700 -@@ -1,3 +1,4 @@ -+# -*- coding: utf-8 -*- - # - # 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 -@@ -78,6 +79,51 @@ - del(ctx_dict['request_id']) - self.assertEqual(self.ctx, ctx_dict) - -+ def test_request_context_to_dict_unicode(self): -+ -+ ctx_origin = {'username': 'mick', -+ 'trustor_user_id': None, -+ 'auth_token': '123', -+ 'auth_token_info': {'123info': 'woop'}, -+ 'is_admin': False, -+ 'user': 'mick', -+ 'password': 'foo', -+ 'trust_id': None, -+ 'global_request_id': None, -+ 'show_deleted': False, -+ 'roles': ['arole', 'notadmin'], -+ 'tenant_id': '456tenant', -+ 'user_id': u'Gāo', -+ 'tenant': u'\u5218\u80dc', -+ 'auth_url': 'http://xyz', -+ 'aws_creds': 'blah', -+ 'region_name': 'RegionOne', -+ 'user_identity': u'Gāo 456tenant', -+ 'user_domain': None, -+ 'project_domain': None} -+ -+ ctx = context.RequestContext( -+ auth_token=ctx_origin.get('auth_token'), -+ username=ctx_origin.get('username'), -+ password=ctx_origin.get('password'), -+ aws_creds=ctx_origin.get('aws_creds'), -+ project_name=ctx_origin.get('tenant'), -+ tenant=ctx_origin.get('tenant_id'), -+ user=ctx_origin.get('user_id'), -+ auth_url=ctx_origin.get('auth_url'), -+ roles=ctx_origin.get('roles'), -+ show_deleted=ctx_origin.get('show_deleted'), -+ is_admin=ctx_origin.get('is_admin'), -+ auth_token_info=ctx_origin.get('auth_token_info'), -+ trustor_user_id=ctx_origin.get('trustor_user_id'), -+ trust_id=ctx_origin.get('trust_id'), -+ region_name=ctx_origin.get('region_name'), -+ user_domain_id=ctx_origin.get('user_domain'), -+ project_domain_id=ctx_origin.get('project_domain')) -+ ctx_dict = ctx.to_dict() -+ del(ctx_dict['request_id']) -+ self.assertEqual(ctx_origin, ctx_dict) -+ - def test_request_context_from_dict(self): - ctx = context.RequestContext.from_dict(self.ctx) - ctx_dict = ctx.to_dict() diff -Nru heat-10.0.1/debian/patches/series heat-10.0.2/debian/patches/series --- heat-10.0.1/debian/patches/series 2018-04-24 07:26:21.000000000 +0000 +++ heat-10.0.2/debian/patches/series 2018-10-01 15:36:52.000000000 +0000 @@ -1,2 +1 @@ sudoers_patch.patch -0001-Fixing-unicode-issue-when-to_dict-is-called-on-py2.7.patch diff -Nru heat-10.0.1/heat/cmd/manage.py heat-10.0.2/heat/cmd/manage.py --- heat-10.0.1/heat/cmd/manage.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/cmd/manage.py 2018-09-04 20:02:39.000000000 +0000 @@ -122,10 +122,8 @@ except exception.NotFound: raise Exception(_("Stack with id %s can not be found.") % CONF.command.stack_id) - except exception.ActionInProgress: - raise Exception(_("The stack or some of its nested stacks are " - "in progress. Note, that all the stacks should be " - "in COMPLETE state in order to be migrated.")) + except (exception.NotSupported, exception.ActionNotComplete) as ex: + raise Exception(ex.message) def purge_deleted(): diff -Nru heat-10.0.1/heat/common/context.py heat-10.0.2/heat/common/context.py --- heat-10.0.1/heat/common/context.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/common/context.py 2018-09-04 20:02:39.000000000 +0000 @@ -156,8 +156,8 @@ return self._clients def to_dict(self): - user_idt = '{user} {tenant}'.format(user=self.user_id or '-', - tenant=self.tenant_id or '-') + user_idt = u'{user} {tenant}'.format(user=self.user_id or '-', + tenant=self.tenant_id or '-') return {'auth_token': self.auth_token, 'username': self.username, diff -Nru heat-10.0.1/heat/common/exception.py heat-10.0.2/heat/common/exception.py --- heat-10.0.1/heat/common/exception.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/common/exception.py 2018-09-04 20:02:49.000000000 +0000 @@ -487,6 +487,11 @@ "in progress.") +class ActionNotComplete(HeatException): + msg_fmt = _("Stack %(stack_name)s has an action (%(action)s) " + "in progress or failed state.") + + class StopActionFailed(HeatException): msg_fmt = _("Failed to stop stack (%(stack_name)s) on other engine " "(%(engine_id)s)") diff -Nru heat-10.0.1/heat/db/sqlalchemy/api.py heat-10.0.2/heat/db/sqlalchemy/api.py --- heat-10.0.1/heat/db/sqlalchemy/api.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/db/sqlalchemy/api.py 2018-09-04 20:02:49.000000000 +0000 @@ -605,7 +605,8 @@ def stack_get_all_by_owner_id(context, owner_id): results = soft_delete_aware_query( - context, models.Stack).filter_by(owner_id=owner_id).all() + context, models.Stack).filter_by(owner_id=owner_id, + backup=False).all() return results @@ -1027,33 +1028,31 @@ # So we must manually supply the IN() values. # pgsql SHOULD work with the pure DELETE/JOIN below but that must be # confirmed via integration tests. - query = _query_all_by_stack(context, stack_id) session = context.session - id_pairs = [(e.id, e.rsrc_prop_data_id) for e in query.order_by( - models.Event.id).limit(limit).all()] - if id_pairs is None: - return 0 - (ids, rsrc_prop_ids) = zip(*id_pairs) - max_id = ids[-1] - # delete the events - retval = session.query(models.Event.id).filter( - models.Event.id <= max_id).filter( - models.Event.stack_id == stack_id).delete() - - # delete unreferenced resource_properties_data - rsrc_prop_ids = set(rsrc_prop_ids) - if rsrc_prop_ids: - still_ref_ids_from_events = [e.rsrc_prop_data_id for e - in _query_all_by_stack( - context, stack_id).all()] - still_ref_ids_from_rsrcs = [r.rsrc_prop_data_id for r - in context.session.query(models.Resource). - filter_by(stack_id=stack_id).all()] - rsrc_prop_ids = rsrc_prop_ids - set(still_ref_ids_from_events) \ - - set(still_ref_ids_from_rsrcs) - q_rpd = session.query(models.ResourcePropertiesData.id).filter( - models.ResourcePropertiesData.id.in_(rsrc_prop_ids)) - q_rpd.delete(synchronize_session=False) + with session.begin(): + query = _query_all_by_stack(context, stack_id) + query = query.order_by(models.Event.id).limit(limit) + id_pairs = [(e.id, e.rsrc_prop_data_id) for e in query.all()] + if not id_pairs: + return 0 + (ids, rsrc_prop_ids) = zip(*id_pairs) + max_id = ids[-1] + # delete the events + retval = session.query(models.Event.id).filter( + models.Event.id <= max_id).filter( + models.Event.stack_id == stack_id).delete() + + # delete unreferenced resource_properties_data + if rsrc_prop_ids: + ev_ref_ids = set(e.rsrc_prop_data_id for e + in _query_all_by_stack(context, stack_id).all()) + rsrc_ref_ids = set(r.rsrc_prop_data_id for r + in session.query(models.Resource).filter_by( + stack_id=stack_id).all()) + clr_prop_ids = set(rsrc_prop_ids) - ev_ref_ids - rsrc_ref_ids + q_rpd = session.query(models.ResourcePropertiesData.id).filter( + models.ResourcePropertiesData.id.in_(clr_prop_ids)) + q_rpd.delete(synchronize_session=False) return retval @@ -1066,8 +1065,11 @@ (event_count_all_by_stack(context, values['stack_id']) >= cfg.CONF.max_events_per_stack)): # prune - _delete_event_rows( - context, values['stack_id'], cfg.CONF.event_purge_batch_size) + try: + _delete_event_rows(context, values['stack_id'], + cfg.CONF.event_purge_batch_size) + except db_exception.DBError as exc: + LOG.error('Failed to purge events: %s', six.text_type(exc)) event_ref = models.Event() event_ref.update(values) event_ref.save(context.session) diff -Nru heat-10.0.1/heat/engine/check_resource.py heat-10.0.2/heat/engine/check_resource.py --- heat-10.0.1/heat/engine/check_resource.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/check_resource.py 2018-09-04 20:02:49.000000000 +0000 @@ -56,16 +56,40 @@ self.msg_queue = msg_queue self.input_data = input_data - def _try_steal_engine_lock(self, cnxt, resource_id): + def _stale_resource_needs_retry(self, cnxt, rsrc, prev_template_id): + """Determine whether a resource needs retrying after failure to lock. + + Return True if we need to retry the check operation because of a + failure to acquire the lock. This can be either because the engine + holding the lock is no longer working, or because no other engine had + locked the resource and the data was just out of date. + + In the former case, the lock will be stolen and the resource status + changed to FAILED. + """ + fields = {'current_template_id', 'engine_id'} rs_obj = resource_objects.Resource.get_obj(cnxt, - resource_id, - fields=('engine_id', )) + rsrc.id, + refresh=True, + fields=fields) if rs_obj.engine_id not in (None, self.engine_id): if not listener_client.EngineListenerClient( rs_obj.engine_id).is_alive(cnxt): # steal the lock. rs_obj.update_and_save({'engine_id': None}) + + # set the resource state as failed + status_reason = ('Worker went down ' + 'during resource %s' % rsrc.action) + rsrc.state_set(rsrc.action, + rsrc.FAILED, + six.text_type(status_reason)) return True + elif (rs_obj.engine_id is None and + rs_obj.current_template_id == prev_template_id): + LOG.debug('Resource id=%d stale; retrying check', rsrc.id) + return True + LOG.debug('Resource id=%d modified by another traversal', rsrc.id) return False def _trigger_rollback(self, stack): @@ -135,6 +159,7 @@ def _do_check_resource(self, cnxt, current_traversal, tmpl, resource_data, is_update, rsrc, stack, adopt_stack_data): + prev_template_id = rsrc.current_template_id try: if is_update: try: @@ -155,14 +180,8 @@ return True except exception.UpdateInProgress: - if self._try_steal_engine_lock(cnxt, rsrc.id): + if self._stale_resource_needs_retry(cnxt, rsrc, prev_template_id): rpc_data = sync_point.serialize_input_data(self.input_data) - # set the resource state as failed - status_reason = ('Worker went down ' - 'during resource %s' % rsrc.action) - rsrc.state_set(rsrc.action, - rsrc.FAILED, - six.text_type(status_reason)) self._rpc_client.check_resource(cnxt, rsrc.id, current_traversal, diff -Nru heat-10.0.1/heat/engine/clients/client_exception.py heat-10.0.2/heat/engine/clients/client_exception.py --- heat-10.0.1/heat/engine/clients/client_exception.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/clients/client_exception.py 2018-09-04 20:02:39.000000000 +0000 @@ -25,3 +25,7 @@ class EntityUniqueMatchNotFound(EntityMatchNotFound): msg_fmt = _("No %(entity)s unique match found for %(args)s.") + + +class InterfaceNotFound(exception.HeatException): + msg_fmt = _("No network interface found for server %(id)s.") diff -Nru heat-10.0.1/heat/engine/clients/os/nova.py heat-10.0.2/heat/engine/clients/os/nova.py --- heat-10.0.1/heat/engine/clients/os/nova.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/clients/os/nova.py 2018-09-04 20:02:49.000000000 +0000 @@ -19,17 +19,20 @@ import pkgutil import string +from neutronclient.common import exceptions as q_exceptions from novaclient import client as nc from novaclient import exceptions from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import netutils import six from six.moves.urllib import parse as urlparse import tenacity from heat.common import exception from heat.common.i18n import _ +from heat.engine.clients import client_exception from heat.engine.clients import client_plugin from heat.engine.clients import os as os_client from heat.engine import constraints @@ -100,7 +103,8 @@ return client def is_not_found(self, ex): - return isinstance(ex, exceptions.NotFound) + return isinstance(ex, (exceptions.NotFound, + q_exceptions.NotFound)) def is_over_limit(self, ex): return isinstance(ex, exceptions.OverLimit) @@ -672,6 +676,54 @@ {'att': attach_id, 'srv': server_id}) return False + def associate_floatingip(self, server_id, floatingip_id): + iface_list = self.fetch_server(server_id).interface_list() + if len(iface_list) == 0: + raise client_exception.InterfaceNotFound(id=server_id) + if len(iface_list) > 1: + LOG.warning("Multiple interfaces found for server %s, " + "using the first one.", server_id) + + port_id = iface_list[0].port_id + fixed_ips = iface_list[0].fixed_ips + fixed_address = next(ip['ip_address'] for ip in fixed_ips + if netutils.is_valid_ipv4(ip['ip_address'])) + request_body = { + 'floatingip': { + 'port_id': port_id, + 'fixed_ip_address': fixed_address}} + + self.clients.client('neutron').update_floatingip(floatingip_id, + request_body) + + def dissociate_floatingip(self, floatingip_id): + request_body = { + 'floatingip': { + 'port_id': None, + 'fixed_ip_address': None}} + self.clients.client('neutron').update_floatingip(floatingip_id, + request_body) + + def associate_floatingip_address(self, server_id, fip_address): + fips = self.clients.client( + 'neutron').list_floatingips( + floating_ip_address=fip_address)['floatingips'] + if len(fips) == 0: + args = {'ip_address': fip_address} + raise client_exception.EntityMatchNotFound(entity='floatingip', + args=args) + self.associate_floatingip(server_id, fips[0]['id']) + + def dissociate_floatingip_address(self, fip_address): + fips = self.clients.client( + 'neutron').list_floatingips( + floating_ip_address=fip_address)['floatingips'] + if len(fips) == 0: + args = {'ip_address': fip_address} + raise client_exception.EntityMatchNotFound(entity='floatingip', + args=args) + self.dissociate_floatingip(fips[0]['id']) + def interface_detach(self, server_id, port_id): with self.ignore_not_found: server = self.fetch_server(server_id) diff -Nru heat-10.0.1/heat/engine/clients/os/openstacksdk.py heat-10.0.2/heat/engine/clients/os/openstacksdk.py --- heat-10.0.1/heat/engine/clients/os/openstacksdk.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/clients/os/openstacksdk.py 2018-09-04 20:02:49.000000000 +0000 @@ -42,9 +42,13 @@ config=self._get_service_interfaces(), region_name=self._get_region_name(), app_name='heat', - app_version=heat.version.version_info.version_string()) + app_version=heat.version.version_info.version_string(), + **self._get_additional_create_args(version)) return connection.Connection(config=config) + def _get_additional_create_args(self, version): + return {} + def _get_service_interfaces(self): interfaces = {} if not os_service_types: diff -Nru heat-10.0.1/heat/engine/clients/os/senlin.py heat-10.0.2/heat/engine/clients/os/senlin.py --- heat-10.0.1/heat/engine/clients/os/senlin.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/clients/os/senlin.py 2018-09-04 20:02:39.000000000 +0000 @@ -29,6 +29,11 @@ client = super(SenlinClientPlugin, self)._create(version=version) return client.clustering + def _get_additional_create_args(self, version): + return { + 'clustering_api_version': version or '1' + } + def generate_spec(self, spec_type, spec_props): spec = {'properties': spec_props} spec['type'], spec['version'] = spec_type.split('-') diff -Nru heat-10.0.1/heat/engine/properties.py heat-10.0.2/heat/engine/properties.py --- heat-10.0.1/heat/engine/properties.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/properties.py 2018-09-04 20:02:39.000000000 +0000 @@ -388,8 +388,10 @@ self.translation = (trans.Translation(properties=self) if translation is None else translation) - def update_translation(self, rules, client_resolve=True): - self.translation.set_rules(rules, client_resolve=client_resolve) + def update_translation(self, rules, client_resolve=True, + ignore_resolve_error=False): + self.translation.set_rules(rules, client_resolve=client_resolve, + ignore_resolve_error=ignore_resolve_error) @staticmethod def schema_from_params(params_snippet): diff -Nru heat-10.0.1/heat/engine/resource.py heat-10.0.2/heat/engine/resource.py --- heat-10.0.1/heat/engine/resource.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/resource.py 2018-09-04 20:02:49.000000000 +0000 @@ -1334,7 +1334,7 @@ return [] def translate_properties(self, properties, - client_resolve=True): + client_resolve=True, ignore_resolve_error=False): """Set resource specific rules for properties translation. The properties parameter is a properties object and the @@ -1342,7 +1342,9 @@ do 'RESOLVE' translation with client lookup. """ rules = self.translation_rules(properties) or [] - properties.update_translation(rules, client_resolve=client_resolve) + properties.update_translation( + rules, client_resolve=client_resolve, + ignore_resolve_error=ignore_resolve_error) def cancel_grace_period(self): canceller = getattr(self, @@ -1455,6 +1457,7 @@ self.state_set(self.UPDATE, self.FAILED, six.text_type(failure)) raise failure + self.replaced_by = None runner = scheduler.TaskRunner( self.update, new_res_def, @@ -1522,7 +1525,7 @@ after_props = after.properties(self.properties_schema, self.context) self.translate_properties(after_props) - self.translate_properties(before_props) + self.translate_properties(before_props, ignore_resolve_error=True) if (cfg.CONF.observe_on_update or self.converge) and before_props: if not self.resource_id: @@ -1607,6 +1610,8 @@ raise exception.ResourceFailure(exc, self, action) elif after_external_id is not None: LOG.debug("Skip update on external resource.") + if update_templ_func is not None: + update_templ_func(persist=True) return after_props, before_props = self._prepare_update_props(after, before) diff -Nru heat-10.0.1/heat/engine/resources/aws/ec2/eip.py heat-10.0.2/heat/engine/resources/aws/ec2/eip.py --- heat-10.0.1/heat/engine/resources/aws/ec2/eip.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/aws/ec2/eip.py 2018-09-04 20:02:39.000000000 +0000 @@ -17,6 +17,7 @@ from heat.common import exception from heat.common.i18n import _ from heat.engine import attributes +from heat.engine.clients import client_exception from heat.engine import constraints from heat.engine import properties from heat.engine import resource @@ -98,36 +99,27 @@ props = {'floating_network_id': ext_net} ips = self.neutron().create_floatingip({ 'floatingip': props})['floatingip'] - self.ipaddress = ips['floating_ip_address'] self.resource_id_set(ips['id']) + self.ipaddress = ips['floating_ip_address'] + LOG.info('ElasticIp create %s', str(ips)) instance_id = self.properties[self.INSTANCE_ID] if instance_id: - server = self.client().servers.get(instance_id) - server.add_floating_ip(self._ipaddress()) + self.client_plugin().associate_floatingip(instance_id, + ips['id']) def handle_delete(self): if self.resource_id is None: return # may be just create an eip when creation, or create the association - # failed when creation, there will no association, if we attempt to + # failed when creation, there will be no association, if we attempt to # disassociate, an exception will raised, we need # to catch and ignore it, and then to deallocate the eip instance_id = self.properties[self.INSTANCE_ID] if instance_id: - try: - server = self.client().servers.get(instance_id) - if server: - server.remove_floating_ip(self._ipaddress()) - except Exception as e: - is_not_found = self.client_plugin('nova').is_not_found(e) - is_unprocessable_entity = self.client_plugin( - 'nova').is_unprocessable_entity(e) - - if (not is_not_found and not is_unprocessable_entity): - raise - + with self.client_plugin().ignore_not_found: + self.client_plugin().dissociate_floatingip(self.resource_id) # deallocate the eip with self.client_plugin('neutron').ignore_not_found: self.neutron().delete_floatingip(self.resource_id) @@ -135,19 +127,13 @@ def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: if self.INSTANCE_ID in prop_diff: - instance_id = prop_diff.get(self.INSTANCE_ID) + instance_id = prop_diff[self.INSTANCE_ID] if instance_id: - # no need to remove the floating ip from the old instance, - # nova does this automatically when calling - # add_floating_ip(). - server = self.client().servers.get(instance_id) - server.add_floating_ip(self._ipaddress()) + self.client_plugin().associate_floatingip( + instance_id, self.resource_id) else: - # to remove the floating_ip from the old instance - instance_id_old = self.properties[self.INSTANCE_ID] - if instance_id_old: - server = self.client().servers.get(instance_id_old) - server.remove_floating_ip(self._ipaddress()) + self.client_plugin().dissociate_floatingip( + self.resource_id) def get_reference_id(self): eip = self._ipaddress() @@ -251,45 +237,31 @@ allocationId, {'floatingip': {'port_id': port_id}}) except Exception as e: - if ignore_not_found: - self.client_plugin('neutron').ignore_not_found(e) - else: + if not (ignore_not_found and self.client_plugin( + 'neutron').is_not_found(e)): raise - def _nova_remove_floating_ip(self, instance_id, eip, - ignore_not_found=False): - server = None + def _remove_floating_ip_address(self, eip, ignore_not_found=False): try: - server = self.client().servers.get(instance_id) - server.remove_floating_ip(eip) + self.client_plugin().dissociate_floatingip_address(eip) except Exception as e: - is_not_found = self.client_plugin('nova').is_not_found(e) - iue = self.client_plugin('nova').is_unprocessable_entity(e) - if ((not ignore_not_found and is_not_found) or - (not is_not_found and not iue)): + addr_not_found = isinstance( + e, client_exception.EntityMatchNotFound) + fip_not_found = self.client_plugin().is_not_found(e) + not_found = addr_not_found or fip_not_found + if not (ignore_not_found and not_found): raise - return server - - def _floatingIp_detach(self, - nova_ignore_not_found=False, - neutron_ignore_not_found=False): + def _floatingIp_detach(self): eip = self.properties[self.EIP] allocation_id = self.properties[self.ALLOCATION_ID] - instance_id = self.properties[self.INSTANCE_ID] - server = None if eip: # if has eip_old, to remove the eip_old from the instance - server = self._nova_remove_floating_ip(instance_id, - eip, - nova_ignore_not_found) + self._remove_floating_ip_address(eip) else: # if hasn't eip_old, to update neutron floatingIp self._neutron_update_floating_ip(allocation_id, - None, - neutron_ignore_not_found) - - return server + None) def _handle_update_eipInfo(self, prop_diff): eip_update = prop_diff.get(self.EIP) @@ -297,13 +269,12 @@ instance_id = self.properties[self.INSTANCE_ID] ni_id = self.properties[self.NETWORK_INTERFACE_ID] if eip_update: - server = self._floatingIp_detach(neutron_ignore_not_found=True) - if server: - # then to attach the eip_update to the instance - server.add_floating_ip(eip_update) - self.resource_id_set(eip_update) + self._floatingIp_detach() + self.client_plugin().associate_floatingip_address(instance_id, + eip_update) + self.resource_id_set(eip_update) elif allocation_id_update: - self._floatingIp_detach(nova_ignore_not_found=True) + self._floatingIp_detach() port_id, port_rsrc = self._get_port_info(ni_id, instance_id) if not port_id or not port_rsrc: LOG.error('Port not specified.') @@ -323,8 +294,8 @@ # if update portInfo, no need to detach the port from # old instance/floatingip. if eip: - server = self.client().servers.get(instance_id_update) - server.add_floating_ip(eip) + self.client_plugin().associate_floatingip_address( + instance_id_update, eip) else: port_id, port_rsrc = self._get_port_info(ni_id_update, instance_id_update) @@ -339,15 +310,15 @@ def handle_create(self): """Add a floating IP address to a server.""" - if self.properties[self.EIP]: - server = self.client().servers.get( - self.properties[self.INSTANCE_ID]) - server.add_floating_ip(self.properties[self.EIP]) - self.resource_id_set(self.properties[self.EIP]) + eip = self.properties[self.EIP] + if eip: + self.client_plugin().associate_floatingip_address( + self.properties[self.INSTANCE_ID], eip) + self.resource_id_set(eip) LOG.debug('ElasticIpAssociation ' '%(instance)s.add_floating_ip(%(eip)s)', {'instance': self.properties[self.INSTANCE_ID], - 'eip': self.properties[self.EIP]}) + 'eip': eip}) elif self.properties[self.ALLOCATION_ID]: ni_id = self.properties[self.NETWORK_INTERFACE_ID] instance_id = self.properties[self.INSTANCE_ID] @@ -370,11 +341,9 @@ return if self.properties[self.EIP]: - instance_id = self.properties[self.INSTANCE_ID] eip = self.properties[self.EIP] - self._nova_remove_floating_ip(instance_id, - eip, - ignore_not_found=True) + self._remove_floating_ip_address(eip, + ignore_not_found=True) elif self.properties[self.ALLOCATION_ID]: float_id = self.properties[self.ALLOCATION_ID] self._neutron_update_floating_ip(float_id, diff -Nru heat-10.0.1/heat/engine/resources/openstack/heat/software_deployment.py heat-10.0.2/heat/engine/resources/openstack/heat/software_deployment.py --- heat-10.0.1/heat/engine/resources/openstack/heat/software_deployment.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/openstack/heat/software_deployment.py 2018-09-04 20:02:49.000000000 +0000 @@ -101,7 +101,7 @@ DEPLOY_USERNAME, DEPLOY_PASSWORD, DEPLOY_PROJECT_ID, DEPLOY_USER_ID, DEPLOY_SIGNAL_VERB, DEPLOY_SIGNAL_TRANSPORT, - DEPLOY_QUEUE_ID + DEPLOY_QUEUE_ID, DEPLOY_REGION_NAME ) = ( 'deploy_server_id', 'deploy_action', 'deploy_signal_id', 'deploy_stack_id', @@ -109,7 +109,7 @@ 'deploy_username', 'deploy_password', 'deploy_project_id', 'deploy_user_id', 'deploy_signal_verb', 'deploy_signal_transport', - 'deploy_queue_id' + 'deploy_queue_id', 'deploy_region_name' ) SIGNAL_TRANSPORTS = ( @@ -416,7 +416,9 @@ yield swc_io.InputConfig( name=self.DEPLOY_PROJECT_ID, value=creds['project_id'], description=_('ID of project for API authentication')) - + yield swc_io.InputConfig( + name=self.DEPLOY_REGION_NAME, value=creds['region_name'], + description=_('Region name for API authentication')) if self._signal_transport_zaqar(): yield swc_io.InputConfig( name=self.DEPLOY_QUEUE_ID, diff -Nru heat-10.0.1/heat/engine/resources/openstack/neutron/port.py heat-10.0.2/heat/engine/resources/openstack/neutron/port.py --- heat-10.0.1/heat/engine/resources/openstack/neutron/port.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/openstack/neutron/port.py 2018-09-04 20:02:49.000000000 +0000 @@ -579,11 +579,13 @@ if self.resource_id is None: return # store port fixed_ips for restoring after failed update - fixed_ips = self._show_resource().get('fixed_ips', []) - self.data_set('port_fip', jsonutils.dumps(fixed_ips)) - # reset fixed_ips for this port by setting fixed_ips to [] - props = {'fixed_ips': []} - self.client().update_port(self.resource_id, {'port': props}) + # Ignore if the port does not exist in neutron (deleted) + with self.client_plugin().ignore_not_found: + fixed_ips = self._show_resource().get('fixed_ips', []) + self.data_set('port_fip', jsonutils.dumps(fixed_ips)) + # reset fixed_ips for this port by setting fixed_ips to [] + props = {'fixed_ips': []} + self.client().update_port(self.resource_id, {'port': props}) def restore_prev_rsrc(self, convergence=False): # In case of convergence, during rollback, the previous rsrc is diff -Nru heat-10.0.1/heat/engine/resources/openstack/nova/floatingip.py heat-10.0.2/heat/engine/resources/openstack/nova/floatingip.py --- heat-10.0.1/heat/engine/resources/openstack/nova/floatingip.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/openstack/nova/floatingip.py 2018-09-04 20:02:49.000000000 +0000 @@ -109,7 +109,6 @@ def handle_delete(self): with self.client_plugin('neutron').ignore_not_found: self.neutron().delete_floatingip(self.resource_id) - return True def _resolve_attribute(self, key): if self.resource_id is None: @@ -167,49 +166,29 @@ return self.physical_resource_name_or_FnGetRefId() def handle_create(self): - server = self.client().servers.get(self.properties[self.SERVER]) - fl_ip = self.neutron().show_floatingip( - self.properties[self.FLOATING_IP]) - - ip_address = fl_ip['floatingip']['floating_ip_address'] - self.client().servers.add_floating_ip(server, ip_address) + self.client_plugin().associate_floatingip( + self.properties[self.SERVER], self.properties[self.FLOATING_IP]) self.resource_id_set(self.id) def handle_delete(self): if self.resource_id is None: return - - try: - server = self.client().servers.get(self.properties[self.SERVER]) - if server: - fl_ip = self.neutron().show_floatingip( - self.properties[self.FLOATING_IP]) - ip_address = fl_ip['floatingip']['floating_ip_address'] - self.client().servers.remove_floating_ip(server, ip_address) - except Exception as e: - if not (self.client_plugin().is_not_found(e) - or self.client_plugin().is_conflict(e) - or self.client_plugin('neutron').is_not_found(e)): - raise + with self.client_plugin().ignore_not_found: + self.client_plugin().dissociate_floatingip( + self.properties[self.FLOATING_IP]) def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: # If floating_ip in prop_diff, we need to remove the old floating # ip from the old server, and then to add the new floating ip # to the old/new(if the server_id is changed) server. - # If prop_diff only has the server_id, no need to remove the - # floating ip from the old server, nova does this automatically - # when calling add_floating_ip(). if self.FLOATING_IP in prop_diff: self.handle_delete() server_id = (prop_diff.get(self.SERVER) or self.properties[self.SERVER]) fl_ip_id = (prop_diff.get(self.FLOATING_IP) or self.properties[self.FLOATING_IP]) - server = self.client().servers.get(server_id) - fl_ip = self.neutron().show_floatingip(fl_ip_id) - ip_address = fl_ip['floatingip']['floating_ip_address'] - self.client().servers.add_floating_ip(server, ip_address) + self.client_plugin().associate_floatingip(server_id, fl_ip_id) self.resource_id_set(self.id) diff -Nru heat-10.0.1/heat/engine/resources/openstack/nova/server_network_mixin.py heat-10.0.2/heat/engine/resources/openstack/nova/server_network_mixin.py --- heat-10.0.1/heat/engine/resources/openstack/nova/server_network_mixin.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/openstack/nova/server_network_mixin.py 2018-09-04 20:02:40.000000000 +0000 @@ -579,7 +579,23 @@ port=port['id'], server=prev_server_id) def prepare_ports_for_replace(self): - self.detach_ports(self) + # Check that the interface can be detached + server = None + # TODO(TheJulia): Once Story #2002001 is underway, + # we should be able to replace the query to nova and + # the check for the failed status with just a check + # to see if the resource has failed. + with self.client_plugin().ignore_not_found: + server = self.client().servers.get(self.resource_id) + if server and server.status != 'ERROR': + self.detach_ports(self) + else: + # If we are replacing an ERROR'ed node, we need to delete + # internal ports that we have created, otherwise we can + # encounter deployment issues with duplicate internal + # port data attempting to be created in instances being + # deployed. + self._delete_internal_ports() def restore_ports_after_rollback(self, convergence): # In case of convergence, during rollback, the previous rsrc is diff -Nru heat-10.0.1/heat/engine/resources/server_base.py heat-10.0.2/heat/engine/resources/server_base.py --- heat-10.0.1/heat/engine/resources/server_base.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/server_base.py 2018-09-04 20:02:40.000000000 +0000 @@ -67,6 +67,8 @@ occ = meta['os-collect-config'] collectors = list(self.default_collectors) occ['collectors'] = collectors + region_name = (self.context.region_name or + cfg.CONF.region_name_for_services) # set existing values to None to override any boot-time config occ_keys = ('heat', 'zaqar', 'cfn', 'request') @@ -85,7 +87,8 @@ 'auth_url': self.context.auth_url, 'project_id': self.stack.stack_user_project_id, 'stack_id': self.stack.identifier().stack_path(), - 'resource_name': self.name}}) + 'resource_name': self.name, + 'region_name': region_name}}) collectors.append('heat') elif self.transport_zaqar_message(props): @@ -95,7 +98,8 @@ 'password': self.password, 'auth_url': self.context.auth_url, 'project_id': self.stack.stack_user_project_id, - 'queue_id': queue_id}}) + 'queue_id': queue_id, + 'region_name': region_name}}) collectors.append('zaqar') elif self.transport_poll_server_cfn(props): diff -Nru heat-10.0.1/heat/engine/resources/signal_responder.py heat-10.0.2/heat/engine/resources/signal_responder.py --- heat-10.0.1/heat/engine/resources/signal_responder.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/resources/signal_responder.py 2018-09-04 20:02:40.000000000 +0000 @@ -108,7 +108,9 @@ 'user_id': self._get_user_id(), 'password': self.password, 'project_id': self.stack.stack_user_project_id, - 'domain_id': self.keystone().stack_domain_id} + 'domain_id': self.keystone().stack_domain_id, + 'region_name': (self.context.region_name or + cfg.CONF.region_name_for_services)} def _get_ec2_signed_url(self, signal_type=SIGNAL): """Create properly formatted and pre-signed URL. diff -Nru heat-10.0.1/heat/engine/service.py heat-10.0.2/heat/engine/service.py --- heat-10.0.1/heat/engine/service.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/service.py 2018-09-04 20:02:49.000000000 +0000 @@ -184,6 +184,11 @@ stack.ROLLBACK, stack.UPDATE)): stack.persist_state_and_release_lock(lock.engine_id) + + notify = kwargs.get('notify') + if notify is not None: + assert not notify.signalled() + notify.signal() else: lock.release() @@ -243,6 +248,38 @@ msg_queue.put_nowait(message) +class NotifyEvent(object): + def __init__(self): + self._queue = eventlet.queue.LightQueue(1) + self._signalled = False + + def signalled(self): + return self._signalled + + def signal(self): + """Signal the event.""" + if self._signalled: + return + self._signalled = True + + self._queue.put(None) + # Yield control so that the waiting greenthread will get the message + # as soon as possible, so that the API handler can respond to the user. + # Another option would be to set the queue length to 0 (which would + # cause put() to block until the event has been seen, but many unit + # tests run in a single greenthread and would thus deadlock. + eventlet.sleep(0) + + def wait(self): + """Wait for the event.""" + try: + # There's no timeout argument to eventlet.event.Event available + # until eventlet 0.22.1, so use a queue. + self._queue.get(timeout=cfg.CONF.rpc_response_timeout) + except eventlet.queue.Empty: + LOG.warning('Timed out waiting for operation to start') + + @profiler.trace_cls("rpc") class EngineListener(object): """Listen on an AMQP queue named for the engine. @@ -978,14 +1015,17 @@ new_stack=updated_stack) else: msg_queue = eventlet.queue.LightQueue() + stored_event = NotifyEvent() th = self.thread_group_mgr.start_with_lock(cnxt, current_stack, self.engine_id, current_stack.update, updated_stack, - msg_queue=msg_queue) + msg_queue=msg_queue, + notify=stored_event) th.link(self.thread_group_mgr.remove_msg_queue, current_stack.id, msg_queue) self.thread_group_mgr.add_msg_queue(current_stack.id, msg_queue) + stored_event.wait() return dict(current_stack.identifier()) @context.request_context @@ -1350,15 +1390,19 @@ self.resource_enforcer.enforce_stack(stack, is_registered_policy=True) if stack.convergence and cfg.CONF.convergence_engine: - def convergence_delete(): - stack.thread_group_mgr = self.thread_group_mgr + stack.thread_group_mgr = self.thread_group_mgr + template = templatem.Template.create_empty_template( + from_template=stack.t) + + # stop existing traversal; mark stack as FAILED + if stack.status == stack.IN_PROGRESS: + self.worker_service.stop_traversal(stack) + + def stop_workers(): self.worker_service.stop_all_workers(stack) - stack.delete_all_snapshots() - template = templatem.Template.create_empty_template( - from_template=stack.t) - stack.converge_stack(template=template, action=stack.DELETE) - self.thread_group_mgr.start(stack.id, convergence_delete) + stack.converge_stack(template=template, action=stack.DELETE, + pre_converge=stop_workers) return lock = stack_lock.StackLock(cnxt, stack.id, self.engine_id) @@ -1367,8 +1411,11 @@ # Successfully acquired lock if acquire_result is None: self.thread_group_mgr.stop_timers(stack.id) + stored = NotifyEvent() self.thread_group_mgr.start_with_acquired_lock(stack, lock, - stack.delete) + stack.delete, + notify=stored) + stored.wait() return # Current engine has the lock @@ -1973,30 +2020,28 @@ @context.request_context def stack_suspend(self, cnxt, stack_identity): """Handle request to perform suspend action on a stack.""" - def _stack_suspend(stack): - LOG.debug("suspending stack %s", stack.name) - stack.suspend() - s = self._get_stack(cnxt, stack_identity) stack = parser.Stack.load(cnxt, stack=s) self.resource_enforcer.enforce_stack(stack, is_registered_policy=True) + stored_event = NotifyEvent() self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, - _stack_suspend, stack) + stack.suspend, + notify=stored_event) + stored_event.wait() @context.request_context def stack_resume(self, cnxt, stack_identity): """Handle request to perform a resume action on a stack.""" - def _stack_resume(stack): - LOG.debug("resuming stack %s", stack.name) - stack.resume() - s = self._get_stack(cnxt, stack_identity) stack = parser.Stack.load(cnxt, stack=s) self.resource_enforcer.enforce_stack(stack, is_registered_policy=True) + stored_event = NotifyEvent() self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, - _stack_resume, stack) + stack.resume, + notify=stored_event) + stored_event.wait() @context.request_context def stack_snapshot(self, cnxt, stack_identity, name): @@ -2068,15 +2113,13 @@ stack = parser.Stack.load(cnxt, stack=s) LOG.info("Checking stack %s", stack.name) + stored_event = NotifyEvent() self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, - stack.check) + stack.check, notify=stored_event) + stored_event.wait() @context.request_context def stack_restore(self, cnxt, stack_identity, snapshot_id): - def _stack_restore(stack, snapshot): - LOG.debug("restoring stack %s", stack.name) - stack.restore(snapshot) - s = self._get_stack(cnxt, stack_identity) stack = parser.Stack.load(cnxt, stack=s) self.resource_enforcer.enforce_stack(stack, is_registered_policy=True) @@ -2092,8 +2135,11 @@ action=stack.RESTORE, new_stack=new_stack) else: + stored_event = NotifyEvent() self.thread_group_mgr.start_with_lock( - cnxt, stack, self.engine_id, _stack_restore, stack, snapshot) + cnxt, stack, self.engine_id, stack.restore, snapshot, + notify=stored_event) + stored_event.wait() @context.request_context def stack_list_snapshots(self, cnxt, stack_identity): @@ -2207,14 +2253,30 @@ parent_stack = parser.Stack.load(ctxt, stack_id=stack_id, show_deleted=False) + + if parent_stack.owner_id is not None: + msg = _("Migration of nested stack %s") % stack_id + raise exception.NotSupported(feature=msg) + + if parent_stack.status != parent_stack.COMPLETE: + raise exception.ActionNotComplete(stack_name=parent_stack.name, + action=parent_stack.action) + if parent_stack.convergence: LOG.info("Convergence was already enabled for stack %s", stack_id) return + db_stacks = stack_object.Stack.get_all_by_root_owner_id( ctxt, parent_stack.id) stacks = [parser.Stack.load(ctxt, stack_id=st.id, stack=st) for st in db_stacks] + + # check if any of the nested stacks is in IN_PROGRESS/FAILED state + for stack in stacks: + if stack.status != stack.COMPLETE: + raise exception.ActionNotComplete(stack_name=stack.name, + action=stack.action) stacks.append(parent_stack) locks = [] try: diff -Nru heat-10.0.1/heat/engine/stack.py heat-10.0.2/heat/engine/stack.py --- heat-10.0.1/heat/engine/stack.py 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/heat/engine/stack.py 2018-09-04 20:02:49.000000000 +0000 @@ -302,16 +302,18 @@ return {n: self.defn.output_definition(n) for n in self.defn.enabled_output_names()} + def _resources_for_defn(self, stack_defn): + return { + name: resource.Resource(name, + stack_defn.resource_definition(name), + self) + for name in stack_defn.enabled_rsrc_names() + } + @property def resources(self): if self._resources is None: - self._resources = { - name: resource.Resource(name, - self.defn.resource_definition(name), - self) - for name in self.defn.enabled_rsrc_names() - } - + self._resources = self._resources_for_defn(self.defn) return self._resources def _update_all_resource_data(self, for_resources, for_outputs): @@ -1114,7 +1116,8 @@ @scheduler.wrappertask def stack_task(self, action, reverse=False, post_func=None, - aggregate_exceptions=False, pre_completion_func=None): + aggregate_exceptions=False, pre_completion_func=None, + notify=None): """A task to perform an action on the stack. All of the resources are traversed in forward or reverse dependency @@ -1138,9 +1141,13 @@ 'Failed stack pre-ops: %s' % six.text_type(e)) if callable(post_func): post_func() + # No need to call notify.signal(), because persistence of the + # state is always deferred here. return self.state_set(action, self.IN_PROGRESS, 'Stack %s started' % action) + if notify is not None: + notify.signal() stack_status = self.COMPLETE reason = 'Stack %s completed successfully' % action @@ -1199,12 +1206,13 @@ @profiler.trace('Stack.check', hide_args=False) @reset_state_on_error - def check(self): + def check(self, notify=None): self.updated_time = oslo_timeutils.utcnow() checker = scheduler.TaskRunner( self.stack_task, self.CHECK, post_func=self.supports_check_action, - aggregate_exceptions=True) + aggregate_exceptions=True, + notify=notify) checker() def supports_check_action(self): @@ -1272,7 +1280,7 @@ @profiler.trace('Stack.update', hide_args=False) @reset_state_on_error - def update(self, newstack, msg_queue=None): + def update(self, newstack, msg_queue=None, notify=None): """Update the stack. Compare the current stack with newstack, @@ -1287,11 +1295,12 @@ """ self.updated_time = oslo_timeutils.utcnow() updater = scheduler.TaskRunner(self.update_task, newstack, - msg_queue=msg_queue) + msg_queue=msg_queue, notify=notify) updater() @profiler.trace('Stack.converge_stack', hide_args=False) - def converge_stack(self, template, action=UPDATE, new_stack=None): + def converge_stack(self, template, action=UPDATE, new_stack=None, + pre_converge=None): """Update the stack template and trigger convergence for resources.""" if action not in [self.CREATE, self.ADOPT]: # no back-up template for create action @@ -1345,9 +1354,10 @@ # TODO(later): lifecycle_plugin_utils.do_pre_ops - self.thread_group_mgr.start(self.id, self._converge_create_or_update) + self.thread_group_mgr.start(self.id, self._converge_create_or_update, + pre_converge=pre_converge) - def _converge_create_or_update(self): + def _converge_create_or_update(self, pre_converge=None): current_resources = self._update_or_store_resources() self._compute_convg_dependencies(self.ext_rsrcs_db, self.dependencies, current_resources) @@ -1364,6 +1374,16 @@ 'action': self.action}) return + if callable(pre_converge): + pre_converge() + if self.action == self.DELETE: + try: + self.delete_all_snapshots() + except Exception as exc: + self.state_set(self.action, self.FAILED, six.text_type(exc)) + self.purge_db() + return + LOG.info('convergence_dependencies: %s', self.convergence_dependencies) @@ -1521,11 +1541,14 @@ self.state_set(self.action, self.FAILED, six.text_type(reason)) @scheduler.wrappertask - def update_task(self, newstack, action=UPDATE, msg_queue=None): + def update_task(self, newstack, action=UPDATE, + msg_queue=None, notify=None): if action not in (self.UPDATE, self.ROLLBACK, self.RESTORE): LOG.error("Unexpected action %s passed to update!", action) self.state_set(self.UPDATE, self.FAILED, "Invalid action %s" % action) + if notify is not None: + notify.signal() return try: @@ -1534,6 +1557,8 @@ except Exception as e: self.state_set(action, self.FAILED, e.args[0] if e.args else 'Failed stack pre-ops: %s' % six.text_type(e)) + if notify is not None: + notify.signal() return if self.status == self.IN_PROGRESS: if action == self.ROLLBACK: @@ -1542,6 +1567,8 @@ reason = _('Attempted to %s an IN_PROGRESS ' 'stack') % action self.reset_stack_and_resources_in_progress(reason) + if notify is not None: + notify.signal() return # Save a copy of the new template. To avoid two DB writes @@ -1555,6 +1582,10 @@ self.status_reason = 'Stack %s started' % action self._send_notification_and_add_event() self.store() + # Notify the caller that the state is stored + if notify is not None: + notify.signal() + if prev_tmpl_id is not None: raw_template_object.RawTemplate.delete(self.context, prev_tmpl_id) @@ -1822,7 +1853,7 @@ @profiler.trace('Stack.delete', hide_args=False) @reset_state_on_error - def delete(self, action=DELETE, backup=False, abandon=False): + def delete(self, action=DELETE, backup=False, abandon=False, notify=None): """Delete all of the resources, and then the stack itself. The action parameter is used to differentiate between a user @@ -1838,12 +1869,16 @@ LOG.error("Unexpected action %s passed to delete!", action) self.state_set(self.DELETE, self.FAILED, "Invalid action %s" % action) + if notify is not None: + notify.signal() return stack_status = self.COMPLETE reason = 'Stack %s completed successfully' % action self.state_set(action, self.IN_PROGRESS, 'Stack %s started' % action) + if notify is not None: + notify.signal() backup_stack = self._backup_stack(False) if backup_stack: @@ -1907,7 +1942,7 @@ @profiler.trace('Stack.suspend', hide_args=False) @reset_state_on_error - def suspend(self): + def suspend(self, notify=None): """Suspend the stack. Invokes handle_suspend for all stack resources. @@ -1918,6 +1953,7 @@ other than move to SUSPEND_COMPLETE, so the resources must implement handle_suspend for this to have any effect. """ + LOG.debug("Suspending stack %s", self) # No need to suspend if the stack has been suspended if self.state == (self.SUSPEND, self.COMPLETE): LOG.info('%s is already suspended', self) @@ -1927,12 +1963,13 @@ sus_task = scheduler.TaskRunner( self.stack_task, action=self.SUSPEND, - reverse=True) + reverse=True, + notify=notify) sus_task(timeout=self.timeout_secs()) @profiler.trace('Stack.resume', hide_args=False) @reset_state_on_error - def resume(self): + def resume(self, notify=None): """Resume the stack. Invokes handle_resume for all stack resources. @@ -1943,6 +1980,7 @@ other than move to RESUME_COMPLETE, so the resources must implement handle_resume for this to have any effect. """ + LOG.debug("Resuming stack %s", self) # No need to resume if the stack has been resumed if self.state == (self.RESUME, self.COMPLETE): LOG.info('%s is already resumed', self) @@ -1952,7 +1990,8 @@ sus_task = scheduler.TaskRunner( self.stack_task, action=self.RESUME, - reverse=False) + reverse=False, + notify=notify) sus_task(timeout=self.timeout_secs()) @profiler.trace('Stack.snapshot', hide_args=False) @@ -1974,21 +2013,28 @@ self.delete_snapshot(snapshot) snapshot_object.Snapshot.delete(self.context, snapshot.id) + @staticmethod + def _template_from_snapshot_data(snapshot_data): + env = environment.Environment(snapshot_data['environment']) + files = snapshot_data['files'] + return tmpl.Template(snapshot_data['template'], env=env, files=files) + @profiler.trace('Stack.delete_snapshot', hide_args=False) def delete_snapshot(self, snapshot): """Remove a snapshot from the backends.""" - for name, rsrc in six.iteritems(self.resources): - snapshot_data = snapshot.data - if snapshot_data: + snapshot_data = snapshot.data + if snapshot_data: + template = self._template_from_snapshot_data(snapshot_data) + ss_defn = self.defn.clone_with_new_template(template, + self.identifier()) + resources = self._resources_for_defn(ss_defn) + for name, rsrc in six.iteritems(resources): data = snapshot.data['resources'].get(name) if data: scheduler.TaskRunner(rsrc.delete_snapshot, data)() def restore_data(self, snapshot): - env = environment.Environment(snapshot.data['environment']) - files = snapshot.data['files'] - template = tmpl.Template(snapshot.data['template'], - env=env, files=files) + template = self._template_from_snapshot_data(snapshot.data) newstack = self.__class__(self.context, self.name, template, timeout_mins=self.timeout_mins, disable_rollback=self.disable_rollback) @@ -2007,16 +2053,17 @@ return newstack, template @reset_state_on_error - def restore(self, snapshot): + def restore(self, snapshot, notify=None): """Restore the given snapshot. Invokes handle_restore on all resources. """ + LOG.debug("Restoring stack %s", self) self.updated_time = oslo_timeutils.utcnow() newstack = self.restore_data(snapshot)[0] updater = scheduler.TaskRunner(self.update_task, newstack, - action=self.RESTORE) + action=self.RESTORE, notify=notify) updater() def get_availability_zones(self): diff -Nru heat-10.0.1/heat/engine/translation.py heat-10.0.2/heat/engine/translation.py --- heat-10.0.1/heat/engine/translation.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/translation.py 2018-09-04 20:02:40.000000000 +0000 @@ -13,10 +13,15 @@ import functools +from oslo_log import log as logging +import six + from heat.common import exception from heat.common.i18n import _ from heat.engine import function +LOG = logging.getLogger(__name__) + @functools.total_ordering class TranslationRule(object): @@ -159,15 +164,18 @@ self.resolved_translations = {} self.is_active = True self.store_translated_values = True + self._ignore_resolve_error = False self._deleted_props = [] self._replaced_props = [] - def set_rules(self, rules, client_resolve=True): + def set_rules(self, rules, client_resolve=True, + ignore_resolve_error=False): if not rules: return self._rules = {} self.store_translated_values = client_resolve + self._ignore_resolve_error = ignore_resolve_error for rule in rules: if not client_resolve and rule.rule == TranslationRule.RESOLVE: continue @@ -220,7 +228,8 @@ resolved_value = resolve_and_find(result, rule.client_plugin, rule.finder, - rule.entity) + rule.entity, + self._ignore_resolve_error) if self.store_translated_values: self.resolved_translations[key] = resolved_value result = resolved_value @@ -322,7 +331,8 @@ return get_value(path[1:], prop) -def resolve_and_find(value, cplugin, finder, entity=None): +def resolve_and_find(value, cplugin, finder, entity=None, + ignore_resolve_error=False): if isinstance(value, function.Function): value = function.resolve(value) if value: @@ -332,10 +342,18 @@ resolved_value.append(resolve_and_find(item, cplugin, finder, - entity)) + entity, + ignore_resolve_error)) return resolved_value finder = getattr(cplugin, finder) - if entity: - return finder(entity, value) - else: - return finder(value) + try: + if entity: + return finder(entity, value) + else: + return finder(value) + except Exception as ex: + if ignore_resolve_error: + LOG.info("Ignoring error in RESOLVE translation: %s", + six.text_type(ex)) + return value + raise diff -Nru heat-10.0.1/heat/engine/worker.py heat-10.0.2/heat/engine/worker.py --- heat-10.0.1/heat/engine/worker.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/engine/worker.py 2018-09-04 20:02:40.000000000 +0000 @@ -125,11 +125,11 @@ _stop_traversal(child) def stop_all_workers(self, stack): - # stop the traversal - if stack.status == stack.IN_PROGRESS: - self.stop_traversal(stack) + """Cancel all existing worker threads for the stack. - # cancel existing workers + Threads will stop running at their next yield point, whether or not the + resource operations are complete. + """ cancelled = _cancel_workers(stack, self.thread_group_mgr, self.engine_id, self._rpc_client) if not cancelled: diff -Nru heat-10.0.1/heat/tests/aws/test_eip.py heat-10.0.2/heat/tests/aws/test_eip.py --- heat-10.0.1/heat/tests/aws/test_eip.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/aws/test_eip.py 2018-09-04 20:02:49.000000000 +0000 @@ -14,6 +14,7 @@ import copy import mock +from neutronclient.common import exceptions as q_exceptions from neutronclient.v2_0 import client as neutronclient import six @@ -175,6 +176,8 @@ self.m.StubOutWithMock(neutronclient.Client, 'list_networks') self.m.StubOutWithMock(self.fc.servers, 'get') self.m.StubOutWithMock(neutronclient.Client, + 'list_floatingips') + self.m.StubOutWithMock(neutronclient.Client, 'create_floatingip') self.m.StubOutWithMock(neutronclient.Client, 'show_floatingip') @@ -183,7 +186,22 @@ self.m.StubOutWithMock(neutronclient.Client, 'delete_floatingip') + def mock_interface(self, port, ip): + class MockIface(object): + def __init__(self, port_id, fixed_ip): + self.port_id = port_id + self.fixed_ips = [{'ip_address': fixed_ip}] + + return MockIface(port, ip) + + def mock_list_floatingips(self): + neutronclient.Client.list_floatingips( + floating_ip_address='11.0.0.1').AndReturn({ + 'floatingips': [{'id': + "fc68ea2c-b60b-4b4f-bd82-94ec81110766"}]}) + def mock_create_floatingip(self): + nova.NovaClientPlugin._create().AndReturn(self.fc) neutronclient.Client.list_networks( **{'router:external': True}).AndReturn({'networks': [{ 'status': 'ACTIVE', @@ -217,6 +235,22 @@ 'id': 'ffff' }}) + def mock_update_floatingip(self, + fip='fc68ea2c-b60b-4b4f-bd82-94ec81110766', + delete_assc=False): + if delete_assc: + request_body = { + 'floatingip': { + 'port_id': None, + 'fixed_ip_address': None}} + else: + request_body = { + 'floatingip': { + 'port_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'fixed_ip_address': '1.2.3.4'}} + neutronclient.Client.update_floatingip( + fip, request_body).AndReturn(None) + def mock_delete_floatingip(self): id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' neutronclient.Client.delete_floatingip(id).AndReturn(None) @@ -245,23 +279,18 @@ rsrc.node_data()) return rsrc - def _mock_server_get(self, server='WebServer', mock_server=None, - multiple=False, mock_again=False): - if not mock_again: - nova.NovaClientPlugin._create().AndReturn(self.fc) - if multiple: - self.fc.servers.get(server).MultipleTimes().AndReturn( - mock_server) - else: - self.fc.servers.get(server).AndReturn(mock_server) - def test_eip(self): mock_server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=mock_server) - self._mock_server_get(mock_again=True) + self.patchobject(self.fc.servers, 'get', + return_value=mock_server) self.mock_create_floatingip() + self.mock_update_floatingip() + self.mock_update_floatingip(delete_assc=True) self.mock_delete_floatingip() self.m.ReplayAll() + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(mock_server, 'interface_list', return_value=[iface]) t = template_format.parse(eip_template) stack = utils.parse_stack(t) @@ -285,13 +314,18 @@ self.m.VerifyAll() def test_eip_update(self): - server_old = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server_old) - - server_update = self.fc.servers.list()[1] - self._mock_server_get(server='5678', mock_server=server_update, - multiple=True, mock_again=True) + mock_server = self.fc.servers.list()[0] + self.patchobject(self.fc.servers, 'get', + return_value=mock_server) self.mock_create_floatingip() + self.mock_update_floatingip() + self.mock_update_floatingip() + self.mock_update_floatingip(delete_assc=True) + + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(mock_server, 'interface_list', return_value=[iface]) + self.m.ReplayAll() t = template_format.parse(eip_template) stack = utils.parse_stack(t) @@ -299,6 +333,13 @@ rsrc = self.create_eip(t, stack, 'IPAddress') self.assertEqual('11.0.0.1', rsrc.FnGetRefId()) # update with the new InstanceId + server_update = self.fc.servers.list()[1] + self.patchobject(self.fc.servers, 'get', + return_value=server_update) + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(server_update, 'interface_list', return_value=[iface]) + props = copy.deepcopy(rsrc.properties.data) update_server_id = '5678' props['InstanceId'] = update_server_id @@ -317,12 +358,20 @@ self.m.VerifyAll() def test_association_eip(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) - + mock_server = self.fc.servers.list()[0] + self.patchobject(self.fc.servers, 'get', + return_value=mock_server) self.mock_create_floatingip() - self.mock_delete_floatingip() self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') + self.mock_update_floatingip() + self.mock_list_floatingips() + self.mock_list_floatingips() + self.mock_update_floatingip(delete_assc=True) + self.mock_delete_floatingip() + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(mock_server, 'interface_list', return_value=[iface]) + self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -425,6 +474,8 @@ self.m.StubOutWithMock(neutronclient.Client, 'update_floatingip') self.m.StubOutWithMock(neutronclient.Client, + 'list_floatingips') + self.m.StubOutWithMock(neutronclient.Client, 'delete_floatingip') self.m.StubOutWithMock(neutronclient.Client, 'add_gateway_router') @@ -435,6 +486,14 @@ self.m.StubOutWithMock(neutronclient.Client, 'remove_gateway_router') + def mock_interface(self, port, ip): + class MockIface(object): + def __init__(self, port_id, fixed_ip): + self.port_id = port_id + self.fixed_ips = [{'ip_address': fixed_ip}] + + return MockIface(port, ip) + def _setup_test_stack_validate(self, stack_name): t = template_format.parse(ipassoc_template_validate) template = tmpl.Template(t) @@ -469,15 +528,18 @@ "id": "22c26451-cf27-4d48-9031-51f5e397b84e" }}) - def _mock_server_get(self, server='WebServer', mock_server=None, - multiple=False, mock_again=False): - if not mock_again: - nova.NovaClientPlugin._create().AndReturn(self.fc) - if multiple: - self.fc.servers.get(server).MultipleTimes().AndReturn( - mock_server) - else: - self.fc.servers.get(server).AndReturn(mock_server) + def _mock_server(self, mock_interface=False, mock_server=None): + self.patchobject(nova.NovaClientPlugin, '_create', + return_value=self.fc) + if not mock_server: + mock_server = self.fc.servers.list()[0] + self.patchobject(self.fc.servers, 'get', + return_value=mock_server) + if mock_interface: + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(mock_server, + 'interface_list', return_value=[iface]) def create_eip(self, t, stack, resource_name): rsrc = eip.ElasticIp(resource_name, @@ -502,11 +564,6 @@ rsrc.node_data()) return rsrc - def mock_update_floatingip(self, port='the_nic'): - neutronclient.Client.update_floatingip( - 'fc68ea2c-b60b-4b4f-bd82-94ec81110766', - {'floatingip': {'port_id': port}}).AndReturn(None) - def mock_create_gateway_attachment(self): neutronclient.Client.add_gateway_router( 'bbbb', {'network_id': 'eeee'}).AndReturn(None) @@ -532,6 +589,29 @@ "floating_ip_address": "11.0.0.1" }}) + def mock_update_floatingip(self, + fip='fc68ea2c-b60b-4b4f-bd82-94ec81110766', + port_id='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + ex=None, + with_address=True, + delete_assc=False): + if delete_assc: + request_body = { + 'floatingip': {'port_id': None}} + if with_address: + request_body['floatingip']['fixed_ip_address'] = None + else: + request_body = { + 'floatingip': {'port_id': port_id}} + if with_address: + request_body['floatingip']['fixed_ip_address'] = '1.2.3.4' + if ex: + neutronclient.Client.update_floatingip( + fip, request_body).AndRaise(ex) + else: + neutronclient.Client.update_floatingip( + fip, request_body).AndReturn(None) + def mock_show_floatingip(self, refid): neutronclient.Client.show_floatingip( refid, @@ -545,6 +625,12 @@ 'id': 'ffff' }}) + def mock_list_floatingips(self, ip_addr='11.0.0.1'): + neutronclient.Client.list_floatingips( + floating_ip_address=ip_addr).AndReturn({ + 'floatingips': [{'id': + "fc68ea2c-b60b-4b4f-bd82-94ec81110766"}]}) + def mock_delete_floatingip(self): id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' neutronclient.Client.delete_floatingip(id).AndReturn(None) @@ -620,9 +706,10 @@ self.mock_list_ports() self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - self.mock_update_floatingip() + self.mock_update_floatingip(port_id='the_nic', + with_address=False) - self.mock_update_floatingip(port=None) + self.mock_update_floatingip(delete_assc=True, with_address=False) self.mock_delete_floatingip() self.m.ReplayAll() @@ -639,10 +726,7 @@ self.m.VerifyAll() def test_association_allocationid_with_instance(self): - server = self.fc.servers.list()[0] - self._mock_server_get(server='1fafbe59-2332-4f5f-bfa4-517b4d6c1b65', - mock_server=server, - multiple=True) + self._mock_server() self.mock_show_network() self.mock_create_floatingip() @@ -650,9 +734,10 @@ self.mock_no_router_for_vpc() self.mock_update_floatingip( - port='a000228d-b40b-4124-8394-a4082ae1b76c') + port_id='a000228d-b40b-4124-8394-a4082ae1b76c', + with_address=False) - self.mock_update_floatingip(port=None) + self.mock_update_floatingip(delete_assc=True, with_address=False) self.mock_delete_floatingip() self.m.ReplayAll() @@ -669,9 +754,9 @@ self.m.VerifyAll() def test_validate_properties_EIP_and_AllocationId(self): - self._mock_server_get(server='1fafbe59-2332-4f5f-bfa4-517b4d6c1b65', - multiple=True) + self._mock_server() self.m.ReplayAll() + template, stack = self._setup_test_stack_validate( stack_name='validate_EIP_AllocationId') @@ -689,8 +774,7 @@ self.m.VerifyAll() def test_validate_EIP_and_InstanceId(self): - self._mock_server_get(server='1fafbe59-2332-4f5f-bfa4-517b4d6c1b65', - multiple=True) + self._mock_server() self.m.ReplayAll() template, stack = self._setup_test_stack_validate( stack_name='validate_EIP_InstanceId') @@ -703,8 +787,7 @@ self.m.VerifyAll() def test_validate_without_NetworkInterfaceId_and_InstanceId(self): - self._mock_server_get(server='1fafbe59-2332-4f5f-bfa4-517b4d6c1b65', - multiple=True) + self._mock_server() self.m.ReplayAll() template, stack = self._setup_test_stack_validate( stack_name='validate_EIP_InstanceId') @@ -727,15 +810,12 @@ self.m.VerifyAll() def test_delete_association_successful_if_create_failed(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) - self.m.StubOutWithMock(self.fc.servers, 'add_floating_ip') - self.fc.servers.add_floating_ip(server, '11.0.0.1').AndRaise( - fakes_nova.fake_exception(400)) + self._mock_server(mock_interface=True) self.mock_create_floatingip() self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') + self.mock_list_floatingips() + self.mock_update_floatingip(ex=q_exceptions.NotFound('Not Found')) self.m.ReplayAll() - t = template_format.parse(eip_template_ipassoc) stack = utils.parse_stack(t) @@ -755,16 +835,12 @@ self.m.VerifyAll() def test_update_association_with_InstanceId(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) - - server_update = self.fc.servers.list()[1] - self._mock_server_get(server='5678', - mock_server=server_update, - multiple=True, - mock_again=True) - + self._mock_server(mock_interface=True) self.mock_create_floatingip() + self.mock_list_floatingips() + self.mock_update_floatingip() + self.mock_list_floatingips() + self.mock_update_floatingip() self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -772,7 +848,8 @@ self.create_eip(t, stack, 'IPAddress') ass = self.create_association(t, stack, 'IPAssoc') self.assertEqual('11.0.0.1', ass.properties['EIP']) - + server_update = self.fc.servers.list()[1] + self._mock_server(mock_interface=True, mock_server=server_update) # update with the new InstanceId props = copy.deepcopy(ass.properties.data) update_server_id = '5678' @@ -786,9 +863,14 @@ self.m.VerifyAll() def test_update_association_with_EIP(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) + self._mock_server(mock_interface=True) self.mock_create_floatingip() + self.mock_list_floatingips() + self.mock_update_floatingip() + self.mock_list_floatingips() + self.mock_update_floatingip(delete_assc=True) + self.mock_list_floatingips(ip_addr='11.0.0.2') + self.mock_update_floatingip() self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -809,17 +891,22 @@ self.m.VerifyAll() def test_update_association_with_AllocationId_or_EIP(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) + self._mock_server(mock_interface=True) self.mock_create_floatingip() - self.mock_list_instance_ports('WebServer') self.mock_show_network() self.mock_no_router_for_vpc() - self.mock_update_floatingip( - port='a000228d-b40b-4124-8394-a4082ae1b76c') + self.mock_list_floatingips() + self.mock_update_floatingip() - self.mock_update_floatingip(port=None) + self.mock_list_floatingips() + self.mock_update_floatingip(delete_assc=True) + self.mock_update_floatingip( + port_id='a000228d-b40b-4124-8394-a4082ae1b76c', + with_address=False) + self.mock_list_floatingips(ip_addr='11.0.0.2') + self.mock_update_floatingip(delete_assc=True, with_address=False) + self.mock_update_floatingip() self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -854,17 +941,11 @@ self.m.VerifyAll() def test_update_association_needs_update_InstanceId(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) + self._mock_server(mock_interface=True) self.mock_create_floatingip() + self.mock_list_floatingips() self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - - server_update = self.fc.servers.list()[1] - self._mock_server_get(server='5678', - mock_server=server_update, - multiple=True, - mock_again=True) - + self.mock_update_floatingip() self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -875,6 +956,8 @@ after_props = {'InstanceId': {'Ref': 'WebServer2'}, 'EIP': '11.0.0.1'} before = self.create_association(t, stack, 'IPAssoc') + update_server = self.fc.servers.list()[1] + self._mock_server(mock_interface=False, mock_server=update_server) after = rsrc_defn.ResourceDefinition(before.name, before.type(), after_props) self.assertTrue(resource.UpdateReplace, @@ -882,17 +965,11 @@ before_props, None)) def test_update_association_needs_update_InstanceId_EIP(self): - server = self.fc.servers.list()[0] - self._mock_server_get(mock_server=server, multiple=True) + self._mock_server(mock_interface=True) self.mock_create_floatingip() + self.mock_list_floatingips() self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - - server_update = self.fc.servers.list()[1] - self._mock_server_get(server='5678', - mock_server=server_update, - multiple=True, - mock_again=True) - + self.mock_update_floatingip() self.m.ReplayAll() t = template_format.parse(eip_template_ipassoc) @@ -901,6 +978,8 @@ after_props = {'InstanceId': '5678', 'EIP': '11.0.0.2'} before = self.create_association(t, stack, 'IPAssoc') + update_server = self.fc.servers.list()[1] + self._mock_server(mock_interface=False, mock_server=update_server) after = rsrc_defn.ResourceDefinition(before.name, before.type(), after_props) updater = scheduler.TaskRunner(before.update, after) @@ -911,21 +990,19 @@ self.mock_list_ports() self.mock_show_network() self.mock_no_router_for_vpc() - self.mock_update_floatingip() + self.mock_update_floatingip(port_id='the_nic', with_address=False) self.mock_list_ports(id='a000228d-b40b-4124-8394-a4082ae1b76b') self.mock_show_network() self.mock_no_router_for_vpc() self.mock_update_floatingip( - port='a000228d-b40b-4124-8394-a4082ae1b76b') + port_id='a000228d-b40b-4124-8394-a4082ae1b76b', with_address=False) - update_server = self.fc.servers.list()[0] - self._mock_server_get(server='5678', mock_server=update_server) self.mock_list_instance_ports('5678') self.mock_show_network() self.mock_no_router_for_vpc() self.mock_update_floatingip( - port='a000228d-b40b-4124-8394-a4082ae1b76c') + port_id='a000228d-b40b-4124-8394-a4082ae1b76c', with_address=False) self.m.ReplayAll() @@ -946,6 +1023,9 @@ self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) # update with the InstanceId + update_server = self.fc.servers.list()[1] + self._mock_server(mock_server=update_server) + props = copy.deepcopy(ass.properties.data) instance_id = '5678' props.pop('NetworkInterfaceId') diff -Nru heat-10.0.1/heat/tests/convergence/framework/fake_resource.py heat-10.0.2/heat/tests/convergence/framework/fake_resource.py --- heat-10.0.1/heat/tests/convergence/framework/fake_resource.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/convergence/framework/fake_resource.py 2018-09-04 20:02:40.000000000 +0000 @@ -24,9 +24,9 @@ class TestResource(resource.Resource): PROPERTIES = ( - A, C, CA, rA, rB + A, B, C, CA, rA, rB ) = ( - 'a', 'c', 'ca', '!a', '!b' + 'a', 'b', 'c', 'ca', '!a', '!b' ) ATTRIBUTES = ( @@ -42,6 +42,12 @@ default='a', update_allowed=True ), + B: properties.Schema( + properties.Schema.STRING, + _('Fake property b.'), + default='b', + update_allowed=True + ), C: properties.Schema( properties.Schema.STRING, _('Fake property c.'), diff -Nru heat-10.0.1/heat/tests/convergence/framework/worker_wrapper.py heat-10.0.2/heat/tests/convergence/framework/worker_wrapper.py --- heat-10.0.1/heat/tests/convergence/framework/worker_wrapper.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/convergence/framework/worker_wrapper.py 2018-09-04 20:02:40.000000000 +0000 @@ -37,5 +37,8 @@ adopt_stack_data, converge) + def stop_traversal(self, current_stack): + pass + def stop_all_workers(self, current_stack): pass diff -Nru heat-10.0.1/heat/tests/convergence/scenarios/update_user_replace_rollback_update.py heat-10.0.2/heat/tests/convergence/scenarios/update_user_replace_rollback_update.py --- heat-10.0.1/heat/tests/convergence/scenarios/update_user_replace_rollback_update.py 1970-01-01 00:00:00.000000000 +0000 +++ heat-10.0.2/heat/tests/convergence/scenarios/update_user_replace_rollback_update.py 2018-09-04 20:02:40.000000000 +0000 @@ -0,0 +1,54 @@ +# +# 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. + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a'), 'b': 'val1'}, []), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template_updated = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a'), 'b': 'val1'}, []), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.update_stack('foo', example_template_updated) +engine.noop(3) + +engine.rollback_stack('foo') +engine.noop(12) +engine.call(verify, example_template) + +example_template_final = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a'), 'b': 'val2'}, []), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) + +engine.update_stack('foo', example_template_final) +engine.noop(3) +engine.call(verify, example_template_final) +engine.noop(4) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff -Nru heat-10.0.1/heat/tests/db/test_sqlalchemy_api.py heat-10.0.2/heat/tests/db/test_sqlalchemy_api.py --- heat-10.0.1/heat/tests/db/test_sqlalchemy_api.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/db/test_sqlalchemy_api.py 2018-09-04 20:02:49.000000000 +0000 @@ -1363,6 +1363,7 @@ 'parameters': {}, 'user_creds_id': user_creds['id'], 'owner_id': None, + 'backup': False, 'timeout': '60', 'disable_rollback': 0, 'current_traversal': 'dummy-uuid', diff -Nru heat-10.0.1/heat/tests/engine/service/test_stack_action.py heat-10.0.2/heat/tests/engine/service/test_stack_action.py --- heat-10.0.1/heat/tests/engine/service/test_stack_action.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/engine/service/test_stack_action.py 2018-09-04 20:02:40.000000000 +0000 @@ -44,12 +44,14 @@ thread = mock.MagicMock() mock_link = self.patchobject(thread, 'link') mock_start.return_value = thread + self.patchobject(service, 'NotifyEvent') result = self.man.stack_suspend(self.ctx, stk.identifier()) self.assertIsNone(result) mock_load.assert_called_once_with(self.ctx, stack=s) mock_link.assert_called_once_with(mock.ANY) - mock_start.assert_called_once_with(stk.id, mock.ANY, stk) + mock_start.assert_called_once_with(stk.id, stk.suspend, + notify=mock.ANY) stk.delete() @@ -64,13 +66,14 @@ thread = mock.MagicMock() mock_link = self.patchobject(thread, 'link') mock_start.return_value = thread + self.patchobject(service, 'NotifyEvent') result = self.man.stack_resume(self.ctx, stk.identifier()) self.assertIsNone(result) mock_load.assert_called_once_with(self.ctx, stack=mock.ANY) mock_link.assert_called_once_with(mock.ANY) - mock_start.assert_called_once_with(stk.id, mock.ANY, stk) + mock_start.assert_called_once_with(stk.id, stk.resume, notify=mock.ANY) stk.delete() @@ -108,6 +111,7 @@ stk = utils.parse_stack(t, stack_name=stack_name) stk.check = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_load.return_value = stk mock_start.side_effect = self._mock_thread_start diff -Nru heat-10.0.1/heat/tests/engine/service/test_stack_events.py heat-10.0.2/heat/tests/engine/service/test_stack_events.py --- heat-10.0.1/heat/tests/engine/service/test_stack_events.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/engine/service/test_stack_events.py 2018-09-04 20:02:40.000000000 +0000 @@ -12,6 +12,8 @@ # under the License. import mock +from oslo_config import cfg +from oslo_messaging import conffixture from heat.engine import resource as res from heat.engine.resources.aws.ec2 import instance as instances @@ -94,6 +96,7 @@ @tools.stack_context('service_event_list_deleted_resource') @mock.patch.object(instances.Instance, 'handle_delete') def test_event_list_deleted_resource(self, mock_delete): + self.useFixture(conffixture.ConfFixture(cfg.CONF)) mock_delete.return_value = None res._register_class('GenericResourceType', @@ -103,7 +106,7 @@ thread.link = mock.Mock(return_value=None) def run(stack_id, func, *args, **kwargs): - func(*args) + func(*args, **kwargs) return thread self.eng.thread_group_mgr.start = run diff -Nru heat-10.0.1/heat/tests/engine/service/test_stack_update.py heat-10.0.2/heat/tests/engine/service/test_stack_update.py --- heat-10.0.1/heat/tests/engine/service/test_stack_update.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/engine/service/test_stack_update.py 2018-09-04 20:02:49.000000000 +0000 @@ -15,6 +15,7 @@ import eventlet.queue import mock from oslo_config import cfg +from oslo_messaging import conffixture from oslo_messaging.rpc import dispatcher import six @@ -43,6 +44,7 @@ def setUp(self): super(ServiceStackUpdateTest, self).setUp() + self.useFixture(conffixture.ConfFixture(cfg.CONF)) self.ctx = utils.dummy_context() self.man = service.EngineService('a-host', 'a-topic') self.man.thread_group_mgr = tools.DummyThreadGroupManager() @@ -68,7 +70,8 @@ mock_validate = self.patchobject(stk, 'validate', return_value=None) msgq_mock = mock.Mock() - self.patchobject(eventlet.queue, 'LightQueue', return_value=msgq_mock) + self.patchobject(eventlet.queue, 'LightQueue', + side_effect=[msgq_mock, eventlet.queue.LightQueue()]) # do update api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: True} @@ -122,7 +125,8 @@ self.patchobject(environment, 'Environment', return_value=stk.env) self.patchobject(stk, 'validate', return_value=None) self.patchobject(eventlet.queue, 'LightQueue', - return_value=mock.Mock()) + side_effect=[mock.Mock(), + eventlet.queue.LightQueue()]) mock_merge = self.patchobject(env_util, 'merge_environments') @@ -160,7 +164,8 @@ mock_validate = self.patchobject(stk, 'validate', return_value=None) msgq_mock = mock.Mock() - self.patchobject(eventlet.queue, 'LightQueue', return_value=msgq_mock) + self.patchobject(eventlet.queue, 'LightQueue', + side_effect=[msgq_mock, eventlet.queue.LightQueue()]) # do update api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False} @@ -219,6 +224,7 @@ t['parameters']['newparam'] = {'type': 'number'} with mock.patch('heat.engine.stack.Stack') as mock_stack: stk.update = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = stk mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stk.identifier(), @@ -275,7 +281,8 @@ rpc_api.PARAM_CONVERGE: False} with mock.patch('heat.engine.stack.Stack') as mock_stack: - stk.update = mock.Mock() + loaded_stack.update = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = loaded_stack mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stk.identifier(), @@ -318,6 +325,7 @@ t['parameters']['newparam'] = {'type': 'number'} with mock.patch('heat.engine.stack.Stack') as mock_stack: stk.update = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = stk mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stk.identifier(), @@ -418,6 +426,7 @@ 'myother.yaml': 'myother'} with mock.patch('heat.engine.stack.Stack') as mock_stack: stk.update = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = stk mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stk.identifier(), @@ -464,6 +473,7 @@ 'resource_registry': {'resources': {}}} with mock.patch('heat.engine.stack.Stack') as mock_stack: stk.update = mock.Mock() + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = stk mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stk.identifier(), @@ -864,6 +874,7 @@ stack.status = stack.COMPLETE with mock.patch('heat.engine.stack.Stack') as mock_stack: + self.patchobject(service, 'NotifyEvent') mock_stack.load.return_value = stack mock_stack.validate.return_value = None result = self.man.update_stack(self.ctx, stack.identifier(), diff -Nru heat-10.0.1/heat/tests/engine/test_check_resource.py heat-10.0.2/heat/tests/engine/test_check_resource.py --- heat-10.0.1/heat/tests/engine/test_check_resource.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/engine/test_check_resource.py 2018-09-04 20:02:49.000000000 +0000 @@ -15,6 +15,7 @@ import eventlet import mock +import uuid from oslo_config import cfg @@ -128,11 +129,11 @@ self.assertFalse(mock_pcr.called) self.assertFalse(mock_csc.called) - @mock.patch.object(check_resource.CheckResource, '_try_steal_engine_lock') + @mock.patch.object(check_resource.CheckResource, + '_stale_resource_needs_retry') @mock.patch.object(stack.Stack, 'time_remaining') - @mock.patch.object(resource.Resource, 'state_set') def test_is_update_traversal_raise_update_inprogress( - self, mock_ss, tr, mock_tsl, mock_cru, mock_crc, mock_pcr, + self, tr, mock_tsl, mock_cru, mock_crc, mock_pcr, mock_csc): mock_cru.side_effect = exception.UpdateInProgress self.worker.engine_id = 'some-thing-else' @@ -145,43 +146,64 @@ self.resource.stack.t.id, {}, self.worker.engine_id, mock.ANY, mock.ANY) - mock_ss.assert_called_once_with(self.resource.action, - resource.Resource.FAILED, - mock.ANY) self.assertFalse(mock_crc.called) self.assertFalse(mock_pcr.called) self.assertFalse(mock_csc.called) + @mock.patch.object(resource.Resource, 'state_set') + def test_stale_resource_retry( + self, mock_ss, mock_cru, mock_crc, mock_pcr, mock_csc): + current_template_id = self.resource.current_template_id + res = self.cr._stale_resource_needs_retry(self.ctx, + self.resource, + current_template_id) + self.assertTrue(res) + mock_ss.assert_not_called() + + @mock.patch.object(resource.Resource, 'state_set') def test_try_steal_lock_alive( - self, mock_cru, mock_crc, mock_pcr, mock_csc): - res = self.cr._try_steal_engine_lock(self.ctx, - self.resource.id) + self, mock_ss, mock_cru, mock_crc, mock_pcr, mock_csc): + res = self.cr._stale_resource_needs_retry(self.ctx, + self.resource, + str(uuid.uuid4())) self.assertFalse(res) + mock_ss.assert_not_called() @mock.patch.object(check_resource.listener_client, 'EngineListenerClient') @mock.patch.object(check_resource.resource_objects.Resource, 'get_obj') + @mock.patch.object(resource.Resource, 'state_set') def test_try_steal_lock_dead( - self, mock_get, mock_elc, mock_cru, mock_crc, mock_pcr, + self, mock_ss, mock_get, mock_elc, mock_cru, mock_crc, mock_pcr, mock_csc): fake_res = mock.Mock() fake_res.engine_id = 'some-thing-else' mock_get.return_value = fake_res mock_elc.return_value.is_alive.return_value = False - res = self.cr._try_steal_engine_lock(self.ctx, - self.resource.id) + current_template_id = self.resource.current_template_id + res = self.cr._stale_resource_needs_retry(self.ctx, + self.resource, + current_template_id) self.assertTrue(res) + mock_ss.assert_called_once_with(self.resource.action, + resource.Resource.FAILED, + mock.ANY) @mock.patch.object(check_resource.listener_client, 'EngineListenerClient') @mock.patch.object(check_resource.resource_objects.Resource, 'get_obj') + @mock.patch.object(resource.Resource, 'state_set') def test_try_steal_lock_not_dead( - self, mock_get, mock_elc, mock_cru, mock_crc, mock_pcr, + self, mock_ss, mock_get, mock_elc, mock_cru, mock_crc, mock_pcr, mock_csc): fake_res = mock.Mock() fake_res.engine_id = self.worker.engine_id mock_get.return_value = fake_res mock_elc.return_value.is_alive.return_value = True - res = self.cr._try_steal_engine_lock(self.ctx, self.resource.id) + current_template_id = self.resource.current_template_id + res = self.cr._stale_resource_needs_retry(self.ctx, + self.resource, + current_template_id) self.assertFalse(res) + mock_ss.assert_not_called() @mock.patch.object(check_resource.CheckResource, '_trigger_rollback') def test_resource_update_failure_sets_stack_state_as_failed( diff -Nru heat-10.0.1/heat/tests/engine/test_engine_worker.py heat-10.0.2/heat/tests/engine/test_engine_worker.py --- heat-10.0.1/heat/tests/engine/test_engine_worker.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/engine/test_engine_worker.py 2018-09-04 20:02:40.000000000 +0000 @@ -209,7 +209,7 @@ stack.id = 'stack_id' stack.rollback = mock.MagicMock() _worker.stop_all_workers(stack) - mock_st.assert_called_once_with(stack) + mock_st.assert_not_called() mock_cw.assert_called_once_with(stack, mock_tgm, 'engine-001', _worker._rpc_client) self.assertFalse(stack.rollback.called) diff -Nru heat-10.0.1/heat/tests/openstack/heat/test_deployed_server.py heat-10.0.2/heat/tests/openstack/heat/test_deployed_server.py --- heat-10.0.1/heat/tests/openstack/heat/test_deployed_server.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/openstack/heat/test_deployed_server.py 2018-09-04 20:02:40.000000000 +0000 @@ -136,7 +136,8 @@ def _setup_test_stack(self, stack_name, test_templ=ds_tmpl): t = template_format.parse(test_templ) tmpl = template.Template(t, env=environment.Environment()) - stack = parser.Stack(utils.dummy_context(), stack_name, tmpl, + stack = parser.Stack(utils.dummy_context(region_name="RegionOne"), + stack_name, tmpl, stack_id=uuidutils.generate_uuid(), stack_user_project_id='8888') return (tmpl, stack) @@ -476,6 +477,7 @@ 'auth_url': 'http://server.test:5000/v2.0', 'password': server.password, 'project_id': '8888', + 'region_name': 'RegionOne', 'resource_name': 'server', 'stack_id': 'server_heat_s/%s' % stack.id, 'user_id': '1234' @@ -507,6 +509,7 @@ 'auth_url': 'http://server.test:5000/v2.0', 'password': server.password, 'project_id': '8888', + 'region_name': 'RegionOne', 'resource_name': 'server', 'stack_id': 'server_heat_s/%s' % stack.id, 'user_id': '1234' @@ -566,6 +569,7 @@ 'password': server.password, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', + 'region_name': 'RegionOne', 'queue_id': queue_id }, 'collectors': ['zaqar', 'local'] @@ -583,6 +587,7 @@ 'password': server.password, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', + 'region_name': 'RegionOne', 'queue_id': queue_id }, 'collectors': ['zaqar', 'local'], diff -Nru heat-10.0.1/heat/tests/openstack/neutron/test_neutron_port.py heat-10.0.2/heat/tests/openstack/neutron/test_neutron_port.py --- heat-10.0.1/heat/tests/openstack/neutron/test_neutron_port.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/openstack/neutron/test_neutron_port.py 2018-09-04 20:02:49.000000000 +0000 @@ -681,6 +681,25 @@ self.assertFalse(port.data_set.called) self.assertFalse(n_client.update_port.called) + def test_prepare_for_replace_port_not_found(self): + t = template_format.parse(neutron_port_template) + stack = utils.parse_stack(t) + port = stack['port'] + port.resource_id = 'test_res_id' + port._show_resource = mock.Mock(side_effect=qe.NotFound) + port.data_set = mock.Mock() + n_client = mock.Mock() + port.client = mock.Mock(return_value=n_client) + + # execute prepare_for_replace + port.prepare_for_replace() + + # check, if the port is not found, do nothing in + # prepare_for_replace() + self.assertTrue(port._show_resource.called) + self.assertFalse(port.data_set.called) + self.assertFalse(n_client.update_port.called) + def test_prepare_for_replace_port(self): t = template_format.parse(neutron_port_template) stack = utils.parse_stack(t) diff -Nru heat-10.0.1/heat/tests/openstack/nova/test_floatingip.py heat-10.0.2/heat/tests/openstack/nova/test_floatingip.py --- heat-10.0.1/heat/tests/openstack/nova/test_floatingip.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/openstack/nova/test_floatingip.py 2018-09-04 20:02:50.000000000 +0000 @@ -63,28 +63,27 @@ class NovaFloatingIPTest(common.HeatTestCase): def setUp(self): super(NovaFloatingIPTest, self).setUp() - self.novaclient = mock.Mock() - self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') - self.m.StubOutWithMock(self.novaclient.servers, 'get') - self.m.StubOutWithMock(neutronclient.Client, 'list_networks') + self.novaclient = fakes_nova.FakeClient() + self.patchobject(nova.NovaClientPlugin, '_create', + return_value=self.novaclient) self.m.StubOutWithMock(neutronclient.Client, 'create_floatingip') self.m.StubOutWithMock(neutronclient.Client, - 'show_floatingip') - self.m.StubOutWithMock(neutronclient.Client, 'update_floatingip') self.m.StubOutWithMock(neutronclient.Client, 'delete_floatingip') - self.m.StubOutWithMock(self.novaclient.servers, 'add_floating_ip') - self.m.StubOutWithMock(self.novaclient.servers, 'remove_floating_ip') - self.patchobject(nova.NovaClientPlugin, 'get_server', - return_value=mock.MagicMock()) - self.patchobject(nova.NovaClientPlugin, 'has_extension', - return_value=True) self.patchobject(neutron.NeutronClientPlugin, 'find_resourceid_by_name_or_id', return_value='eeee') + def mock_interface(self, port, ip): + class MockIface(object): + def __init__(self, port_id, fixed_ip): + self.port_id = port_id + self.fixed_ips = [{'ip_address': fixed_ip}] + + return MockIface(port, ip) + def mock_create_floatingip(self): neutronclient.Client.create_floatingip({ 'floatingip': {'floating_network_id': u'eeee'} @@ -95,22 +94,28 @@ "floating_ip_address": "11.0.0.1" }}) - def mock_show_floatingip(self, refid): - if refid == 'fc68ea2c-b60b-4b4f-bd82-94ec81110766': - address = '11.0.0.1' + def mock_update_floatingip(self, + fip='fc68ea2c-b60b-4b4f-bd82-94ec81110766', + ex=None, fip_request=None, + delete_assc=False): + if fip_request: + request_body = fip_request + elif delete_assc: + request_body = { + 'floatingip': { + 'port_id': None, + 'fixed_ip_address': None}} else: - address = '11.0.0.2' - neutronclient.Client.show_floatingip( - refid, - ).AndReturn({'floatingip': { - 'router_id': None, - 'tenant_id': 'e936e6cd3e0b48dcb9ff853a8f253257', - 'floating_network_id': 'eeee', - 'fixed_ip_address': None, - 'floating_ip_address': address, - 'port_id': None, - 'id': 'ffff' - }}) + request_body = { + 'floatingip': { + 'port_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'fixed_ip_address': '1.2.3.4'}} + if ex: + neutronclient.Client.update_floatingip( + fip, request_body).AndRaise(ex) + else: + neutronclient.Client.update_floatingip( + fip, request_body).AndReturn(None) def mock_delete_floatingip(self): id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' @@ -127,10 +132,12 @@ self.stack) def prepare_floating_ip_assoc(self): - nova.NovaClientPlugin._create().AndReturn( - self.novaclient) - self.novaclient.servers.get('67dc62f9-efde-4c8b-94af-013e00f5dc57') - self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') + return_server = self.novaclient.servers.list()[1] + self.patchobject(self.novaclient.servers, 'get', + return_value=return_server) + iface = self.mock_interface('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '1.2.3.4') + self.patchobject(return_server, 'interface_list', return_value=[iface]) template = template_format.parse(floating_ip_template_with_assoc) self.stack = utils.parse_stack(template) resource_defns = self.stack.t.resource_definitions(self.stack) @@ -169,9 +176,7 @@ 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( - fakes_nova.fake_exception(400)) - + self.mock_update_floatingip(fakes_nova.fake_exception(400)) self.m.ReplayAll() rsrc.validate() @@ -185,7 +190,7 @@ 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.mock_update_floatingip() self.m.ReplayAll() rsrc.validate() @@ -200,12 +205,8 @@ 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.servers.remove_floating_ip('server', '11.0.0.1') - self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - + self.mock_update_floatingip() + self.mock_update_floatingip(delete_assc=True) self.m.ReplayAll() rsrc.validate() @@ -216,16 +217,11 @@ self.m.VerifyAll() - def create_delete_assoc_with_exc(self, exc_code): + def test_floating_ip_assoc_delete_not_found(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.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - self.novaclient.servers.remove_floating_ip("server", - "11.0.0.1").AndRaise( - fakes_nova.fake_exception(exc_code)) - + self.mock_update_floatingip() + self.mock_update_floatingip(ex=fakes_nova.fake_exception(404), + delete_assc=True) self.m.ReplayAll() rsrc.validate() @@ -236,26 +232,28 @@ self.m.VerifyAll() - def test_floating_ip_assoc_delete_conflict(self): - self.create_delete_assoc_with_exc(exc_code=409) - - def test_floating_ip_assoc_delete_not_found(self): - self.create_delete_assoc_with_exc(exc_code=404) - def test_floating_ip_assoc_update_server_id(self): rsrc = self.prepare_floating_ip_assoc() - # for create - self.novaclient.servers.add_floating_ip(None, '11.0.0.1') - # for update - self.novaclient.servers.get( - '2146dfbf-ba77-4083-8e86-d052f671ece5').AndReturn('server') - self.novaclient.servers.add_floating_ip('server', '11.0.0.1') - self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') + self.mock_update_floatingip() + fip_request = {'floatingip': { + 'fixed_ip_address': '4.5.6.7', + 'port_id': 'bbbbb-bbbb-bbbb-bbbbbbbbb'} + } + self.mock_update_floatingip(fip_request=fip_request) self.m.ReplayAll() rsrc.validate() scheduler.TaskRunner(rsrc.create)() self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + # for update + return_server = self.novaclient.servers.list()[2] + self.patchobject(self.novaclient.servers, 'get', + return_value=return_server) + iface = self.mock_interface('bbbbb-bbbb-bbbb-bbbbbbbbb', + '4.5.6.7') + self.patchobject(return_server, 'interface_list', return_value=[iface]) + # update with the new server_id props = copy.deepcopy(rsrc.properties.data) update_server_id = '2146dfbf-ba77-4083-8e86-d052f671ece5' @@ -270,17 +268,11 @@ def test_floating_ip_assoc_update_fl_ip(self): rsrc = self.prepare_floating_ip_assoc() # for create - self.novaclient.servers.add_floating_ip(None, '11.0.0.1') + self.mock_update_floatingip() # mock for delete the old association - self.novaclient.servers.get( - '67dc62f9-efde-4c8b-94af-013e00f5dc57').AndReturn('server') - self.novaclient.servers.remove_floating_ip('server', '11.0.0.1') + self.mock_update_floatingip(delete_assc=True) # mock for new association - self.novaclient.servers.get( - '67dc62f9-efde-4c8b-94af-013e00f5dc57').AndReturn('server') - self.novaclient.servers.add_floating_ip('server', '11.0.0.2') - self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - self.mock_show_floatingip('fc68ea2c-cccc-4b4f-bd82-94ec81110766') + self.mock_update_floatingip(fip='fc68ea2c-dddd-4b4f-bd82-94ec81110766') self.m.ReplayAll() rsrc.validate() @@ -288,7 +280,7 @@ self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) # update with the new floatingip props = copy.deepcopy(rsrc.properties.data) - props['floating_ip'] = 'fc68ea2c-cccc-4b4f-bd82-94ec81110766' + props['floating_ip'] = 'fc68ea2c-dddd-4b4f-bd82-94ec81110766' update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) scheduler.TaskRunner(rsrc.update, update_snippet)() @@ -299,28 +291,33 @@ def test_floating_ip_assoc_update_both(self): rsrc = self.prepare_floating_ip_assoc() # for create - self.novaclient.servers.add_floating_ip(None, '11.0.0.1') + self.mock_update_floatingip() # mock for delete the old association - self.novaclient.servers.get( - '67dc62f9-efde-4c8b-94af-013e00f5dc57').AndReturn('server') - self.novaclient.servers.remove_floating_ip('server', '11.0.0.1') + self.mock_update_floatingip(delete_assc=True) # mock for new association - self.novaclient.servers.get( - '2146dfbf-ba77-4083-8e86-d052f671ece5').AndReturn('new_server') - self.novaclient.servers.add_floating_ip('new_server', '11.0.0.2') - self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') - self.mock_show_floatingip('fc68ea2c-cccc-4b4f-bd82-94ec81110766') - + fip_request = {'floatingip': { + 'fixed_ip_address': '4.5.6.7', + 'port_id': 'bbbbb-bbbb-bbbb-bbbbbbbbb'} + } + self.mock_update_floatingip(fip='fc68ea2c-dddd-4b4f-bd82-94ec81110766', + fip_request=fip_request) self.m.ReplayAll() rsrc.validate() scheduler.TaskRunner(rsrc.create)() self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) - # update with the new floatingip + # update with the new floatingip and server + return_server = self.novaclient.servers.list()[2] + self.patchobject(self.novaclient.servers, 'get', + return_value=return_server) + iface = self.mock_interface('bbbbb-bbbb-bbbb-bbbbbbbbb', + '4.5.6.7') + self.patchobject(return_server, 'interface_list', return_value=[iface]) + props = copy.deepcopy(rsrc.properties.data) update_server_id = '2146dfbf-ba77-4083-8e86-d052f671ece5' props['server_id'] = update_server_id - props['floating_ip'] = 'fc68ea2c-cccc-4b4f-bd82-94ec81110766' + props['floating_ip'] = 'fc68ea2c-dddd-4b4f-bd82-94ec81110766' update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) scheduler.TaskRunner(rsrc.update, update_snippet)() diff -Nru heat-10.0.1/heat/tests/openstack/nova/test_server.py heat-10.0.2/heat/tests/openstack/nova/test_server.py --- heat-10.0.1/heat/tests/openstack/nova/test_server.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/openstack/nova/test_server.py 2018-09-04 20:02:50.000000000 +0000 @@ -19,6 +19,7 @@ from keystoneauth1 import exceptions as ks_exceptions from neutronclient.v2_0 import client as neutronclient from novaclient import exceptions as nova_exceptions +from novaclient.v2 import client as novaclient from oslo_serialization import jsonutils from oslo_utils import uuidutils import requests @@ -272,7 +273,8 @@ env=environment.Environment( {'key_name': 'test'}), files=files) - stack = parser.Stack(utils.dummy_context(), stack_name, templ, + stack = parser.Stack(utils.dummy_context(region_name="RegionOne"), + stack_name, templ, stack_id=uuidutils.generate_uuid(), stack_user_project_id='8888') return templ, stack @@ -888,6 +890,7 @@ 'auth_url': 'http://server.test:5000/v2.0', 'password': server.password, 'project_id': '8888', + 'region_name': 'RegionOne', 'resource_name': 'WebServer', 'stack_id': 'software_config_s/%s' % stack.id, 'user_id': '1234' @@ -907,6 +910,7 @@ 'auth_url': 'http://server.test:5000/v2.0', 'password': server.password, 'project_id': '8888', + 'region_name': 'RegionOne', 'resource_name': 'WebServer', 'stack_id': 'software_config_s/%s' % stack.id, 'user_id': '1234' @@ -1058,7 +1062,8 @@ 'password': server.password, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', - 'queue_id': queue_id + 'queue_id': queue_id, + 'region_name': 'RegionOne', }, 'collectors': ['ec2', 'zaqar', 'local'] }, @@ -1079,7 +1084,8 @@ 'password': server.password, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', - 'queue_id': mock.ANY + 'queue_id': mock.ANY, + 'region_name': 'RegionOne', }, 'collectors': ['ec2', 'zaqar', 'local'] }, @@ -1099,7 +1105,8 @@ 'password': server.password, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', - 'queue_id': queue_id + 'queue_id': queue_id, + 'region_name': 'RegionOne', }, 'collectors': ['ec2', 'zaqar', 'local'], 'polling_interval': 10 @@ -1765,7 +1772,7 @@ 'metadata_url': None, 'path': None, 'secret_access_key': None, - 'stack_name': None + 'stack_name': None, }, 'request': { 'metadata_url': 'the_url', @@ -1786,6 +1793,7 @@ 'auth_url': 'http://server.test:5000/v2.0', 'password': password, 'project_id': '8888', + 'region_name': 'RegionOne', 'resource_name': 'WebServer', 'stack_id': 'software_config_s/%s' % stack.id, 'user_id': '1234' @@ -1818,12 +1826,14 @@ 'password': password_1, 'auth_url': 'http://server.test:5000/v2.0', 'project_id': '8888', - 'queue_id': server.data().get('metadata_queue_id') + 'queue_id': server.data().get('metadata_queue_id'), + 'region_name': 'RegionOne', }, 'heat': { 'auth_url': None, 'password': None, 'project_id': None, + 'region_name': None, 'resource_name': None, 'stack_id': None, 'user_id': None @@ -4426,6 +4436,10 @@ self.port_show = self.patchobject(neutronclient.Client, 'show_port') + self.server_get = self.patchobject(novaclient.servers.ServerManager, + 'get') + self.server_get.return_value = self.fc.servers.list()[1] + def neutron_side_effect(*args): if args[0] == 'subnet': return '1234' @@ -4905,10 +4919,14 @@ t, stack, server = self._return_template_stack_and_rsrc_defn( 'test', tmpl_server_with_network_id) server.resource_id = 'test_server' - port_ids = [{'id': 1122}, {'id': 3344}] - external_port_ids = [{'id': 5566}] + port_ids = [{'id': '1122'}, {'id': '3344'}] + external_port_ids = [{'id': '5566'}] server._data = {"internal_ports": jsonutils.dumps(port_ids), "external_ports": jsonutils.dumps(external_port_ids)} + nova_server = self.fc.servers.list()[1] + server.client = mock.Mock() + server.client().servers.get.return_value = nova_server + self.patchobject(nova.NovaClientPlugin, 'interface_detach', return_value=True) self.patchobject(nova.NovaClientPlugin, 'check_interface_detach', @@ -4918,25 +4936,61 @@ # check, that the ports were detached from server nova.NovaClientPlugin.interface_detach.assert_has_calls([ - mock.call('test_server', 1122), - mock.call('test_server', 3344), - mock.call('test_server', 5566)]) + mock.call('test_server', '1122'), + mock.call('test_server', '3344'), + mock.call('test_server', '5566')]) def test_prepare_ports_for_replace_not_found(self): t, stack, server = self._return_template_stack_and_rsrc_defn( 'test', tmpl_server_with_network_id) server.resource_id = 'test_server' - port_ids = [{'id': 1122}, {'id': 3344}] - external_port_ids = [{'id': 5566}] + port_ids = [{'id': '1122'}, {'id': '3344'}] + external_port_ids = [{'id': '5566'}] server._data = {"internal_ports": jsonutils.dumps(port_ids), "external_ports": jsonutils.dumps(external_port_ids)} self.patchobject(nova.NovaClientPlugin, 'fetch_server', side_effect=nova_exceptions.NotFound(404)) check_detach = self.patchobject(nova.NovaClientPlugin, 'check_interface_detach') + nova_server = self.fc.servers.list()[1] + nova_server.status = 'DELETED' + self.server_get.return_value = nova_server server.prepare_for_replace() check_detach.assert_not_called() + self.assertEqual(0, self.port_delete.call_count) + + def test_prepare_ports_for_replace_error_state(self): + t, stack, server = self._return_template_stack_and_rsrc_defn( + 'test', tmpl_server_with_network_id) + server.resource_id = 'test_server' + port_ids = [{'id': '1122'}, {'id': '3344'}] + external_port_ids = [{'id': '5566'}] + server._data = {"internal_ports": jsonutils.dumps(port_ids), + "external_ports": jsonutils.dumps(external_port_ids)} + + nova_server = self.fc.servers.list()[1] + nova_server.status = 'ERROR' + self.server_get.return_value = nova_server + + self.patchobject(nova.NovaClientPlugin, 'interface_detach', + return_value=True) + self.patchobject(nova.NovaClientPlugin, 'check_interface_detach', + return_value=True) + data_set = self.patchobject(server, 'data_set') + data_delete = self.patchobject(server, 'data_delete') + + server.prepare_for_replace() + + # check, that the internal ports were deleted + self.assertEqual(2, self.port_delete.call_count) + self.assertEqual(('1122',), self.port_delete.call_args_list[0][0]) + self.assertEqual(('3344',), self.port_delete.call_args_list[1][0]) + data_set.assert_has_calls(( + mock.call('internal_ports', + '[{"id": "3344"}]'), + mock.call('internal_ports', '[{"id": "1122"}]'))) + data_delete.assert_called_once_with('internal_ports') def test_prepare_ports_for_replace_not_created(self): t, stack, server = self._return_template_stack_and_rsrc_defn( diff -Nru heat-10.0.1/heat/tests/test_common_context.py heat-10.0.2/heat/tests/test_common_context.py --- heat-10.0.1/heat/tests/test_common_context.py 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/heat/tests/test_common_context.py 2018-09-04 20:02:40.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # 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 @@ -78,6 +79,51 @@ del(ctx_dict['request_id']) self.assertEqual(self.ctx, ctx_dict) + def test_request_context_to_dict_unicode(self): + + ctx_origin = {'username': 'mick', + 'trustor_user_id': None, + 'auth_token': '123', + 'auth_token_info': {'123info': 'woop'}, + 'is_admin': False, + 'user': 'mick', + 'password': 'foo', + 'trust_id': None, + 'global_request_id': None, + 'show_deleted': False, + 'roles': ['arole', 'notadmin'], + 'tenant_id': '456tenant', + 'user_id': u'Gāo', + 'tenant': u'\u5218\u80dc', + 'auth_url': 'http://xyz', + 'aws_creds': 'blah', + 'region_name': 'RegionOne', + 'user_identity': u'Gāo 456tenant', + 'user_domain': None, + 'project_domain': None} + + ctx = context.RequestContext( + auth_token=ctx_origin.get('auth_token'), + username=ctx_origin.get('username'), + password=ctx_origin.get('password'), + aws_creds=ctx_origin.get('aws_creds'), + project_name=ctx_origin.get('tenant'), + tenant=ctx_origin.get('tenant_id'), + user=ctx_origin.get('user_id'), + auth_url=ctx_origin.get('auth_url'), + roles=ctx_origin.get('roles'), + show_deleted=ctx_origin.get('show_deleted'), + is_admin=ctx_origin.get('is_admin'), + auth_token_info=ctx_origin.get('auth_token_info'), + trustor_user_id=ctx_origin.get('trustor_user_id'), + trust_id=ctx_origin.get('trust_id'), + region_name=ctx_origin.get('region_name'), + user_domain_id=ctx_origin.get('user_domain'), + project_domain_id=ctx_origin.get('project_domain')) + ctx_dict = ctx.to_dict() + del(ctx_dict['request_id']) + self.assertEqual(ctx_origin, ctx_dict) + def test_request_context_from_dict(self): ctx = context.RequestContext.from_dict(self.ctx) ctx_dict = ctx.to_dict() diff -Nru heat-10.0.1/heat/tests/test_convg_stack.py heat-10.0.2/heat/tests/test_convg_stack.py --- heat-10.0.1/heat/tests/test_convg_stack.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/test_convg_stack.py 2018-09-04 20:02:50.000000000 +0000 @@ -21,6 +21,7 @@ from heat.engine import template as templatem from heat.objects import raw_template as raw_template_object from heat.objects import resource as resource_objects +from heat.objects import snapshot as snapshot_objects from heat.objects import stack as stack_object from heat.objects import sync_point as sync_point_object from heat.rpc import worker_client @@ -551,6 +552,37 @@ self.assertTrue(mock_syncpoint_del.called) self.assertTrue(mock_ccu.called) + def test_snapshot_delete(self, mock_cr): + tmpl = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': {'R1': {'Type': 'GenericResourceType'}}} + stack = parser.Stack(utils.dummy_context(), 'updated_time_test', + templatem.Template(tmpl)) + stack.current_traversal = 'prev_traversal' + stack.action, stack.status = stack.CREATE, stack.COMPLETE + stack.store() + stack.thread_group_mgr = tools.DummyThreadGroupManager() + snapshot_values = { + 'stack_id': stack.id, + 'name': 'fake_snapshot', + 'tenant': stack.context.tenant_id, + 'status': 'COMPLETE', + 'data': None + } + snapshot_objects.Snapshot.create(stack.context, snapshot_values) + + # Ensure that snapshot is not deleted on stack update + stack.converge_stack(template=stack.t, action=stack.UPDATE) + db_snapshot_obj = snapshot_objects.Snapshot.get_all( + stack.context, stack.id) + self.assertEqual('fake_snapshot', db_snapshot_obj[0].name) + self.assertEqual(stack.id, db_snapshot_obj[0].stack_id) + + # Ensure that snapshot is deleted on stack delete + stack.converge_stack(template=stack.t, action=stack.DELETE) + self.assertEqual([], snapshot_objects.Snapshot.get_all( + stack.context, stack.id)) + self.assertTrue(mock_cr.called) + @mock.patch.object(parser.Stack, '_persist_state') class TestConvgStackStateSet(common.HeatTestCase): diff -Nru heat-10.0.1/heat/tests/test_translation_rule.py heat-10.0.2/heat/tests/test_translation_rule.py --- heat-10.0.1/heat/tests/test_translation_rule.py 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/heat/tests/test_translation_rule.py 2018-09-04 20:02:40.000000000 +0000 @@ -547,10 +547,13 @@ self.assertIsNone(tran.translate('far')) self.assertIsNone(tran.resolved_translations['far']) - def _test_resolve_rule(self, is_list=False): + def _test_resolve_rule(self, is_list=False, + check_error=False): class FakeClientPlugin(object): def find_name_id(self, entity=None, src_value='far'): + if check_error: + raise exception.NotFound() if entity == 'rose': return 'pink' return 'yellow' @@ -738,6 +741,24 @@ self.assertEqual(['yellow', 'pink'], result) self.assertEqual(['yellow', 'pink'], tran.resolved_translations['far']) + def test_resolve_rule_ignore_error(self): + client_plugin, schema = self._test_resolve_rule(check_error=True) + data = {'far': 'one'} + props = properties.Properties(schema, data) + rule = translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + ['far'], + client_plugin=client_plugin, + finder='find_name_id') + + tran = translation.Translation(props) + tran.set_rules([rule], ignore_resolve_error=True) + self.assertTrue(tran.has_translation('far')) + result = tran.translate('far', data['far']) + self.assertEqual('one', result) + self.assertEqual('one', tran.resolved_translations['far']) + def test_resolve_rule_other(self): client_plugin, schema = self._test_resolve_rule() data = {'far': 'one'} diff -Nru heat-10.0.1/heat.egg-info/pbr.json heat-10.0.2/heat.egg-info/pbr.json --- heat-10.0.1/heat.egg-info/pbr.json 2018-05-08 04:30:36.000000000 +0000 +++ heat-10.0.2/heat.egg-info/pbr.json 2018-09-04 20:06:15.000000000 +0000 @@ -1 +1 @@ -{"git_version": "825731d", "is_release": true} \ No newline at end of file +{"git_version": "1f08105", "is_release": true} \ No newline at end of file diff -Nru heat-10.0.1/heat.egg-info/PKG-INFO heat-10.0.2/heat.egg-info/PKG-INFO --- heat-10.0.1/heat.egg-info/PKG-INFO 2018-05-08 04:30:36.000000000 +0000 +++ heat-10.0.2/heat.egg-info/PKG-INFO 2018-09-04 20:06:15.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: heat -Version: 10.0.1 +Version: 10.0.2 Summary: OpenStack Orchestration Home-page: http://docs.openstack.org/developer/heat/ Author: OpenStack diff -Nru heat-10.0.1/heat.egg-info/SOURCES.txt heat-10.0.2/heat.egg-info/SOURCES.txt --- heat-10.0.1/heat.egg-info/SOURCES.txt 2018-05-08 04:30:38.000000000 +0000 +++ heat-10.0.2/heat.egg-info/SOURCES.txt 2018-09-04 20:06:18.000000000 +0000 @@ -878,6 +878,7 @@ heat/tests/convergence/scenarios/update_replace_rollback.py heat/tests/convergence/scenarios/update_user_replace.py heat/tests/convergence/scenarios/update_user_replace_rollback.py +heat/tests/convergence/scenarios/update_user_replace_rollback_update.py heat/tests/db/__init__.py heat/tests/db/test_migrations.py heat/tests/db/test_sqlalchemy_api.py @@ -1113,6 +1114,7 @@ heat_integrationtests/locale/ko_KR/LC_MESSAGES/heat_integrationtests.po heat_upgradetests/post_test_hook.sh heat_upgradetests/pre_test_hook.sh +playbooks/get_amphora_tarball.yaml playbooks/devstack/functional/post.yaml playbooks/devstack/functional/run.yaml playbooks/devstack/grenade/run.yaml @@ -1152,6 +1154,7 @@ releasenotes/notes/cinder-quota-resource-f13211c04020cd0c.yaml releasenotes/notes/configurable-server-name-limit-947d9152fe9b43ee.yaml releasenotes/notes/converge-flag-for-stack-update-e0e92a7fe232f10f.yaml +releasenotes/notes/convergence-delete-race-5b821bbd4c5ba5dc.yaml releasenotes/notes/deployment-swift-data-server-property-51fd4f9d1671fc90.yaml releasenotes/notes/deprecate-nova-floatingip-resources-d5c9447a199be402.yaml releasenotes/notes/deprecate-threshold-alarm-5738f5ab8aebfd20.yaml @@ -1175,6 +1178,7 @@ releasenotes/notes/keystone-project-allow-get-attribute-b382fe97694e3987.yaml releasenotes/notes/keystone-region-ce3b435c73c81ce4.yaml releasenotes/notes/know-limit-releasenote-4d21fc4d91d136d9.yaml +releasenotes/notes/legacy-client-races-ba7a60cef5ec1694.yaml releasenotes/notes/legacy-stack-user-id-cebbad8b0f2ed490.yaml releasenotes/notes/magnum-resource-update-0f617eec45ef8ef7.yaml releasenotes/notes/make_url-function-d76737adb1e54801.yaml diff -Nru heat-10.0.1/PKG-INFO heat-10.0.2/PKG-INFO --- heat-10.0.1/PKG-INFO 2018-05-08 04:30:38.000000000 +0000 +++ heat-10.0.2/PKG-INFO 2018-09-04 20:06:18.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: heat -Version: 10.0.1 +Version: 10.0.2 Summary: OpenStack Orchestration Home-page: http://docs.openstack.org/developer/heat/ Author: OpenStack diff -Nru heat-10.0.1/playbooks/devstack/functional/run.yaml heat-10.0.2/playbooks/devstack/functional/run.yaml --- heat-10.0.1/playbooks/devstack/functional/run.yaml 2018-05-08 04:27:15.000000000 +0000 +++ heat-10.0.2/playbooks/devstack/functional/run.yaml 2018-09-04 20:02:50.000000000 +0000 @@ -65,6 +65,9 @@ export PROJECTS="openstack/neutron-lbaas $PROJECTS" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin neutron-lbaas https://git.openstack.org/openstack/neutron-lbaas" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin octavia https://git.openstack.org/openstack/octavia" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"OCTAVIA_AMP_IMAGE_FILE=/tmp/test-only-amphora-x64-haproxy-ubuntu-xenial.qcow2" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"OCTAVIA_AMP_IMAGE_SIZE=3" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"OCTAVIA_AMP_IMAGE_NAME=test-only-amphora-x64-haproxy-ubuntu-xenial" # enabling lbaas plugin does not enable the lbaasv2 service, explicitly enable it services+=,q-lbaasv2,octavia,o-cw,o-hk,o-hm,o-api export PROJECTS="openstack/barbican $PROJECTS" diff -Nru heat-10.0.1/playbooks/get_amphora_tarball.yaml heat-10.0.2/playbooks/get_amphora_tarball.yaml --- heat-10.0.1/playbooks/get_amphora_tarball.yaml 1970-01-01 00:00:00.000000000 +0000 +++ heat-10.0.2/playbooks/get_amphora_tarball.yaml 2018-09-04 20:02:40.000000000 +0000 @@ -0,0 +1,6 @@ +- hosts: primary + tasks: + - name: Download amphora tarball + get_url: + url: "https://tarballs.openstack.org/octavia/test-images/test-only-amphora-x64-haproxy-ubuntu-xenial.qcow2" + dest: /tmp/test-only-amphora-x64-haproxy-ubuntu-xenial.qcow2 diff -Nru heat-10.0.1/releasenotes/notes/convergence-delete-race-5b821bbd4c5ba5dc.yaml heat-10.0.2/releasenotes/notes/convergence-delete-race-5b821bbd4c5ba5dc.yaml --- heat-10.0.1/releasenotes/notes/convergence-delete-race-5b821bbd4c5ba5dc.yaml 1970-01-01 00:00:00.000000000 +0000 +++ heat-10.0.2/releasenotes/notes/convergence-delete-race-5b821bbd4c5ba5dc.yaml 2018-09-04 20:02:40.000000000 +0000 @@ -0,0 +1,11 @@ +--- +fixes: + - | + Previously, when deleting a convergence stack, the API call would return + immediately, so that it was possible for a client immediately querying the + status of the stack to see the state of the previous operation in progress + or having failed, and confuse that with a current status. (This included + Heat itself when acting as a client for a nested stack.) Convergence stacks + are now guaranteed to have moved to the ``DELETE_IN_PROGRESS`` state before + the delete API call returns, so any subsequent polling will reflect + up-to-date information. diff -Nru heat-10.0.1/releasenotes/notes/legacy-client-races-ba7a60cef5ec1694.yaml heat-10.0.2/releasenotes/notes/legacy-client-races-ba7a60cef5ec1694.yaml --- heat-10.0.1/releasenotes/notes/legacy-client-races-ba7a60cef5ec1694.yaml 1970-01-01 00:00:00.000000000 +0000 +++ heat-10.0.2/releasenotes/notes/legacy-client-races-ba7a60cef5ec1694.yaml 2018-09-04 20:02:40.000000000 +0000 @@ -0,0 +1,12 @@ +--- +fixes: + - | + Previously, the suspend, resume, and check API calls for all stacks, and + the update, restore, and delete API calls for non-convergence stacks, + returned immediately after starting the stack operation. This meant that + for a client reading the state immediately when performing the same + operation twice in a row, it could have misinterpreted a previous state as + the latest unless careful reference were made to the updated_at timestamp. + Stacks are now guaranteed to have moved to the ``IN_PROGRESS`` state before + any of these APIs return (except in the case of deleting a non-convergence + stack where another operation was already in progress). diff -Nru heat-10.0.1/tox.ini heat-10.0.2/tox.ini --- heat-10.0.1/tox.ini 2018-05-08 04:27:22.000000000 +0000 +++ heat-10.0.2/tox.ini 2018-09-04 20:02:50.000000000 +0000 @@ -104,7 +104,7 @@ [flake8] show-source = true exclude=.*,dist,*lib/python*,*egg,build,*convergence/scenarios/* -max-complexity=20 +max-complexity=24 [hacking] import_exceptions = heat.common.i18n diff -Nru heat-10.0.1/.zuul.yaml heat-10.0.2/.zuul.yaml --- heat-10.0.1/.zuul.yaml 2018-05-08 04:27:21.000000000 +0000 +++ heat-10.0.2/.zuul.yaml 2018-09-04 20:02:49.000000000 +0000 @@ -27,6 +27,7 @@ - ^heat/locale/.*$ - ^heat/tests/.*$ - ^releasenotes/.*$ + pre-run: playbooks/get_amphora_tarball.yaml vars: disable_convergence: 'false' sql: mysql