boltwash 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57665589df84a1e04ea2dad5a2fecc31adda58d86d3b2b579ba9324e4e58af02
4
- data.tar.gz: 8b42d2eb0e9fdc9ed9d0e0052253550e87cd8041d67ebe562573611f49f1f0a0
3
+ metadata.gz: caeda79144ee72ab2b2bca808db20a8d5bb80998bae6b1fd11dde3a02ea25f65
4
+ data.tar.gz: bb4a8b58c5110e6c8e0f10aa5ba3df7ed0840c8830168639732f43038495c2ee
5
5
  SHA512:
6
- metadata.gz: dea6c8c3db7641f673fe5a6cfca2191b79499721fc6ddff80d4d1dd59c39f5c0b908f9910092984d2a459d08481062f40acaa0fa0f950fb0e18067e390fd3283
7
- data.tar.gz: f75a41d46944efe6b48ec63374703ac847a85675bb6c14f69858cec4569973a9caebed88f6a30113b37f98b2471f8f4f2faba3b8664c5b1f2d192b0829a42e20
6
+ metadata.gz: 57a6f9fdc85bae6e548c8a39be8f1dfb0612d12fe5109bdd57a200abcab62e40b18f8aaa1319d88d51c0ec02a6f75d6894a5d785ccc45b91a7096481ddd9b2a7
7
+ data.tar.gz: 4e89cb536309eaa3de01e80c5730024550a4c62c4cc4a276ba5dc3d1f8716438a160ab9e7dd9c896f374e451452c297e6d5e8316e2a40448adbd63a46bf61080
data/bolt.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bolt/inventory'
5
4
  require 'wash'
6
5
 
7
6
  # For now we're going to mock out the plugin interface. It requires more setup
@@ -38,6 +37,7 @@ class Boltwash < Wash::Entry
38
37
  DESC
39
38
 
40
39
  def init(config)
40
+ require 'bolt/inventory'
41
41
  boltdir = config[:dir] ? Bolt::Boltdir.new(config[:dir]) : Bolt::Boltdir.default_boltdir
42
42
  bolt_config = Bolt::Config.from_boltdir(boltdir)
43
43
  @inventory = Bolt::Inventory.from_config(bolt_config, Plugin.new)
@@ -69,10 +69,25 @@ class Group < Wash::Entry
69
69
  end
70
70
  end
71
71
 
72
+ def get_login_shell(target)
73
+ # Bolt's inventory defines a shell as a feature. Some transports provide
74
+ # default features as well. Use these to determine the login shell.
75
+ if target.features.include?('powershell')
76
+ 'powershell'
77
+ elsif target.features.include?('bash')
78
+ 'posixshell'
79
+ elsif target.transport == 'winrm'
80
+ 'powershell'
81
+ elsif target.transport == 'ssh' || target.transport.nil?
82
+ 'posixshell'
83
+ end
84
+ end
72
85
 
73
86
  class Target < Wash::Entry
74
87
  label 'target'
75
88
  parent_of VOLUMEFS
89
+ state :target
90
+ attributes :os
76
91
  description <<~DESC
77
92
  This is a target. You can view target configuration with the 'meta' command,
78
93
  and SSH to the target if it accepts SSH connections. If SSH works, the 'fs'
@@ -117,36 +132,59 @@ class Target < Wash::Entry
117
132
  }
118
133
  end
119
134
 
120
- def known_hosts(host_key_check)
121
- return nil unless host_key_check == false
122
-
123
- # Disable host key checking by redirecting known hosts to an empty file
124
- # This is future-proofing for when Wash works on Windows.
125
- Gem.win_platform? ? 'NUL' : '/dev/null'
126
- end
127
-
128
- def transport_options(target)
129
- {
130
- host: target.host,
131
- port: target.port,
132
- user: target.user,
133
- password: target.password,
134
- identity_file: target.options['private-key'],
135
- known_hosts: known_hosts(target.options['host-key-check'])
136
- }
137
- end
138
-
139
135
  def initialize(target)
140
136
  # Save just the target information we need as state.
