robot-army 0.1.8
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/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
|