robot-army 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.markdown +34 -0
- data/Rakefile +9 -0
- data/examples/whoami.rb +13 -0
- data/lib/robot-army.rb +109 -0
- data/lib/robot-army/at_exit.rb +19 -0
- data/lib/robot-army/connection.rb +174 -0
- data/lib/robot-army/dependency_loader.rb +38 -0
- data/lib/robot-army/eval_builder.rb +84 -0
- data/lib/robot-army/eval_command.rb +17 -0
- data/lib/robot-army/gate_keeper.rb +28 -0
- data/lib/robot-army/io.rb +106 -0
- data/lib/robot-army/keychain.rb +10 -0
- data/lib/robot-army/loader.rb +85 -0
- data/lib/robot-army/marshal_ext.rb +52 -0
- data/lib/robot-army/messenger.rb +31 -0
- data/lib/robot-army/officer.rb +35 -0
- data/lib/robot-army/officer_connection.rb +5 -0
- data/lib/robot-army/officer_loader.rb +13 -0
- data/lib/robot-army/proxy.rb +35 -0
- data/lib/robot-army/remote_evaler.rb +59 -0
- data/lib/robot-army/ruby2ruby_ext.rb +19 -0
- data/lib/robot-army/soldier.rb +37 -0
- data/lib/robot-army/task_master.rb +317 -0
- data/spec/at_exit_spec.rb +25 -0
- data/spec/connection_spec.rb +126 -0
- data/spec/dependency_loader_spec.rb +46 -0
- data/spec/gate_keeper_spec.rb +46 -0
- data/spec/integration_spec.rb +40 -0
- data/spec/io_spec.rb +36 -0
- data/spec/keychain_spec.rb +15 -0
- data/spec/loader_spec.rb +13 -0
- data/spec/marshal_ext_spec.rb +89 -0
- data/spec/messenger_spec.rb +28 -0
- data/spec/officer_spec.rb +36 -0
- data/spec/proxy_spec.rb +52 -0
- data/spec/ruby2ruby_ext_spec.rb +67 -0
- data/spec/soldier_spec.rb +71 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/task_master_spec.rb +306 -0
- 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,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,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
|