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
@@ -0,0 +1,516 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bash/tmpdir'
4
+ require 'shellwords'
5
+
6
+ module Bolt
7
+ class Shell
8
+ class Bash < Shell
9
+ CHUNK_SIZE = 4096
10
+
11
+ def initialize(target, conn)
12
+ super
13
+
14
+ @run_as = nil
15
+ @sudo_id = SecureRandom.uuid
16
+ @sudo_password = @target.options['sudo-password'] || @target.password
17
+ end
18
+
19
+ def provided_features
20
+ ['shell']
21
+ end
22
+
23
+ def run_command(command, options = {}, position = [])
24
+ running_as(options[:run_as]) do
25
+ output = execute(command, environment: options[:env_vars], sudoable: true)
26
+ Bolt::Result.for_command(target,
27
+ output.to_h,
28
+ 'command',
29
+ command,
30
+ position)
31
+ end
32
+ end
33
+
34
+ def upload(source, destination, options = {})
35
+ running_as(options[:run_as]) do
36
+ with_tmpdir do |dir|
37
+ basename = File.basename(source)
38
+ tmpfile = File.join(dir.to_s, basename)
39
+ conn.upload_file(source, tmpfile)
40
+ # pass over file ownership if we're using run-as to be a different user
41
+ dir.chown(run_as)
42
+ result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
43
+ if result.exit_code != 0
44
+ message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
45
+ raise Bolt::Node::FileError.new(message, 'MV_ERROR')
46
+ end
47
+ end
48
+ Bolt::Result.for_upload(target, source, destination)
49
+ end
50
+ end
51
+
52
+ def download(source, destination, options = {})
53
+ running_as(options[:run_as]) do
54
+ download = File.join(destination, Bolt::Util.unix_basename(source))
55
+
56
+ # If using run-as, the file is copied to a tmpdir and chowned to the
57
+ # connecting user. This is a workaround for limitations in net-ssh that
58
+ # only allow for downloading files as the connecting user, which is a
59
+ # problem for users who cannot connect to targets as the root user.
60
+ # This temporary copy should *always* be deleted.
61
+ if run_as
62
+ with_tmpdir(force_cleanup: true) do |dir|
63
+ tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source))
64
+
65
+ result = execute(['cp', '-r', source, dir.to_s], sudoable: true)
66
+
67
+ if result.exit_code != 0
68
+ message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}"
69
+ raise Bolt::Node::FileError.new(message, 'CP_ERROR')
70
+ end
71
+
72
+ # We need to force the chown, otherwise this will just return
73
+ # without doing anything since the chown user is the same as the
74
+ # connecting user.
75
+ dir.chown(conn.user, force: true)
76
+
77
+ conn.download_file(tmpfile, destination, download)
78
+ end
79
+ # If not using run-as, we can skip creating a temporary copy and just
80
+ # download the file directly.
81
+ else
82
+ conn.download_file(source, destination, download)
83
+ end
84
+
85
+ Bolt::Result.for_download(target, source, destination, download)
86
+ end
87
+ end
88
+
89
+ def run_script(script, arguments, options = {}, position = [])
90
+ # unpack any Sensitive data
91
+ arguments = unwrap_sensitive_args(arguments)
92
+
93
+ running_as(options[:run_as]) do
94
+ with_tmpdir do |dir|
95
+ path = write_executable(dir.to_s, script)
96
+ dir.chown(run_as)
97
+
98
+ exec_args = [path, *arguments]
99
+ interpreter = select_interpreter(script, target.options['interpreters'])
100
+
101
+ # Only use interpreter if script_interpreter config is enabled
102
+ if options[:script_interpreter] && interpreter
103
+ exec_args.unshift(interpreter).flatten!
104
+ logger.trace("Running '#{script}' using '#{interpreter}' interpreter")
105
+ end
106
+
107
+ output = execute(exec_args, environment: options[:env_vars], sudoable: true)
108
+ Bolt::Result.for_command(target,
109
+ output.to_h,
110
+ 'script',
111
+ script,
112
+ position)
113
+ end
114
+ end
115
+ end
116
+
117
+ def run_task(task, arguments, options = {}, position = [])
118
+ implementation = select_implementation(target, task)
119
+ executable = implementation['path']
120
+ input_method = implementation['input_method']
121
+ extra_files = implementation['files']
122
+
123
+ running_as(options[:run_as]) do
124
+ stdin, output = nil
125
+ execute_options = {}
126
+ execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
127
+ interpreter_debug = if execute_options[:interpreter]
128
+ " using '#{execute_options[:interpreter]}' interpreter"
129
+ end
130
+ # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
131
+ logger.trace("Running '#{executable}' with #{arguments.to_json}#{interpreter_debug}")
132
+ # unpack any Sensitive data
133
+ arguments = unwrap_sensitive_args(arguments)
134
+
135
+ with_tmpdir do |dir|
136
+ if extra_files.empty?
137
+ task_dir = dir
138
+ else
139
+ # TODO: optimize upload of directories
140
+ arguments['_installdir'] = dir.to_s
141
+ task_dir = File.join(dir.to_s, task.tasks_dir)
142
+ dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
143
+ extra_files.each do |file|
144
+ conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
145
+ end
146
+ end
147
+
148
+ if Bolt::Task::STDIN_METHODS.include?(input_method)
149
+ stdin = JSON.dump(arguments)
150
+ end
151
+
152
+ if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
153
+ execute_options[:environment] = envify_params(arguments)
154
+ end
155
+
156
+ remote_task_path = write_executable(task_dir, executable)
157
+
158
+ execute_options[:stdin] = stdin
159
+
160
+ # Avoid the horrors of passing data on stdin via a tty on multiple platforms
161
+ # by writing a wrapper script that directs stdin to the task.
162
+ if stdin && target.options['tty']
163
+ wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
164
+ # Wrapper script handles interpreter and stdin. Delete these execute options
165
+ execute_options.delete(:interpreter)
166
+ execute_options.delete(:stdin)
167
+ execute_options[:wrapper] = true
168
+ remote_task_path = write_executable(dir, wrapper, 'wrapper.sh')
169
+ end
170
+
171
+ dir.chown(run_as)
172
+
173
+ execute_options[:sudoable] = true if run_as
174
+ output = execute(remote_task_path, **execute_options)
175
+ end
176
+ Bolt::Result.for_task(target, output.stdout.string,
177
+ output.stderr.string,
178
+ output.exit_code,
179
+ task.name,
180
+ position)
181
+ end
182
+ end
183
+
184
+ # If prompted for sudo password, send password to stdin and return an
185
+ # empty string. Otherwise, check for sudo errors and raise Bolt error.
186
+ # If sudo_id is detected, that means the task needs to have stdin written.
187
+ # If error is not sudo-related, return the stderr string to be added to
188
+ # node output
189
+ def handle_sudo(stdin, err, sudo_stdin)
190
+ if err.include?(sudo_prompt)
191
+ # A wild sudo prompt has appeared!
192
+ if @sudo_password
193
+ stdin.write("#{@sudo_password}\n")
194
+ ''
195
+ else
196
+ raise Bolt::Node::EscalateError.new(
197
+ "Sudo password for user #{conn.user} was not provided for #{target}",
198
+ 'NO_PASSWORD'
199
+ )
200
+ end
201
+ elsif err =~ /^#{@sudo_id}/
202
+ if sudo_stdin
203
+ begin
204
+ stdin.write("#{sudo_stdin}\n")
205
+ stdin.close
206
+ # If a task has stdin as an input_method but doesn't actually read
207
+ # from stdin, the task may return and close the input stream before
208
+ # we finish writing
209
+ rescue Errno::EPIPE
210
+ end
211
+ end
212
+ ''
213
+ else
214
+ handle_sudo_errors(err)
215
+ end
216
+ end
217
+
218
+ # See if there's a sudo prompt in the output
219
+ # If not, return the output
220
+ def check_sudo(out, inp, stdin)
221
+ buffer = out.readpartial(CHUNK_SIZE)
222
+ # Split on newlines, including the newline
223
+ lines = buffer.split(/(?<=\n)/)
224
+ # handle_sudo will return the line if it is not a sudo prompt or error
225
+ lines.map! { |line| handle_sudo(inp, line, stdin) }
226
+ lines.join
227
+ # If stream has reached EOF, no password prompt is expected
228
+ # return an empty string
229
+ rescue EOFError
230
+ ''
231
+ end
232
+
233
+ def handle_sudo_errors(err)
234
+ case err
235
+ when /^#{conn.user} is not in the sudoers file\./
236
+ @logger.trace { err }
237
+ raise Bolt::Node::EscalateError.new(
238
+ "User #{conn.user} does not have sudo permission on #{target}",
239
+ 'SUDO_DENIED'
240
+ )
241
+ when /^Sorry, try again\./
242
+ @logger.trace { err }
243
+ raise Bolt::Node::EscalateError.new(
244
+ "Sudo password for user #{conn.user} not recognized on #{target}",
245
+ 'BAD_PASSWORD'
246
+ )
247
+ else
248
+ # No need to raise an error - just return the string
249
+ err
250
+ end
251
+ end
252
+
253
+ def make_wrapper_stringio(task_path, stdin, interpreter = nil)
254
+ if interpreter
255
+ StringIO.new(<<~SCRIPT)
256
+ #!/bin/sh
257
+ #{Array(interpreter).map { |word| "'#{word}'" }.join(' ')} '#{task_path}' <<'EOF'
258
+ #{stdin}
259
+ EOF
260
+ SCRIPT
261
+ else
262
+ StringIO.new(<<~SCRIPT)
263
+ #!/bin/sh
264
+ '#{task_path}' <<'EOF'
265
+ #{stdin}
266
+ EOF
267
+ SCRIPT
268
+ end
269
+ end
270
+
271
+ # This method allows the @run_as variable to be used as a per-operation
272
+ # override for the user to run as. When @run_as is unset, the user
273
+ # specified on the target will be used.
274
+ def run_as
275
+ @run_as || target.options['run-as']
276
+ end
277
+
278
+ # Run as the specified user for the duration of the block.
279
+ def running_as(user)
280
+ @run_as = user
281
+ yield
282
+ ensure
283
+ @run_as = nil
284
+ end
285
+
286
+ def make_executable(path)
287
+ result = execute(['chmod', 'u+x', path])
288
+ if result.exit_code != 0
289
+ message = "Could not make file '#{path}' executable: #{result.stderr.string}"
290
+ raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
291
+ end
292
+ end
293
+
294
+ def make_tmpdir
295
+ tmpdir = @target.options.fetch('tmpdir', '/tmp')
296
+ script_dir = @target.options.fetch('script-dir', SecureRandom.uuid)
297
+ tmppath = File.join(tmpdir, script_dir)
298
+ command = ['mkdir', '-m', 700, tmppath]
299
+
300
+ result = execute(command)
301
+ if result.exit_code != 0
302
+ raise Bolt::Node::FileError.new("Could not make tmpdir: #{result.stderr.string}", 'TMPDIR_ERROR')
303
+ end
304
+ path = tmppath || result.stdout.string.chomp
305
+ Bolt::Shell::Bash::Tmpdir.new(self, path)
306
+ end
307
+
308
+ def write_executable(dir, file, filename = nil)
309
+ filename ||= File.basename(file)
310
+ remote_path = File.join(dir.to_s, filename)
311
+ conn.upload_file(file, remote_path)
312
+ make_executable(remote_path)
313
+ remote_path
314
+ end
315
+
316
+ # A helper to create and delete a tmpdir on the remote system. Yields the
317
+ # directory name.
318
+ def with_tmpdir(force_cleanup: false)
319
+ dir = make_tmpdir
320
+ yield dir
321
+ ensure
322
+ if dir
323
+ if target.options['cleanup'] || force_cleanup
324
+ dir.delete
325
+ else
326
+ Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
327
+ end
328
+ end
329
+ end
330
+
331
+ def sudo_success(sudo_id)
332
+ "echo #{sudo_id} 1>&2"
333
+ end
334
+
335
+ # Returns string with the interpreter conditionally prepended
336
+ def inject_interpreter(interpreter, command)
337
+ if interpreter
338
+ command = Array(command).unshift(interpreter).flatten
339
+ end
340
+
341
+ command.is_a?(String) ? command : Shellwords.shelljoin(command)
342
+ end
343
+
344
+ def execute(command, sudoable: false, **options)
345
+ run_as = options[:run_as] || self.run_as
346
+ escalate = sudoable && run_as && conn.user != run_as
347
+ use_sudo = escalate && @target.options['run-as-command'].nil?
348
+
349
+ # Depending on the transport, whether we're using sudo and whether
350
+ # there are environment variables to set, we may need to stitch
351
+ # together multiple commands into a single sh invocation
352
+ commands = [inject_interpreter(options[:interpreter], command)]
353
+
354
+ # Let the transport handle adding environment variables if it's custom.
355
+ if options[:environment]
356
+ if defined? conn.add_env_vars
357
+ conn.add_env_vars(options[:environment])
358
+ else
359
+ env_decl = '/usr/bin/env ' + options[:environment].map do |env, val|
360
+ "#{env}=#{Shellwords.shellescape(val)}"
361
+ end.join(' ')
362
+ end
363
+ end
364
+
365
+ if escalate
366
+ sudo_str = if use_sudo
367
+ sudo_exec = target.options['sudo-executable'] || "sudo"
368
+ sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
369
+ Shellwords.shelljoin(sudo_flags)
370
+ else
371
+ Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
372
+ end
373
+ commands.unshift('cd') if conn.reset_cwd?
374
+ commands.unshift(sudo_success(@sudo_id)) if options[:stdin] && !options[:wrapper]
375
+ end
376
+
377
+ command_str = if sudo_str || env_decl
378
+ "sh -c #{Shellwords.shellescape(commands.join('; '))}"
379
+ else
380
+ commands.last
381
+ end
382
+
383
+ command_str = [sudo_str, env_decl, command_str].compact.join(' ')
384
+
385
+ @logger.trace { "Executing `#{command_str}`" }
386
+
387
+ in_buffer = if !use_sudo && options[:stdin]
388
+ String.new(options[:stdin], encoding: 'binary')
389
+ else
390
+ String.new(encoding: 'binary')
391
+ end
392
+ # Chunks of this size will be read in one iteration
393
+ index = 0
394
+ timeout = 0.1
395
+ result_output = Bolt::Node::Output.new
396
+
397
+ inp, out, err, t = conn.execute(command_str)
398
+ read_streams = { out => String.new,
399
+ err => String.new }
400
+ write_stream = in_buffer.empty? ? [] : [inp]
401
+
402
+ # See if there's a sudo prompt
403
+ if use_sudo
404
+ ready_read = select([err], nil, nil, timeout * 5)
405
+ to_print = check_sudo(err, inp, options[:stdin]) if ready_read
406
+ unless to_print.nil?
407
+ log_stream(to_print, 'err')
408
+ read_streams[err] << to_print
409
+ result_output.merged_output << to_print
410
+ end
411
+ end
412
+
413
+ # True while the process is running or waiting for IO input
414
+ while t.alive?
415
+ # See if we can read from out or err, or write to in
416
+ ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
417
+
418
+ ready_read&.each do |stream|
419
+ stream_name = stream == out ? 'out' : 'err'
420
+ # Check for sudo prompt
421
+ to_print = if use_sudo
422
+ check_sudo(stream, inp, options[:stdin])
423
+ else
424
+ stream.readpartial(CHUNK_SIZE)
425
+ end
426
+ log_stream(to_print, stream_name)
427
+ read_streams[stream] << to_print
428
+ result_output.merged_output << to_print
429
+ rescue EOFError
430
+ end
431
+
432
+ # select will either return an empty array if there are no
433
+ # writable streams or nil if no IO object is available before the
434
+ # timeout is reached.
435
+ writable = if ready_write.respond_to?(:empty?)
436
+ !ready_write.empty?
437
+ else
438
+ !ready_write.nil?
439
+ end
440
+
441
+ begin
442
+ if writable && index < in_buffer.length
443
+ to_print = in_buffer[index..-1]
444
+ # On Windows, select marks the input stream as writable even if
445
+ # it's full. We need to check whether we received wait_writable
446
+ # and treat that as not having written anything.
447
+ written = inp.write_nonblock(to_print, exception: false)
448
+ index += written unless written == :wait_writable
449
+
450
+ if index >= in_buffer.length && !write_stream.empty?
451
+ inp.close
452
+ write_stream = []
453
+ end
454
+ end
455
+ # If a task has stdin as an input_method but doesn't actually read
456
+ # from stdin, the task may return and close the input stream before
457
+ # we finish writing
458
+ rescue Errno::EPIPE
459
+ write_stream = []
460
+ end
461
+ end
462
+ # Read any remaining data in the pipe. Do not wait for
463
+ # EOF in case the pipe is inherited by a child process.
464
+ read_streams.each do |stream, _|
465
+ stream_name = stream == out ? 'out' : 'err'
466
+ loop {
467
+ to_print = stream.read_nonblock(CHUNK_SIZE)
468
+ log_stream(to_print, stream_name)
469
+ read_streams[stream] << to_print
470
+ result_output.merged_output << to_print
471
+ }
472
+ rescue Errno::EAGAIN, EOFError
473
+ ensure
474
+ stream.close
475
+ end
476
+ inp.close
477
+ result_output.stdout << read_streams[out]
478
+ result_output.stderr << read_streams[err]
479
+ result_output.exit_code = t.value.respond_to?(:exitstatus) ? t.value.exitstatus : t.value
480
+
481
+ case result_output.exit_code
482
+ when 0
483
+ @logger.trace { "Command `#{command_str}` returned successfully" }
484
+ when 126
485
+ msg = "\n\nThis might be caused by the default tmpdir being mounted "\
486
+ "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
487
+ result_output.stderr << msg
488
+ result_output.merged_output << msg
489
+ @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
490
+ else
491
+ @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
492
+ end
493
+ result_output
494
+ rescue StandardError
495
+ # Ensure we close stdin and kill the child process
496
+ inp.close unless inp.nil? || inp.closed?
497
+ t&.terminate if t&.alive?
498
+ @logger.trace { "Command aborted" }
499
+ raise
500
+ end
501
+
502
+ def sudo_prompt
503
+ '[sudo] Bolt needs to run as another user, password: '
504
+ end
505
+
506
+ private def log_stream(to_print, stream_name)
507
+ if !to_print.chomp.empty? && @stream_logger
508
+ formatted = to_print.lines.map do |msg|
509
+ "[#{@target.safe_name}] #{stream_name}: #{msg.chomp}"
510
+ end.join("\n")
511
+ @stream_logger.warn(formatted)
512
+ end
513
+ end
514
+ end
515
+ end
516
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Shell
5
+ class Powershell < Shell
6
+ module Snippets
7
+ class << self
8
+ def execute_process(command)
9
+ <<~PS
10
+ if ([Console]::InputEncoding -eq [System.Text.Encoding]::UTF8) {
11
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding $False
12
+ }
13
+ if ([Console]::OutputEncoding -eq [System.Text.Encoding]::UTF8) {
14
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $False
15
+ }
16
+ $OutputEncoding = [Console]::OutputEncoding
17
+ #{command}
18
+ if (-not $? -and ($LASTEXITCODE -eq $null)) { exit 1 }
19
+ exit $LASTEXITCODE
20
+ PS
21
+ end
22
+
23
+ def exit_with_code(command)
24
+ <<~PS
25
+ #{command}
26
+ if (-not $? -and ($LASTEXITCODE -eq $null)) { exit 1 }
27
+ exit $LASTEXITCODE
28
+ PS
29
+ end
30
+
31
+ def make_tmpdir(parent)
32
+ <<~PS
33
+ $parent = #{parent}
34
+ $name = [System.IO.Path]::GetRandomFileName()
35
+ $path = Join-Path $parent $name -ErrorAction Stop
36
+ New-Item -ItemType Directory -Path $path -ErrorAction Stop | Out-Null
37
+ $path
38
+ PS
39
+ end
40
+
41
+ def rmdir(dir)
42
+ <<~PS
43
+ Remove-Item -Force -Recurse -Path "#{dir}"
44
+ PS
45
+ end
46
+
47
+ def run_script(arguments, script_path)
48
+ build_arg_list = arguments.map do |a|
49
+ "$invokeArgs.ArgumentList += @'\n#{a}\n'@"
50
+ end.join("\n")
51
+ <<~PS
52
+ $invokeArgs = @{
53
+ ScriptBlock = (Get-Command "#{script_path}").ScriptBlock
54
+ ArgumentList = @()
55
+ }
56
+ #{build_arg_list}
57
+
58
+ try
59
+ {
60
+ switch -regex ( Get-ExecutionPolicy )
61
+ {
62
+ '^AllSigned'
63
+ {
64
+ if ((Get-AuthenticodeSignature -File "#{script_path}").Status -ne 'Valid') {
65
+ $Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} and script '#{script_path}' does not contain a valid signature.")
66
+ exit 1;
67
+ }
68
+ }
69
+ '^Restricted'
70
+ {
71
+ $Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} which denies running any scripts on the target.")
72
+ exit 1;
73
+ }
74
+ }
75
+ }
76
+ catch {}
77
+
78
+ if([string]::IsNullOrEmpty($invokeArgs.ScriptBlock)){
79
+ $Host.UI.WriteErrorLine("Error: Failed to obtain scriptblock from '#{script_path}'. Running scripts might be disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170");
80
+ exit 1;
81
+ }
82
+
83
+ try
84
+ {
85
+ Invoke-Command @invokeArgs
86
+ }
87
+ catch
88
+ {
89
+ $Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
90
+ exit 1;
91
+ }
92
+ PS
93
+ end
94
+
95
+ def append_ps_module_path(directory)
96
+ <<~PS
97
+ $env:PSModulePath += ";#{directory}"
98
+ PS
99
+ end
100
+
101
+ def ps_task(path, arguments)
102
+ <<~PS
103
+ $private:tempArgs = Get-ContentAsJson (
104
+ [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
105
+ )
106
+ $allowedArgs = (Get-Command "#{path}").Parameters.Keys
107
+ $private:taskArgs = @{}
108
+ $private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
109
+ try {
110
+ & "#{path}" @taskArgs
111
+ } catch {
112
+ $Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
113
+ exit 1;
114
+ }
115
+ PS
116
+ end
117
+
118
+ def try_catch(command)
119
+ %(try { & "#{command}" } catch { Write-Error $_.Exception; exit 1 })
120
+ end
121
+
122
+ def shell_init
123
+ <<~PS
124
+ $installRegKey = Get-ItemProperty -Path "HKLM:\\Software\\Puppet Labs\\Puppet" -ErrorAction 0
125
+ if(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir64)){
126
+ $boltBaseDir = $installRegKey.RememberedInstallDir64
127
+ }elseif(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir)){
128
+ $boltBaseDir = $installRegKey.RememberedInstallDir
129
+ }else{
130
+ $boltBaseDir = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet"
131
+ }
132
+
133
+ $ENV:PATH += ";${boltBaseDir}\\bin\\;" +
134
+ "${boltBaseDir}\\puppet\\bin;" +
135
+ "${boltBaseDir}\\sys\\ruby\\bin\\"
136
+ $ENV:RUBYLIB = "${boltBaseDir}\\puppet\\lib;" +
137
+ "${boltBaseDir}\\facter\\lib;" +
138
+ "${boltBaseDir}\\hiera\\lib;" +
139
+ $ENV:RUBYLIB
140
+
141
+ function ConvertFrom-PSCustomObject
142
+ {
143
+ PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
144
+ PROCESS {
145
+ if ($null -eq $InputObject) { return $null }
146
+ if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
147
+ $collection = @(
148
+ foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
149
+ )
150
+
151
+ $collection
152
+ } elseif ($InputObject -is [System.Management.Automation.PSCustomObject]) {
153
+ $hash = @{}
154
+ foreach ($property in $InputObject.PSObject.Properties) {
155
+ $hash[$property.Name] = ConvertFrom-PSCustomObject $property.Value
156
+ }
157
+
158
+ $hash
159
+ } else {
160
+ $InputObject
161
+ }
162
+ }
163
+ }
164
+
165
+ function Get-ContentAsJson
166
+ {
167
+ [CmdletBinding()]
168
+ PARAM(
169
+ [Parameter(Mandatory = $true)] $Text,
170
+ [Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
171
+ )
172
+
173
+ $Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
174
+ }
175
+ PS
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end