wesabe-robot-army 0.1.1 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/whoami.rb +13 -0
- data/lib/robot-army.rb +47 -21
- data/lib/robot-army/at_exit.rb +19 -0
- data/lib/robot-army/eval_builder.rb +84 -0
- data/lib/robot-army/eval_command.rb +17 -0
- data/lib/robot-army/keychain.rb +10 -0
- data/lib/robot-army/marshal_ext.rb +26 -7
- data/lib/robot-army/remote_evaler.rb +59 -0
- data/lib/robot-army/ruby2ruby_ext.rb +6 -18
- data/lib/robot-army/task_master.rb +113 -201
- data/spec/at_exit_spec.rb +25 -0
- data/spec/connection_spec.rb +126 -0
- data/spec/dependency_loader_spec.rb +46 -0
- data/spec/gate_keeper_spec.rb +46 -0
- data/spec/integration_spec.rb +40 -0
- data/spec/io_spec.rb +36 -0
- data/spec/keychain_spec.rb +15 -0
- data/spec/loader_spec.rb +13 -0
- data/spec/marshal_ext_spec.rb +89 -0
- data/spec/messenger_spec.rb +28 -0
- data/spec/officer_spec.rb +36 -0
- data/spec/proxy_spec.rb +52 -0
- data/spec/ruby2ruby_ext_spec.rb +67 -0
- data/spec/soldier_spec.rb +71 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/task_master_spec.rb +272 -0
- metadata +49 -16
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::AtExit do
|
4
|
+
before do
|
5
|
+
@at_exit = RobotArmy::AtExit.shared_instance
|
6
|
+
end
|
7
|
+
|
8
|
+
it "runs the provided block when directed" do
|
9
|
+
foo = 'foo'
|
10
|
+
@at_exit.at_exit { foo = 'bar' }
|
11
|
+
foo.must == 'foo'
|
12
|
+
@at_exit.do_exit
|
13
|
+
foo.must == 'bar'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "does not run the same block twice" do
|
17
|
+
foo = 0
|
18
|
+
@at_exit.at_exit { foo += 1 }
|
19
|
+
foo.must == 0
|
20
|
+
@at_exit.do_exit
|
21
|
+
foo.must == 1
|
22
|
+
@at_exit.do_exit
|
23
|
+
foo.must == 1
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Connection do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@host = 'example.com'
|
7
|
+
@connection = RobotArmy::Connection.new(@host)
|
8
|
+
@messenger = mock(:messenger)
|
9
|
+
@messenger.stub!(:post)
|
10
|
+
@connection.stub!(:messenger).and_return(@messenger)
|
11
|
+
@connection.stub!(:start_child)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "is not closed after opening" do
|
15
|
+
# when
|
16
|
+
@connection.open
|
17
|
+
|
18
|
+
# then
|
19
|
+
@connection.must_not be_closed
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns itself from open" do
|
23
|
+
@connection.open.must == @connection
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the result of the block passed to open" do
|
27
|
+
# when
|
28
|
+
@connection.stub!(:close)
|
29
|
+
|
30
|
+
# then
|
31
|
+
@connection.open{ 3 }.must == 3
|
32
|
+
end
|
33
|
+
|
34
|
+
it "closes the connection if a block is passed to open" do
|
35
|
+
# then
|
36
|
+
proc{ @connection.open{ 3 } }.
|
37
|
+
must_not change(@connection, :closed?).from(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "closes the connection even if an exception is raised in the block passed to open" do
|
41
|
+
# then
|
42
|
+
proc do
|
43
|
+
proc{ @connection.open{ raise 'BOO!' } }.
|
44
|
+
must_not change(@connection, :closed?).from(true)
|
45
|
+
end.
|
46
|
+
must raise_error('BOO!')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "does not start another child process if we're already open" do
|
50
|
+
# then
|
51
|
+
@connection.should_not_receive(:start_child)
|
52
|
+
|
53
|
+
# when
|
54
|
+
@connection.stub!(:closed?).and_return(false)
|
55
|
+
@connection.open
|
56
|
+
end
|
57
|
+
|
58
|
+
it "raises an exception when calling close if a connection is already closed" do
|
59
|
+
# when
|
60
|
+
@connection.stub!(:closed?).and_return(true)
|
61
|
+
|
62
|
+
# then
|
63
|
+
proc{ @connection.close }.must raise_error(RobotArmy::ConnectionNotOpen)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "sends an exit command to its child upon closing" do
|
67
|
+
# then
|
68
|
+
@messenger.should_receive(:post).with(:command => :exit)
|
69
|
+
|
70
|
+
# when
|
71
|
+
@connection.stub!(:closed?).and_return(false)
|
72
|
+
@connection.close
|
73
|
+
end
|
74
|
+
|
75
|
+
it "starts a closed local connection when calling localhost" do
|
76
|
+
RobotArmy::Connection.localhost.must be_closed
|
77
|
+
end
|
78
|
+
|
79
|
+
it "raises a Warning when handling a message with status=warning" do
|
80
|
+
proc{ @connection.handle_response(:status => 'warning', :data => 'foobar') }.
|
81
|
+
must raise_error(RobotArmy::Warning)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe RobotArmy::Connection, 'answer_sudo_prompt' do
|
86
|
+
before do
|
87
|
+
@connection = RobotArmy::Connection.new(:localhost, 'root')
|
88
|
+
@password_proc = proc { 'password' }
|
89
|
+
@stdin = StringIO.new
|
90
|
+
@stderr = stub(:stderr, :readpartial => nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "calls back using the password proc if it is a proc" do
|
94
|
+
# when
|
95
|
+
@connection.stub!(:password).and_return(@password_proc)
|
96
|
+
@connection.stub!(:asking_for_password?).and_return(true, false)
|
97
|
+
@connection.answer_sudo_prompt(@stdin, @stderr)
|
98
|
+
|
99
|
+
# then
|
100
|
+
@stdin.string.must == "password\n"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "raises if password is a string and is rejected" do
|
104
|
+
# when
|
105
|
+
@connection.stub!(:password).and_return('password')
|
106
|
+
@connection.stub!(:asking_for_password?).and_return(true)
|
107
|
+
|
108
|
+
# then
|
109
|
+
proc { @connection.answer_sudo_prompt(@stdin, @stderr) }.
|
110
|
+
must raise_error(RobotArmy::InvalidPassword)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "calls back three times before raising if password is a proc" do
|
114
|
+
calls = 0
|
115
|
+
|
116
|
+
# when
|
117
|
+
@connection.stub!(:password).and_return(proc{ calls += 1 })
|
118
|
+
|
119
|
+
# then
|
120
|
+
@connection.should_receive(:asking_for_password?).
|
121
|
+
exactly(4).times.and_return(true)
|
122
|
+
proc { @connection.answer_sudo_prompt(@stdin, @stderr) }.
|
123
|
+
must raise_error(RobotArmy::InvalidPassword)
|
124
|
+
calls.must == 3
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::DependencyLoader do
|
4
|
+
before do
|
5
|
+
@loader = RobotArmy::DependencyLoader.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should have no dependencies by default" do
|
9
|
+
@loader.dependencies.must == []
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should store the dependency requirement by name" do
|
13
|
+
name = "RedCloth"
|
14
|
+
@loader.add_dependency name
|
15
|
+
@loader.dependencies.must == [[name]]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should store the dependency requirement with version restriction" do
|
19
|
+
name = "RedCloth"
|
20
|
+
version_str = "> 3.1.0"
|
21
|
+
@loader.add_dependency name, version_str
|
22
|
+
@loader.dependencies.must == [[name, version_str]]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should gem load a dependency by name only" do
|
26
|
+
name = "foobarbaz"
|
27
|
+
@loader.add_dependency name
|
28
|
+
@loader.should_receive(:gem).with(name)
|
29
|
+
@loader.load!
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should gem load a dependency by name and version" do
|
33
|
+
name = "foobarbaz"
|
34
|
+
version = "> 3.1"
|
35
|
+
@loader.add_dependency name, version
|
36
|
+
@loader.should_receive(:gem).with(name, version)
|
37
|
+
@loader.load!
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
it "should raise when a dependency is not met" do
|
42
|
+
@loader.add_dependency "foobarbaz"
|
43
|
+
@loader.should_receive(:gem).and_raise Gem::LoadError
|
44
|
+
lambda { @loader.load! }.must raise_error(RobotArmy::DependencyError)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::GateKeeper do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@keeper = RobotArmy::GateKeeper.new
|
7
|
+
@host = 'example.com'
|
8
|
+
@connection = mock(:connection)
|
9
|
+
@connection.stub!(:closed?).and_return(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "establishes a new connection to a host if one does not already exist" do
|
13
|
+
# then
|
14
|
+
@keeper.should_receive(:establish_connection).with(@host)
|
15
|
+
|
16
|
+
# when
|
17
|
+
@keeper.stub!(:get_connection).and_return(nil)
|
18
|
+
@keeper.connect(@host)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "terminates all connections on close" do
|
22
|
+
# then
|
23
|
+
@connection.should_receive(:close)
|
24
|
+
|
25
|
+
# when
|
26
|
+
@keeper.stub!(:connections).and_return(@host => @connection)
|
27
|
+
@keeper.close
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates a new Connection with the given host when establish_connection is called" do
|
31
|
+
# then
|
32
|
+
RobotArmy::OfficerConnection.should_receive(:new).with(@host).and_return(@connection)
|
33
|
+
@connection.should_receive(:open).and_return(@connection)
|
34
|
+
|
35
|
+
# when
|
36
|
+
@keeper.establish_connection(@host)
|
37
|
+
|
38
|
+
# and
|
39
|
+
@keeper.connections[@host].must == @connection
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has a shared instance that doesn't change" do
|
43
|
+
RobotArmy::GateKeeper.shared_instance.must be_an_instance_of(RobotArmy::GateKeeper)
|
44
|
+
RobotArmy::GateKeeper.shared_instance.must == RobotArmy::GateKeeper.shared_instance
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
# THIS SPEC IS INTENDED TO BE RUN BY ITSELF:
|
4
|
+
#
|
5
|
+
# env SUDO_AS=brad INTEGRATION_HOST=example.com spec spec/integration_spec.rb --format=specdoc --color
|
6
|
+
#
|
7
|
+
|
8
|
+
if $INTEGRATION_HOST = ENV['INTEGRATION_HOST']
|
9
|
+
$TESTING = false
|
10
|
+
$ROBOT_ARMY_DEBUG = true
|
11
|
+
|
12
|
+
class Integration < RobotArmy::TaskMaster
|
13
|
+
host $INTEGRATION_HOST
|
14
|
+
end
|
15
|
+
|
16
|
+
describe Integration do
|
17
|
+
before do
|
18
|
+
@tm = Integration.new
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can do sudo" do
|
22
|
+
@tm.sudo { Time.now }.must be_a(Time)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "does sudo as root by default" do
|
26
|
+
@tm.sudo { Process.uid }.must == 0
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can do sudo as ourselves" do
|
30
|
+
my_remote_uid = @tm.remote { Process.uid }
|
31
|
+
@tm.sudo(:user => ENV['USER']) { Process.uid }.must == my_remote_uid
|
32
|
+
end
|
33
|
+
|
34
|
+
if sudo_user = ENV['SUDO_AS']
|
35
|
+
it "can sudo as another non-root user" do
|
36
|
+
@tm.sudo(:user => sudo_user) { ENV['USER'] }.must == sudo_user
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/io_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::IO, 'class method read_data' do
|
4
|
+
before do
|
5
|
+
@stream = stub(:stream)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "reads all data as long as it is available" do
|
9
|
+
RobotArmy::IO.stub!(:has_data?).and_return(true, true, false)
|
10
|
+
@stream.stub!(:readpartial).and_return('foo', 'bar')
|
11
|
+
|
12
|
+
RobotArmy::IO.read_data(@stream).must == "foobar"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe RobotArmy::IO do
|
17
|
+
before do
|
18
|
+
@stream = RobotArmy::IO.new(:stdout)
|
19
|
+
@upstream = RobotArmy.upstream = stub(:upstream)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can capture output of IO method calls" do
|
23
|
+
@stream.send(:capture, :print, 'foo').must == 'foo'
|
24
|
+
end
|
25
|
+
|
26
|
+
it "proxies output upstream" do
|
27
|
+
@upstream.should_receive(:post).
|
28
|
+
with(:status => 'output', :data => {:stream => 'stdout', :string => "foo\n"})
|
29
|
+
|
30
|
+
@stream.puts 'foo'
|
31
|
+
end
|
32
|
+
|
33
|
+
after do
|
34
|
+
@stream.stop_capture
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Keychain do
|
4
|
+
before do
|
5
|
+
@keychain = RobotArmy::Keychain.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "asks for all passwords over stdin" do
|
9
|
+
@keychain.
|
10
|
+
should_receive(:read_with_prompt).
|
11
|
+
with("[sudo] password for bob@example.com: ").
|
12
|
+
and_return("god")
|
13
|
+
@keychain.get_password_for_user_on_host('bob', 'example.com').must == 'god'
|
14
|
+
end
|
15
|
+
end
|
data/spec/loader_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Loader do
|
4
|
+
before do
|
5
|
+
@loader = RobotArmy::Loader.new
|
6
|
+
@messenger = @loader.messenger = mock(:messenger)
|
7
|
+
@messenger.stub!(:post)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "doesn't catch the RobotArmy::Exit exception" do
|
11
|
+
proc{ @loader.safely{ raise RobotArmy::Exit } }.must raise_error(RobotArmy::Exit)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
class CannotMarshalMe
|
4
|
+
def marshalable?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class CustomContainer
|
10
|
+
def initialize(child)
|
11
|
+
@child = child
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Marshal do
|
16
|
+
it "can dump Fixnums" do
|
17
|
+
42.must be_marshalable
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can dump Strings" do
|
21
|
+
"foo".must be_marshalable
|
22
|
+
end
|
23
|
+
|
24
|
+
it "can dump Arrays" do
|
25
|
+
[2, 'foo'].must be_marshalable
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can't dump things that return false for marshalable?" do
|
29
|
+
CannotMarshalMe.new.must_not be_marshalable
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can't dump Arrays containing unmarshalable items" do
|
33
|
+
[2, $stdin].must_not be_marshalable
|
34
|
+
[2, CannotMarshalMe.new].must_not be_marshalable
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can dump hashes" do
|
38
|
+
{:foo => 'bar'}.must be_marshalable
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can't dump hashes with unmarshalable keys" do
|
42
|
+
{$stdin => 'bar'}.must_not be_marshalable
|
43
|
+
{CannotMarshalMe.new => 'bar'}.must_not be_marshalable
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can't dump hashes with unmarshalable values" do
|
47
|
+
{:foo => $stdin}.must_not be_marshalable
|
48
|
+
{:foo => CannotMarshalMe.new}.must_not be_marshalable
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can't dump hashes with an unmarshalable default value" do
|
52
|
+
Hash.new($stdin).must_not be_marshalable
|
53
|
+
Hash.new(CannotMarshalMe.new).must_not be_marshalable
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can dump hashes with a marshalable default value" do
|
57
|
+
Hash.new(0).must be_marshalable
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can't dump hashes with a default proc" do
|
61
|
+
Hash.new {|a,b| a[b] = rand}.must_not be_marshalable
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can't dump objects containing references to unmarshalable objects" do
|
65
|
+
CustomContainer.new(CannotMarshalMe.new).must_not be_marshalable
|
66
|
+
end
|
67
|
+
|
68
|
+
it "can't dump IOs" do
|
69
|
+
$stdin.must_not be_marshalable
|
70
|
+
end
|
71
|
+
|
72
|
+
it "can't dump Methods" do
|
73
|
+
method(:to_s).must_not be_marshalable
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can't dump bindings" do
|
77
|
+
binding.must_not be_marshalable
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can't dump Procs" do
|
81
|
+
proc{ 2 }.must_not be_marshalable
|
82
|
+
end
|
83
|
+
|
84
|
+
it "can't dump anything whose _dump method raises a TypeError" do
|
85
|
+
class NotDumpable; def _dump(*args); raise TypeError; end; end
|
86
|
+
NotDumpable.new.must_not be_marshalable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|