robot-army 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENSE +13 -0
  2. data/README.markdown +34 -0
  3. data/Rakefile +9 -0
  4. data/examples/whoami.rb +13 -0
  5. data/lib/robot-army.rb +109 -0
  6. data/lib/robot-army/at_exit.rb +19 -0
  7. data/lib/robot-army/connection.rb +174 -0
  8. data/lib/robot-army/dependency_loader.rb +38 -0
  9. data/lib/robot-army/eval_builder.rb +84 -0
  10. data/lib/robot-army/eval_command.rb +17 -0
  11. data/lib/robot-army/gate_keeper.rb +28 -0
  12. data/lib/robot-army/io.rb +106 -0
  13. data/lib/robot-army/keychain.rb +10 -0
  14. data/lib/robot-army/loader.rb +85 -0
  15. data/lib/robot-army/marshal_ext.rb +52 -0
  16. data/lib/robot-army/messenger.rb +31 -0
  17. data/lib/robot-army/officer.rb +35 -0
  18. data/lib/robot-army/officer_connection.rb +5 -0
  19. data/lib/robot-army/officer_loader.rb +13 -0
  20. data/lib/robot-army/proxy.rb +35 -0
  21. data/lib/robot-army/remote_evaler.rb +59 -0
  22. data/lib/robot-army/ruby2ruby_ext.rb +19 -0
  23. data/lib/robot-army/soldier.rb +37 -0
  24. data/lib/robot-army/task_master.rb +317 -0
  25. data/spec/at_exit_spec.rb +25 -0
  26. data/spec/connection_spec.rb +126 -0
  27. data/spec/dependency_loader_spec.rb +46 -0
  28. data/spec/gate_keeper_spec.rb +46 -0
  29. data/spec/integration_spec.rb +40 -0
  30. data/spec/io_spec.rb +36 -0
  31. data/spec/keychain_spec.rb +15 -0
  32. data/spec/loader_spec.rb +13 -0
  33. data/spec/marshal_ext_spec.rb +89 -0
  34. data/spec/messenger_spec.rb +28 -0
  35. data/spec/officer_spec.rb +36 -0
  36. data/spec/proxy_spec.rb +52 -0
  37. data/spec/ruby2ruby_ext_spec.rb +67 -0
  38. data/spec/soldier_spec.rb +71 -0
  39. data/spec/spec_helper.rb +19 -0
  40. data/spec/task_master_spec.rb +306 -0
  41. metadata +142 -0
