nutshell 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/Manifest.txt +12 -0
- data/README.txt +76 -0
- data/Rakefile +46 -0
- data/lib/nutshell.rb +42 -0
- data/lib/nutshell/remote_shell.rb +268 -0
- data/lib/nutshell/shell.rb +440 -0
- data/test/mocks/mock_object.rb +179 -0
- data/test/mocks/mock_open4.rb +117 -0
- data/test/test_helper.rb +108 -0
- data/test/test_remote_shell.rb +102 -0
- data/test/test_shell.rb +98 -0
- metadata +120 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module MockOpen4
|
2
|
+
|
3
|
+
CMD_RETURN = {
|
4
|
+
Nutshell::RemoteShell::LOGIN_LOOP => [:out, "ready\n"]
|
5
|
+
}
|
6
|
+
|
7
|
+
attr_reader :cmd_log
|
8
|
+
|
9
|
+
def popen4(*args)
|
10
|
+
cmd = args.join(" ")
|
11
|
+
@cmd_log ||= []
|
12
|
+
@cmd_log << cmd
|
13
|
+
|
14
|
+
|
15
|
+
pid = "test_pid"
|
16
|
+
inn_w = StringIO.new
|
17
|
+
out_r, out_w = IO.pipe
|
18
|
+
err_r, err_w = IO.pipe
|
19
|
+
|
20
|
+
ios = {:inn => inn_w, :out => out_w, :err => err_w}
|
21
|
+
stream, string = output_for cmd
|
22
|
+
|
23
|
+
ios[stream].write string
|
24
|
+
out_w.write nil
|
25
|
+
err_w.write nil
|
26
|
+
|
27
|
+
out_w.close
|
28
|
+
err_w.close
|
29
|
+
|
30
|
+
if block_given?
|
31
|
+
yield
|
32
|
+
inn_w.close
|
33
|
+
out_r.close
|
34
|
+
err_r.close
|
35
|
+
end
|
36
|
+
|
37
|
+
return pid, inn_w, out_r, err_r
|
38
|
+
end
|
39
|
+
|
40
|
+
def output_for(cmd)
|
41
|
+
@mock_output ||= {}
|
42
|
+
if @mock_output
|
43
|
+
output = @mock_output[cmd]
|
44
|
+
output ||= @mock_output[nil].shift if Array === @mock_output[nil]
|
45
|
+
@mock_output.delete(cmd)
|
46
|
+
if output
|
47
|
+
Process.set_exitcode output.delete(output.last)
|
48
|
+
return output
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
CMD_RETURN.each do |cmd_key, return_val|
|
53
|
+
return return_val if cmd.include? cmd_key
|
54
|
+
end
|
55
|
+
return :out, "some_value"
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_mock_response code, stream_vals={}, options={}
|
59
|
+
@mock_output ||= {}
|
60
|
+
@mock_output[nil] ||= []
|
61
|
+
new_stream_vals = {}
|
62
|
+
|
63
|
+
stream_vals.each do |key, val|
|
64
|
+
if Symbol === key
|
65
|
+
@mock_output[nil] << [key, val, code]
|
66
|
+
next
|
67
|
+
end
|
68
|
+
|
69
|
+
if Nutshell::RemoteShell === self
|
70
|
+
key = build_remote_cmd(key, options).join(" ")
|
71
|
+
end
|
72
|
+
|
73
|
+
new_stream_vals[key] = (val.dup << code)
|
74
|
+
end
|
75
|
+
@mock_output.merge! new_stream_vals
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class StatusStruct < Struct.new("Status", :exitstatus)
|
82
|
+
def success?
|
83
|
+
self.exitstatus == 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
Process.class_eval do
|
89
|
+
class << self
|
90
|
+
|
91
|
+
def set_exitcode(code)
|
92
|
+
@exit_code = code
|
93
|
+
end
|
94
|
+
|
95
|
+
alias old_waitpid2 waitpid2
|
96
|
+
undef waitpid2
|
97
|
+
|
98
|
+
def waitpid2(*args)
|
99
|
+
pid = args[0]
|
100
|
+
if pid == "test_pid"
|
101
|
+
exitcode = @exit_code ||= 0
|
102
|
+
@exit_code = 0
|
103
|
+
return [StatusStruct.new(exitcode)]
|
104
|
+
else
|
105
|
+
return old_waitpid2(*args)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
alias old_kill kill
|
110
|
+
undef kill
|
111
|
+
|
112
|
+
def kill(type, pid)
|
113
|
+
return true if type == 0 && pid == "test_pid"
|
114
|
+
old_kill(type, pid)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'nutshell'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
require 'test/mocks/mock_object'
|
5
|
+
require 'test/mocks/mock_open4'
|
6
|
+
|
7
|
+
|
8
|
+
def mock_remote_shell host=nil
|
9
|
+
host ||= "user@some_server.com"
|
10
|
+
remote_shell = Nutshell::RemoteShell.new host
|
11
|
+
|
12
|
+
remote_shell.extend MockOpen4
|
13
|
+
remote_shell.extend MockObject
|
14
|
+
|
15
|
+
use_remote_shell remote_shell
|
16
|
+
|
17
|
+
remote_shell.connect
|
18
|
+
remote_shell
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def mock_remote_shell host=nil
|
23
|
+
host ||= "user@some_server.com"
|
24
|
+
remote_shell = Nutshell::RemoteShell.new host
|
25
|
+
|
26
|
+
remote_shell.extend MockOpen4
|
27
|
+
remote_shell.extend MockObject
|
28
|
+
|
29
|
+
use_remote_shell remote_shell
|
30
|
+
|
31
|
+
remote_shell.connect
|
32
|
+
remote_shell
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def assert_not_called *args
|
37
|
+
assert !@remote_shell.method_called?(:call, :args => [*args]),
|
38
|
+
"Command called by #{@remote_shell.host} but should't have:\n #{args[0]}"
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def assert_server_call *args
|
43
|
+
assert @remote_shell.method_called?(:call, :args => [*args]),
|
44
|
+
"Command was not called by #{@remote_shell.host}:\n #{args[0]}"
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def assert_bash_script name, cmds, check_value
|
49
|
+
cmds = cmds.map{|cmd| "(#{cmd})" }
|
50
|
+
cmds << "echo true"
|
51
|
+
|
52
|
+
bash = <<-STR
|
53
|
+
#!/bin/bash
|
54
|
+
if [ "$1" == "--no-env" ]; then
|
55
|
+
#{cmds.flatten.join(" && ")}
|
56
|
+
else
|
57
|
+
#{@app.root_path}/env #{@app.root_path}/#{name} --no-env
|
58
|
+
fi
|
59
|
+
STR
|
60
|
+
|
61
|
+
assert_equal bash, check_value
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def assert_ssh_call expected, ds=@remote_shell, options={}
|
66
|
+
expected = ds.build_remote_cmd(expected, options).join(" ")
|
67
|
+
|
68
|
+
error_msg = "No such command in remote_shell log [#{ds.host}]\n#{expected}"
|
69
|
+
error_msg << "\n\n#{ds.cmd_log.select{|c| c =~ /^ssh/}.join("\n\n")}"
|
70
|
+
|
71
|
+
assert ds.cmd_log.include?(expected), error_msg
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def assert_rsync from, to, ds=@remote_shell, sudo=false
|
76
|
+
received = ds.cmd_log.last
|
77
|
+
|
78
|
+
rsync_path = if sudo
|
79
|
+
path = ds.sudo_cmd('rsync', sudo).join(' ')
|
80
|
+
"--rsync-path='#{ path }' "
|
81
|
+
end
|
82
|
+
|
83
|
+
rsync_cmd = "rsync -azP #{rsync_path}-e \"ssh #{ds.ssh_flags.join(' ')}\""
|
84
|
+
|
85
|
+
error_msg = "No such command in remote_shell log [#{ds.host}]\n#{rsync_cmd}"
|
86
|
+
error_msg << "#{from.inspect} #{to.inspect}"
|
87
|
+
error_msg << "\n\n#{ds.cmd_log.select{|c| c =~ /^rsync/}.join("\n\n")}"
|
88
|
+
|
89
|
+
if Regexp === from
|
90
|
+
found = ds.cmd_log.select do |cmd|
|
91
|
+
|
92
|
+
cmd_from = cmd.split(" ")[-2]
|
93
|
+
cmd_to = cmd.split(" ").last
|
94
|
+
|
95
|
+
cmd_from =~ from && cmd_to == to && cmd.index(rsync_cmd) == 0
|
96
|
+
end
|
97
|
+
|
98
|
+
assert !found.empty?, error_msg
|
99
|
+
else
|
100
|
+
expected = "#{rsync_cmd} #{from} #{to}"
|
101
|
+
assert ds.cmd_log.include?(expected), error_msg
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def use_remote_shell remote_shell
|
107
|
+
@remote_shell = remote_shell
|
108
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class TestRemoteShell < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Nutshell::RemoteShell.class_eval{ include MockOpen4 }
|
7
|
+
|
8
|
+
@host = "user@some_server.com"
|
9
|
+
@remote_shell = mock_remote_shell @host
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
@remote_shell.disconnect
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_connect
|
17
|
+
login_cmd = Nutshell::RemoteShell::LOGIN_LOOP
|
18
|
+
login_cmd = @remote_shell.send :quote_cmd, login_cmd
|
19
|
+
login_cmd = @remote_shell.send :ssh_cmd, login_cmd, :sudo => false
|
20
|
+
|
21
|
+
assert @remote_shell.method_called?(:popen4, :args => [login_cmd.join(" ")])
|
22
|
+
assert @remote_shell.connected?
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_disconnect
|
26
|
+
@remote_shell.disconnect
|
27
|
+
assert !@remote_shell.connected?
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_call
|
31
|
+
@remote_shell.call "echo 'line1'; echo 'line2'"
|
32
|
+
assert_ssh_call "echo 'line1'; echo 'line2'"
|
33
|
+
|
34
|
+
@remote_shell.sudo = "sudouser"
|
35
|
+
@remote_shell.call "sudocall"
|
36
|
+
assert_ssh_call "sudocall", @remote_shell, :sudo => "sudouser"
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_call_with_stderr
|
40
|
+
@remote_shell.set_mock_response 1, :err => 'this is an error'
|
41
|
+
cmd = "echo 'this is an error'"
|
42
|
+
@remote_shell.call cmd
|
43
|
+
raise "Didn't raise CmdError on stderr"
|
44
|
+
rescue Nutshell::CmdError => e
|
45
|
+
ssh_cmd = @remote_shell.build_remote_cmd(cmd).join(" ")
|
46
|
+
assert_equal "Execution failed with status 1: #{ssh_cmd}", e.message
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_upload
|
50
|
+
@remote_shell.upload "test/fixtures/nutshell_test", "nutshell_test"
|
51
|
+
assert_rsync "test/fixtures/nutshell_test",
|
52
|
+
"#{@remote_shell.host}:nutshell_test"
|
53
|
+
|
54
|
+
@remote_shell.sudo = "blah"
|
55
|
+
@remote_shell.upload "test/fixtures/nutshell_test", "nutshell_test"
|
56
|
+
assert_rsync "test/fixtures/nutshell_test",
|
57
|
+
"#{@remote_shell.host}:nutshell_test", @remote_shell, "blah"
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_download
|
61
|
+
@remote_shell.download "nutshell_test", "."
|
62
|
+
assert_rsync "#{@remote_shell.host}:nutshell_test", "."
|
63
|
+
|
64
|
+
@remote_shell.download "nutshell_test", ".", :sudo => "sudouser"
|
65
|
+
assert_rsync "#{@remote_shell.host}:nutshell_test", ".",
|
66
|
+
@remote_shell, "sudouser"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_make_file
|
70
|
+
@remote_shell.make_file("some_dir/nutshell_test_file", "test data")
|
71
|
+
tmp_file = "#{Nutshell::TMP_DIR}/nutshell_test_file"
|
72
|
+
tmp_file = Regexp.escape tmp_file
|
73
|
+
assert_rsync(/^#{tmp_file}_[0-9]+/,
|
74
|
+
"#{@remote_shell.host}:some_dir/nutshell_test_file")
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_os_name
|
78
|
+
@remote_shell.os_name
|
79
|
+
assert_ssh_call "uname -s"
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_equality
|
83
|
+
ds_equal = Nutshell::RemoteShell.new @host
|
84
|
+
ds_diff1 = Nutshell::RemoteShell.new @host, :user => "blarg"
|
85
|
+
ds_diff2 = Nutshell::RemoteShell.new "some_other_host"
|
86
|
+
|
87
|
+
assert_equal ds_equal, @remote_shell
|
88
|
+
assert_equal ds_diff1, @remote_shell
|
89
|
+
assert ds_diff2 != @remote_shell
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_file?
|
93
|
+
@remote_shell.file? "some/file/path"
|
94
|
+
assert_ssh_call "test -f some/file/path"
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_symlink
|
98
|
+
@remote_shell.symlink "target_file", "sym_name"
|
99
|
+
assert_ssh_call "ln -sfT target_file sym_name"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
data/test/test_shell.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class TestShell < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@output = StringIO.new
|
7
|
+
@output.extend MockObject
|
8
|
+
|
9
|
+
@shell = Nutshell::Shell.new @output
|
10
|
+
@shell.extend MockOpen4
|
11
|
+
|
12
|
+
@shell.input.extend MockObject
|
13
|
+
@shell.input.mock :ask, :return => "someinput"
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def test_initialize
|
18
|
+
assert_equal @output, @shell.output
|
19
|
+
assert_equal HighLine, @shell.input.class
|
20
|
+
assert_equal `whoami`.chomp, @shell.user
|
21
|
+
assert_equal `hostname`.chomp, @shell.host
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def test_ask
|
26
|
+
@shell.ask "input something!"
|
27
|
+
assert 1, @shell.input.method_call_count(:ask)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def test_close
|
32
|
+
@shell.close
|
33
|
+
assert 1, @shell.output.method_call_count(:close)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def test_write
|
38
|
+
@shell.write "blah"
|
39
|
+
assert @output.method_called?(:write, :args => "blah")
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def test_prompt_for_password
|
44
|
+
@shell.prompt_for_password
|
45
|
+
|
46
|
+
args = "#{@shell.user}@#{@shell.host} Password:"
|
47
|
+
assert @shell.input.method_called?(:ask, :args => args)
|
48
|
+
assert_equal "someinput", @shell.password
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def test_execute
|
53
|
+
@shell.set_mock_response 0, "say hi" => [:out, "hi\n"]
|
54
|
+
|
55
|
+
response = @shell.execute("say hi") do |stream, data|
|
56
|
+
assert_equal :out, stream
|
57
|
+
assert_equal "hi\n", data
|
58
|
+
end
|
59
|
+
assert_equal "hi", response
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def test_execute_errorstatus
|
64
|
+
@shell.set_mock_response 1, "error me" => [:err, "ERROR'D!"]
|
65
|
+
|
66
|
+
begin
|
67
|
+
@shell.execute("error me") do |stream, data|
|
68
|
+
assert_equal :err, stream
|
69
|
+
assert_equal "ERROR'D!", data
|
70
|
+
end
|
71
|
+
raise "Didn't call CmdError when it should have"
|
72
|
+
rescue Nutshell::CmdError => e
|
73
|
+
msg = "Execution failed with status 1: error me"
|
74
|
+
assert_equal msg, e.message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def test_execute_stderronly
|
80
|
+
@shell.set_mock_response 0, "stderr" => [:err, "fake error"]
|
81
|
+
|
82
|
+
response = @shell.execute("stderr") do |stream, data|
|
83
|
+
assert_equal :err, stream
|
84
|
+
assert_equal "fake error", data
|
85
|
+
end
|
86
|
+
assert_equal "", response
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def test_execute_password_prompt
|
91
|
+
@shell.set_mock_response 0, "do that thing" => [:err, "Password:"]
|
92
|
+
@shell.input.mock :ask, :return => "new_password"
|
93
|
+
|
94
|
+
@shell.execute("do that thing")
|
95
|
+
assert_equal "new_password", @shell.password
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nutshell
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jeremie Castagna
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-28 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: open4
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 0
|
30
|
+
- 1
|
31
|
+
version: 1.0.1
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: highline
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 5
|
44
|
+
- 1
|
45
|
+
version: 1.5.1
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: hoe
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 2
|
57
|
+
- 3
|
58
|
+
- 3
|
59
|
+
version: 2.3.3
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
description: A light weight ssh client that wraps the ssh and rsync commands.
|
63
|
+
email:
|
64
|
+
- yaksnrainbows@gmail.com
|
65
|
+
executables: []
|
66
|
+
|
67
|
+
extensions: []
|
68
|
+
|
69
|
+
extra_rdoc_files:
|
70
|
+
- History.txt
|
71
|
+
- Manifest.txt
|
72
|
+
- README.txt
|
73
|
+
files:
|
74
|
+
- History.txt
|
75
|
+
- Manifest.txt
|
76
|
+
- README.txt
|
77
|
+
- Rakefile
|
78
|
+
- lib/nutshell.rb
|
79
|
+
- lib/nutshell/remote_shell.rb
|
80
|
+
- lib/nutshell/shell.rb
|
81
|
+
- test/mocks/mock_object.rb
|
82
|
+
- test/mocks/mock_open4.rb
|
83
|
+
- test/test_helper.rb
|
84
|
+
- test/test_remote_shell.rb
|
85
|
+
- test/test_shell.rb
|
86
|
+
has_rdoc: true
|
87
|
+
homepage: http://github.com/yaksnrainbows/nutshell
|
88
|
+
licenses: []
|
89
|
+
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options:
|
92
|
+
- --main
|
93
|
+
- README.txt
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
requirements: []
|
111
|
+
|
112
|
+
rubyforge_project: nutshell
|
113
|
+
rubygems_version: 1.3.6
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: A light weight ssh client that wraps the ssh and rsync commands.
|
117
|
+
test_files:
|
118
|
+
- test/test_helper.rb
|
119
|
+
- test/test_remote_shell.rb
|
120
|
+
- test/test_shell.rb
|