openbolt 5.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/Puppetfile +52 -0
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +60 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +71 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +55 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +65 -0
  9. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +93 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +33 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +38 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +208 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +62 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +57 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +130 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +31 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +52 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +87 -0
  19. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +34 -0
  20. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +35 -0
  21. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +74 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +97 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +47 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +52 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +40 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +42 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +106 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
  30. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +291 -0
  31. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +145 -0
  32. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +164 -0
  33. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +211 -0
  34. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +48 -0
  35. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +43 -0
  36. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +145 -0
  37. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +38 -0
  38. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +101 -0
  39. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +29 -0
  40. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +131 -0
  41. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +59 -0
  42. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +39 -0
  43. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +50 -0
  44. data/bolt-modules/boltlib/types/planresult.pp +18 -0
  45. data/bolt-modules/boltlib/types/targetspec.pp +7 -0
  46. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +42 -0
  47. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +20 -0
  48. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  49. data/bolt-modules/file/lib/puppet/functions/file/delete.rb +21 -0
  50. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +28 -0
  51. data/bolt-modules/file/lib/puppet/functions/file/join.rb +20 -0
  52. data/bolt-modules/file/lib/puppet/functions/file/read.rb +33 -0
  53. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +28 -0
  54. data/bolt-modules/file/lib/puppet/functions/file/write.rb +24 -0
  55. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
  56. data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
  57. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
  58. data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
  59. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
  60. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
  61. data/bolt-modules/out/lib/puppet/functions/out/message.rb +36 -0
  62. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
  63. data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
  64. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +65 -0
  65. data/bolt-modules/system/lib/puppet/functions/system/env.rb +20 -0
  66. data/exe/bolt +17 -0
  67. data/guides/debugging.yaml +27 -0
  68. data/guides/inventory.yaml +23 -0
  69. data/guides/links.yaml +12 -0
  70. data/guides/logging.yaml +17 -0
  71. data/guides/module.yaml +18 -0
  72. data/guides/modulepath.yaml +24 -0
  73. data/guides/project.yaml +21 -0
  74. data/guides/targets.yaml +28 -0
  75. data/guides/transports.yaml +22 -0
  76. data/lib/bolt/analytics.rb +233 -0
  77. data/lib/bolt/application.rb +806 -0
  78. data/lib/bolt/applicator.rb +368 -0
  79. data/lib/bolt/apply_inventory.rb +93 -0
  80. data/lib/bolt/apply_result.rb +154 -0
  81. data/lib/bolt/apply_target.rb +90 -0
  82. data/lib/bolt/bolt_option_parser.rb +1226 -0
  83. data/lib/bolt/catalog/logging.rb +15 -0
  84. data/lib/bolt/catalog.rb +144 -0
  85. data/lib/bolt/cli.rb +949 -0
  86. data/lib/bolt/config/modulepath.rb +30 -0
  87. data/lib/bolt/config/options.rb +673 -0
  88. data/lib/bolt/config/transport/base.rb +133 -0
  89. data/lib/bolt/config/transport/docker.rb +34 -0
  90. data/lib/bolt/config/transport/jail.rb +33 -0
  91. data/lib/bolt/config/transport/local.rb +39 -0
  92. data/lib/bolt/config/transport/lxd.rb +34 -0
  93. data/lib/bolt/config/transport/options.rb +431 -0
  94. data/lib/bolt/config/transport/orch.rb +41 -0
  95. data/lib/bolt/config/transport/podman.rb +33 -0
  96. data/lib/bolt/config/transport/remote.rb +24 -0
  97. data/lib/bolt/config/transport/ssh.rb +138 -0
  98. data/lib/bolt/config/transport/winrm.rb +63 -0
  99. data/lib/bolt/config.rb +515 -0
  100. data/lib/bolt/container_result.rb +105 -0
  101. data/lib/bolt/error.rb +194 -0
  102. data/lib/bolt/executor.rb +539 -0
  103. data/lib/bolt/fiber_executor.rb +190 -0
  104. data/lib/bolt/inventory/group.rb +446 -0
  105. data/lib/bolt/inventory/inventory.rb +391 -0
  106. data/lib/bolt/inventory/options.rb +139 -0
  107. data/lib/bolt/inventory/target.rb +293 -0
  108. data/lib/bolt/inventory.rb +120 -0
  109. data/lib/bolt/logger.rb +252 -0
  110. data/lib/bolt/module.rb +54 -0
  111. data/lib/bolt/module_installer/installer.rb +44 -0
  112. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  113. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  114. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  115. data/lib/bolt/module_installer/puppetfile.rb +131 -0
  116. data/lib/bolt/module_installer/resolver.rb +129 -0
  117. data/lib/bolt/module_installer/specs/forge_spec.rb +91 -0
  118. data/lib/bolt/module_installer/specs/git_spec.rb +150 -0
  119. data/lib/bolt/module_installer/specs/id/base.rb +116 -0
  120. data/lib/bolt/module_installer/specs/id/gitclone.rb +120 -0
  121. data/lib/bolt/module_installer/specs/id/github.rb +90 -0
  122. data/lib/bolt/module_installer/specs/id/gitlab.rb +92 -0
  123. data/lib/bolt/module_installer/specs.rb +95 -0
  124. data/lib/bolt/module_installer.rb +208 -0
  125. data/lib/bolt/node/errors.rb +55 -0
  126. data/lib/bolt/node/output.rb +29 -0
  127. data/lib/bolt/outputter/human.rb +958 -0
  128. data/lib/bolt/outputter/json.rb +205 -0
  129. data/lib/bolt/outputter/logger.rb +76 -0
  130. data/lib/bolt/outputter/rainbow.rb +118 -0
  131. data/lib/bolt/outputter.rb +57 -0
  132. data/lib/bolt/pal/issues.rb +19 -0
  133. data/lib/bolt/pal/logging.rb +17 -0
  134. data/lib/bolt/pal/yaml_plan/evaluator.rb +83 -0
  135. data/lib/bolt/pal/yaml_plan/loader.rb +94 -0
  136. data/lib/bolt/pal/yaml_plan/parameter.rb +63 -0
  137. data/lib/bolt/pal/yaml_plan/step/command.rb +45 -0
  138. data/lib/bolt/pal/yaml_plan/step/download.rb +37 -0
  139. data/lib/bolt/pal/yaml_plan/step/eval.rb +42 -0
  140. data/lib/bolt/pal/yaml_plan/step/message.rb +31 -0
  141. data/lib/bolt/pal/yaml_plan/step/plan.rb +42 -0
  142. data/lib/bolt/pal/yaml_plan/step/resources.rb +170 -0
  143. data/lib/bolt/pal/yaml_plan/step/script.rb +62 -0
  144. data/lib/bolt/pal/yaml_plan/step/task.rb +42 -0
  145. data/lib/bolt/pal/yaml_plan/step/upload.rb +37 -0
  146. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  147. data/lib/bolt/pal/yaml_plan/step.rb +223 -0
  148. data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
  149. data/lib/bolt/pal/yaml_plan.rb +172 -0
  150. data/lib/bolt/pal.rb +847 -0
  151. data/lib/bolt/plan_creator.rb +219 -0
  152. data/lib/bolt/plan_future.rb +86 -0
  153. data/lib/bolt/plan_result.rb +44 -0
  154. data/lib/bolt/plugin/cache.rb +76 -0
  155. data/lib/bolt/plugin/env_var.rb +54 -0
  156. data/lib/bolt/plugin/module.rb +276 -0
  157. data/lib/bolt/plugin/prompt.rb +36 -0
  158. data/lib/bolt/plugin/puppet_connect_data.rb +84 -0
  159. data/lib/bolt/plugin/puppetdb.rb +124 -0
  160. data/lib/bolt/plugin/task.rb +72 -0
  161. data/lib/bolt/plugin.rb +380 -0
  162. data/lib/bolt/project.rb +219 -0
  163. data/lib/bolt/project_manager/config_migrator.rb +113 -0
  164. data/lib/bolt/project_manager/inventory_migrator.rb +67 -0
  165. data/lib/bolt/project_manager/migrator.rb +39 -0
  166. data/lib/bolt/project_manager/module_migrator.rb +203 -0
  167. data/lib/bolt/project_manager.rb +221 -0
  168. data/lib/bolt/puppetdb/client.rb +153 -0
  169. data/lib/bolt/puppetdb/config.rb +176 -0
  170. data/lib/bolt/puppetdb/instance.rb +146 -0
  171. data/lib/bolt/puppetdb.rb +15 -0
  172. data/lib/bolt/r10k_log_proxy.rb +30 -0
  173. data/lib/bolt/rerun.rb +55 -0
  174. data/lib/bolt/resource_instance.rb +133 -0
  175. data/lib/bolt/result.rb +247 -0
  176. data/lib/bolt/result_set.rb +128 -0
  177. data/lib/bolt/shell/bash/tmpdir.rb +62 -0
  178. data/lib/bolt/shell/bash.rb +516 -0
  179. data/lib/bolt/shell/powershell/snippets.rb +181 -0
  180. data/lib/bolt/shell/powershell.rb +365 -0
  181. data/lib/bolt/shell.rb +105 -0
  182. data/lib/bolt/target.rb +174 -0
  183. data/lib/bolt/task/puppet_server.rb +27 -0
  184. data/lib/bolt/task/run.rb +55 -0
  185. data/lib/bolt/task.rb +163 -0
  186. data/lib/bolt/transport/base.rb +252 -0
  187. data/lib/bolt/transport/docker/connection.rb +150 -0
  188. data/lib/bolt/transport/docker.rb +23 -0
  189. data/lib/bolt/transport/jail/connection.rb +81 -0
  190. data/lib/bolt/transport/jail.rb +21 -0
  191. data/lib/bolt/transport/local/connection.rb +106 -0
  192. data/lib/bolt/transport/local.rb +20 -0
  193. data/lib/bolt/transport/lxd/connection.rb +115 -0
  194. data/lib/bolt/transport/lxd.rb +26 -0
  195. data/lib/bolt/transport/orch/connection.rb +111 -0
  196. data/lib/bolt/transport/orch.rb +271 -0
  197. data/lib/bolt/transport/podman/connection.rb +102 -0
  198. data/lib/bolt/transport/podman.rb +19 -0
  199. data/lib/bolt/transport/remote.rb +41 -0
  200. data/lib/bolt/transport/simple.rb +54 -0
  201. data/lib/bolt/transport/ssh/connection.rb +321 -0
  202. data/lib/bolt/transport/ssh/exec_connection.rb +140 -0
  203. data/lib/bolt/transport/ssh.rb +48 -0
  204. data/lib/bolt/transport/winrm/connection.rb +378 -0
  205. data/lib/bolt/transport/winrm.rb +33 -0
  206. data/lib/bolt/util/format.rb +68 -0
  207. data/lib/bolt/util/puppet_log_level.rb +21 -0
  208. data/lib/bolt/util.rb +465 -0
  209. data/lib/bolt/validator.rb +227 -0
  210. data/lib/bolt/version.rb +5 -0
  211. data/lib/bolt.rb +8 -0
  212. data/lib/bolt_server/acl.rb +39 -0
  213. data/lib/bolt_server/base_config.rb +112 -0
  214. data/lib/bolt_server/config.rb +64 -0
  215. data/lib/bolt_server/file_cache.rb +200 -0
  216. data/lib/bolt_server/request_error.rb +11 -0
  217. data/lib/bolt_server/schemas/action-check_node_connections.json +14 -0
  218. data/lib/bolt_server/schemas/action-run_command.json +12 -0
  219. data/lib/bolt_server/schemas/action-run_script.json +47 -0
  220. data/lib/bolt_server/schemas/action-run_task.json +20 -0
  221. data/lib/bolt_server/schemas/action-upload_file.json +47 -0
  222. data/lib/bolt_server/schemas/partials/target-any.json +10 -0
  223. data/lib/bolt_server/schemas/partials/target-ssh.json +88 -0
  224. data/lib/bolt_server/schemas/partials/target-winrm.json +67 -0
  225. data/lib/bolt_server/schemas/partials/task.json +94 -0
  226. data/lib/bolt_server/schemas/transport-ssh.json +25 -0
  227. data/lib/bolt_server/schemas/transport-winrm.json +19 -0
  228. data/lib/bolt_server/transport_app.rb +554 -0
  229. data/lib/bolt_spec/bolt_context.rb +226 -0
  230. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +51 -0
  231. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  232. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  233. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +59 -0
  234. data/lib/bolt_spec/plans/action_stubs/task_stub.rb +57 -0
  235. data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +65 -0
  236. data/lib/bolt_spec/plans/action_stubs.rb +196 -0
  237. data/lib/bolt_spec/plans/mock_executor.rb +361 -0
  238. data/lib/bolt_spec/plans/publish_stub.rb +49 -0
  239. data/lib/bolt_spec/plans.rb +190 -0
  240. data/lib/bolt_spec/run.rb +246 -0
  241. data/lib/logging_extensions/logging.rb +13 -0
  242. data/libexec/apply_catalog.rb +130 -0
  243. data/libexec/bolt_catalog +68 -0
  244. data/libexec/custom_facts.rb +63 -0
  245. data/libexec/query_resources.rb +75 -0
  246. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +21 -0
  247. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +22 -0
  248. data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
  249. data/modules/aggregate/plans/count.pp +56 -0
  250. data/modules/aggregate/plans/targets.pp +56 -0
  251. data/modules/canary/lib/puppet/functions/canary/merge.rb +13 -0
  252. data/modules/canary/lib/puppet/functions/canary/random_split.rb +22 -0
  253. data/modules/canary/lib/puppet/functions/canary/skip.rb +25 -0
  254. data/modules/canary/plans/init.pp +100 -0
  255. data/modules/puppet_connect/plans/test_input_data.pp +94 -0
  256. data/modules/puppetdb_fact/plans/init.pp +20 -0
  257. data/resources/bolt_bash_completion.sh +214 -0
  258. metadata +735 -0