141
137
  @name = target.name
142
138
  @partial_metadata = target.detail
143
- # TODO: add WinRM
144
- transport :ssh, transport_options(target) if target.protocol == 'ssh'
139
+ @target = target.to_h
140
+ if (shell = get_login_shell(target))
141
+ @os = { login_shell: shell }
142
+ end
145
143
  prefetch :list
146
144
  end
147
145
 
148
- def exec(*_args)
149
- raise 'non-ssh protocols are not yet implemented'
146
+ # Only implements SSH, WinRM, and Docker. Local is trivial, and remote is not
147
+ # really usable. PCP I hope to implement later.
148
+ def exec(cmd, args, opts)
149
+ # lazy-load dependencies to make the plugin as fast as possible
150
+ require 'bolt/target'
151
+ require 'logging'
152
+
153
+ # opts can contain 'tty', 'stdin', and 'elevate'. If tty is set, apply it
154
+ # to the target for this exec.
155
+ target_opts = @target.transform_keys(&:to_s)
156
+ target_opts['tty'] = true if opts[:tty]
157
+ target = Bolt::Target.new(@target[:uri], target_opts)
158
+
159
+ logger = Logging.logger($stderr)
160
+ logger.level = :warn
161
+
162
+ transport = target.transport || 'ssh'
163
+ case transport
164
+ when 'ssh'
165
+ require_relative 'transport_ssh.rb'
166
+ connection = BoltSSH.new(target, logger)
167
+ when 'winrm'
168
+ require_relative 'transport_winrm.rb'
169
+ connection = BoltWinRM.new(target, logger)
170
+ when 'docker'
171
+ require_relative 'transport_docker.rb'
172
+ connection = BoltDocker.new(target)
173
+ else
174
+ raise "#{transport} unsupported"
175
+ end
176
+
177
+ begin
178
+ connection.connect
179
+ # Returns exit code
180
+ connection.execute(cmd, args, stdin: opts[:stdin])
181
+ ensure
182
+ begin
183
+ connection&.disconnect
184
+ rescue StandardError => e
185
+ logger.info("Failed to close connection to #{target}: #{e}")
186
+ end
187
+ end
150
188
  end
151
189
 
152
190
  def list
