expect-behaviors 0.1.1
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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +99 -0
- data/LICENSE +22 -0
- data/README.md +16 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/examples/example_ssh_localhost.rb +36 -0
- data/expect-behaviors.gemspec +95 -0
- data/lib/expect/behavior.rb +134 -0
- data/lib/expect/match.rb +39 -0
- data/lib/expect/ssh.rb +161 -0
- data/lib/expect/timeout_error.rb +4 -0
- data/tasks/doc.rake +10 -0
- data/tasks/jeweler.rake +14 -0
- data/tasks/test.rake +32 -0
- data/test/acceptance/sshd/erb/ssh_config.erb +8 -0
- data/test/acceptance/sshd/erb/sshd_config.erb +65 -0
- data/test/acceptance/test_expect_behaviors_slow_tests.rb +109 -0
- data/test/acceptance/test_expect_ssh.rb +97 -0
- data/test/class_including_expect_behavior.rb +33 -0
- data/test/helper.rb +22 -0
- data/test/sshd.rb +139 -0
- data/test/unit/expect/test_expect_behaviors.rb +178 -0
- data/test/unit/expect/test_expect_match.rb +64 -0
- data/test/unit/expect/test_includer_class.rb +34 -0
- metadata +250 -0
data/lib/expect/match.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Expect
|
2
|
+
class Match
|
3
|
+
attr_reader :buffer, :success
|
4
|
+
|
5
|
+
def initialize(expression, buffer)
|
6
|
+
@expression = expression
|
7
|
+
@buffer = buffer
|
8
|
+
@matches = @buffer.match(@expression)
|
9
|
+
end
|
10
|
+
|
11
|
+
def exact_match_string
|
12
|
+
@matches.nil? ? nil : @matches[0]
|
13
|
+
end
|
14
|
+
|
15
|
+
def expr_substring_to_match
|
16
|
+
Regexp.new(".*?#{@expression.source}", @expression.options | Regexp::MULTILINE)
|
17
|
+
end
|
18
|
+
|
19
|
+
def nil?
|
20
|
+
@matches.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def substring_up_to_match
|
24
|
+
@matches.nil? ? nil : @buffer.match(expr_substring_to_match)[0]
|
25
|
+
end
|
26
|
+
alias_method :to_s, :substring_up_to_match
|
27
|
+
|
28
|
+
def substring_remainder
|
29
|
+
if @matches.nil?
|
30
|
+
@buffer
|
31
|
+
else
|
32
|
+
start_index = substring_up_to_match.length
|
33
|
+
@buffer[start_index..-1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
alias_method :remainder, :substring_remainder
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/expect/ssh.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'logger'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
require 'expect/behavior'
|
6
|
+
|
7
|
+
module Expect
|
8
|
+
|
9
|
+
##
|
10
|
+
# An SSH Accessor with expect-like behaviors. See also Expect::Behavior.
|
11
|
+
class SSH
|
12
|
+
include Expect::Behavior
|
13
|
+
# :Required methods to be created by the class mixing Expect::Behaviors :
|
14
|
+
# #exp_process - should do one iteration of handle input and append buffer
|
15
|
+
# #exp_buffer - provide the current buffer contents and empty it
|
16
|
+
|
17
|
+
attr_reader :auth_methods
|
18
|
+
|
19
|
+
def initialize(hostname, username,
|
20
|
+
# keyword args follow
|
21
|
+
port: 22,
|
22
|
+
password: nil, # password for login
|
23
|
+
ignore_known_hosts: false, # ignore host key mismatches?
|
24
|
+
key_file: nil, # path to private key file
|
25
|
+
logout_command: "exit", # command to exit/logout SSH session on remote host
|
26
|
+
wait_interval_sec: 0.1, # process interval
|
27
|
+
log_level: Logger::WARN)
|
28
|
+
@hostname = hostname
|
29
|
+
@username = username
|
30
|
+
@port = port
|
31
|
+
@password = password
|
32
|
+
@ignore_known_hosts = ignore_known_hosts
|
33
|
+
@key_file = key_file
|
34
|
+
@logout_command = logout_command
|
35
|
+
@wait_interval_sec = wait_interval_sec
|
36
|
+
@auth_methods = ['none', 'publickey', 'password']
|
37
|
+
@ssh = nil
|
38
|
+
@logger = Logger.new($stdout)
|
39
|
+
@logger.level = log_level if log_level
|
40
|
+
@receive_buffer = ''
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Transmit the contents of +command+ using the SSH @channel
|
45
|
+
def send_data(command)
|
46
|
+
@logger.debug("[Expect::SSH##{__method__}] [@hostname=#{@hostname}] [command=#{command}]")
|
47
|
+
command += "\n" unless command.end_with?("\n")
|
48
|
+
@channel.send_data(command)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Initiate SSH connection
|
53
|
+
def start
|
54
|
+
$stdout.puts(
|
55
|
+
"[Expect::SSH##{__method__}] [@hostname=#{@hostname}] [@username=#{@username}] [options=#{options}]"
|
56
|
+
)
|
57
|
+
@ssh = Net::SSH.start(@hostname, @username, options)
|
58
|
+
raise(RuntimeError, "[Expect::SSH##{__method__}]: SSH Start Failed") unless @ssh
|
59
|
+
@channel = request_channel_pty_shell
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Close SSH connection
|
64
|
+
def stop
|
65
|
+
@logger.debug("[Expect::SSH##{__method__}]: Closing Channel")
|
66
|
+
@channel.send_data(@logout_command + "\n")
|
67
|
+
@channel.close
|
68
|
+
begin
|
69
|
+
# A net-ssh quirk is that if you send a graceful close but you don't send an exit, it'll hang forever
|
70
|
+
# ...see also: http://stackoverflow.com/questions/25576454/ruby-net-ssh-script-not-closing
|
71
|
+
# I send an exit but just in case, also force the shutdown if it doesn't happen in 1 second. #NotPatient
|
72
|
+
Timeout::timeout(1) do
|
73
|
+
@logger.debug("[Expect::SSH##{__method__}]: Closing Session")
|
74
|
+
@ssh.close
|
75
|
+
end
|
76
|
+
rescue Timeout::Error
|
77
|
+
@logger.debug("[Expect::SSH##{__method__}]: FORCE Closing Session")
|
78
|
+
@ssh.shutdown!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# exp_buffer - provide the current buffer contents and empty it
|
84
|
+
def exp_buffer
|
85
|
+
result = @receive_buffer
|
86
|
+
@receive_buffer = ''
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# exp_process - should do one iteration of handle input and append buffer
|
92
|
+
def exp_process
|
93
|
+
sleep(@wait_sec.to_f)
|
94
|
+
@ssh.process(0)
|
95
|
+
end
|
96
|
+
|
97
|
+
################
|
98
|
+
private
|
99
|
+
################
|
100
|
+
|
101
|
+
##
|
102
|
+
# Sets up the channel, pty, and shell.
|
103
|
+
# Configures callbacks for handling incoming data.
|
104
|
+
def request_channel_pty_shell
|
105
|
+
channel = @ssh.open_channel do |channel|
|
106
|
+
request_pty(channel)
|
107
|
+
request_shell(channel)
|
108
|
+
register_callbacks(channel)
|
109
|
+
end
|
110
|
+
@logger.debug("[Expect::SSH##{__method__}] complete")
|
111
|
+
channel
|
112
|
+
end
|
113
|
+
|
114
|
+
def request_pty(channel)
|
115
|
+
@logger.debug("[Expect::SSH##{__method__}]: Requesting PTY")
|
116
|
+
channel.request_pty do |_ch, success|
|
117
|
+
raise(RuntimeError, "[Expect::SSH##{__method__}]: Unable to get PTY") unless success
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def request_shell(channel)
|
122
|
+
@logger.debug("[Expect::SSH##{__method__}]: Requesting Shell")
|
123
|
+
channel.send_channel_request("shell") do |_ch, success|
|
124
|
+
raise(RuntimeError, "[Expect::SSH##{__method__}]: Unable to get SHELL") unless success
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def register_callbacks(channel)
|
129
|
+
@logger.debug("[Expect::SSH##{__method__}]: Registering Callbacks")
|
130
|
+
channel.on_data do |_ch, data|
|
131
|
+
@logger.debug("[Expect::SSH] [on_data=#{data}]")
|
132
|
+
@receive_buffer << data
|
133
|
+
false
|
134
|
+
end
|
135
|
+
channel.on_extended_data do |_ch, type, data|
|
136
|
+
@logger.debug("[Expect::SSH] [on_extended_data=#{data}]")
|
137
|
+
@receive_buffer << data if type == 1
|
138
|
+
false
|
139
|
+
end
|
140
|
+
channel.on_close do
|
141
|
+
@logger.debug("[Expect::SSH]: Close Channel")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Construct the options hash to feed Net::SSH
|
147
|
+
def options
|
148
|
+
override_options = {
|
149
|
+
:auth_methods => auth_methods,
|
150
|
+
:logger => @logger,
|
151
|
+
:port => @port,
|
152
|
+
}
|
153
|
+
override_options[:keys] = [@key_file] if @key_file
|
154
|
+
override_options[:user_known_hosts_file] = '/dev/null' if @ignore_known_hosts
|
155
|
+
override_options[:password] = @password if @password
|
156
|
+
Net::SSH.configuration_for(@host).merge(override_options)
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
data/tasks/doc.rake
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rdoc/task'
|
2
|
+
|
3
|
+
Rake::RDocTask.new do |rdoc|
|
4
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
5
|
+
|
6
|
+
rdoc.rdoc_dir = 'rdoc'
|
7
|
+
rdoc.title = "expect-behaviors #{version}"
|
8
|
+
rdoc.rdoc_files.include('README*')
|
9
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
10
|
+
end
|
data/tasks/jeweler.rake
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
5
|
+
gem.name = "expect-behaviors"
|
6
|
+
gem.homepage = "http://github.com/francisluong/expect-behaviors"
|
7
|
+
gem.license = "MIT"
|
8
|
+
gem.summary = %Q{Ruby Mixin to add Expect Behaviors}
|
9
|
+
gem.description = %Q{Ruby Mixin to add Expect Behaviors to SSH/Serial/Telnet controllers}
|
10
|
+
gem.email = "me@francisluong.com"
|
11
|
+
gem.authors = ["Francis Luong (Franco)"]
|
12
|
+
# dependencies defined in Gemfile
|
13
|
+
end
|
14
|
+
Jeweler::RubygemsDotOrgTasks.new
|
data/tasks/test.rake
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
|
4
|
+
namespace :test do
|
5
|
+
|
6
|
+
test_libs = {
|
7
|
+
:default => ['lib', 'test']
|
8
|
+
}
|
9
|
+
|
10
|
+
test_suites = [:unit, :acceptance]
|
11
|
+
|
12
|
+
task :all do
|
13
|
+
test_suites.each do |suite_sym|
|
14
|
+
test_name = "test:#{suite_sym}"
|
15
|
+
Rake::Task[test_name].invoke
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
test_suites.each do |subtest_sym|
|
20
|
+
Rake::TestTask.new(subtest_sym) do |test|
|
21
|
+
libs = test_libs[subtest_sym]
|
22
|
+
libs ||= test_libs[:default]
|
23
|
+
test.libs += libs
|
24
|
+
test.pattern = "test/#{subtest_sym.to_s}/**/test_*.rb"
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Run All Tests"
|
32
|
+
task :test => 'test:all'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# $OpenBSD: sshd_config,v 1.89 2013/02/06 00:20:42 dtucker Exp $
|
2
|
+
|
3
|
+
# This is the sshd server system-wide configuration file. See
|
4
|
+
# sshd_config(5) for more information.
|
5
|
+
|
6
|
+
# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
7
|
+
|
8
|
+
# The strategy used for options in the default sshd_config shipped with
|
9
|
+
# OpenSSH is to specify options with their default value where
|
10
|
+
# possible, but leave them commented. Uncommented options override the
|
11
|
+
# default value.
|
12
|
+
|
13
|
+
# See sshd_config(5) for details on setting the Port and Listen values on Mac OS X
|
14
|
+
Port <%= @port %>
|
15
|
+
#AddressFamily any
|
16
|
+
ListenAddress <%= @address %>
|
17
|
+
#ListenAddress ::
|
18
|
+
|
19
|
+
# The default requires explicit activation of protocol 1
|
20
|
+
#Protocol 2
|
21
|
+
|
22
|
+
HostKey <%= SSHD_RSA_HOST_KEY_PATH %>
|
23
|
+
HostKey <%= SSHD_DSA_HOST_KEY_PATH %>
|
24
|
+
|
25
|
+
# Lifetime and size of ephemeral version 1 server key
|
26
|
+
#KeyRegenerationInterval 1h
|
27
|
+
#ServerKeyBits 1024
|
28
|
+
|
29
|
+
# Logging
|
30
|
+
# obsoletes QuietMode and FascistLogging
|
31
|
+
SyslogFacility AUTHPRIV
|
32
|
+
#LogLevel INFO
|
33
|
+
|
34
|
+
# Authentication:
|
35
|
+
|
36
|
+
#LoginGraceTime 2m
|
37
|
+
#PermitRootLogin yes
|
38
|
+
#StrictModes yes
|
39
|
+
#MaxAuthTries 6
|
40
|
+
#MaxSessions 10
|
41
|
+
|
42
|
+
#RSAAuthentication yes
|
43
|
+
PubkeyAuthentication yes
|
44
|
+
|
45
|
+
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
|
46
|
+
# but this is overridden so installations will only check .ssh/authorized_keys
|
47
|
+
AuthorizedKeysFile <%= SSHD_AUTHORIZED_KEYS_PATH %>
|
48
|
+
|
49
|
+
|
50
|
+
#AuthorizedPrincipalsFile none
|
51
|
+
|
52
|
+
#AuthorizedKeysCommand none
|
53
|
+
#AuthorizedKeysCommandUser nobody
|
54
|
+
|
55
|
+
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
|
56
|
+
#RhostsRSAAuthentication no
|
57
|
+
# similar for protocol version 2
|
58
|
+
#HostbasedAuthentication no
|
59
|
+
# Change to yes if you don't trust ~/.ssh/known_hosts for
|
60
|
+
# RhostsRSAAuthentication and HostbasedAuthentication
|
61
|
+
#IgnoreUserKnownHosts no
|
62
|
+
# Don't read the user's ~/.rhosts and ~/.shosts files
|
63
|
+
#IgnoreRhosts yes
|
64
|
+
|
65
|
+
PidFile <%= PIDFILE_PATH %>
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'expect/behavior'
|
3
|
+
require 'class_including_expect_behavior'
|
4
|
+
gem 'mocha'
|
5
|
+
require 'mocha/test_unit'
|
6
|
+
|
7
|
+
class TestExpectBehaviorsSlowTests < Test::Unit::TestCase
|
8
|
+
|
9
|
+
####################################
|
10
|
+
context "expect" do
|
11
|
+
setup do
|
12
|
+
@values = []
|
13
|
+
@values << "the sun is a mass"
|
14
|
+
@values << "\nof incandescent gas"
|
15
|
+
@values << "\nswitch-prompt#"
|
16
|
+
@values << "\nblip blip"
|
17
|
+
@values << "\nblah blah"
|
18
|
+
@values << "\nswitch-prompt2#"
|
19
|
+
@includer = ClassIncludingExpectBehavior.new(values: @values)
|
20
|
+
@wait_sec = 0.5
|
21
|
+
end
|
22
|
+
|
23
|
+
should "not timeout for switch-prompt2# if expect_continue for blip" do
|
24
|
+
@includer.wait_sec = @wait_sec * 1
|
25
|
+
result = @includer.expect do
|
26
|
+
when_matching(/switch-prompt2#/) do
|
27
|
+
@exp_match
|
28
|
+
end
|
29
|
+
when_matching(/blip/) do
|
30
|
+
exp_continue
|
31
|
+
end
|
32
|
+
when_timeout(@wait_sec * 4) do
|
33
|
+
"timed out"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
expected = "the sun is a mass\nof incandescent gas\nswitch-prompt#\nblip blip\nblah blah\nswitch-prompt2#"
|
37
|
+
assert_equal(expected, result.to_s)
|
38
|
+
end
|
39
|
+
|
40
|
+
should "timeout for switch-prompt2# if expect_continue for incandescent" do
|
41
|
+
@includer.wait_sec = @wait_sec * 1
|
42
|
+
result = @includer.expect do
|
43
|
+
when_matching(/switch-prompt2#/) do
|
44
|
+
@exp_match
|
45
|
+
end
|
46
|
+
when_matching(/incandescent/) do
|
47
|
+
exp_continue
|
48
|
+
end
|
49
|
+
when_timeout(@wait_sec * 3) do
|
50
|
+
"timed out"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
expected = "timed out"
|
54
|
+
assert_equal(expected, result.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
####################################
|
61
|
+
context "timeout" do
|
62
|
+
setup do
|
63
|
+
@wait_sec = 0.5
|
64
|
+
@includer = ClassIncludingExpectBehavior.new(wait: @wait_sec * 2)
|
65
|
+
end
|
66
|
+
|
67
|
+
should "raise TimeoutError when timeout is reached before match is found" do
|
68
|
+
@includer.exp_timeout_sec = 1
|
69
|
+
assert_raises(Expect::TimeoutError) do
|
70
|
+
@includer.expect do
|
71
|
+
when_matching(/bob/) do
|
72
|
+
return_value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
should "execute arbitrary block on timeout with override" do
|
79
|
+
result = @includer.expect do
|
80
|
+
when_matching(/bob/) do
|
81
|
+
return_value
|
82
|
+
end
|
83
|
+
when_timeout(@wait_sec * 1) do
|
84
|
+
"timeout"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
assert_equal('timeout', result)
|
88
|
+
end
|
89
|
+
|
90
|
+
should "revert to previous timeout after an expect block" do
|
91
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_sec))
|
92
|
+
result = @includer.expect do
|
93
|
+
when_matching(/bob/) { @exp_match }
|
94
|
+
when_timeout(1) { "timeout" }
|
95
|
+
end
|
96
|
+
assert_equal('timeout', result)
|
97
|
+
assert_equal(10, @includer.instance_variable_get(:@exp_timeout_sec))
|
98
|
+
@includer.exp_timeout_sec = 2
|
99
|
+
assert_equal(2, @includer.instance_variable_get(:@exp_timeout_sec))
|
100
|
+
result = @includer.expect do
|
101
|
+
when_matching(/bob/) { @exp_match }
|
102
|
+
when_timeout(1) { "timeout" }
|
103
|
+
end
|
104
|
+
assert_equal('timeout', result)
|
105
|
+
assert_equal(2, @includer.instance_variable_get(:@exp_timeout_sec))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|