sshake 1.0.0 → 1.0.1

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
- SHA1:
3
- metadata.gz: 100663608c7ad37ad009f8e8390bb8529a310f67
4
- data.tar.gz: 5941b458f2d5157b8e94affd1da506c9208d2a78
2
+ SHA256:
3
+ metadata.gz: 708fa3da7960099e94421e102ad22c9b8243a586b5bc6b978b6a8083857f4a68
4
+ data.tar.gz: d6711b1debc1e0dacd0d1f0880e3508f3ce939e142036d647e6471ce920e2011
5
5
  SHA512:
6
- metadata.gz: 373f8dfeffe16e7287d08566587c63ecbc58fe99749e5acc21b176bf13eac1964b933c24d14e7eff1fd8f66df41543a4a099ae7e45652c8f579cf709fc6bbf93
7
- data.tar.gz: f0e6974235d398df3e19f8acc4cee2322148750f66f7591ff7a6de7b2afc44cc82c9ffa13726e9f424f352a8674cd10c8764d949d5e3cd1a2fd9080d924209d1
6
+ metadata.gz: 70f79029d3b42745a2661ea1627e3f8cd73201d50d6bf00abdb983e02663077fb3df4667ddc4d5dcf5920f4cfac2fb7b0927df7a7a8382a8354d65cd7f90c5ad
7
+ data.tar.gz: 60cdd9feff7600fecc5bec047eef5d7bd513baf9fcb4ba280b4c1f2e964e9e5e81c0a1828d7cfabe32c30216ea3b5893eb6cad4a1941fd7e21cffec6fa724a77
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sshake/session'
4
+ require 'sshake/version'
@@ -0,0 +1,114 @@
1
+ require 'securerandom'
2
+ require 'sshake/logger'
3
+ require 'sshake/execution_options'
4
+
5
+ module SSHake
6
+ class BaseSession
7
+ # A logger for this session
8
+ #
9
+ # @return [Logger, nil]
10
+ attr_accessor :logger
11
+
12
+ # An ID for this session
13
+ #
14
+ # @return [String]
15
+ attr_reader :id
16
+
17
+ # Specify the default behaviour for raising erors
18
+ #
19
+ # @return [Boolean]
20
+ attr_accessor :raise_on_error
21
+
22
+ def initialize(*args)
23
+ @id = SecureRandom.hex(4)
24
+ end
25
+
26
+ # Connect to the SSH server
27
+ #
28
+ # @return [void]
29
+ def connect
30
+ raise "Override #connect in sub-sessions"
31
+ end
32
+
33
+ # Is there an established SSH connection
34
+ #
35
+ # @return [Boolean]
36
+ def connected?
37
+ raise "Override #connected? in sub-sessions"
38
+ end
39
+
40
+ # Disconnect the underlying SSH connection
41
+ #
42
+ # @return [void]
43
+ def disconnect
44
+ raise "Override #disconnect in sub-sessions"
45
+ end
46
+
47
+ # Kill the underlying connection
48
+ def kill!
49
+ raise "Override #kill! in sub-sessions"
50
+ end
51
+
52
+ # Execute a command
53
+ #
54
+ def execute(commands, options = nil, &block)
55
+ raise "Override #execute in sub-sessions"
56
+ end
57
+
58
+ def write_data(path, data, options = nil, &block)
59
+ raise "Override #write_data in sub-sessions"
60
+ end
61
+
62
+ private
63
+
64
+ def add_sudo_to_commands_array(commands, user)
65
+ commands.map do |command|
66
+ "sudo -u #{user} --stdin #{command}"
67
+ end
68
+ end
69
+
70
+ def create_options(hash, block)
71
+ if block && hash
72
+ raise Error, 'You cannot provide a block and options'
73
+ elsif block
74
+ ExecutionOptions.from_block(&block)
75
+ elsif hash.is_a?(Hash)
76
+ ExecutionOptions.from_hash(hash)
77
+ else
78
+ ExecutionOptions.new
79
+ end
80
+ end
81
+
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
+ def prepare_commands(commands, execution_options, **options)
94
+ commands = [commands] unless commands.is_a?(Array)
95
+
96
+ # Map sudo onto command
97
+ if execution_options.sudo_user && options[:add_sudo] != false
98
+ commands = add_sudo_to_commands_array(commands, execution_options.sudo_user)
99
+ end
100
+
101
+ # Construct a full command string to execute
102
+ commands.join(' && ')
103
+ end
104
+
105
+ def handle_response(response, options)
106
+ if !response.success? && ((options.raise_on_error.nil? && @raise_on_error) || options.raise_on_error?)
107
+ raise ExecutionError, response
108
+ else
109
+ response
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -6,7 +6,7 @@ module SSHake
6
6
 
