bolt 3.13.0 → 3.14.1

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.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  8. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  9. data/lib/bolt/analytics.rb +1 -1
  10. data/lib/bolt/bolt_option_parser.rb +4 -1
  11. data/lib/bolt/cli.rb +21 -6
  12. data/lib/bolt/config/transport/options.rb +12 -0
  13. data/lib/bolt/config/transport/ssh.rb +7 -0
  14. data/lib/bolt/executor.rb +12 -4
  15. data/lib/bolt/fiber_executor.rb +57 -12
  16. data/lib/bolt/outputter/human.rb +117 -12
  17. data/lib/bolt/outputter/json.rb +3 -5
  18. data/lib/bolt/outputter/logger.rb +1 -1
  19. data/lib/bolt/pal.rb +36 -3
  20. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  21. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  22. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  23. data/lib/bolt/plan_future.rb +21 -6
  24. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  25. data/lib/bolt/version.rb +1 -1
  26. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  27. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  28. data/lib/bolt_server/transport_app.rb +81 -50
  29. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  30. metadata +11 -14
  31. data/guides/debugging.txt +0 -28
  32. data/guides/guide.txt +0 -17
  33. data/guides/inventory.txt +0 -24
  34. data/guides/links.txt +0 -13
  35. data/guides/logging.txt +0 -18
  36. data/guides/module.txt +0 -19
  37. data/guides/modulepath.txt +0 -25
  38. data/guides/project.txt +0 -22
  39. data/guides/targets.txt +0 -29
  40. data/guides/transports.txt +0 -23
@@ -341,7 +341,7 @@ module Bolt
341
341
  info << if task.description
342
342
  indent(2, task.description.chomp)
343
343
  else
344
- indent(2, 'No description')
344
+ indent(2, 'No description available.')
345
345
  end
346
346
  info << "\n\n"
347
347
 
@@ -400,7 +400,7 @@ module Bolt
400
400
  info << if plan['description']
401
401
  indent(2, plan['description'].chomp)
402
402
  else
403
- indent(2, 'No description')
403
+ indent(2, 'No description available.')
404
404
  end
405
405
  info << "\n\n"
406
406
 
@@ -471,8 +471,16 @@ module Bolt
471
471
  @stream.puts info
472
472
  end
473
473
 
474
- def print_guide(guide, _topic)
475
- @stream.puts(guide)
474
+ def print_guide(topic:, guide:, documentation: nil)
475
+ info = +"#{colorize(:cyan, topic)}\n"
476
+ info << indent(2, guide)
477
+
478
+ if documentation
479
+ info << "\n#{colorize(:cyan, 'Documentation')}\n"
480
+ info << indent(2, documentation.join("\n"))
481
+ end
482
+
483
+ @stream.puts info
476
484
  end
477
485
 
478
486
  def print_plan_lookup(value)
@@ -480,15 +488,19 @@ module Bolt
480
488
  end
481
489
 
482
490
  def print_module_list(module_list)
491
+ info = +''
492
+
483
493
  module_list.each do |path, modules|
484
- if (mod = modules.find { |m| m[:internal_module_group] })
485
- @stream.puts(colorize(:cyan, mod[:internal_module_group]))
486
- else
487
- @stream.puts(colorize(:cyan, path))
488
- end
494
+ info << if (mod = modules.find { |m| m[:internal_module_group] })
495
+ colorize(:cyan, mod[:internal_module_group])
496
+ else
497
+ colorize(:cyan, path)
498
+ end
499
+
500
+ info << "\n"
489
501
 
490
502
  if modules.empty?
491
- @stream.puts('(no modules installed)')
503
+ info << '(no modules installed)'
492
504
  else
493
505
  module_info = modules.map do |m|
494
506
  version = if m[:version].nil?
@@ -500,11 +512,87 @@ module Bolt
500
512
  [m[:name], version]
501
513
  end
502
514
 
