robot-army 0.1.8

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