bolt 2.16.0 → 2.21.0

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

Potentially problematic release.


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

Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +6 -4
  8. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  9. data/lib/bolt/applicator.rb +19 -14
  10. data/lib/bolt/apply_result.rb +1 -1
  11. data/lib/bolt/bolt_option_parser.rb +60 -16
  12. data/lib/bolt/catalog.rb +3 -2
  13. data/lib/bolt/cli.rb +121 -43
  14. data/lib/bolt/config.rb +37 -34
  15. data/lib/bolt/config/options.rb +340 -173
  16. data/lib/bolt/config/transport/options.rb +315 -160
  17. data/lib/bolt/config/transport/ssh.rb +24 -10
  18. data/lib/bolt/executor.rb +21 -0
  19. data/lib/bolt/inventory/group.rb +3 -2
  20. data/lib/bolt/inventory/inventory.rb +4 -3
  21. data/lib/bolt/logger.rb +24 -1
  22. data/lib/bolt/outputter.rb +1 -1
  23. data/lib/bolt/outputter/rainbow.rb +14 -3
  24. data/lib/bolt/pal.rb +28 -10
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  26. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  27. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  28. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  29. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  30. data/lib/bolt/plugin/module.rb +2 -4
  31. data/lib/bolt/plugin/puppetdb.rb +3 -2
  32. data/lib/bolt/project.rb +20 -6
  33. data/lib/bolt/puppetdb/client.rb +2 -0
  34. data/lib/bolt/puppetdb/config.rb +16 -0
  35. data/lib/bolt/result.rb +7 -0
  36. data/lib/bolt/shell/bash.rb +45 -37
  37. data/lib/bolt/shell/powershell.rb +21 -11
  38. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  39. data/lib/bolt/transport/base.rb +24 -0
  40. data/lib/bolt/transport/docker.rb +16 -4
  41. data/lib/bolt/transport/docker/connection.rb +20 -2
  42. data/lib/bolt/transport/local/connection.rb +14 -1
  43. data/lib/bolt/transport/orch.rb +20 -0
  44. data/lib/bolt/transport/simple.rb +6 -0
  45. data/lib/bolt/transport/ssh.rb +7 -1
  46. data/lib/bolt/transport/ssh/connection.rb +9 -1
  47. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  48. data/lib/bolt/transport/winrm/connection.rb +118 -8
  49. data/lib/bolt/util.rb +26 -11
  50. data/lib/bolt/version.rb +1 -1
  51. data/lib/bolt_server/transport_app.rb +3 -2
  52. data/lib/bolt_spec/bolt_context.rb +7 -2
  53. data/lib/bolt_spec/plans.rb +15 -2
  54. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  55. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  56. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  57. data/lib/bolt_spec/run.rb +22 -0
  58. data/libexec/bolt_catalog +3 -2
  59. data/modules/secure_env_vars/plans/init.pp +20 -0
  60. metadata +21 -29
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Message < Step
8
+ def self.allowed_keys
9
+ super + Set['message']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['message']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @message = step_body['message']
19
+ end
20
+
21
+ def transpile
22
+ code = String.new(" ")
23
+ code << function_call('out::message', [@message])
24
+ code << "\n"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -6,16 +6,16 @@ module Bolt
6
6
  class Step
7
7
  class Upload < Step
8
8
  def self.allowed_keys
9
- super + Set['source', 'destination']
9
+ super + Set['source', 'destination', 'upload']
10
10
  end
11
11
 
12
12
  def self.required_keys
13
- Set['destination', 'source', 'targets']
13
+ Set['upload', 'destination', 'targets']
14
14
  end
15
15
 
16
16
  def initialize(step_body)
17
17
  super
18
- @source = step_body['source']
18
+ @source = step_body['upload'] || step_body['source']
19
19
  @destination = step_body['destination']
20
20
  end
21
21
 
@@ -184,12 +184,10 @@ module Bolt
184
184
  # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
185
  # modifies the keys so they are the correct format
186
186
  def handle_deprecated_pkcs7_keys(params)
