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