sshake 1.0.0

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