bolt 1.0.0 → 1.1.0

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

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e8d1a8d37a48ddf246cd058bbe4b071732c000c90592da48aac5ed7285143cf
4
- data.tar.gz: 9c1aeade908ced86c6d6b6f3e459949c438e0831e4dfc2401daaca6124663077
3
+ metadata.gz: 5955d8922691cc5c2875f1614d69181289c0ba9561e6016fb8c18bcecd09391a
4
+ data.tar.gz: 0077fbd497fbbabf23c72a7916517b660a9c21553e8a6bc04ffaddc8cc283e53
5
5
  SHA512:
6
- metadata.gz: e060cf1af91bfe4f3f6ea97d83e38fd4ed5a65a2fdc1e3d1af28337bb59835fa2fba62cc34d98d7000c37426f5ea76b83e7bd2d540f48c09a40aef8f37cdf202
7
- data.tar.gz: d01e9efae28d08b0dffaab1bd6ca6b2679c01b7f390473a09557f61118a957d909f509e0aa480999bc0b8df3dced6b00bea2806ad6a89fb9f1d8bd5ccf0c4e5c
6
+ metadata.gz: a92a53a9e95ff635ff3adf36e594d6ba38d130cbfd117ecfe7111404cba7f453c63e483269120ecf51d1cd1b58f96cd8bfa6087d88e4ed3c1692c7dfe804ece9
7
+ data.tar.gz: f2df408dcd301c87b0d598b0f5848576b832a815f2bf13aa5784e85a2384a098b3a34a0b12e3b7b1d80292e334913ab553848826a52e712cb563fc3814abc24d
data/lib/bolt/catalog.rb CHANGED
@@ -12,6 +12,10 @@ require 'bolt/catalog/logging'
12
12
 
13
13
  module Bolt
14
14
  class Catalog
15
+ def initialize(log_level = 'debug')
16
+ @log_level = log_level
17
+ end
18
+
15
19
  def with_puppet_settings(hiera_config = {})
16
20
  Dir.mktmpdir('bolt') do |dir|
17
21
  cli = []
@@ -24,7 +28,7 @@ module Bolt
24
28
 
25
29
  # Use a special logdest that serializes all log messages and their level to stderr.
26
30
  Puppet::Util::Log.newdestination(:stderr)
27
- Puppet.settings[:log_level] = 'debug'
31
+ Puppet.settings[:log_level] = @log_level
28
32
  yield
29
33
  end
30
34
  end
data/lib/bolt/cli.rb CHANGED
@@ -298,7 +298,7 @@ module Bolt
298
298
  if dest.nil?
299
299
  raise Bolt::CLIError, "A destination path must be specified"
300
300
  end
301
- validate_file('source file', src)
301
+ validate_file('source file', src, true)
302
302
  executor.upload_file(targets, src, dest, executor_opts) do |event|
303
303
  outputter.print_event(event)
304
304
  end
@@ -392,7 +392,7 @@ module Bolt
392
392
  @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.compile_concurrency)
393
393
  end
394
394
 
395
- def validate_file(type, path)
395
+ def validate_file(type, path, allow_dir = false)
396
396
  if path.nil?
397
397
  raise Bolt::CLIError, "A #{type} must be specified"
398
398
  end
@@ -401,8 +401,14 @@ module Bolt
401
401
 
402
402
  if !stat.readable?
403
403
  raise Bolt::FileError.new("The #{type} '#{path}' is unreadable", path)
404
- elsif !stat.file?
405
- raise Bolt::FileError.new("The #{type} '#{path}' is not a file", path)
404
+ elsif !stat.file? && (!allow_dir || !stat.directory?)
405
+ expected = allow_dir ? 'file or directory' : 'file'
406
+ raise Bolt::FileError.new("The #{type} '#{path}' is not a #{expected}", path)
407
+ elsif stat.directory?
408
+ Dir.foreach(path) do |file|
409
+ next if %w[. ..].include?(file)
410
+ validate_file(type, File.join(path, file), allow_dir)
411
+ end
406
412
  end
407
413
  rescue Errno::ENOENT
408
414
  raise Bolt::FileError.new("The #{type} '#{path}' does not exist", path)
