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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 314760390e24b395431def2b202b606569f833dce1148bdedc556c7ca7869c3f
4
- data.tar.gz: 82eee2aa19795c54837ebe9b3c20c5d2349c7f8402240ac3bc13c07692ee46d2
3
+ metadata.gz: 95a0f6a6db803ff62826997e18e0b5d5ec81977046833bc32c10e29ebdbfd883
4
+ data.tar.gz: 8a2c9aa526b96827b3d50d25cd8b479bf3310eb6857822790410e79a38ac8423
5
5
  SHA512:
6
- metadata.gz: a2310a628972c8062ae526c569b135dd2cc4ee37a057f2924d81e43605f2aaaa8d42fb39c3078cf20e766857fbdc8c081bf6efad9c61d184f04ac1b0fafd33b9
7
- data.tar.gz: 67cb1c5689970aaac6c7987bb4e19d15152bf18dd36e2fa0bdaada484019ffd690b35025489c9cc7e8128c6c0d151c5f887aa62d9802d09c089c78d630c1fc55
6
+ metadata.gz: dc53e93b8e027f96c6078acb1626a056512dbf932a5650a1e4e4dfa53d28d72c219cff4acb392ca3d46fcbd5cd136aa7114b3edca27884817f86ace0648ec0c5
7
+ data.tar.gz: 25e70b953ed507800fa3f1a0531c926023d383f8a73caf8de201e12db2b4dd4e864af0080bf657952ad00112528572368e6c9eced39adf254858897e9487a6e3
@@ -1,13 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
- require 'sshake/logger'
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 [Logger, nil]
10
- attr_accessor :logger
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(*args)
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 "Override #connect in sub-sessions"
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 "Override #connected? in sub-sessions"
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 "Override #disconnect in sub-sessions"
49
+ raise 'Override #disconnect in sub-sessions'
45
50
  end
46
51
 
47
52
  # Kill the underlying connection
48
53
  def kill!
49
- raise "Override #kill! in sub-sessions"
54
+ raise 'Override #kill! in sub-sessions'
50
55
  end
51
56
 
52
57
  # Execute a command
53
58
  #
54
- def execute(commands, options = nil, &block)
55
- raise "Override #execute in sub-sessions"
59
+ def execute(_commands, _options = nil)
60
+ raise 'Override #execute in sub-sessions'
56
61
  end
57
62
 
58
- def write_data(path, data, options = nil, &block)
59
- raise "Override #write_data in sub-sessions"
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
- "sudo -u #{user} --stdin #{command}"
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
- elsif block
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} (stderr: #{@response.stderr}) (exit code: #{@response.exit_code})"
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
- # @return [File]
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
- if hash[:sudo].is_a?(String)
77
+ case hash[:sudo]
78
+ when String
74
79
  options.sudo_user = hash[:sudo]
75
- elsif hash[:sudo].is_a?(Hash)
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
- elsif hash[:sudo] == true
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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SSHake
4
+
5
+ class << self
6
+
7
+ def klogger
8
+ @klogger ||= Klogger.new(:ssh, destination: $stdout, formatter: :go)
9
+ end
10
+ attr_writer :klogger
11
+
12
+ end
13
+
14
+ end
@@ -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
- if @block
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
  require 'sshake/mock/command'
2
4
 
3
5
  module SSHake
@@ -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 ? @session.store : nil
15
+ @session&.store
16
16
  end
17
17
 
18
18
  def written_files
19
- @session ? @session.written_files : nil
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
@@ -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, :add_sudo => false)
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, options = nil, &block)
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 > 0
92
+ find_executed_commands(matcher).size.positive?
97
93
  end
94
+ # rubocop:enable Naming/PredicateName
98
95
 
99
96
  end
100
97
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sshake/error'
2
4
 
3
5
  module SSHake
@@ -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
@@ -2,24 +2,27 @@
2
2
 
3
3
  module SSHake
4
4
  class Response
5
- def initialize
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 == 0
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
@@ -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, *args)
26
+ def initialize(host, username = nil, **options)
21
27
  super
22
28
  @host = host
23
- @session_options = args
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
- log :debug, "Creating connection to #{@host}"
31
- log :debug, "Session options: #{@session_options.inspect}"
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
- log :debug, 'Closing connectiong'
71
+ klogger.debug 'Closing connection', id: @id, host: @host
51
72
  @session.close
52
- log :debug, 'Connection closed successfully'
73
+ klogger.debug 'Connection closed', id: @id, host: @host
53
74
  rescue StandardError => e
54
- log :debug, "Connection not closed: #{e.message} (#{e.class})"
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
- log :debug, "Attempting kill/shutdown of session"
84
+ klogger.debug 'Attemping to shutdown', id: @id, host: @host
64
85
  @session.shutdown!
65
- log :debug, "Session shutdown success"
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
- # Log the command
79
- log :info, "Executing: #{command_to_execute}"
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
- begin
83
- channel = nil
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
- if options.file_to_stream.nil?
95
- ch.eof!
96
- end
106
+ Timeout.timeout(options.timeout) do
107
+ channel = @session.open_channel do |ch|
108
+ response.start_time = Time.now
97
109
 
98
- ch.on_data do |_, data|
99
- response.stdout += data
100
- options.stdout&.call(data)
101
- log :debug, data.gsub(/[\r]/, '')
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
- ch.on_extended_data do |_, _, data|
105
- response.stderr += data.delete("\r")
106
- options.stderr&.call(data)
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
- ch.on_request('exit-status') do |_, data|
115
- response.exit_code = data.read_long&.to_i
116
- log :debug, "Exit code: #{response.exit_code}"
117
- end
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
- ch.on_request('exit-signal') do |_, data|
120
- response.exit_signal = data.read_long
121
- end
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
- if options.file_to_stream
124
- ch.on_process do |_, data|
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
- channel.wait
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SSHake
4
- VERSION = '1.0.2'
4
+
5
+ VERSION = '2.0.0'
6
+
5
7
  end
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: 1.0.2
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: 2020-07-04 00:00:00.000000000 Z
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/logger.rb
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
- post_install_message:
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: '0'
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.0.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: []
data/lib/sshake/logger.rb DELETED
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SSHake
4
- class << self
5
- attr_accessor :logger
6
- end
7
- end