wesabe-robot-army 0.1.1 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/whoami.rb +13 -0
- data/lib/robot-army.rb +47 -21
- data/lib/robot-army/at_exit.rb +19 -0
- data/lib/robot-army/eval_builder.rb +84 -0
- data/lib/robot-army/eval_command.rb +17 -0
- data/lib/robot-army/keychain.rb +10 -0
- data/lib/robot-army/marshal_ext.rb +26 -7
- data/lib/robot-army/remote_evaler.rb +59 -0
- data/lib/robot-army/ruby2ruby_ext.rb +6 -18
- data/lib/robot-army/task_master.rb +113 -201
- 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 +26 -0
- data/spec/task_master_spec.rb +272 -0
- metadata +49 -16
data/examples/whoami.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'robot-army')
|
3
|
+
|
4
|
+
class Whoami < RobotArmy::TaskMaster
|
5
|
+
desc 'test', "Tests whoami"
|
6
|
+
method_options :root => :boolean, :host => :string
|
7
|
+
def test(options={})
|
8
|
+
self.host = options['host']
|
9
|
+
puts options['root'] ?
|
10
|
+
sudo{ `whoami` } :
|
11
|
+
remote{ `whoami` }
|
12
|
+
end
|
13
|
+
end
|
data/lib/robot-army.rb
CHANGED
@@ -2,29 +2,35 @@ require 'rubygems'
|
|
2
2
|
require 'open3'
|
3
3
|
require 'base64'
|
4
4
|
require 'thor'
|
5
|
-
|
5
|
+
|
6
|
+
gem 'ParseTree', '>=3'
|
7
|
+
require 'parse_tree'
|
8
|
+
|
9
|
+
gem 'ruby2ruby', '>=1.2.0'
|
6
10
|
require 'ruby2ruby'
|
11
|
+
require 'parse_tree_extensions'
|
12
|
+
|
7
13
|
require 'fileutils'
|
8
14
|
|
9
15
|
module RobotArmy
|
10
16
|
# Gets the upstream messenger.
|
11
|
-
#
|
17
|
+
#
|
12
18
|
# @return [RobotArmy::Messenger]
|
13
19
|
# A messenger connection pointing upstream.
|
14
|
-
#
|
20
|
+
#
|
15
21
|
def self.upstream
|
16
22
|
@upstream
|
17
23
|
end
|
18
|
-
|
24
|
+
|
19
25
|
# Sets the upstream messenger.
|
20
|
-
#
|
26
|
+
#
|
21
27
|
# @param messenger [RobotArmy::Messenger]
|
22
28
|
# A messenger connection pointing upstream.
|
23
|
-
#
|
29
|
+
#
|
24
30
|
def self.upstream=(messenger)
|
25
31
|
@upstream = messenger
|
26
32
|
end
|
27
|
-
|
33
|
+
|
28
34
|
class ConnectionNotOpen < StandardError; end
|
29
35
|
class Warning < StandardError; end
|
30
36
|
class HostArityError < StandardError; end
|
@@ -35,41 +41,61 @@ module RobotArmy
|
|
35
41
|
end
|
36
42
|
class RobotArmy::Exit < Exception
|
37
43
|
attr_accessor :status
|
38
|
-
|
44
|
+
|
39
45
|
def initialize(status=0)
|
40
46
|
@status = status
|
41
47
|
end
|
42
48
|
end
|
43
|
-
|
49
|
+
|
44
50
|
CHARACTERS = %w[a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9]
|
45
|
-
|
51
|
+
|
46
52
|
# Generates a random string of lowercase letters and numbers.
|
47
|
-
#
|
53
|
+
#
|
48
54
|
# @param length [Fixnum]
|
49
55
|
# The length of the string to generate.
|
50
|
-
#
|
56
|
+
#
|
51
57
|
# @return [String]
|
52
58
|
# The random string.
|
53
|
-
#
|
59
|
+
#
|
54
60
|
def self.random_string(length=16)
|
55
61
|
(0...length).map{ CHARACTERS[rand(CHARACTERS.size)] }.join
|
56
62
|
end
|
57
|
-
end
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
marshal_ext gate_keeper ruby2ruby_ext].each do |file|
|
64
|
-
require File.join(File.dirname(__FILE__), 'robot-army', file)
|
64
|
+
def self.ask_for_password(host, data={})
|
65
|
+
require 'highline'
|
66
|
+
HighLine.new.ask("[sudo] password for #{data[:user]}@#{host}: ") {|q| q.echo = false}
|
67
|
+
end
|
65
68
|
end
|
66
69
|
|
70
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
71
|
+
|
72
|
+
require 'robot-army/loader'
|
73
|
+
require 'robot-army/dependency_loader'
|
74
|
+
require 'robot-army/io'
|
75
|
+
require 'robot-army/officer_loader'
|
76
|
+
require 'robot-army/soldier'
|
77
|
+
require 'robot-army/officer'
|
78
|
+
require 'robot-army/messenger'
|
79
|
+
require 'robot-army/task_master'
|
80
|
+
require 'robot-army/proxy'
|
81
|
+
require 'robot-army/eval_builder'
|
82
|
+
require 'robot-army/eval_command'
|
83
|
+
require 'robot-army/remote_evaler'
|
84
|
+
require 'robot-army/keychain'
|
85
|
+
require 'robot-army/connection'
|
86
|
+
require 'robot-army/officer_connection'
|
87
|
+
require 'robot-army/marshal_ext'
|
88
|
+
require 'robot-army/gate_keeper'
|
89
|
+
require 'robot-army/at_exit'
|
90
|
+
require 'robot-army/ruby2ruby_ext'
|
91
|
+
|
67
92
|
at_exit do
|
93
|
+
RobotArmy::AtExit.shared_instance.do_exit
|
68
94
|
RobotArmy::GateKeeper.shared_instance.close
|
69
95
|
end
|
70
96
|
|
71
97
|
def debug(*whatever)
|
72
98
|
File.open('/tmp/robot-army.log', 'a') do |f|
|
73
99
|
f.puts "[#{Process.pid}] #{whatever.join(' ')}"
|
74
|
-
end if $TESTING
|
100
|
+
end if $TESTING || $ROBOT_ARMY_DEBUG
|
75
101
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class RobotArmy::AtExit
|
2
|
+
def at_exit(&block)
|
3
|
+
callbacks << block
|
4
|
+
end
|
5
|
+
|
6
|
+
def do_exit
|
7
|
+
callbacks.pop.call while callbacks.last
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.shared_instance
|
11
|
+
@shared_instance ||= new
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def callbacks
|
17
|
+
@callbacks ||= []
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class RobotArmy::EvalBuilder
|
2
|
+
def self.build(command)
|
3
|
+
new.build(command)
|
4
|
+
end
|
5
|
+
|
6
|
+
def build(command)
|
7
|
+
proc, procargs, context, dependencies =
|
8
|
+
command.proc, command.args, command.context, command.dependencies
|
9
|
+
|
10
|
+
options = {}
|
11
|
+
proxies = {context.hash => context}
|
12
|
+
|
13
|
+
# fix stack traces
|
14
|
+
file, line = eval('[__FILE__, __LINE__]', proc.binding)
|
15
|
+
|
16
|
+
# include local variables
|
17
|
+
local_variables = eval('local_variables', proc.binding)
|
18
|
+
locals, lproxies = dump_values(local_variables) { |name,| eval(name, proc.binding) }
|
19
|
+
proxies.merge! lproxies
|
20
|
+
|
21
|
+
# include arguments
|
22
|
+
args, aproxies = dump_values(proc.arguments) { |_, i| procargs[i] }
|
23
|
+
proxies.merge! aproxies
|
24
|
+
|
25
|
+
# include dependency loader
|
26
|
+
dep_loading = "Marshal.load(#{Marshal.dump(dependencies).inspect}).load!"
|
27
|
+
|
28
|
+
# get the code for the proc
|
29
|
+
proc = "proc{ #{proc.to_ruby_without_proc_wrapper} }"
|
30
|
+
messenger = "RobotArmy::Messenger.new($stdin, $stdout)"
|
31
|
+
context = "RobotArmy::Proxy.new(#{messenger}, #{context.hash.inspect})"
|
32
|
+
|
33
|
+
code = %{
|
34
|
+
#{dep_loading} # load dependencies
|
35
|
+
#{(locals+args).join("\n")} # all local variables
|
36
|
+
#{context}.__proxy_instance_eval(&#{proc}) # run the block
|
37
|
+
}
|
38
|
+
|
39
|
+
options[:file] = file
|
40
|
+
options[:line] = line
|
41
|
+
options[:code] = code
|
42
|
+
options[:user] = command.user if command.user
|
43
|
+
|
44
|
+
return options, proxies
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Dumps the values associated with the given names for transport.
|
50
|
+
#
|
51
|
+
# @param names [Array[String]]
|
52
|
+
# The names of the variables to dump.
|
53
|
+
#
|
54
|
+
# @yield [name, index]
|
55
|
+
# Yields the name and its index and expects
|
56
|
+
# to get the corresponding value.
|
57
|
+
#
|
58
|
+
# @yieldparam [String] name
|
59
|
+
# The name of the value for the block to return.
|
60
|
+
#
|
61
|
+
# @yieldparam [Fixnum] index
|
62
|
+
# The index of the value for the block to return.
|
63
|
+
#
|
64
|
+
# @return [(Array[Object], Hash[Fixnum => Object])]
|
65
|
+
# The pair +values+ and +proxies+.
|
66
|
+
#
|
67
|
+
def dump_values(names)
|
68
|
+
proxies = {}
|
69
|
+
values = []
|
70
|
+
|
71
|
+
names.each_with_index do |name, i|
|
72
|
+
value = yield name, i
|
73
|
+
if value.marshalable?
|
74
|
+
dump = Marshal.dump(value)
|
75
|
+
values << "#{name} = Marshal.load(#{dump.inspect})"
|
76
|
+
else
|
77
|
+
proxies[value.hash] = value
|
78
|
+
values << "#{name} = #{RobotArmy::Proxy.generator_for(value)}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
return values, proxies
|
83
|
+
end
|
84
|
+
end
|
@@ -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
|
@@ -1,17 +1,17 @@
|
|
1
1
|
module Marshal
|
2
2
|
class <<self
|
3
3
|
# Determines whether a given object can be dumped.
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# @param object Object
|
6
6
|
# The object to check.
|
7
|
-
#
|
7
|
+
#
|
8
8
|
# @return [Boolean]
|
9
|
-
# +true+ if dumping the object does not raise an error,
|
9
|
+
# +true+ if dumping the object does not raise an error,
|
10
10
|
# +false+ if a +TypeError+ is raised.
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# @raise Exception
|
13
13
|
# Whatever +Marshal.dump+ might raise that isn't a +TypeError+.
|
14
|
-
#
|
14
|
+
#
|
15
15
|
def can_dump?(object)
|
16
16
|
begin
|
17
17
|
Marshal.dump(object)
|
@@ -25,9 +25,28 @@ end
|
|
25
25
|
|
26
26
|
class Object
|
27
27
|
# Syntactic sugar for +Marshal.can_dump?+.
|
28
|
-
#
|
28
|
+
#
|
29
29
|
# @see Marshal.can_dump?
|
30
30
|
def marshalable?
|
31
|
-
Marshal.can_dump?(self)
|
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?
|
32
51
|
end
|
33
52
|
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
|
@@ -2,30 +2,18 @@ class Proc
|
|
2
2
|
def arguments
|
3
3
|
(to_ruby[/\Aproc \{ \|([^\|]+)\|/, 1] || '').split(/\s*,\s*/)
|
4
4
|
end
|
5
|
-
|
6
|
-
def
|
7
|
-
|
5
|
+
|
6
|
+
def to_ruby_without_proc_wrapper
|
7
|
+
to_ruby[/\Aproc\s*\{\s*(\|[^\|]+\|)?\s*(.*?)\s*\}\Z/m, 2] || raise("Unable to parse proc's Ruby: #{to_ruby}")
|
8
8
|
end
|
9
|
-
|
10
|
-
alias :to_ruby_without_body_flag :to_ruby
|
11
|
-
alias :to_ruby :to_ruby_with_body_flag
|
12
9
|
end
|
13
10
|
|
14
11
|
class Method
|
15
12
|
def arguments
|
16
13
|
(to_ruby[/\A(def [^\s\(]+)(?:\(([^\)]*)\))?/, 2] || '').split(/\s*,\s*/)
|
17
14
|
end
|
18
|
-
|
19
|
-
def
|
20
|
-
|
21
|
-
if only_body
|
22
|
-
ruby.sub!(/\A(def [^\s\(]+)(?:\(([^\)]*)\))?/, '') # move args
|
23
|
-
ruby.sub!(/end\Z/, '') # strip end
|
24
|
-
end
|
25
|
-
ruby.gsub!(/\s+$/, '') # trailing WS bugs me
|
26
|
-
ruby
|
15
|
+
|
16
|
+
def to_ruby_without_method_declaration
|
17
|
+
to_ruby[/\Adef [^\s\(]+(?:\([^\)]*\))?\s*(.*?)\s*end\Z/m, 1] || raise("Unable to parse method's Ruby: #{to_ruby}")
|
27
18
|
end
|
28
|
-
|
29
|
-
alias :to_ruby_without_body_flag :to_ruby
|
30
|
-
alias :to_ruby :to_ruby_with_body_flag
|
31
19
|
end
|
@@ -1,46 +1,49 @@
|
|
1
1
|
module RobotArmy
|
2
2
|
# The place where the magic happens
|
3
|
-
#
|
3
|
+
#
|
4
4
|
# ==== Types (shortcuts for use in this file)
|
5
5
|
# HostList:: <Array[String], String, nil>
|
6
6
|
class TaskMaster < Thor
|
7
|
+
|
8
|
+
no_tasks do
|
9
|
+
|
7
10
|
def initialize(*args)
|
8
11
|
super
|
9
12
|
@dep_loader = DependencyLoader.new
|
10
13
|
end
|
11
|
-
|
14
|
+
|
12
15
|
# Gets or sets a single host that instances of +RobotArmy::TaskMaster+ subclasses will use.
|
13
|
-
#
|
16
|
+
#
|
14
17
|
# @param host [String, :localhost]
|
15
18
|
# The fully-qualified domain name to connect to.
|
16
|
-
#
|
19
|
+
#
|
17
20
|
# @return [String, :localhost]
|
18
21
|
# The current value for the host.
|
19
|
-
#
|
22
|
+
#
|
20
23
|
# @raise RobotArmy::HostArityError
|
21
|
-
# If you're using the getter form of this method and you've already
|
24
|
+
# If you're using the getter form of this method and you've already
|
22
25
|
# set multiple hosts, an error will be raised.
|
23
|
-
#
|
26
|
+
#
|
24
27
|
def self.host(host=nil)
|
25
28
|
if host
|
26
29
|
@hosts = nil
|
27
30
|
@host = host
|
28
31
|
elsif @hosts
|
29
|
-
raise RobotArmy::HostArityError,
|
32
|
+
raise RobotArmy::HostArityError,
|
30
33
|
"There are #{@hosts.size} hosts, so calling host doesn't make sense"
|
31
34
|
else
|
32
35
|
@host
|
33
36
|
end
|
34
37
|
end
|
35
|
-
|
38
|
+
|
36
39
|
# Gets or sets the hosts that instances of +RobotArmy::TaskMaster+ subclasses will use.
|
37
|
-
#
|
40
|
+
#
|
38
41
|
# @param hosts [Array[String]]
|
39
42
|
# A list of fully-qualified domain names to connect to.
|
40
|
-
#
|
43
|
+
#
|
41
44
|
# @return [Array[String]]
|
42
45
|
# The current list of hosts.
|
43
|
-
#
|
46
|
+
#
|
44
47
|
def self.hosts(hosts=nil)
|
45
48
|
if hosts
|
46
49
|
@host = nil
|
@@ -51,42 +54,42 @@ module RobotArmy
|
|
51
54
|
@hosts || []
|
52
55
|
end
|
53
56
|
end
|
54
|
-
|
57
|
+
|
55
58
|
# Gets the first host for this instance of +RobotArmy::TaskMaster+.
|
56
|
-
#
|
59
|
+
#
|
57
60
|
# @return [String, :localhost]
|
58
61
|
# The host value to use.
|
59
|
-
#
|
62
|
+
#
|
60
63
|
# @raise RobotArmy::HostArityError
|
61
|
-
# If you're using the getter form of this method and you've already
|
64
|
+
# If you're using the getter form of this method and you've already
|
62
65
|
# set multiple hosts, an error will be raised.
|
63
|
-
#
|
66
|
+
#
|
64
67
|
def host
|
65
68
|
if @host
|
66
69
|
@host
|
67
70
|
elsif @hosts
|
68
|
-
raise RobotArmy::HostArityError,
|
71
|
+
raise RobotArmy::HostArityError,
|
69
72
|
"There are #{@hosts.size} hosts, so calling host doesn't make sense"
|
70
73
|
else
|
71
74
|
self.class.host
|
72
75
|
end
|
73
76
|
end
|
74
|
-
|
77
|
+
|
75
78
|
# Sets a single host for this instance of +RobotArmy::TaskMaster+.
|
76
|
-
#
|
79
|
+
#
|
77
80
|
# @param host [String, :localhost]
|
78
81
|
# The host value to use.
|
79
|
-
#
|
82
|
+
#
|
80
83
|
def host=(host)
|
81
84
|
@hosts = nil
|
82
85
|
@host = host
|
83
86
|
end
|
84
|
-
|
87
|
+
|
85
88
|
# Gets the hosts for the instance of +RobotArmy::TaskMaster+.
|
86
|
-
#
|
89
|
+
#
|
87
90
|
# @return [Array[String]]
|
88
91
|
# A list of hosts.
|
89
|
-
#
|
92
|
+
#
|
90
93
|
def hosts
|
91
94
|
if @hosts
|
92
95
|
@hosts
|
@@ -96,104 +99,104 @@ module RobotArmy
|
|
96
99
|
self.class.hosts
|
97
100
|
end
|
98
101
|
end
|
99
|
-
|
102
|
+
|
100
103
|
# Sets the hosts for this instance of +RobotArmy::TaskMaster+.
|
101
|
-
#
|
104
|
+
#
|
102
105
|
# @param hosts [Array[String]]
|
103
106
|
# A list of hosts.
|
104
|
-
#
|
107
|
+
#
|
105
108
|
def hosts=(hosts)
|
106
109
|
@host = nil
|
107
110
|
@hosts = hosts
|
108
111
|
end
|
109
|
-
|
112
|
+
|
110
113
|
# Gets an open connection for the host this instance is configured to use.
|
111
|
-
#
|
114
|
+
#
|
112
115
|
# @return RobotArmy::Connection
|
113
116
|
# An open connection with an active Ruby process.
|
114
|
-
#
|
117
|
+
#
|
115
118
|
def connection(host)
|
116
119
|
RobotArmy::GateKeeper.shared_instance.connect(host)
|
117
120
|
end
|
118
|
-
|
119
|
-
# Runs a block of Ruby on the machine specified by a host string as root
|
121
|
+
|
122
|
+
# Runs a block of Ruby on the machine specified by a host string as root
|
120
123
|
# and returns the return value of the block. Example:
|
121
|
-
#
|
124
|
+
#
|
122
125
|
# sudo { `shutdown -r now` }
|
123
|
-
#
|
126
|
+
#
|
124
127
|
# You may also specify a user other than root. In this case +sudo+ is the
|
125
128
|
# same as +remote+:
|
126
|
-
#
|
129
|
+
#
|
127
130
|
# sudo(:user => 'www-data') { `/etc/init.d/apache2 restart` }
|
128
|
-
#
|
131
|
+
#
|
129
132
|
# @param host [String, :localhost]
|
130
|
-
# The fully-qualified domain name of the machine to connect to, or
|
133
|
+
# The fully-qualified domain name of the machine to connect to, or
|
131
134
|
# +:localhost+ if you want to use the same machine.
|
132
|
-
#
|
135
|
+
#
|
133
136
|
# @options options
|
134
137
|
# :user -> String => shell user
|
135
|
-
#
|
138
|
+
#
|
136
139
|
# @raise Exception
|
137
140
|
# Whatever is raised by the block.
|
138
|
-
#
|
141
|
+
#
|
139
142
|
# @return [Object]
|
140
143
|
# Whatever is returned by the block.
|
141
|
-
#
|
144
|
+
#
|
142
145
|
# @see remote
|
143
146
|
def sudo(hosts=self.hosts, options={}, &proc)
|
144
147
|
options, hosts = hosts, self.hosts if hosts.is_a?(Hash)
|
145
148
|
remote hosts, {:user => 'root'}.merge(options), &proc
|
146
149
|
end
|
147
|
-
|
148
|
-
# Runs a block of Ruby on the machine specified by a host string and
|
150
|
+
|
151
|
+
# Runs a block of Ruby on the machine specified by a host string and
|
149
152
|
# returns the return value of the block. Example:
|
150
|
-
#
|
153
|
+
#
|
151
154
|
# remote { "foo" } # => "foo"
|
152
|
-
#
|
153
|
-
# Local variables accessible from the block are also passed along to the
|
155
|
+
#
|
156
|
+
# Local variables accessible from the block are also passed along to the
|
154
157
|
# remote process:
|
155
|
-
#
|
158
|
+
#
|
156
159
|
# foo = "bar"
|
157
160
|
# remote { foo } # => "bar"
|
158
|
-
#
|
159
|
-
# Objects which can't be marshalled, such as IO streams, will be proxied
|
161
|
+
#
|
162
|
+
# Objects which can't be marshalled, such as IO streams, will be proxied
|
160
163
|
# instead:
|
161
|
-
#
|
164
|
+
#
|
162
165
|
# file = File.open("README.markdown", "r")
|
163
166
|
# remote { file.gets } # => "Robot Army\n"
|
164
|
-
#
|
167
|
+
#
|
165
168
|
# @param hosts [HostList]
|
166
169
|
# Which hosts to run the block on.
|
167
|
-
#
|
170
|
+
#
|
168
171
|
# @options options
|
169
172
|
# :user -> String => shell user
|
170
|
-
#
|
173
|
+
#
|
171
174
|
# @raise Exception
|
172
175
|
# Whatever is raised by the block.
|
173
|
-
#
|
176
|
+
#
|
174
177
|
# @return [Object]
|
175
178
|
# Whatever is returned by the block.
|
176
|
-
#
|
179
|
+
#
|
177
180
|
def remote(hosts=self.hosts, options={}, &proc)
|
178
181
|
options, hosts = hosts, self.hosts if hosts.is_a?(Hash)
|
179
182
|
results = Array(hosts).map {|host| remote_eval({:host => host}.merge(options), &proc) }
|
180
183
|
results.size == 1 ? results.first : results
|
181
184
|
end
|
182
|
-
|
185
|
+
|
183
186
|
# Copies src to dest on each host.
|
184
|
-
#
|
187
|
+
#
|
185
188
|
# @param src [String]
|
186
189
|
# A local file to copy.
|
187
|
-
#
|
190
|
+
#
|
188
191
|
# @param dest [String]
|
189
192
|
# The path of a remote file to copy to.
|
190
|
-
#
|
193
|
+
#
|
191
194
|
# @raise Errno::EACCES
|
192
195
|
# If the destination path cannot be written to.
|
193
|
-
#
|
196
|
+
#
|
194
197
|
# @raise Errno::ENOENT
|
195
198
|
# If the source path cannot be read.
|
196
|
-
#
|
199
|
+
#
|
197
200
|
def scp(src, dest, hosts=self.hosts)
|
198
201
|
Array(hosts).each do |host|
|
199
202
|
output = `scp -q #{src} #{"#{host}:" unless host == :localhost}#{dest} 2>&1`
|
@@ -204,35 +207,35 @@ module RobotArmy
|
|
204
207
|
raise Errno::ENOENT, output.chomp
|
205
208
|
end unless $?.exitstatus == 0
|
206
209
|
end
|
207
|
-
|
210
|
+
|
208
211
|
return nil
|
209
212
|
end
|
210
|
-
|
213
|
+
|
211
214
|
# Copies path to a temporary directory on each host.
|
212
|
-
#
|
215
|
+
#
|
213
216
|
# @param path [String]
|
214
217
|
# A local file to copy.
|
215
|
-
#
|
218
|
+
#
|
216
219
|
# @param hosts [HostList]
|
217
220
|
# Which hosts to connect to.
|
218
|
-
#
|
221
|
+
#
|
219
222
|
# @yield [path]
|
220
223
|
# Yields the path of the newly copied file on each remote host.
|
221
|
-
#
|
224
|
+
#
|
222
225
|
# @yieldparam [String] path
|
223
|
-
# The path of the file under in a new directory under a
|
226
|
+
# The path of the file under in a new directory under a
|
224
227
|
# temporary directory on the remote host.
|
225
|
-
#
|
228
|
+
#
|
226
229
|
# @return [Array<String>]
|
227
230
|
# An array of destination paths.
|
228
|
-
#
|
231
|
+
#
|
229
232
|
def cptemp(path, hosts=self.hosts, options={}, &block)
|
230
233
|
hosts, options = self.hosts, hosts if hosts.is_a?(Hash)
|
231
|
-
|
234
|
+
|
232
235
|
results = remote(hosts, options) do
|
233
236
|
File.join(%x{mktemp -d -t robot-army.XXXX}.chomp, File.basename(path))
|
234
237
|
end
|
235
|
-
|
238
|
+
|
236
239
|
me = ENV['USER']
|
237
240
|
host_and_path = Array(hosts).zip(Array(results))
|
238
241
|
# copy them over
|
@@ -245,161 +248,70 @@ module RobotArmy
|
|
245
248
|
results = host_and_path.map do |host, tmp|
|
246
249
|
remote(host, options.merge(:args => [tmp]), &block)
|
247
250
|
end if block
|
248
|
-
|
251
|
+
|
252
|
+
# delete it when we're done
|
253
|
+
RobotArmy::AtExit.shared_instance.at_exit do
|
254
|
+
host_and_path.each do |host, tmp|
|
255
|
+
remote(host, options) do
|
256
|
+
FileUtils.rm_rf(tmp)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
249
261
|
results.size == 1 ? results.first : results
|
250
262
|
end
|
251
|
-
|
263
|
+
|
252
264
|
# Add a gem dependency this TaskMaster checks for on each remote host.
|
253
|
-
#
|
265
|
+
#
|
254
266
|
# @param dep [String]
|
255
267
|
# The name of the gem to check for.
|
256
|
-
#
|
268
|
+
#
|
257
269
|
# @param ver [String]
|
258
270
|
# The version string of the gem to check for.
|
259
|
-
#
|
271
|
+
#
|
260
272
|
def dependency(dep, ver = nil)
|
261
273
|
@dep_loader.add_dependency dep, ver
|
262
274
|
end
|
263
|
-
|
275
|
+
|
264
276
|
private
|
265
|
-
|
277
|
+
|
266
278
|
def say(something)
|
267
279
|
something = HighLine.new.color(something, :bold) if defined?(HighLine)
|
268
280
|
puts "** #{something}"
|
269
281
|
end
|
270
|
-
|
271
|
-
# Dumps the values associated with the given names for transport.
|
272
|
-
#
|
273
|
-
# @param names [Array[String]]
|
274
|
-
# The names of the variables to dump.
|
275
|
-
#
|
276
|
-
# @yield [name, index]
|
277
|
-
# Yields the name and its index and expects
|
278
|
-
# to get the corresponding value.
|
279
|
-
#
|
280
|
-
# @yieldparam [String] name
|
281
|
-
# The name of the value for the block to return.
|
282
|
-
#
|
283
|
-
# @yieldparam [Fixnum] index
|
284
|
-
# The index of the value for the block to return.
|
285
|
-
#
|
286
|
-
# @return [(Array[Object], Hash[Fixnum => Object])]
|
287
|
-
# The pair +values+ and +proxies+.
|
288
|
-
#
|
289
|
-
def dump_values(names)
|
290
|
-
proxies = {}
|
291
|
-
values = []
|
292
|
-
|
293
|
-
names.each_with_index do |name, i|
|
294
|
-
value = yield name, i
|
295
|
-
if value.marshalable?
|
296
|
-
dump = Marshal.dump(value)
|
297
|
-
values << "#{name} = Marshal.load(#{dump.inspect})"
|
298
|
-
else
|
299
|
-
proxies[value.hash] = value
|
300
|
-
values << "#{name} = #{RobotArmy::Proxy.generator_for(value)}"
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
return values, proxies
|
305
|
-
end
|
306
|
-
|
282
|
+
|
307
283
|
# Handles remotely eval'ing a Ruby Proc.
|
308
|
-
#
|
284
|
+
#
|
309
285
|
# @options options
|
310
286
|
# :host -> [String, :localhost] => remote host
|
311
287
|
# :user -> String => shell user
|
312
288
|
# :password -> [String, nil] => sudo password
|
313
|
-
#
|
289
|
+
#
|
314
290
|
# @return Object
|
315
291
|
# Whatever the block returns.
|
316
|
-
#
|
292
|
+
#
|
317
293
|
# @raise Exception
|
318
294
|
# Whatever the block raises.
|
319
|
-
#
|
295
|
+
#
|
320
296
|
def remote_eval(options, &proc)
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
file, line = eval('[__FILE__, __LINE__]', proc.binding)
|
332
|
-
|
333
|
-
# include local variables
|
334
|
-
local_variables = eval('local_variables', proc.binding)
|
335
|
-
locals, lproxies = dump_values(local_variables) { |name,| eval(name, proc.binding) }
|
336
|
-
proxies.merge! lproxies
|
337
|
-
|
338
|
-
# include arguments
|
339
|
-
args, aproxies = dump_values(proc.arguments) { |_, i| procargs[i] }
|
340
|
-
proxies.merge! aproxies
|
341
|
-
|
342
|
-
# include dependency loader
|
343
|
-
dep_loading = "Marshal.load(#{Marshal.dump(@dep_loader).inspect}).load!"
|
344
|
-
|
345
|
-
# get the code for the proc
|
346
|
-
proc = "proc{ #{proc.to_ruby(true)} }"
|
347
|
-
messenger = "RobotArmy::Messenger.new($stdin, $stdout)"
|
348
|
-
context = "RobotArmy::Proxy.new(#{messenger}, #{self.hash.inspect})"
|
349
|
-
|
350
|
-
code = %{
|
351
|
-
#{dep_loading} # load dependencies
|
352
|
-
#{(locals+args).join("\n")} # all local variables
|
353
|
-
#{context}.__proxy_instance_eval(&#{proc}) # run the block
|
354
|
-
}
|
355
|
-
|
356
|
-
options[:file] = file
|
357
|
-
options[:line] = line
|
358
|
-
options[:code] = code
|
359
|
-
|
360
|
-
##
|
361
|
-
## send the child a message
|
362
|
-
##
|
363
|
-
|
364
|
-
conn.post(:command => :eval, :data => options)
|
365
|
-
|
366
|
-
##
|
367
|
-
## get and evaluate the response
|
368
|
-
##
|
369
|
-
|
370
|
-
loop do
|
371
|
-
# we want to loop until we get something other than "proxy"
|
372
|
-
response = conn.messenger.get
|
373
|
-
case response[:status]
|
374
|
-
when 'proxy'
|
375
|
-
begin
|
376
|
-
proxy = proxies[response[:data][:hash]]
|
377
|
-
data = proxy.send(*response[:data][:call])
|
378
|
-
if data.marshalable?
|
379
|
-
conn.post :status => 'ok', :data => data
|
380
|
-
else
|
381
|
-
proxies[data.hash] = data
|
382
|
-
conn.post :status => 'proxy', :data => data.hash
|
383
|
-
end
|
384
|
-
rescue Object => e
|
385
|
-
conn.post :status => 'error', :data => e
|
386
|
-
end
|
387
|
-
when 'password'
|
388
|
-
conn.post :status => 'ok', :data => ask_for_password(host, response[:data])
|
389
|
-
else
|
390
|
-
begin
|
391
|
-
return conn.handle_response(response)
|
392
|
-
rescue RobotArmy::Warning => e
|
393
|
-
$stderr.puts "WARNING: #{e.message}"
|
394
|
-
return nil
|
395
|
-
end
|
396
|
-
end
|
397
|
-
end
|
297
|
+
evaler = RemoteEvaler.new(connection(options[:host]), EvalCommand.new do |command|
|
298
|
+
command.user = options[:user]
|
299
|
+
command.proc = proc
|
300
|
+
command.args = options[:args] || []
|
301
|
+
command.context = self
|
302
|
+
command.dependencies = @dep_loader
|
303
|
+
command.keychain = keychain
|
304
|
+
end)
|
305
|
+
|
306
|
+
return evaler.execute_command
|
398
307
|
end
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
308
|
+
|
309
|
+
# Returns the default password manager. This should be
|
310
|
+
# overridden if you wish to have different sudo behavior.
|
311
|
+
def keychain
|
312
|
+
@keychain ||= Keychain.new
|
403
313
|
end
|
404
314
|
end
|
315
|
+
|
316
|
+
end
|
405
317
|
end
|