bolt 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87e8cf7f5ccf5d0649f608b088e4162495b6f5c1c2887711ab6f4355b6a3934f
4
- data.tar.gz: 0bbac1eed6b37508b1e1f1b90f67a7b687f089415e4daf333e63e71d784c3538
3
+ metadata.gz: 575cccb1487a82a6537a83d5d23a89d9e3ed44898af25e2fe34acda2ecd765ae
4
+ data.tar.gz: a9de53717fdcb5380f95ef5ee1fb26b3d28eaa59f92355d9558954d14192f307
5
5
  SHA512:
6
- metadata.gz: b534fbfc3a4b15ad9b130a24fb3a1274f05592d9dedb185031fe409364e9e0c10b1af5c90571f0e9a9fd0b5d70817d2e5edf17bf25c9750ac801c0e83c872766
7
- data.tar.gz: 3032fed7c7493eeb06d97814eee9496c634522905b36ec67e794da36581815a5786d35afb2f413484c3b5b2b578e8107e7b0c224dfb3003095dc47d0d8a643ec
6
+ metadata.gz: 60378d6872763f5fb557deae88207c29db0626d90562e37d80df1f604368629049da79ff0a0216602057566c7eb10892855f33a99b03bfc348fce8b443e7682d
7
+ data.tar.gz: e2f8dd460e346648e53921e299db5b7beaf4ba1a0cd7efe95728e2e2d42c3e4b3624c40ef8a94a21b7e4be5003d67a1a64c48d1a36a9e314c4d7bb296ce85eab
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Send a command with a payload to PuppetDB.
6
+ #
7
+ # The `pdb_command` function only supports version 5 of the `replace_facts`
8
+ # command. Other commands might also work, but are not tested or supported
9
+ # by Bolt.
10
+ #
11
+ # See the [commands endpoint](https://puppet.com/docs/puppetdb/latest/api/command/v1/commands.html)
12
+ # documentation for more information about available commands and payload
13
+ # format.
14
+ #
15
+ # _This function is experimental and subject to change._
16
+ #
17
+ # > **Note:** Not available in apply block
18
+ #
19
+ Puppet::Functions.create_function(:puppetdb_command) do
20
+ # @param command The command to invoke.
21
+ # @param version The version of the command to invoke.
22
+ # @param payload The payload to the command.
23
+ # @return The UUID identifying the response sent by PuppetDB.
24
+ # @example Replace facts for a target
25
+ # $payload = {
26
+ # 'certname' => 'localhost',
27
+ # 'environment' => 'dev',
28
+ # 'producer' => 'bolt',
29
+ # 'producer_timestamp' => '1970-01-01',
30
+ # 'values' => { 'orchestrator' => 'bolt' }
31
+ # }
32
+ #
33
+ # puppetdb_command('replace_facts', 5, $payload)
34
+ dispatch :puppetdb_command do
35
+ param 'String[1]', :command
36
+ param 'Integer', :version
37
+ param 'Hash[Data, Data]', :payload
38
+ return_type 'String'
39
+ end
40
+
41
+ def puppetdb_command(command, version, payload)
42
+ # Disallow in apply blocks.
43
+ unless Puppet[:tasks]
44
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
45
+ Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
46
+ action: 'puppetdb_command'
47
+ )
48
+ end
49
+
50
+ # Send analytics report.
51
+ Puppet.lookup(:bolt_executor).report_function_call(self.class.name)
52
+
53
+ puppetdb_client = Puppet.lookup(:bolt_pdb_client)
54
+
55
+ # Error if the PDB client does not implement :send_command
56
+ unless puppetdb_client.respond_to?(:send_command)
57
+ raise Bolt::Error.new(
58
+ "PuppetDB client #{puppetdb_client.class} does not implement :send_command, "\
59
+ "unable to invoke command.",
60
+ 'bolt/pdb-command'
61
+ )
62
+ end
63
+
64
+ puppetdb_client.send_command(command, version, payload)
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ TOPIC
2
+ targets
3
+
4
+ DESCRIPTION
5
+ A target is a device that Bolt connects to and runs actions on. Targets can
6
+ be physical, such as servers, or virtual, such as containers or virtual
7
+ machines.
8
+
9
+ Several of Bolt's commands connect to targets and run actions on them. When
10
+ you run these commands, Bolt requires a list of targets to run the action
11
+ on. For example, the `bolt script run` command and `Invoke-BoltScript`
12
+ PowerShell cmdlet require you to provide a list of targets to run your
13
+ script on. You can provide a list of targets to a command using one of the
14
+ following command-line options:
15
+
16
+ Shell commands
17
+ -t, --targets TARGETS
18
+ -q, --query QUERY
19
+ --rerun FILTER
20
+
21
+ PowerShell cmdlets
22
+ -T, -Targets TARGETS
23
+ -Q, -Query QUERY
24
+ -Rerun FILTER
25
+
26
+ Typically, targets and their configuration and data are listed in a
27
+ project's inventory file. For more information about inventory files,
28
+ see 'bolt guide inventory'.
29
+
30
+ DOCUMENTATION
31
+ https://pup.pt/bolt-commands
@@ -190,7 +190,8 @@ module Bolt
190
190
  apply
