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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE +21 -0
  4. data/README.md +460 -4
  5. data/docs/architecture.md +1420 -0
  6. data/docs/operations.md +531 -0
  7. data/lib/vagrant-xapi/action/base.rb +103 -0
  8. data/lib/vagrant-xapi/action/cloud_init.rb +130 -0
  9. data/lib/vagrant-xapi/action/create.rb +127 -0
  10. data/lib/vagrant-xapi/action/destroy.rb +68 -0
  11. data/lib/vagrant-xapi/action/network.rb +229 -0
  12. data/lib/vagrant-xapi/action/package.rb +103 -0
  13. data/lib/vagrant-xapi/action/power.rb +185 -0
  14. data/lib/vagrant-xapi/action/snapshots.rb +43 -0
  15. data/lib/vagrant-xapi/action.rb +317 -0
  16. data/lib/vagrant-xapi/api/client.rb +393 -0
  17. data/lib/vagrant-xapi/api/errors.rb +98 -0
  18. data/lib/vagrant-xapi/api/http_transfer.rb +332 -0
  19. data/lib/vagrant-xapi/api/redactor.rb +77 -0
  20. data/lib/vagrant-xapi/api/session_registry.rb +59 -0
  21. data/lib/vagrant-xapi/api/task.rb +156 -0
  22. data/lib/vagrant-xapi/api/transport.rb +249 -0
  23. data/lib/vagrant-xapi/cap.rb +90 -0
  24. data/lib/vagrant-xapi/command/adopt.rb +92 -0
  25. data/lib/vagrant-xapi/command/base.rb +79 -0
  26. data/lib/vagrant-xapi/command/console.rb +87 -0
  27. data/lib/vagrant-xapi/command/diff.rb +267 -0
  28. data/lib/vagrant-xapi/command/hosts.rb +63 -0
  29. data/lib/vagrant-xapi/command/info.rb +94 -0
  30. data/lib/vagrant-xapi/command/marked.rb +85 -0
  31. data/lib/vagrant-xapi/command/orphans.rb +96 -0
  32. data/lib/vagrant-xapi/command/root.rb +55 -0
  33. data/lib/vagrant-xapi/command/templates.rb +91 -0
  34. data/lib/vagrant-xapi/config.rb +333 -0
  35. data/lib/vagrant-xapi/errors.rb +113 -0
  36. data/lib/vagrant-xapi/extensions.rb +80 -0
  37. data/lib/vagrant-xapi/glue.rb +330 -0
  38. data/lib/vagrant-xapi/legacy/plugin.rb +61 -0
  39. data/lib/vagrant-xapi/legacy/xenapi.rb +129 -0
  40. data/lib/vagrant-xapi/plugin.rb +58 -0
  41. data/lib/vagrant-xapi/provider.rb +135 -0
  42. data/lib/vagrant-xapi/services/adoption.rb +73 -0
  43. data/lib/vagrant-xapi/services/base.rb +242 -0
  44. data/lib/vagrant-xapi/services/box_source.rb +218 -0
  45. data/lib/vagrant-xapi/services/console.rb +115 -0
  46. data/lib/vagrant-xapi/services/content_diff.rb +96 -0
  47. data/lib/vagrant-xapi/services/destroyer.rb +104 -0
  48. data/lib/vagrant-xapi/services/diff_disks.rb +346 -0
  49. data/lib/vagrant-xapi/services/differ.rb +382 -0
  50. data/lib/vagrant-xapi/services/disk_image.rb +122 -0
  51. data/lib/vagrant-xapi/services/disks.rb +281 -0
  52. data/lib/vagrant-xapi/services/exporter.rb +128 -0
  53. data/lib/vagrant-xapi/services/file_diff.rb +249 -0
  54. data/lib/vagrant-xapi/services/guest_files.rb +730 -0
  55. data/lib/vagrant-xapi/services/hosts.rb +55 -0
  56. data/lib/vagrant-xapi/services/image_upload.rb +172 -0
  57. data/lib/vagrant-xapi/services/inventory.rb +225 -0
  58. data/lib/vagrant-xapi/services/ip_detector.rb +107 -0
  59. data/lib/vagrant-xapi/services/lock.rb +293 -0
  60. data/lib/vagrant-xapi/services/lookup.rb +181 -0
  61. data/lib/vagrant-xapi/services/networks.rb +248 -0
  62. data/lib/vagrant-xapi/services/power.rb +108 -0
  63. data/lib/vagrant-xapi/services/rfb.rb +129 -0
  64. data/lib/vagrant-xapi/services/snapshots.rb +81 -0
  65. data/lib/vagrant-xapi/services/template_cache.rb +263 -0
  66. data/lib/vagrant-xapi/services/tunnels.rb +217 -0
  67. data/lib/vagrant-xapi/services/vm_builder.rb +215 -0
  68. data/lib/vagrant-xapi/version.rb +7 -0
  69. data/lib/vagrant-xapi/xenserver/config.rb +69 -0
  70. data/lib/vagrant-xapi/xenserver/migrate.rb +103 -0
  71. data/lib/vagrant-xapi/xenserver/plugin.rb +96 -0
  72. data/lib/vagrant-xapi/xenserver/provider.rb +22 -0
  73. data/lib/vagrant-xapi/xenserver/takeover.rb +79 -0
  74. data/lib/vagrant-xapi.rb +21 -2
  75. data/lib/xenapi.rb +22 -0
  76. data/locales/en.yml +517 -0
  77. data/locales/legacy.en.yml +6 -0
  78. 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