vagrant-xapi 0.0.1 → 1.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/README.md +460 -4
- data/docs/architecture.md +1420 -0
- data/docs/operations.md +531 -0
- data/lib/vagrant-xapi/action/base.rb +103 -0
- data/lib/vagrant-xapi/action/cloud_init.rb +130 -0
- data/lib/vagrant-xapi/action/create.rb +127 -0
- data/lib/vagrant-xapi/action/destroy.rb +68 -0
- data/lib/vagrant-xapi/action/network.rb +229 -0
- data/lib/vagrant-xapi/action/package.rb +103 -0
- data/lib/vagrant-xapi/action/power.rb +185 -0
- data/lib/vagrant-xapi/action/snapshots.rb +43 -0
- data/lib/vagrant-xapi/action.rb +317 -0
- data/lib/vagrant-xapi/api/client.rb +393 -0
- data/lib/vagrant-xapi/api/errors.rb +98 -0
- data/lib/vagrant-xapi/api/http_transfer.rb +332 -0
- data/lib/vagrant-xapi/api/redactor.rb +77 -0
- data/lib/vagrant-xapi/api/session_registry.rb +59 -0
- data/lib/vagrant-xapi/api/task.rb +156 -0
- data/lib/vagrant-xapi/api/transport.rb +249 -0
- data/lib/vagrant-xapi/cap.rb +90 -0
- data/lib/vagrant-xapi/command/adopt.rb +92 -0
- data/lib/vagrant-xapi/command/base.rb +79 -0
- data/lib/vagrant-xapi/command/console.rb +87 -0
- data/lib/vagrant-xapi/command/diff.rb +267 -0
- data/lib/vagrant-xapi/command/hosts.rb +63 -0
- data/lib/vagrant-xapi/command/info.rb +94 -0
- data/lib/vagrant-xapi/command/marked.rb +85 -0
- data/lib/vagrant-xapi/command/orphans.rb +96 -0
- data/lib/vagrant-xapi/command/root.rb +55 -0
- data/lib/vagrant-xapi/command/templates.rb +91 -0
- data/lib/vagrant-xapi/config.rb +333 -0
- data/lib/vagrant-xapi/errors.rb +113 -0
- data/lib/vagrant-xapi/extensions.rb +80 -0
- data/lib/vagrant-xapi/glue.rb +330 -0
- data/lib/vagrant-xapi/legacy/plugin.rb +61 -0
- data/lib/vagrant-xapi/legacy/xenapi.rb +129 -0
- data/lib/vagrant-xapi/plugin.rb +58 -0
- data/lib/vagrant-xapi/provider.rb +135 -0
- data/lib/vagrant-xapi/services/adoption.rb +73 -0
- data/lib/vagrant-xapi/services/base.rb +242 -0
- data/lib/vagrant-xapi/services/box_source.rb +218 -0
- data/lib/vagrant-xapi/services/console.rb +115 -0
- data/lib/vagrant-xapi/services/content_diff.rb +96 -0
- data/lib/vagrant-xapi/services/destroyer.rb +104 -0
- data/lib/vagrant-xapi/services/diff_disks.rb +346 -0
- data/lib/vagrant-xapi/services/differ.rb +382 -0
- data/lib/vagrant-xapi/services/disk_image.rb +122 -0
- data/lib/vagrant-xapi/services/disks.rb +281 -0
- data/lib/vagrant-xapi/services/exporter.rb +128 -0
- data/lib/vagrant-xapi/services/file_diff.rb +249 -0
- data/lib/vagrant-xapi/services/guest_files.rb +730 -0
- data/lib/vagrant-xapi/services/hosts.rb +55 -0
- data/lib/vagrant-xapi/services/image_upload.rb +172 -0
- data/lib/vagrant-xapi/services/inventory.rb +225 -0
- data/lib/vagrant-xapi/services/ip_detector.rb +107 -0
- data/lib/vagrant-xapi/services/lock.rb +293 -0
- data/lib/vagrant-xapi/services/lookup.rb +181 -0
- data/lib/vagrant-xapi/services/networks.rb +248 -0
- data/lib/vagrant-xapi/services/power.rb +108 -0
- data/lib/vagrant-xapi/services/rfb.rb +129 -0
- data/lib/vagrant-xapi/services/snapshots.rb +81 -0
- data/lib/vagrant-xapi/services/template_cache.rb +263 -0
- data/lib/vagrant-xapi/services/tunnels.rb +217 -0
- data/lib/vagrant-xapi/services/vm_builder.rb +215 -0
- data/lib/vagrant-xapi/version.rb +7 -0
- data/lib/vagrant-xapi/xenserver/config.rb +69 -0
- data/lib/vagrant-xapi/xenserver/migrate.rb +103 -0
- data/lib/vagrant-xapi/xenserver/plugin.rb +96 -0
- data/lib/vagrant-xapi/xenserver/provider.rb +22 -0
- data/lib/vagrant-xapi/xenserver/takeover.rb +79 -0
- data/lib/vagrant-xapi.rb +21 -2
- data/lib/xenapi.rb +22 -0
- data/locales/en.yml +517 -0
- data/locales/legacy.en.yml +6 -0
- metadata +86 -5
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
module VagrantPlugins
|
|
10
|
+
module Xapi
|
|
11
|
+
module Api
|
|
12
|
+
# Streams uploads and downloads to XAPI HTTP handlers (/import,
|
|
13
|
+
# /import_raw_vdi, /export, /export_raw_vdi) with net/http.
|
|
14
|
+
#
|
|
15
|
+
# The session travels as a cookie, never in the URL, and no external
|
|
16
|
+
# process (curl) is involved, so no secret ever shows up in `ps`.
|
|
17
|
+
#
|
|
18
|
+
# XAPI answers these handlers with a 302 to another pool host when the
|
|
19
|
+
# receiving host cannot reach the SR. A PUT cannot follow that after
|
|
20
|
+
# its body is streamed, so callers pass the right host up front
|
|
21
|
+
# (`host:` is a host ref; nil means the master). GETs also follow
|
|
22
|
+
# redirects.
|
|
23
|
+
#
|
|
24
|
+
# Never send `Expect: 100-continue`: XAPI does not answer it with
|
|
25
|
+
# "100 Continue", and Net::HTTP would then wait forever for a body it
|
|
26
|
+
# never sent.
|
|
27
|
+
class HttpTransfer
|
|
28
|
+
MAX_REDIRECTS = 3
|
|
29
|
+
|
|
30
|
+
# IO wrapper that reports how many bytes have been read.
|
|
31
|
+
class ProgressIO
|
|
32
|
+
def initialize(io, total, progress)
|
|
33
|
+
@io = io
|
|
34
|
+
@total = total.to_f
|
|
35
|
+
@progress = progress
|
|
36
|
+
@done = 0
|
|
37
|
+
@last = -1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read(length = nil, outbuf = nil)
|
|
41
|
+
data = @io.read(length, outbuf)
|
|
42
|
+
if data
|
|
43
|
+
@done += data.bytesize
|
|
44
|
+
report
|
|
45
|
+
end
|
|
46
|
+
data
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def size
|
|
50
|
+
@total.to_i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def report
|
|
56
|
+
return unless @progress && @total.positive?
|
|
57
|
+
|
|
58
|
+
pct = (@done / @total * 100).floor
|
|
59
|
+
return if pct == @last
|
|
60
|
+
|
|
61
|
+
@last = pct
|
|
62
|
+
@progress.call(@done, @total.to_i)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(client)
|
|
67
|
+
@client = client
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# PUT a local file to an XAPI handler. When task_label is given, a task
|
|
71
|
+
# is created, passed as task_id, and waited for; its Result is returned.
|
|
72
|
+
def upload(path, query, file, host: nil, progress: nil, task_label: nil, cancel: nil)
|
|
73
|
+
with_session_retry do
|
|
74
|
+
upload_once(path, query, file, host: host, progress: progress, task_label: task_label, cancel: cancel)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sends an empty PUT and returns only the status code, without waiting
|
|
79
|
+
# for the handler to finish. Used to ask whether a handler accepts a
|
|
80
|
+
# parameter (e.g. format=qcow2) before streaming a large file.
|
|
81
|
+
# @return [Integer, nil] nil when there was no answer in time
|
|
82
|
+
def probe(path, query, host: nil, timeout: 15)
|
|
83
|
+
status = probe_once(path, query, host: host, timeout: timeout)
|
|
84
|
+
return status unless status == 401
|
|
85
|
+
|
|
86
|
+
# An expired session, not an answer about the parameter.
|
|
87
|
+
@client.renew_session(@cookie)
|
|
88
|
+
probe_once(path, query, host: host, timeout: timeout)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# GET from an XAPI handler into a local file, following redirects to
|
|
92
|
+
# other pool hosts. When task_label is given, a task is created,
|
|
93
|
+
# passed as task_id and waited for once the body is in: the end of the
|
|
94
|
+
# stream alone does not say that the export on the host succeeded.
|
|
95
|
+
def download(path, query, dest, host: nil, progress: nil, cancel: nil, task_label: nil)
|
|
96
|
+
with_session_retry do
|
|
97
|
+
download_once(path, query, dest, host: host, progress: progress, cancel: cancel, task_label: task_label)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# XAPI's HTTP handlers answer an expired or evicted session (e.g. the
|
|
104
|
+
# oldest of a user with too many) with 401. Log in again once and
|
|
105
|
+
# start the transfer over, as Client#call does on SESSION_INVALID.
|
|
106
|
+
def with_session_retry
|
|
107
|
+
renewed = false
|
|
108
|
+
begin
|
|
109
|
+
yield
|
|
110
|
+
rescue HttpError => e
|
|
111
|
+
raise unless e.status == 401 && !renewed
|
|
112
|
+
|
|
113
|
+
renewed = true
|
|
114
|
+
@client.renew_session(@cookie)
|
|
115
|
+
retry
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The session cookie for a request, remembered so that a 401 renews
|
|
120
|
+
# exactly that session.
|
|
121
|
+
def cookie
|
|
122
|
+
@cookie = @client.session_cookie
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def upload_once(path, query, file, host:, progress:, task_label:, cancel:)
|
|
126
|
+
waiting = false
|
|
127
|
+
task_ref = task_label ? @client.create_task(task_label) : nil
|
|
128
|
+
query = query.merge("task_id" => task_ref) if task_ref
|
|
129
|
+
size = File.size(file)
|
|
130
|
+
transport = @client.transport_for(host)
|
|
131
|
+
|
|
132
|
+
File.open(file, "rb") do |io|
|
|
133
|
+
req = Net::HTTP::Put.new(build_path(path, query))
|
|
134
|
+
req["Cookie"] = cookie
|
|
135
|
+
req["Content-Length"] = size.to_s
|
|
136
|
+
req["Content-Type"] = "application/octet-stream"
|
|
137
|
+
req["User-Agent"] = Client::ORIGINATOR
|
|
138
|
+
req.body_stream = ProgressIO.new(io, size, progress)
|
|
139
|
+
response = perform(transport, req, path, cancel: cancel, task_ref: task_ref)
|
|
140
|
+
check!(response, path)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return nil unless task_ref
|
|
144
|
+
|
|
145
|
+
waiting = true
|
|
146
|
+
@client.wait_task(task_ref, label: task_label, cancel: cancel) # destroys the task
|
|
147
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
148
|
+
abandon_task(task_ref) if task_ref && !waiting
|
|
149
|
+
raise
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def probe_once(path, query, host:, timeout:)
|
|
153
|
+
transport = @client.transport_for(host)
|
|
154
|
+
http = transport.new_connection(read_timeout: timeout)
|
|
155
|
+
begin
|
|
156
|
+
req = Net::HTTP::Put.new(build_path(path, query))
|
|
157
|
+
req["Cookie"] = cookie
|
|
158
|
+
req["User-Agent"] = Client::ORIGINATOR
|
|
159
|
+
req["Connection"] = "close"
|
|
160
|
+
req.body = ""
|
|
161
|
+
# The block runs once the status and headers are in; returning
|
|
162
|
+
# from it skips reading the body, which XAPI may never close.
|
|
163
|
+
http.request(req) { |response| return response.code.to_i } # rubocop:disable Lint/NonLocalExitFromIterator
|
|
164
|
+
ensure
|
|
165
|
+
begin
|
|
166
|
+
http.finish if http.started?
|
|
167
|
+
rescue StandardError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
rescue TimeoutError, *Transport::TIMEOUT_ERRORS
|
|
172
|
+
# No answer in time: the caller converts instead (and asks again
|
|
173
|
+
# next time). A refused or unreachable host is a real error below.
|
|
174
|
+
nil
|
|
175
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
176
|
+
raise TlsError.new(transport.host, e.message)
|
|
177
|
+
rescue *Transport::NETWORK_ERRORS => e
|
|
178
|
+
raise ConnectionError.new(transport.host, e.message)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def download_once(path, query, dest, host:, progress:, cancel:, task_label:)
|
|
182
|
+
waiting = false
|
|
183
|
+
task_ref = task_label ? @client.create_task(task_label) : nil
|
|
184
|
+
query = query.merge("task_id" => task_ref) if task_ref
|
|
185
|
+
transport = @client.transport_for(host)
|
|
186
|
+
target = build_path(path, query)
|
|
187
|
+
MAX_REDIRECTS.times do
|
|
188
|
+
location = get_into(transport, target, path, dest, progress, cancel)
|
|
189
|
+
unless location
|
|
190
|
+
if task_ref
|
|
191
|
+
waiting = true
|
|
192
|
+
@client.wait_task(task_ref, label: task_label, cancel: cancel) # destroys the task
|
|
193
|
+
end
|
|
194
|
+
return dest
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
uri = URI.parse(location)
|
|
198
|
+
transport = redirect_transport(uri)
|
|
199
|
+
target = uri.request_uri
|
|
200
|
+
end
|
|
201
|
+
raise HttpError.new(302, path, "too many redirects")
|
|
202
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
203
|
+
abandon_task(task_ref) if task_ref && !waiting
|
|
204
|
+
raise
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @return [String, nil] redirect location, or nil when done
|
|
208
|
+
def get_into(transport, target, path, dest, progress, cancel)
|
|
209
|
+
http = transport.new_connection(read_timeout: @client.task_timeout)
|
|
210
|
+
begin
|
|
211
|
+
req = Net::HTTP::Get.new(target)
|
|
212
|
+
req["Cookie"] = cookie
|
|
213
|
+
req["User-Agent"] = Client::ORIGINATOR
|
|
214
|
+
http.request(req) do |response|
|
|
215
|
+
return response["Location"] if response.is_a?(Net::HTTPRedirection) && response["Location"] # rubocop:disable Lint/NonLocalExitFromIterator
|
|
216
|
+
|
|
217
|
+
check!(response, path)
|
|
218
|
+
total = response["Content-Length"].to_i
|
|
219
|
+
done = 0
|
|
220
|
+
File.open(dest, "wb") do |f|
|
|
221
|
+
response.read_body do |chunk|
|
|
222
|
+
raise TaskCancelled, path if cancel&.call
|
|
223
|
+
|
|
224
|
+
f.write(chunk)
|
|
225
|
+
done += chunk.bytesize
|
|
226
|
+
progress&.call(done, total)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
# Net::HTTP ignores an early end of the stream by default.
|
|
230
|
+
if response["Content-Length"] && done != total
|
|
231
|
+
raise ProtocolError, "the download of #{path} ended after #{done} of #{total} bytes"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
nil
|
|
235
|
+
ensure
|
|
236
|
+
begin
|
|
237
|
+
http.finish if http.started?
|
|
238
|
+
rescue StandardError
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
243
|
+
raise TlsError.new(transport.host, e.message)
|
|
244
|
+
rescue *Transport::NETWORK_ERRORS => e
|
|
245
|
+
raise ConnectionError.new(transport.host, e.message)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# A redirect points at another pool host by address; find its ref so
|
|
249
|
+
# its certificate can be pinned. hostname, not host: an IPv6 address
|
|
250
|
+
# comes without its brackets, as host.get_address returns it.
|
|
251
|
+
def redirect_transport(uri)
|
|
252
|
+
host_ref = @client.call("host.get_all").find do |ref|
|
|
253
|
+
@client.call("host.get_address", ref) == uri.hostname
|
|
254
|
+
end
|
|
255
|
+
raise HttpError.new(302, uri.path, "redirect to unknown host #{uri.host}") unless host_ref
|
|
256
|
+
|
|
257
|
+
@client.transport_for(host_ref)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def perform(transport, req, path, cancel:, task_ref:)
|
|
261
|
+
http = transport.new_connection(read_timeout: @client.task_timeout)
|
|
262
|
+
watcher = start_cancel_watcher(http, cancel, task_ref)
|
|
263
|
+
begin
|
|
264
|
+
http.request(req)
|
|
265
|
+
ensure
|
|
266
|
+
watcher&.kill
|
|
267
|
+
begin
|
|
268
|
+
http.finish if http.started?
|
|
269
|
+
rescue StandardError
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
274
|
+
raise TaskCancelled, path if cancel&.call
|
|
275
|
+
|
|
276
|
+
raise TlsError.new(transport.host, e.message)
|
|
277
|
+
rescue *Transport::NETWORK_ERRORS => e
|
|
278
|
+
raise TaskCancelled, path if cancel&.call
|
|
279
|
+
|
|
280
|
+
raise ConnectionError.new(transport.host, e.message)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# On Ctrl-C, cancel the task and close the socket so the PUT stops.
|
|
284
|
+
def start_cancel_watcher(http, cancel, task_ref)
|
|
285
|
+
return nil unless cancel
|
|
286
|
+
|
|
287
|
+
Thread.new do
|
|
288
|
+
sleep 0.5 until cancel.call
|
|
289
|
+
cancel_task(task_ref)
|
|
290
|
+
begin
|
|
291
|
+
http.finish if http.started?
|
|
292
|
+
rescue StandardError
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def cancel_task(task_ref)
|
|
299
|
+
return unless task_ref
|
|
300
|
+
|
|
301
|
+
@client.call("task.cancel", task_ref)
|
|
302
|
+
rescue Error
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# A task we created but will not wait for: cancel and remove it.
|
|
307
|
+
def abandon_task(task_ref)
|
|
308
|
+
cancel_task(task_ref)
|
|
309
|
+
@client.call("task.destroy", task_ref)
|
|
310
|
+
rescue Error
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def check!(response, path)
|
|
315
|
+
return if response.is_a?(Net::HTTPSuccess)
|
|
316
|
+
|
|
317
|
+
body = begin
|
|
318
|
+
response.body
|
|
319
|
+
rescue StandardError
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
raise HttpError.new(response.code, path, body)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def build_path(path, query)
|
|
326
|
+
q = query.compact.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }
|
|
327
|
+
q.empty? ? path : "#{path}?#{q.join("&")}"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Api
|
|
8
|
+
# Keeps track of secrets (passwords, session ids) and removes them from
|
|
9
|
+
# any string before it is logged or shown. Process-wide, thread-safe.
|
|
10
|
+
module Redactor
|
|
11
|
+
MASK = "[REDACTED]"
|
|
12
|
+
@secrets = Set.new
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def register(secret)
|
|
17
|
+
return if secret.nil?
|
|
18
|
+
|
|
19
|
+
s = secret.to_s
|
|
20
|
+
return if s.empty?
|
|
21
|
+
|
|
22
|
+
@mutex.synchronize { @secrets << s }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unregister(secret)
|
|
26
|
+
@mutex.synchronize { @secrets.delete(secret.to_s) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scrub(value)
|
|
30
|
+
case value
|
|
31
|
+
when String
|
|
32
|
+
out = value.dup
|
|
33
|
+
secrets.each { |s| out = out.gsub(s, MASK) }
|
|
34
|
+
# session_id never belongs in a URL or a log line, nor do
|
|
35
|
+
# credentials in a URL (e.g. an xva_url).
|
|
36
|
+
out = out.gsub(/(session_id=)[^&\s"';]+/, "\\1#{MASK}")
|
|
37
|
+
out.gsub(%r{(\w+://)[^/\s:@]+:[^/\s@]+@}, "\\1#{MASK}@")
|
|
38
|
+
when Array
|
|
39
|
+
value.map { |v| scrub(v) }
|
|
40
|
+
when Hash
|
|
41
|
+
value.each_with_object({}) { |(k, v), h| h[k] = scrub(v) }
|
|
42
|
+
else
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def secrets
|
|
48
|
+
@mutex.synchronize { @secrets.to_a.sort_by { |s| -s.length } }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# For tests.
|
|
52
|
+
def reset!
|
|
53
|
+
@mutex.synchronize { @secrets.clear }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Minimal logger wrapper: everything passes through the Redactor first.
|
|
59
|
+
# Wraps any object with debug/info/warn/error (e.g. a Log4r::Logger);
|
|
60
|
+
# defaults to a no-op logger so the client works without Vagrant.
|
|
61
|
+
class SafeLogger
|
|
62
|
+
def initialize(inner = nil)
|
|
63
|
+
@inner = inner
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
%i[debug info warn error].each do |level|
|
|
67
|
+
define_method(level) do |msg = nil, &blk|
|
|
68
|
+
return unless @inner
|
|
69
|
+
|
|
70
|
+
msg = blk.call if msg.nil? && blk
|
|
71
|
+
@inner.public_send(level, Redactor.scrub(msg.to_s))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "client"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Api
|
|
10
|
+
# One logged-in Client per process per pool and user, shared between
|
|
11
|
+
# threads (Vagrant runs machines in parallel). All sessions are logged
|
|
12
|
+
# out when the process exits.
|
|
13
|
+
module SessionRegistry
|
|
14
|
+
@clients = {}
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@at_exit_installed = false
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# @param opts [Hash] Client.new options
|
|
20
|
+
def fetch(**opts)
|
|
21
|
+
# A digest of the password, so machines with other credentials for
|
|
22
|
+
# the same host and user do not share a session.
|
|
23
|
+
key = [opts[:host], opts[:port], opts[:username], Digest::SHA256.hexdigest(opts[:password].to_s),
|
|
24
|
+
opts[:tls].to_a.sort.to_s, opts[:api_timeout], opts[:task_timeout]].join("|")
|
|
25
|
+
client = @mutex.synchronize do
|
|
26
|
+
install_at_exit
|
|
27
|
+
@clients[key] ||= Client.new(**opts)
|
|
28
|
+
end
|
|
29
|
+
client.login
|
|
30
|
+
client
|
|
31
|
+
rescue Error
|
|
32
|
+
# Do not cache a client whose login failed (wrong password etc.).
|
|
33
|
+
@mutex.synchronize { @clients.delete(key) if @clients[key] && !@clients[key].logged_in? }
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def logout_all
|
|
38
|
+
clients = @mutex.synchronize { @clients.values.tap { @clients.clear } }
|
|
39
|
+
clients.each(&:logout)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# For tests.
|
|
43
|
+
def reset!
|
|
44
|
+
@mutex.synchronize { @clients.clear }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def install_at_exit
|
|
50
|
+
return if @at_exit_installed
|
|
51
|
+
|
|
52
|
+
@at_exit_installed = true
|
|
53
|
+
at_exit { logout_all }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Api
|
|
8
|
+
# Waits for an XAPI task using event.from, reports progress, supports
|
|
9
|
+
# cancellation (task.cancel) and destroys the task afterwards.
|
|
10
|
+
class Task
|
|
11
|
+
EVENT_WAIT = 5.0 # seconds per event.from call
|
|
12
|
+
# How long the pool may be unreachable while a task runs. Giving up
|
|
13
|
+
# destroys the task record, but XAPI goes on with the work (e.g. a
|
|
14
|
+
# VM.copy whose result nobody would then mark or clean up), so a
|
|
15
|
+
# dropped connection or a read timeout is retried for a while first.
|
|
16
|
+
CONNECTION_GRACE = 60.0
|
|
17
|
+
RETRY_PAUSE = 2.0
|
|
18
|
+
|
|
19
|
+
# Result of a finished task. XAPI returns results as an XML-RPC-ish
|
|
20
|
+
# string, e.g. "<value>OpaqueRef:...</value>".
|
|
21
|
+
class Result
|
|
22
|
+
attr_reader :raw
|
|
23
|
+
|
|
24
|
+
def initialize(raw)
|
|
25
|
+
@raw = raw.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# All OpaqueRefs in the result (VM.import returns several).
|
|
29
|
+
def refs
|
|
30
|
+
@raw.scan(/OpaqueRef:[0-9A-Za-z-]+/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ref
|
|
34
|
+
refs.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The first <value> content, or the raw string if it is not wrapped.
|
|
38
|
+
def value
|
|
39
|
+
m = @raw.match(%r{<value>(?:<string>)?([^<]*)(?:</string>)?</value>})
|
|
40
|
+
m ? m[1] : @raw
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_reader :ref, :label
|
|
45
|
+
|
|
46
|
+
def initialize(client, ref, label:, logger: nil)
|
|
47
|
+
@client = client
|
|
48
|
+
@ref = ref
|
|
49
|
+
@label = label
|
|
50
|
+
@logger = logger || SafeLogger.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param progress [#call] called with a Float 0.0..1.0
|
|
54
|
+
# @param cancel [#call] returns true when the user asked to cancel
|
|
55
|
+
def wait(timeout:, progress: nil, cancel: nil)
|
|
56
|
+
deadline = monotonic + timeout
|
|
57
|
+
token = ""
|
|
58
|
+
cancelled = false
|
|
59
|
+
use_events = true
|
|
60
|
+
last_progress = nil
|
|
61
|
+
@lost_since = nil
|
|
62
|
+
|
|
63
|
+
loop do
|
|
64
|
+
if cancel&.call && !cancelled
|
|
65
|
+
cancelled = true
|
|
66
|
+
@logger.info("cancelling task #{@label}")
|
|
67
|
+
begin
|
|
68
|
+
@client.call("task.cancel", @ref)
|
|
69
|
+
rescue Error => e
|
|
70
|
+
@logger.debug("task.cancel: #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
record = through_connection_loss(deadline) { @client.call("task.get_record", @ref) }
|
|
75
|
+
next if record.nil?
|
|
76
|
+
|
|
77
|
+
status = record["status"].to_s.downcase
|
|
78
|
+
pct = record["progress"].to_f
|
|
79
|
+
if progress && pct != last_progress
|
|
80
|
+
last_progress = pct
|
|
81
|
+
progress.call(pct)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
case status
|
|
85
|
+
when "success"
|
|
86
|
+
return Result.new(record["result"])
|
|
87
|
+
when "failure"
|
|
88
|
+
info = Array(record["error_info"])
|
|
89
|
+
raise TaskCancelled, @label if cancelled && info.first.to_s =~ /CANCELLED/
|
|
90
|
+
|
|
91
|
+
raise XapiError.new(info.first || "TASK_FAILED", info.drop(1), method_name: @label)
|
|
92
|
+
when "cancelled", "cancelling"
|
|
93
|
+
raise TaskCancelled, @label if status == "cancelled"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if monotonic > deadline
|
|
97
|
+
begin
|
|
98
|
+
@client.call("task.cancel", @ref)
|
|
99
|
+
rescue Error
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
raise TaskTimeout.new(@label, timeout)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if use_events
|
|
106
|
+
begin
|
|
107
|
+
res = through_connection_loss(deadline) do
|
|
108
|
+
@client.call_with_timeout("event.from", EVENT_WAIT + @client.api_timeout,
|
|
109
|
+
["task/#{@ref}"], token, EVENT_WAIT)
|
|
110
|
+
end
|
|
111
|
+
token = res["token"] if res.is_a?(Hash) && res["token"]
|
|
112
|
+
rescue XapiError => e
|
|
113
|
+
# EVENTS_LOST or RBAC: fall back to polling.
|
|
114
|
+
@logger.debug("event.from failed (#{e.code}); polling task instead")
|
|
115
|
+
use_events = false
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
sleep 1
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
ensure
|
|
122
|
+
destroy
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# The block's result, or nil after a connection error within the
|
|
128
|
+
# grace period (the caller then polls again). Raises the error once
|
|
129
|
+
# the pool has been unreachable for CONNECTION_GRACE seconds or the
|
|
130
|
+
# task's deadline has passed.
|
|
131
|
+
def through_connection_loss(deadline)
|
|
132
|
+
result = yield
|
|
133
|
+
@lost_since = nil
|
|
134
|
+
result
|
|
135
|
+
rescue ConnectionError => e
|
|
136
|
+
@lost_since ||= monotonic
|
|
137
|
+
raise if monotonic - @lost_since >= CONNECTION_GRACE || monotonic > deadline
|
|
138
|
+
|
|
139
|
+
@logger.info("waiting for task #{@label}: #{e.message}; trying again")
|
|
140
|
+
sleep RETRY_PAUSE
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def destroy
|
|
145
|
+
@client.call("task.destroy", @ref)
|
|
146
|
+
rescue Error
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def monotonic
|
|
151
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|