191
191
 
192
192
  USAGE
193
- bolt apply [manifest.pp] [options]
193
+ bolt apply [manifest] {--targets TARGETS | --query QUERY | --rerun FILTER}
194
+ [options]
194
195
 
195
196
  DESCRIPTION
196
197
  Apply Puppet manifest code on the specified targets.
@@ -219,7 +220,8 @@ module Bolt
219
220
  run
220
221
 
221
222
  USAGE
222
- bolt command run <command> [options]
223
+ bolt command run <command> {--targets TARGETS | --query QUERY | --rerun FILTER}
224
+ [options]
223
225
 
224
226
  DESCRIPTION
225
227
  Run a command on the specified targets.
@@ -248,7 +250,8 @@ module Bolt
248
250
  download
249
251
 
250
252
  USAGE
251
- bolt file download <src> <dest> [options]
253
+ bolt file download <source> <destination> {--targets TARGETS | --query QUERY | --rerun FILTER}
254
+ [options]
252
255
 
253
256
  DESCRIPTION
254
257
  Download a file or directory from one or more targets.
@@ -267,7 +270,8 @@ module Bolt
267
270
  upload
268
271
 
269
272
  USAGE
270
- bolt file upload <src> <dest> [options]
273
+ bolt file upload <source> <destination> {--targets TARGETS | --query QUERY | --rerun FILTER}
274
+ [options]
271
275
 
272
276
  DESCRIPTION
273
277
  Upload a local file or directory.
@@ -343,7 +347,12 @@ module Bolt
343
347
  bolt inventory show [options]
344
348
 
345
349
  DESCRIPTION
346
- Show the list of targets an action would run on.
350
+ Show the list of targets an action would run on. This command will list
351
+ all targets in the project's inventory by default.
352
+
353
+ To filter the targets in the list, use the --targets, --query, or --rerun
354
+ options. To view detailed configuration and data for targets, use the
355
+ --detail option.
347
356
  HELP
348
357
 
349
358
  MODULE_HELP = <<~HELP
@@ -432,7 +441,7 @@ module Bolt
432
441
  plan
433
442
 
434
443
  USAGE
435
- bolt plan <action> [parameters] [options]
444
+ bolt plan <action> [options]
436
445
 
437
446
  DESCRIPTION
438
447
  Convert, create, show, and run Bolt plans.
@@ -449,16 +458,18 @@ module Bolt
449
458
  convert
450
459
 
451
460
  USAGE
452
- bolt plan convert <path> [options]
461
+ bolt plan convert <plan name> [options]
453
462
 
454
463
  DESCRIPTION
455
- Convert a YAML plan to a Puppet language plan and print the converted plan to stdout.
464
+ Convert a YAML plan to a Puppet language plan and print the converted
465
+ plan to stdout.
456
466
 
457
467
  Converting a YAML plan might result in a plan that is syntactically
458
468
  correct but has different behavior. Always verify a converted plan's
459
469
  functionality. Note that the converted plan is not written to a file.
460
470
 
461
471
  EXAMPLES
472
+ bolt plan convert myproject::myplan
462
473
  bolt plan convert path/to/plan/myplan.yaml
463
474
  HELP
464
475
 
@@ -467,7 +478,7 @@ module Bolt
467
478
  new
468
479
 
469
480
  USAGE
470
- bolt plan new <plan> [options]
481
+ bolt plan new <plan name> [options]
471
482
 
472
483
  DESCRIPTION
473
484
  Create a new plan in the current project.
@@ -481,7 +492,7 @@ module Bolt
481
492
  run
482
493
 
483
494
  USAGE
484
- bolt plan run <plan> [parameters] [options]
495
+ bolt plan run <plan name> [parameters] [options]
485
496
 
486
497
  DESCRIPTION
487
498
  Run a plan on the specified targets.
