remote-exec 0.0.1 → 0.5.0
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 +4 -4
- data/lib/remote-exec.rb +4 -0
- data/lib/remote_exec/base.rb +70 -0
- data/lib/remote_exec/fake.rb +45 -0
- data/lib/remote_exec/local.rb +45 -0
- data/lib/remote_exec/ssh.rb +139 -0
- data/lib/remote_exec/version.rb +5 -0
- data/test/remote_exec/fake_test.rb +46 -0
- data/test/remote_exec/local_test.rb +29 -0
- data/test/remote_exec/ssh_test.rb +271 -0
- data/test/test_helper.rb +34 -0
- metadata +88 -8
- data/lib/remote/exec/fake.rb +0 -15
- data/lib/remote/exec/local.rb +0 -21
- data/lib/remote/exec/ssh.rb +0 -36
- data/lib/remote/exec/version.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca220332149439af73e75f9c632cf6387ae16970
|
4
|
+
data.tar.gz: 7f5b2ac70fb84011a475b9bfee2d4e079103ec2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa812243644b3eebfda7b71741ba1f9556090835cc7759b1a84e09edbbbfbbd9f5f904e2a07e792f598cc876b94170bddebe8f4c561d51cfb68204454bb55e82
|
7
|
+
data.tar.gz: f731278f988714a0232ee75ca89653caae39519f0f04ea25a1f9b1ddd84e7fad2898aa227a7d676e04954d4bf5cbba3bf08230da4d7956aaa94a935a85528b50
|
data/lib/remote-exec.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright 2014 Michal Papis <mpapis@gmail.com>
|
3
|
+
|
4
|
+
See the file LICENSE for copying permission.
|
5
|
+
=end
|
6
|
+
|
7
|
+
require 'ruby/hooks'
|
8
|
+
require "remote_exec/version"
|
9
|
+
|
10
|
+
# Define minimal interface for execution handlers
|
11
|
+
class RemoteExec::Base
|
12
|
+
extend Ruby::Hooks::InstanceHooks
|
13
|
+
|
14
|
+
# called before connection attempt will be made, used when connection may fail
|
15
|
+
# @param object [Object] the target that invoked the method
|
16
|
+
define_hook :before_connect
|
17
|
+
|
18
|
+
# called when connection attempt failed and we are about to sleep and retry
|
19
|
+
# @param object [Object] the target that invoked the method
|
20
|
+
# @param exception [Exception] exception that made the connection attempt fail
|
21
|
+
# @param retries [Integer] number of retries left
|
22
|
+
define_hook :on_connect_retry
|
23
|
+
|
24
|
+
# called when connection attempt failed and we no more retries left
|
25
|
+
# @param object [Object] the target that invoked the method
|
26
|
+
# @param exception [Exception] exception that made the connection attempt fail
|
27
|
+
define_hook :on_connect_fail
|
28
|
+
|
29
|
+
# called after connection / session is esatablished
|
30
|
+
# @param object [Object] the target that invoked the method
|
31
|
+
define_hook :after_connect
|
32
|
+
|
33
|
+
# called before terminating connection - only when needed
|
34
|
+
# @param object [Object] the target that invoked the method
|
35
|
+
define_hook :before_shutdown
|
36
|
+
|
37
|
+
# called before executing command
|
38
|
+
# @param object [Object] the target that invoked the method
|
39
|
+
# @param command [String] the command to execute
|
40
|
+
define_hook :before_execute
|
41
|
+
|
42
|
+
# called before executing command
|
43
|
+
# @param object [Object] the target that invoked the method
|
44
|
+
# @param stdout [String] standard output of the command, can be nil
|
45
|
+
# @param stderr [String] standard error output of the command, can be nil
|
46
|
+
define_hook :on_execute_data
|
47
|
+
|
48
|
+
# called after executing command
|
49
|
+
# @param object [Object] the target that invoked the method
|
50
|
+
# @param command [String] the executed command
|
51
|
+
# @param result [Integer] the executed command status code (0 - ok, >0 - fail)
|
52
|
+
define_hook :after_execute
|
53
|
+
|
54
|
+
# standard in place handler that ensures shutdown is called
|
55
|
+
def initialize
|
56
|
+
if block_given?
|
57
|
+
begin
|
58
|
+
yield self
|
59
|
+
ensure
|
60
|
+
shutdown
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# minimal handler for shutdown
|
66
|
+
def shutdown
|
67
|
+
before_shutdown.changed_and_notify(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "remote_exec/base"
|
2
|
+
|
3
|
+
# Class to fake running commands and transfering files.
|
4
|
+
class RemoteExec::Fake < RemoteExec::Base
|
5
|
+
|
6
|
+
##
|
7
|
+
# The story to tell in +execute+, take an array
|
8
|
+
#
|
9
|
+
# @example usage
|
10
|
+
#
|
11
|
+
# [1, [[nil,"error\n"]]
|
12
|
+
#
|
13
|
+
# consist of an array: [ return_status, [[ stdout, stderr],...] ]
|
14
|
+
|
15
|
+
attr_accessor :story
|
16
|
+
|
17
|
+
# Constructs a new Fake object.
|
18
|
+
#
|
19
|
+
# @yield [self] if a block is given then the constructed
|
20
|
+
# object yields itself and calls `#shutdown` at the end, closing the
|
21
|
+
# remote connection
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
after_connect.changed_and_notify(self)
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Execute fake command
|
30
|
+
#
|
31
|
+
# @param command [String] command string to execute
|
32
|
+
# @return [Integer] exit status of the command
|
33
|
+
|
34
|
+
def execute(command)
|
35
|
+
before_execute.changed_and_notify(self, command)
|
36
|
+
last_status, outputs = @story
|
37
|
+
outputs.each do |out, err|
|
38
|
+
on_execute_data.changed_and_notify(self, out, err)
|
39
|
+
yield(out, err) if block_given?
|
40
|
+
end
|
41
|
+
after_execute.changed_and_notify(self, command, last_status)
|
42
|
+
last_status
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'session'
|
2
|
+
require "remote_exec/base"
|
3
|
+
|
4
|
+
# Class to run local commands and transfer files localy.
|
5
|
+
class RemoteExec::Local < RemoteExec::Base
|
6
|
+
# name of the shell to run
|
7
|
+
attr_reader :shell
|
8
|
+
|
9
|
+
# Constructs a new Local object.
|
10
|
+
#
|
11
|
+
# @param shell [String] name of the shell to run
|
12
|
+
# @yield [self] if a block is given then the constructed
|
13
|
+
# object yields itself and calls `#shutdown` at the end, closing the
|
14
|
+
# remote connection
|
15
|
+
|
16
|
+
def initialize(shell = "sh")
|
17
|
+
@shell = shell
|
18
|
+
super()
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Execute command locally
|
23
|
+
#
|
24
|
+
# @param command [String] command string to execute
|
25
|
+
# @return [Integer] exit status of the command
|
26
|
+
|
27
|
+
def execute(command)
|
28
|
+
before_execute.changed_and_notify(self, command)
|
29
|
+
shell_session.execute(command) do |out,err|
|
30
|
+
on_execute_data.changed_and_notify(self, out, err)
|
31
|
+
yield(out, err) if block_given?
|
32
|
+
end
|
33
|
+
last_status = shell_session.status
|
34
|
+
after_execute.changed_and_notify(self, command, last_status)
|
35
|
+
last_status
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def shell_session
|
41
|
+
@shell_session ||= Session::Sh.new(:prog => shell).tap do |shell|
|
42
|
+
after_connect.changed_and_notify(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright 2014 Michal Papis <mpapis@gmail.com>
|
3
|
+
|
4
|
+
See the file LICENSE for copying permission.
|
5
|
+
|
6
|
+
Partially based on test-kitchen by Fletcher Nichol <fnichol@nichol.ca>
|
7
|
+
License: https://github.com/test-kitchen/test-kitchen/blob/459238b88c/LICENSE
|
8
|
+
=end
|
9
|
+
|
10
|
+
require 'net/ssh'
|
11
|
+
require 'ruby/hooks'
|
12
|
+
require "remote_exec/base"
|
13
|
+
|
14
|
+
# Class to help establish SSH connections, issue remote commands, and
|
15
|
+
# transfer files between a local system and remote node.
|
16
|
+
class RemoteExec::Ssh < RemoteExec::Base
|
17
|
+
# hostname for the connection
|
18
|
+
attr_reader :hostname
|
19
|
+
# username for the connection
|
20
|
+
attr_reader :username
|
21
|
+
# options for the connection
|
22
|
+
attr_accessor :options
|
23
|
+
|
24
|
+
# Constructs a new Ssh object.
|
25
|
+
#
|
26
|
+
# @param hostname [String] the remote hostname (IP address, FQDN, etc.)
|
27
|
+
# @param username [String] the username for the remote host
|
28
|
+
# @param options [Hash] configuration options for ssh
|
29
|
+
# @yield [self] if a block is given then the constructed
|
30
|
+
# object yields itself and calls `#shutdown` at the end, closing the
|
31
|
+
# remote connection
|
32
|
+
def initialize(hostname, username, options = {})
|
33
|
+
@hostname = hostname
|
34
|
+
@username = username
|
35
|
+
@options = options
|
36
|
+
super()
|
37
|
+
end
|
38
|
+
|
39
|
+
# Shuts down the session connection, if it is still active.
|
40
|
+
def shutdown
|
41
|
+
super
|
42
|
+
return if @ssh.nil?
|
43
|
+
ssh.shutdown!
|
44
|
+
ensure
|
45
|
+
@ssh = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Execute command on remote host
|
50
|
+
#
|
51
|
+
# @param command [String] command string to execute
|
52
|
+
# @return [Integer] exit status of the command
|
53
|
+
|
54
|
+
def execute(command)
|
55
|
+
# TODO: make it run in one session
|
56
|
+
@last_status = nil
|
57
|
+
@command = command
|
58
|
+
ssh.open_channel(&method(:execute_open_channel))
|
59
|
+
ssh.loop
|
60
|
+
@last_status
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def execute_open_channel(channel)
|
66
|
+
before_execute.changed_and_notify(self, @command)
|
67
|
+
channel.request_pty
|
68
|
+
channel.exec(@command, &method(:execute_channel_exec))
|
69
|
+
channel.wait
|
70
|
+
after_execute.changed_and_notify(self, @command, @last_status)
|
71
|
+
end
|
72
|
+
|
73
|
+
def execute_channel_exec(channel, success)
|
74
|
+
channel.on_data(&method(:execute_on_stdout))
|
75
|
+
channel.on_extended_data(&method(:execute_on_stderr))
|
76
|
+
channel.on_request("exit-status") do |channel, data|
|
77
|
+
@last_status = data.read_long
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def execute_on_stdout(channel, data)
|
82
|
+
on_execute_data.changed_and_notify(self, data, nil)
|
83
|
+
yield(data, nil) if block_given?
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute_on_stderr(channel, type, data)
|
87
|
+
case type
|
88
|
+
when 1
|
89
|
+
on_execute_data.changed_and_notify(self, nil, data)
|
90
|
+
yield(nil, data) if block_given?
|
91
|
+
else
|
92
|
+
raise "Unsupported SSH extended_data type: #{type.inspect}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def ssh
|
97
|
+
@ssh ||= establish_connection
|
98
|
+
end
|
99
|
+
|
100
|
+
RESCUE_EXCEPTIONS = [
|
101
|
+
Errno::EACCES,
|
102
|
+
Errno::EADDRINUSE,
|
103
|
+
Errno::ECONNREFUSED,
|
104
|
+
Errno::ECONNRESET,
|
105
|
+
Errno::ENETUNREACH,
|
106
|
+
Errno::EHOSTUNREACH,
|
107
|
+
Net::SSH::Disconnect,
|
108
|
+
]
|
109
|
+
|
110
|
+
# Establish a connection session to the remote host.
|
111
|
+
#
|
112
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
113
|
+
# @api private
|
114
|
+
def establish_connection
|
115
|
+
@retries = options[:ssh_retries] || 2
|
116
|
+
begin
|
117
|
+
before_connect.changed_and_notify(self)
|
118
|
+
ssh = Net::SSH.start(hostname, username, options)
|
119
|
+
rescue *RESCUE_EXCEPTIONS => exception
|
120
|
+
handle_exception_retry(exception)
|
121
|
+
retry
|
122
|
+
end
|
123
|
+
after_connect.changed_and_notify(self)
|
124
|
+
ssh
|
125
|
+
end
|
126
|
+
|
127
|
+
def handle_exception_retry(exception)
|
128
|
+
if @retries > 0
|
129
|
+
on_connect_retry.changed_and_notify(self, exception, @retries)
|
130
|
+
sleep options[:ssh_timeout] || 1
|
131
|
+
@retries -= 1
|
132
|
+
else
|
133
|
+
on_connect_fail.changed_and_notify(self, exception)
|
134
|
+
# TODO: should we wrap the error in some other common class?
|
135
|
+
raise exception
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'remote_exec/fake'
|
3
|
+
|
4
|
+
describe RemoteExec::Fake do
|
5
|
+
subject do
|
6
|
+
RemoteExec::Fake.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "runs true" do
|
10
|
+
test_command = "true"
|
11
|
+
subject.story = [0,[]]
|
12
|
+
called = 0
|
13
|
+
status =
|
14
|
+
subject.execute(test_command) do |out, err|
|
15
|
+
called+=1
|
16
|
+
end
|
17
|
+
called.must_equal(0)
|
18
|
+
status.must_equal(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "runs false" do
|
22
|
+
test_command = "false"
|
23
|
+
subject.story = [1,[]]
|
24
|
+
called = 0
|
25
|
+
status =
|
26
|
+
subject.execute(test_command) do |out, err|
|
27
|
+
called+=1
|
28
|
+
end
|
29
|
+
called.must_equal(0)
|
30
|
+
status.must_equal(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "runs echo test" do
|
34
|
+
test_command = "echo test"
|
35
|
+
subject.story = [0,[["test\n",nil]]]
|
36
|
+
called = 0
|
37
|
+
status =
|
38
|
+
subject.execute(test_command) do |out, err|
|
39
|
+
out.must_equal "test\n"
|
40
|
+
err.must_be_nil
|
41
|
+
called+=1
|
42
|
+
end
|
43
|
+
called.must_equal(1)
|
44
|
+
status.must_equal(0)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'remote_exec/local'
|
3
|
+
|
4
|
+
describe RemoteExec::Local do
|
5
|
+
subject do
|
6
|
+
RemoteExec::Local.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "runs true" do
|
10
|
+
subject.execute("true").must_equal(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "runs false" do
|
14
|
+
subject.execute("false").must_equal(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "runs echo test" do
|
18
|
+
test_command = "echo test"
|
19
|
+
@called = 0
|
20
|
+
status =
|
21
|
+
subject.execute(test_command) do |out, err|
|
22
|
+
assert_equal out.strip, "test"
|
23
|
+
assert_equal err, nil
|
24
|
+
@called+=1
|
25
|
+
end
|
26
|
+
@called.must_equal(1)
|
27
|
+
status.must_equal(0)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright 2014 Michal Papis <mpapis@gmail.com>
|
3
|
+
|
4
|
+
See the file LICENSE for copying permission.
|
5
|
+
|
6
|
+
Partially based on test-kitchen by Fletcher Nichol <fnichol@nichol.ca>
|
7
|
+
License: https://github.com/test-kitchen/test-kitchen/blob/459238b88c/LICENSE
|
8
|
+
=end
|
9
|
+
|
10
|
+
require 'test_helper'
|
11
|
+
require 'remote_exec/ssh'
|
12
|
+
require 'net/ssh/test'
|
13
|
+
|
14
|
+
module Net
|
15
|
+
module SSH
|
16
|
+
module Test
|
17
|
+
class Channel
|
18
|
+
|
19
|
+
def sends_request_pty
|
20
|
+
pty_data = ["xterm", 80, 24, 640, 480, "\0"]
|
21
|
+
script.events << Class.new(Net::SSH::Test::LocalPacket) do
|
22
|
+
def types
|
23
|
+
if
|
24
|
+
@type == 98 && @data[1] == "pty-req"
|
25
|
+
then
|
26
|
+
@types ||= [
|
27
|
+
:long, :string, :bool, :string,
|
28
|
+
:long, :long, :long, :long, :string
|
29
|
+
]
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end.new(:channel_request, remote_id, "pty-req", false, *pty_data)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ErrorCounter
|
43
|
+
attr_reader :errors
|
44
|
+
def on_error(method, *args)
|
45
|
+
@errors ||= {}
|
46
|
+
@errors[method] ||= 0
|
47
|
+
@errors[method] += 1
|
48
|
+
end
|
49
|
+
def on_connect_retry(*args)
|
50
|
+
on_error(:on_connect_retry, *args)
|
51
|
+
end
|
52
|
+
def on_connect_fail(*args)
|
53
|
+
on_error(:on_connect_fail, *args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class ExecutaDataHook < Struct.new(:object, :stdout, :stderr)
|
58
|
+
attr_reader :results
|
59
|
+
def initialize(*args)
|
60
|
+
@results = []
|
61
|
+
super
|
62
|
+
end
|
63
|
+
def update(*args)
|
64
|
+
@results << self.class.new(*args)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe RemoteExec::Ssh do
|
69
|
+
include Net::SSH::Test
|
70
|
+
|
71
|
+
subject do
|
72
|
+
RemoteExec::Ssh.allocate.tap do |ssh|
|
73
|
+
ssh.instance_variable_set(:@ssh, connection)
|
74
|
+
ssh.instance_variable_set(:@options, {})
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#initialize" do
|
79
|
+
|
80
|
+
it "sets default variables" do
|
81
|
+
subject.send(:initialize, 1, 2, 3)
|
82
|
+
subject.hostname.must_equal 1
|
83
|
+
subject.username.must_equal 2
|
84
|
+
subject.options.must_equal 3
|
85
|
+
end
|
86
|
+
|
87
|
+
it "executes initialize block once" do
|
88
|
+
calls = 0
|
89
|
+
subject.send(:initialize, 1, 2, 3) { calls+=1 }
|
90
|
+
calls.must_equal 1
|
91
|
+
end
|
92
|
+
|
93
|
+
end #initialize
|
94
|
+
|
95
|
+
describe "#execute" do
|
96
|
+
let(:hook) { ExecutaDataHook.new }
|
97
|
+
|
98
|
+
it "executes true" do
|
99
|
+
story do |session|
|
100
|
+
channel = session.opens_channel
|
101
|
+
channel.sends_request_pty
|
102
|
+
channel.sends_exec "true"
|
103
|
+
channel.gets_exit_status(0)
|
104
|
+
channel.gets_close
|
105
|
+
channel.sends_close
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_scripted do
|
109
|
+
subject.execute("true").must_equal 0
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it "executes false" do
|
114
|
+
story do |session|
|
115
|
+
channel = session.opens_channel
|
116
|
+
channel.sends_request_pty
|
117
|
+
channel.sends_exec "false"
|
118
|
+
channel.gets_exit_status(1)
|
119
|
+
channel.gets_close
|
120
|
+
channel.sends_close
|
121
|
+
end
|
122
|
+
|
123
|
+
assert_scripted do
|
124
|
+
subject.execute("false").must_equal 1
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "executes echo test" do
|
129
|
+
story do |session|
|
130
|
+
channel = session.opens_channel
|
131
|
+
channel.sends_request_pty
|
132
|
+
channel.sends_exec "echo test me"
|
133
|
+
channel.gets_data("test me\n")
|
134
|
+
channel.gets_exit_status(0)
|
135
|
+
channel.gets_close
|
136
|
+
channel.sends_close
|
137
|
+
end
|
138
|
+
|
139
|
+
assert_scripted do
|
140
|
+
subject.on_execute_data.add_observer(hook, :update)
|
141
|
+
subject.execute("echo test me") do |out, err|
|
142
|
+
out.must_equal "test me\n"
|
143
|
+
err.must_be_nil
|
144
|
+
end.must_equal 0
|
145
|
+
hook.results.must_equal([ExecutaDataHook.new(subject, "test me\n", nil)])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "executes echo test>&2" do
|
150
|
+
story do |session|
|
151
|
+
channel = session.opens_channel
|
152
|
+
channel.sends_request_pty
|
153
|
+
channel.sends_exec "echo test me>&2"
|
154
|
+
channel.gets_extended_data("test me\n")
|
155
|
+
channel.gets_exit_status(0)
|
156
|
+
channel.gets_close
|
157
|
+
channel.sends_close
|
158
|
+
end
|
159
|
+
|
160
|
+
assert_scripted do
|
161
|
+
subject.on_execute_data.add_observer(hook, :update)
|
162
|
+
subject.execute("echo test me>&2") do |out, err|
|
163
|
+
out.must_be_nil
|
164
|
+
err.must_equal "test me\n"
|
165
|
+
end.must_equal 0
|
166
|
+
hook.results.must_equal([ExecutaDataHook.new(subject, nil, "test me\n")])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end #execute
|
171
|
+
|
172
|
+
describe "#execute methods" do
|
173
|
+
let(:hook) { ExecutaDataHook.new }
|
174
|
+
|
175
|
+
it "handles stdout" do
|
176
|
+
subject.on_execute_data.add_observer(hook, :update)
|
177
|
+
subject.send(:execute_on_stdout, :channel, "some text") do |stdout, stderr|
|
178
|
+
stdout.must_equal("some text")
|
179
|
+
stderr.must_be_nil
|
180
|
+
end
|
181
|
+
hook.results.must_equal([ExecutaDataHook.new(subject, "some text", nil)])
|
182
|
+
end
|
183
|
+
|
184
|
+
it "handles stderr" do
|
185
|
+
subject.on_execute_data.add_observer(hook, :update)
|
186
|
+
subject.send(:execute_on_stderr, :channel, 1, "some text") do |stdout, stderr|
|
187
|
+
stdout.must_be_nil
|
188
|
+
stderr.must_equal("some text")
|
189
|
+
end
|
190
|
+
hook.results.must_equal([ExecutaDataHook.new(subject, nil, "some text")])
|
191
|
+
end
|
192
|
+
|
193
|
+
it "does not handle extended data other then stderr" do
|
194
|
+
subject.on_execute_data.add_observer(hook, :update)
|
195
|
+
lambda {
|
196
|
+
subject.send(:execute_on_stderr, :channel, 666, "some text")
|
197
|
+
}.must_raise(RuntimeError, "Unsupported SSH extended_data type: 666")
|
198
|
+
hook.results.must_be_empty
|
199
|
+
end
|
200
|
+
|
201
|
+
end #execute methods
|
202
|
+
|
203
|
+
describe "#establish_connection" do
|
204
|
+
it "does connect" do
|
205
|
+
Net::SSH.unstub(:start)
|
206
|
+
Net::SSH.stubs(:start).returns(connection)
|
207
|
+
subject.send(:establish_connection).must_equal(connection)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "exception in establishing connection" do
|
212
|
+
|
213
|
+
[
|
214
|
+
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
|
215
|
+
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
216
|
+
Net::SSH::Disconnect
|
217
|
+
].each do |klass|
|
218
|
+
describe "raising #{klass}" do
|
219
|
+
|
220
|
+
before do
|
221
|
+
@error_counter = ErrorCounter.new
|
222
|
+
Net::SSH.unstub(:start)
|
223
|
+
Net::SSH.stubs(:start).raises(klass)
|
224
|
+
subject.instance_variable_set(:@ssh, nil)
|
225
|
+
subject.options[:ssh_retries] = 2
|
226
|
+
end
|
227
|
+
|
228
|
+
it "reraises the #{klass} exception" do
|
229
|
+
subject.stubs(:sleep)
|
230
|
+
proc { subject.send(:establish_connection) }.must_raise klass
|
231
|
+
end
|
232
|
+
|
233
|
+
it "sleeps for 1 second between retries" do
|
234
|
+
subject.expects(:sleep).with(1).twice
|
235
|
+
begin
|
236
|
+
subject.send(:establish_connection)
|
237
|
+
rescue
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
it "calls hooks on retry/fail ':ssh_retries' times" do
|
242
|
+
subject.stubs(:sleep)
|
243
|
+
subject.on_connect_retry.add_observer(@error_counter, :on_connect_retry)
|
244
|
+
subject.on_connect_fail.add_observer(@error_counter, :on_connect_fail)
|
245
|
+
begin
|
246
|
+
subject.send(:establish_connection)
|
247
|
+
rescue
|
248
|
+
end
|
249
|
+
@error_counter.errors.must_equal({:on_connect_retry=>2, :on_connect_fail=>1})
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
end #"exception in establishing connection"
|
256
|
+
|
257
|
+
describe "#handle_exception_retry" do
|
258
|
+
it "does decrease reties count" do
|
259
|
+
subject.instance_variable_set(:@retries, 2)
|
260
|
+
subject.send(:handle_exception_retry, "exception_test")
|
261
|
+
subject.instance_variable_get(:@retries).must_equal(1)
|
262
|
+
subject.send(:handle_exception_retry, "exception_test")
|
263
|
+
subject.instance_variable_get(:@retries).must_equal(0)
|
264
|
+
lambda {
|
265
|
+
subject.send(:handle_exception_retry, "exception_test")
|
266
|
+
}.must_raise(RuntimeError, "exception_test")
|
267
|
+
subject.instance_variable_get(:@retries).must_equal(0)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright 2014 Michal Papis <mpapis@gmail.com>
|
3
|
+
|
4
|
+
See the file LICENSE for copying permission.
|
5
|
+
=end
|
6
|
+
|
7
|
+
require "rubygems"
|
8
|
+
gem "minitest"
|
9
|
+
|
10
|
+
if
|
11
|
+
RUBY_VERSION == "2.0.0" && # check Gemfile
|
12
|
+
$0 != "-e" # do not do that in guard
|
13
|
+
then
|
14
|
+
require "coveralls"
|
15
|
+
require "simplecov"
|
16
|
+
|
17
|
+
SimpleCov.start do
|
18
|
+
formatter SimpleCov::Formatter::MultiFormatter[
|
19
|
+
SimpleCov::Formatter::HTMLFormatter,
|
20
|
+
Coveralls::SimpleCov::Formatter,
|
21
|
+
]
|
22
|
+
command_name "Unit Tests"
|
23
|
+
add_filter "/test/"
|
24
|
+
end
|
25
|
+
|
26
|
+
Coveralls.noisy = true unless ENV["CI"]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Autoload all lib/**/*.rb files so simplecov does not misses anything
|
30
|
+
Dir[File.expand_path("../../lib/**/*.rb", __FILE__)].each{|f| require f }
|
31
|
+
|
32
|
+
require "minitest/autorun" unless $0=="-e" # skip in guard
|
33
|
+
require "minitest/unit"
|
34
|
+
require "mocha/setup"
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remote-exec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michal Papis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ruby-hooks
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: session
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,34 @@ dependencies:
|
|
38
52
|
- - '>='
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-yard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
84
|
name: rake
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +94,34 @@ dependencies:
|
|
52
94
|
- - '>='
|
53
95
|
- !ruby/object:Gem::Version
|
54
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
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
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: mocha
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
55
125
|
description: Invoke commands on remote hosts
|
56
126
|
email:
|
57
127
|
- mpapis@gmail.com
|
@@ -59,10 +129,16 @@ executables: []
|
|
59
129
|
extensions: []
|
60
130
|
extra_rdoc_files: []
|
61
131
|
files:
|
62
|
-
- lib/remote
|
63
|
-
- lib/
|
64
|
-
- lib/
|
65
|
-
- lib/
|
132
|
+
- lib/remote-exec.rb
|
133
|
+
- lib/remote_exec/base.rb
|
134
|
+
- lib/remote_exec/fake.rb
|
135
|
+
- lib/remote_exec/local.rb
|
136
|
+
- lib/remote_exec/ssh.rb
|
137
|
+
- lib/remote_exec/version.rb
|
138
|
+
- test/remote_exec/fake_test.rb
|
139
|
+
- test/remote_exec/local_test.rb
|
140
|
+
- test/remote_exec/ssh_test.rb
|
141
|
+
- test/test_helper.rb
|
66
142
|
homepage: https://github.com/remote-exec/remote-exec
|
67
143
|
licenses:
|
68
144
|
- MIT
|
@@ -83,9 +159,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
159
|
version: '0'
|
84
160
|
requirements: []
|
85
161
|
rubyforge_project:
|
86
|
-
rubygems_version: 2.
|
162
|
+
rubygems_version: 2.2.2
|
87
163
|
signing_key:
|
88
164
|
specification_version: 4
|
89
165
|
summary: Invoke commands on remote hosts
|
90
|
-
test_files:
|
166
|
+
test_files:
|
167
|
+
- test/test_helper.rb
|
168
|
+
- test/remote_exec/local_test.rb
|
169
|
+
- test/remote_exec/ssh_test.rb
|
170
|
+
- test/remote_exec/fake_test.rb
|
91
171
|
has_rdoc:
|
data/lib/remote/exec/fake.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
class Remote::Exec::Fake
|
2
|
-
attr_reader :last_status
|
3
|
-
|
4
|
-
def execute(command)
|
5
|
-
@last_status, outputs = @respond.call(command)
|
6
|
-
outputs.each do |out, err|
|
7
|
-
yield(out, err)
|
8
|
-
end
|
9
|
-
@last_status
|
10
|
-
end
|
11
|
-
|
12
|
-
def respond(&block)
|
13
|
-
@respond = block
|
14
|
-
end
|
15
|
-
end
|
data/lib/remote/exec/local.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'session'
|
2
|
-
|
3
|
-
class Remote::Exec::Local
|
4
|
-
attr_reader :shell, :last_status
|
5
|
-
|
6
|
-
def initialize(shell = "sh")
|
7
|
-
@shell = shell
|
8
|
-
end
|
9
|
-
|
10
|
-
def execute(command)
|
11
|
-
@last_status = nil
|
12
|
-
shell_session.execute(command) do |out, err|
|
13
|
-
yield(out, err)
|
14
|
-
end
|
15
|
-
@last_status = shell_session.status
|
16
|
-
end
|
17
|
-
|
18
|
-
def shell_session
|
19
|
-
@shell_session ||= Session::Sh.new(:prog => shell)
|
20
|
-
end
|
21
|
-
end
|
data/lib/remote/exec/ssh.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'net/ssh'
|
2
|
-
|
3
|
-
class Remote::Exec::Ssh
|
4
|
-
attr_reader :host, :user, :last_status
|
5
|
-
|
6
|
-
def initialize(host, user = nil)
|
7
|
-
@host = host
|
8
|
-
@user = user
|
9
|
-
end
|
10
|
-
|
11
|
-
# TODO: make it run in one session
|
12
|
-
def execute(command)
|
13
|
-
@last_status = nil
|
14
|
-
ssh.open_channel do |channel|
|
15
|
-
channel.request_pty
|
16
|
-
channel.exec command do |ch, success|
|
17
|
-
channel.on_data do |ch, data|
|
18
|
-
yield(data, nil)
|
19
|
-
end
|
20
|
-
channel.on_extended_data do |ch, type, data|
|
21
|
-
yield(nil, data)
|
22
|
-
end
|
23
|
-
channel.on_request("exit-status") do |ch, data|
|
24
|
-
@last_status = data.read_long
|
25
|
-
end
|
26
|
-
end
|
27
|
-
channel.wait
|
28
|
-
end
|
29
|
-
ssh.loop
|
30
|
-
@last_status
|
31
|
-
end
|
32
|
-
|
33
|
-
def ssh
|
34
|
-
@ssh ||= Net::SSH.start(host, user)
|
35
|
-
end
|
36
|
-
end
|