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.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Expect
2
+ class TimeoutError < StandardError
3
+ end
4
+ end
@@ -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
@@ -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
@@ -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,8 @@
1
+ Port <%= @port %>
2
+ AddressFamily inet
3
+ IdentityFile <%= SSHD_CLIENT_KEY_PATH %>
4
+ GlobalKnownHostsFile /dev/null
5
+ GSSAPIAuthentication no
6
+ KbdInteractiveAuthentication no
7
+ PasswordAuthentication no
8
+ StrictHostKeyChecking no
@@ -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