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,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
require_relative "redactor"
|
|
8
|
+
require_relative "transport"
|
|
9
|
+
require_relative "task"
|
|
10
|
+
require_relative "http_transfer"
|
|
11
|
+
require_relative "../version"
|
|
12
|
+
|
|
13
|
+
module VagrantPlugins
|
|
14
|
+
module Xapi
|
|
15
|
+
module Api
|
|
16
|
+
# XAPI client over JSON-RPC 2.0 (https://<host>/jsonrpc) using only the
|
|
17
|
+
# Ruby standard library.
|
|
18
|
+
#
|
|
19
|
+
# client = Client.new(host: "xcp1", username: "root", password: "...")
|
|
20
|
+
# client.login
|
|
21
|
+
# vm = client.call("VM.get_by_uuid", uuid) # session is prepended
|
|
22
|
+
# client.VM.get_record(vm) # same, via proxy
|
|
23
|
+
# client.run_task("VM.clone", vm, "name") # Async.* + wait
|
|
24
|
+
#
|
|
25
|
+
# One Client = one session. It is safe to share between threads.
|
|
26
|
+
class Client
|
|
27
|
+
API_VERSION = "1.0"
|
|
28
|
+
ORIGINATOR = "vagrant-xapi/#{VERSION}"
|
|
29
|
+
MAX_REDIRECTS = 2
|
|
30
|
+
|
|
31
|
+
attr_reader :host, :port, :username, :features, :session_ref, :transport
|
|
32
|
+
|
|
33
|
+
def initialize(host:, username:, password:, port: 443, tls: {}, api_timeout: 60,
|
|
34
|
+
task_timeout: 3600, logger: nil)
|
|
35
|
+
@host = host
|
|
36
|
+
@port = port
|
|
37
|
+
@username = username
|
|
38
|
+
@password = password
|
|
39
|
+
@tls = tls
|
|
40
|
+
@api_timeout = api_timeout
|
|
41
|
+
@task_timeout = task_timeout
|
|
42
|
+
@logger = SafeLogger.new(logger)
|
|
43
|
+
@transport = Transport.new(host: host, port: port, tls: tls, timeout: api_timeout)
|
|
44
|
+
@id = 0
|
|
45
|
+
@id_mutex = Mutex.new
|
|
46
|
+
@login_mutex = Mutex.new
|
|
47
|
+
@session_ref = nil
|
|
48
|
+
@features = {}
|
|
49
|
+
@host_transports = {}
|
|
50
|
+
@host_transports_mutex = Mutex.new
|
|
51
|
+
@master_ref = nil
|
|
52
|
+
Redactor.register(password)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :task_timeout, :api_timeout
|
|
56
|
+
|
|
57
|
+
def logged_in?
|
|
58
|
+
!@session_ref.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Logs in, following HOST_IS_SLAVE to the pool master.
|
|
62
|
+
def login
|
|
63
|
+
@login_mutex.synchronize { login_unlocked unless @session_ref }
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def logout
|
|
68
|
+
ref = nil
|
|
69
|
+
@login_mutex.synchronize do
|
|
70
|
+
ref = @session_ref
|
|
71
|
+
@session_ref = nil
|
|
72
|
+
end
|
|
73
|
+
return unless ref
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
rpc("session.logout", [ref])
|
|
77
|
+
rescue Error => e
|
|
78
|
+
@logger.debug("logout failed: #{e.message}")
|
|
79
|
+
ensure
|
|
80
|
+
Redactor.unregister(ref)
|
|
81
|
+
@transport.shutdown
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Calls a XAPI method with the session prepended to params.
|
|
86
|
+
# Retries once after re-login on SESSION_INVALID.
|
|
87
|
+
def call(method, *params)
|
|
88
|
+
call_with_session(method, params)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Calls a method with a longer read timeout (event.from).
|
|
92
|
+
def call_with_timeout(method, read_timeout, *params)
|
|
93
|
+
call_with_session(method, params, read_timeout: read_timeout)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Starts Async.<method> and waits for the task.
|
|
97
|
+
# @return [Task::Result]
|
|
98
|
+
def run_task(method, *params, label: nil, timeout: nil, progress: nil, cancel: nil)
|
|
99
|
+
task_ref = call("Async.#{method}", *params)
|
|
100
|
+
wait_task(task_ref, label: label || method, timeout: timeout, progress: progress, cancel: cancel)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def wait_task(task_ref, label:, timeout: nil, progress: nil, cancel: nil)
|
|
104
|
+
Task.new(self, task_ref, label: label, logger: @logger)
|
|
105
|
+
.wait(timeout: timeout || @task_timeout, progress: progress, cancel: cancel)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Creates a task object for HTTP handlers that accept task_id.
|
|
109
|
+
def create_task(label)
|
|
110
|
+
call("task.create", label, ORIGINATOR)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def http
|
|
114
|
+
HttpTransfer.new(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Proxy: client.VM.get_record(ref) == client.call("VM.get_record", ref)
|
|
118
|
+
class ClassProxy
|
|
119
|
+
def initialize(client, klass)
|
|
120
|
+
@client = client
|
|
121
|
+
@klass = klass
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def method_missing(name, *args)
|
|
125
|
+
@client.call("#{@klass}.#{name}", *args)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def respond_to_missing?(*)
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Only the capitalized classes: lowercase ones (host, pool, ...) would
|
|
134
|
+
# clash with the client's own methods. Use #call for those.
|
|
135
|
+
XAPI_CLASSES = %w[VM VBD VDI VIF SR PBD PIF VM_guest_metrics VM_metrics VTPM].freeze
|
|
136
|
+
|
|
137
|
+
XAPI_CLASSES.each do |klass|
|
|
138
|
+
define_method(klass) { ClassProxy.new(self, klass) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Optional XAPI features, detected on first login.
|
|
142
|
+
def feature?(name)
|
|
143
|
+
@features[name.to_sym] ? true : false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def set_feature(name, value)
|
|
147
|
+
@features = @features.merge(name.to_sym => value)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def session_cookie
|
|
151
|
+
"session_id=#{current_session}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The HTTP handlers answer an expired or evicted session with 401:
|
|
155
|
+
# log in again (unless another thread already did), as #call does on
|
|
156
|
+
# SESSION_INVALID. cookie is the one that was refused.
|
|
157
|
+
def renew_session(cookie)
|
|
158
|
+
relogin(cookie.to_s.delete_prefix("session_id="), nil)
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The pool master's host ref.
|
|
163
|
+
def master_ref
|
|
164
|
+
@master_ref ||= call("pool.get_master", call("pool.get_all").first)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Transport to a specific pool host, for HTTP transfers and the
|
|
168
|
+
# console. The master uses the main connection. For other members the
|
|
169
|
+
# certificate is fetched through the (already verified) master with
|
|
170
|
+
# host.get_server_certificate and pinned, because a configured pin or
|
|
171
|
+
# a hostname check would not match them.
|
|
172
|
+
def transport_for(host_ref)
|
|
173
|
+
return @transport if host_ref.nil? || host_ref == master_ref
|
|
174
|
+
|
|
175
|
+
address = call("host.get_address", host_ref)
|
|
176
|
+
@host_transports_mutex.synchronize do
|
|
177
|
+
@host_transports[address] ||= Transport.new(host: address, port: @port, tls: member_tls(host_ref),
|
|
178
|
+
timeout: @api_timeout)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def member_tls(host_ref)
|
|
185
|
+
pem = call("host.get_server_certificate", host_ref)
|
|
186
|
+
{ fingerprint: OpenSSL::Digest::SHA256.hexdigest(OpenSSL::X509::Certificate.new(pem).to_der) }
|
|
187
|
+
rescue XapiError, OpenSSL::X509::CertificateError => e
|
|
188
|
+
@logger.debug("no server certificate for member host (#{e.message}); using the configured TLS settings")
|
|
189
|
+
@tls
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def current_session
|
|
193
|
+
login unless @session_ref
|
|
194
|
+
@session_ref || login_and_get
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def login_and_get
|
|
198
|
+
@login_mutex.synchronize do
|
|
199
|
+
login_unlocked unless @session_ref
|
|
200
|
+
@session_ref
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def call_with_session(method, params, read_timeout: nil)
|
|
205
|
+
ref = current_session
|
|
206
|
+
attempt = 0
|
|
207
|
+
begin
|
|
208
|
+
rpc(method, [ref, *params], read_timeout: read_timeout)
|
|
209
|
+
rescue XapiError => e
|
|
210
|
+
raise unless e.code == "SESSION_INVALID" && attempt.zero?
|
|
211
|
+
|
|
212
|
+
attempt += 1
|
|
213
|
+
@logger.info("session expired, logging in again")
|
|
214
|
+
ref = relogin(ref, e)
|
|
215
|
+
retry
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def next_id
|
|
220
|
+
@id_mutex.synchronize { @id += 1 }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Logs in into a local variable first; the session is only swapped in
|
|
224
|
+
# once it is complete, so other threads never see a half state.
|
|
225
|
+
def login_unlocked
|
|
226
|
+
redirects = 0
|
|
227
|
+
member = nil
|
|
228
|
+
begin
|
|
229
|
+
ref = rpc("session.login_with_password", [@username, @password, API_VERSION, ORIGINATOR])
|
|
230
|
+
rescue TlsError => e
|
|
231
|
+
raise unless member
|
|
232
|
+
|
|
233
|
+
# The master's address never appears in the configuration, so
|
|
234
|
+
# say where it came from.
|
|
235
|
+
raise TlsError.new(e.host, "#{e.reason}. #{redirect_hint(member, e.host)}")
|
|
236
|
+
rescue XapiError => e
|
|
237
|
+
raise unless e.code == "HOST_IS_SLAVE" && redirects < MAX_REDIRECTS && e.params.first
|
|
238
|
+
|
|
239
|
+
master = e.params.first
|
|
240
|
+
# One pin matches only the member's own certificate, never the
|
|
241
|
+
# master's. With several (the pool's hosts) the master's
|
|
242
|
+
# certificate is checked against them.
|
|
243
|
+
if Transport.normalize_fingerprints(@tls && @tls[:fingerprint]).size == 1
|
|
244
|
+
raise PoolMemberError.new(@host, master)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
redirects += 1
|
|
248
|
+
member ||= @host
|
|
249
|
+
@logger.info("#{@host} is a pool member; reconnecting to master #{master}")
|
|
250
|
+
@transport.shutdown
|
|
251
|
+
@host = master
|
|
252
|
+
@transport = Transport.new(host: master, port: @port, tls: @tls, timeout: @api_timeout)
|
|
253
|
+
retry
|
|
254
|
+
end
|
|
255
|
+
Redactor.register(ref)
|
|
256
|
+
features = detect_features(ref)
|
|
257
|
+
# Answers learned during the session (ImageUpload's qcow2 probe per
|
|
258
|
+
# host) survive a relogin.
|
|
259
|
+
@features = features.merge(@features.select { |key, _| key.to_s.start_with?("qcow2_import@") })
|
|
260
|
+
@master_ref = features[:master_ref]
|
|
261
|
+
@session_ref = ref
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# For a TLS failure on the master after HOST_IS_SLAVE, by TLS mode.
|
|
265
|
+
# `vagrant xapi hosts` cannot help yet: it would take the same way.
|
|
266
|
+
def redirect_hint(member, master)
|
|
267
|
+
said = "#{member} is a pool member and sent the login on to its master #{master}"
|
|
268
|
+
if Transport.normalize_fingerprints(@tls && @tls[:fingerprint]).empty?
|
|
269
|
+
"#{said}; the master's certificate must pass the same CA check, for the address #{master}"
|
|
270
|
+
else
|
|
271
|
+
"#{said}, whose certificate is not among the tls_fingerprint pins. Add the master's fingerprint " \
|
|
272
|
+
"(openssl s_client -connect #{master}:#{@port} </dev/null | openssl x509 -noout -fingerprint " \
|
|
273
|
+
"-sha256); after that `vagrant xapi hosts` lists the fingerprints of all hosts"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Logs in again unless another thread already did. A failing login
|
|
278
|
+
# raises its own error (e.g. a rotated password), not SESSION_INVALID.
|
|
279
|
+
# @return [String] the session to use
|
|
280
|
+
def relogin(failed_ref, _error)
|
|
281
|
+
@login_mutex.synchronize do
|
|
282
|
+
return @session_ref if @session_ref && @session_ref != failed_ref
|
|
283
|
+
|
|
284
|
+
@session_ref = nil
|
|
285
|
+
Redactor.unregister(failed_ref) if failed_ref
|
|
286
|
+
login_unlocked
|
|
287
|
+
@session_ref
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def detect_features(ref)
|
|
292
|
+
features = {}
|
|
293
|
+
begin
|
|
294
|
+
pool = rpc("pool.get_all", [ref]).first
|
|
295
|
+
master = rpc("pool.get_master", [ref, pool])
|
|
296
|
+
features[:master_ref] = master
|
|
297
|
+
major = rpc("host.get_API_version_major", [ref, master]).to_i
|
|
298
|
+
minor = rpc("host.get_API_version_minor", [ref, master]).to_i
|
|
299
|
+
features[:api_version] = [major, minor]
|
|
300
|
+
sw = rpc("host.get_software_version", [ref, master])
|
|
301
|
+
features[:product_brand] = sw["product_brand"] || sw["platform_name"]
|
|
302
|
+
features[:product_version] = sw["product_version"] || sw["platform_version"]
|
|
303
|
+
rescue XapiError => e
|
|
304
|
+
@logger.debug("version detection failed: #{e.message}")
|
|
305
|
+
end
|
|
306
|
+
features[:vtpm] = method_available?(ref, "VTPM.get_all")
|
|
307
|
+
features
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def method_available?(ref, method)
|
|
311
|
+
rpc(method, [ref])
|
|
312
|
+
true
|
|
313
|
+
rescue XapiError => e
|
|
314
|
+
return false if %w[MESSAGE_METHOD_UNKNOWN UNKNOWN_XENAPI_MESSAGE].include?(e.code)
|
|
315
|
+
|
|
316
|
+
# Other errors (e.g. RBAC) still mean the class exists.
|
|
317
|
+
true
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Raw JSON-RPC call; params as given.
|
|
321
|
+
def rpc(method, params, read_timeout: nil)
|
|
322
|
+
id = next_id
|
|
323
|
+
body = JSON.generate(jsonrpc: "2.0", method: method, params: params, id: id)
|
|
324
|
+
@logger.debug { "xapi -> #{method} #{log_params(method, params).inspect}" }
|
|
325
|
+
|
|
326
|
+
response = @transport.with_connection(read_timeout: read_timeout) do |http|
|
|
327
|
+
req = Net::HTTP::Post.new("/jsonrpc")
|
|
328
|
+
req["Content-Type"] = "application/json"
|
|
329
|
+
req["User-Agent"] = ORIGINATOR
|
|
330
|
+
req.body = body
|
|
331
|
+
http.request(req)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
335
|
+
raise ProtocolError, "HTTP #{response.code} from #{@host} for #{method}"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
parse_response(method, response.body)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parse_response(method, body)
|
|
342
|
+
data = begin
|
|
343
|
+
JSON.parse(body)
|
|
344
|
+
rescue JSON::ParserError => e
|
|
345
|
+
raise ProtocolError, "invalid JSON from #{@host} for #{method}: #{e.message}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
if data.is_a?(Hash) && data["error"]
|
|
349
|
+
err = data["error"]
|
|
350
|
+
code, params = extract_error(err)
|
|
351
|
+
@logger.debug("xapi <- #{method} error #{code} #{params.inspect}")
|
|
352
|
+
raise XapiError.new(code, params, method_name: method)
|
|
353
|
+
end
|
|
354
|
+
raise ProtocolError, "no result from #{@host} for #{method}" unless data.is_a?(Hash) && data.key?("result")
|
|
355
|
+
|
|
356
|
+
result = data["result"]
|
|
357
|
+
# Some XAPI versions wrap like XML-RPC: {"Status":"Success","Value":...}
|
|
358
|
+
if result.is_a?(Hash) && result.key?("Status") && result.size <= 2
|
|
359
|
+
if result["Status"] == "Success"
|
|
360
|
+
result = result["Value"]
|
|
361
|
+
else
|
|
362
|
+
info = Array(result["ErrorDescription"])
|
|
363
|
+
raise XapiError.new(info.first || "UNKNOWN", info.drop(1), method_name: method)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
@logger.debug("xapi <- #{method} ok")
|
|
367
|
+
result
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def extract_error(err)
|
|
371
|
+
if err.is_a?(Hash)
|
|
372
|
+
data = err["data"]
|
|
373
|
+
if data.is_a?(Array) && data.first.is_a?(String) && err["message"].to_s.empty?
|
|
374
|
+
[data.first, data.drop(1)]
|
|
375
|
+
else
|
|
376
|
+
[err["message"] || err["code"] || "UNKNOWN", Array(data)]
|
|
377
|
+
end
|
|
378
|
+
elsif err.is_a?(Array)
|
|
379
|
+
[err.first, err.drop(1)]
|
|
380
|
+
else
|
|
381
|
+
[err.to_s, []]
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def log_params(method, params)
|
|
386
|
+
return ["[user]", Redactor::MASK, *params.drop(2)] if method.start_with?("session.login")
|
|
387
|
+
|
|
388
|
+
params.each_with_index.map { |p, i| i.zero? && p.to_s.start_with?("OpaqueRef:") ? "<session>" : p }
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VagrantPlugins
|
|
4
|
+
module Xapi
|
|
5
|
+
module Api
|
|
6
|
+
# Base class for everything the XAPI client raises. The client layer does
|
|
7
|
+
# not know Vagrant; the integration layer translates these into
|
|
8
|
+
# Vagrant::Errors::VagrantError subclasses (see VagrantPlugins::Xapi::Errors).
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# An error returned by XAPI itself, e.g. ["SR_FULL", "123", "456"].
|
|
12
|
+
class XapiError < Error
|
|
13
|
+
attr_reader :code, :params, :method_name
|
|
14
|
+
|
|
15
|
+
def initialize(code, params = [], method_name: nil)
|
|
16
|
+
@code = code.to_s
|
|
17
|
+
@params = Array(params).map(&:to_s)
|
|
18
|
+
@method_name = method_name
|
|
19
|
+
super("#{@code}#{@params.empty? ? "" : " #{@params.inspect}"}" \
|
|
20
|
+
"#{method_name ? " (#{method_name})" : ""}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The host could not be reached at the TCP/HTTP level.
|
|
25
|
+
class ConnectionError < Error
|
|
26
|
+
attr_reader :host, :reason
|
|
27
|
+
|
|
28
|
+
def initialize(host, reason)
|
|
29
|
+
@host = host
|
|
30
|
+
@reason = reason
|
|
31
|
+
super("cannot connect to #{host}: #{reason}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The host did not answer in time (connect or read timeout).
|
|
36
|
+
class TimeoutError < ConnectionError; end
|
|
37
|
+
|
|
38
|
+
# TLS verification failed (certificate, CA or fingerprint pin).
|
|
39
|
+
class TlsError < Error
|
|
40
|
+
attr_reader :host, :reason
|
|
41
|
+
|
|
42
|
+
def initialize(host, reason)
|
|
43
|
+
@host = host
|
|
44
|
+
@reason = reason
|
|
45
|
+
super("TLS verification of #{host} failed: #{reason}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Login on a pool member while a TLS pin is configured: the pin cannot
|
|
50
|
+
# match the master's certificate, so the redirect is refused.
|
|
51
|
+
class PoolMemberError < Error
|
|
52
|
+
attr_reader :host, :master
|
|
53
|
+
|
|
54
|
+
def initialize(host, master)
|
|
55
|
+
@host = host
|
|
56
|
+
@master = master
|
|
57
|
+
super("#{host} is a pool member; the pool master is #{master}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The response was not valid JSON-RPC.
|
|
62
|
+
class ProtocolError < Error; end
|
|
63
|
+
|
|
64
|
+
# HTTP transfer (/import, /export, ...) failed.
|
|
65
|
+
class HttpError < Error
|
|
66
|
+
attr_reader :status, :path
|
|
67
|
+
|
|
68
|
+
def initialize(status, path, body = nil)
|
|
69
|
+
@status = status.to_i
|
|
70
|
+
@path = path
|
|
71
|
+
detail = body.to_s.strip[0, 300]
|
|
72
|
+
super("HTTP #{status} on #{path}#{detail.empty? ? "" : ": #{detail}"}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A task did not finish within the configured timeout.
|
|
77
|
+
class TaskTimeout < Error
|
|
78
|
+
attr_reader :label, :timeout
|
|
79
|
+
|
|
80
|
+
def initialize(label, timeout)
|
|
81
|
+
@label = label
|
|
82
|
+
@timeout = timeout
|
|
83
|
+
super("task '#{label}' did not finish within #{timeout}s")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A task was cancelled (by Ctrl-C or by someone else).
|
|
88
|
+
class TaskCancelled < Error
|
|
89
|
+
attr_reader :label
|
|
90
|
+
|
|
91
|
+
def initialize(label)
|
|
92
|
+
@label = label
|
|
93
|
+
super("task '#{label}' was cancelled")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|