robot-army 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
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,17 @@
1
+ class RobotArmy::EvalCommand
2
+ def initialize
3
+ yield self if block_given?
4
+ end
5
+
6
+ attr_accessor :user
7
+
8
+ attr_accessor :proc
9
+
10
+ attr_accessor :args
11
+
12
+ attr_accessor :context
13
+
14
+ attr_accessor :dependencies
15
+
16
+ attr_accessor :keychain
17
+ end
@@ -0,0 +1,28 @@
1
+ class RobotArmy::GateKeeper
2
+ attr_reader :connection_class
3
+
4
+ def initialize(connection_class=RobotArmy::OfficerConnection)
5
+ @connection_class = connection_class
6
+ end
7
+
8
+ def connect(host)
9
+ connections[host] ||= establish_connection(host)
10
+ end
11
+
12
+ def establish_connection(host)
13
+ connection = connections[host] = connection_class.new(host)
14
+ connection.open
15
+ end
16
+
17
+ def connections
18
+ @connections ||= {}
19
+ end
20
+
21
+ def close
22
+ connections.each { |host,c| c.close unless c.closed? }
23
+ end
24
+
25
+ def self.shared_instance
26
+ @shared_instance ||= new
27
+ end
28
+ end
@@ -0,0 +1,106 @@
1
+ class RobotArmy::IO
2
+ attr_reader :name
3
+
4
+ # Starts capturing output of the named stream.
5
+ #
6
+ def start_capture
7
+ eval "$#{name} = self"
8
+ end
9
+
10
+ # Stops capturing output of the named stream.
11
+ #
12
+ def stop_capture
13
+ eval "$#{name} = #{name.upcase}"
14
+ end
15
+
16
+ def puts(*args) #:nodoc:
17
+ post capture(:puts, *args)
18
+ end
19
+
20
+ def print(*args) #:nodoc:
21
+ post capture(:print, *args)
22
+ end
23
+
24
+ def write(*args) #:nodoc:
25
+ post capture(:write, *args)
26
+ end
27
+
28
+ private
29
+
30
+ def initialize(name)
31
+ @name = name.to_s
32
+ start_capture
33
+ end
34
+
35
+ def post(string)
36
+ RobotArmy.upstream.post(:status => 'output', :data => {:stream => name, :string => string})
37
+ end
38
+
39
+ def capture(*call)
40
+ stream = StringIO.new
41
+ stream.send(*call)
42
+ stream.string
43
+ end
44
+
45
+ class <<self
46
+ # Determines whether the given stream has any data to be read.
47
+ #
48
+ # @param stream [IO]
49
+ # The +IO+ stream to check.
50
+ #
51
+ # @return [Boolean]
52
+ # +true+ if stream has data to be read, +false+ otherwise.
53
+ #
54
+ def has_data?(stream)
55
+ selected, _ = IO.select([stream], nil, nil, 0.5)
56
+ return selected && !selected.empty?
57
+ end
58
+
59
+ # Reads immediately available data from the given stream.
60
+ #
61
+ # # echo foo | ruby test.rb
62
+ # RobotArmy::IO.read_data($stdin) # => "foo\n"
63
+ #
64
+ # @param stream [IO]
65
+ # The +IO+ stream to read from.
66
+ #
67
+ # @return [String]
68
+ # The data read from the stream.
69
+ #
70
+ def read_data(stream)
71
+ data = []
72
+ data << stream.readpartial(1024) while has_data?(stream)
73
+ return data.join
74
+ end
75
+
76
+ # Redirects the named stream to a +StringIO+.
77
+ #
78
+ # RobotArmy::IO.capture(:stdout) { puts "foo" } # => "foo\n"
79
+ #
80
+ # RobotArmy::IO.silence(:stderr) { system "rm non-existent-file" }
81
+ #
82
+ # @param stream [Symbol]
83
+ # The name of the stream to redirect (i.e. +:stderr+, +:stdout+).
84
+ #
85
+ # @yield
86
+ # The block whose output we should capture.
87
+ #
88
+ # @return [String]
89
+ # The string result of the output produced by the block.
90
+ #
91
+ def capture(stream)
92
+ begin
93
+ stream = stream.to_s
94
+ eval "$#{stream} = StringIO.new"
95
+ yield
96
+ result = eval("$#{stream}").string
97
+ ensure
98
+ eval("$#{stream} = #{stream.upcase}")
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ alias_method :silence, :capture
105
+ end
106
+ end
@@ -0,0 +1,10 @@
1
+ class RobotArmy::Keychain
2
+ def get_password_for_user_on_host(user, host)
3
+ read_with_prompt("[sudo] password for #{user}@#{host}: ")
4
+ end
5
+
6
+ def read_with_prompt(prompt)
7
+ require 'highline'
8
+ HighLine.new.ask(prompt) {|q| q.echo = false}
9
+ end
10
+ end
@@ -0,0 +1,85 @@
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
+ $TESTING = #{$TESTING.inspect}
16
+ $stdout.sync = $stdin.sync = true
17
+ #{libraries.map{|l| "require #{l.inspect}"}.join("\n")}
18
+
19
+
20
+ ##
21
+ ## local Robot Army objects to communicate with the parent
22
+ ##
23
+
24
+ loader = #{self.class.name}.new
25
+ RobotArmy.upstream = RobotArmy::Messenger.new($stdin, $stdout)
26
+ loader.messenger = RobotArmy.upstream
27
+ loader.messenger.post(:status => 'ok')
28
+
29
+ ##
30
+ ## event loop
31
+ ##
32
+
33
+ loader.load
34
+ rescue Object => e
35
+ ##
36
+ ## exception handler of last resort
37
+ ##
38
+
39
+ if defined?(RobotArmy::Exit) && e.is_a?(RobotArmy::Exit)
40
+ # don't stomp on our own "let me out" exception
41
+ exit(e.status)
42
+ else
43
+ # if we got here that means something up to and including loader.load
44
+ # went unexpectedly wrong. this could be a missing library, or it
45
+ # could be a bug in Robot Army. either way we should report the error
46
+ # back to the place we came from so that they may re-raise the exception
47
+
48
+ # a little bit of un-DRY
49
+ print Base64.encode64(Marshal.dump(:status => 'error', :data => e))+'|'
50
+ exit(1)
51
+ end
52
+ end
53
+ }
54
+ end
55
+
56
+ def safely
57
+ begin
58
+ return yield, true
59
+ rescue RobotArmy::Exit
60
+ # let RobotArmy::Exit through
61
+ raise
62
+ rescue Object => e
63
+ messenger.post(:status => 'error', :data => e)
64
+ return nil, false
65
+ end
66
+ end
67
+
68
+ def safely_or_die(&block)
69
+ retval, success = safely(&block)
70
+ exit(1) unless success
71
+ return retval
72
+ end
73
+
74
+ def load
75
+ # create a soldier
76
+ soldier = safely_or_die{ RobotArmy::Soldier.new(messenger) }
77
+
78
+ # use the soldier to start listening to incoming commands
79
+ # at this point everything has been loaded successfully, so we
80
+ # don't have to exit if an exception is thrown
81
+ loop do
82
+ safely{ soldier.listen }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,52 @@
1
+ module Marshal
2
+ class <<self
3
+ # Determines whether a given object can be dumped.
4
+ #
5
+ # @param object Object
6
+ # The object to check.
7
+ #
8
+ # @return [Boolean]
9
+ # +true+ if dumping the object does not raise an error,
10
+ # +false+ if a +TypeError+ is raised.
11
+ #
12
+ # @raise Exception
13
+ # Whatever +Marshal.dump+ might raise that isn't a +TypeError+.
14
+ #
15
+ def can_dump?(object)
16
+ begin
17
+ Marshal.dump(object)
18
+ return true
19
+ rescue TypeError
20
+ return false
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ class Object
27
+ # Syntactic sugar for +Marshal.can_dump?+.
28
+ #
29
+ # @see Marshal.can_dump?
30
+ def marshalable?
31
+ Marshal.can_dump?(self) &&
32
+ instance_variables.all? do |ivar|
33
+ instance_variable_get(ivar).marshalable?
34
+ end
35
+ end
36
+ end
37
+
38
+ class Array
39
+ def marshalable?
40
+ super &&
41
+ all? {|item| item.marshalable?}
42
+ end
43
+ end
44
+
45
+ class Hash
46
+ def marshalable?
47
+ super &&
48
+ keys.marshalable? &&
49
+ values.marshalable? &&
50
+ default.marshalable?
51
+ end
52
+ end
@@ -0,0 +1,31 @@
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
+ dump = Marshal.dump(response)
11
+ dump = Base64.encode64(dump) + '|'
12
+ output << dump
13
+ end
14
+
15
+ def get
16
+ data = nil
17
+ loop do
18
+ case data = input.gets('|')
19
+ when nil, ''
20
+ return nil
21
+ when /^\s*$/
22
+ # again!
23
+ else
24
+ break
25
+ end
26
+ end
27
+ data = Base64.decode64(data.chop)
28
+ Marshal.load(data)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ class RobotArmy::Officer < RobotArmy::Soldier
2
+ def run(command, data)
3
+ case command
4
+ when :eval
5
+ debug "officer delegating eval command for user=#{data[:user].inspect}"
6
+ RobotArmy::Connection.localhost(data[:user], proc{ ask_for_password(data[:user]) }) do |local|
7
+ local.post(:command => command, :data => data)
8
+
9
+ loop do
10
+ # we want to stay in this loop as long as we
11
+ # have proxy requests coming back from our child
12
+ response = local.get
13
+ case response[:status]
14
+ when 'proxy'
15
+ # forward proxy requests on to our parent
16
+ messenger.post(response)
17
+ # and send the response back to our child
18
+ local.post(messenger.get)
19
+ else
20
+ return RobotArmy::Connection.handle_response(response)
21
+ end
22
+ end
23
+ end
24
+ when :exit
25
+ super
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def ask_for_password(user)
32
+ messenger.post(:status => 'password', :data => {:as => user, :user => ENV['USER']})
33
+ RobotArmy::Connection.handle_response messenger.get
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ class RobotArmy::OfficerConnection < RobotArmy::Connection
2
+ def loader
3
+ @loader ||= RobotArmy::OfficerLoader.new
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ class RobotArmy::OfficerLoader < RobotArmy::Loader
2
+ def load
3
+ # create a soldier
4
+ soldier = safely_or_die{ RobotArmy::Officer.new(messenger) }
5
+
6
+ # use the soldier to start listening to incoming commands
7
+ # at this point everything has been loaded successfully, so we
8
+ # don't have to exit if an exception is thrown
9
+ loop do
10
+ safely{ soldier.listen }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ class RobotArmy::Proxy
2
+ alias_method :__proxy_instance_eval, :instance_eval
3
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
4
+
5
+ def initialize(messenger, hash)
6
+ @messenger = messenger
7
+ @hash = hash
8
+ end
9
+
10
+ def sh(binary, *args)
11
+ command = [binary, *args].join(' ')
12
+ output = %x{#{command} 2>&1}
13
+
14
+ if not $?.success?
15
+ raise RobotArmy::ShellCommandError.new(command, $?.exitstatus, output)
16
+ end
17
+ end
18
+
19
+ def self.generator_for(object)
20
+ "RobotArmy::Proxy.new(RobotArmy.upstream, #{object.hash.inspect})"
21
+ end
22
+
23
+ private
24
+
25
+ def method_missing(*args, &block)
26
+ @messenger.post(:status => 'proxy', :data => {:hash => @hash, :call => args})
27
+ response = @messenger.get
28
+ case response[:status]
29
+ when 'proxy'
30
+ return RobotArmy::Proxy.new(@messenger, response[:data])
31
+ else
32
+ return RobotArmy::Connection.handle_response(response)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ class RobotArmy::RemoteEvaler
2
+ attr_reader :connection, :command, :options, :proxies
3
+
4
+ def initialize(connection, command)
5
+ @connection = connection
6
+ @command = command
7
+ end
8
+
9
+ def execute_command
10
+ @options, @proxies = RobotArmy::EvalBuilder.build(command)
11
+ send_eval_command
12
+ return loop_until_done
13
+ end
14
+
15
+ private
16
+
17
+ def send_eval_command
18
+ debug("Evaling code remotely:\n#{options[:code]}")
19
+ connection.post(:command => :eval, :data => options)
20
+ end
21
+
22
+ def loop_until_done
23
+ catch :done do
24
+ loop { process_response(connection.messenger.get) }
25
+ end
26
+ end
27
+
28
+ def process_response(response)
29
+ case response[:status]
30
+ when 'proxy'
31
+ handle_proxy_response(response)
32
+ when 'password'
33
+ debug("Got password request: #{response[:data].inspect}")
34
+ connection.post :status => 'ok', :data => command.keychain.get_password_for_user_on_host(response[:data][:user], connection.host)
35
+ else
36
+ begin
37
+ throw :done, connection.handle_response(response)
38
+ rescue RobotArmy::Warning => e
39
+ $stderr.puts "WARNING: #{e.message}"
40
+ throw :done, nil
41
+ end
42
+ end
43
+ end
44
+
45
+ def handle_proxy_response(response)
46
+ begin
47
+ proxy = proxies[response[:data][:hash]]
48
+ data = proxy.send(*response[:data][:call])
49
+ if data.marshalable?
50
+ connection.post :status => 'ok', :data => data
51
+ else
52
+ proxies[data.hash] = data
53
+ connection.post :status => 'proxy', :data => data.hash
54
+ end
55
+ rescue Object => e
56
+ connection.post :status => 'error', :data => e
57
+ end
58
+ end
59
+ end