187
- if (params.key?('private-key') || params.key?('public-key')) && !@deprecation_warning_issued
188
- @deprecation_warning_issued = true
189
-
187
+ if params.key?('private-key') || params.key?('public-key')
190
188
  message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
191
189
  "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
192
- Logging.logger[self].warn(message)
190
+ Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
193
191
  end
194
192
 
195
193
  params['private_key'] = params.delete('private-key') if params.key?('private-key')
@@ -85,7 +85,8 @@ module Bolt
85
85
 
86
86
  def resolve_facts(config, certname, target_data)
87
87
  Bolt::Util.walk_vals(config) do |value|
88
- if value.is_a?(String)
88
+ case value
89
+ when String
89
90
  if value == 'certname'
90
91
  certname
91
92
  else
@@ -94,7 +95,7 @@ module Bolt
94
95
  # If there's no fact data this will be nil
95
96
  data&.fetch('value', nil)
96
97
  end
97
- elsif value.is_a?(Array) || value.is_a?(Hash)
98
+ when Array, Hash
98
99
  value
99
100
  else
100
101
  raise FactLookupError.new(value, "fact lookups must be a string")
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
- require 'bolt/pal'
5
4
  require 'bolt/config'
5
+ require 'bolt/pal'
6
6
 
7
7
  module Bolt
8
8
  class Project
@@ -16,7 +16,8 @@ module Bolt
16
16
  }.freeze
17
17
 
18
18
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
19
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
+ :deprecations, :downloads
20
21
 
21
22
  def self.default_project
22
23
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -31,6 +32,7 @@ module Bolt
31
32
  # hierarchy, falling back to the default if we reach the root.
32
33
  def self.find_boltdir(dir)
33
34
  dir = Pathname.new(dir)
35
+
34
36
  if (dir + BOLTDIR_NAME).directory?
35
37
  create_project(dir + BOLTDIR_NAME, 'embedded')
36
38
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
@@ -44,6 +46,15 @@ module Bolt
44
46
 
45
47
  def self.create_project(path, type = 'option')
46
48
  fullpath = Pathname.new(path).expand_path
49
+
50
+ if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
51
+ raise Bolt::Error.new(
52
+ "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
53
+ "BOLT_PROJECT='#{fullpath}' to force the use of this project directory.",
54
+ "bolt/world-writable-error"
55
+ )
56
+ end
57
+
47
58
  project_file = File.join(fullpath, 'bolt-project.yaml')
48
59
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
49
60
  new(data, path, type)
@@ -51,14 +62,16 @@ module Bolt
51
62
 
52
63
  def initialize(raw_data, path, type = 'option')
53
64
  @path = Pathname.new(path).expand_path
65
+
54
66
  @project_file = @path + 'bolt-project.yaml'
55
67
 
56
68
  @warnings = []
69
+ @deprecations = []
57
70
  if (@path + 'bolt.yaml').file? && project_file?
58
71
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
59
72
  "Transport config should be set in inventory.yaml, all other config should be set in "\
60
73
  "bolt-project.yaml."
61
- @warnings << { msg: msg }
74
+ @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
62
75
  end
63
76
 
64
77
  @inventory_file = @path + 'inventory.yaml'
@@ -68,6 +81,7 @@ module Bolt
68
81
  @rerunfile = @path + '.rerun.json'
69
82
  @resource_types = @path + '.resource_types'
70
83
  @type = type
84
+ @downloads = @path + 'downloads'
71
85
 
72
86
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
73
87
  if tc.any?
@@ -98,7 +112,7 @@ module Bolt
98
112
  # This API is used to prepend the project as a module to Puppet's internal
99
113
  # module_references list. CHANGE AT YOUR OWN RISK
100
114
  def to_h
101
- { path: @path, name: name }
115
+ { path: @path.to_s, name: name }
102
116
  end
103
117
 
104
118
  def eql?(other)
@@ -147,8 +161,8 @@ module Bolt
147
161
 
148
162
  def check_deprecated_file
149
163
  if (@path + 'project.yaml').file?
150
- logger = Logging.logger[self]
151
- logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
164
+ msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
165
+ Bolt::Logger.deprecation_warning('Using project.yaml instead of bolt-project.yaml', msg)
152
166
  end
