wesabe-robot-army 0.1
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 +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
|
+
|