class Transaction
the class that actually walks our resource/property tree, collects the changes, and performs them
@api private
Attributes
Routes and stores any events and subscriptions.
@!attribute [r] persistence
@return [Puppet::Transaction::Persistence] persistence object for cross transaction storage.
The report, once generated.
Handles most of the actual interacting with resources
Public Class Methods
# File lib/puppet/transaction.rb 42 def initialize(catalog, report, prioritizer) 43 @catalog = catalog 44 45 @persistence = Puppet::Transaction::Persistence.new 46 47 @report = report || Puppet::Transaction::Report.new(catalog.version, catalog.environment) 48 49 @prioritizer = prioritizer 50 51 @report.add_times(:config_retrieval, @catalog.retrieval_duration || 0) 52 53 @event_manager = Puppet::Transaction::EventManager.new(self) 54 55 @resource_harness = Puppet::Transaction::ResourceHarness.new(self) 56 57 @prefetched_providers = Hash.new { |h,k| h[k] = {} } 58 59 @prefetch_failed_providers = Hash.new { |h,k| h[k] = {} } 60 61 # With merge_dependency_warnings, notify and warn about class dependency failures ... just once per class. TJK 2019-09-09 62 @merge_dependency_warnings = Puppet[:merge_dependency_warnings] 63 @failed_dependencies_already_notified = Set.new() 64 @failed_class_dependencies_already_notified = Set.new() 65 @failed_class_dependencies_already_warned = Set.new() 66 end
Public Instance Methods
Are there any failed resources in this transaction?
# File lib/puppet/transaction.rb 215 def any_failed? 216 report.resource_statuses.values.detect { |status| 217 status.failed? || status.failed_to_restart? 218 } 219 end
Find all of the changed resources.
# File lib/puppet/transaction.rb 222 def changed? 223 report.resource_statuses.values.find_all { |status| status.changed }.collect { |status| catalog.resource(status.resource) } 224 end
This method does all the actual work of running a transaction. It collects all of the changes, executes them, and responds to any necessary events.
# File lib/puppet/transaction.rb 98 def evaluate(&block) 99 block ||= method(:eval_resource) 100 generator = AdditionalResourceGenerator.new(@catalog, nil, @prioritizer) 101 @catalog.vertices.each { |resource| generator.generate_additional_resources(resource) } 102 103 perform_pre_run_checks 104 105 persistence.load if persistence.enabled?(catalog) 106 107 Puppet.info _("Applying configuration version '%{version}'") % { version: catalog.version } if catalog.version 108 109 continue_while = lambda { !stop_processing? } 110 111 post_evalable_providers = Set.new 112 pre_process = lambda do |resource| 113 prov_class = resource.provider.class 114 post_evalable_providers << prov_class if prov_class.respond_to?(:post_resource_eval) 115 116 prefetch_if_necessary(resource) 117 118 # If we generated resources, we don't know what they are now 119 # blocking, so we opt to recompute it, rather than try to track every 120 # change that would affect the number. 121 relationship_graph.clear_blockers if generator.eval_generate(resource) 122 end 123 124 providerless_types = [] 125 overly_deferred_resource_handler = lambda do |resource| 126 # We don't automatically assign unsuitable providers, so if there 127 # is one, it must have been selected by the user. 128 return if missing_tags?(resource) 129 if resource.provider 130 resource.err _("Provider %{name} is not functional on this host") % { name: resource.provider.class.name } 131 else 132 providerless_types << resource.type 133 end 134 135 resource_status(resource).failed = true 136 end 137 138 canceled_resource_handler = lambda do |resource| 139 resource_status(resource).skipped = true 140 resource.debug "Transaction canceled, skipping" 141 end 142 143 teardown = lambda do 144 # Just once per type. No need to punish the user. 145 providerless_types.uniq.each do |type| 146 Puppet.err _("Could not find a suitable provider for %{type}") % { type: type } 147 end 148 149 post_evalable_providers.each do |provider| 150 begin 151 provider.post_resource_eval 152 rescue => detail 153 Puppet.log_exception(detail, _("post_resource_eval failed for provider %{provider}") % { provider: provider }) 154 end 155 end 156 157 persistence.save if persistence.enabled?(catalog) 158 end 159 160 # Graph cycles are returned as an array of arrays 161 # - outer array is an array of cycles 162 # - each inner array is an array of resources involved in a cycle 163 # Short circuit resource evaluation if we detect cycle(s) in the graph. Mark 164 # each corresponding resource as failed in the report before we fail to 165 # ensure accurate reporting. 166 graph_cycle_handler = lambda do |cycles| 167 cycles.flatten.uniq.each do |resource| 168 # We add a failed resource event to the status to ensure accurate 169 # reporting through the event manager. 170 resource_status(resource).fail_with_event(_('resource is part of a dependency cycle')) 171 end 172 raise Puppet::Error, _('One or more resource dependency cycles detected in graph') 173 end 174 175 # Generate the relationship graph, set up our generator to use it 176 # for eval_generate, then kick off our traversal. 177 generator.relationship_graph = relationship_graph 178 progress = 0 179 relationship_graph.traverse(:while => continue_while, 180 :pre_process => pre_process, 181 :overly_deferred_resource_handler => overly_deferred_resource_handler, 182 :canceled_resource_handler => canceled_resource_handler, 183 :graph_cycle_handler => graph_cycle_handler, 184 :teardown => teardown) do |resource| 185 progress += 1 186 if resource.is_a?(Puppet::Type::Component) 187 Puppet.warning _("Somehow left a component in the relationship graph") 188 else 189 if Puppet[:evaltrace] && @catalog.host_config? 190 resource.info _("Starting to evaluate the resource (%{progress} of %{total})") % { progress: progress, total: relationship_graph.size } 191 end 192 seconds = thinmark { block.call(resource) } 193 resource.info _("Evaluated in %{seconds} seconds") % { seconds: "%0.2f" % seconds } if Puppet[:evaltrace] && @catalog.host_config? 194 end 195 end 196 197 # if one or more resources has attempted and failed to generate resources, 198 # report it 199 if generator.resources_failed_to_generate 200 report.resources_failed_to_generate = true 201 end 202 203 # mark the end of transaction evaluate. 204 report.transaction_completed = true 205 206 Puppet.debug { "Finishing transaction #{object_id}" } 207 end
Invoke the pre_run_check hook in every resource in the catalog. This should (only) be called by Transaction#evaluate before applying the catalog.
@see Puppet::Transaction#evaluate @see Puppet::Type#pre_run_check @raise [Puppet::Error] If any pre-run checks failed. @return [void]
# File lib/puppet/transaction.rb 76 def perform_pre_run_checks 77 prerun_errors = {} 78 79 @catalog.vertices.each do |res| 80 begin 81 res.pre_run_check 82 rescue Puppet::Error => detail 83 prerun_errors[res] = detail 84 end 85 end 86 87 unless prerun_errors.empty? 88 prerun_errors.each do |res, detail| 89 res.log_exception(detail) 90 end 91 raise Puppet::Error, _("Some pre-run checks failed") 92 end 93 end
# File lib/puppet/transaction.rb 245 def prefetch_if_necessary(resource) 246 provider_class = resource.provider.class 247 if !provider_class.respond_to?(:prefetch) or 248 prefetched_providers[resource.type][provider_class.name] or 249 prefetch_failed_providers[resource.type][provider_class.name] 250 return 251 end 252 253 resources = resources_by_provider(resource.type, provider_class.name) 254 255 if provider_class == resource.class.defaultprovider 256 providerless_resources = resources_by_provider(resource.type, nil) 257 providerless_resources.values.each {|res| res.provider = provider_class.name} 258 resources.merge! providerless_resources 259 end 260 261 prefetch(provider_class, resources) 262 end
# File lib/puppet/transaction.rb 226 def relationship_graph 227 catalog.relationship_graph(@prioritizer) 228 end
# File lib/puppet/transaction.rb 230 def resource_status(resource) 231 report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource)) 232 end
Should this resource be skipped?
# File lib/puppet/transaction.rb 400 def skip?(resource) 401 if skip_tags?(resource) 402 resource.debug "Skipping with skip tags #{skip_tags.join(", ")}" 403 elsif missing_tags?(resource) 404 resource.debug "Not tagged with #{tags.join(", ")}" 405 elsif ! scheduled?(resource) 406 resource.debug "Not scheduled" 407 elsif failed_dependencies?(resource) 408 # When we introduced the :whit into the graph, to reduce the combinatorial 409 # explosion of edges, we also ended up reporting failures for containers 410 # like class and stage. This is undesirable; while just skipping the 411 # output isn't perfect, it is RC-safe. --daniel 2011-06-07 412 # With merge_dependency_warnings, warn about class dependency failures ... just once per class. TJK 2019-09-09 413 unless resource.class == Puppet::Type.type(:whit) 414 if @merge_dependency_warnings && resource.parent && failed_dependencies?(resource.parent) 415 ps = resource_status(resource.parent) 416 ps.failed_dependencies.find_all { |d| !(@failed_class_dependencies_already_warned.include?(d.ref)) }.each do |dep| 417 resource.parent.warning _("Skipping resources in class because of failed class dependencies") 418 @failed_class_dependencies_already_warned.add(dep.ref) 419 end 420 else 421 resource.warning _("Skipping because of failed dependencies") 422 end 423 end 424 elsif resource_status(resource).failed? && @prefetch_failed_providers[resource.type][resource.provider.class.name] 425 #Do not try to evaluate a resource with a known failed provider 426 resource.warning _("Skipping because provider prefetch failed") 427 elsif resource.virtual? 428 resource.debug "Skipping because virtual" 429 elsif !host_and_device_resource?(resource) && resource.appliable_to_host? && for_network_device 430 resource.debug "Skipping host resources because running on a device" 431 elsif !host_and_device_resource?(resource) && resource.appliable_to_device? && !for_network_device 432 resource.debug "Skipping device resources because running on a posix host" 433 else 434 return false 435 end 436 true 437 end
Wraps application run state check to flag need to interrupt processing
# File lib/puppet/transaction.rb 210 def stop_processing? 211 Puppet::Application.stop_requested? && catalog.host_config? 212 end
Private Instance Methods
# File lib/puppet/transaction.rb 390 def add_resource_status(status) 391 report.add_resource_status(status) 392 end
Apply all changes for a resource
# File lib/puppet/transaction.rb 267 def apply(resource, ancestor = nil) 268 status = resource_harness.evaluate(resource) 269 add_resource_status(status) 270 ancestor ||= resource 271 if !(status.failed? || status.failed_to_restart?) 272 event_manager.queue_events(ancestor, status.events) 273 end 274 rescue => detail 275 resource.err _("Could not evaluate: %{detail}") % { detail: detail } 276 end
Evaluate a single resource.
# File lib/puppet/transaction.rb 279 def eval_resource(resource, ancestor = nil) 280 resolve_resource(resource) 281 propagate_failure(resource) 282 if skip?(resource) 283 resource_status(resource).skipped = true 284 resource.debug("Resource is being skipped, unscheduling all events") 285 event_manager.dequeue_all_events_for_resource(resource) 286 persistence.copy_skipped(resource.ref) 287 else 288 resource_status(resource).scheduled = true 289 apply(resource, ancestor) 290 event_manager.process_events(resource) 291 end 292 end
Does this resource have any failed dependencies?
# File lib/puppet/transaction.rb 295 def failed_dependencies?(resource) 296 # When we introduced the :whit into the graph, to reduce the combinatorial 297 # explosion of edges, we also ended up reporting failures for containers 298 # like class and stage. This is undesirable; while just skipping the 299 # output isn't perfect, it is RC-safe. --daniel 2011-06-07 300 is_puppet_class = resource.class == Puppet::Type.type(:whit) 301 # With merge_dependency_warnings, notify about class dependency failures ... just once per class. TJK 2019-09-09 302 s = resource_status(resource) 303 if s && s.dependency_failed? 304 if @merge_dependency_warnings && is_puppet_class 305 # Notes: Puppet::Resource::Status.failed_dependencies() is an Array of Puppet::Resource(s) and 306 # Puppet::Resource.ref() calls Puppet::Resource.to_s() which is: "#{type}[#{title}]" and 307 # Puppet::Resource.resource_status(resource) calls Puppet::Resource.to_s() 308 class_dependencies_to_be_notified = (s.failed_dependencies.map(&:ref) - @failed_class_dependencies_already_notified.to_a) 309 class_dependencies_to_be_notified.each do |dep_ref| 310 resource.notice _("Class dependency %{dep} has failures: %{status}") % { dep: dep_ref, status: resource_status(dep_ref).failed } 311 end 312 @failed_class_dependencies_already_notified.merge(class_dependencies_to_be_notified) 313 else 314 unless @merge_dependency_warnings || is_puppet_class 315 s.failed_dependencies.find_all { |d| !(@failed_dependencies_already_notified.include?(d.ref)) }.each do |dep| 316 resource.notice _("Dependency %{dep} has failures: %{status}") % { dep: dep, status: resource_status(dep).failed } 317 @failed_dependencies_already_notified.add(dep.ref) 318 end 319 end 320 end 321 end 322 323 s && s.dependency_failed? 324 end
# File lib/puppet/transaction.rb 439 def host_and_device_resource?(resource) 440 resource.appliable_to_host? && resource.appliable_to_device? 441 end
Prefetch any providers that support it, yo. We don't support prefetching types, just providers.
# File lib/puppet/transaction.rb 374 def prefetch(provider_class, resources) 375 type_name = provider_class.resource_type.name 376 return if @prefetched_providers[type_name][provider_class.name] || 377 @prefetch_failed_providers[type_name][provider_class.name] 378 Puppet.debug { "Prefetching #{provider_class.name} resources for #{type_name}" } 379 begin 380 provider_class.prefetch(resources) 381 rescue LoadError, StandardError => detail 382 #TRANSLATORS `prefetch` is a function name and should not be translated 383 message = _("Could not prefetch %{type_name} provider '%{name}': %{detail}") % { type_name: type_name, name: provider_class.name, detail: detail } 384 Puppet.log_exception(detail, message) 385 @prefetch_failed_providers[type_name][provider_class.name] = true 386 end 387 @prefetched_providers[type_name][provider_class.name] = true 388 end
We need to know if a resource has any failed dependencies before we try to process it. We keep track of this by keeping a list on each resource of the failed dependencies, and incrementally computing it as the union of the failed dependencies of each first-order dependency. We have to do this as-we-go instead of up-front at failure time because the graph may be mutated as we walk it.
# File lib/puppet/transaction.rb 333 def propagate_failure(resource) 334 335 provider_class = resource.provider.class 336 s = resource_status(resource) 337 if prefetch_failed_providers[resource.type][provider_class.name] && !s.nil? 338 message = _("Prefetch failed for %{type_name} provider '%{name}'") % { type_name: resource.type, name: provider_class.name } 339 s.fail_with_event(message) 340 end 341 342 failed = Set.new 343 relationship_graph.direct_dependencies_of(resource).each do |dep| 344 s = resource_status(dep) 345 next if s.nil? 346 failed.merge(s.failed_dependencies) if s.dependency_failed? 347 failed.add(dep) if s.failed? || s.failed_to_restart? 348 end 349 resource_status(resource).failed_dependencies = failed.to_a 350 end
These two methods are only made public to enable the existing spec tests to run under rspec 3 (apparently rspec 2 didn't enforce access controls?). Please do not treat these as part of a public API. Possible future improvement: rewrite to not require access to private methods.
# File lib/puppet/transaction.rb 469 def resolve_resource(resource) 470 return unless catalog.host_config? 471 472 deferred_validate = false 473 474 resource.eachparameter do |param| 475 if param.value.instance_of?(Puppet::Pops::Evaluator::DeferredValue) 476 # Puppet::Parameter#value= triggers validation and munging. Puppet::Property#value= 477 # overrides the method, but also triggers validation and munging, since we're 478 # setting the desired/should value. 479 resolved = param.value.resolve 480 # resource.notice("Resolved deferred value to #{resolved}") 481 param.value = resolved 482 deferred_validate = true 483 end 484 end 485 486 if deferred_validate 487 resource.validate_resource 488 end 489 end
# File lib/puppet/transaction.rb 357 def resources_by_provider(type_name, provider_name) 358 unless @resources_by_provider 359 @resources_by_provider = Hash.new { |h, k| h[k] = Hash.new { |h1, k1| h1[k1] = {} } } 360 361 @catalog.vertices.each do |resource| 362 if resource.class.attrclass(:provider) 363 prov = resource.provider && resource.provider.class.name 364 @resources_by_provider[resource.type][prov][resource.name] = resource 365 end 366 end 367 end 368 369 @resources_by_provider[type_name][provider_name] || {} 370 end
Is the resource currently scheduled?
# File lib/puppet/transaction.rb 395 def scheduled?(resource) 396 self.ignoreschedules || resource_harness.scheduled?(resource) 397 end