data/lib/bolt/cli.rb ADDED
@@ -0,0 +1,949 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Avoid requiring the CLI from other files. It has side-effects - such as loading r10k -
4
+ # that are undesirable when using Bolt as a library.
5
+
6
+ require 'uri'
7
+ require 'benchmark'
8
+ require 'json'
9
+ require 'io/console'
10
+ require 'logging'
11
+ require 'optparse'
12
+ require_relative '../bolt/analytics'
13
+ require_relative '../bolt/application'
14
+ require_relative '../bolt/bolt_option_parser'
15
+ require_relative '../bolt/config'
16
+ require_relative '../bolt/error'
17
+ require_relative '../bolt/executor'
18
+ require_relative '../bolt/inventory'
19
+ require_relative '../bolt/logger'
20
+ require_relative '../bolt/module_installer'
21
+ require_relative '../bolt/outputter'
22
+ require_relative '../bolt/pal'
23
+ require_relative '../bolt/plugin'
24
+ require_relative '../bolt/project_manager'
25
+ require_relative '../bolt/puppetdb'
26
+ require_relative '../bolt/rerun'
27
+ require_relative '../bolt/target'
28
+ require_relative '../bolt/version'
29
+
30
+ module Bolt
31
+ class CLIExit < StandardError; end
32
+
33
+ class CLI
34
+ attr_reader :outputter, :rerun
35
+
36
+ COMMANDS = {
37
+ 'apply' => %w[],
38
+ 'command' => %w[run],
39
+ 'file' => %w[download upload],
40
+ 'group' => %w[show],
41
+ 'guide' => %w[],
42
+ 'inventory' => %w[show],
43
+ 'lookup' => %w[],
44
+ 'module' => %w[add generate-types install show],
45
+ 'plan' => %w[show run convert new],
46
+ 'plugin' => %w[show],
47
+ 'policy' => %w[apply new show],
48
+ 'project' => %w[init migrate],
49
+ 'script' => %w[run],
50
+ 'secret' => %w[encrypt decrypt createkeys],
51
+ 'task' => %w[show run]
52
+ }.freeze
53
+
54
+ TARGETING_OPTIONS = %i[query rerun targets].freeze
55
+
56
+ SUCCESS = 0
57
+ FAILURE = 1
58
+
59
+ def initialize(argv)
60
+ Bolt::Logger.initialize_logging
61
+ @logger = Bolt::Logger.logger(self)
62
+ @argv = argv
63
+ end
64
+
65
+ # TODO: Move this to the parser.
66
+ #
67
+ # Query whether the help text needs to be displayed.
68
+ #
69
+ # @param remaining [Array] Remaining arguments after parsing the command.
70
+ # @param options [Hash] The CLI options.
71
+ #
72
+ private def help?(options, remaining)
73
+ # Set the subcommand
74
+ options[:subcommand] = remaining.shift
75
+
76
+ if options[:subcommand] == 'help'
77
+ options[:help] = true
78
+ options[:subcommand] = remaining.shift
79
+ end
80
+
81
+ # This section handles parsing non-flag options which are
82
+ # subcommand specific rather then part of the config
83
+ actions = COMMANDS[options[:subcommand]]
84
+ if actions && !actions.empty?
85
+ options[:action] = remaining.shift
86
+ end
87
+
88
+ options[:help]
89
+ end
90
+
91
+ # TODO: Move most of this to the parser.
92
+ #
93
+ # Parse the command and validate options. All errors that are raised here
94
+ # are not handled by the outputter, as it relies on config being loaded.
95
+ #
96
+ def parse
97
+ with_error_handling do
98
+ options = {}
99
+ parser = BoltOptionParser.new(options)
100
+
101
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
102
+ remaining = parser.permute(@argv) unless @argv.empty?
103
+
104
+ if @argv.empty? || help?(options, remaining)
105
+ # If the subcommand is not enabled, display the default
106
+ # help text
107
+ options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
108
+
109
+ if Bolt::Util.first_run?
110
+ FileUtils.touch(Bolt::Util.first_runs_free)
111
+
112
+ if options[:subcommand].nil? && $stdout.isatty
113
+ welcome_message
114
+ raise Bolt::CLIExit
115
+ end
116
+ end
117
+
118
+ # Update the parser for the subcommand (or lack thereof)
119
+ parser.update
120
+ puts parser.help
121
+ raise Bolt::CLIExit
122
+ end
123
+
124
+ if options[:version]
125
+ puts Bolt::VERSION
126
+ raise Bolt::CLIExit
127
+ end
128
+
129
+ options[:object] = remaining.shift
130
+
131
+ # Handle reading a command from a file
132
+ if options[:subcommand] == 'command' && options[:object]
133
+ options[:object] = Bolt::Util.get_arg_input(options[:object])
134
+ end
135
+
136
+ # Only parse params for task or plan
137
+ if %w[task plan].include?(options[:subcommand])
138
+ params, remaining = remaining.partition { |s| s =~ /.+=/ }
139
+ if options[:params]
140
+ unless params.empty?
141
+ raise Bolt::CLIError,
142
+ "Parameters must be specified through either the --params " \
143
+ "option or param=value pairs, not both"
144
+ end
145
+ options[:params_parsed] = true
146
+ elsif params.any?
147
+ options[:params_parsed] = false
148
+ options[:params] = Hash[params.map { |a| a.split('=', 2) }]
149
+ else
150
+ options[:params_parsed] = true
151
+ options[:params] = {}
152
+ end
153
+ end
154
+ options[:leftovers] = remaining
155
+
156
+ # Default to verbose for everything except plans
157
+ unless options.key?(:verbose)
158
+ options[:verbose] = options[:subcommand] != 'plan'
159
+ end
160
+
161
+ validate(options)
162
+ validate_ps_version
163
+
164
+ options
165
+ end
166
+ end
167
+
168
+ # TODO: Move this to the parser.
169
+ #
170
+ # Print a welcome message when users first install Bolt and run `bolt`,
171
+ # `bolt help` or `bolt --help`.
172
+ #
173
+ private def welcome_message
174
+ bolt = <<~BOLT
175
+ `.::-`
176
+ `.-:///////-.`
177
+ `-:////:. `-:///:- /ooo. .ooo/
178
+ `.-:///::///:-` `-//: ymmm- :mmmy .---.
179
+ :///:-. `.:////. -//: ymmm- :mmmy +mmm+
180
+ ://. ///. -//: ymmm--/++/- `-/++/:` :mmmy-:smmms::-
181
+ ://. ://. .://: ymmmdmmmmmmdo` .smmmmmmmmh: :mmmysmmmmmmmms
182
+ ://. ://:///:-. ymmmh/--/hmmmy -mmmd/-.:hmmm+:mmmy.-smmms--.
183
+ ://:.` .-////:-` ymmm- ymmm:hmmm- `dmmm/mmmy +mmm+
184
+ `-:///:-..:///:-.` ymmm- ommm/dmmm` hmmm+mmmy +mmm+
185
+ `.-:////:-` ymmm+ /mmmm.ommms` /mmmh:mmmy +mmmo
186
+ `-.` ymmmmmhhmmmmd: ommmmhydmmmy`:mmmy -mmmmdhd
187
+ oyyy+shddhs/` .+shddhy+- -yyyo .ohddhs
188
+
189
+
190
+ BOLT
191
+ example_cmd = if Bolt::Util.windows?
192
+ "Invoke-BoltCommand -Command 'hostname' -Targets localhost"
193
+ else
194
+ "bolt command run 'hostname' --target localhost"
195
+ end
196
+ prev_cmd = String.new("bolt")
197
+ prev_cmd << " #{@argv[0]}" unless @argv.empty?
198
+
199
+ message = <<~MSG
200
+ 🎉 Welcome to Bolt #{VERSION}
201
+ 😌 We're here to help bring order to the chaos
202
+ 📖 Find our documentation at https://bolt.guide
203
+ 🙋 Ask a question in #bolt on https://slack.puppet.com/
204
+ 🔩 Contribute at https://github.com/puppetlabs/bolt/
205
+ 💡 Not sure where to start? Try "#{example_cmd}"
206
+
207
+ We only print this message once. Run "#{prev_cmd}" again for help text.
208
+ MSG
209
+
210
+ $stdout.print "\033[36m#{bolt}\033[0m"
211
+ $stdout.print message
212
+ end
213
+
214
+ # TODO: Move this to the parser.
215
+ #
216
+ # Validate the command. Ensure that the subcommand and action are
217
+ # recognized, all required arguments are specified, and only supported
218
+ # command-line options are used.
219
+ #
220
+ # @param options [Hash] The CLI options.
221
+ #
222
+ private def validate(options)
223
+ unless COMMANDS.include?(options[:subcommand])
224
+ command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
225
+ raise Bolt::CLIError,
226
+ "'#{options[:subcommand]}' is not a Bolt command. See '#{command}'."
227
+ end
228
+
229
+ actions = COMMANDS[options[:subcommand]]
230
+ if actions.any?
231
+ if options[:action].nil?
232
+ raise Bolt::CLIError,
233
+ "Expected an action of the form 'bolt #{options[:subcommand]} <action>'"
234
+ end
235
+
236
+ unless actions.include?(options[:action])
237
+ raise Bolt::CLIError,
238
+ "Expected action '#{options[:action]}' to be one of " \
239
+ "#{actions.join(', ')}"
240
+ end
241
+ end
242
+
243
+ if %w[task plan script].include?(options[:subcommand]) && options[:action] == 'run'
244
+ if options[:object].nil?
245
+ raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
246
+ end
247
+ end
248
+
249
+ # This may mean that we parsed a parameter as the object
250
+ if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
251
+ unless options[:object] =~ /\A([a-z][a-z0-9_]*)?(::[a-z][a-z0-9_]*)*\Z/
252
+ raise Bolt::CLIError,
253
+ "Invalid #{options[:subcommand]} '#{options[:object]}'"
254
+ end
255
+ end
256
+
257
+ if options[:subcommand] == 'apply' && (options[:object] && options[:code])
258
+ raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
259
+ end
260
+
261
+ if options[:subcommand] == 'apply' && (!options[:object] && !options[:code])
262
+ raise Bolt::CLIError, "a manifest file or --execute is required"
263
+ end
264
+
265
+ if options[:subcommand] == 'lookup' && !options[:object]
266
+ raise Bolt::CLIError, "Must specify a key to look up"
267
+ end
268
+
269
+ if options[:subcommand] == 'command' && (!options[:object] || options[:object].empty?)
270
+ raise Bolt::CLIError, "Must specify a command to run"
271
+ end
272
+
273
+ if options[:subcommand] == 'secret' &&
274
+ (options[:action] == 'decrypt' || options[:action] == 'encrypt') &&
275
+ !options[:object]
276
+ raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
277
+ end
278
+
279
+ if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
280
+ raise Bolt::CLIError, "Must specify a plan name."
281
+ end
282
+
283
+ if options[:subcommand] == 'module' && options[:action] == 'add' && !options[:object]
284
+ raise Bolt::CLIError, "Must specify a module name."
285
+ end
286
+
287
+ if options[:action] == 'convert' && !options[:object]
288
+ raise Bolt::CLIError, "Must specify a plan."
289
+ end
290
+
291
+ if options[:subcommand] == 'policy'
292
+ if options[:action] == 'apply' && !options[:object]
293
+ raise Bolt::CLIError, "Must specify one or more policies to apply."
294
+ end
295
+
296
+ if options[:action] == 'apply' && options[:leftovers].any?
297
+ raise Bolt::CLIError, "Unknown argument(s) #{options[:leftovers].join(', ')}. "\
298
+ "To apply multiple policies, provide a comma-separated list of "\
299
+ "policy names."
300
+ end
301
+
302
+ if options[:action] == 'new' && !options[:object]
303
+ raise Bolt::CLIError, "Must specify a name for the new policy."
304
+ end
305
+
306
+ if options[:action] == 'show' && options[:object]
307
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}."
308
+ end
309
+ end
310
+
311
+ if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
312
+ command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
313
+ raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
314
+ "the project, run '#{command} #{options[:object]}'."
315
+ end
316
+
317
+ if %w[download upload].include?(options[:action])
318
+ raise Bolt::CLIError, "Must specify a source" unless options[:object]
319
+
320
+ if options[:leftovers].empty?
321
+ raise Bolt::CLIError, "Must specify a destination"
322
+ elsif options[:leftovers].size > 1
323
+ raise Bolt::CLIError, "Unknown arguments #{options[:leftovers].drop(1).join(', ')}"
324
+ end
325
+ end
326
+
327
+ if options[:subcommand] == 'group' && options[:object]
328
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
329
+ end
330
+
331
+ if options[:action] == 'generate-types' && options[:object]
332
+ raise Bolt::CLIError, "Unknown argument #{options[:object]}"
333
+ end
334
+
335
+ if !%w[file script lookup].include?(options[:subcommand]) &&
336
+ !options[:leftovers].empty?
337
+ raise Bolt::CLIError,
338
+ "Unknown argument(s) #{options[:leftovers].join(', ')}"
339
+ end
340
+
341
+ target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
342
+ if options[:subcommand] == 'lookup' &&
343
+ target_opts.any? && options[:plan_hierarchy]
344
+ raise Bolt::CLIError, "The 'lookup' command accepts either targeting option OR --plan-hierarchy."
345
+ end
346
+
347
+ if options[:noop] &&
348
+ !(options[:subcommand] == 'task' && options[:action] == 'run') &&
349
+ options[:subcommand] != 'apply' &&
350
+ options[:action] != 'apply'
351
+ raise Bolt::CLIError,
352
+ "Option '--noop' can only be specified when running a task or applying manifest code"
353
+ end
354
+
355
+ if options[:env_vars]
356
+ unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
357
+ raise Bolt::CLIError,
358
+ "Option '--env-var' can only be specified when running a command or script"
359
+ end
360
+ end
361
+
362
+ validate_targeting_options(options)
363
+ end
364
+
365
+ # Validates that only one targeting option is provided and that commands
366
+ # requiring a targeting option received one.
367
+ #
368
+ # @param options [Hash] The CLI options.
369
+ #
370
+ private def validate_targeting_options(options)
371
+ target_opts = options.slice(*TARGETING_OPTIONS)
372
+ target_string = "'--targets', '--rerun', or '--query'"
373
+
374
+ if target_opts.length > 1
375
+ raise Bolt::CLIError, "Only one targeting option can be specified: #{target_string}"
376
+ end
377
+
378
+ return if %w[guide module plan project secret].include?(options[:subcommand]) ||
379
+ %w[convert new show].include?(options[:action]) ||
380
+ options[:plan_hierarchy]
381
+
382
+ if target_opts.empty?
383
+ raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
384
+ end
385
+ end
386
+
387
+ # Execute a Bolt command. The +options+ hash includes the subcommand and
388
+ # action to be run, as well as any additional arguments and options for the
389
+ # command.
390
+ #
391
+ # @param options [Hash] The CLI options.
392
+ #
393
+ def execute(options)
394
+ with_signal_handling do
395
+ with_error_handling do
396
+ # TODO: Separate from options hash and pass as own args.
397
+ command = options[:subcommand]
398
+ action = options[:action]
399
+
400
+ #
401
+ # INITIALIZE CORE CLASSES
402
+ #
403
+
404
+ project = if ENV['BOLT_PROJECT']
405
+ Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
406
+ elsif options[:project]
407
+ dir = Pathname.new(options[:project])
408
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
409
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
410
+ else
411
+ Bolt::Project.create_project(dir)
412
+ end
413
+ else
414
+ Bolt::Project.find_boltdir(Dir.pwd)
415
+ end
416
+
417
+ config = Bolt::Config.from_project(project, options)
418
+
419
+ @outputter = Bolt::Outputter.for_format(
420
+ config.format,
421
+ config.color,
422
+ options[:verbose],
423
+ config.trace,
424
+ config.spinner
425
+ )
426
+
427
+ @rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
428
+
429
+ # TODO: Subscribe this to the executor.
430
+ analytics = begin
431
+ client = Bolt::Analytics.build_client(config.analytics)
432
+ client.bundled_content = bundled_content(options)
433
+ client
434
+ end
435
+
436
+ Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
437
+ Bolt::Logger.stream = config.stream
438
+ Bolt::Logger.analytics = analytics
439
+ Bolt::Logger.flush_queue
440
+
441
+ executor = Bolt::Executor.new(
442
+ config.concurrency,
443
+ analytics,
444
+ options[:noop],
445
+ config.modified_concurrency,
446
+ config.future
447
+ )
448
+
449
+ pal = Bolt::PAL.new(
450
+ Bolt::Config::Modulepath.new(config.modulepath),
451
+ config.hiera_config,
452
+ config.project.resource_types,
453
+ config.compile_concurrency,
454
+ config.trusted_external,
455
+ config.apply_settings,
456
+ config.project
457
+ )
458
+
459
+ plugins = Bolt::Plugin.new(config, pal, analytics)
460
+
461
+ inventory = Bolt::Inventory.from_config(config, plugins)
462
+
463
+ log_outputter = Bolt::Outputter::Logger.new(options[:verbose], config.trace)
464
+
465
+ #
466
+ # FINALIZING SETUP
467
+ #
468
+
469
+ check_gem_install
470
+ warn_inventory_overrides_cli(config, options)
471
+ submit_screen_view(analytics, config, inventory, options)
472
+ options[:targets] = process_target_list(plugins, @rerun, options)
473
+
474
+ # TODO: Fix casing issue in Windows.
475
+ config.check_path_case('modulepath', config.modulepath)
476
+
477
+ if options[:clear_cache]
478
+ FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
479
+ FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
480
+ FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
481
+ end
482
+
483
+ case command
484
+ when 'apply', 'lookup'
485
+ if %w[human rainbow].include?(config.format)
486
+ executor.subscribe(outputter)
487
+ end
488
+ when 'plan'
489
+ if %w[human rainbow].include?(config.format)
490
+ executor.subscribe(outputter)
491
+ else
492
+ executor.subscribe(outputter, %i[message verbose])
493
+ end
494
+ else
495
+ executor.subscribe(outputter)
496
+ end
497
+
498
+ executor.subscribe(log_outputter)
499
+
500
+ # TODO: Figure out where this should really go. It doesn't seem to
501
+ # make sense in the application, since the params should already
502
+ # be data when they reach that point.
503
+ if %w[plan task].include?(command) && action == 'run'
504
+ options[:params] = parse_params(
505
+ command,
506
+ options[:object],
507
+ pal,
508
+ **options.slice(:params, :params_parsed)
509
+ )
510
+ end
511
+
512
+ application = Bolt::Application.new(
513
+ analytics: analytics,
514
+ config: config,
515
+ executor: executor,
516
+ inventory: inventory,
517
+ pal: pal,
518
+ plugins: plugins
519
+ )
520
+
521
+ process_command(application, command, action, options)
522
+ ensure
523
+ analytics&.finish
524
+ end
525
+ end
526
+ end
527
+
528
+ # Process the command.
529
+ #
530
+ # @param app [Bolt::Application] The application.
531
+ # @param command [String] The command.
532
+ # @param action [String, NilClass] The action.
533
+ # @param options [Hash] The CLI options.
534
+ #
535
+ private def process_command(app, command, action, options)
536
+ case command
537
+ when 'apply'
538
+ results = outputter.spin do
539
+ app.apply(options[:object], options[:targets], **options.slice(:code, :noop))
540
+ end
541
+ rerun.update(results)
542
+ app.shutdown
543
+ outputter.print_apply_result(results)
544
+ results.ok? ? SUCCESS : FAILURE
545
+
546
+ when 'command'
547
+ outputter.print_head
548
+ results = app.run_command(options[:object], options[:targets], **options.slice(:env_vars))
549
+ rerun.update(results)
550
+ app.shutdown
551
+ outputter.print_summary(results, results.elapsed_time)
552
+ results.ok? ? SUCCESS : FAILURE
553
+
554
+ when 'file'
555
+ case action
556
+ when 'download'
557
+ outputter.print_head
558
+ results = app.download_file(options[:object], options[:leftovers].first, options[:targets])
559
+ rerun.update(results)
560
+ app.shutdown
561
+ outputter.print_summary(results, results.elapsed_time)
562
+ results.ok? ? SUCCESS : FAILURE
563
+ when 'upload'
564
+ outputter.print_head
565
+ results = app.upload_file(options[:object], options[:leftovers].first, options[:targets])
566
+ rerun.update(results)
567
+ app.shutdown
568
+ outputter.print_summary(results, results.elapsed_time)
569
+ results.ok? ? SUCCESS : FAILURE
570
+ end
571
+
572
+ when 'group'
573
+ outputter.print_groups(**app.list_groups)
574
+ SUCCESS
575
+
576
+ when 'guide'
577
+ if options[:object]
578
+ outputter.print_guide(**app.show_guide(options[:object]))
579
+ else
580
+ outputter.print_topics(**app.list_guides)
581
+ end
582
+ SUCCESS
583
+
584
+ when 'inventory'
585
+ targets = app.show_inventory(options[:targets])
586
+ .merge(flag: !options[:targets].nil?)
587
+ if options[:detail]
588
+ outputter.print_target_info(**targets)
589
+ else
590
+ outputter.print_targets(**targets)
591
+ end
592
+ SUCCESS
593
+
594
+ when 'lookup'
595
+ options[:vars] = parse_vars(options[:leftovers])
596
+ if options[:plan_hierarchy]
597
+ outputter.print_plan_lookup(app.plan_lookup(options[:object], **options.slice(:vars)))
598
+ SUCCESS
599
+ else
600
+ results = outputter.spin do
601
+ app.lookup(options[:object], options[:targets], **options.slice(:vars))
602
+ end
603
+ rerun.update(results)
604
+ app.shutdown
605
+ outputter.print_result_set(results)
606
+ results.ok? ? SUCCESS : FAILURE
607
+ end
608
+
609
+ when 'module'
610
+ case action
611
+ when 'add'
612
+ ok = outputter.spin { app.add_module(options[:object], outputter) }
613
+ ok ? SUCCESS : FAILURE
614
+ when 'generate-types'
615
+ app.generate_types
616
+ SUCCESS
617
+ when 'install'
618
+ ok = outputter.spin { app.install_modules(outputter, **options.slice(:force, :resolve)) }
619
+ ok ? SUCCESS : FAILURE
620
+ when 'show'
621
+ if options[:object]
622
+ outputter.print_module_info(**app.show_module(options[:object]))
623
+ else
624
+ outputter.print_module_list(app.list_modules)
625
+ end
626
+ SUCCESS
627
+ end
628
+
629
+ when 'plan'
630
+ case action
631
+ when 'convert'
632
+ app.convert_plan(options[:object])
633
+ SUCCESS
634
+ when 'new'
635
+ result = app.new_plan(options[:object], **options.slice(:puppet, :plan_script))
636
+ outputter.print_new_plan(**result)
637
+ SUCCESS
638
+ when 'run'
639
+ result = app.run_plan(options[:object], options[:targets], **options.slice(:params))
640
+ rerun.update(result)
641
+ app.shutdown
642
+ outputter.print_plan_result(result)
643
+ result.ok? ? SUCCESS : FAILURE
644
+ when 'show'
645
+ if options[:object]
646
+ outputter.print_plan_info(app.show_plan(options[:object]))
647
+ else
648
+ outputter.print_plans(**app.list_plans(**options.slice(:filter)))
649
+ end
650
+ SUCCESS
651
+ end
652
+
653
+ when 'plugin'
654
+ outputter.print_plugin_list(**app.list_plugins)
655
+ SUCCESS
656
+
657
+ when 'policy'
658
+ Bolt::Logger.warn('policy_command', 'This command is experimental and is subject to change.')
659
+ case action
660
+ when 'apply'
661
+ results = outputter.spin do
662
+ app.apply_policies(options[:object], options[:targets], **options.slice(:noop))
663
+ end
664
+ rerun.update(results)
665
+ app.shutdown
666
+ outputter.print_apply_result(results)
667
+ results.ok? ? SUCCESS : FAILURE
668
+ when 'new'
669
+ result = app.new_policy(options[:object])
670
+ outputter.print_new_policy(**result)
671
+ SUCCESS
672
+ when 'show'
673
+ outputter.print_policy_list(**app.list_policies)
674
+ SUCCESS
675
+ end
676
+
677
+ when 'project'
678
+ case action
679
+ when 'init'
680
+ app.create_project(options[:object], outputter, **options.slice(:modules))
681
+ SUCCESS
682
+ when 'migrate'
683
+ app.migrate_project(outputter)
684
+ SUCCESS
685
+ end
686
+
687
+ when 'script'
688
+ outputter.print_head
689
+ opts = options.slice(:env_vars).merge(arguments: options[:leftovers])
690
+ results = app.run_script(options[:object], options[:targets], **opts)
691
+ rerun.update(results)
692
+ app.shutdown
693
+ outputter.print_summary(results, results.elapsed_time)
694
+ results.ok? ? SUCCESS : FAILURE
695
+
696
+ when 'secret'
697
+ case action
698
+ when 'createkeys'
699
+ result = app.create_secret_keys(**options.slice(:force, :plugin))
700
+ outputter.print_message(result)
701
+ SUCCESS
702
+ when 'decrypt'
703
+ result = app.decrypt_secret(options[:object], **options.slice(:plugin))
704
+ outputter.print_message(result)
705
+ SUCCESS
706
+ when 'encrypt'
707
+ result = app.encrypt_secret(options[:object], **options.slice(:plugin))
708
+ outputter.print_message(result)
709
+ SUCCESS
710
+ end
711
+
712
+ when 'task'
713
+ case action
714
+ when 'run'
715
+ outputter.print_head
716
+ results = app.run_task(options[:object], options[:targets], **options.slice(:params))
717
+ rerun.update(results)
718
+ app.shutdown
719
+ outputter.print_summary(results, results.elapsed_time)
720
+ results.ok? ? SUCCESS : FAILURE
721
+ when 'show'
722
+ if options[:object]
723
+ outputter.print_task_info(**app.show_task(options[:object]))
724
+ else
725
+ outputter.print_tasks(**app.list_tasks(**options.slice(:filter)))
726
+ end
727
+ SUCCESS
728
+ end
729
+ end
730
+ end
731
+
732
+ # Process the target list by turning a PuppetDB query or rerun mode into a
733
+ # list of target names.
734
+ #
735
+ # @param plugins [Bolt::Plugin] The Plugin instance.
736
+ # @param rerun [Bolt::Rerun] The Rerun instance.
737
+ # @param options [Hash] The CLI options.
738
+ # @return [Hash] The target list.
739
+ #
740
+ private def process_target_list(plugins, rerun, options)
741
+ if options[:query]
742
+ plugins.puppetdb_client.query_certnames(options[:query])
743
+ elsif options[:rerun]
744
+ rerun.get_targets(options[:rerun])
745
+ elsif options[:targets]
746
+ options[:targets]
747
+ end
748
+ end
749
+
750
+ # List content that ships with Bolt.
751
+ #
752
+ # @param options [Hash] The CLI options.
753
+ #
754
+ private def bundled_content(options)
755
+ # We only need to enumerate bundled content when running a task or plan
756
+ content = { 'Plan' => [],
757
+ 'Task' => [],
758
+ 'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
759
+ if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
760
+ default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
761
+ content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
762
+ col << iter&.first
763
+ end
764
+ content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
765
+ col << iter&.first
766
+ end
767
+ end
768
+
769
+ content
770
+ end
771
+
772
+ # Check and warn if Bolt is installed as a gem.
773
+ #
774
+ private def check_gem_install
775
+ if ENV['BOLT_GEM'].nil? && incomplete_install?
776
+ msg = <<~MSG.chomp
777
+ Bolt might be installed as a gem. To use Bolt reliably and with all of its
778
+ dependencies, uninstall the 'bolt' gem and install Bolt as a package:
779
+ https://puppet.com/docs/bolt/latest/bolt_installing.html
780
+
781
+ If you meant to install Bolt as a gem and want to disable this warning,
782
+ set the BOLT_GEM environment variable.
783
+ MSG
784
+
785
+ Bolt::Logger.warn("gem_install", msg)
786
+ end
787
+ end
788
+
789
+ # Print a fatal error. Print using the outputter if it's configured.
790
+ # Otherwise, mock the output by printing directly to stdout.
791
+ #
792
+ # @param error [StandardError] The error to print.
793
+ #
794
+ private def fatal_error(error)
795
+ if @outputter
796
+ @outputter.fatal_error(error)
797
+ elsif $stdout.isatty
798
+ $stdout.puts("\033[31m#{error.message}\033[0m")
799
+ else
800
+ $stdout.puts(error.message)
801
+ end
802
+ end
803
+
804
+ # Query whether Bolt is installed as a gem or package by checking if all
805
+ # built-in modules are installed.
806
+ #
807
+ private def incomplete_install?
808
+ builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
809
+ (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
810
+ end
811
+
812
+ # Parse parameters for tasks and plans.
813
+ #
814
+ # @param options [Hash] Options from the calling method.
815
+ #
816
+ private def parse_params(command, object, pal, params: nil, params_parsed: nil)
817
+ if params
818
+ params_parsed ? params : pal.parse_params(command, object, params)
819
+ else
820
+ {}
821
+ end
822
+ end
823
+
824
+ # Parse variables for lookups.
825
+ #
826
+ # @param vars [Array, NilClass] Unparsed variables.
827
+ #
828
+ private def parse_vars(vars)
829
+ return unless vars
830
+ Hash[vars.map { |a| a.split('=', 2) }]
831
+ end
832
+
833
+ # TODO: See if this can be moved to Bolt::Analytics.
834
+ #
835
+ # Submit a screen view to the analytics client.
836
+ #
837
+ # @param analytics [Bolt::Analytics] The analytics client.
838
+ # @param config [Bolt::Config] The config.
839
+ # @param inventory [Bolt::Inventory] The inventory.
840
+ # @param options [Hash] The CLI options.
841
+ #
842
+ private def submit_screen_view(analytics, config, inventory, options)
843
+ screen = "#{options[:subcommand]}_#{options[:action]}"
844
+
845
+ if options[:action] == 'show' && options[:object]
846
+ screen += '_object'
847
+ end
848
+
849
+ pp_count, yaml_count = if File.exist?(config.project.plans_path)
850
+ %w[pp yaml].map do |extension|
851
+ Find.find(config.project.plans_path.to_s)
852
+ .grep(/.*\.#{extension}/)
853
+ .length
854
+ end
855
+ else
856
+ [0, 0]
857
+ end
858
+
859
+ screen_view_fields = {
860
+ output_format: config.format,
861
+ boltdir_type: config.project.type,
862
+ puppet_plan_count: pp_count,
863
+ yaml_plan_count: yaml_count
864
+ }
865
+
866
+ if options.key?(:targets)
867
+ screen_view_fields.merge!(
868
+ target_nodes: options[:targets].count,
869
+ inventory_nodes: inventory.node_names.count,
870
+ inventory_groups: inventory.group_names.count,
871
+ inventory_version: inventory.version
872
+ )
873
+ end
874
+
875
+ analytics.screen_view(screen, **screen_view_fields)
876
+ end
877
+
878
+ # Issue a deprecation warning if the user is running an unsupported version
879
+ # of PowerShell on the controller.
880
+ #
881
+ private def validate_ps_version
882
+ if Bolt::Util.powershell?
883
+ command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
884
+ "Bypass -Command $PSVersionTable.PSVersion.Major"
885
+ stdout, _stderr, _status = Open3.capture3(command)
886
+
887
+ return unless !stdout.empty? && stdout.to_i < 3
888
+
889
+ msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
890
+ Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
891
+ end
892
+ end
893
+
894
+ # Warn the user that transport configuration options set from the command
895
+ # line may be overridden by transport configuration set in the inventory.
896
+ #
897
+ # @param opts [Hash] The CLI options.
898
+ #
899
+ private def warn_inventory_overrides_cli(config, opts)
900
+ inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
901
+ Bolt::Inventory::ENVIRONMENT_VAR
902
+ elsif config.inventoryfile
903
+ config.inventoryfile
904
+ elsif File.exist?(config.default_inventoryfile)
905
+ config.default_inventoryfile
906
+ end
907
+
908
+ inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
909
+ acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
910
+ end
911
+
912
+ inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
913
+
914
+ conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
915
+
916
+ if inventory_source && conflicting_options.any?
917
+ Bolt::Logger.warn(
918
+ "cli_overrides",
919
+ "CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
920
+ )
921
+ end
922
+ end
923
+
924
+ # Handle and print errors.
925
+ #
926
+ private def with_error_handling
927
+ yield
928
+ rescue Bolt::Error => e
929
+ fatal_error(e)
930
+ raise e
931
+ end
932
+
933
+ # Handle signals.
934
+ #
935
+ private def with_signal_handling
936
+ handler = Signal.trap :INT do |signo|
937
+ Bolt::Logger.logger(self).info(
938
+ "Exiting after receiving SIG#{Signal.signame(signo)} signal. "\
939
+ "There might be processes left executing on some targets."
940
+ )
941
+ exit!
942
+ end
943
+
944
+ yield
945
+ ensure
946
+ Signal.trap :INT, handler if handler
947
+ end
948
+ end
949
+ end