@@ -0,0 +1,71 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RobotArmy::Soldier do
4
+ before do
5
+ # given
6
+ @messenger = mock(:messenger)
7
+ @soldier = RobotArmy::Soldier.new(@messenger)
8
+ end
9
+
10
+ it "can accept eval commands" do
11
+ # then
12
+ @soldier.run(:eval, :code => '3+4', :file => __FILE__, :line => __LINE__).
13
+ should == 7
14
+
15
+ # and
16
+ @soldier.run(:eval, :code => 'Time.now', :file => __FILE__, :line => __LINE__).
17
+ should be_an_instance_of(Time)
18
+ end
19
+
20
+ it "evaluates each command in the same process" do
21
+ # when
22
+ pid = proc{ @soldier.run(:eval, :code => 'Process.pid', :file => __FILE__, :line => __LINE__) }
23
+
24
+ # then
25
+ pid.call.should == pid.call
26
+ end
27
+
28
+ it "raises on unrecognized commands" do
29
+ proc{ @soldier.run(:foo, nil) }.should raise_error(ArgumentError)
30
+ end
31
+
32
+ it "listens for commands from the messenger to run" do
33
+ # then
34
+ @soldier.should_receive(:run).with(:eval, :code => 'Hash.new')
35
+
36
+ # when
37
+ @messenger.stub!(:post)
38
+ @messenger.stub!(:get).and_return(:command => :eval, :data => {:code => 'Hash.new'})
39
+ @soldier.listen
40
+ end
41
+
42
+ it "posts through the messenger the result of commands run by listening" do
43
+ # then
44
+ @messenger.should_receive(:post).with(:status => 'ok', :data => 1)
45
+
46
+ # when
47
+ @messenger.stub!(:get).and_return(:command => :eval, :data => {:code => '1'})
48
+ @soldier.stub!(:run).and_return(1)
49
+ @soldier.listen
50
+ end
51
+
52
+ it "posts back and raises RobotArmy::Exit when running the exit command" do
53
+ @messenger.should_receive(:post).with(:status => 'ok')
54
+ proc{ @soldier.run(:exit, nil) }.should raise_error(RobotArmy::Exit)
55
+ end
56
+
57
+ it "returns the pid and type when asked for info" do
58
+ @soldier.run(:info, nil).should == {:pid => Process.pid, :type => 'RobotArmy::Soldier'}
59
+ end
60
+
61
+ it "posts back a warning if the :eval return value is not marshalable" do
62
+ # then
63
+ @messenger.should_receive(:post).
64
+ with(:status => 'warning', :data => "ignoring invalid remote return value #{$stdin.inspect}")
65
+
66
+ # when
67
+ @messenger.stub!(:get).and_return(
68
+ :command => :eval, :data => {:code => '$stdin', :file => __FILE__, :line => __LINE__})
69
+ @soldier.listen
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ $TESTING=true
2
+ load File.join(File.dirname(__FILE__), '..', 'lib', 'robot-army.rb')
3
+
4
+ Spec::Runner.configure do |config|
5
+ def capture(stream)
6
+ begin
7
+ stream = stream.to_s
8
+ eval "$#{stream} = StringIO.new"
9
+ yield
10
+ result = eval("$#{stream}").string
11
+ ensure
12
+ eval("$#{stream} = #{stream.upcase}")
13
+ end
14
+
15
+ result
16
+ end
17
+
18
+ alias silence capture
19
+ end
@@ -0,0 +1,306 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class Example < RobotArmy::TaskMaster
4
+ hosts %[www1.example.com www2.example.com]
5
+ end
6
+
7
+ class Localhost < RobotArmy::TaskMaster
8
+ host :localhost
9
+ end
10
+
11
+ describe RobotArmy::TaskMaster, 'host management' do
12
+ before do
13
+ @example = Example.new
14
+ end
15
+
16
+ it "allows setting a single host" do
17
+ Example.host 'example.com'
18
+ Example.host.should == 'example.com'
19
+ end
20
+
21
+ it "allows accessing multi-hosts when using the single-host interface" do
22
+ Example.host 'example.com'
23
+ Example.hosts.should == %w[example.com]
24
+ end
25
+
26
+ it "allows setting multiple hosts on the class" do
27
+ Example.hosts %w[example.com test.com]
28
+ Example.hosts.should == %w[example.com test.com]
29
+ end
30
+
31
+ it "denies accessing a single host when using the multi-host interface" do
32
+ Example.hosts %w[example.com test.com]
33
+ proc { Example.host }.should raise_error(
34
+ RobotArmy::HostArityError, "There are 2 hosts, so calling host doesn't make sense")
35
+ end
36
+
37
+ it "instances default to the hosts set on the class" do
38
+ Example.host 'example.com'
39
+ @example.host.should == 'example.com'
40
+
41
+ Example.hosts %w[example.com test.com]
42
+ @example.hosts.should == %w[example.com test.com]
43
+ end
44
+
45
+ it "allows setting a single host on an instance" do
46
+ @example.host = 'example.com'
47
+ @example.host.should == 'example.com'
48
+ end
49
+
50
+ it "allows accessing multi-hosts when using the single-host interface on instances" do
51
+ @example.host = 'example.com'
52
+ @example.hosts.should == %w[example.com]
53
+ end
54
+
55
+ it "allows setting multiple hosts on an instance" do
56
+ @example.hosts = %w[example.com test.com]
57
+ @example.hosts.should == %w[example.com test.com]
58
+ end
59
+
60
+ it "denies accessing a single host when using the multi-host interface" do
61
+ @example.hosts = %w[example.com test.com test2.com]
62
+ proc { @example.host }.should raise_error(
63
+ RobotArmy::HostArityError, "There are 3 hosts, so calling host doesn't make sense")
64
+ end
65
+ end
66
+
67
+ describe RobotArmy::TaskMaster, 'remote' do
68
+ before do
69
+ @localhost = Localhost.new
70
+ @example = Example.new
71
+ end
72
+
73
+ it "returns a single item when using the single-host interface" do
74
+ @localhost.stub!(:remote_eval).and_return(7)
75
+ @localhost.remote { 3+4 }.should == 7
76
+ end
77
+
78
+ it "returns an array of items when using the multi-host interface" do
79
+ @example.stub!(:remote_eval).and_return(7)
80
+ @example.remote { 3+4 }.should == [7, 7]
81
+ end
82
+ end
83
+
84
+ describe RobotArmy::TaskMaster do
85
+ before do
86
+ @localhost = Localhost.new
87
+ @example = Example.new
88
+ end
89
+
90
+ it "runs a remote block on each host" do
91
+ @example.should_receive(:remote_eval).exactly(2).times
92
+ @example.remote { 3+4 }
93
+ end
94
+
95
+
96
+ it "can execute a Ruby block and return the result" do
97
+ @localhost.remote { 3+4 }.should == 7
98
+ end
99
+
100
+ it "executes its block in a different process" do
101
+ @localhost.remote { Process.pid }.should_not == Process.pid
102
+ end
103
+
104
+ it "preserves local variables" do
105
+ a = 42
106
+ @localhost.remote { a }.should == 42
107
+ end
108
+
109
+ it "warns about invalid remote return values" do
110
+ capture(:stderr) { @localhost.remote { $stdin } }.
111
+ should =~ /WARNING: ignoring invalid remote return value/
112
+ end
113
+
114
+ it "returns nil if the remote return value is invalid" do
115
+ silence(:stderr) { @localhost.remote { $stdin }.should be_nil }
116
+ end
117
+
118
+ it "re-raises exceptions thrown remotely" do
119
+ proc { @localhost.remote { raise ArgumentError, "You fool!" } }.
120
+ should raise_error(ArgumentError)
121
+ end
122
+
123
+ it "prints the child Ruby's stderr to stderr" do
124
+ pending('we may not want to do this, even')
125
+ capture(:stderr) { @localhost.remote { $stderr.print "foo" } }.should == "foo"
126
+ end
127
+
128
+ it "runs multiple remote blocks for the same host in different processes" do
129
+ @localhost.remote { $a = 1 }
130
+ @localhost.remote { $a }.should be_nil
131
+ end
132
+
133
+ it "only loads one Officer process on the remote machine" do
134
+ info = @localhost.connection(@localhost.host).info
135
+ info[:pid].should_not == Process.pid
136
+ info[:type].should == 'RobotArmy::Officer'
137
+ @localhost.connection(@localhost.host).info.should == info
138
+ end
139
+
140
+ it "runs as a normal (non-super) user by default" do
141
+ @localhost.remote{ Process.uid }.should_not == 0
142
+ end
143
+
144
+ it "loads dependencies" do
145
+ @localhost.dependency "thor"
146
+ @localhost.remote { Thor ; 45 }.should == 45 # loading should not bail here
147
+ end
148
+
149
+ it "delegates scp to the scp binary" do
150
+ @localhost.should_receive(:`).with('scp -q file.tgz example.com:/tmp 2>&1')
151
+ @localhost.host = 'example.com'
152
+ @localhost.scp 'file.tgz', '/tmp'
153
+ end
154
+
155
+ it "delegates to scp without a host when host is localhost" do
156
+ @localhost.should_receive(:`).with('scp -q file.tgz /tmp 2>&1')
157
+ @localhost.scp 'file.tgz', '/tmp'
158
+ end
159
+ end
160
+
161
+ describe RobotArmy::TaskMaster, 'scp' do
162
+ before do
163
+ @localhost = Localhost.new
164
+ end
165
+
166
+ it "raises if scp fails due to a permissions error" do
167
+ @localhost.stub!(:`).and_return("scp: /tmp/foo: Permission denied\n")
168
+ $?.stub!(:exitstatus).and_return(1)
169
+ lambda { @localhost.scp('foo', '/tmp') }.should raise_error(Errno::EACCES)
170
+ end
171
+
172
+ it "raises if scp cannot locate the source file" do
173
+ lambda { @localhost.scp('i-dont-exist', '/tmp') }.should raise_error(Errno::ENOENT)
174
+ end
175
+ end
176
+
177
+ describe RobotArmy::TaskMaster, 'sh' do
178
+ before do
179
+ @localhost = Localhost.new
180
+ end
181
+
182
+ it "raises exceptions on failed commands" do
183
+ lambda {
184
+ @localhost.remote { sh 'which this-command-does-not-exist' }
185
+ }.should raise_error(RobotArmy::ShellCommandError)
186
+ end
187
+
188
+ it "does not raise exceptions on successful commands" do
189
+ lambda {
190
+ @localhost.remote { sh 'echo foo' }
191
+ }.should_not raise_error(RobotArmy::ShellCommandError)
192
+ end
193
+
194
+ it "includes stderr from failed commands" do
195
+ begin
196
+ @localhost.remote { sh 'ruby -e "STDERR.puts %{stderr message}; exit(1)"' }
197
+ rescue RobotArmy::ShellCommandError => e
198
+ e.output.should == "stderr message\n"
199
+ end
200
+ end
201
+
202
+ it "includes stdout from failed commands" do
203
+ begin
204
+ @localhost.remote { sh 'ruby -e "STDOUT.puts %{stdout message}; exit(1)"' }
205
+ rescue RobotArmy::ShellCommandError => e
206
+ e.output.should == "stdout message\n"
207
+ end
208
+ end
209
+ end
210
+
211
+ describe RobotArmy::TaskMaster, 'remote (with args)' do
212
+ before do
213
+ @localhost = Localhost.new
214
+ end
215
+
216
+ it "can pass arguments explicitly" do
217
+ @localhost.remote(:args => [42]) { |a| a }.should == 42
218
+ end
219
+
220
+ it "shadows local variables of the same name" do
221
+ a = 23
222
+ @localhost.remote(:args => [42]) { |a| a }.should == 42
223
+ end
224
+ end
225
+
226
+ describe RobotArmy::TaskMaster, 'cptemp' do
227
+ before do
228
+ @localhost = Localhost.new
229
+ @path = 'cptemp-spec-file'
230
+ File.open(@path, 'w') {|f| f << 'testing'}
231
+ end
232
+
233
+ it "safely copies to a new temporary directory" do
234
+ destination = @localhost.cptemp @path
235
+ File.read(destination).should == 'testing'
236
+ end
237
+
238
+ it "yields the path to each host if a block is passed" do
239
+ path, pid = @localhost.cptemp(@path) { |path| [path, Process.pid] }
240
+ File.basename(path).should == @path
241
+ pid.should_not be_nil
242
+ pid.should_not == Process.pid
243
+ end
244
+
245
+ it "deletes the file on exit" do
246
+ destination = @localhost.cptemp @path
247
+ RobotArmy::AtExit.shared_instance.do_exit
248
+ fail "Expected cptemp'ed file to be deleted when exit callbacks were run" if File.exist?(destination)
249
+ end
250
+
251
+ after do
252
+ FileUtils.rm_f(@path)
253
+ end
254
+ end
255
+
256
+ describe RobotArmy::TaskMaster, 'with proxies' do
257
+ before do
258
+ @localhost = Localhost.new
259
+ end
260
+
261
+ it "can allow remote method calls on the local object" do
262
+ def @localhost.foo; 'bar'; end
263
+ @localhost.remote { foo }.should == 'bar'
264
+ end
265
+
266
+ it "allows calling methods with arguments" do
267
+ def @localhost.echo(o) o; end
268
+ @localhost.remote { echo 42 }.should == 42
269
+ end
270
+
271
+ it "allows passing a block to method calls on proxy objects" do
272
+ pending('this is insane. should I do this?')
273
+ end
274
+
275
+ it "allows interaction with IOs" do
276
+ capture(:stdout) {
277
+ stdout = $stdout
278
+ @localhost.remote { stdout.puts "hey there" }
279
+ }.should == "hey there\n"
280
+ end
281
+
282
+ it "returns a proxy if the return value of an upstream call can't be marshaled" do
283
+ def @localhost.stdout; $stdout; end
284
+ capture(:stdout) { @localhost.remote { stdout.puts "foo" } }.should == "foo\n"
285
+ end
286
+ end
287
+
288
+ describe RobotArmy::TaskMaster, 'sudo' do
289
+ before do
290
+ @localhost = Localhost.new
291
+ end
292
+
293
+ it "runs remote with the root user by default" do
294
+ @localhost.should_receive(:remote).
295
+ with(@localhost.hosts, :user => 'root')
296
+
297
+ @localhost.sudo { File.read('/etc/passwd') }
298
+ end
299
+
300
+ it "allows specifying a particular user" do
301
+ @localhost.should_receive(:remote).
302
+ with(@localhost.hosts, :user => 'www-data')
303
+
304
+ @localhost.sudo(:user => 'www-data') { %x{/etc/init.d/apache2 restart} }
305
+ end
306
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: robot-army
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 8
9
+ version: 0.1.8
10
+ platform: ruby
11
+ authors:
12
+ - Brian Donovan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-26 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ParseTree
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ - 0
31
+ version: 3.0.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: ruby2ruby
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 2
44
+ - 0
45
+ version: 1.2.0
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: thor
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ - 11
58
+ - 7
59
+ version: 0.11.7
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ description: Deploy using Thor by executing Ruby remotely
63
+ email: brian@wesabe.com
64
+ executables: []
65
+
66
+ extensions: []
67
+
68
+ extra_rdoc_files:
69
+ - LICENSE
70
+ - README.markdown
71
+ files:
72
+ - LICENSE
73
+ - README.markdown
74
+ - Rakefile
75
+ - lib/robot-army.rb
76
+ - lib/robot-army/at_exit.rb
77
+ - lib/robot-army/connection.rb
78
+ - lib/robot-army/dependency_loader.rb
79
+ - lib/robot-army/eval_builder.rb
80
+ - lib/robot-army/eval_command.rb
81
+ - lib/robot-army/gate_keeper.rb
82
+ - lib/robot-army/io.rb
83
+ - lib/robot-army/keychain.rb
84
+ - lib/robot-army/loader.rb
85
+ - lib/robot-army/marshal_ext.rb
86
+ - lib/robot-army/messenger.rb
87
+ - lib/robot-army/officer.rb
88
+ - lib/robot-army/officer_connection.rb
89
+ - lib/robot-army/officer_loader.rb
90
+ - lib/robot-army/proxy.rb
91
+ - lib/robot-army/remote_evaler.rb
92
+ - lib/robot-army/ruby2ruby_ext.rb
93
+ - lib/robot-army/soldier.rb
94
+ - lib/robot-army/task_master.rb
95
+ has_rdoc: true
96
+ homepage: http://github.com/wesabe/robot-army
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options:
101
+ - --charset=UTF-8
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project: robot-army
121
+ rubygems_version: 1.3.6
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Deploy using Thor by executing Ruby remotely
125
+ test_files:
126
+ - spec/at_exit_spec.rb
127
+ - spec/connection_spec.rb
128
+ - spec/dependency_loader_spec.rb
129
+ - spec/gate_keeper_spec.rb
130
+ - spec/integration_spec.rb
131
+ - spec/io_spec.rb
132
+ - spec/keychain_spec.rb
133
+ - spec/loader_spec.rb
134
+ - spec/marshal_ext_spec.rb
135
+ - spec/messenger_spec.rb
136
+ - spec/officer_spec.rb
137
+ - spec/proxy_spec.rb
138
+ - spec/ruby2ruby_ext_spec.rb
139
+ - spec/soldier_spec.rb
140
+ - spec/spec_helper.rb
141
+ - spec/task_master_spec.rb
142
+ - examples/whoami.rb