503
- @stream.puts format_table(module_info, 2, 1)
515
+ info << format_table(module_info, 2, 1).to_s
516
+ end
517
+
518
+ info << "\n\n"
519
+ end
520
+
521
+ command = Bolt::Util.powershell? ? 'Get-BoltModule -Name <MODULE>' : 'bolt module show <MODULE>'
522
+ info << colorize(:cyan, "Additional information\n")
523
+ info << indent(2, "Use '#{command}' to view details for a specific module.")
524
+
525
+ @stream.puts info
526
+ end
527
+
528
+ # Prints detailed module information.
529
+ #
530
+ # @param name [String] The module's short name.
531
+ # @param metadata [Hash] The module's metadata.
532
+ # @param path [String] The path to the module.
533
+ # @param plans [Array] The module's plans.
534
+ # @param tasks [Array] The module's tasks.
535
+ #
536
+ def print_module_info(name:, metadata:, path:, plans:, tasks:, **_kwargs)
537
+ info = +''
538
+
539
+ info << colorize(:cyan, name)
540
+
541
+ info << colorize(:dim, " [#{metadata['version']}]") if metadata['version']
542
+ info << "\n"
543
+
544
+ info << if metadata['summary']
545
+ indent(2, wrap(metadata['summary'].strip, 76))
546
+ else
547
+ indent(2, "No description available.\n")
548
+ end
549
+ info << "\n"
550
+
551
+ if tasks.any?
552
+ length = tasks.map(&:first).map(&:length).max
553
+ data = tasks.map { |task, desc| [task, truncate(desc, 76 - length)] }
554
+ info << colorize(:cyan, "Tasks\n")
555
+ info << format_table(data, 2).to_s
556
+ info << "\n\n"
557
+ end
558
+
559
+ if plans.any?
560
+ length = plans.map(&:first).map(&:length).max
561
+ data = plans.map { |plan, desc| [plan, truncate(desc, 76 - length)] }
562
+ info << colorize(:cyan, "Plans\n")
563
+ info << format_table(data, 2).to_s
564
+ info << "\n\n"
565
+ end
566
+
567
+ if metadata['operatingsystem_support']&.any?
568
+ supported = metadata['operatingsystem_support'].map do |os|
569
+ [os['operatingsystem'], os['operatingsystemrelease']&.join(', ')]
570
+ end
571
+
572
+ info << colorize(:cyan, "Operating system support\n")
573
+ info << format_table(supported, 2).to_s
574
+ info << "\n\n"
575
+ end
576
+
577
+ if metadata['dependencies']&.any?
578
+ dependencies = metadata['dependencies'].map do |dep|
579
+ [dep['name'], dep['version_requirement']]
504
580
  end
505
581
 
506
- @stream.write("\n")
582
+ info << colorize(:cyan, "Dependencies\n")
583
+ info << format_table(dependencies, 2).to_s
584
+ info << "\n\n"
507
585
  end
586
+
587
+ info << colorize(:cyan, "Path\n")
588
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH) ||
589
+ path.start_with?(Bolt::Config::Modulepath::BOLTLIB_PATH)
590
+ indent(2, 'built-in module')
591
+ else
592
+ indent(2, path)
593
+ end
594
+
595
+ @stream.puts info
508
596
  end
509
597
 
510
598
  def print_plugin_list(plugin_list, modulepath)
@@ -665,6 +753,12 @@ module Bolt
665
753
  print_container_result(value.result)
666
754
  when Bolt::ResultSet
667
755
  print_result_set(value)
756
+ when Bolt::Result
757
+ print_result(value)
758
+ when Bolt::ApplyResult
759
+ print_apply_result(value)
760
+ when Bolt::Error
761
+ print_bolt_error(**value.to_h.transform_keys(&:to_sym))
668
762
  else
669
763
  @stream.puts(::JSON.pretty_generate(plan_result, quirks_mode: true))
670
764
  end
@@ -704,6 +798,17 @@ module Bolt
704
798
  @stream.puts(colorize(:red, message))
705
799
  end
706
800
 
801
+ def print_bolt_error(msg:, details:, **_kwargs)
802
+ err = msg
803
+ if (f = details[:file])
804
+ err += "\n (file: #{f}"
805
+ err += ", line: #{details[:line]}" if details[:line]
806
+ err += ", column: #{details[:column]}" if details[:column]
807
+ err += ")"
808
+ end
809
+ @stream.puts(colorize(:red, err))
810
+ end
811
+
707
812
  def print_prompt(prompt)
708
813
  @stream.print(colorize(:cyan, indent(4, prompt)))
709
814
  end
@@ -47,6 +47,7 @@ module Bolt
47
47
  @stream.puts results.to_json
48
48
  end
49
49
  alias print_module_list print_table
50
+ alias print_module_info print_table
50
51
 
51
52
  def print_task_info(task)