@@ -0,0 +1,39 @@
1
+ require 'bolt/transport/base'
2
+ require 'bolt/transport/docker/connection'
3
+ require 'open3'
4
+ require 'shellwords'
5
+
6
+ class BoltDocker < Bolt::Transport::Docker::Connection
7
+ attr_reader :target
8
+
9
+ # Adapted from Bolt::Transport::Docker::Connection.execute.
10
+ def execute(cmd, args, stdin: nil)
11
+ command_options = []
12
+ # Need to be interactive if redirecting STDIN
13
+ command_options << '--interactive' unless stdin.nil?
14
+ command_options << '--tty' if target.options['tty']
15
+ command_options << container_id
16
+ command = Shellwords.join([cmd] + args)
17
+ if target.options['shell-command'] && !target.options['shell-command'].empty?
18
+ # escape any double quotes in command
19
+ command = command.gsub('"', '\"')
20
+ command = "#{target.options['shell-command']} \" #{command}\""
21
+ end
22
+ command_options.concat(Shellwords.split(command))
23
+
24
+ env_hash = {}
25
+ # Set the DOCKER_HOST if we are using a non-default service-url
26
+ env_hash['DOCKER_HOST'] = @docker_host unless @docker_host.nil?
27
+
28
+ in_r, in_w = IO.pipe
29
+ in_w.sync = true
30
+ in_w.binmode
31
+ pid = Process.spawn(env_hash, 'docker', 'exec', *command_options, in: in_r)
32
+ in_w.write(stdin) if stdin
33
+ in_w.close
34
+ _, status = Process.wait2(pid)
35
+
36
+ # The actual result is the exitstatus not the process object
37
+ status.nil? ? -32768 : status.exitstatus
38
+ end
39
+ end
@@ -0,0 +1,66 @@
1
+ require 'bolt/transport/base'
2
+ require 'bolt/transport/sudoable'
3
+ require 'bolt/transport/ssh/connection'
4
+ require 'shellwords'
5
+
6
+ class BoltSSH < Bolt::Transport::SSH::Connection
7
+ # Adapted from Bolt::Transport::SSH::Connection.execute with output copied to
8
+ # $stdout/$stderr. Returns an exit code instead of Bolt::Result. It also
9
+ # handles stdin differently with sudo; it waits for some response, then
10
+ # sends stdin.
11
+ def execute(cmd, args, stdin: nil)
12
+ command = Shellwords.join([cmd] + args)
13
+ raise 'stdin not supported while using tty' if stdin && target.options['tty']
14
+
15
+ # If not nil, stdin==STDIN. Read all input so we have a string to pass around.
16
+ stdin = stdin.read if stdin
17
+
18
+ escalate = run_as && @user != run_as
19
+ use_sudo = escalate && target.options['run-as-command'].nil?
20
+
21
+ if escalate
22
+ if use_sudo
23
+ sudo_exec = target.options['sudo-executable'] || "sudo"
24
+ sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", Bolt::Transport::Sudoable.sudo_prompt]
25
+ sudo_str = Shellwords.shelljoin(sudo_flags)
26
+ else
27
+ sudo_str = Shellwords.shelljoin(target.options['run-as-command'] + [run_as])
28
+ end
29
+ command = build_sudoable_command_str(command, sudo_str, @sudo_id, stdin: stdin, reset_cwd: true)
30
+ end
31
+
32
+ exit_code = 0
33
+ session_channel = @session.open_channel do |channel|
34
+ # Request a pseudo tty
35
+ channel.request_pty if target.options['tty']
36
+
37
+ channel.exec(command) do |_, success|
38
+ unless success
39
+ raise Bolt::Node::ConnectError.new(
40
+ "Could not execute command: #{command}",
41
+ 'EXEC_ERROR'
42
+ )
43
+ end
44
+
45
+ channel.on_data do |_, data|
46
+ $stdout << data unless use_sudo && handled_sudo(channel, data, stdin)
47
+ end
48
+
49
+ channel.on_extended_data do |_, _, data|
50
+ $stderr << data unless use_sudo && handled_sudo(channel, data, stdin)
51
+ end
52
+
53
+ channel.on_request('exit-status') do |_, data|
54
+ exit_code = data.read_long
55
+ end
56
+
57
+ if stdin && !use_sudo
58
+ channel.send_data(stdin)
59
+ channel.eof!
60
+ end
61
+ end
62
+ end
63
+ session_channel.wait
64
+ exit_code
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ require 'bolt/transport/base'
2
+ require 'bolt/transport/winrm/connection'
3
+ require 'winrm'
4
+
5
+ class BoltWinRM < Bolt::Transport::WinRM::Connection
6
+ # Override execute so it streams output and returns the exit code
7
+ def execute(cmd, args, stdin: nil)
8
+ # The WinRM gem doesn't provide a way to pass stdin. It would require
9
+ # sending a whole script to make it work and we don't have a lot of cases
10
+ # where it's needed yet.
11
+ raise 'input on stdin not supported' if stdin
12
+
13
+ # The powershell implementation ignores 'args', so just string join (which
14
+ # is how powershell joins arg arrays). If you use characters that need to be
15
+ # escaped, pass the argument as a single string with appropriate escaping.
16
+ command = ([cmd] + args).join(' ')
17
+ output = @session.run(command) do |stdout, stderr|
18
+ $stdout << stdout
19
+ $stderr << stderr
20
+ end
21
+ output.exitcode
22
+ end
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boltwash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-29 00:00:00.000000000 Z
11
+ date: 2020-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bolt
@@ -46,6 +46,9 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - bolt.rb
49
+ - transport_docker.rb
50
+ - transport_ssh.rb
51
+ - transport_winrm.rb
49
52
  homepage: https://github.com/puppetlabs/boltwash
50
53
  licenses:
51
54
  - Apache-2.0