shells 0.1.5

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,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: []