@@ -495,7 +506,7 @@ module Bolt
495
506
  show
496
507
 
497
508
  USAGE
498
- bolt plan show [plan] [options]
509
+ bolt plan show [plan name] [options]
499
510
 
500
511
  DESCRIPTION
501
512
  Show available plans and plan documentation.
@@ -557,7 +568,8 @@ module Bolt
557
568
  bolt project migrate [options]
558
569
 
559
570
  DESCRIPTION
560
- Migrate a Bolt project to use current best practices and the latest version of configuration files.
571
+ Migrate a Bolt project to use current best practices and the latest version of
572
+ configuration files.
561
573
  HELP
562
574
 
563
575
  SCRIPT_HELP = <<~HELP
@@ -579,7 +591,8 @@ module Bolt
579
591
  run
580
592
 
581
593
  USAGE
582
- bolt script run <script> [arguments] [options]
594
+ bolt script run <script> [arguments] {--targets TARGETS | --query QUERY | --rerun FILTER}
595
+ [options]
583
596
 
584
597
  DESCRIPTION
585
598
  Run a script on the specified targets.
@@ -661,7 +674,8 @@ module Bolt
661
674
  run
662
675
 
663
676
  USAGE
664
- bolt task run <task> [parameters] [options]
677
+ bolt task run <task name> [parameters] {--targets TARGETS | --query QUERY | --rerun FILTER}
678
+ [options]
665
679
 
666
680
  DESCRIPTION
667
681
  Run a task on the specified targets.
@@ -677,7 +691,7 @@ module Bolt
677
691
  show
678
692
 
679
693
  USAGE
680
- bolt task show [task] [options]
694
+ bolt task show [task name] [options]
681
695
 
682
696
  DESCRIPTION
683
697
  Show available tasks and task documentation.
@@ -710,7 +724,8 @@ module Bolt
710
724
  "SSH is the default protocol; can be #{TRANSPORTS.keys.join(', ')}",
711
725
  'For Windows targets, specify the winrm:// protocol if it has not be configured',
712
726
  'For SSH, port defaults to `22`',
713
- 'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
727
+ 'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting',
728
+ "For more information, see 'bolt guide targets'.") do |targets|
714
729
  @options[:targets] ||= []
715
730
  @options[:targets] << Bolt::Util.get_arg_input(targets)
716
731
  end
@@ -888,7 +903,10 @@ module Bolt
888
903
  define('-v', '--[no-]verbose', 'Display verbose logging') do |value|
889
904
  @options[:verbose] = value
890
905
  end
891
- define('--stream', 'Stream output from scripts and commands to the console') do |_|
906
+ define('--stream',
907
+ 'Stream output from scripts and commands to the console.',
908
+ 'Run with --no-verbose to prevent Bolt from displaying output',
909
+ 'a second time after the action is completed.') do |_|
892
910
  @options[:stream] = true
893
911
  end
894
912
  define('--trace', 'Display error stack traces') do |_|
data/lib/bolt/cli.rb CHANGED
@@ -47,6 +47,8 @@ module Bolt
47
47
  'guide' => %w[]
48
48
  }.freeze
49
49
 
50
+ TARGETING_OPTIONS = %i[query rerun targets].freeze
51
+
50
52
  attr_reader :config, :options
51
53
 
52
54
  def initialize(argv)
@@ -262,7 +264,7 @@ module Bolt
262
264
  end
263
265
 
264
266
  def update_targets(options)
265
- target_opts = options.keys.select { |opt| %i[query rerun targets].include?(opt) }
267
+ target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
266
268
  target_string = "'--targets', '--rerun', or '--query'"
267
269
  if target_opts.length > 1
268
270
  raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
@@ -634,13 +636,33 @@ module Bolt
634
636
  end
635
637
 
636
638
  def list_targets
637
- inventoryfile = config.inventoryfile || config.default_inventoryfile
638
- outputter.print_targets(group_targets_by_source, inventoryfile)
639
+ if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
640
+ target_flag = true
641
+ else
642
+ options[:targets] = 'all'
643
+ end
644
+
645
+ outputter.print_targets(
646
+ group_targets_by_source,
647
+ inventory.source,
648
+ config.default_inventoryfile,
649
+ target_flag
650
+ )
639
651
  end
640
652
 
641
653
  def show_targets
