shells 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,232 @@
1
+ require 'net/ssh'
2
+ require 'shells/shell_base'
3
+ require 'shells/bash_common'
4
+
5
+ module Shells
6
+ ##
7
+ # Executes an SSH session with a host.
8
+ #
9
+ # The default setup of this class should work well with any bash-like shell.
10
+ # In particular, the +exec_prompt+ method sets the "PS1" environment variable, which should set the prompt the shell
11
+ # uses, and the +get_exit_code+ methods retrieves the value of the "$?" variable which should contain the exit code
12
+ # from the last action. Because there is a possibility that your shell does not utilize those methods, the
13
+ # +override_set_prompt+ and +override_get_exit_code+ options are available to change the behavior.
14
+ #
15
+ #
16
+ # Valid options:
17
+ # +host+::
18
+ # The name or IP address of the host to connect to. Defaults to 'localhost'.
19
+ # +port+::
20
+ # The port on the host to connect to. Defaults to 22.
21
+ # +user+::
22
+ # The user to login with. This option is required.
23
+ # +password+::
24
+ # The password to login with.
25
+ # If our public key is an authorized key on the host, the password is ignored.
26
+ # +prompt+::
27
+ # The prompt used to determine when processes finish execution.
28
+ # Defaults to '~~#', but if that doesn't work for some reason because it is valid output from one or more
29
+ # commands, you can change it to something else. It must be unique and cannot contain certain characters.
30
+ # The characters you should avoid are !, $, \, /, ", and ' because no attempt is made to escape them and the
31
+ # resulting prompt can very easily become something else entirely. If they are provided, they will be
32
+ # replaced to protect the shell from getting stuck.
33
+ # +shell+::
34
+ # If set to :shell, then the default shell is executed.
35
+ # If set to anything else, it is assumed to be the executable path to the shell you want to run.
36
+ # +quit+::
37
+ # If set, this defines the command to execute when quitting the session.
38
+ # The default is "exit" which will probably work most of the time.
39
+ # +retrieve_exit_code+::
40
+ # If set to a non-false value, then the default behavior will be to retrieve the exit code from the shell after
41
+ # executing a command. If set to a false or nil value, the default behavior will be to ignore the exit code
42
+ # from the shell. When retrieved, the exit code is stored in the +last_exit_code+ property.
43
+ # This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
44
+ # +on_non_zero_exit_code+::
45
+ # If set to :ignore (the default) then non-zero exit codes will not cause errors. You will still be able to check
46
+ # the +last_exit_code+ property to determine if the command was successful.
47
+ # If set to :raise then non-zero exit codes will cause a Shells::NonZeroExitCode to be raised when a command exits
48
+ # with a non-zero return value.
49
+ # This option only comes into play when +retrieve_exit_code+ is set to a non-false value.
50
+ # This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
51
+ # +silence_timeout+::
52
+ # When a command is executing, this is the maximum amount of time to wait for any feedback from the shell.
53
+ # If set to 0 (or less) there is no timeout.
54
+ # Unlike +command_timeout+ this value resets every time we receive feedback.
55
+ # This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
56
+ # +command_timeout+::
57
+ # When a command is executing, this is the maximum amount of time to wait for the command to finish.
58
+ # If set to 0 (or less) there is no timeout.
59
+ # Unlike +silence_timeout+ this value does not reset when we receive feedback.
60
+ # This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
61
+ # +connect_timeout+::
62
+ # This is the maximum amount of time to wait for the initial connection to the SSH shell.
63
+ # +override_set_prompt+::
64
+ # If provided, this must be set to either a command string that will set the prompt, or a Proc that accepts
65
+ # the shell as an argument.
66
+ # If set to a string, the string is sent to the shell and we wait up to two seconds for the prompt to appear.
67
+ # If that fails, we resend the string and wait one more time before failing.
68
+ # If set to a Proc, the Proc is called. If the Proc returns a false value, we fail. If the Proc returns
69
+ # a non-false value, we consider it successful.
70
+ # +override_get_exit_code+::
71
+ # If provided, this must be set to either a command string that will retrieve the exit code, or a Proc that
72
+ # accepts the shell as an argument.
73
+ # If set to a string, the string is sent to the shell and the output is parsed as an integer and used as the exit
74
+ # code.
75
+ # If set to a Proc, the Proc is called and the return value of the proc is used as the exit code.
76
+ #
77
+ # Shells::SshSession.new(
78
+ # host: '10.10.10.10',
79
+ # user: 'somebody',
80
+ # password: 'super-secret'
81
+ # ) do |shell|
82
+ # shell.exec('cd /usr/local/bin')
83
+ # user_bin_files = shell.exec('ls -A1').split("\n")
84
+ # @app_is_installed = user_bin_files.include?('my_app')
85
+ # end
86
+ #
87
+ class SshSession < Shells::ShellBase
88
+
89
+ include Shells::BashCommon
90
+
91
+ ##
92
+ # The error raised when we failed to request a PTY.
93
+ class FailedToRequestPty < Shells::ShellError
94
+
95
+ end
96
+
97
+ ##
98
+ # The error raised when we fail to start the shell on the PTY.
99
+ class FailedToStartShell < Shells::ShellError
100
+
101
+ end
102
+
103
+
104
+ protected
105
+
106
+ def validate_options #:nodoc:
107
+ options[:host] ||= 'localhost'
108
+ options[:port] ||= 22
109
+ options[:shell] ||= :shell
110
+ options[:quit] ||= 'exit'
111
+ options[:connect_timeout] ||= 5
112
+
113
+ raise InvalidOption, 'Missing host.' if options[:host].to_s.strip == ''
114
+ raise InvalidOption, 'Missing user.' if options[:user].to_s.strip == ''
115
+ end
116
+
117
+ def exec_shell(&block) #:nodoc:
118
+
119
+ ignore_io_error = false
120
+ begin
121
+
122
+ Net::SSH.start(
123
+ options[:host],
124
+ options[:user],
125
+ password: options[:password],
126
+ port: options[:port],
127
+ non_interactive: true,
128
+ timeout: options[:connect_timeout]
129
+ ) do |ssh|
130
+
131
+ # open the channel
132
+ debug 'Opening channel...'
133
+ ssh.open_channel do |ch|
134
+ # request a PTY
135
+ debug 'Requesting PTY...'
136
+ ch.request_pty do |ch_pty, success_pty|
137
+ raise FailedToRequestPty unless success_pty
138
+
139
+ # pick a method to start the shell with.
140
+ meth = (options[:shell] == :shell) ? :send_channel_request : :exec
141
+
142
+ @channel = ch_pty
143
+ buffer_input
144
+
145
+ # start the shell
146
+ debug 'Starting shell...'
147
+ ch_pty.send(meth, options[:shell].to_s) do |ch_sh, success_sh|
148
+ raise FailedToStartShell unless success_sh
149
+
150
+ # give the shell a chance to get ready.
151
+ sleep 0.25
152
+
153
+ begin
154
+ # yield to the block
155
+ block.call
156
+
157
+ ensure
158
+ # send the exit command.
159
+ ignore_io_error = true
160
+ debug 'Closing connection...'
161
+ send_data options[:quit] + line_ending
162
+ end
163
+
164
+ @channel.wait
165
+ end
166
+
167
+ end
168
+
169
+ debug 'Waiting for channel to close...'
170
+ ch.wait
171
+ debug 'Channel has been closed.'
172
+ end
173
+
174
+ end
175
+ rescue IOError
176
+ unless ignore_io_error
177
+ raise
178
+ end
179
+ ensure
180
+ @channel = nil
181
+ end
182
+
183
+ end
184
+
185
+ def exec_prompt(&block) #:nodoc:
186
+ cmd = options[:override_set_prompt] || "PS1=\"#{options[:prompt]}\""
187
+ if cmd.respond_to?(:call)
188
+ raise Shells::FailedToSetPrompt unless cmd.call(self)
189
+ else
190
+ # set the prompt, wait up to 2 seconds for a response, then try one more time.
191
+ begin
192
+ exec cmd, command_timeout: 2, retrieve_exit_code: false
193
+ rescue Shells::CommandTimeout
194
+ begin
195
+ exec cmd, command_timeout: 2, retrieve_exit_code: false
196
+ rescue Shells::CommandTimeout
197
+ raise Shells::FailedToSetPrompt
198
+ end
199
+ end
200
+ end
201
+
202
+ # yield to the block
203
+ block.call
204
+ end
205
+
206
+ def send_data(data) #:nodoc:
207
+ @channel.send_data data
208
+ debug "Sent: (#{data.size} bytes) #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
209
+ end
210
+
211
+ def loop(&block) #:nodoc:
212
+ @channel.connection.loop(&block)
213
+ end
214
+
215
+ def stdout_received(&block) #:nodoc:
216
+ @channel.on_data do |_,data|
217
+ debug "Received: (#{data.size} bytes) #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
218
+ block.call data
219
+ end
220
+ end
221
+
222
+ def stderr_received(&block) #:nodoc:
223
+ @channel.on_extended_data do |_, type, data|
224
+ if type == 1
225
+ debug "Received: (#{data.size} bytes) [E] #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
226
+ block.call data
227
+ end
228
+ end
229
+ end
230
+
231
+ end
232
+ end
@@ -0,0 +1,5 @@
1
+ module Shells
2
+ ##
3
+ # The current version of the gem.
4
+ VERSION = "0.1.5"
5
+ end
data/lib/shells.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'shells/version'
2
+ require 'shells/errors'
3
+ require 'shells/shell_base'
4
+ require 'shells/ssh_session'
5
+ require 'shells/serial_session'
6
+ require 'shells/pf_sense_ssh_session'
7
+ require 'shells/pf_sense_serial_session'
8
+
9
+
10
+ ##
11
+ # A set of basic shell classes.
12
+ #
13
+ # All shell sessions can be accessed by class name without calling +new+.
14
+ # Shells::SshSession(host: ...)
15
+ # Shells::SerialSession(path: ...)
16
+ # Shells::PfSenseSshSession(host: ...)
17
+ # Shells::PfSenseSerialSession(path: ...)
18
+ #
19
+ module Shells
20
+
21
+ ##
22
+ # Provides the ability for the Shells module to allow sessions to be instantiated without calling +new+.
23
+ def self.method_missing(m, *args, &block) #:nodoc:
24
+
25
+ is_const = const_defined?(m) rescue nil
26
+
27
+ if is_const
28
+ val = const_get(m)
29
+ if val.is_a?(Class) && Shells::ShellBase > val
30
+ return val.new(*args, &block)
31
+ end
32
+ end
33
+
34
+ super
35
+ end
36
+
37
+ end
data/shells.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shells/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'shells'
8
+ spec.version = Shells::VERSION
9
+ spec.authors = ['Beau Barker']
10
+ spec.email = ['beau@barkerest.com']
11
+
12
+ spec.summary = 'A set of simple shells for interacting with other devices.'
13
+ spec.homepage = 'http://www.barkerest.com/'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'net-ssh', '~> 3.0.2'
24
+ spec.add_dependency 'rubyserial', '~> 0.4.0'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.14'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'minitest', '~> 5.0'
29
+ spec.add_development_dependency 'minitest-reporters'
30
+ spec.add_development_dependency 'pry'
31
+ end
32
+
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shells
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Beau Barker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyserial
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.4.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.4.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-reporters
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - beau@barkerest.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - bin/console
124
+ - bin/setup
125
+ - lib/shells.rb
126
+ - lib/shells/bash_common.rb
127
+ - lib/shells/errors.rb
128
+ - lib/shells/pf_sense_common.rb
129
+ - lib/shells/pf_sense_serial_session.rb
130
+ - lib/shells/pf_sense_ssh_session.rb
131
+ - lib/shells/serial_session.rb
132
+ - lib/shells/shell_base.rb
133
+ - lib/shells/ssh_session.rb
134
+ - lib/shells/version.rb
135
+ - shells.gemspec
136
+ homepage: http://www.barkerest.com/
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.6.11
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: A set of simple shells for interacting with other devices.
160
+ test_files: []