data/lib/bolt/executor.rb CHANGED
@@ -79,9 +79,14 @@ module Bolt
79
79
  Array(results).each do |result|
80
80
  result_promises[result.target].set(result)
81
81
  end
82
- # NotImplementedError can be thrown if the transport is implemented improperly
82
+ # NotImplementedError can be thrown if the transport is not implemented improperly
83
83
  rescue StandardError, NotImplementedError => e
84
84
  result_promises.each do |target, promise|
85
+ # If an exception happens while running, the result won't be logged
86
+ # by the CLI. Log a warning, as this is probably a problem with the transport.
87
+ # If batch_* commands are used from the Base transport, then exceptions
88
+ # normally shouldn't reach here.
89
+ @logger.warn(e)
85
90
  promise.set(Bolt::Result.from_exception(target, e))
86
91
  end
87
92
  ensure
data/lib/bolt/pal.rb CHANGED
@@ -179,9 +179,11 @@ module Bolt
179
179
  def list_tasks
180
180
  in_bolt_compiler do |compiler|
181
181
  tasks = compiler.list_tasks
182
- tasks.map(&:name).sort.map do |task_name|
182
+ tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
183
183
  task_sig = compiler.task_signature(task_name)
184
- [task_name, task_sig.task_hash['metadata']['description']]
184
+ unless task_sig.task_hash['metadata']['private']
185
+ data << [task_name, task_sig.task_hash['metadata']['description']]
186
+ end
185
187
  end
186
188
  end
187
189
  end
data/lib/bolt/task.rb CHANGED
@@ -58,7 +58,20 @@ module Bolt
58
58
  impl['input_method'] = inmethod unless inmethod.nil?
59
59
 
60
60
  mfiles = impl.fetch('files', []) + metadata.fetch('files', [])
61
- impl['files'] = mfiles.map { |file| { 'name' => file, 'path' => file_map[file] } } unless mfiles.empty?
61
+ dirnames, filenames = mfiles.partition { |file| file.end_with?('/') }
62
+ impl['files'] = filenames.map do |file|
63
+ path = file_map[file]
64
+ raise "No file found for reference #{file}" if path.nil?
65
+ { 'name' => file, 'path' => path }
66
+ end
67
+
68
+ unless dirnames.empty?
69
+ files.each do |file|
70
+ if dirnames.any? { |dirname| file['name'].start_with?(dirname) }
71
+ impl['files'] << file
72
+ end
73
+ end
74
+ end
62
75
 
63
76
  impl
64
77
  end
@@ -60,7 +60,7 @@ module Bolt
60
60
 
61
61
  result = begin
62
62
  yield
63
- rescue StandardError => ex
63
+ rescue StandardError, NotImplementedError => ex
64
64
  Bolt::Result.from_exception(target, ex)
65
65
  end
66
66
 
@@ -36,7 +36,7 @@ module Bolt
36
36
  private :in_tmpdir
37
37
 
38
38
  def copy_file(source, destination)
39
- FileUtils.copy_file(source, destination)
39
+ FileUtils.cp_r(source, destination, remove_destination: true)
40
40
  rescue StandardError => e
41
41
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
42
42
  end
@@ -83,14 +83,25 @@ module Bolt
83
83
  implementation = task.select_implementation(target, PROVIDED_FEATURES)
84
84
  executable = implementation['path']
85
85
  input_method = implementation['input_method'] || 'both'
86
-
87
- # unpack any Sensitive data, write it to a separate variable because
88
- # we log 'arguments' below
89
- unwrapped_arguments = unwrap_sensitive_args(arguments)
90
- stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
91
- env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
86
+ extra_files = implementation['files']
92
87
 
93
88
  with_tmpscript(executable, target.options['tmpdir']) do |script, dir|
89
+ unless extra_files.empty?
90
+ installdir = File.join(dir, '_installdir')
91
+ arguments['_installdir'] = installdir
92
+ FileUtils.mkdir_p(extra_files.map { |file| File.join(installdir, File.dirname(file['name'])) })
93
+
94
+ extra_files.each do |file|
95
+ copy_file(file['path'], File.join(installdir, file['name']))
96
+ end
97
+ end
98
+
99
+ # unpack any Sensitive data, write it to a separate variable because
100
+ # we log 'arguments' below
101
+ unwrapped_arguments = unwrap_sensitive_args(arguments)
102
+ stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
103
+ env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
104
+
94
105
  # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