642
- inventoryfile = config.inventoryfile || config.default_inventoryfile
643
- outputter.print_target_info(group_targets_by_source, inventoryfile)
654
+ if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
655
+ target_flag = true
656
+ else
657
+ options[:targets] = 'all'
658
+ end
659
+
660
+ outputter.print_target_info(
661
+ group_targets_by_source,
662
+ inventory.source,
663
+ config.default_inventoryfile,
664
+ target_flag
665
+ )
644
666
  end
645
667
 
646
668
  # Returns a hash of targets sorted by those that are found in the
@@ -661,8 +683,7 @@ module Bolt
661
683
  end
662
684
 
663
685
  def list_groups
664
- groups = inventory.group_names
665
- outputter.print_groups(groups)
686
+ outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
666
687
  end
667
688
 
668
689
  def run_plan(plan_name, plan_arguments, nodes, options)
@@ -86,6 +86,7 @@ module Bolt
86
86
  if config.default_inventoryfile.exist?
87
87
  logger.debug("Loaded inventory from #{config.default_inventoryfile}")
88
88
  else
89
+ source = nil
89
90
  logger.debug("Tried to load inventory from #{config.default_inventoryfile}, but the file does not exist")
90
91
  end
91
92
  end
@@ -100,17 +101,17 @@ module Bolt
100
101
  validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
101
102
  end
102
103
 
103
- inventory = create_version(data, config.transport, config.transports, plugins)
104
+ inventory = create_version(data, config.transport, config.transports, plugins, source)
104
105
  inventory.validate
105
106
  inventory
106
107
  end
107
108
 
108
- def self.create_version(data, transport, transports, plugins)
109
+ def self.create_version(data, transport, transports, plugins, source = nil)
109
110
  version = (data || {}).delete('version') { 2 }
110
111
 
111
112
  case version
112
113
  when 2
113
- Bolt::Inventory::Inventory.new(data, transport, transports, plugins)
114
+ Bolt::Inventory::Inventory.new(data, transport, transports, plugins, source)
114
115
  else
115
116
  raise ValidationError.new("Unsupported version #{version} specified in inventory", nil)
116
117
  end
@@ -120,7 +121,7 @@ module Bolt
120
121
  config = Bolt::Config.default
121
122
  plugins = Bolt::Plugin.setup(config, nil)
122
123
 
123
- create_version({}, config.transport, config.transports, plugins)
124
+ create_version({}, config.transport, config.transports, plugins, nil)
124
125
  end
125
126
  end
126
127
  end
@@ -6,7 +6,7 @@ require 'bolt/inventory/target'
6
6
  module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
- attr_reader :targets, :plugins, :config, :transport
9
+ attr_reader :config, :plugins, :source, :targets, :transport
10
10
 
11
11
  class WildcardError < Bolt::Error
12
12
  def initialize(target)
@@ -15,7 +15,7 @@ module Bolt
15
15
  end
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
- def initialize(data, transport, transports, plugins)
18
+ def initialize(data, transport, transports, plugins, source = nil)
19
19
  @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
@@ -24,6 +24,7 @@ module Bolt
24
24
  @groups = Group.new(@data, plugins, all_group: true)
25
25
  @group_lookup = {}
26
26
  @targets = {}
27
+ @source = source
27
28
 
28
29
  @groups.resolve_string_targets(@groups.target_aliases, @groups.all_targets)
29
30
 
@@ -483,7 +483,7 @@ module Bolt
483
483
  end
484
484
  end
485
485
 
486
- def print_targets(target_list, inventoryfile)
486
+ def print_targets(target_list, inventory_source, default_inventory, target_flag)
487
487
  adhoc = colorize(:yellow, "(Not found in inventory file)")
488
488
 
489
489
  targets = []
@@ -501,16 +501,13 @@ module Bolt
501
501
  end
502
502
  info << "\n\n"
503
503
 
504
- @stream.puts info
504
+ info << format_inventory_source(inventory_source, default_inventory)
505
+ info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
505
506
 
506
- print_inventory_summary(
507
- target_list[:inventory].count,
508
- target_list[:adhoc].count,
509
- inventoryfile
510
- )
507
+ @stream.puts info
511
508
  end
512
509
 
513
- def print_target_info(target_list, inventoryfile)
510
+ def print_target_info(target_list, inventory_source, default_inventory, target_flag)
514
511
  adhoc_targets = target_list[:adhoc].map(&:name).to_set
515
512
  inventory_targets = target_list[:inventory].map(&:name).to_set
516
513
  targets = target_list.values.flatten.sort_by(&:name)
@@ -532,26 +529,27 @@ module Bolt
532
529
  info << indent(2, "No targets\n\n")
533
530
  end
534
531
 