52
53
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
@@ -98,11 +99,8 @@ module Bolt
98
99
  print_table('topics' => topics)
99
100
  end
100
101
 
101
- def print_guide(guide, topic)
102
- @stream.puts({
103
- 'topic' => topic,
104
- 'guide' => guide
105
- }.to_json)
102
+ def print_guide(**kwargs)
103
+ @stream.puts(kwargs.to_json)
106
104
  end
107
105
 
108
106
  def print_plan_lookup(value)
@@ -24,7 +24,7 @@ module Bolt
24
24
  log_container_start(event)
25
25
  when :container_finish
26
26
  log_container_finish(event)
27
- when :log
27
+ when :log, :message, :verbose
28
28
  log_message(**event)
29
29
  end
30
30
  end
data/lib/bolt/pal.rb CHANGED
@@ -339,7 +339,7 @@ module Bolt
339
339
  end
340
340
 
341
341
  # Write the cache if any entries were updated
342
- File.write(@project.task_cache_file, task_cache.to_json) if updated
342
+ File.write(@project.task_cache_file, task_cache.to_json) if updated && @project
343
343
  filter_content ? filter_content(task_list, @project&.tasks) : task_list
344
344
  end
345
345
 
@@ -435,7 +435,7 @@ module Bolt
435
435
  list << [plan_name, data['description']] unless data['private']
436
436
  end
437
437
 
438
- File.write(@project.plan_cache_file, plan_cache.to_json) if updated
438
+ File.write(@project.plan_cache_file, plan_cache.to_json) if updated && @project
439
439
 
440
440
  filter_content ? filter_content(plan_list, @project&.plans) : plan_list
441
441
  end
@@ -635,6 +635,36 @@ module Bolt
635
635
  end
636
636
  end
637
637
 
638
+ # Return information about a module.
639
+ #
640
+ # @param name [String] The name of the module.
641
+ # @return [Hash]
642
+ #
643
+ def show_module(name)
644
+ name = name.tr('-', '/')
645
+
646
+ data = in_bolt_compiler do |_compiler|
647
+ mod = Puppet.lookup(:current_environment).module(name.split(%r{[/-]}, 2).last)
648
+
649
+ unless mod && (mod.forge_name == name || mod.name == name)
650
+ raise Bolt::Error.new("Could not find module '#{name}' on the modulepath.", 'bolt/unknown-module')
651
+ end
652
+
653
+ {
654
+ name: mod.forge_name || mod.name,
655
+ metadata: mod.metadata,
656
+ path: mod.path,
657
+ plans: mod.plans.map(&:name).sort,
658
+ tasks: mod.tasks.map(&:name).sort
659
+ }
660
+ end
661
+
662
+ data[:plans] = list_plans_with_cache.to_h.slice(*data[:plans]).to_a
663
+ data[:tasks] = list_tasks_with_cache.to_h.slice(*data[:tasks]).to_a
664
+
665
+ data
666
+ end
667
+
638
668
  def generate_types(cache: false)
639
669
  require 'puppet/face/generate'
640
670
  in_bolt_compiler do
@@ -683,7 +713,10 @@ module Bolt
683
713
  # Create a Fiber for the main plan. This will be run along with any
684
714
  # other Fibers created during the plan run in the round_robin, with the
685
715
  # main plan always taking precedence in being resumed.
686
- future = executor.create_future(name: plan_name) do |_scope|
716
+ #
717
+ # Every future except for the main plan needs to have a plan id in
718
+ # order to be tracked for the `wait()` function with no arguments.
719
+ future = executor.create_future(name: plan_name, plan_id: 1) do |_scope|
687
720
  r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
688
721
  Bolt::PlanResult.from_pcore(r, 'success')
689
722
  rescue Bolt::Error => e
@@ -13,6 +13,7 @@ module Bolt
13
13
  download
14
14
  eval
15
15
  message
16
+ verbose
16
17
  plan
17
18
  resources
18
19
  script
@@ -219,3 +220,4 @@ require 'bolt/pal/yaml_plan/step/task'
219
220
  require 'bolt/pal/yaml_plan/step/upload'
220
221
  require 'bolt/pal/yaml_plan/step/download'
221
222
  require 'bolt/pal/yaml_plan/step/message'
223
+ require 'bolt/pal/yaml_plan/step/verbose'
@@ -24,14 +24,6 @@ module Bolt
24
24
  private def function
25
25
  'out::message'
26
26
  end
