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,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
require_relative "base"
|
|
8
|
+
|
|
9
|
+
module VagrantPlugins
|
|
10
|
+
module Xapi
|
|
11
|
+
module Services
|
|
12
|
+
# A lock that works across processes, users and machines: a key
|
|
13
|
+
# vagrant-xapi.lock.<name> in the pool's other_config.
|
|
14
|
+
#
|
|
15
|
+
# pool.add_to_other_config refuses an existing key with
|
|
16
|
+
# MAP_DUPLICATE_KEY, so taking the lock is atomic. The value holds the
|
|
17
|
+
# owner and an expiry time. While the lock is held, a heartbeat keeps
|
|
18
|
+
# it alive (keys <lock>.alive.0 and .alive.1, written in turn so one of
|
|
19
|
+
# them is always fresh). Only a lock whose expiry and heartbeats have
|
|
20
|
+
# all passed counts as abandoned and may be taken over. Times are the
|
|
21
|
+
# pool master's clock, so machines with skewed clocks agree.
|
|
22
|
+
class Lock
|
|
23
|
+
KEY_PREFIX = "#{Marking::PREFIX}lock."
|
|
24
|
+
|
|
25
|
+
attr_reader :name, :owner
|
|
26
|
+
|
|
27
|
+
# @param ttl [Integer] seconds before the lock counts as abandoned
|
|
28
|
+
# @param wait [Integer] seconds to wait for a busy lock
|
|
29
|
+
def initialize(ctx, name, ttl:, wait:, poll: 2, clock: nil)
|
|
30
|
+
@ctx = ctx
|
|
31
|
+
@client = ctx.client
|
|
32
|
+
@name = name
|
|
33
|
+
@ttl = ttl
|
|
34
|
+
@wait = wait
|
|
35
|
+
@poll = poll
|
|
36
|
+
@clock = clock || method(:server_now)
|
|
37
|
+
@owner = "#{Marking.user}@#{Marking.client_host}:#{Process.pid}:#{SecureRandom.hex(4)}"
|
|
38
|
+
@held = false
|
|
39
|
+
@in_process = self.class.in_process_mutex(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def key
|
|
43
|
+
"#{KEY_PREFIX}#{@name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def held?
|
|
47
|
+
@held
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Runs the block while holding the lock.
|
|
51
|
+
def synchronize
|
|
52
|
+
# Threads of one process first serialize on a local mutex, so they
|
|
53
|
+
# do not poll XAPI against each other. Waiting for it obeys the same
|
|
54
|
+
# deadline and Ctrl-C as waiting for the pool lock.
|
|
55
|
+
deadline = @clock.call + @wait
|
|
56
|
+
announced = false
|
|
57
|
+
until @in_process.try_lock
|
|
58
|
+
raise Error.new(:lock_timeout, name: @name, holder: "another machine of this run") if @clock.call > deadline
|
|
59
|
+
raise Api::TaskCancelled, "lock #{@name}" if @ctx.cancelled?
|
|
60
|
+
|
|
61
|
+
unless announced
|
|
62
|
+
@ctx.ui&.info(:waiting_for_lock, name: @name, holder: "another machine of this run")
|
|
63
|
+
announced = true
|
|
64
|
+
end
|
|
65
|
+
sleep [@poll, 0.5].min
|
|
66
|
+
end
|
|
67
|
+
begin
|
|
68
|
+
acquire(deadline)
|
|
69
|
+
heartbeat = start_heartbeat
|
|
70
|
+
begin
|
|
71
|
+
yield
|
|
72
|
+
ensure
|
|
73
|
+
stop_heartbeat(heartbeat)
|
|
74
|
+
release
|
|
75
|
+
end
|
|
76
|
+
ensure
|
|
77
|
+
@in_process.unlock
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Refreshes one heartbeat key; called every ttl/4 while held. While we
|
|
82
|
+
# hold the lock, any other value in the slot is stale (a crashed
|
|
83
|
+
# holder's), so it is replaced.
|
|
84
|
+
# @return [Boolean] whether the heartbeat was written
|
|
85
|
+
def beat(slot)
|
|
86
|
+
slot_key = alive_key(slot)
|
|
87
|
+
remove(slot_key) if raw_value(slot_key)
|
|
88
|
+
return true if add(slot_key, new_value)
|
|
89
|
+
|
|
90
|
+
@ctx.logger&.warn("lock #{@name}: heartbeat #{slot_key} could not be written")
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def acquire(deadline = nil)
|
|
95
|
+
deadline ||= @clock.call + @wait
|
|
96
|
+
announced = false
|
|
97
|
+
loop do
|
|
98
|
+
return true if try_acquire
|
|
99
|
+
|
|
100
|
+
raise Error.new(:lock_timeout, name: @name, holder: holder_description) if @clock.call > deadline
|
|
101
|
+
raise Api::TaskCancelled, "lock #{@name}" if @ctx.cancelled?
|
|
102
|
+
|
|
103
|
+
unless announced
|
|
104
|
+
@ctx.ui&.info(:waiting_for_lock, name: @name, holder: holder_description)
|
|
105
|
+
announced = true
|
|
106
|
+
end
|
|
107
|
+
sleep @poll
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def try_acquire
|
|
112
|
+
return verify_held if add(key, new_value)
|
|
113
|
+
|
|
114
|
+
stale = raw_value
|
|
115
|
+
take_over(stale) if stale && abandoned?(stale)
|
|
116
|
+
@held
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def release
|
|
120
|
+
return unless @held
|
|
121
|
+
|
|
122
|
+
[alive_key(0), alive_key(1)].each do |slot_key|
|
|
123
|
+
current = raw_value(slot_key)
|
|
124
|
+
remove(slot_key) if current && parse(current)["owner"] == @owner
|
|
125
|
+
end
|
|
126
|
+
current = read
|
|
127
|
+
remove(key) if current.nil? || current["owner"] == @owner
|
|
128
|
+
@held = false
|
|
129
|
+
rescue Api::Error
|
|
130
|
+
@held = false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def alive_key(slot)
|
|
136
|
+
"#{key}.alive.#{slot}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def start_heartbeat
|
|
140
|
+
interval = [@ttl / 4.0, 1].max
|
|
141
|
+
@stop_beating = false
|
|
142
|
+
Thread.new do
|
|
143
|
+
slot = 0
|
|
144
|
+
until @stop_beating
|
|
145
|
+
waited = 0.0
|
|
146
|
+
while waited < interval && !@stop_beating
|
|
147
|
+
sleep 0.2
|
|
148
|
+
waited += 0.2
|
|
149
|
+
end
|
|
150
|
+
break if @stop_beating
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
beat(slot)
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
@ctx.logger&.warn("lock heartbeat for #{@name} failed: #{e.message}")
|
|
156
|
+
end
|
|
157
|
+
slot = 1 - slot
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Stops the heartbeat and waits for it, so no beat lands after release.
|
|
163
|
+
def stop_heartbeat(thread)
|
|
164
|
+
return unless thread
|
|
165
|
+
|
|
166
|
+
@stop_beating = true
|
|
167
|
+
thread.join(30) || thread.kill
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A lock is abandoned when its own expiry and every heartbeat of the
|
|
171
|
+
# same owner have passed.
|
|
172
|
+
def abandoned?(stale)
|
|
173
|
+
lock = parse(stale)
|
|
174
|
+
return false unless lock.fetch("expires", 0).to_i < @clock.call
|
|
175
|
+
|
|
176
|
+
[alive_key(0), alive_key(1)].none? do |slot_key|
|
|
177
|
+
beat = raw_value(slot_key)
|
|
178
|
+
next false unless beat
|
|
179
|
+
|
|
180
|
+
value = parse(beat)
|
|
181
|
+
value["owner"] == lock["owner"] && value.fetch("expires", 0).to_i >= @clock.call
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Seconds since the epoch on the pool master; the local clock when
|
|
186
|
+
# the master's time cannot be read.
|
|
187
|
+
def server_now
|
|
188
|
+
@server_offset ||= begin
|
|
189
|
+
raw = @client.call("host.get_servertime", @client.call("pool.get_master", pool))
|
|
190
|
+
Time.strptime(raw.to_s, "%Y%m%dT%H:%M:%S%z").to_i - Time.now.to_i
|
|
191
|
+
rescue Api::Error, ArgumentError => e
|
|
192
|
+
@ctx.logger&.warn("lock #{@name}: cannot read the pool master's time (#{e.message}); " \
|
|
193
|
+
"using this machine's clock, so clock skew between machines matters")
|
|
194
|
+
0
|
|
195
|
+
end
|
|
196
|
+
Time.now.to_i + @server_offset
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def new_value
|
|
200
|
+
JSON.generate("owner" => @owner, "expires" => @clock.call + @ttl)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Taking over an abandoned lock is serialized through a second key,
|
|
204
|
+
# also set atomically: XAPI has no compare-and-swap, so without it two
|
|
205
|
+
# processes could both remove the stale value and each think they
|
|
206
|
+
# hold the lock.
|
|
207
|
+
def take_over(stale)
|
|
208
|
+
guard = "#{key}.takeover"
|
|
209
|
+
guard_value = JSON.generate("owner" => @owner, "expires" => @clock.call + 60)
|
|
210
|
+
unless add(guard, guard_value)
|
|
211
|
+
# A guard left by a crashed taker: remove it, but only if it is
|
|
212
|
+
# still that same value (not a fresh guard of another process).
|
|
213
|
+
current_guard = raw_value(guard)
|
|
214
|
+
remove(guard) if current_guard && expired?(current_guard) && raw_value(guard) == current_guard
|
|
215
|
+
return
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
# Still the same stale value? Then nobody else took it over.
|
|
220
|
+
return unless raw_value == stale && abandoned?(stale)
|
|
221
|
+
|
|
222
|
+
remove(key)
|
|
223
|
+
return unless add(key, new_value) && verify_held
|
|
224
|
+
|
|
225
|
+
# The crashed holder's heartbeats would block ours.
|
|
226
|
+
[alive_key(0), alive_key(1)].each { |slot_key| remove(slot_key) }
|
|
227
|
+
ensure
|
|
228
|
+
remove(guard)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Re-read after taking the lock: only a lock that carries our owner
|
|
233
|
+
# counts as held.
|
|
234
|
+
def verify_held
|
|
235
|
+
current = read
|
|
236
|
+
@held = current ? current["owner"] == @owner : false
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @return [Boolean] whether the key was added
|
|
240
|
+
def add(name, value)
|
|
241
|
+
@client.call("pool.add_to_other_config", pool, name, value)
|
|
242
|
+
true
|
|
243
|
+
rescue Api::XapiError => e
|
|
244
|
+
raise Error.new(:lock_permission_denied, name: @name) if e.code == "RBAC_PERMISSION_DENIED"
|
|
245
|
+
raise unless e.code == "MAP_DUPLICATE_KEY"
|
|
246
|
+
|
|
247
|
+
false
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def expired?(raw)
|
|
251
|
+
parse(raw).fetch("expires", 0).to_i < @clock.call
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def holder_description
|
|
255
|
+
current = read
|
|
256
|
+
current ? current["owner"].to_s.sub(/:[0-9a-f]{8}\z/, "") : "unknown"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def read
|
|
260
|
+
raw = raw_value
|
|
261
|
+
raw && parse(raw)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def raw_value(name = key)
|
|
265
|
+
@client.call("pool.get_other_config", pool)[name]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def parse(raw)
|
|
269
|
+
JSON.parse(raw)
|
|
270
|
+
rescue JSON::ParserError
|
|
271
|
+
{ "owner" => "unknown", "expires" => 0 }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def remove(name)
|
|
275
|
+
@client.call("pool.remove_from_other_config", pool, name)
|
|
276
|
+
rescue Api::XapiError
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def pool
|
|
281
|
+
@pool ||= @client.call("pool.get_all").first
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
MUTEXES = {} # rubocop:disable Style/MutableConstant
|
|
285
|
+
MUTEXES_LOCK = Mutex.new
|
|
286
|
+
|
|
287
|
+
def self.in_process_mutex(name)
|
|
288
|
+
MUTEXES_LOCK.synchronize { MUTEXES[name] ||= Mutex.new }
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Services
|
|
8
|
+
# Finds pool objects by name or UUID and turns "not found" into
|
|
9
|
+
# readable errors.
|
|
10
|
+
class Lookup
|
|
11
|
+
UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(ctx)
|
|
14
|
+
@ctx = ctx
|
|
15
|
+
@client = ctx.client
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.uuid?(value)
|
|
19
|
+
UUID_RE.match?(value.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String, nil] VM ref, nil when the UUID is unknown
|
|
23
|
+
def vm_by_uuid(uuid)
|
|
24
|
+
return nil if uuid.nil? || uuid.to_s.empty?
|
|
25
|
+
|
|
26
|
+
@client.call("VM.get_by_uuid", uuid.to_s)
|
|
27
|
+
rescue Api::XapiError => e
|
|
28
|
+
return nil if %w[UUID_INVALID HANDLE_INVALID].include?(e.code)
|
|
29
|
+
|
|
30
|
+
raise
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pool
|
|
34
|
+
@pool ||= @client.call("pool.get_all").first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def master
|
|
38
|
+
@master ||= @client.call("pool.get_master", pool)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param name [String, nil] name or UUID; nil = pool default SR
|
|
42
|
+
def sr(name)
|
|
43
|
+
if name.nil? || name.to_s.empty?
|
|
44
|
+
ref = @client.call("pool.get_default_SR", pool)
|
|
45
|
+
raise Error.new(:no_default_sr) if Helpers.null_ref?(ref)
|
|
46
|
+
|
|
47
|
+
return ref
|
|
48
|
+
end
|
|
49
|
+
find(:SR, name) || raise(Error.new(:sr_not_found, name: name))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# By UUID, name, or bridge (e.g. "xenbr1"), in that order: users of the
|
|
53
|
+
# old plugin know their networks by bridge name.
|
|
54
|
+
def network(name)
|
|
55
|
+
find(:network, name) || find_by_bridge(name) || raise(Error.new(:network_not_found, name: name))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def network_by_bridge(bridge)
|
|
59
|
+
find_by_bridge(bridge) || raise(Error.new(:network_not_found, name: bridge))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [String, nil] network ref
|
|
63
|
+
def find_by_bridge(bridge)
|
|
64
|
+
@client.call("network.get_all_records_where", %(field "bridge"="#{Helpers.where_value(bridge)}")).keys.first
|
|
65
|
+
rescue Api::XapiError
|
|
66
|
+
@client.call("network.get_all_records").find { |_r, n| n["bridge"] == bridge.to_s }&.first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def host(name)
|
|
70
|
+
find(:host, name, by: %w[name_label hostname address]) ||
|
|
71
|
+
raise(Error.new(:host_not_found, name: name))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The network of the master's management PIF.
|
|
75
|
+
def management_network
|
|
76
|
+
pifs = @client.call("host.get_PIFs", master)
|
|
77
|
+
pifs.each do |pif|
|
|
78
|
+
rec = @client.call("PIF.get_record", pif)
|
|
79
|
+
return rec["network"] if rec["management"]
|
|
80
|
+
end
|
|
81
|
+
raise Error.new(:no_management_network)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The host-internal management network (HIMN), if present.
|
|
85
|
+
def himn_network
|
|
86
|
+
@client.call("network.get_all_records").each do |ref, rec|
|
|
87
|
+
oc = rec["other_config"] || {}
|
|
88
|
+
return ref if oc["is_host_internal_management_network"].to_s == "true"
|
|
89
|
+
end
|
|
90
|
+
raise Error.new(:himn_not_found)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def other_install_media_template
|
|
94
|
+
refs = @client.call("VM.get_by_name_label", "Other install media")
|
|
95
|
+
ref = refs.find { |r| @client.call("VM.get_is_a_template", r) }
|
|
96
|
+
raise Error.new(:base_template_missing, name: "Other install media") unless ref
|
|
97
|
+
|
|
98
|
+
ref
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def iso_vdi(name)
|
|
102
|
+
if Lookup.uuid?(name)
|
|
103
|
+
begin
|
|
104
|
+
return @client.call("VDI.get_by_uuid", name)
|
|
105
|
+
rescue Api::XapiError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
refs = @client.call("VDI.get_by_name_label", name)
|
|
110
|
+
refs.each do |ref|
|
|
111
|
+
sr = @client.call("VDI.get_SR", ref)
|
|
112
|
+
return ref if @client.call("SR.get_content_type", sr) == "iso"
|
|
113
|
+
end
|
|
114
|
+
raise Error.new(:iso_not_found, name: name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The host to send an HTTP transfer for this SR to: XAPI redirects
|
|
118
|
+
# (302) to a host that has the SR attached, and a streamed PUT cannot
|
|
119
|
+
# follow that. nil means the master (shared SRs, or the master has it).
|
|
120
|
+
def transfer_host_for_sr(sr)
|
|
121
|
+
hosts = attached_hosts(sr)
|
|
122
|
+
return nil if hosts.empty? || hosts.include?(master)
|
|
123
|
+
|
|
124
|
+
hosts.first
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The host for exporting a VM: one that sees all its disks' SRs,
|
|
128
|
+
# preferring the master, then the host the VM runs on.
|
|
129
|
+
def transfer_host_for_vm(vm)
|
|
130
|
+
srs = @client.call("VM.get_VBDs", vm).filter_map do |vbd|
|
|
131
|
+
rec = @client.call("VBD.get_record", vbd)
|
|
132
|
+
next if rec["type"] != "Disk" || Helpers.null_ref?(rec["VDI"])
|
|
133
|
+
|
|
134
|
+
@client.call("VDI.get_SR", rec["VDI"])
|
|
135
|
+
end.uniq
|
|
136
|
+
return nil if srs.empty?
|
|
137
|
+
|
|
138
|
+
candidates = srs.map { |sr| attached_hosts(sr) }.reject(&:empty?).reduce { |a, b| a & b } || []
|
|
139
|
+
return nil if candidates.empty? || candidates.include?(master)
|
|
140
|
+
|
|
141
|
+
resident = @client.call("VM.get_resident_on", vm)
|
|
142
|
+
candidates.include?(resident) ? resident : candidates.first
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def find(klass, name, by: %w[name_label])
|
|
148
|
+
value = name.to_s
|
|
149
|
+
if Lookup.uuid?(value)
|
|
150
|
+
begin
|
|
151
|
+
return @client.call("#{klass}.get_by_uuid", value)
|
|
152
|
+
rescue Api::XapiError => e
|
|
153
|
+
raise unless %w[UUID_INVALID HANDLE_INVALID].include?(e.code)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if by == %w[name_label]
|
|
158
|
+
refs = @client.call("#{klass}.get_by_name_label", value)
|
|
159
|
+
raise Error.new(:ambiguous_name, kind: klass.to_s, name: value, count: refs.size) if refs.size > 1
|
|
160
|
+
|
|
161
|
+
return refs.first
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
matches = @client.call("#{klass}.get_all_records").select do |_ref, rec|
|
|
165
|
+
by.any? { |field| rec[field].to_s == value }
|
|
166
|
+
end
|
|
167
|
+
raise Error.new(:ambiguous_name, kind: klass.to_s, name: value, count: matches.size) if matches.size > 1
|
|
168
|
+
|
|
169
|
+
matches.keys.first
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def attached_hosts(sr)
|
|
173
|
+
@client.call("SR.get_PBDs", sr).filter_map do |pbd|
|
|
174
|
+
rec = @client.call("PBD.get_record", pbd)
|
|
175
|
+
rec["host"] if rec["currently_attached"]
|
|
176
|
+
end.uniq
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|