sshake 1.0.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 +7 -0
- data/lib/sshake/error.rb +18 -0
- data/lib/sshake/execution_options.rb +93 -0
- data/lib/sshake/execution_options_dsl.rb +32 -0
- data/lib/sshake/logger.rb +7 -0
- data/lib/sshake/response.rb +30 -0
- data/lib/sshake/session.rb +178 -0
- data/lib/sshake/version.rb +5 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 100663608c7ad37ad009f8e8390bb8529a310f67
|
4
|
+
data.tar.gz: 5941b458f2d5157b8e94affd1da506c9208d2a78
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 373f8dfeffe16e7287d08566587c63ecbc58fe99749e5acc21b176bf13eac1964b933c24d14e7eff1fd8f66df41543a4a099ae7e45652c8f579cf709fc6bbf93
|
7
|
+
data.tar.gz: f0e6974235d398df3e19f8acc4cee2322148750f66f7591ff7a6de7b2afc44cc82c9ffa13726e9f424f352a8674cd10c8764d949d5e3cd1a2fd9080d924209d1
|
data/lib/sshake/error.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SSHake
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ExecutionError < Error
|
8
|
+
def initialize(response)
|
9
|
+
response
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :response
|
13
|
+
|
14
|
+
def message
|
15
|
+
"Failed to execute command: #{@response.command} (exit code: #{@response.exit_code})"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'sshake/execution_options_dsl'
|
2
|
+
|
3
|
+
module SSHake
|
4
|
+
class ExecutionOptions
|
5
|
+
# The timeout
|
6
|
+
#
|
7
|
+
# @return [Integer]
|
8
|
+
def timeout
|
9
|
+
@timeout || self.class.default_timeout
|
10
|
+
end
|
11
|
+
attr_writer :timeout
|
12
|
+
|
13
|
+
# The user to execute sudo commands as. If nil, commands will
|
14
|
+
# not be executed with sudo.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
attr_accessor :sudo_user
|
18
|
+
|
19
|
+
# The password to be provided to the interactive sudo prompt
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
attr_accessor :sudo_password
|
23
|
+
|
24
|
+
# Should errors be raised?
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
attr_accessor :raise_on_error
|
28
|
+
|
29
|
+
# The data to pass to stdin when executing this command
|
30
|
+
#
|
31
|
+
# @return [String]
|
32
|
+
attr_accessor :stdin
|
33
|
+
|
34
|
+
# A proc to call whenever data is received on stdout
|
35
|
+
#
|
36
|
+
# @return [Proc]
|
37
|
+
attr_accessor :stdout
|
38
|
+
|
39
|
+
# A proc to call whenever data is received on stderr
|
40
|
+
#
|
41
|
+
# @return [Proc]
|
42
|
+
attr_accessor :stderr
|
43
|
+
|
44
|
+
# Should errors be raised
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
def raise_on_error?
|
48
|
+
!!@raise_on_error
|
49
|
+
end
|
50
|
+
|
51
|
+
class << self
|
52
|
+
# Return the default timeout
|
53
|
+
#
|
54
|
+
# @return [Integer]
|
55
|
+
def default_timeout
|
56
|
+
@default_timeout || 60
|
57
|
+
end
|
58
|
+
attr_writer :default_timeout
|
59
|
+
|
60
|
+
# Create a new set of options from a given hash
|
61
|
+
#
|
62
|
+
# @param [Hash] hash
|
63
|
+
# @return [SSHake::ExecutionOptions]
|
64
|
+
def from_hash(hash)
|
65
|
+
options = new
|
66
|
+
options.timeout = hash[:timeout]
|
67
|
+
if hash[:sudo].is_a?(String)
|
68
|
+
options.sudo_user = hash[:sudo]
|
69
|
+
elsif hash[:sudo].is_a?(Hash)
|
70
|
+
options.sudo_user = hash[:sudo][:user]
|
71
|
+
options.sudo_password = hash[:sudo][:password]
|
72
|
+
elsif hash[:sudo] == true
|
73
|
+
options.sudo_user = 'root'
|
74
|
+
end
|
75
|
+
options.raise_on_error = !!hash[:raise_on_error]
|
76
|
+
options.stdin = hash[:stdin]
|
77
|
+
options.stdout = hash[:stdout]
|
78
|
+
options.stderr = hash[:stderr]
|
79
|
+
options
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create a new set of options from a block
|
83
|
+
#
|
84
|
+
# @return [SSHake::ExecutionOptions]
|
85
|
+
def from_block
|
86
|
+
options = new
|
87
|
+
dsl = ExecutionOptionsDSL.new(options)
|
88
|
+
yield dsl
|
89
|
+
options
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SSHake
|
2
|
+
class ExecutionOptionsDSL
|
3
|
+
def initialize(options)
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def timeout(timeout)
|
8
|
+
@options.timeout = timeout
|
9
|
+
end
|
10
|
+
|
11
|
+
def sudo(options = {})
|
12
|
+
@options.sudo_user = options[:user] || 'root'
|
13
|
+
@options.sudo_password = options[:password]
|
14
|
+
end
|
15
|
+
|
16
|
+
def raise_on_error
|
17
|
+
@options.raise_on_error = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def stdin(value)
|
21
|
+
@options.stdin = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def stdout(&block)
|
25
|
+
@options.stdout = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def stderr(&block)
|
29
|
+
@options.stderr = block
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SSHake
|
4
|
+
class Response
|
5
|
+
def initialize
|
6
|
+
@stdout = ''
|
7
|
+
@stderr = ''
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :command
|
11
|
+
attr_accessor :stdout
|
12
|
+
attr_accessor :stderr
|
13
|
+
attr_accessor :exit_code
|
14
|
+
attr_accessor :exit_signal
|
15
|
+
attr_accessor :start_time
|
16
|
+
attr_accessor :finish_time
|
17
|
+
|
18
|
+
def success?
|
19
|
+
@exit_code == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def time
|
23
|
+
(finish_time - start_time).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def timeout?
|
27
|
+
@exit_code == -255
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/sftp'
|
5
|
+
require 'timeout'
|
6
|
+
require 'sshake/error'
|
7
|
+
require 'sshake/logger'
|
8
|
+
require 'sshake/response'
|
9
|
+
require 'sshake/execution_options'
|
10
|
+
|
11
|
+
module SSHake
|
12
|
+
class Session
|
13
|
+
# The underlying net/ssh session
|
14
|
+
#
|
15
|
+
# @return [Net::SSH::Session]
|
16
|
+
attr_reader :session
|
17
|
+
|
18
|
+
# A logger for this session
|
19
|
+
#
|
20
|
+
# @return [Logger, nil]
|
21
|
+
attr_accessor :logger
|
22
|
+
|
23
|
+
# Create a new SSH session
|
24
|
+
#
|
25
|
+
# @return [Sshake::Session]
|
26
|
+
def initialize(host, *args)
|
27
|
+
@host = host
|
28
|
+
@session_options = args
|
29
|
+
end
|
30
|
+
|
31
|
+
# Connect to the SSH server
|
32
|
+
#
|
33
|
+
# @return [void]
|
34
|
+
def connect
|
35
|
+
@session = Net::SSH.start(@host, *@session_options)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is there an established SSH connection
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
def connected?
|
43
|
+
!@session.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Disconnect the underlying SSH connection
|
47
|
+
#
|
48
|
+
# @return [void]
|
49
|
+
def disconnect
|
50
|
+
begin
|
51
|
+
@session.close
|
52
|
+
rescue StandardError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
@session = nil
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Kill the underlying connection
|
60
|
+
def kill!
|
61
|
+
@session.shutdown!
|
62
|
+
@session = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# Execute a command
|
66
|
+
#
|
67
|
+
def execute(commands, options = nil, &block)
|
68
|
+
commands = [commands] unless commands.is_a?(Array)
|
69
|
+
|
70
|
+
options = create_options(options, block)
|
71
|
+
|
72
|
+
# Map sudo onto command
|
73
|
+
if options.sudo_user
|
74
|
+
commands = add_sudo_to_commands_array(commands, options.sudo_user)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Construct a full command string to execute
|
78
|
+
command = commands.join(' && ')
|
79
|
+
|
80
|
+
# Log the command
|
81
|
+
log :info, "\e[44;37m=> #{command}\e[0m"
|
82
|
+
|
83
|
+
# Execute the command
|
84
|
+
response = Response.new
|
85
|
+
response.command = command
|
86
|
+
connect unless connected?
|
87
|
+
begin
|
88
|
+
channel = nil
|
89
|
+
Timeout.timeout(options.timeout) do
|
90
|
+
channel = @session.open_channel do |ch|
|
91
|
+
response.start_time = Time.now
|
92
|
+
channel.exec(command) do |_, success|
|
93
|
+
raise "Command \"#{command}\" was unable to execute" unless success
|
94
|
+
|
95
|
+
ch.send_data(options.stdin) if options.stdin
|
96
|
+
ch.eof!
|
97
|
+
|
98
|
+
ch.on_data do |_, data|
|
99
|
+
response.stdout += data
|
100
|
+
options.stdout&.call(data)
|
101
|
+
log :debug, data.gsub(/[\r]/, ''), tab: 4
|
102
|
+
end
|
103
|
+
|
104
|
+
ch.on_extended_data do |_, _, data|
|
105
|
+
response.stderr += data.delete("\r")
|
106
|
+
options.stderr&.call(data)
|
107
|
+
log :warn, data, tab: 4
|
108
|
+
if data =~ /^\[sudo\] password for/
|
109
|
+
ch.send_data "#{options.sudo_password}\n"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
ch.on_request('exit-status') do |_, data|
|
114
|
+
response.exit_code = data.read_long&.to_i
|
115
|
+
log :info, "\e[43;37m=> Exit code: #{response.exit_code}\e[0m"
|
116
|
+
end
|
117
|
+
|
118
|
+
ch.on_request('exit-signal') do |_, data|
|
119
|
+
response.exit_signal = data.read_long
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
channel.wait
|
124
|
+
end
|
125
|
+
rescue Timeout::Error => e
|
126
|
+
kill!
|
127
|
+
response.exit_code = -255
|
128
|
+
ensure
|
129
|
+
response.finish_time = Time.now
|
130
|
+
end
|
131
|
+
|
132
|
+
if options.raise_on_error? && !response.success?
|
133
|
+
raise ExecutionError, response
|
134
|
+
else
|
135
|
+
response
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def write_data(path, data, options = nil, &block)
|
140
|
+
connect unless connected?
|
141
|
+
tmp_path = "/tmp/sshake-tmp-file-#{SecureRandom.hex(32)}"
|
142
|
+
@session.sftp.file.open(path, 'w') { |f| f.write(data) }
|
143
|
+
response = execute("mv #{tmp_path} #{path}", options, &block)
|
144
|
+
response.success?
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def add_sudo_to_commands_array(commands, user)
|
150
|
+
commands.map do |command|
|
151
|
+
"sudo -u #{user} --stdin #{command}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def create_options(hash, block)
|
156
|
+
if block && hash
|
157
|
+
raise Error, 'You cannot provide a block and options'
|
158
|
+
elsif block
|
159
|
+
ExecutionOptions.from_block(&block)
|
160
|
+
elsif hash.is_a?(Hash)
|
161
|
+
ExecutionOptions.from_hash(hash)
|
162
|
+
else
|
163
|
+
ExecutionOptions.new
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def log(type, text, options = {})
|
168
|
+
logger = @logger || SSHake.logger
|
169
|
+
return unless logger
|
170
|
+
|
171
|
+
prefix = "\e[45;37m[#{@host}]\e[0m"
|
172
|
+
tabs = ' ' * (options[:tab] || 0)
|
173
|
+
text.split(/\n/).each do |line|
|
174
|
+
logger.send(type, prefix + tabs + line)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sshake
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Cooke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-sftp
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-ssh
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
description: A wrapper for net/ssh to make running commands more fun
|
42
|
+
email:
|
43
|
+
- me@adamcooke.io
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- lib/sshake/error.rb
|
49
|
+
- lib/sshake/execution_options.rb
|
50
|
+
- lib/sshake/execution_options_dsl.rb
|
51
|
+
- lib/sshake/logger.rb
|
52
|
+
- lib/sshake/response.rb
|
53
|
+
- lib/sshake/session.rb
|
54
|
+
- lib/sshake/version.rb
|
55
|
+
homepage: https://github.com/adamcooke/sshake
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 2.5.2.3
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: A wrapper for net/ssh to make running commands more fun
|
79
|
+
test_files: []
|