27
-
28
- # Transpiles the step into the plan language
29
- #
30
- def transpile
31
- code = String.new(" ")
32
- code << function_call(function, format_args(body))
33
- code << "\n"
34
- end
35
27
  end
36
28
  end
37
29
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Verbose < Step
8
+ def self.allowed_keys
9
+ super + Set['verbose']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['verbose']
14
+ end
15
+
16
+ # Returns an array of arguments to pass to the step's function call
17
+ #
18
+ private def format_args(body)
19
+ [body['verbose']]
20
+ end
21
+
22
+ # Returns the function corresponding to the step
23
+ #
24
+ private def function
25
+ 'out::verbose'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,13 +5,28 @@ require 'fiber'
5
5
  module Bolt
6
6
  class PlanFuture
7
7
  attr_reader :fiber, :id
8
- attr_accessor :value
8
+ attr_accessor :value, :plan_stack
9
9
 
10
- def initialize(fiber, id, name = nil)
11
- @fiber = fiber
12
- @id = id
13
- @name = name
14
- @value = nil
10
+ def initialize(fiber, id, plan_id:, name: nil)
11
+ @fiber = fiber
12
+ @id = id
13
+ @name = name
14
+ @value = nil
15
+ # The plan invocation ID when the Future is created may be
16
+ # different from the plan ID of the Future when we switch to it if a new
17
+ # plan was run inside the Future, so keep track of the plans that a
18
+ # Future is executing in as a stack. When one plan finishes, pop it off
19
+ # since now we're in the calling plan. These IDs are unique to each plan
20
+ # invocation, not just plan names.
21
+ @plan_stack = [plan_id]
22
+ end
23
+
24
+ def original_plan
25
+ @plan_stack.last
26
+ end
27
+
28
+ def current_plan
29
+ @plan_stack.first
15
30
  end
16
31
 
17
32
  def name
@@ -47,7 +47,9 @@ module Bolt
47
47
  cmd = []
48
48
  # BatchMode is SSH's noninteractive option: if key authentication
49
49
  # fails it will error out instead of falling back to password prompt
50
- cmd += %w[-o BatchMode=yes]
50
+ batch_mode = @target.transport_config['batch-mode'] ? 'yes' : 'no'
51
+ cmd += %W[-o BatchMode=#{batch_mode}]
52
+
51
53
  cmd += %W[-o Port=#{@target.port}] if @target.port
52
54
 
53
55
  if @target.transport_config.key?('host-key-check')
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.13.0'
4
+ VERSION = '3.14.1'
5
5
  end
@@ -62,6 +62,10 @@
62
62
  "interpreters": {
63
63
  "type": "object",
64
64
  "description": "Map of file extensions to remote executable"
65
+ },
66
+ "plugin_hooks": {
67
+ "type": "object",
68
+ "description": "Configuration for plugins to use"
65
69
  }
66
70
  },
67
71
  "oneOf": [
@@ -56,6 +56,10 @@
56
56
  "smb-port": {
57
57
  "type": "integer",
58
58
  "description": "Port for SMB protocol"
59
+ },
60
+ "plugin_hooks": {
61
+ "type": "object",
62
+ "description": "Configuration for plugins to use"
59
63
  }
60
64
  },
61
65
  "required": ["hostname", "user", "password"],
@@ -389,6 +389,63 @@ module BoltServer
389
389
  end
390
390
  end
391
391
 
392
+ # The provided block takes a module object and returns the list
393
+ # of directories to search through. This is similar to
394
+ # Bolt::Applicator.build_plugin_tarball.
395
+ def build_project_plugins_tarball(versioned_project, &block)
396
+ start_time = Time.now
397
+
398
+ # Fetch the plugin files
399
+ plugin_files = in_bolt_project(versioned_project) do |context|
400
+ files = {}
401
+
402
+ # Bolt also sets plugin_modulepath to user modulepath so do it here too for
403
+ # consistency
404
+ plugin_modulepath = context[:pal].user_modulepath
405
+ Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
406
+ search_dirs = block.call(mod)
407
+
408
+ files[mod] ||= []
409
+ Find.find(*search_dirs).each do |file|
410
+ files[mod] << file if File.file?(file)
411
+ end
412
+ end
413
+
414
+ files
415
+ end
416
+
417
+ # Pack the plugin files
418
+ sio = StringIO.new
419
+ begin
420
+ output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
421
+
422
+ plugin_files.each do |mod, files|
423
+ tar_dir = Pathname.new(mod.name)
424
+ mod_dir = Pathname.new(mod.path)
425
+
426
+ files.each do |file|
427
+ tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
428
+ stat = File.stat(file)
429
+ content = File.binread(file)
430
+ output.tar.add_file_simple(
431
+ tar_path.to_s,
432
+ data: content,
433
+ size: content.size,
434
+ mode: stat.mode & 0o777,
435
+ mtime: stat.mtime
436
+ )
437
+ end
438
+ end
439
+
440
+ duration = Time.now - start_time
441
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
442
+ ensure
443
+ output.close
444
+ end
445
+
446
+ Base64.encode64(sio.string)
447
+ end
448
+
392
449
  get '/' do
393
450
  200
394
451
  end
@@ -441,7 +498,7 @@ module BoltServer
441
498
  'uri' => target_hash['hostname'],
442
499
  'config' => {
443
500
  'transport' => 'ssh',
444
- 'ssh' => opts
501
+ 'ssh' => opts.slice(*Bolt::Config::Transport::SSH.options)
445
502
  }
446
503
  }