153
167
  end
154
168
  end
@@ -96,6 +96,8 @@ module Bolt
96
96
  @http = HTTPClient.new
97
97
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
98
98
  @http.ssl_config.add_trust_ca(@config.cacert)
99
+ @http.connect_timeout = @config.connect_timeout if @config.connect_timeout
100
+ @http.receive_timeout = @config.read_timeout if @config.read_timeout
99
101
 
100
102
  @http
101
103
  end
@@ -132,6 +132,22 @@ module Bolt
132
132
  end
133
133
  end
134
134
 
135
+ def connect_timeout
136
+ validate_timeout('connect_timeout')
137
+ @settings['connect_timeout']
138
+ end
139
+
140
+ def read_timeout
141
+ validate_timeout('read_timeout')
142
+ @settings['read_timeout']
143
+ end
144
+
145
+ def validate_timeout(timeout)
146
+ unless @settings[timeout].nil? || (@settings[timeout].is_a?(Integer) && @settings[timeout] > 0)
147
+ raise Bolt::PuppetDBError, "#{timeout} must be a positive integer, received #{@settings[timeout]}"
148
+ end
149
+ end
150
+
135
151
  def to_hash
136
152
  @settings.dup
137
153
  end
@@ -79,6 +79,13 @@ module Bolt
79
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
80
80
  end
81
81
 
82
+ def self.for_download(target, source, destination, download)
83
+ msg = "Downloaded '#{target.host}:#{source}' to '#{destination}'"
84
+ value = { 'path' => download }
85
+
86
+ new(target, value: value, message: msg, action: 'download', object: source)
87
+ end
88
+
82
89
  # Satisfies the Puppet datatypes API
83
90
  def self.from_asserted_args(target, value)
84
91
  new(target, value: value)
@@ -23,7 +23,7 @@ module Bolt
23
23
 
24
24
  def run_command(command, options = {})
25
25
  running_as(options[:run_as]) do
