1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
require 'base64'
require 'json'
require 'socket'
require 'timeout'
module RemoteShell
class ServerFailure < StandardError
end
# Used to differentiate vs Timeout::Error, which is thrown by
# try_for() (by default) and often wraps around remote shell usage
# -- in that case we don't want to catch that "outer" exception in
# our handling of remote shell timeouts below.
class Timeout < ServerFailure
end
DEFAULT_TIMEOUT = 20*60
# Counter providing unique id:s for each communicate() call.
@@request_id ||= 0
def communicate(vm, *args, **opts)
opts[:timeout] ||= DEFAULT_TIMEOUT
socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
id = (@@request_id += 1)
# Since we already have defined our own Timeout in the current
# scope, we have to be more careful when referring to the Timeout
# class from the 'timeout' module. However, note that we want it
# to throw our own Timeout exception.
Object::Timeout.timeout(opts[:timeout], Timeout) do
socket.puts(JSON.dump([id] + args))
socket.flush
loop do
line = socket.readline("\n").chomp("\n")
response_id, status, *rest = JSON.load(line)
if response_id == id
if status != "success"
if status == "error" and rest.class == Array and rest.size == 1
msg = rest.first
raise ServerFailure.new("#{msg}")
else
raise ServerFailure.new("Uncaught exception: #{status}: #{rest}")
end
end
return rest
else
debug_log("Dropped out-of-order remote shell response: " +
"got id #{response_id} but expected id #{id}")
end
end
end
ensure
socket.close if defined?(socket) && socket
end
module_function :communicate
private :communicate
class ShellCommand
# If `:spawn` is false the server will block until it has finished
# executing `cmd`. If it's true the server won't block, and the
# response will always be [0, "", ""] (only used as an
# ACK). execute() will always block until a response is received,
# though. Spawning is useful when starting processes in the
# background (or running scripts that does the same) or any
# application we want to interact with.
def self.execute(vm, cmd, **opts)
opts[:user] ||= "root"
opts[:spawn] = false unless opts.has_key?(:spawn)
type = opts[:spawn] ? "spawn" : "call"
debug_log("#{type}ing as #{opts[:user]}: #{cmd}")
ret = RemoteShell.communicate(vm, 'sh_' + type, opts[:user], cmd, **opts)
debug_log("#{type} returned: #{ret}") if not(opts[:spawn])
return ret
end
attr_reader :cmd, :returncode, :stdout, :stderr
def initialize(vm, cmd, **opts)
@cmd = cmd
@returncode, @stdout, @stderr = self.class.execute(vm, cmd, **opts)
end
def success?
return @returncode == 0
end
def failure?
return not(success?)
end
def to_s
"Return status: #{@returncode}\n" +
"STDOUT:\n" +
@stdout +
"STDERR:\n" +
@stderr
end
end
class PythonCommand
def self.execute(vm, code, **opts)
opts[:user] ||= "root"
show_code = code.chomp
if show_code["\n"]
show_code = "\n" + show_code.lines.map { |l| " "*4 + l.chomp } .join("\n")
end
debug_log("executing Python as #{opts[:user]}: #{show_code}")
ret = RemoteShell.communicate(
vm, 'python_execute', opts[:user], code, **opts
)
debug_log("execution complete")
return ret
end
attr_reader :code, :exception, :stdout, :stderr
def initialize(vm, code, **opts)
@code = code
@exception, @stdout, @stderr = self.class.execute(vm, code, **opts)
end
def success?
return @exception == nil
end
def failure?
return not(success?)
end
def to_s
"Exception: #{@exception}\n" +
"STDOUT:\n" +
@stdout +
"STDERR:\n" +
@stderr
end
end
# An IO-like object that is more or less equivalent to a File object
# opened in rw mode.
class File
def self.open(vm, mode, path, *args, **opts)
debug_log("opening file #{path} in '#{mode}' mode")
ret = RemoteShell.communicate(vm, 'file_' + mode, path, *args, **opts)
if ret.size != 1
raise ServerFailure.new("expected 1 value but got #{ret.size}")
end
debug_log("#{mode} complete")
return ret.first
end
attr_reader :vm, :path
def initialize(vm, path)
@vm, @path = vm, path
end
def read()
Base64.decode64(self.class.open(@vm, 'read', @path))
end
def write(data)
self.class.open(@vm, 'write', @path, Base64.encode64(data))
end
def append(data)
self.class.open(@vm, 'append', @path, Base64.encode64(data))
end
end
end
|