sshake 1.0.0

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