26
- output = execute(command, sudoable: true)
26
+ output = execute(command, environment: options[:env_vars], sudoable: true)
27
27
  Bolt::Result.for_command(target,
28
28
  output.stdout.string,
29
29
  output.stderr.string,
@@ -37,7 +37,7 @@ module Bolt
37
37
  with_tmpdir do |dir|
38
38
  basename = File.basename(source)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
- conn.copy_file(source, tmpfile)
40
+ conn.upload_file(source, tmpfile)
41
41
  # pass over file ownership if we're using run-as to be a different user
42
42
  dir.chown(run_as)
43
43
  result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
@@ -50,6 +50,27 @@ module Bolt
50
50
  end
51
51
  end
52
52
 
53
+ def download(source, destination, options = {})
54
+ running_as(options[:run_as]) do
55
+ # Target OS may be either Unix or Windows. Without knowing the target OS before-hand
56
+ # we can't assume whether the path separator is '/' or '\'. Assume we're connecting
57
+ # to a target with Unix and then check if the path exists after downloading.
58
+ download = File.join(destination, Bolt::Util.unix_basename(source))
59
+
60
+ conn.download_file(source, destination, download)
61
+
62
+ # If the download path doesn't exist, then the file was likely downloaded from Windows
63
+ # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
64
+ # should be saved to the expected location, so update the download path assuming a
65
+ # Windows basename so the result shows the correct local path.
66
+ unless File.exist?(download)
67
+ download = File.join(destination, Bolt::Util.windows_basename(source))
68
+ end
69
+
70
+ Bolt::Result.for_download(target, source, destination, download)
71
+ end
72
+ end
73
+
53
74
  def run_script(script, arguments, options = {})
54
75
  # unpack any Sensitive data
55
76
  arguments = unwrap_sensitive_args(arguments)
@@ -58,7 +79,7 @@ module Bolt
58
79
  with_tmpdir do |dir|
59
80
  path = write_executable(dir.to_s, script)
60
81
  dir.chown(run_as)
61
- output = execute([path, *arguments], sudoable: true)
82
+ output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
62
83
  Bolt::Result.for_command(target,
63
84
  output.stdout.string,
64
85
  output.stderr.string,
@@ -95,7 +116,7 @@ module Bolt
95
116
  task_dir = File.join(dir.to_s, task.tasks_dir)
96
117
  dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
97
118
  extra_files.each do |file|
98
- conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
119
+ conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
99
120
  end
100
121
  end
101
122
 
@@ -170,7 +191,7 @@ module Bolt
170
191
  def check_sudo(out, inp, stdin)
171
192
  buffer = out.readpartial(CHUNK_SIZE)
172
193
  # Split on newlines, including the newline
173
- lines = buffer.split(/(?<=[\n])/)
194
+ lines = buffer.split(/(?<=\n)/)
174
195
  # handle_sudo will return the line if it is not a sudo prompt or error
175
196
  lines.map! { |line| handle_sudo(inp, line, stdin) }
176
197
  lines.join("")
@@ -257,7 +278,7 @@ module Bolt
257
278
  def write_executable(dir, file, filename = nil)
258
279
  filename ||= File.basename(file)
259
280
  remote_path = File.join(dir.to_s, filename)
260
- conn.copy_file(file, remote_path)
281
+ conn.upload_file(file, remote_path)
261
282
  make_executable(remote_path)
262
283
  remote_path
263
284
  end
@@ -277,31 +298,8 @@ module Bolt
277
298
  end
278
299
  end
279
300
 
280
- # In the case where a task is run with elevated privilege and needs stdin
281
- # a random string is echoed to stderr indicating that the stdin is available
282
- # for task input data because the sudo password has already either been
283
- # provided on stdin or was not needed.
284
- def prepend_sudo_success(sudo_id, command_str)
285
- command_str = "cd; #{command_str}" if conn.reset_cwd?
286
- "sh -c #{Shellwords.shellescape("echo #{sudo_id} 1>&2; #{command_str}")}"
287
- end
288
-
289
- def prepend_chdir(command_str)
290
- "sh -c #{Shellwords.shellescape("cd; #{command_str}")}"
291
- end
292
-
293
- # A helper to build up a single string that contains all of the options for
294
- # privilege escalation. A wrapper script is used to direct task input to stdin
295
- # when a tty is allocated and thus we do not need to prepend_sudo_success when
296
- # using the wrapper or when the task does not require stdin data.
297
- def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
298
- if options[:stdin] && !options[:wrapper]
299
- "#{sudo_str} #{prepend_sudo_success(sudo_id, command_str)}"
300
- elsif conn.reset_cwd?
301
- "#{sudo_str} #{prepend_chdir(command_str)}"
302
- else
303
- "#{sudo_str} #{command_str}"
304
- end
301
+ def sudo_success(sudo_id)
302
+ "echo #{sudo_id} 1>&2"
305
303
  end
306
304
 
307
305
  # Returns string with the interpreter conditionally prepended
@@ -322,27 +320,37 @@ module Bolt
322
320
  escalate = sudoable && run_as && conn.user != run_as
323
321
  use_sudo = escalate && @target.options['run-as-command'].nil?
324
322
 
325
- command_str = inject_interpreter(options[:interpreter], command)
323
+ # Depending on the transport, whether we're using sudo and whether
324
+ # there are environment variables to set, we may need to stitch
325
+ # together multiple commands into a single sh invocation
326
+ commands = [inject_interpreter(options[:interpreter], command)]
326
327
 
327
328
  if options[:environment]
328
- env_decls = options[:environment].map do |env, val|
329
+ env_decl = options[:environment].map do |env, val|
329
330
  "#{env}=#{Shellwords.shellescape(val)}"
330
- end
331
- command_str = "#{env_decls.join(' ')} #{command_str}"
331
+ end.join(' ')
332
332
  end
333
333
 
334
334
  if escalate
335
335
  sudo_str = if use_sudo
336
336
  sudo_exec = target.options['sudo-executable'] || "sudo"
337
337
  sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
338
- sudo_flags += ["-E"] if options[:environment]
339
338
  Shellwords.shelljoin(sudo_flags)
340
339
  else
341
340
  Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
342
341
  end
343
- command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
342
+ commands.unshift('cd') if conn.reset_cwd?
343
+ commands.unshift(sudo_success(@sudo_id)) if options[:stdin] && !options[:wrapper]
344
344
  end
345
345
 
346
+ command_str = if sudo_str || env_decl
347
+ "sh -c #{Shellwords.shellescape(commands.join('; '))}"
348
+ else
349
+ commands.last
350
+ end
351
+
352
+ command_str = [sudo_str, env_decl, command_str].compact.join(' ')
353
+
346
354
  @logger.debug { "Executing: #{command_str}" }
347
355
 
348
356
  in_buffer = if !use_sudo && options[:stdin]
@@ -71,8 +71,10 @@ module Bolt
71
71
  end
72
72
  end
73
73
 
74
- def set_env(arg, val)
75
- "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
74
+ def env_declarations(env_vars)
75
+ env_vars.map do |var, val|
76
+ "[Environment]::SetEnvironmentVariable('#{var}', @'\n#{val}\n'@)"
77
+ end
76
78
  end
77
79
 
78
80
  def quote_string(string)
@@ -83,7 +85,7 @@ module Bolt
83
85
  filename ||= File.basename(file)
84
86
  validate_extensions(File.extname(filename))
85
87
  destination = "#{dir}\\#{filename}"
86
- conn.copy_file(file, destination)
88
+ conn.upload_file(file, destination)
87
89
  destination
88
90
  end
89
91
 
@@ -162,11 +164,19 @@ module Bolt
162
164
  end
163
165
 
164
166
  def upload(source, destination, _options = {})
165
- conn.copy_file(source, destination)
167
+ conn.upload_file(source, destination)
166
168
  Bolt::Result.for_upload(target, source, destination)
167
169
  end
168
170
 
169
- def run_command(command, _options = {})
171
+ def download(source, destination, _options = {})
172
+ download = File.join(destination, Bolt::Util.windows_basename(source))
173
+ conn.download_file(source, destination, download)
174
+ Bolt::Result.for_download(target, source, destination, download)
175
+ end
176
+
177
+ def run_command(command, options = {})
178
+ command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
179
+
170
180
  output = execute(command)
171
181
  Bolt::Result.for_command(target,
172
182
  output.stdout.string,
@@ -175,7 +185,7 @@ module Bolt
175
185
  'command', command)
176
186
  end
177
187
 
178
- def run_script(script, arguments, _options = {})
188
+ def run_script(script, arguments, options = {})
179
189
  # unpack any Sensitive data
180
190
  arguments = unwrap_sensitive_args(arguments)
181
191
  with_tmpdir do |dir|
@@ -187,6 +197,8 @@ module Bolt
187
197
  args += escape_arguments(arguments)
188
198
  execute_process(path, args)
189
199
  end
200
+ command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
201
+
190
202
  output = execute(command)
191
203
  Bolt::Result.for_command(target,
192
204
  output.stdout.string,
@@ -214,7 +226,7 @@ module Bolt
214
226
  task_dir = File.join(dir, task.tasks_dir)
215
227
  mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
216
228
  extra_files.each do |file|
217
- conn.copy_file(file['path'], File.join(dir, file['name']))
229
+ conn.upload_file(file['path'], File.join(dir, file['name']))
218
230
  end
219
231
  end
220
232
 
@@ -237,9 +249,7 @@ module Bolt
237
249
  end
238
250
 
239
251
  env_assignments = if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
240
- envify_params(arguments).map do |(arg, val)|
241
- set_env(arg, val)
242
- end
252
+ env_declarations(envify_params(arguments))
243
253
  else
244
254
  []
245
255
  end
@@ -258,7 +268,7 @@ module Bolt
258
268
  return with_tmpdir do |dir|
259
269
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
260
270
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
261
- conn.copy_file(StringIO.new(command), script_file)
271
+ conn.upload_file(StringIO.new(command), script_file)
262
272
  args = escape_arguments([script_file])
263
273
  script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
264
274
  execute(script_invocation)