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