535
- @stream.puts info
532
+ info << format_inventory_source(inventory_source, default_inventory)
533
+ info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
536
534
 
537
- print_inventory_summary(
538
- inventory_targets.count,
539
- adhoc_targets.count,
540
- inventoryfile
541
- )
535
+ @stream.puts info
542
536
  end
543
537
 
544
- private def print_inventory_summary(inventory_count, adhoc_count, inventoryfile)
538
+ private def format_inventory_source(inventory_source, default_inventory)
545
539
  info = +''
546
540
 
547
541
  # Add inventory file source
548
- info << colorize(:cyan, "Inventory file\n")
549
- info << if File.exist?(inventoryfile)
550
- indent(2, "#{inventoryfile}\n")
542
+ info << colorize(:cyan, "Inventory source\n")
543
+ info << if inventory_source
544
+ indent(2, "#{inventory_source}\n")
551
545
  else
552
- indent(2, wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist\n"))
546
+ indent(2, wrap("Tried to load inventory from #{default_inventory}, but the file does not exist\n"))
553
547
  end
554
548
  info << "\n"
549
+ end
550
+
551
+ private def format_target_summary(inventory_count, adhoc_count, target_flag, detail_flag)
552
+ info = +''
555
553
 
556
554
  # Add target count summary
557
555
  count = "#{inventory_count + adhoc_count} total, "\
@@ -560,13 +558,40 @@ module Bolt
560
558
  info << colorize(:cyan, "Target count\n")
561
559
  info << indent(2, count)
562
560
 
563
- @stream.puts info
561
+ # Add filtering information
562
+ unless target_flag && detail_flag
563
+ info << colorize(:cyan, "\n\nAdditional information\n")
564
+
565
+ unless target_flag
566
+ opt = Bolt::Util.windows? ? "'-Targets', '-Query', or '-Rerun'" : "'--targets', '--query', or '--rerun'"
567
+ info << indent(2, "Use the #{opt} option to view specific targets\n")
568
+ end
569
+
570
+ unless detail_flag
571
+ opt = Bolt::Util.windows? ? '-Detail' : '--detail'
572
+ info << indent(2, "Use the '#{opt}' option to view target configuration and data")
573
+ end
574
+ end
575
+
576
+ info
564
577
  end
565
578
 
566
- def print_groups(groups)
567
- count = "#{groups.count} group#{'s' unless groups.count == 1}"
568
- @stream.puts groups.join("\n")
569
- @stream.puts colorize(:green, count)
579
+ def print_groups(groups, inventory_source, default_inventory)
580
+ info = +''
581
+
582
+ # Add group list
583
+ info << colorize(:cyan, "Groups\n")
584
+ info << indent(2, groups.join("\n"))
585
+ info << "\n\n"
586
+
587
+ # Add inventory file source
588
+ info << format_inventory_source(inventory_source, default_inventory)
589
+
590
+ # Add group count summary
591
+ info << colorize(:cyan, "Group count\n")
592
+ info << indent(2, "#{groups.count} total")
593
+
594
+ @stream.puts info
570
595
  end
571
596
 
572
597
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
@@ -100,12 +100,12 @@ module Bolt
100
100
  moduledir: moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
- def print_targets(target_list, inventoryfile)
103
+ def print_targets(target_list, inventory_source, default_inventory, _target_flag)
104
104
  @stream.puts ::JSON.pretty_generate(
105
105
  inventory: {
106
106
  targets: target_list[:inventory].map(&:name),
107
107
  count: target_list[:inventory].count,
108
- file: inventoryfile.to_s
108
+ file: (inventory_source || default_inventory).to_s
109
109
  },
110
110
  adhoc: {
111
111
  targets: target_list[:adhoc].map(&:name),
@@ -116,7 +116,7 @@ module Bolt
116
116
  )
117
117
  end
118
118
 
119
- def print_target_info(target_list, _inventoryfile)
119
+ def print_target_info(target_list, _inventory_source, _default_inventory, _target_flag)
120
120
  targets = target_list.values.flatten
121
121
 
122
122
  @stream.puts ::JSON.pretty_generate(
@@ -125,7 +125,7 @@ module Bolt
125
125
  )
126
126
  end
127
127
 
128
- def print_groups(groups)
128
+ def print_groups(groups, _inventory_source, _default_inventory)
129
129
  count = groups.count
130
130
  @stream.puts({ groups: groups,
131
131
  count: count }.to_json)
@@ -122,9 +122,11 @@ module Bolt
122
122
  raise StepError.new("Parameters key must be a hash", body['name'], step_number)
123
123
  end
124
124
 
125
- metaparams = option_keys.map { |key| "_#{key}" }
125
+ metaparams = body['parameters'].keys
126
+ .select { |key| key.start_with?('_') }
127
+ .map { |key| key.sub(/^_/, '') }
126
128
 
127
- if (dups = body['parameters'].keys & metaparams).any?
129
+ if (dups = body.keys & metaparams).any?
128
130
  raise StepError.new(
129
131
  "Cannot specify metaparameters when using top-level keys with same name: #{dups.join(', ')}",
130
132
  body['name'],
@@ -36,8 +36,8 @@ module Bolt
36
36
  prefix, _, basename = segment_plan_name(plan_name)
37
37
 
38
38
  unless prefix == project.name
39
- message = "First segment of plan name '#{plan_name}' must match project name '#{project.name}'. "\
40
- "Did you mean '#{project.name}::#{plan_name}'?"
39
+ message = "Incomplete plan name: A plan name must be prefixed with the name of the "\
40
+ "project or module. Did you mean '#{project.name}::#{plan_name}'?"
41
41
 
42
42
  raise Bolt::ValidationError, message
43
43
  end
@@ -95,6 +95,60 @@ module Bolt
95
95
  make_query(query, path)
96
96
  end
97
97
 
98
+ # Sends a command to PuppetDB using version 1 of the commands API.
99
+ # https://puppet.com/docs/puppetdb/latest/api/command/v1/commands.html
100
+ #
101
+ # @param command [String] The command to invoke.
102
+ # @param version [Integer] The version of the command to invoke.
103
+ # @param payload [Hash] The payload to send with the command.
104
+ # @return A UUID identifying the submitted command.
105
+ #
106
+ def send_command(command, version, payload)
107
+ command = command.dup.force_encoding('utf-8')
108
+ body = JSON.generate(payload)
109
+
110
+ # PDB requires the following query parameters to the POST request.
111
+ # Error early if there's no certname, as PDB does not return a
112
+ # message indicating it's required.
113
+ unless payload['certname']
114
+ raise Bolt::Error.new(
115
+ "Payload must include 'certname', unable to invoke command.",
116
+ 'bolt/pdb-command'
117
+ )
118
+ end
119
+
120
+ url = uri.tap do |u|
121
+ u.path = 'pdb/cmd/v1'
122
+ u.query_values = { 'command' => command,
123
+ 'version' => version,
124
+ 'certname' => payload['certname'] }
125
+ end
126
+
127
+ # Send the command to PDB
128
+ begin
129
+ @logger.debug("Sending PuppetDB command '#{command}' to #{url}")
130
+ response = http_client.post(url.to_s, body: body, header: headers)
131
+ rescue StandardError => e
132
+ raise Bolt::PuppetDBFailoverError, "Failed to invoke PuppetDB command: #{e}"
133
+ end
134
+
135
+ @logger.debug("Got response code #{response.code} from PuppetDB")
136
+ if response.code != 200
137
+ raise Bolt::PuppetDBError, "Failed to invoke PuppetDB command: #{response.body}"
138
+ end
139
+
140
+ # Return the UUID string from the response body
141
+ begin
142
+ JSON.parse(response.body).fetch('uuid', nil)
143
+ rescue JSON::ParserError
144
+ raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
145
+ end
146
+ rescue Bolt::PuppetDBFailoverError => e
147
+ @logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
148
+ reject_url
149
+ send_command(command, version, payload)
150
+ end
151
+
98
152
  def http_client
99
153
  return @http if @http
100
154
  # lazy-load expensive gem code
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.6.1'
4
+ VERSION = '3.7.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-07 00:00:00.000000000 Z
11
+ date: 2021-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -414,6 +414,7 @@ files:
414
414
  - bolt-modules/boltlib/lib/puppet/functions/get_target.rb
415
415
  - bolt-modules/boltlib/lib/puppet/functions/get_targets.rb
416
416
  - bolt-modules/boltlib/lib/puppet/functions/parallelize.rb
417
+ - bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb
417
418
  - bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb
418
419
  - bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb
419
420
  - bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb
@@ -454,6 +455,7 @@ files:
454
455
  - guides/module.txt
455
456
  - guides/modulepath.txt
456
457
  - guides/project.txt
458
+ - guides/targets.txt
457
459
  - lib/bolt.rb
458
460
  - lib/bolt/analytics.rb
459
461
  - lib/bolt/applicator.rb