boltwash 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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