shells 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/shells/bash_common.rb +170 -0
- data/lib/shells/errors.rb +57 -0
- data/lib/shells/pf_sense_common.rb +400 -0
- data/lib/shells/pf_sense_serial_session.rb +55 -0
- data/lib/shells/pf_sense_ssh_session.rb +56 -0
- data/lib/shells/serial_session.rb +184 -0
- data/lib/shells/shell_base.rb +846 -0
- data/lib/shells/ssh_session.rb +232 -0
- data/lib/shells/version.rb +5 -0
- data/lib/shells.rb +37 -0
- data/shells.gemspec +32 -0
- metadata +160 -0
@@ -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
|
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: []
|