7
7
  class ExecutionError < Error
8
8
  def initialize(response)
9
- response
9
+ @response = response
10
10
  end
11
11
 
12
12
  attr_reader :response
@@ -41,6 +41,12 @@ module SSHake
41
41
  # @return [Proc]
42
42
  attr_accessor :stderr
43
43
 
44
+ # A file that you wish to stream to the remote channel
45
+ # with the current commend
46
+ #
47
+ # @return [File]
48
+ attr_accessor :file_to_stream
49
+
44
50
  # Should errors be raised
45
51
  #
46
52
  # @return [Boolean]
@@ -76,6 +82,7 @@ module SSHake
76
82
  options.stdin = hash[:stdin]
77
83
  options.stdout = hash[:stdout]
78
84
  options.stderr = hash[:stderr]
85
+ options.file_to_stream = hash[:file_to_stream]
79
86
  options
80
87
  end
81
88
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SSHake
2
4
  class ExecutionOptionsDSL
3
5
  def initialize(options)
@@ -13,8 +15,12 @@ module SSHake
13
15
  @options.sudo_password = options[:password]
14
16
  end
15
17
 
16
- def raise_on_error
17
- @options.raise_on_error = true
18
+ def raise_on_error(bool = true)
19
+ @options.raise_on_error = bool
20
+ end
21
+
22
+ def dont_raise_on_error
23
+ @options.raise_on_error = false
18
24
  end
19
25
 
20
26
  def stdin(value)
@@ -28,5 +34,9 @@ module SSHake
28
34
  def stderr(&block)
29
35
  @options.stderr = block
30
36
  end
37
+
38
+ def file_to_stream(file)
39
+ @options.file_to_stream = file
40
+ end
31
41
  end
32
42
  end
