sshake 1.0.2 → 2.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 +4 -4
- data/lib/sshake/base_session.rb +29 -29
- data/lib/sshake/error.rb +6 -1
- data/lib/sshake/execution_options.rb +14 -5
- data/lib/sshake/execution_options_dsl.rb +4 -0
- data/lib/sshake/klogger.rb +14 -0
- data/lib/sshake/mock/command.rb +3 -3
- data/lib/sshake/mock/command_set.rb +2 -0
- data/lib/sshake/mock/environment.rb +5 -5
- data/lib/sshake/mock/executed_command.rb +3 -3
- data/lib/sshake/mock/session.rb +10 -13
- data/lib/sshake/mock/unsupported_command_error.rb +2 -0
- data/lib/sshake/recorded_session.rb +47 -0
- data/lib/sshake/recorder.rb +94 -0
- data/lib/sshake/recording.rb +29 -0
- data/lib/sshake/response.rb +14 -10
- data/lib/sshake/session.rb +107 -65
- data/lib/sshake/version.rb +3 -1
- metadata +33 -9
- data/lib/sshake/logger.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95a0f6a6db803ff62826997e18e0b5d5ec81977046833bc32c10e29ebdbfd883
|
4
|
+
data.tar.gz: 8a2c9aa526b96827b3d50d25cd8b479bf3310eb6857822790410e79a38ac8423
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc53e93b8e027f96c6078acb1626a056512dbf932a5650a1e4e4dfa53d28d72c219cff4acb392ca3d46fcbd5cd136aa7114b3edca27884817f86ace0648ec0c5
|
7
|
+
data.tar.gz: 25e70b953ed507800fa3f1a0531c926023d383f8a73caf8de201e12db2b4dd4e864af0080bf657952ad00112528572368e6c9eced39adf254858897e9487a6e3
|
data/lib/sshake/base_session.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'securerandom'
|
2
|
-
require '
|
4
|
+
require 'klogger'
|
5
|
+
require 'sshake/klogger'
|
3
6
|
require 'sshake/execution_options'
|
4
7
|
|
5
8
|
module SSHake
|
6
9
|
class BaseSession
|
10
|
+
|
7
11
|
# A logger for this session
|
8
12
|
#
|
9
|
-
# @return [
|
10
|
-
attr_accessor :
|
13
|
+
# @return [Klogger, nil]
|
14
|
+
attr_accessor :klogger
|
11
15
|
|
12
16
|
# An ID for this session
|
13
17
|
#
|
@@ -19,58 +23,65 @@ module SSHake
|
|
19
23
|
# @return [Boolean]
|
20
24
|
attr_accessor :raise_on_error
|
21
25
|
|
22
|
-
def initialize(*
|
26
|
+
def initialize(*_args, klogger: nil)
|
23
27
|
@id = SecureRandom.hex(4)
|
28
|
+
@klogger = klogger || SSHake.klogger
|
24
29
|
end
|
25
30
|
|
26
31
|
# Connect to the SSH server
|
27
32
|
#
|
28
33
|
# @return [void]
|
29
34
|
def connect
|
30
|
-
raise
|
35
|
+
raise 'Override #connect in sub-sessions'
|
31
36
|
end
|
32
37
|
|
33
38
|
# Is there an established SSH connection
|
34
39
|
#
|
35
40
|
# @return [Boolean]
|
36
41
|
def connected?
|
37
|
-
raise
|
42
|
+
raise 'Override #connected? in sub-sessions'
|
38
43
|
end
|
39
44
|
|
40
45
|
# Disconnect the underlying SSH connection
|
41
46
|
#
|
42
47
|
# @return [void]
|
43
48
|
def disconnect
|
44
|
-
raise
|
49
|
+
raise 'Override #disconnect in sub-sessions'
|
45
50
|
end
|
46
51
|
|
47
52
|
# Kill the underlying connection
|
48
53
|
def kill!
|
49
|
-
raise
|
54
|
+
raise 'Override #kill! in sub-sessions'
|
50
55
|
end
|
51
56
|
|
52
57
|
# Execute a command
|
53
58
|
#
|
54
|
-
def execute(
|
55
|
-
raise
|
59
|
+
def execute(_commands, _options = nil)
|
60
|
+
raise 'Override #execute in sub-sessions'
|
56
61
|
end
|
57
62
|
|
58
|
-
def write_data(
|
59
|
-
raise
|
63
|
+
def write_data(_path, _data, _options = nil)
|
64
|
+
raise 'Override #write_data in sub-sessions'
|
60
65
|
end
|
61
66
|
|
62
67
|
private
|
63
68
|
|
64
|
-
def add_sudo_to_commands_array(commands, user)
|
69
|
+
def add_sudo_to_commands_array(commands, user, password = nil)
|
70
|
+
sudo_prefix = "sudo -u #{user}"
|
71
|
+
unless password.nil?
|
72
|
+
sudo_prefix += " --stdin -p '[sshake-sudo-password]: ' "
|
73
|
+
end
|
65
74
|
commands.map do |command|
|
66
|
-
"
|
75
|
+
"#{sudo_prefix} #{command}"
|
67
76
|
end
|
68
77
|
end
|
69
78
|
|
70
79
|
def create_options(hash, block)
|
71
80
|
if block && hash
|
72
81
|
raise Error, 'You cannot provide a block and options'
|
73
|
-
|
82
|
+
end
|
83
|
+
|
84
|
+
if block
|
74
85
|
ExecutionOptions.from_block(&block)
|
75
86
|
elsif hash.is_a?(Hash)
|
76
87
|
ExecutionOptions.from_hash(hash)
|
@@ -79,23 +90,12 @@ module SSHake
|
|
79
90
|
end
|
80
91
|
end
|
81
92
|
|
82
|
-
def log(type, text, options = {})
|
83
|
-
logger = @logger || SSHake.logger
|
84
|
-
return unless logger
|
85
|
-
|
86
|
-
prefix = "[#{@id}] [#{@host}] "
|
87
|
-
|
88
|
-
text.split(/\n/).each do |line|
|
89
|
-
logger.send(type, prefix + line)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
93
|
def prepare_commands(commands, execution_options, **options)
|
94
94
|
commands = [commands] unless commands.is_a?(Array)
|
95
95
|
|
96
96
|
# Map sudo onto command
|
97
97
|
if execution_options.sudo_user && options[:add_sudo] != false
|
98
|
-
commands = add_sudo_to_commands_array(commands, execution_options.sudo_user)
|
98
|
+
commands = add_sudo_to_commands_array(commands, execution_options.sudo_user, execution_options.sudo_password)
|
99
99
|
end
|
100
100
|
|
101
101
|
# Construct a full command string to execute
|
@@ -105,9 +105,9 @@ module SSHake
|
|
105
105
|
def handle_response(response, options)
|
106
106
|
if !response.success? && ((options.raise_on_error.nil? && @raise_on_error) || options.raise_on_error?)
|
107
107
|
raise ExecutionError, response
|
108
|
-
else
|
109
|
-
response
|
110
108
|
end
|
109
|
+
|
110
|
+
response
|
111
111
|
end
|
112
112
|
|
113
113
|
end
|
data/lib/sshake/error.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SSHake
|
4
|
+
|
4
5
|
class Error < StandardError
|
5
6
|
end
|
6
7
|
|
7
8
|
class ExecutionError < Error
|
9
|
+
|
8
10
|
def initialize(response)
|
9
11
|
@response = response
|
10
12
|
end
|
@@ -16,7 +18,10 @@ module SSHake
|
|
16
18
|
end
|
17
19
|
|
18
20
|
def message
|
19
|
-
"Failed to execute command: #{@response.command}
|
21
|
+
"Failed to execute command: #{@response.command} " \
|
22
|
+
"(stderr: #{@response.stderr}) (exit code: #{@response.exit_code})"
|
20
23
|
end
|
24
|
+
|
21
25
|
end
|
26
|
+
|
22
27
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'sshake/execution_options_dsl'
|
2
4
|
|
3
5
|
module SSHake
|
4
6
|
class ExecutionOptions
|
7
|
+
|
5
8
|
# The timeout
|
6
9
|
#
|
7
10
|
# @return [Integer]
|
@@ -44,7 +47,7 @@ module SSHake
|
|
44
47
|
# A file that you wish to stream to the remote channel
|
45
48
|
# with the current commend
|
46
49
|
#
|
47
|
-
#
|
50
|
+
# @return [File]
|
48
51
|
attr_accessor :file_to_stream
|
49
52
|
|
50
53
|
# Should errors be raised
|
@@ -55,6 +58,7 @@ module SSHake
|
|
55
58
|
end
|
56
59
|
|
57
60
|
class << self
|
61
|
+
|
58
62
|
# Return the default timeout
|
59
63
|
#
|
60
64
|
# @return [Integer]
|
@@ -70,15 +74,18 @@ module SSHake
|
|
70
74
|
def from_hash(hash)
|
71
75
|
options = new
|
72
76
|
options.timeout = hash[:timeout]
|
73
|
-
|
77
|
+
case hash[:sudo]
|
78
|
+
when String
|
74
79
|
options.sudo_user = hash[:sudo]
|
75
|
-
|
76
|
-
options.sudo_user = hash[:sudo][:user]
|
80
|
+
when Hash
|
81
|
+
options.sudo_user = hash[:sudo][:user] || 'root'
|
77
82
|
options.sudo_password = hash[:sudo][:password]
|
78
|
-
|
83
|
+
when true
|
79
84
|
options.sudo_user = 'root'
|
80
85
|
end
|
86
|
+
# rubocop:disable Style/DoubleNegation
|
81
87
|
options.raise_on_error = !!hash[:raise_on_error]
|
88
|
+
# rubocop:enable Style/DoubleNegation
|
82
89
|
options.stdin = hash[:stdin]
|
83
90
|
options.stdout = hash[:stdout]
|
84
91
|
options.stderr = hash[:stderr]
|
@@ -95,6 +102,8 @@ module SSHake
|
|
95
102
|
yield dsl
|
96
103
|
options
|
97
104
|
end
|
105
|
+
|
98
106
|
end
|
107
|
+
|
99
108
|
end
|
100
109
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module SSHake
|
4
4
|
class ExecutionOptionsDSL
|
5
|
+
|
5
6
|
def initialize(options)
|
6
7
|
@options = options
|
7
8
|
end
|
@@ -15,9 +16,11 @@ module SSHake
|
|
15
16
|
@options.sudo_password = options[:password]
|
16
17
|
end
|
17
18
|
|
19
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
18
20
|
def raise_on_error(bool = true)
|
19
21
|
@options.raise_on_error = bool
|
20
22
|
end
|
23
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
21
24
|
|
22
25
|
def dont_raise_on_error
|
23
26
|
@options.raise_on_error = false
|
@@ -38,5 +41,6 @@ module SSHake
|
|
38
41
|
def file_to_stream(file)
|
39
42
|
@options.file_to_stream = file
|
40
43
|
end
|
44
|
+
|
41
45
|
end
|
42
46
|
end
|
data/lib/sshake/mock/command.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'sshake/response'
|
2
4
|
|
3
5
|
module SSHake
|
@@ -24,9 +26,7 @@ module SSHake
|
|
24
26
|
def make_response(environment)
|
25
27
|
response = SSHake::Response.new
|
26
28
|
response.start_time = Time.now
|
27
|
-
|
28
|
-
@block.call(response, environment)
|
29
|
-
end
|
29
|
+
@block&.call(response, environment)
|
30
30
|
response.finish_time = Time.now
|
31
31
|
response
|
32
32
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SSHake
|
2
4
|
module Mock
|
3
5
|
class Environment
|
@@ -7,16 +9,14 @@ module SSHake
|
|
7
9
|
@captures = []
|
8
10
|
end
|
9
11
|
|
10
|
-
attr_accessor :command
|
11
|
-
attr_accessor :options
|
12
|
-
attr_accessor :captures
|
12
|
+
attr_accessor :command, :options, :captures
|
13
13
|
|
14
14
|
def store
|
15
|
-
@session
|
15
|
+
@session&.store
|
16
16
|
end
|
17
17
|
|
18
18
|
def written_files
|
19
|
-
@session
|
19
|
+
@session&.written_files
|
20
20
|
end
|
21
21
|
|
22
22
|
end
|
@@ -1,10 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SSHake
|
2
4
|
module Mock
|
3
5
|
class ExecutedCommand
|
4
6
|
|
5
|
-
attr_reader :command
|
6
|
-
attr_reader :environment
|
7
|
-
attr_reader :response
|
7
|
+
attr_reader :command, :environment, :response
|
8
8
|
|
9
9
|
def initialize(command, environment, response)
|
10
10
|
@command = command
|
data/lib/sshake/mock/session.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'net/ssh/errors'
|
2
4
|
require 'sshake/base_session'
|
3
5
|
require 'sshake/mock/command_set'
|
@@ -9,10 +11,7 @@ module SSHake
|
|
9
11
|
module Mock
|
10
12
|
class Session < BaseSession
|
11
13
|
|
12
|
-
attr_reader :command_set
|
13
|
-
attr_reader :store
|
14
|
-
attr_reader :written_files
|
15
|
-
attr_reader :executed_commands
|
14
|
+
attr_reader :command_set, :store, :written_files, :executed_commands
|
16
15
|
|
17
16
|
def initialize(**options)
|
18
17
|
@options = options
|
@@ -57,25 +56,21 @@ module SSHake
|
|
57
56
|
environment = Environment.new(self)
|
58
57
|
|
59
58
|
environment.options = create_options(options, block)
|
60
|
-
environment.command = prepare_commands(commands, environment.options, :
|
59
|
+
environment.command = prepare_commands(commands, environment.options, add_sudo: false)
|
61
60
|
|
62
61
|
command, environment.captures = @command_set.match(environment.command)
|
63
62
|
|
64
|
-
if command.nil?
|
65
|
-
raise UnsupportedCommandError.new(environment.command)
|
66
|
-
end
|
63
|
+
raise UnsupportedCommandError, environment.command if command.nil?
|
67
64
|
|
68
65
|
response = command.make_response(environment)
|
69
66
|
|
70
|
-
if environment.options.file_to_stream
|
71
|
-
response.bytes_streamed = environment.options.file_to_stream.size
|
72
|
-
end
|
67
|
+
response.bytes_streamed = environment.options.file_to_stream.size if environment.options.file_to_stream
|
73
68
|
|
74
69
|
@executed_commands << ExecutedCommand.new(command, environment, response)
|
75
70
|
handle_response(response, environment.options)
|
76
71
|
end
|
77
72
|
|
78
|
-
def write_data(path, data,
|
73
|
+
def write_data(path, data, _options = nil)
|
79
74
|
connect unless connected?
|
80
75
|
@written_files[path] = data
|
81
76
|
true
|
@@ -92,9 +87,11 @@ module SSHake
|
|
92
87
|
end
|
93
88
|
end
|
94
89
|
|
90
|
+
# rubocop:disable Naming/PredicateName
|
95
91
|
def has_executed_command?(matcher)
|
96
|
-
find_executed_commands(matcher).size
|
92
|
+
find_executed_commands(matcher).size.positive?
|
97
93
|
end
|
94
|
+
# rubocop:enable Naming/PredicateName
|
98
95
|
|
99
96
|
end
|
100
97
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sshake/base_session'
|
4
|
+
require 'sshake/recorder'
|
5
|
+
require 'sshake/session'
|
6
|
+
|
7
|
+
module SSHake
|
8
|
+
class RecordedSession < BaseSession
|
9
|
+
|
10
|
+
attr_reader :recorder
|
11
|
+
attr_reader :session
|
12
|
+
|
13
|
+
def initialize(recorder, session, **options)
|
14
|
+
super
|
15
|
+
@recorder = recorder
|
16
|
+
@session = session
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute(commands, options = nil, &block)
|
20
|
+
options = create_options(options, block)
|
21
|
+
command_to_execute = prepare_commands(commands, options)
|
22
|
+
|
23
|
+
cached_response = @recorder.play(command_to_execute, options: options, connection: connection_hash)
|
24
|
+
return cached_response if cached_response
|
25
|
+
|
26
|
+
response = @session.execute(commands, options)
|
27
|
+
record(command_to_execute, options, response)
|
28
|
+
response
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def record(command, options, response)
|
34
|
+
@recorder.record(command, response, options: options, connection: connection_hash)
|
35
|
+
@recorder.save
|
36
|
+
end
|
37
|
+
|
38
|
+
def connection_hash
|
39
|
+
{
|
40
|
+
host: @session.host,
|
41
|
+
user: @session.user,
|
42
|
+
port: @session.port
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
module SSHake
|
5
|
+
class Recorder
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# Return the root where all recorded sessions should be stored
|
10
|
+
#
|
11
|
+
# @return [nil, String]
|
12
|
+
attr_accessor :save_root
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :name
|
17
|
+
attr_reader :cache
|
18
|
+
|
19
|
+
def initialize(name, cache: nil)
|
20
|
+
@name = name
|
21
|
+
@cache = cache || {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def load
|
25
|
+
return if self.class.save_root.nil?
|
26
|
+
|
27
|
+
@cache = YAML.load_file(File.join(self.class.save_root, "#{name}.yml"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def save
|
31
|
+
return if self.class.save_root.nil?
|
32
|
+
|
33
|
+
FileUtils.mkdir_p(self.class.save_root)
|
34
|
+
File.write(File.join(self.class.save_root, "#{name}.yml"), @cache.to_yaml)
|
35
|
+
end
|
36
|
+
|
37
|
+
def play(command, connection: {}, options: nil)
|
38
|
+
possibilities = @cache[command]
|
39
|
+
return nil if possibilities.nil?
|
40
|
+
|
41
|
+
options_as_hash = options_to_hash(options)
|
42
|
+
|
43
|
+
possibility = possibilities.find do |p|
|
44
|
+
p[:options] == options_as_hash &&
|
45
|
+
p[:connection] == connection
|
46
|
+
end
|
47
|
+
|
48
|
+
return nil if possibility.nil?
|
49
|
+
|
50
|
+
response = Response.new(cached: true)
|
51
|
+
possibility[:response].each do |key, value|
|
52
|
+
response.public_send("#{key}=", value)
|
53
|
+
end
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
def record(command, response, connection: {}, options: nil)
|
58
|
+
@cache[command] ||= []
|
59
|
+
@cache[command] << {
|
60
|
+
connection: connection,
|
61
|
+
options: options_to_hash(options),
|
62
|
+
response: response_to_hash(response)
|
63
|
+
}
|
64
|
+
save
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def response_to_hash(response)
|
70
|
+
{
|
71
|
+
stdout: response.stdout,
|
72
|
+
stderr: response.stderr,
|
73
|
+
exit_code: response.exit_code,
|
74
|
+
start_time: response.start_time.to_i,
|
75
|
+
finish_time: response.finish_time.to_i
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def options_to_hash(options)
|
80
|
+
options = ExecutionOptions.from_hash({}) if options.nil?
|
81
|
+
|
82
|
+
hash = {}
|
83
|
+
hash[:timeout] = options.timeout if options.timeout
|
84
|
+
hash[:sudo_user] = options.sudo_user if options.sudo_user
|
85
|
+
hash[:sudo_password] = options.sudo_password if options.sudo_password
|
86
|
+
hash[:raise_on_error] = true if options.raise_on_error?
|
87
|
+
hash[:stdin] = Digest::SHA1.hexdigest(options.stdin) if options.stdin
|
88
|
+
hash[:file_to_stream] = Digest::SHA1.hexdigest(options.file_to_stream.read) if options.file_to_stream
|
89
|
+
|
90
|
+
hash
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sshake/recorder'
|
4
|
+
require 'sshake/recorded_session'
|
5
|
+
require 'sshake/error'
|
6
|
+
|
7
|
+
module SSHake
|
8
|
+
|
9
|
+
class NestedRecordingsUnsupportedError < Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def record(name)
|
15
|
+
if Thread.current[:sshake_recorder]
|
16
|
+
raise NestedRecordingsUnsupportedError, 'You cannot nest SSHake.record blocks'
|
17
|
+
end
|
18
|
+
|
19
|
+
recorder = Recorder.new(name)
|
20
|
+
recorder.load
|
21
|
+
Thread.current[:sshake_recorder] = recorder
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
Thread.current[:sshake_recorder] = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/sshake/response.rb
CHANGED
@@ -2,24 +2,27 @@
|
|
2
2
|
|
3
3
|
module SSHake
|
4
4
|
class Response
|
5
|
-
|
5
|
+
|
6
|
+
def initialize(cached: false)
|
6
7
|
@stdout = ''
|
7
8
|
@stderr = ''
|
8
9
|
@exit_code = 0
|
9
10
|
@bytes_streamed = 0
|
11
|
+
@cached = cached
|
10
12
|
end
|
11
13
|
|
12
|
-
attr_accessor :command
|
13
|
-
attr_accessor :stdout
|
14
|
-
attr_accessor :stderr
|
15
|
-
attr_accessor :exit_code
|
16
|
-
attr_accessor :exit_signal
|
17
|
-
attr_accessor :start_time
|
18
|
-
attr_accessor :finish_time
|
19
|
-
attr_accessor :bytes_streamed
|
14
|
+
attr_accessor :command, :stdout, :stderr, :exit_code, :exit_signal, :start_time, :finish_time, :bytes_streamed
|
20
15
|
|
21
16
|
def success?
|
22
|
-
@exit_code
|
17
|
+
@exit_code.zero?
|
18
|
+
end
|
19
|
+
|
20
|
+
def cached?
|
21
|
+
@cached == true
|
22
|
+
end
|
23
|
+
|
24
|
+
def cached!
|
25
|
+
@cached = true
|
23
26
|
end
|
24
27
|
|
25
28
|
def time
|
@@ -33,5 +36,6 @@ module SSHake
|
|
33
36
|
def timeout!
|
34
37
|
@exit_code = -255
|
35
38
|
end
|
39
|
+
|
36
40
|
end
|
37
41
|
end
|
data/lib/sshake/session.rb
CHANGED
@@ -9,27 +9,48 @@ require 'sshake/base_session'
|
|
9
9
|
|
10
10
|
module SSHake
|
11
11
|
class Session < BaseSession
|
12
|
+
|
12
13
|
# The underlying net/ssh session
|
13
14
|
#
|
14
15
|
# @return [Net::SSH::Session]
|
15
16
|
attr_reader :session
|
16
17
|
|
18
|
+
# Return the host to connect to
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :host
|
22
|
+
|
17
23
|
# Create a new SSH session
|
18
24
|
#
|
19
25
|
# @return [Sshake::Session]
|
20
|
-
def initialize(host,
|
26
|
+
def initialize(host, username = nil, **options)
|
21
27
|
super
|
22
28
|
@host = host
|
23
|
-
@
|
29
|
+
@username = username
|
30
|
+
@session_options = options
|
31
|
+
@session_options.delete(:klogger)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the username for the connection
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
def user
|
38
|
+
@user || ENV.fetch('USER', nil)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return the port that will be connected to
|
42
|
+
#
|
43
|
+
# @return [Integer]
|
44
|
+
def port
|
45
|
+
@session_options[:port] || 22
|
24
46
|
end
|
25
47
|
|
26
48
|
# Connect to the SSH server
|
27
49
|
#
|
28
50
|
# @return [void]
|
29
51
|
def connect
|
30
|
-
|
31
|
-
|
32
|
-
@session = Net::SSH.start(@host, *@session_options)
|
52
|
+
klogger.debug 'Connecting', id: @id, host: @host, user: @user, port: @session_options[:port] || 22
|
53
|
+
@session = Net::SSH.start(@host, user, @session_options)
|
33
54
|
true
|
34
55
|
end
|
35
56
|
|
@@ -47,11 +68,11 @@ module SSHake
|
|
47
68
|
return false if @session.nil?
|
48
69
|
|
49
70
|
begin
|
50
|
-
|
71
|
+
klogger.debug 'Closing connection', id: @id, host: @host
|
51
72
|
@session.close
|
52
|
-
|
73
|
+
klogger.debug 'Connection closed', id: @id, host: @host
|
53
74
|
rescue StandardError => e
|
54
|
-
|
75
|
+
logger.exception(e, 'Connection not closed')
|
55
76
|
nil
|
56
77
|
end
|
57
78
|
@session = nil
|
@@ -60,12 +81,13 @@ module SSHake
|
|
60
81
|
|
61
82
|
# Kill the underlying connection
|
62
83
|
def kill!
|
63
|
-
|
84
|
+
klogger.debug 'Attemping to shutdown', id: @id, host: @host
|
64
85
|
@session.shutdown!
|
65
|
-
|
86
|
+
klogger.debug 'Shutdown success', id: @id, host: @host
|
66
87
|
@session = nil
|
67
88
|
end
|
68
89
|
|
90
|
+
# rubocop:disable Metrics/AbcSize
|
69
91
|
def execute(commands, options = nil, &block)
|
70
92
|
options = create_options(options, block)
|
71
93
|
command_to_execute = prepare_commands(commands, options)
|
@@ -75,91 +97,111 @@ module SSHake
|
|
75
97
|
response.command = command_to_execute
|
76
98
|
connect unless connected?
|
77
99
|
|
78
|
-
|
79
|
-
|
80
|
-
log :debug, "Timeout: #{options.timeout}"
|
100
|
+
klogger.group(id: @id, host: @host) do
|
101
|
+
klogger.info 'Executing command', command: command_to_execute, timeout: options.timeout
|
81
102
|
|
82
|
-
|
83
|
-
|
84
|
-
Timeout.timeout(options.timeout) do
|
85
|
-
channel = @session.open_channel do |ch|
|
86
|
-
response.start_time = Time.now
|
87
|
-
channel.exec(command_to_execute) do |_, success|
|
88
|
-
raise "Command \"#{command_to_execute}\" was unable to execute" unless success
|
89
|
-
|
90
|
-
if options.stdin
|
91
|
-
ch.send_data(options.stdin)
|
92
|
-
end
|
103
|
+
begin
|
104
|
+
channel = nil
|
93
105
|
|
94
|
-
|
95
|
-
|
96
|
-
|
106
|
+
Timeout.timeout(options.timeout) do
|
107
|
+
channel = @session.open_channel do |ch|
|
108
|
+
response.start_time = Time.now
|
97
109
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
end
|
110
|
+
channel.exec(command_to_execute) do |_, success|
|
111
|
+
raise "Command \"#{command_to_execute}\" was unable to execute" unless success
|
112
|
+
|
113
|
+
ch.send_data(options.stdin) if options.stdin
|
103
114
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
log :debug, data
|
108
|
-
if data =~ /^\[sudo\] password for/
|
109
|
-
log :debug, 'Sending sudo password'
|
110
|
-
ch.send_data "#{options.sudo_password}\n"
|
115
|
+
if options.file_to_stream.nil? && options.sudo_password.nil?
|
116
|
+
klogger.debug 'Sending EOF to channel'
|
117
|
+
ch.eof!
|
111
118
|
end
|
112
|
-
end
|
113
119
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
120
|
+
ch.on_data do |_, data|
|
121
|
+
response.stdout += data
|
122
|
+
options.stdout&.call(data)
|
123
|
+
klogger.debug "[stdout] #{data.gsub(/\r/, '').strip}"
|
124
|
+
end
|
118
125
|
|
119
|
-
|
120
|
-
|
121
|
-
|
126
|
+
ch.on_extended_data do |_, _, data|
|
127
|
+
response.stderr += data.delete("\r")
|
128
|
+
options.stderr&.call(data)
|
129
|
+
klogger.debug "[stderr] #{data.gsub(/\r/, '').strip}"
|
130
|
+
if options.sudo_password && data =~ /^\[sshake-sudo-password\]:\s\z/
|
131
|
+
klogger.debug 'Sending sudo password', length: options.sudo_password.length
|
132
|
+
ch.send_data "#{options.sudo_password}\n"
|
122
133
|
|
123
|
-
|
124
|
-
|
125
|
-
next if ch.eof?
|
126
|
-
if ch.output.length < 128 * 1024
|
127
|
-
if data = options.file_to_stream.read(1024 * 1024)
|
128
|
-
ch.send_data(data)
|
129
|
-
response.bytes_streamed += data.bytesize
|
130
|
-
else
|
134
|
+
if options.file_to_stream.nil?
|
135
|
+
klogger.debug 'Sending EOF after password'
|
131
136
|
ch.eof!
|
132
137
|
end
|
133
138
|
end
|
134
139
|
end
|
140
|
+
|
141
|
+
ch.on_request('exit-status') do |_, data|
|
142
|
+
response.exit_code = data.read_long&.to_i
|
143
|
+
klogger.info 'Exited', exit_code: response.exit_code
|
144
|
+
end
|
145
|
+
|
146
|
+
ch.on_request('exit-signal') do |_, data|
|
147
|
+
response.exit_signal = data.read_long
|
148
|
+
end
|
149
|
+
|
150
|
+
if options.file_to_stream
|
151
|
+
ch.on_process do |_, data|
|
152
|
+
next if ch.eof?
|
153
|
+
|
154
|
+
if ch.output.length < 128 * 1024
|
155
|
+
if data = options.file_to_stream.read(1024 * 1024)
|
156
|
+
ch.send_data(data)
|
157
|
+
response.bytes_streamed += data.bytesize
|
158
|
+
else
|
159
|
+
ch.eof!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
135
164
|
end
|
136
165
|
end
|
166
|
+
channel.wait
|
137
167
|
end
|
138
|
-
|
168
|
+
rescue Timeout::Error
|
169
|
+
klogger.debug 'Command timed out'
|
170
|
+
kill!
|
171
|
+
response.timeout!
|
172
|
+
ensure
|
173
|
+
response.finish_time = Time.now
|
139
174
|
end
|
140
|
-
rescue Timeout::Error => e
|
141
|
-
log :debug, "Got timeout error while executing command"
|
142
|
-
kill!
|
143
|
-
response.timeout!
|
144
|
-
ensure
|
145
|
-
response.finish_time = Time.now
|
146
175
|
end
|
147
176
|
|
148
177
|
handle_response(response, options)
|
149
178
|
end
|
179
|
+
# rubocop:enable Metrics/AbcSize
|
150
180
|
|
151
181
|
def write_data(path, data, options = nil, &block)
|
152
182
|
connect unless connected?
|
153
183
|
tmp_path = "/tmp/sshake-tmp-file-#{SecureRandom.hex(32)}"
|
154
184
|
@session.sftp.file.open(tmp_path, 'w') do |f|
|
155
185
|
d = data.dup.force_encoding('BINARY')
|
156
|
-
until d.empty?
|
157
|
-
f.write(d.slice!(0, 1024))
|
158
|
-
end
|
186
|
+
f.write(d.slice!(0, 1024)) until d.empty?
|
159
187
|
end
|
160
188
|
response = execute("mv #{tmp_path} #{path}", options, &block)
|
161
189
|
response.success?
|
162
190
|
end
|
163
191
|
|
192
|
+
class << self
|
193
|
+
|
194
|
+
def create(*args)
|
195
|
+
session = new(*args)
|
196
|
+
|
197
|
+
if recorder = Thread.current[:sshake_recorder]
|
198
|
+
return RecordedSession.new(recorder, session)
|
199
|
+
end
|
200
|
+
|
201
|
+
session
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
164
206
|
end
|
165
207
|
end
|
data/lib/sshake/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sshake
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Cooke
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: klogger-logger
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: net-sftp
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -50,21 +70,25 @@ files:
|
|
50
70
|
- lib/sshake/error.rb
|
51
71
|
- lib/sshake/execution_options.rb
|
52
72
|
- lib/sshake/execution_options_dsl.rb
|
53
|
-
- lib/sshake/
|
73
|
+
- lib/sshake/klogger.rb
|
54
74
|
- lib/sshake/mock/command.rb
|
55
75
|
- lib/sshake/mock/command_set.rb
|
56
76
|
- lib/sshake/mock/environment.rb
|
57
77
|
- lib/sshake/mock/executed_command.rb
|
58
78
|
- lib/sshake/mock/session.rb
|
59
79
|
- lib/sshake/mock/unsupported_command_error.rb
|
80
|
+
- lib/sshake/recorded_session.rb
|
81
|
+
- lib/sshake/recorder.rb
|
82
|
+
- lib/sshake/recording.rb
|
60
83
|
- lib/sshake/response.rb
|
61
84
|
- lib/sshake/session.rb
|
62
85
|
- lib/sshake/version.rb
|
63
86
|
homepage: https://github.com/adamcooke/sshake
|
64
87
|
licenses:
|
65
88
|
- MIT
|
66
|
-
metadata:
|
67
|
-
|
89
|
+
metadata:
|
90
|
+
rubygems_mfa_required: 'true'
|
91
|
+
post_install_message:
|
68
92
|
rdoc_options: []
|
69
93
|
require_paths:
|
70
94
|
- lib
|
@@ -72,15 +96,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
96
|
requirements:
|
73
97
|
- - ">="
|
74
98
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
99
|
+
version: '2.6'
|
76
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
101
|
requirements:
|
78
102
|
- - ">="
|
79
103
|
- !ruby/object:Gem::Version
|
80
104
|
version: '0'
|
81
105
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
106
|
+
rubygems_version: 3.2.32
|
107
|
+
signing_key:
|
84
108
|
specification_version: 4
|
85
109
|
summary: A wrapper for net/ssh to make running commands more fun
|
86
110
|
test_files: []
|