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