95
106
  logger.debug("Running '#{script}' with #{arguments}")
96
107
 
@@ -2,8 +2,12 @@
2
2
 
3
3
  require 'base64'
4
4
  require 'concurrent'
5
+ require 'find'
5
6
  require 'json'
7
+ require 'minitar'
6
8
  require 'orchestrator_client'
9
+ require 'pathname'
10
+ require 'zlib'
7
11
  require 'bolt/transport/base'
8
12
  require 'bolt/transport/orch/connection'
9
13
 
@@ -114,14 +118,50 @@ module Bolt
114
118
  end
115
119
  end
116
120
 
121
+ def pack(directory)
122
+ start_time = Time.now
123
+ io = StringIO.new
124
+ output = Minitar::Output.new(Zlib::GzipWriter.new(io))
125
+ Find.find(directory) do |file|
126
+ next unless File.file?(file)
127
+
128
+ tar_path = Pathname.new(file).relative_path_from(Pathname.new(directory))
129
+ @logger.debug("Packing #{file} to #{tar_path}")
130
+ stat = File.stat(file)
131
+ content = File.binread(file)
132
+ output.tar.add_file_simple(
133
+ tar_path.to_s,
134
+ data: content,
135
+ size: content.size,
136
+ mode: stat.mode & 0o777,
137
+ mtime: stat.mtime
138
+ )
139
+ end
140
+
141
+ duration = Time.now - start_time
142
+ @logger.debug("Packed upload in #{duration * 1000} ms")
143
+
144
+ output.close
145
+ io.string
146
+ ensure
147
+ # Closes both tar and sgz.
148
+ output&.close
149
+ end
150
+
117
151
  def batch_upload(targets, source, destination, options = {}, &callback)
118
- content = File.open(source, &:read)
152
+ stat = File.stat(source)
153
+ content = if stat.directory?
154
+ pack(source)
155
+ else
156
+ File.open(source, &:read)
157
+ end
119
158
  content = Base64.encode64(content)
120
159
  mode = File.stat(source).mode
121
160
  params = {
122
161
  'path' => destination,
123
162
  'content' => content,
124
- 'mode' => mode
163
+ 'mode' => mode,
164
+ 'directory' => stat.directory?
125
165
  }
126
166
  callback ||= proc {}
127
167
  results = run_task_job(targets, BOLT_UPLOAD_TASK, params, options, &callback)
@@ -126,10 +126,12 @@ module Bolt
126
126
  executable = task.file['filename']
127
127
  file_content = Base64.decode64(task.file['file_content'])
128
128
  input_method = task.metadata['input_method']
129
+ extra_files = []
129
130
  else
130
131
  implementation = task.select_implementation(target, PROVIDED_FEATURES)
131
132
  executable = implementation['path']
132
133
  input_method = implementation['input_method']
134
+ extra_files = implementation['files']
133
135
  end
134
136
  input_method ||= 'both'
135
137
 
@@ -138,18 +140,9 @@ module Bolt
138
140
  with_connection(target, options.fetch('_load_config', true)) do |conn|
139
141
  conn.running_as(options['_run_as']) do
140
142
  stdin, output = nil
141
-
142
143
  command = []
143
144
  execute_options = {}
144
145
 
145
- if STDIN_METHODS.include?(input_method)
146
- stdin = JSON.dump(arguments)
147
- end
148
-
149
- if ENVIRONMENT_METHODS.include?(input_method)
150
- execute_options[:environment] = envify_params(arguments)
151
- end
152
-
153
146
  conn.with_remote_tempdir do |dir|
154
147
  remote_task_path = if from_api?(task)
155
148
  conn.write_executable_from_content(dir, file_content, executable)
@@ -157,6 +150,24 @@ module Bolt
157
150
  conn.write_remote_executable(dir, executable)
