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 +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
|