wesabe-robot-army 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.markdown +36 -0
- data/Rakefile +9 -0
- data/lib/robot-army/connection.rb +106 -0
- data/lib/robot-army/gate_keeper.rb +22 -0
- data/lib/robot-army/loader.rb +83 -0
- data/lib/robot-army/messenger.rb +32 -0
- data/lib/robot-army/officer.rb +14 -0
- data/lib/robot-army/ruby2ruby_ext.rb +24 -0
- data/lib/robot-army/soldier.rb +27 -0
- data/lib/robot-army/task_master.rb +65 -0
- data/lib/robot-army.rb +26 -0
- metadata +83 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2007 Wesabe, Inc.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.markdown
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Robot Army
|
2
|
+
==========
|
3
|
+
|
4
|
+
Robot Army is deploy scripting which offers remote execution of Ruby in addition to the usual shell scripting offered by other deploy packages.
|
5
|
+
|
6
|
+
If you want to test this, be sure that the `robot-army` gem is installed on *both* the client and server machines. You should get an error if you try to execute it against a server with it installed.
|
7
|
+
|
8
|
+
Example
|
9
|
+
-------
|
10
|
+
|
11
|
+
class AppServer < RobotArmy::TaskMaster
|
12
|
+
host 'app1.prod.example.com'
|
13
|
+
|
14
|
+
desc "time", "Get the time on the server (delta will be slightly off depending on SSH delay)"
|
15
|
+
def time
|
16
|
+
rtime = remote{ Time.now }
|
17
|
+
ltime = Time.now
|
18
|
+
|
19
|
+
say "The time on #{host} is #{rtime}, " +
|
20
|
+
"#{(rtime-ltime).abs} seconds #{rtime < ltime ? 'behind' : 'ahead of'} localhost"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "deployed_revision", "Gets the deployed revision"
|
24
|
+
def deployed_revision
|
25
|
+
say "Checking deployed revision on #{host}"
|
26
|
+
say "Deployed revision: #{remote{ File.read("/opt/app/current/REVISION") }}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Known Issues
|
31
|
+
------------
|
32
|
+
|
33
|
+
* No attempt is made to support `sudo` yet
|
34
|
+
* Code executed in `remote` has no access to instance variables, globals, or methods on `self`
|
35
|
+
* Multiple hosts are not yet supported
|
36
|
+
* Probably doesn't work with Windows
|
data/Rakefile
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
class RobotArmy::Connection
|
2
|
+
attr_reader :host, :messenger
|
3
|
+
|
4
|
+
def initialize(host)
|
5
|
+
@host = host
|
6
|
+
@closed = true
|
7
|
+
end
|
8
|
+
|
9
|
+
def loader
|
10
|
+
@loader ||= RobotArmy::Loader.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def open(&block)
|
14
|
+
start_child if closed?
|
15
|
+
@closed = false
|
16
|
+
unless block_given?
|
17
|
+
return self
|
18
|
+
else
|
19
|
+
begin
|
20
|
+
return yield(self)
|
21
|
+
ensure
|
22
|
+
close unless closed?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_child
|
28
|
+
begin
|
29
|
+
##
|
30
|
+
## bootstrap the child process
|
31
|
+
##
|
32
|
+
|
33
|
+
# small hack to retain control of stdin
|
34
|
+
cmd = %{ruby -rbase64 -e "eval(Base64.decode64(STDIN.gets(%(|))))"}
|
35
|
+
cmd = "ssh #{host} '#{cmd}'" if host
|
36
|
+
|
37
|
+
stdin, stdout, stderr = Open3.popen3 cmd
|
38
|
+
stdin.sync = stdout.sync = stderr.sync = true
|
39
|
+
|
40
|
+
loader.libraries.replace $TESTING ?
|
41
|
+
[File.join(File.dirname(__FILE__), '..', 'robot-army')] : %w[rubygems robot-army]
|
42
|
+
|
43
|
+
ruby = loader.render
|
44
|
+
code = Base64.encode64(ruby)
|
45
|
+
stdin << code << '|'
|
46
|
+
|
47
|
+
|
48
|
+
##
|
49
|
+
## make sure it was loaded okay
|
50
|
+
##
|
51
|
+
|
52
|
+
@messenger = RobotArmy::Messenger.new(stdout, stdin)
|
53
|
+
response = messenger.get
|
54
|
+
|
55
|
+
if response
|
56
|
+
case response[:status]
|
57
|
+
when 'error'
|
58
|
+
$stderr.puts "Error trying to execute: #{ruby.gsub(/^/, ' ')}\n"
|
59
|
+
raise response[:data]
|
60
|
+
when 'ok'
|
61
|
+
# yay! established connection
|
62
|
+
end
|
63
|
+
else
|
64
|
+
# try to get stderr
|
65
|
+
begin
|
66
|
+
require 'timeout'
|
67
|
+
err = timeout(1){ "process stderr: #{stderr.read}" }
|
68
|
+
rescue Timeout::Error
|
69
|
+
err = 'additionally, failed to get stderr'
|
70
|
+
end
|
71
|
+
|
72
|
+
raise "Failed to start remote ruby process. #{err}"
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
## finish up
|
77
|
+
##
|
78
|
+
|
79
|
+
@closed = false
|
80
|
+
rescue Object => e
|
81
|
+
$stderr.puts "Failed to establish connection to #{host}: #{e.message}"
|
82
|
+
raise e
|
83
|
+
ensure
|
84
|
+
@closed = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def post(*args)
|
89
|
+
messenger.post(*args)
|
90
|
+
end
|
91
|
+
|
92
|
+
def closed?
|
93
|
+
@closed
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
raise RobotArmy::ConnectionNotOpen if closed?
|
98
|
+
messenger.post(:command => :exit)
|
99
|
+
@closed = true
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.localhost(&block)
|
103
|
+
conn = new(nil)
|
104
|
+
block ? conn.open(&block) : conn
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class RobotArmy::GateKeeper
|
2
|
+
def connect(host)
|
3
|
+
connections[host] ||= establish_connection(host)
|
4
|
+
end
|
5
|
+
|
6
|
+
def establish_connection(host)
|
7
|
+
connection = connections[host] = RobotArmy::Connection.new(host)
|
8
|
+
connection.open
|
9
|
+
end
|
10
|
+
|
11
|
+
def connections
|
12
|
+
@connections ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
connections.each { |host,c| c.close unless c.closed? }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.shared_instance
|
20
|
+
@shared_instance ||= new
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class RobotArmy::Loader
|
2
|
+
attr_accessor :messenger
|
3
|
+
|
4
|
+
def libraries
|
5
|
+
@libraries ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def render
|
9
|
+
%{
|
10
|
+
begin
|
11
|
+
##
|
12
|
+
## setup
|
13
|
+
##
|
14
|
+
|
15
|
+
$stdout.sync = $stdin.sync = true
|
16
|
+
#{libraries.map{|l| "require #{l.inspect}"}.join("\n")}
|
17
|
+
|
18
|
+
|
19
|
+
##
|
20
|
+
## local Robot Army objects to communicate with the parent
|
21
|
+
##
|
22
|
+
|
23
|
+
loader = RobotArmy::Loader.new
|
24
|
+
loader.messenger = RobotArmy::Messenger.new($stdin, $stdout)
|
25
|
+
loader.messenger.post(:status => 'ok')
|
26
|
+
|
27
|
+
##
|
28
|
+
## event loop
|
29
|
+
##
|
30
|
+
|
31
|
+
loader.load
|
32
|
+
rescue Object => e
|
33
|
+
##
|
34
|
+
## exception handler of last resort
|
35
|
+
##
|
36
|
+
|
37
|
+
if defined?(RobotArmy::Exit) && e.is_a?(RobotArmy::Exit)
|
38
|
+
# don't stomp on our own "let me out" exception
|
39
|
+
exit(e.status)
|
40
|
+
else
|
41
|
+
# if we got here that means something up to and including loader.load
|
42
|
+
# went unexpectedly wrong. this could be a missing library, or it
|
43
|
+
# could be a bug in Robot Army. either way we should report the error
|
44
|
+
# back to the place we came from so that they may re-raise the exception
|
45
|
+
|
46
|
+
# a little bit of un-DRY
|
47
|
+
print Base64.encode64(Marshal.dump(:status => 'error', :data => e))+'|'
|
48
|
+
exit(1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def safely
|
55
|
+
begin
|
56
|
+
return yield, true
|
57
|
+
rescue RobotArmy::Exit
|
58
|
+
# let RobotArmy::Exit through
|
59
|
+
raise
|
60
|
+
rescue Object => e
|
61
|
+
messenger.post(:status => 'error', :data => e)
|
62
|
+
return nil, false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def safely_or_die(&block)
|
67
|
+
retval, success = safely(&block)
|
68
|
+
exit(1) unless success
|
69
|
+
return retval
|
70
|
+
end
|
71
|
+
|
72
|
+
def load
|
73
|
+
# create a soldier
|
74
|
+
soldier = safely_or_die{ RobotArmy::Soldier.new(messenger) }
|
75
|
+
|
76
|
+
# use the soldier to start listening to incoming commands
|
77
|
+
# at this point everything has been loaded successfully, so we
|
78
|
+
# don't have to exit if an exception is thrown
|
79
|
+
loop do
|
80
|
+
safely{ soldier.listen }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RobotArmy
|
2
|
+
class Messenger
|
3
|
+
attr_reader :input, :output
|
4
|
+
|
5
|
+
def initialize(input, output)
|
6
|
+
@input, @output = input, output
|
7
|
+
end
|
8
|
+
|
9
|
+
def post(response)
|
10
|
+
debug "post(#{response.inspect})"
|
11
|
+
dump = Marshal.dump(response)
|
12
|
+
dump = Base64.encode64(dump) + '|'
|
13
|
+
output << dump
|
14
|
+
end
|
15
|
+
|
16
|
+
def get
|
17
|
+
data = nil
|
18
|
+
loop do
|
19
|
+
case data = input.gets('|')
|
20
|
+
when nil, ''
|
21
|
+
return nil
|
22
|
+
when /^\s*$/
|
23
|
+
# again!
|
24
|
+
else
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
data = Base64.decode64(data.chop)
|
29
|
+
Marshal.load(data)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class RobotArmy::Officer < RobotArmy::Soldier
|
2
|
+
def run(command, data)
|
3
|
+
case command
|
4
|
+
when :eval
|
5
|
+
RobotArmy::Connection.localhost do |local|
|
6
|
+
local.post(:command => command, :data => data)
|
7
|
+
end
|
8
|
+
when :exit
|
9
|
+
super
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Proc
|
2
|
+
def to_ruby_with_body_flag(only_body=false)
|
3
|
+
ruby = to_ruby_without_body_flag
|
4
|
+
only_body ? "#{ruby}.call" : ruby
|
5
|
+
end
|
6
|
+
|
7
|
+
alias :to_ruby_without_body_flag :to_ruby
|
8
|
+
alias :to_ruby :to_ruby_with_body_flag
|
9
|
+
end
|
10
|
+
|
11
|
+
class Method
|
12
|
+
def to_ruby_with_body_flag(only_body=false)
|
13
|
+
ruby = self.to_ruby_without_body_flag
|
14
|
+
if only_body
|
15
|
+
ruby.sub!(/\A(def \S+)(?:\(([^\)]*)\))?/, '') # move args
|
16
|
+
ruby.sub!(/end\Z/, '') # strip end
|
17
|
+
end
|
18
|
+
ruby.gsub!(/\s+$/, '') # trailing WS bugs me
|
19
|
+
ruby
|
20
|
+
end
|
21
|
+
|
22
|
+
alias :to_ruby_without_body_flag :to_ruby
|
23
|
+
alias :to_ruby :to_ruby_with_body_flag
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class RobotArmy::Soldier
|
2
|
+
attr_reader :messenger
|
3
|
+
|
4
|
+
def initialize(messenger)
|
5
|
+
@messenger = messenger
|
6
|
+
end
|
7
|
+
|
8
|
+
def listen
|
9
|
+
request = messenger.get
|
10
|
+
result = run(request[:command], request[:data])
|
11
|
+
response = {:status => 'ok', :data => result}
|
12
|
+
messenger.post response
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(command, data)
|
16
|
+
case command
|
17
|
+
when :eval
|
18
|
+
instance_eval(data[:code], data[:file], data[:line])
|
19
|
+
when :exit
|
20
|
+
# tell the parent we're okay before we exit
|
21
|
+
messenger.post(:status => 'ok')
|
22
|
+
raise RobotArmy::Exit
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Unrecognized command #{command.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RobotArmy
|
2
|
+
class TaskMaster < Thor
|
3
|
+
def self.host(host=nil)
|
4
|
+
@host = host if host
|
5
|
+
@host
|
6
|
+
end
|
7
|
+
|
8
|
+
def host
|
9
|
+
self.class.host
|
10
|
+
end
|
11
|
+
|
12
|
+
def say(something)
|
13
|
+
puts "** #{something}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def connection
|
17
|
+
RobotArmy::GateKeeper.shared_instance.connect(host)
|
18
|
+
end
|
19
|
+
|
20
|
+
def remote(host=self.host, &proc)
|
21
|
+
##
|
22
|
+
## build the code to send it
|
23
|
+
##
|
24
|
+
|
25
|
+
# fix stack traces
|
26
|
+
file, line = eval('[__FILE__, __LINE__]', proc.binding)
|
27
|
+
|
28
|
+
# include local variables
|
29
|
+
locals = eval('local_variables', proc.binding).map do |name|
|
30
|
+
"#{name} = Marshal.load(#{Marshal.dump(eval(name, proc.binding)).inspect})"
|
31
|
+
end
|
32
|
+
|
33
|
+
code = %{
|
34
|
+
#{locals.join("\n")} # all local variables
|
35
|
+
#{proc.to_ruby(true)} # the proc itself
|
36
|
+
}
|
37
|
+
|
38
|
+
|
39
|
+
##
|
40
|
+
## send the child a message
|
41
|
+
##
|
42
|
+
|
43
|
+
connection.messenger.post(:command => :eval, :data => {
|
44
|
+
:code => code,
|
45
|
+
:file => file,
|
46
|
+
:line => line
|
47
|
+
})
|
48
|
+
|
49
|
+
##
|
50
|
+
## get and evaluate the response
|
51
|
+
##
|
52
|
+
|
53
|
+
response = connection.messenger.get
|
54
|
+
|
55
|
+
case response[:status]
|
56
|
+
when 'ok'
|
57
|
+
return response[:data]
|
58
|
+
when 'error'
|
59
|
+
raise response[:data]
|
60
|
+
else
|
61
|
+
raise RuntimeError, "Unknown response status from remote process: #{response[:status]}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/robot-army.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
%w[rubygems open3 base64 thor ruby2ruby].each do |library|
|
2
|
+
require library
|
3
|
+
end
|
4
|
+
|
5
|
+
module RobotArmy
|
6
|
+
class ConnectionNotOpen < StandardError; end
|
7
|
+
class RobotArmy::Exit < Exception
|
8
|
+
attr_accessor :status
|
9
|
+
|
10
|
+
def initialize(status=0)
|
11
|
+
@status = status
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
%w[loader soldier officer messenger task_master connection gate_keeper ruby2ruby_ext].each do |file|
|
17
|
+
require File.join(File.dirname(__FILE__), 'robot-army', file)
|
18
|
+
end
|
19
|
+
|
20
|
+
at_exit do
|
21
|
+
RobotArmy::GateKeeper.shared_instance.close
|
22
|
+
end
|
23
|
+
|
24
|
+
def debug(*whatever)
|
25
|
+
File.open('/tmp/robot-army', 'a') { |f| f.puts "[#{Process.pid}] #{whatever.join(' ')}" }
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wesabe-robot-army
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Donovan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-20 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ruby2ruby
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.1.7
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: thor
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 0.0.0
|
32
|
+
version:
|
33
|
+
description: Deploy using Thor by executing Ruby remotely
|
34
|
+
email: brian@wesabe.com
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.markdown
|
41
|
+
- LICENSE
|
42
|
+
files:
|
43
|
+
- LICENSE
|
44
|
+
- README.markdown
|
45
|
+
- Rakefile
|
46
|
+
- lib/robot-army
|
47
|
+
- lib/robot-army/connection.rb
|
48
|
+
- lib/robot-army/gate_keeper.rb
|
49
|
+
- lib/robot-army/loader.rb
|
50
|
+
- lib/robot-army/messenger.rb
|
51
|
+
- lib/robot-army/officer.rb
|
52
|
+
- lib/robot-army/ruby2ruby_ext.rb
|
53
|
+
- lib/robot-army/soldier.rb
|
54
|
+
- lib/robot-army/task_master.rb
|
55
|
+
- lib/robot-army.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/wesabe/robot-army
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project: robot-army
|
78
|
+
rubygems_version: 1.0.1
|
79
|
+
signing_key:
|
80
|
+
specification_version: 2
|
81
|
+
summary: Deploy using Thor by executing Ruby remotely
|
82
|
+
test_files: []
|
83
|
+
|