447
504
 
@@ -479,7 +536,7 @@ module BoltServer
479
536
  'uri' => target_hash['hostname'],
480
537
  'config' => {
481
538
  'transport' => 'winrm',
482
- 'winrm' => opts
539
+ 'winrm' => opts.slice(*Bolt::Config::Transport::WinRM.options)
483
540
  }
484
541
  }
485
542
 
@@ -699,60 +756,34 @@ module BoltServer
699
756
  raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
700
757
  content_type :json
701
758
 
702
- # Inspired by Bolt::Applicator.build_plugin_tarball
703
- start_time = Time.now
704
-
705
- # Fetch the plugin files
706
- plugin_files = in_bolt_project(params['versioned_project']) do |context|
707
- files = {}
708
-
709
- # Bolt also sets plugin_modulepath to user modulepath so do it here too for
710
- # consistency
711
- plugin_modulepath = context[:pal].user_modulepath
712
- Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
713
- search_dirs = []
714
- search_dirs << mod.plugins if mod.plugins?
715
- search_dirs << mod.pluginfacts if mod.pluginfacts?
716
-
717
- files[mod] ||= []
718
- Find.find(*search_dirs).each do |file|
719
- files[mod] << file if File.file?(file)
720
- end
721
- end
722
-
723
- files
759
+ plugins_tarball = build_project_plugins_tarball(params['versioned_project']) do |mod|
760
+ search_dirs = []
761
+ search_dirs << mod.plugins if mod.plugins?
762
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
763
+ search_dirs
724
764
  end
725
765
 
726
- # Pack the plugin files
727
- sio = StringIO.new
728
- begin
729
- output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
730
-
731
- plugin_files.each do |mod, files|
732
- tar_dir = Pathname.new(mod.name)
733
- mod_dir = Pathname.new(mod.path)
766
+ [200, plugins_tarball.to_json]
767
+ end
734
768
 
735
- files.each do |file|
736
- tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
737
- stat = File.stat(file)
738
- content = File.binread(file)
739
- output.tar.add_file_simple(
740
- tar_path.to_s,
741
- data: content,
742
- size: content.size,
743
- mode: stat.mode & 0o777,
744
- mtime: stat.mtime
745
- )
746
- end
747
- end
769
+ # Returns the base64 encoded tar archive of _all_ plugin code for a project
770
+ #
771
+ # @param versioned_project [String] the versioned_project to build the plugin tarball from
772
+ get '/project_plugin_tarball' do
773
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
774
+ content_type :json
748
775
 
749
- duration = Time.now - start_time
750
- @logger.trace("Packed plugins in #{duration * 1000} ms")
751
- ensure
752
- output.close
776
+ plugins_tarball = build_project_plugins_tarball(params['versioned_project']) do |mod|
777
+ search_dirs = []
778
+ search_dirs << mod.plugins if mod.plugins?
779
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
780
+ search_dirs << mod.files if mod.files?
781
+ type_files = "#{mod.path}/types"
782
+ search_dirs << type_files if File.exist?(type_files)
783
+ search_dirs
753
784
  end
754
785
 
755
- [200, Base64.encode64(sio.string).to_json]
786
+ [200, plugins_tarball.to_json]
756
787
  end
757
788
 
758
789
  error 404 do