File: configurer.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (755 lines) | stat: -rw-r--r-- 29,357 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
# The client for interacting with the puppetmaster config server.
require 'timeout'
require_relative '../puppet/util'
require 'securerandom'
#require 'puppet/parser/script_compiler'
require_relative '../puppet/pops/evaluator/deferred_resolver'

class Puppet::Configurer
  require_relative 'configurer/fact_handler'
  require_relative 'configurer/plugin_handler'

  include Puppet::Configurer::FactHandler

  # For benchmarking
  include Puppet::Util

  attr_reader :environment

  # Provide more helpful strings to the logging that the Agent does
  def self.to_s
    _("Puppet configuration client")
  end

  def self.should_pluginsync?
    if Puppet[:use_cached_catalog]
      false
    else
      true
    end
  end

  def execute_postrun_command
    execute_from_setting(:postrun_command)
  end

  def execute_prerun_command
    execute_from_setting(:prerun_command)
  end

  # Initialize and load storage
  def init_storage
      Puppet::Util::Storage.load
  rescue => detail
    Puppet.log_exception(detail, _("Removing corrupt state file %{file}: %{detail}") % { file: Puppet[:statefile], detail: detail })
    begin
      Puppet::FileSystem.unlink(Puppet[:statefile])
      retry
    rescue => detail
      raise Puppet::Error.new(_("Cannot remove %{file}: %{detail}") % { file: Puppet[:statefile], detail: detail }, detail)
    end
  end

  def initialize(transaction_uuid = nil, job_id = nil)
    @running = false
    @splayed = false
    @running_failure = false
    @cached_catalog_status = 'not_used'
    @environment = Puppet[:environment]
    @transaction_uuid = transaction_uuid || SecureRandom.uuid
    @job_id = job_id
    @static_catalog = true
    @checksum_type = Puppet[:supported_checksum_types]
    @handler = Puppet::Configurer::PluginHandler.new()
  end

  # Get the remote catalog, yo.  Returns nil if no catalog can be found.
  def retrieve_catalog(facts, query_options)
    query_options ||= {}
    if Puppet[:use_cached_catalog] || @running_failure
      result = retrieve_catalog_from_cache(query_options)
    end

    if result
      if Puppet[:use_cached_catalog]
        @cached_catalog_status = 'explicitly_requested'
      elsif @running_failure
        @cached_catalog_status = 'on_failure'
      end

      Puppet.info _("Using cached catalog from environment '%{environment}'") % { environment: result.environment }
    else
      result = retrieve_new_catalog(facts, query_options)

      if !result
        if !Puppet[:usecacheonfailure]
          Puppet.warning _("Not using cache on failed catalog")
          return nil
        end

        result = retrieve_catalog_from_cache(query_options)

        if result
          # don't use use cached catalog if it doesn't match server specified environment
          if result.environment != @environment
            Puppet.err _("Not using cached catalog because its environment '%{catalog_env}' does not match '%{local_env}'") % { catalog_env: result.environment, local_env: @environment }
            return nil
          end

          @cached_catalog_status = 'on_failure'
          Puppet.info _("Using cached catalog from environment '%{catalog_env}'") % { catalog_env: result.environment }
        end
      end
    end

    result
  end

  # Convert a plain resource catalog into our full host catalog.
  def convert_catalog(result, duration, facts, options = {})
    catalog = nil

    catalog_conversion_time = thinmark do
      # Will mutate the result and replace all Deferred values with resolved values
      if facts
        Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(facts, result, Puppet.lookup(:current_environment), Puppet[:preprocess_deferred])
      end

      catalog = result.to_ral
      catalog.finalize
      catalog.retrieval_duration = duration

      if Puppet[:write_catalog_summary]
        catalog.write_class_file
        catalog.write_resource_file
      end
    end
    options[:report].add_times(:convert_catalog, catalog_conversion_time) if options[:report]

    catalog
  end

  def warn_number_of_facts(size, max_number)
    Puppet.warning _("The current total number of facts: %{size} exceeds the number of facts limit: %{max_size}") % { size: size, max_size: max_number }
  end

  def warn_fact_name_length(name, max_length)
    Puppet.warning _("Fact %{name} with length: '%{length}' exceeds the length limit: %{limit}") % { name: name, length: name.to_s.bytesize, limit: max_length }
  end

  def warn_number_of_top_level_facts(size, max_number)
    Puppet.warning _("The current number of top level facts: %{size} exceeds the top facts limit: %{max_size}") % { size: size, max_size: max_number }
  end

  def warn_fact_value_length(value, max_length)
    Puppet.warning _("Fact value '%{value}' with the value length: '%{length}' exceeds the value length limit: %{max_length}") % { value: value, length:value.to_s.bytesize, max_length: max_length }
  end

  def warn_fact_payload_size(payload, max_size)
    Puppet.warning _("Payload with the current size of: '%{payload}' exceeds the payload size limit: %{max_size}") % { payload: payload, max_size: max_size }
  end

  def check_fact_name_length(name, number_of_dots)
    max_length = Puppet[:fact_name_length_soft_limit]
    return if max_length.zero?

    # rough byte size estimations of fact path as a postgresql btree index 
    size_as_btree_index = 8 + (number_of_dots * 2) + name.to_s.bytesize
    warn_fact_name_length(name, max_length) if size_as_btree_index > max_length
  end

  def check_fact_values_length(values)
    max_length = Puppet[:fact_value_length_soft_limit]
    return if max_length.zero?

    warn_fact_value_length(values, max_length) if values.to_s.bytesize > max_length
  end

  def check_top_level_number_limit(size)
    max_size = Puppet[:top_level_facts_soft_limit]
    return if max_size.zero?

    warn_number_of_top_level_facts(size, max_size) if size > max_size
  end

  def check_total_number_limit(size)
    max_size = Puppet[:number_of_facts_soft_limit]
    return if max_size.zero?

    warn_number_of_facts(size, max_size) if size > max_size
  end

  def check_payload_size(payload)
    max_size = Puppet[:payload_soft_limit]
    return if max_size.zero?

    warn_fact_payload_size(payload, max_size) if payload > max_size
    Puppet.debug _("The size of the payload is %{payload}") % {payload: payload}
  end

  def parse_fact_name_and_value_limits(object, path = [])
    case object
    when Hash
      object.each do |key, value|
        path.push(key)
        parse_fact_name_and_value_limits(value, path)
        path.pop
        @number_of_facts += 1
      end
    when Array
      object.each_with_index do |e, idx|
        path.push(idx)
        parse_fact_name_and_value_limits(e, path)
        path.pop
      end
    else
      check_fact_name_length(path.join(), path.size)
      check_fact_values_length(object)
    end
  end

  def check_facts_limits(facts)
    @number_of_facts = 0
    check_top_level_number_limit(facts.size)

    parse_fact_name_and_value_limits(facts)
    check_total_number_limit(@number_of_facts)
    Puppet.debug _("The total number of facts registered is %{number_of_facts}") % {number_of_facts: @number_of_facts}
  end

  def get_facts(options)
    if options[:pluginsync]
      plugin_sync_time = thinmark do
        remote_environment_for_plugins = Puppet::Node::Environment.remote(@environment)
        download_plugins(remote_environment_for_plugins)

        Puppet::GettextConfig.reset_text_domain('agent')
        Puppet::ModuleTranslations.load_from_vardir(Puppet[:vardir])
      end
      options[:report].add_times(:plugin_sync, plugin_sync_time) if options[:report]
    end

    facts_hash = {}
    facts = nil
    if Puppet::Resource::Catalog.indirection.terminus_class == :rest
      # This is a bit complicated.  We need the serialized and escaped facts,
      # and we need to know which format they're encoded in.  Thus, we
      # get a hash with both of these pieces of information.
      #
      # facts_for_uploading may set Puppet[:node_name_value] as a side effect
      facter_time = thinmark do
        facts = find_facts
        check_facts_limits(facts.to_data_hash['values'])
        facts_hash = encode_facts(facts) # encode for uploading # was: facts_for_uploading
        check_payload_size(facts_hash[:facts].bytesize)
      end
      options[:report].add_times(:fact_generation, facter_time) if options[:report]
    end
    [facts_hash, facts]
  end

  def prepare_and_retrieve_catalog(cached_catalog, facts, options, query_options)
    # set report host name now that we have the fact
    options[:report].host = Puppet[:node_name_value]

    query_options[:transaction_uuid] = @transaction_uuid
    query_options[:job_id] = @job_id
    query_options[:static_catalog] = @static_catalog

    # Query params don't enforce ordered evaluation, so munge this list into a
    # dot-separated string.
    query_options[:checksum_type] = @checksum_type.join('.')

    # apply passes in ral catalog
    catalog = cached_catalog || options[:catalog]
    unless catalog
      # retrieve_catalog returns resource catalog
      catalog = retrieve_catalog(facts, query_options)
      Puppet.err _("Could not retrieve catalog; skipping run") unless catalog
    end
    catalog
  end

  def prepare_and_retrieve_catalog_from_cache(options = {})
    result = retrieve_catalog_from_cache({:transaction_uuid => @transaction_uuid, :static_catalog => @static_catalog})
    Puppet.info _("Using cached catalog from environment '%{catalog_env}'") % { catalog_env: result.environment } if result
    result
  end

  # Apply supplied catalog and return associated application report
  def apply_catalog(catalog, options)
    report = options[:report]
    report.configuration_version = catalog.version

    benchmark(:notice, _("Applied catalog in %{seconds} seconds")) do
      apply_catalog_time = thinmark do
        catalog.apply(options)
      end
      options[:report].add_times(:catalog_application, apply_catalog_time)
    end

    report
  end

  # The code that actually runs the catalog.
  # This just passes any options on to the catalog,
  # which accepts :tags and :ignoreschedules.
  def run(options = {})
    # We create the report pre-populated with default settings for
    # environment and transaction_uuid very early, this is to ensure
    # they are sent regardless of any catalog compilation failures or
    # exceptions.
    options[:report] ||= Puppet::Transaction::Report.new(nil, @environment, @transaction_uuid, @job_id, options[:start_time] || Time.now)
    report = options[:report]
    init_storage

    Puppet::Util::Log.newdestination(report)

    completed = nil
    begin
      # Skip failover logic if the server_list setting is empty
      do_failover = Puppet.settings[:server_list] && !Puppet.settings[:server_list].empty?

      # When we are passed a catalog, that means we're in apply
      # mode. We shouldn't try to do any failover in that case.
      if options[:catalog].nil? && do_failover
        server, port = find_functional_server
        if server.nil?
          detail = _("Could not select a functional puppet server from server_list: '%{server_list}'") % { server_list: Puppet.settings.value(:server_list, Puppet[:environment].to_sym, true) }
          if Puppet[:usecacheonfailure]
            options[:pluginsync] = false
            @running_failure = true

            server = Puppet[:server_list].first[0]
            port = Puppet[:server_list].first[1] || Puppet[:serverport]

            Puppet.err(detail)
          else
            raise Puppet::Error, detail
          end
        else
          #TRANSLATORS 'server_list' is the name of a setting and should not be translated
          Puppet.debug _("Selected puppet server from the `server_list` setting: %{server}:%{port}") % { server: server, port: port }
          report.server_used = "#{server}:#{port}"
        end
        Puppet.override(server: server, serverport: port) do
          completed = run_internal(options)
        end
      else
        completed = run_internal(options)
      end
    ensure
      # we may sleep for awhile, close connections now
      Puppet.runtime[:http].close
    end

    completed ? report.exit_status : nil
  end

  def run_internal(options)
    report = options[:report]
    report.initial_environment = Puppet[:environment]

    if options[:start_time]
      startup_time = Time.now - options[:start_time]
      report.add_times(:startup_time, startup_time)
    end

    # If a cached catalog is explicitly requested, attempt to retrieve it. Skip the node request,
    # don't pluginsync and switch to the catalog's environment if we successfully retrieve it.
    if Puppet[:use_cached_catalog]
      Puppet::GettextConfig.reset_text_domain('agent')
      Puppet::ModuleTranslations.load_from_vardir(Puppet[:vardir])

      cached_catalog = prepare_and_retrieve_catalog_from_cache(options)
      if cached_catalog
        @cached_catalog_status = 'explicitly_requested'

        if @environment != cached_catalog.environment && !Puppet[:strict_environment_mode]
          Puppet.notice _("Local environment: '%{local_env}' doesn't match the environment of the cached catalog '%{catalog_env}', switching agent to '%{catalog_env}'.") % { local_env: @environment, catalog_env: cached_catalog.environment }
          @environment = cached_catalog.environment
        end

        report.environment = @environment
      else
        # Don't try to retrieve a catalog from the cache again after we've already
        # failed to do so the first time.
        Puppet[:use_cached_catalog] = false
        Puppet[:usecacheonfailure] = false
        options[:pluginsync] = Puppet::Configurer.should_pluginsync?
      end
    end

    begin
      unless Puppet[:node_name_fact].empty?
        query_options, facts = get_facts(options)
      end

      configured_environment = Puppet[:environment] if Puppet.settings.set_by_config?(:environment)

      # We only need to find out the environment to run in if we don't already have a catalog
      unless (cached_catalog || options[:catalog] || Puppet.settings.set_by_cli?(:environment) || Puppet[:strict_environment_mode])
        Puppet.debug(_("Environment not passed via CLI and no catalog was given, attempting to find out the last server-specified environment"))
        initial_environment, loaded_last_environment = last_server_specified_environment

        unless Puppet[:use_last_environment] && loaded_last_environment
          Puppet.debug(_("Requesting environment from the server"))
          initial_environment = current_server_specified_environment(@environment, configured_environment, options)
        end

        if initial_environment
          @environment = initial_environment
          report.environment = initial_environment

          push_current_environment_and_loaders
        else
          Puppet.debug(_("Could not find a usable environment in the lastrunfile. Either the file does not exist, does not have the required keys, or the values of 'initial_environment' and 'converged_environment' are identical."))
        end
      end

      Puppet.info _("Using environment '%{env}'") % { env: @environment }

      # This is to maintain compatibility with anyone using this class
      # aside from agent, apply, device.
      unless Puppet.lookup(:loaders) { nil }
        push_current_environment_and_loaders
      end

      temp_value = options[:pluginsync]

      # only validate server environment if pluginsync is requested
      options[:pluginsync] = valid_server_environment? if options[:pluginsync]

      query_options, facts = get_facts(options) unless query_options
      options[:pluginsync] = temp_value

      query_options[:configured_environment] = configured_environment

      catalog = prepare_and_retrieve_catalog(cached_catalog, facts, options, query_options)
      unless catalog
        return nil
      end

      if Puppet[:strict_environment_mode] && catalog.environment != @environment
        Puppet.err _("Not using catalog because its environment '%{catalog_env}' does not match agent specified environment '%{local_env}' and strict_environment_mode is set") % { catalog_env: catalog.environment, local_env: @environment }
        return nil
      end

      # Here we set the local environment based on what we get from the
      # catalog. Since a change in environment means a change in facts, and
      # facts may be used to determine which catalog we get, we need to
      # rerun the process if the environment is changed.
      tries = 0
      while catalog.environment and not catalog.environment.empty? and catalog.environment != @environment
        if tries > 3
          raise Puppet::Error, _("Catalog environment didn't stabilize after %{tries} fetches, aborting run") % { tries: tries }
        end
        Puppet.notice _("Local environment: '%{local_env}' doesn't match server specified environment '%{catalog_env}', restarting agent run with environment '%{catalog_env}'") % { local_env: @environment, catalog_env: catalog.environment }
        @environment = catalog.environment
        report.environment = @environment

        push_current_environment_and_loaders

        query_options, facts = get_facts(options)
        query_options[:configured_environment] = configured_environment

        # if we get here, ignore the cached catalog
        catalog = prepare_and_retrieve_catalog(nil, facts, options, query_options)
        return nil unless catalog
        tries += 1
      end

      # now that environment has converged, convert resource catalog into ral catalog
      # unless we were given a RAL catalog
      if !cached_catalog && options[:catalog]
        ral_catalog = options[:catalog]
      else
        # Ordering here matters. We have to resolve deferred resources in the
        # resource catalog, convert the resource catalog to a RAL catalog (which
        # triggers type/provider validation), and only if that is successful,
        # should we cache the *original* resource catalog. However, deferred
        # evaluation mutates the resource catalog, so we need to make a copy of
        # it here. If PUP-9323 is ever implemented so that we resolve deferred
        # resources in the RAL catalog as they are needed, then we could eliminate
        # this step.
        catalog_to_cache = Puppet.override(:rich_data => Puppet[:rich_data]) do
          Puppet::Resource::Catalog.from_data_hash(catalog.to_data_hash)
        end

        # REMIND @duration is the time spent loading the last catalog, and doesn't
        # account for things like we failed to download and fell back to the cache
        ral_catalog = convert_catalog(catalog, @duration, facts, options)

        # Validation succeeded, so commit the `catalog_to_cache` for non-noop runs. Don't
        # commit `catalog` since it contains the result of deferred evaluation. Ideally
        # we'd just copy the downloaded response body, instead of serializing the
        # in-memory catalog, but that's hard due to the indirector.
        indirection = Puppet::Resource::Catalog.indirection
        if !Puppet[:noop] && indirection.cache?
          request = indirection.request(:save, nil, catalog_to_cache, environment: Puppet::Node::Environment.remote(catalog_to_cache.environment))
          Puppet.info("Caching catalog for #{request.key}")
          indirection.cache.save(request)
        end
      end

      execute_prerun_command or return nil

      options[:report].code_id = ral_catalog.code_id
      options[:report].catalog_uuid = ral_catalog.catalog_uuid
      options[:report].cached_catalog_status = @cached_catalog_status
      apply_catalog(ral_catalog, options)
      true
    rescue => detail
      Puppet.log_exception(detail, _("Failed to apply catalog: %{detail}") % { detail: detail })
      return nil
    ensure
      execute_postrun_command or return nil
    end
  ensure
    if Puppet[:resubmit_facts]
      # TODO: Should mark the report as "failed" if an error occurs and
      #       resubmit_facts returns false. There is currently no API for this.
      resubmit_facts_time = thinmark { resubmit_facts }

      report.add_times(:resubmit_facts, resubmit_facts_time)
    end

    report.cached_catalog_status ||= @cached_catalog_status
    report.add_times(:total, Time.now - report.time)
    report.finalize_report
    Puppet::Util::Log.close(report)
    send_report(report)
    Puppet.pop_context
  end
  private :run_internal

  def valid_server_environment?
    session = Puppet.lookup(:http_session)
    begin
      fs = session.route_to(:fileserver)
      fs.get_file_metadatas(path: URI(Puppet[:pluginsource]).path, recurse: :false, environment: @environment)
      true
    rescue Puppet::HTTP::ResponseError => detail
      if detail.response.code == 404
        if Puppet[:strict_environment_mode]
          raise Puppet::Error.new(_("Environment '%{environment}' not found on server, aborting run.") % { environment: @environment })
        else
          Puppet.notice(_("Environment '%{environment}' not found on server, skipping initial pluginsync.") % { environment: @environment })
        end
      else
        Puppet.log_exception(detail, detail.message)
      end
      false
    rescue => detail
      Puppet.log_exception(detail, detail.message)
      false
    end
  end

  def find_functional_server
    begin
      session = Puppet.lookup(:http_session)
      service = session.route_to(:puppet)
      return [service.url.host, service.url.port]
    rescue Puppet::HTTP::ResponseError => e
      Puppet.debug(_("Puppet server %{host}:%{port} is unavailable: %{code} %{reason}") %
                   { host: e.response.url.host, port: e.response.url.port, code: e.response.code, reason: e.response.reason })
    rescue => detail
      #TRANSLATORS 'server_list' is the name of a setting and should not be translated
      Puppet.debug _("Unable to connect to server from server_list setting: %{detail}") % {detail: detail}
    end
    [nil, nil]
  end
  private :find_functional_server

  #
  # @api private
  #
  # Read the last server-specified environment from the lastrunfile. The
  # environment is considered to be server-specified if the values of
  # `initial_environment` and `converged_environment` are different.
  #
  # @return [String, Boolean] An array containing a string with the environment
  #   read from the lastrunfile in case the server is authoritative, and a
  #   boolean marking whether the last environment was correctly loaded.
  def last_server_specified_environment
    return @last_server_specified_environment, @loaded_last_environment if @last_server_specified_environment

    if Puppet::FileSystem.exist?(Puppet[:lastrunfile])
      summary = Puppet::Util::Yaml.safe_load_file(Puppet[:lastrunfile])
      return [nil, nil] unless summary['application']['run_mode'] == 'agent'
      initial_environment = summary['application']['initial_environment']
      converged_environment = summary['application']['converged_environment']
      @last_server_specified_environment = converged_environment if initial_environment != converged_environment
      Puppet.debug(_("Successfully loaded last environment from the lastrunfile"))
      @loaded_last_environment = true
    end

    Puppet.debug(_("Found last server-specified environment: %{environment}") % { environment: @last_server_specified_environment }) if @last_server_specified_environment
    [@last_server_specified_environment, @loaded_last_environment]
  rescue => detail
    Puppet.debug(_("Could not find last server-specified environment: %{detail}") % { detail: detail })
    [nil, nil]
  end
  private :last_server_specified_environment

  def current_server_specified_environment(current_environment, configured_environment, options)
    return @server_specified_environment if @server_specified_environment

    begin
      node_retr_time = thinmark do
        node = Puppet::Node.indirection.find(Puppet[:node_name_value],
                                             :environment => Puppet::Node::Environment.remote(current_environment),
                                             :configured_environment => configured_environment,
                                             :ignore_cache => true,
                                             :transaction_uuid => @transaction_uuid,
                                             :fail_on_404 => true)

        @server_specified_environment = node.environment_name.to_s

        if @server_specified_environment != @environment
          Puppet.notice _("Local environment: '%{local_env}' doesn't match server specified node environment '%{node_env}', switching agent to '%{node_env}'.") % { local_env: @environment, node_env: @server_specified_environment }
        end
      end

      options[:report].add_times(:node_retrieval, node_retr_time)

      @server_specified_environment
    rescue => detail
      Puppet.warning(_("Unable to fetch my node definition, but the agent run will continue:"))
      Puppet.warning(detail)
      nil
    end
  end
  private :current_server_specified_environment

  def send_report(report)
    puts report.summary if Puppet[:summarize]
    save_last_run_summary(report)
    if Puppet[:report]
      remote = Puppet::Node::Environment.remote(@environment)
      begin
        Puppet::Transaction::Report.indirection.save(report, nil, ignore_cache: true, environment: remote)
      ensure
        Puppet::Transaction::Report.indirection.save(report, nil, ignore_terminus: true, environment: remote)
      end
    end
  rescue => detail
    Puppet.log_exception(detail, _("Could not send report: %{detail}") % { detail: detail })
  end

  def save_last_run_summary(report)
    mode = Puppet.settings.setting(:lastrunfile).mode
    Puppet::Util.replace_file(Puppet[:lastrunfile], mode) do |fh|
      fh.print YAML.dump(report.raw_summary)
    end
  rescue => detail
    Puppet.log_exception(detail, _("Could not save last run local report: %{detail}") % { detail: detail })
  end

  # Submit updated facts to the Puppet Server
  #
  # This method will clear all current fact values, load a fresh set of
  # fact data, and then submit it to the Puppet Server.
  #
  # @return [true] If fact submission succeeds.
  # @return [false] If an exception is raised during fact generation or
  #   submission.
  def resubmit_facts
    Puppet.runtime[:facter].clear
    facts = find_facts

    client = Puppet.runtime[:http]
    session = client.create_session
    puppet = session.route_to(:puppet)

    Puppet.info(_("Uploading facts for %{node} to %{server}") % {
                  node: facts.name,
                  server: puppet.url.hostname})

    puppet.put_facts(facts.name, facts: facts, environment: Puppet.lookup(:current_environment).name.to_s)

    return true
  rescue => detail
    Puppet.log_exception(detail, _("Failed to submit facts: %{detail}") %
                                 { detail: detail })

    return false
  end

  private

  def execute_from_setting(setting)
    return true if (command = Puppet[setting]) == ""

    begin
      Puppet::Util::Execution.execute([command])
      true
    rescue => detail
      Puppet.log_exception(detail, _("Could not run command from %{setting}: %{detail}") % { setting: setting, detail: detail })
      false
    end
  end

  def push_current_environment_and_loaders
    new_env = Puppet::Node::Environment.remote(@environment)
    Puppet.push_context(
      {
        :current_environment => new_env,
        :loaders => Puppet::Pops::Loaders.new(new_env, true)
      },
      "Local node environment #{@environment} for configurer transaction"
    )
  end

  def retrieve_catalog_from_cache(query_options)
    result = nil
    @duration = thinmark do
      result = Puppet::Resource::Catalog.indirection.find(
        Puppet[:node_name_value],
        query_options.merge(
          :ignore_terminus => true,
          :environment     => Puppet::Node::Environment.remote(@environment)
        )
      )
    end
    result
  rescue => detail
    Puppet.log_exception(detail, _("Could not retrieve catalog from cache: %{detail}") % { detail: detail })
    return nil
  end

  def retrieve_new_catalog(facts, query_options)
    result = nil
    @duration = thinmark do
      result = Puppet::Resource::Catalog.indirection.find(
        Puppet[:node_name_value],
        query_options.merge(
          :ignore_cache      => true,
          # don't update cache until after environment converges
          :ignore_cache_save => true,
          :environment       => Puppet::Node::Environment.remote(@environment),
          :check_environment => true,
          :fail_on_404       => true,
          :facts_for_catalog => facts
        )
      )
    end
    result
  rescue StandardError => detail
    Puppet.log_exception(detail, _("Could not retrieve catalog from remote server: %{detail}") % { detail: detail })
    return nil
  end

  def download_plugins(remote_environment_for_plugins)
    begin
      @handler.download_plugins(remote_environment_for_plugins)
    rescue Puppet::Error => detail
      if !Puppet[:ignore_plugin_errors] && Puppet[:usecacheonfailure]
        @running_failure = true
      else
        raise detail
      end
    end
  end
end