wesabe-robot-army 0.1.1 → 0.1.7
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/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
|