sshake 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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