@@ -0,0 +1,36 @@
1
+ require 'sshake/response'
2
+
3
+ module SSHake
4
+ module Mock
5
+ class Command
6
+
7
+ def initialize(matcher, &block)
8
+ @matcher = matcher
9
+ @block = block
10
+ end
11
+
12
+ def match(command)
13
+ command = command.to_s
14
+ case @matcher
15
+ when String
16
+ @matcher == command ? [] : nil
17
+ when Regexp
18
+ if match = command.match(/\A#{@matcher}\z/)
19
+ match.captures
20
+ end
21
+ end
22
+ end
23
+
24
+ def make_response(environment)
25
+ response = SSHake::Response.new
26
+ response.start_time = Time.now
27
+ if @block
28
+ @block.call(response, environment)
29
+ end
30
+ response.finish_time = Time.now
31
+ response
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'sshake/mock/command'
2
+
3
+ module SSHake
4
+ module Mock
5
+ class CommandSet
6
+
7
+ def initialize
8
+ @commands = []
9
+ end
10
+
11
+ def add(matcher, &block)
12
+ command = Command.new(matcher, &block)
13
+ @commands << command
14
+ command
15
+ end
16
+
17
+ def match(given_command)
18
+ @commands.each do |command|
19
+ if matches = command.match(given_command)
20
+ return [command, matches]
21
+ end
22
+ end
23
+ nil
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module SSHake
2
+ module Mock
3
+ class Environment
4
+
5
+ def initialize(session)
6
+ @session = session
7
+ @captures = []
8
+ end
9
+
10
+ attr_accessor :command
11
+ attr_accessor :options
12
+ attr_accessor :captures
13
+
14
+ def store
15
+ @session ? @session.store : nil
16
+ end
17
+
18
+ def written_files
19
+ @session ? @session.written_files : nil
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module SSHake
2
+ module Mock
3
+ class ExecutedCommand
4
+
5
+ attr_reader :command
6
+ attr_reader :environment
7
+ attr_reader :response
8
+
9
+ def initialize(command, environment, response)
10
+ @command = command
11
+ @environment = environment
12
+ @response = response
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,101 @@
1
+ require 'net/ssh/errors'
2
+ require 'sshake/base_session'
3
+ require 'sshake/mock/command_set'
4
+ require 'sshake/mock/environment'
5
+ require 'sshake/mock/unsupported_command_error'
6
+ require 'sshake/mock/executed_command'
7
+
8
+ module SSHake
9
+ module Mock
10
+ class Session < BaseSession
11
+
12
+ attr_reader :command_set
13
+ attr_reader :store
14
+ attr_reader :written_files
15
+ attr_reader :executed_commands
16
+
17
+ def initialize(**options)
18
+ @options = options
19
+ @command_set = options[:command_set] || CommandSet.new
20
+ @executed_commands = []
21
+ @store = {}
22
+ @written_files = {}
23
+ @connected = false
24
+ yield(self) if block_given?
25
+ end
26
+
27
+ def connect
28
+ case @options[:connection_error]
29
+ when :timeout
30
+ raise Net::SSH::ConnectionTimeout
31
+ when :authentication_failed
32
+ raise Net::SSH::AuthenticationFailed
33
+ when :connection_refused
34
+ raise Errno::ECONNREFUSED
35
+ when :host_unreachable
36
+ raise Errno::EHOSTUNREACH
37
+ else
38
+ @connected = true
39
+ end
40
+ end
41
+
42
+ def connected?
43
+ @connected == true
44
+ end
45
+
46
+ def disconnect
47
+ @connected = false
48
+ end
49
+
50
+ def kill!
51
+ disconnect
52
+ end
53
+
54
+ def execute(commands, options = nil, &block)
55
+ connect unless connected?
56
+
57
+ environment = Environment.new(self)
58
+
59
+ environment.options = create_options(options, block)
60
+ environment.command = prepare_commands(commands, environment.options, :add_sudo => false)
61
+
62
+ command, environment.captures = @command_set.match(environment.command)
63
+
64
+ if command.nil?
65
+ raise UnsupportedCommandError.new(environment.command)
66
+ end
67
+
68
+ response = command.make_response(environment)
69
+
70
+ if environment.options.file_to_stream
71
+ response.bytes_streamed = environment.options.file_to_stream.size
72
+ end
73
+
74
+ @executed_commands << ExecutedCommand.new(command, environment, response)
75
+ handle_response(response, environment.options)
76
+ end
77
+
78
+ def write_data(path, data, options = nil, &block)
79
+ connect unless connected?
80
+ @written_files[path] = data
81
+ true
82
+ end
83
+
84
+ def find_executed_commands(matcher)
85
+ if matcher.is_a?(Regexp)
86
+ matcher = /\A#{matcher}\z/
87
+ else
88
+ matcher = /\A#{Regexp.escape(matcher.to_s)}\z/
89
+ end
90
+ @executed_commands.select do |command|
91
+ command.environment.command =~ matcher
92
+ end
93
+ end
94
+
95
+ def has_executed_command?(matcher)
96
+ find_executed_commands(matcher).size > 0
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,17 @@
1
+ require 'sshake/error'
2
+
3
+ module SSHake
4
+ module Mock
5
+ class UnsupportedCommandError < Error
6
+
7
+ def initialize(command)
8
+ @command = command
9
+ end
10
+
11
+ def to_s
12
+ "Executed command is not support by the mock session (`#{@command}`)"
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -5,6 +5,8 @@ module SSHake
5
5
  def initialize
6
6
  @stdout = ''
7
7
  @stderr = ''
8
+ @exit_code = 0
9
+ @bytes_streamed = 0
8
10
  end
9
11
 
10
12
  attr_accessor :command
@@ -14,6 +16,7 @@ module SSHake
14
16
  attr_accessor :exit_signal
15
17
  attr_accessor :start_time
16
18
  attr_accessor :finish_time
19
+ attr_accessor :bytes_streamed
17
20
 
18
21
  def success?
19
22
  @exit_code == 0
@@ -26,5 +29,9 @@ module SSHake
26
29
  def timeout?
27
30
  @exit_code == -255
28
31
  end
32
+
33
+ def timeout!
34
+ @exit_code = -255
35
+ end
29
36
  end
30
37
  end
@@ -4,26 +4,21 @@ require 'net/ssh'
4
4
  require 'net/sftp'
5
5
  require 'timeout'
6
6
  require 'sshake/error'
7
- require 'sshake/logger'
8
7
  require 'sshake/response'
9
- require 'sshake/execution_options'
8
+ require 'sshake/base_session'
10
9
 
11
10
  module SSHake
12
- class Session
11
+ class Session < BaseSession
13
12
  # The underlying net/ssh session
14
13
  #
15
14
  # @return [Net::SSH::Session]
16
15
  attr_reader :session
17
16
 
18
- # A logger for this session
19
- #
20
- # @return [Logger, nil]
21
- attr_accessor :logger
22
-
23
17
  # Create a new SSH session
24
18
  #
25
19
  # @return [Sshake::Session]
26
20
  def initialize(host, *args)
21
+ super
27
22
  @host = host
28
23
  @session_options = args
29
24
  end
@@ -32,6 +27,8 @@ module SSHake
32
27
  #
33
28
  # @return [void]
34
29
  def connect
30
+ log :debug, "Creating connection to #{@host}"
31
+ log :debug, "Session options: #{@session_options.inspect}"
35
32
  @session = Net::SSH.start(@host, *@session_options)
36
33
  true
37
34
  end
@@ -47,9 +44,14 @@ module SSHake
47
44
  #
48
45
  # @return [void]
49
46
  def disconnect
47
+ return false if @session.nil?
48
+
50
49
  begin
50
+ log :debug, 'Closing connectiong'
51
51
  @session.close
52
- rescue StandardError
52
+ log :debug, 'Connection closed successfully'
53
+ rescue StandardError => e
54
+ log :debug, "Connection not closed: #{e.message} (#{e.class})"
53
55
  nil
54
56
  end
55
57
  @session = nil
@@ -58,121 +60,106 @@ module SSHake
58
60
 
59
61
  # Kill the underlying connection
60
62
  def kill!
63
+ log :debug, "Attempting kill/shutdown of session"
61
64
  @session.shutdown!
65
+ log :debug, "Session shutdown success"
62
66
  @session = nil
63
67
  end
64
68
 
65
- # Execute a command
66
- #
67
69
  def execute(commands, options = nil, &block)
68
- commands = [commands] unless commands.is_a?(Array)
69
-
70
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"
71
+ command_to_execute = prepare_commands(commands, options)
82
72
 
83
73
  # Execute the command
84
74
  response = Response.new
85
- response.command = command
75
+ response.command = command_to_execute
86
76
  connect unless connected?
77
+
78
+ # Log the command
79
+ log :info, "Executing: #{command_to_execute}"
80
+ log :debug, "Timeout: #{options.timeout}"
81
+
87
82
  begin
88
83
  channel = nil
89
84
  Timeout.timeout(options.timeout) do
90
85
  channel = @session.open_channel do |ch|
91
86
  response.start_time = Time.now
92
- channel.exec(command) do |_, success|
93
- raise "Command \"#{command}\" was unable to execute" unless success
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
94
93
 
95
- ch.send_data(options.stdin) if options.stdin
96
- ch.eof!
94
+ if options.file_to_stream.nil?
95
+ ch.eof!
96
+ end
97
97
 
98
98
  ch.on_data do |_, data|
99
99
  response.stdout += data
100
100
  options.stdout&.call(data)
101
- log :debug, data.gsub(/[\r]/, ''), tab: 4
101
+ log :debug, data.gsub(/[\r]/, '')
102
102
  end
103
103
 
104
104
  ch.on_extended_data do |_, _, data|
105
105
  response.stderr += data.delete("\r")
106
106
  options.stderr&.call(data)
107
- log :warn, data, tab: 4
107
+ log :debug, data
108
108
  if data =~ /^\[sudo\] password for/
109
+ log :debug, 'Sending sudo password'
109
110
  ch.send_data "#{options.sudo_password}\n"
110
111
  end
111
112
  end
112
113
 
113
114
  ch.on_request('exit-status') do |_, data|
114
115
  response.exit_code = data.read_long&.to_i
115
- log :info, "\e[43;37m=> Exit code: #{response.exit_code}\e[0m"
116
+ log :debug, "Exit code: #{response.exit_code}"
116
117
  end
117
118
 
118
119
  ch.on_request('exit-signal') do |_, data|
119
120
  response.exit_signal = data.read_long
120
121
  end
122
+
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
131
+ ch.eof!
132
+ end
133
+ end
134
+ end
135
+ end
121
136
  end
122
137
  end
123
138
  channel.wait
124
139
  end
125
140
  rescue Timeout::Error => e
141
+ log :debug, "Got timeout error while executing command"
126
142
  kill!
127
- response.exit_code = -255
143
+ response.timeout!
128
144
  ensure
129
145
  response.finish_time = Time.now
130
146
  end
131
147
 
132
- if options.raise_on_error? && !response.success?
133
- raise ExecutionError, response
134
- else
135
- response
136
- end
148
+ handle_response(response, options)
137
149
  end
138
150
 
139
151
  def write_data(path, data, options = nil, &block)
140
152
  connect unless connected?
141
153
  tmp_path = "/tmp/sshake-tmp-file-#{SecureRandom.hex(32)}"
142
- @session.sftp.file.open(path, 'w') { |f| f.write(data) }
154
+ @session.sftp.file.open(tmp_path, 'w') do |f|
155
+ d = data.dup.force_encoding('BINARY')
156
+ until d.empty?
157
+ f.write(d.slice!(0, 1024))
158
+ end
159
+ end
143
160
  response = execute("mv #{tmp_path} #{path}", options, &block)
144
161
  response.success?
145
162
  end
146
163
 
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
164
  end
178
165
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SSHake
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sshake
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-14 00:00:00.000000000 Z
11
+ date: 2020-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-sftp
@@ -45,10 +45,18 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - lib/sshake.rb
49
+ - lib/sshake/base_session.rb
48
50
  - lib/sshake/error.rb
49
51
  - lib/sshake/execution_options.rb
50
52
  - lib/sshake/execution_options_dsl.rb
51
53
  - lib/sshake/logger.rb
54
+ - lib/sshake/mock/command.rb
55
+ - lib/sshake/mock/command_set.rb
56
+ - lib/sshake/mock/environment.rb
57
+ - lib/sshake/mock/executed_command.rb
58
+ - lib/sshake/mock/session.rb
59
+ - lib/sshake/mock/unsupported_command_error.rb
52
60
  - lib/sshake/response.rb
53
61
  - lib/sshake/session.rb
54
62
  - lib/sshake/version.rb
@@ -71,8 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
79
  - !ruby/object:Gem::Version
72
80
  version: '0'
73
81
  requirements: []
74
- rubyforge_project:
75
- rubygems_version: 2.5.2.3
82
+ rubygems_version: 3.0.3
76
83
  signing_key:
77
84
  specification_version: 4
78
85
  summary: A wrapper for net/ssh to make running commands more fun