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