def update(self, json_snippet=None): ''' update the resource. Subclasses should provide a handle_update() method to customise update, the base-class handle_update will fail by default. ''' assert json_snippet is not None, 'Must specify update json snippet' if self.state in (self.CREATE_IN_PROGRESS, self.UPDATE_IN_PROGRESS): raise exception.ResourceFailure( Exception('Resource update already requested')) logger.info('updating %s' % str(self)) try: self.state_set(self.UPDATE_IN_PROGRESS) properties = Properties(self.properties_schema, json_snippet.get('Properties', {}), self.stack.resolve_runtime_data, self.name) properties.validate() tmpl_diff = self.update_template_diff(json_snippet) prop_diff = self.update_template_diff_properties(json_snippet) if callable(getattr(self, 'handle_update', None)): result = self.handle_update(json_snippet, tmpl_diff, prop_diff) except UpdateReplace: logger.debug("Resource %s update requires replacement" % self.name) raise except Exception as ex: logger.exception('update %s : %s' % (str(self), str(ex))) failure = exception.ResourceFailure(ex) self.state_set(self.UPDATE_FAILED, str(failure)) raise failure else: self.t = self.stack.resolve_static_data(json_snippet) self.state_set(self.UPDATE_COMPLETE)
def test_heat_exception(self): base_exc = ValueError('sorry mom') heat_exc = exception.ResourceFailure(base_exc, None, action='UPDATE') exc = exception.ResourceFailure(heat_exc, None, action='UPDATE') self.assertEqual('ValueError', exc.error) self.assertEqual([], exc.path) self.assertEqual('sorry mom', exc.error_message)
def update(self, after, before=None, prev_resource=None): ''' update the resource. Subclasses should provide a handle_update() method to customise update, the base-class handle_update will fail by default. ''' action = self.UPDATE (cur_class_def, cur_ver) = self.implementation_signature() prev_ver = cur_ver if prev_resource is not None: (prev_class_def, prev_ver) = prev_resource.implementation_signature() if prev_class_def != cur_class_def: raise UpdateReplace(self.name) if before is None: before = self.parsed_template() if prev_ver == cur_ver and before == after: return if (self.action, self.status) in ((self.CREATE, self.IN_PROGRESS), (self.UPDATE, self.IN_PROGRESS), (self.ADOPT, self.IN_PROGRESS)): exc = Exception(_('Resource update already requested')) raise exception.ResourceFailure(exc, self, action) logger.info('updating %s' % str(self)) try: self.updated_time = datetime.utcnow() self.state_set(action, self.IN_PROGRESS) properties = Properties(self.properties_schema, after.get('Properties', {}), self._resolve_runtime_data, self.name, self.context) properties.validate() tmpl_diff = self.update_template_diff(after, before) prop_diff = self.update_template_diff_properties(after, before) if callable(getattr(self, 'handle_update', None)): handle_data = self.handle_update(after, tmpl_diff, prop_diff) yield if callable(getattr(self, 'check_update_complete', None)): while not self.check_update_complete(handle_data): yield except UpdateReplace: with excutils.save_and_reraise_exception(): logger.debug(_("Resource %s update requires replacement") % self.name) except Exception as ex: logger.exception('update %s : %s' % (str(self), str(ex))) failure = exception.ResourceFailure(ex, self, action) self.state_set(action, self.FAILED, str(failure)) raise failure else: self.json_snippet = copy.deepcopy(after) self.reparse() self.state_set(action, self.COMPLETE)
def test_nested_exceptions(self): res = mock.Mock() res.name = 'frodo' res.stack.t.get_section_name.return_value = 'Resources' reason = ('Resource UPDATE failed: ValueError: resources.oops: ' 'Test Resource failed oops') base_exc = exception.ResourceFailure(reason, res, action='UPDATE') exc = exception.ResourceFailure(base_exc, res, action='UPDATE') self.assertEqual(['Resources', 'frodo', 'resources', 'oops'], exc.path) self.assertEqual('ValueError', exc.error) self.assertEqual('Test Resource failed oops', exc.error_message)
def handle_signal(self, details=None): inputs, params = self._get_inputs_and_params(details) self._validate_signal_data(inputs, params) inputs_result = copy.deepcopy(self.properties[self.INPUT]) params_result = copy.deepcopy(self.properties[self.PARAMS]) or {} # NOTE(prazumovsky): Signal can contains some data, interesting # for workflow, e.g. inputs. So, if signal data contains input # we update override inputs, other leaved defined in template. if inputs: inputs_result.update(inputs) if params: params_result.update(params) try: execution = self.client().executions.create( self._workflow_name(), workflow_input=jsonutils.dumps(inputs_result), **params_result) except Exception as ex: raise exception.ResourceFailure(ex, self) executions = [execution.id] if self.EXECUTIONS in self.data(): executions.extend(self.data().get(self.EXECUTIONS).split(',')) self.data_set(self.EXECUTIONS, ','.join(executions))
def handle_signal(self, details=None): self._validate_signal_data(details) result_input = {} result_params = {} inputs, params = self._get_inputs_and_params(details) if inputs is not None: # NOTE(prazumovsky): Signal can contains some data, interesting # for workflow, e.g. inputs. So, if signal data contains input # we update override inputs, other leaved defined in template. for key, value in six.iteritems(self.properties.get(self.INPUT)): result_input.update({key: inputs.get(key) or value}) if params is not None: if self.properties.get(self.PARAMS) is not None: result_params.update(self.properties.get(self.PARAMS)) result_params.update(params) if not result_input and self.properties.get(self.INPUT): result_input.update(self.properties.get(self.INPUT)) if not result_params and self.properties.get(self.PARAMS): result_params.update(self.properties.get(self.PARAMS)) try: execution = self.client().executions.create( self._workflow_name(), jsonutils.dumps(result_input), **result_params) except Exception as ex: raise exception.ResourceFailure(ex, self) executions = [execution.id] if self.EXECUTIONS in self.data(): executions.extend(self.data().get(self.EXECUTIONS).split(',')) self.data_set(self.EXECUTIONS, ','.join(executions))
def _action_recorder(self, action, expected_exceptions=tuple()): '''Return a context manager to record the progress of an action. Upon entering the context manager, the state is set to IN_PROGRESS. Upon exiting, the state will be set to COMPLETE if no exception was raised, or FAILED otherwise. Non-exit exceptions will be translated to ResourceFailure exceptions. Expected exceptions are re-raised, with the Resource left in the IN_PROGRESS state. ''' try: self.state_set(action, self.IN_PROGRESS) yield except expected_exceptions as ex: with excutils.save_and_reraise_exception(): LOG.debug('%s', six.text_type(ex)) except Exception as ex: LOG.info('%(action)s: %(info)s', { "action": action, "info": str(self) }, exc_info=True) failure = exception.ResourceFailure(ex, self, action) self.state_set(action, self.FAILED, six.text_type(failure)) raise failure except: # noqa with excutils.save_and_reraise_exception(): try: self.state_set(action, self.FAILED, '%s aborted' % action) except Exception: LOG.exception(_('Error marking resource as failed')) else: self.state_set(action, self.COMPLETE)
def handle_create(self): '''Create the BIG-IP® LTM Virtual Server resource on the given device. :raises: ResourceFailure exception ''' destination = '/{0}/{1}:{2}'.format(self.partition_name, self.properties[self.IP], self.properties[self.PORT]) create_kwargs = { 'name': self.properties[self.NAME], 'partition': self.partition_name, 'destination': destination, 'sourceAddressTranslation': { 'type': 'automap' } } if self.properties[self.DEFAULT_POOL]: create_kwargs['pool'] = self.properties[self.DEFAULT_POOL] if self.properties[self.VLANS]: create_kwargs['vlans'] = self.properties[self.VLANS] create_kwargs['vlansEnabled'] = True try: self.bigip.tm.ltm.virtuals.virtual.create(**create_kwargs) except Exception as ex: raise exception.ResourceFailure(ex, None, action='CREATE')
def _check_execution(self, action, execution_id): """Check execution status. Returns False if in IDLE, RUNNING or PAUSED returns True if in SUCCESS raises ResourceFailure if in ERROR, CANCELLED raises ResourceUnknownState otherwise. """ execution = self.client().executions.get(execution_id) LOG.debug('Mistral execution %(id)s is in state ' '%(state)s' % { 'id': execution_id, 'state': execution.state }) if execution.state in ('IDLE', 'RUNNING', 'PAUSED'): return False, {} if execution.state in ('SUCCESS', ): return True, jsonutils.loads(execution.output) if execution.state in ('ERROR', 'CANCELLED'): raise exception.ResourceFailure(exception_or_error=execution.state, resource=self, action=action) raise exception.ResourceUnknownStatus( resource_status=execution.state, result=_('Mistral execution is in unknown state.'))
def test_status_reason_resource(self): reason = ('Resource CREATE failed: ValueError: resources.oops: ' 'Test Resource failed oops') exc = exception.ResourceFailure(reason, None, action='CREATE') self.assertEqual('ValueError', exc.error) self.assertEqual(['resources', 'oops'], exc.path) self.assertEqual('Test Resource failed oops', exc.error_message)
def _check_status_complete(self, expected_action, cookie=None): try: data = stack_object.Stack.get_status(self.context, self.resource_id) except exception.NotFound: if expected_action == self.DELETE: return True # It's possible the engine handling the create hasn't persisted # the stack to the DB when we first start polling for state return False action, status, status_reason, updated_time = data if action != expected_action: return False # Has the action really started? # # The rpc call to update does not guarantee that the stack will be # placed into IN_PROGRESS by the time it returns (it runs stack.update # in a thread) so you could also have a situation where we get into # this method and the update hasn't even started. # # So we are using a mixture of state (action+status) and updated_at # to see if the action has actually progressed. # - very fast updates (like something with one RandomString) we will # probably miss the state change, but we should catch the updated_at. # - very slow updates we won't see the updated_at for quite a while, # but should see the state change. if cookie is not None: prev_state = cookie['previous']['state'] prev_updated_at = cookie['previous']['updated_at'] if (prev_updated_at == updated_time and prev_state == (action, status)): return False if status == self.IN_PROGRESS: return False elif status == self.COMPLETE: # For operations where we do not take a resource lock # (i.e. legacy-style), check that the stack lock has been # released before reporting completeness. done = (self._should_lock_on_action(expected_action) or stack_lock.StackLock.get_engine_id( self.context, self.resource_id) is None) if done: # Reset nested, to indicate we changed status self._nested = None return done elif status == self.FAILED: raise exception.ResourceFailure(status_reason, self, action=action) else: raise exception.ResourceUnknownStatus( resource_status=status, status_reason=status_reason, result=_('Stack unknown status'))
def DeleteTemplateSideEffect(F5SysiAppTemplate): F5SysiAppTemplate.get_bigip() F5SysiAppTemplate.bigip.tm.sys.application.templates.template.load.\ side_effect = exception.ResourceFailure( mock.MagicMock(), None, action='DELETE' ) return F5SysiAppTemplate
def test_std_exception_with_resource(self): base_exc = ValueError('sorry mom') res = mock.Mock() res.name = 'fred' res.stack.t.get_section_name.return_value = 'Resources' exc = exception.ResourceFailure(base_exc, res, action='UPDATE') self.assertEqual('ValueError', exc.error) self.assertEqual(['Resources', 'fred'], exc.path) self.assertEqual('sorry mom', exc.error_message)
def DeleteServiceSideEffect(F5SysiAppService): F5SysiAppService.get_bigip() F5SysiAppService.bigip.tm.sys.application.services.service.load.\ side_effect = exception.ResourceFailure( mock.MagicMock(), None, action='Delete' ) return F5SysiAppService
def DeletePartitionSideEffect(F5SysPartition): F5SysPartition.get_bigip() F5SysPartition.bigip.tm.sys.folders.folder.load.\ side_effect = exception.ResourceFailure( mock.MagicMock(), None, action='Delete' ) return F5SysPartition
def test_status_reason_general_res(self): res = mock.Mock() res.name = 'fred' res.stack.t.get_section_name.return_value = 'Resources' reason = ('something strange happened') exc = exception.ResourceFailure(reason, res, action='CREATE') self.assertEqual('', exc.error) self.assertEqual(['Resources', 'fred'], exc.path) self.assertEqual('something strange happened', exc.error_message)
def handle_create(self): '''Save the configuration on the BIG-IP® device. :raises: ResourceFailure exception ''' try: self.bigip.tm.sys.config.exec_cmd('save') except Exception as ex: raise exception.ResourceFailure(ex, None, action='CREATE')
def handle_create(self): super(Workflow, self).handle_create() props = self.prepare_properties(self.properties) try: workflow = self.client().workflows.create(props) except Exception as ex: raise exception.ResourceFailure(ex, self) # NOTE(prazumovsky): Mistral uses unique names for resource # identification. self.resource_id_set(workflow[0].name)
def update(self, after, before=None): ''' update the resource. Subclasses should provide a handle_update() method to customise update, the base-class handle_update will fail by default. ''' action = self.UPDATE if before is None: before = self.parsed_template() if (self.action, self.status) in ((self.CREATE, self.IN_PROGRESS), (self.UPDATE, self.IN_PROGRESS)): exc = Exception('Resource update already requested') raise exception.ResourceFailure(exc, self, action) logger.info('updating %s' % str(self)) try: self.state_set(action, self.IN_PROGRESS) properties = Properties(self.properties_schema, after.get('Properties', {}), self._resolve_runtime_data, self.name) properties.validate() tmpl_diff = self.update_template_diff(after, before) prop_diff = self.update_template_diff_properties(after, before) if callable(getattr(self, 'handle_update', None)): handle_data = self.handle_update(after, tmpl_diff, prop_diff) yield if callable(getattr(self, 'check_update_complete', None)): while not self.check_update_complete(handle_data): yield except UpdateReplace: logger.debug("Resource %s update requires replacement" % self.name) raise except Exception as ex: logger.exception('update %s : %s' % (str(self), str(ex))) failure = exception.ResourceFailure(ex, self, action) self.state_set(action, self.FAILED, str(failure)) raise failure else: self.t = self.stack.resolve_static_data(after) self.state_set(action, self.COMPLETE)
def _check_replace_restricted(self, res): registry = res.stack.env.registry restricted_actions = registry.get_rsrc_restricted_actions(res.name) existing_res = self.existing_stack[res.name] if 'replace' in restricted_actions: ex = exception.ResourceActionRestricted(action='replace') failure = exception.ResourceFailure(ex, existing_res, existing_res.UPDATE) existing_res._add_event(existing_res.UPDATE, existing_res.FAILED, six.text_type(ex)) raise failure
def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: props = json_snippet.properties(self.properties_schema, self.context) new_props = self.prepare_properties(props) try: workflow = self.client().workflows.update(new_props) except Exception as ex: raise exception.ResourceFailure(ex, self) self.data_set(self.NAME, workflow[0].name) self.resource_id_set(workflow[0].name)
def delete(self): ''' Delete the resource. Subclasses should provide a handle_delete() method to customise deletion. ''' action = self.DELETE if (self.action, self.status) == (self.DELETE, self.COMPLETE): return # No need to delete if the resource has never been created if self.action == self.INIT: return initial_state = self.state logger.info(_('deleting %s') % str(self)) try: self.state_set(action, self.IN_PROGRESS) if self.abandon_in_progress: deletion_policy = RETAIN else: deletion_policy = self.t.get('DeletionPolicy', DELETE) handle_data = None if deletion_policy == DELETE: if callable(getattr(self, 'handle_delete', None)): handle_data = self.handle_delete() yield elif deletion_policy == SNAPSHOT: if callable(getattr(self, 'handle_snapshot_delete', None)): handle_data = self.handle_snapshot_delete(initial_state) yield if (deletion_policy != RETAIN and callable( getattr(self, 'check_delete_complete', None))): while not self.check_delete_complete(handle_data): yield except Exception as ex: logger.exception(_('Delete %s') % str(self)) failure = exception.ResourceFailure(ex, self, self.action) self.state_set(action, self.FAILED, six.text_type(failure)) raise failure except: with excutils.save_and_reraise_exception(): try: self.state_set(action, self.FAILED, 'Deletion aborted') except Exception: logger.exception( _('Error marking resource deletion ' 'failed')) else: self.state_set(action, self.COMPLETE)
def handle_delete(self): '''Delete the BigIP Pool resource on the given device. :raises: ResourceFailure ''' self.get_bigip() try: self.bigip.pool.delete(self.properties[self.NAME]) except Exception as ex: raise exception.ResourceFailure(ex, None, action='DELETE')
def handle_delete(self): '''Deletes the iApp Service :raises: Resource Failure # TODO Change to proper exception ''' self.get_bigip() try: self.bigip.iapp.delete_service(name=self.properties[self.NAME]) except Exception as ex: raise exception.ResourceFailure(ex, None, action='DELETE')
def handle_delete(self): '''Delete the BigIP Virtual Server resource on the given device. :raises: ResourceFailure exception ''' self.get_bigip() try: self.bigip.virtual_server.delete(name=self.properties[self.NAME]) except Exception as ex: raise exception.ResourceFailure(ex, None, action='DELETE')
def handle_create(self): '''Create the iApp® Template on the BIG-IP®. :raises: ResourceFailure ''' self._validate_template_partition() try: template = self.bigip.tm.sys.application.templates.template template.create(**self.template_dict) except Exception as ex: raise exception.ResourceFailure(ex, None, action='CREATE')
def test_update_modify_replace_failed_delete(self): # patch in a dummy property schema for GenericResource dummy_schema = {'Foo': {'Type': 'String'}} generic_rsrc.GenericResource.properties_schema = dummy_schema tmpl = { 'Resources': { 'AResource': { 'Type': 'GenericResourceType', 'Properties': { 'Foo': 'abc' } } } } self.stack = parser.Stack(self.ctx, 'update_test_stack', template.Template(tmpl), disable_rollback=True) self.stack.store() self.stack.create() self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE) tmpl2 = { 'Resources': { 'AResource': { 'Type': 'GenericResourceType', 'Properties': { 'Foo': 'xyz' } } } } updated_stack = parser.Stack(self.ctx, 'updated_stack', template.Template(tmpl2)) # Calls to GenericResource.handle_update will raise # resource.UpdateReplace because we've not specified the modified # key/property in update_allowed_keys/update_allowed_properties # make the update fail deleting the existing resource self.m.StubOutWithMock(resource.Resource, 'destroy') exc = exception.ResourceFailure(Exception()) resource.Resource.destroy().AndRaise(exc) self.m.ReplayAll() self.stack.update(updated_stack) self.assertEqual(self.stack.state, parser.Stack.UPDATE_FAILED) self.m.VerifyAll() # Unset here so destroy() is not stubbed for stack.delete cleanup self.m.UnsetStubs()
def test_rollback_is_not_triggered_on_rollback_disabled_stack( self, mock_tr, mock_cru, mock_crc, mock_pcr, mock_csc): self.stack.disable_rollback = True self.stack.store() dummy_ex = exception.ResourceNotAvailable( resource_name=self.resource.name) mock_cru.side_effect = exception.ResourceFailure( dummy_ex, self.resource, action=self.stack.CREATE) self.worker.check_resource(self.ctx, self.resource.id, self.stack.current_traversal, {}, self.is_update, None) self.assertFalse(mock_tr.called)
def test_resource_update_failure_purges_db_for_stack_failure( self, mock_cru, mock_crc, mock_pcr, mock_csc): self.stack.disable_rollback = True self.stack.store() self.stack.purge_db = mock.Mock() dummy_ex = exception.ResourceNotAvailable( resource_name=self.resource.name) mock_cru.side_effect = exception.ResourceFailure( dummy_ex, self.resource, action=self.resource.UPDATE) self.worker.check_resource(self.ctx, self.resource.id, self.stack.current_traversal, {}, self.is_update, None) self.assertTrue(self.stack.purge_db.called)
def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None): update_allowed = [self.INPUT, self.PARAMS, self.DESCRIPTION] for prop in update_allowed: if prop in prop_diff: del prop_diff[prop] if len(prop_diff) > 0: new_props = self.prepare_properties(tmpl_diff['Properties']) try: workflow = self.client().workflows.update(new_props) except Exception as ex: raise exception.ResourceFailure(ex, self) self.data_set(self.NAME, workflow[0].name) self.resource_id_set(workflow[0].name)