robot-army 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.markdown +34 -0
- data/Rakefile +9 -0
- data/examples/whoami.rb +13 -0
- data/lib/robot-army.rb +109 -0
- data/lib/robot-army/at_exit.rb +19 -0
- data/lib/robot-army/connection.rb +174 -0
- data/lib/robot-army/dependency_loader.rb +38 -0
- data/lib/robot-army/eval_builder.rb +84 -0
- data/lib/robot-army/eval_command.rb +17 -0
- data/lib/robot-army/gate_keeper.rb +28 -0
- data/lib/robot-army/io.rb +106 -0
- data/lib/robot-army/keychain.rb +10 -0
- data/lib/robot-army/loader.rb +85 -0
- data/lib/robot-army/marshal_ext.rb +52 -0
- data/lib/robot-army/messenger.rb +31 -0
- data/lib/robot-army/officer.rb +35 -0
- data/lib/robot-army/officer_connection.rb +5 -0
- data/lib/robot-army/officer_loader.rb +13 -0
- data/lib/robot-army/proxy.rb +35 -0
- data/lib/robot-army/remote_evaler.rb +59 -0
- data/lib/robot-army/ruby2ruby_ext.rb +19 -0
- data/lib/robot-army/soldier.rb +37 -0
- data/lib/robot-army/task_master.rb +317 -0
- 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 +19 -0
- data/spec/task_master_spec.rb +306 -0
- metadata +142 -0
@@ -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.should == []
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should store the dependency requirement by name" do
|
13
|
+
name = "RedCloth"
|
14
|
+
@loader.add_dependency name
|
15
|
+
@loader.dependencies.should == [[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.should == [[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! }.should 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].should == @connection
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has a shared instance that doesn't change" do
|
43
|
+
RobotArmy::GateKeeper.shared_instance.should be_an_instance_of(RobotArmy::GateKeeper)
|
44
|
+
RobotArmy::GateKeeper.shared_instance.should == 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 }.should be_a(Time)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "does sudo as root by default" do
|
26
|
+
@tm.sudo { Process.uid }.should == 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 }.should == 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'] }.should == 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).should == "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').should == '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').should == '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 } }.should 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.should be_marshalable
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can dump Strings" do
|
21
|
+
"foo".should be_marshalable
|
22
|
+
end
|
23
|
+
|
24
|
+
it "can dump Arrays" do
|
25
|
+
[2, 'foo'].should be_marshalable
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can't dump things that return false for marshalable?" do
|
29
|
+
CannotMarshalMe.new.should_not be_marshalable
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can't dump Arrays containing unmarshalable items" do
|
33
|
+
[2, $stdin].should_not be_marshalable
|
34
|
+
[2, CannotMarshalMe.new].should_not be_marshalable
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can dump hashes" do
|
38
|
+
{:foo => 'bar'}.should be_marshalable
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can't dump hashes with unmarshalable keys" do
|
42
|
+
{$stdin => 'bar'}.should_not be_marshalable
|
43
|
+
{CannotMarshalMe.new => 'bar'}.should_not be_marshalable
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can't dump hashes with unmarshalable values" do
|
47
|
+
{:foo => $stdin}.should_not be_marshalable
|
48
|
+
{:foo => CannotMarshalMe.new}.should_not be_marshalable
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can't dump hashes with an unmarshalable default value" do
|
52
|
+
Hash.new($stdin).should_not be_marshalable
|
53
|
+
Hash.new(CannotMarshalMe.new).should_not be_marshalable
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can dump hashes with a marshalable default value" do
|
57
|
+
Hash.new(0).should be_marshalable
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can't dump hashes with a default proc" do
|
61
|
+
Hash.new {|a,b| a[b] = rand}.should_not be_marshalable
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can't dump objects containing references to unmarshalable objects" do
|
65
|
+
CustomContainer.new(CannotMarshalMe.new).should_not be_marshalable
|
66
|
+
end
|
67
|
+
|
68
|
+
it "can't dump IOs" do
|
69
|
+
$stdin.should_not be_marshalable
|
70
|
+
end
|
71
|
+
|
72
|
+
it "can't dump Methods" do
|
73
|
+
method(:to_s).should_not be_marshalable
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can't dump bindings" do
|
77
|
+
binding.should_not be_marshalable
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can't dump Procs" do
|
81
|
+
proc{ 2 }.should_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.should_not be_marshalable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Messenger do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@in, @out = StringIO.new, StringIO.new
|
7
|
+
|
8
|
+
@messenger = RobotArmy::Messenger.new(@in, @out)
|
9
|
+
@response = {:status => 'ok', :data => 1}
|
10
|
+
@dump = "#{Base64.encode64(Marshal.dump(@response))}|"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "posts messages to @out" do
|
14
|
+
# when
|
15
|
+
@messenger.post(@response)
|
16
|
+
|
17
|
+
# then
|
18
|
+
@out.string.should == @dump
|
19
|
+
end
|
20
|
+
|
21
|
+
it "gets messages from @in" do
|
22
|
+
# when
|
23
|
+
@in.string = @dump
|
24
|
+
|
25
|
+
# then
|
26
|
+
@messenger.get.should == @response
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Officer do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@messenger = mock(:messenger)
|
7
|
+
@officer = RobotArmy::Officer.new(@messenger)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "evaluates each command in a different process" do
|
11
|
+
# when
|
12
|
+
pid = proc{ @officer.run(:eval, :code => 'Process.pid', :file => __FILE__, :line => __LINE__) }
|
13
|
+
|
14
|
+
# then
|
15
|
+
pid.call.should_not == pid.call
|
16
|
+
end
|
17
|
+
|
18
|
+
it "asks for a password by posting back status=password" do
|
19
|
+
# then
|
20
|
+
@messenger.should_receive(:post).
|
21
|
+
with(:status => 'password', :data => {:as => 'root', :user => ENV['USER']})
|
22
|
+
|
23
|
+
# when
|
24
|
+
@messenger.stub!(:get).and_return(:status => 'ok', :data => 'password')
|
25
|
+
@officer.ask_for_password('root')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns the password given upstream" do
|
29
|
+
# when
|
30
|
+
@messenger.stub!(:post)
|
31
|
+
@messenger.stub!(:get).and_return(:status => 'ok', :data => 'password')
|
32
|
+
|
33
|
+
# then
|
34
|
+
@officer.ask_for_password('root').should == 'password'
|
35
|
+
end
|
36
|
+
end
|
data/spec/proxy_spec.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Proxy do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@messenger = stub(:messenger, :post => nil, :get => nil)
|
7
|
+
@hash = self.hash
|
8
|
+
@proxy = RobotArmy::Proxy.new(@messenger, @hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "posts back a proxy status when a method is called on it" do
|
12
|
+
# then
|
13
|
+
@messenger.should_receive(:post).
|
14
|
+
with(:status => 'proxy', :data => {:hash => @hash, :call => [:to_s]})
|
15
|
+
|
16
|
+
# when
|
17
|
+
@messenger.stub!(:get).and_return(:status => 'ok', :data => 'foo')
|
18
|
+
RobotArmy::Connection.stub!(:handle_response)
|
19
|
+
@proxy.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns the value returned by a successful incoming message" do
|
23
|
+
# when
|
24
|
+
@messenger.stub!(:get).and_return(:status => 'ok', :data => 'bar')
|
25
|
+
|
26
|
+
# then
|
27
|
+
@proxy.to_s.should == 'bar'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "lets exceptions bubble up from handling the message" do
|
31
|
+
# when
|
32
|
+
RobotArmy::Connection.stub!(:handle_response).and_raise
|
33
|
+
|
34
|
+
# then
|
35
|
+
proc { @proxy.to_s }.should raise_error
|
36
|
+
end
|
37
|
+
|
38
|
+
it "returns a new proxy if the response has status 'proxy'" do
|
39
|
+
# then
|
40
|
+
RobotArmy::Proxy.should_receive(:new).
|
41
|
+
with(@messenger, @hash)
|
42
|
+
|
43
|
+
# when
|
44
|
+
@messenger.stub!(:get).and_return(:status => 'proxy', :data => @hash)
|
45
|
+
@proxy.me
|
46
|
+
end
|
47
|
+
|
48
|
+
it "can generate Ruby code to create a Proxy for an object" do
|
49
|
+
RobotArmy::Proxy.generator_for(self).
|
50
|
+
should == "RobotArmy::Proxy.new(RobotArmy.upstream, #{self.hash.inspect})"
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Proc, "to_ruby" do
|
4
|
+
before do
|
5
|
+
@proc = proc{ 1 }
|
6
|
+
end
|
7
|
+
|
8
|
+
it "can render itself as ruby not enclosed in a proc" do
|
9
|
+
@proc.to_ruby_without_proc_wrapper.should == "1"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can render itself as ruby that evaluates to a Proc" do
|
13
|
+
@proc.to_ruby.should == "proc { 1 }"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can get a list of arguments" do
|
17
|
+
proc{ |a, b| a + b }.arguments.should == %w[a b]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MethodToRubyFixture
|
22
|
+
def one
|
23
|
+
1
|
24
|
+
end
|
25
|
+
|
26
|
+
def echo(a)
|
27
|
+
a
|
28
|
+
end
|
29
|
+
|
30
|
+
def add(a, b)
|
31
|
+
a + b
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Method, "to_ruby" do
|
36
|
+
before do
|
37
|
+
@method = MethodToRubyFixture.new.method(:one)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can render itself as ruby that executes itself" do
|
41
|
+
@method.to_ruby_without_method_declaration.should =~ /\A\s*1\s*\Z/
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can render itself as ruby that evaluates to a Method" do
|
45
|
+
@method.to_ruby.should == "def one\n 1\nend"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe Method, "arguments" do
|
50
|
+
before do
|
51
|
+
@no_args = MethodToRubyFixture.new.method(:one)
|
52
|
+
@one_arg = MethodToRubyFixture.new.method(:echo)
|
53
|
+
@many_args = MethodToRubyFixture.new.method(:add)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns an empty list for a method without arguments" do
|
57
|
+
@no_args.arguments.should == []
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns a single argument for a method with a single argument" do
|
61
|
+
@one_arg.arguments.should == %w[a]
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns a comma-separated list of arguments when there are many args" do
|
65
|
+
@many_args.arguments.should == %w[a b]
|
66
|
+
end
|
67
|
+
end
|