158
151
  end
159
152
 
153
+ unless extra_files.empty?
154
+ # TODO: optimize upload of directories
155
+ installdir = File.join(dir.to_s, '_installdir')
156
+ arguments['_installdir'] = installdir
157
+ dir.mkdirs(extra_files.map { |file| File.join('_installdir', File.dirname(file['name'])) })
158
+ extra_files.each do |file|
159
+ conn.write_remote_file(file['path'], File.join(installdir, file['name']))
160
+ end
161
+ end
162
+
163
+ if STDIN_METHODS.include?(input_method)
164
+ stdin = JSON.dump(arguments)
165
+ end
166
+
167
+ if ENVIRONMENT_METHODS.include?(input_method)
168
+ execute_options[:environment] = envify_params(arguments)
169
+ end
170
+
160
171
  if conn.run_as && stdin
161
172
  wrapper = make_wrapper_stringio(remote_task_path, stdin)
162
173
  remote_wrapper_path = conn.write_remote_executable(dir, wrapper, 'wrapper.sh')
@@ -22,6 +22,15 @@ module Bolt
22
22
  @path
23
23
  end
24
24
 
25
+ def mkdirs(subdirs)
26
+ abs_subdirs = subdirs.map { |subdir| File.join(@path, subdir) }
27
+ result = @node.execute(['mkdir', '-p'] + abs_subdirs)
28
+ if result.exit_code != 0
29
+ message = "Could not create subdirectories in '#{@path}': #{result.stderr.string}"
30
+ raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
31
+ end
32
+ end
33
+
25
34
  def chown(owner)
26
35
  return if owner.nil? || owner == @owner
27
36
 
@@ -92,8 +101,16 @@ module Bolt
92
101
 
93
102
  options[:port] = target.port if target.port
94
103
  options[:password] = target.password if target.password
104
+ # Support both net-ssh 4 and 5. We use 5 in packaging, but Beaker pins to 4 so we
105
+ # want the gem to be compatible with version 4.
95
106
  options[:verify_host_key] = if target.options['host-key-check']
96
- Net::SSH::Verifiers::Secure.new
107
+ if defined?(Net::SSH::Verifiers::Always)
108
+ Net::SSH::Verifiers::Always.new
109
+ else
110
+ Net::SSH::Verifiers::Secure.new
111
+ end
112
+ elsif defined?(Net::SSH::Verifiers::Never)
113
+ Net::SSH::Verifiers::Never.new
97
114
  else
98
115
  Net::SSH::Verifiers::Null.new
99
116
  end
@@ -281,7 +298,7 @@ module Bolt
281
298
  end
282
299
 
283
300
  def write_remote_file(source, destination)
284
- @session.scp.upload!(source, destination)
301
+ @session.scp.upload!(source, destination, recursive: true)
285
302
  rescue StandardError => e
286
303
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
287
304
  end
@@ -112,36 +112,49 @@ catch
112
112
  executable = task.file['filename']
113
113
  file_content = StringIO.new(Base64.decode64(task.file['file_content']))
114
114
  input_method = task.metadata['input_method']
115
+ extra_files = []
115
116
  else
116
117
  implementation = task.select_implementation(target, PROVIDED_FEATURES)
117
118
  executable = implementation['path']
118
119
  input_method = implementation['input_method']
120
+ extra_files = implementation['files']
119
121
  end
120
122
  input_method ||= powershell_file?(executable) ? 'powershell' : 'both'
121
123
 
122
124
  # unpack any Sensitive data
123
125
  arguments = unwrap_sensitive_args(arguments)
124
126
  with_connection(target) do |conn|
125
- if STDIN_METHODS.include?(input_method)
126
- stdin = JSON.dump(arguments)
127
- end
128
-
129
- if ENVIRONMENT_METHODS.include?(input_method)
130
- envify_params(arguments).each do |(arg, val)|
131
- cmd = "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
132
- result = conn.execute(cmd)
133
- if result.exit_code != 0
134
- raise Bolt::Node::EnvironmentVarError.new(arg, val)
135
- end
136
- end
137
- end
138
-
139
127
  conn.with_remote_tempdir do |dir|
