nutshell 1.0.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.
- 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
|