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 +4 -4
- data/bolt.rb +62 -24
- data/transport_docker.rb +39 -0
- data/transport_ssh.rb +66 -0
- data/transport_winrm.rb +23 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: caeda79144ee72ab2b2bca808db20a8d5bb80998bae6b1fd11dde3a02ea25f65
|
4
|
+
data.tar.gz: bb4a8b58c5110e6c8e0f10aa5ba3df7ed0840c8830168639732f43038495c2ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
144
|
-
|
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
|
-
|
149
|
-
|
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
|
data/transport_docker.rb
ADDED
@@ -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
|
data/transport_ssh.rb
ADDED
@@ -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
|
data/transport_winrm.rb
ADDED
@@ -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.
|
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-
|
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
|