140
128
  remote_task_path = if from_api?(task)
141
129
  conn.write_executable_from_content(dir, file_content, executable)
142
130
  else
143
131
  conn.write_remote_executable(dir, executable)
144
132
  end
133
+
134
+ unless extra_files.empty?
135
+ # TODO: optimize upload of directories
136
+ installdir = File.join(dir, '_installdir')
137
+ arguments['_installdir'] = installdir
138
+ conn.mkdirs(extra_files.map { |file| File.join(installdir, File.dirname(file['name'])) })
139
+ extra_files.each do |file|
140
+ conn.write_remote_file(file['path'], File.join(installdir, file['name']))
141
+ end
142
+ end
143
+
144
+ if STDIN_METHODS.include?(input_method)
145
+ stdin = JSON.dump(arguments)
146
+ end
147
+
148
+ if ENVIRONMENT_METHODS.include?(input_method)
149
+ envify_params(arguments).each do |(arg, val)|
150
+ cmd = "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
151
+ result = conn.execute(cmd)
152
+ if result.exit_code != 0
153
+ raise Bolt::Node::EnvironmentVarError.new(arg, val)
154
+ end
155
+ end
156
+ end
157
+
145
158
  conn.shell_init
146
159
  output =
147
160
  if powershell_file?(remote_task_path) && stdin.nil?
@@ -334,6 +334,14 @@ exit $LASTEXITCODE
334
334
  PS
335
335
  end
336
336
 
337
+ def mkdirs(dirs)
338
+ result = execute("mkdir -Force #{dirs.uniq.sort.join(',')}")
339
+ if result.exit_code != 0
340
+ message = "Could not create directories: #{result.stderr}"
341
+ raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
342
+ end
343
+ end
344
+
337
345
  def write_remote_file(source, destination)
338
346
  fs = ::WinRM::FS::FileManager.new(@connection)
339
347
  fs.upload(source, destination)
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -45,6 +45,10 @@
45
45
  "type": "string",
46
46
  "description": "The directory to upload and execute temporary files on the target"
47
47
  },
48
+ "tty": {
49
+ "type": "boolean",
50
+ "description": "Should bolt use pseudo tty to meet sudoer restrictions"
51
+ },
48
52
  "host-key-check": {
49
53
  "type": "boolean",
50
54
  "description": "Whether to perform host key validation when connecting over SSH"
@@ -65,7 +69,7 @@
65
69
  "parameters": {
66
70
  "type": "object",
67
71
  "description": "JSON formatted parameters to be provided to task"
68
- }
72
+ }
69
73
  },
70
74
  "required": ["target", "task"]
71
- }
75
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'sinatra'
4
4
  require 'bolt'
5
+ require 'bolt/target'
5
6
  require 'bolt/task'
6
7
  require 'json'
7
8
  require 'json-schema'
@@ -50,8 +51,10 @@ class TransportAPI < Sinatra::Base
50
51
  schema_error = JSON::Validator.fully_validate(@schemas["ssh-run_task"], body)
51
52
  return [400, schema_error.join] if schema_error.any?
52
53
 
53
- keys = %w[user password port ssh-key-content connect-timeout run-as-command run-as
54
- tmpdir host-key-check known-hosts-content private-key-content sudo-password]
54
+ # CODEREVIEW: the schema is additionalProperties false do we need this?
55
+ keys = %w[user password port connect-timeout run-as-command run-as
56
+ tmpdir host-key-check known-hosts-content private-key-content sudo-password
57
+ tty]
55
58
  opts = body['target'].select { |k, _| keys.include? k }
56
59
 
57
60
  if opts['private-key-content']
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'hocon'
4
+ require 'bolt/error'
4
5
 
5
6
  class TransportConfig
6
7
  attr_accessor :host, :port, :ssl_cert, :ssl_key, :ssl_ca_cert, :ssl_cipher_suites,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-08 00:00:00.000000000 Z
11
+ date: 2018-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: net-ssh
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '4.2'
89
+ version: '4.0'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '4.2'
96
+ version: '4.0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: orchestrator_client
99
99
  requirement: !ruby/object:Gem::Requirement