wesabe-robot-army 0.1

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'thor'
4
+
5
+ $stderr.puts "Robot Army uses thor, not rake. Here are the available tasks:"
6
+ exec "thor -T"
7
+ rescue LoadError
8
+ $stderr.puts "Robot Army uses thor, not rake. Please install thor."
9
+ end
@@ -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
+