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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Services
|
|
10
|
+
# The hosts of the pool as the master reports them: address, role and
|
|
11
|
+
# TLS certificate. The certificates come from host.get_server_certificate,
|
|
12
|
+
# so through the verified connection to the master.
|
|
13
|
+
class Hosts
|
|
14
|
+
HostInfo = Struct.new(:uuid, :name, :address, :role, :enabled, :fingerprint, :certificate,
|
|
15
|
+
keyword_init: true)
|
|
16
|
+
|
|
17
|
+
def initialize(ctx)
|
|
18
|
+
@client = ctx.client
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array<HostInfo>] the master first, then by name
|
|
22
|
+
def list
|
|
23
|
+
master = @client.master_ref
|
|
24
|
+
@client.call("host.get_all_records").map do |ref, rec|
|
|
25
|
+
pem = certificate(ref)
|
|
26
|
+
HostInfo.new(uuid: rec["uuid"], name: rec["name_label"], address: rec["address"],
|
|
27
|
+
role: ref == master ? "master" : "member", enabled: rec["enabled"] != false,
|
|
28
|
+
fingerprint: pem && fingerprint(pem), certificate: pem)
|
|
29
|
+
end.sort_by { |h| [h.role == "master" ? 0 : 1, h.name.to_s] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# SHA-256 of the DER certificate, as tls_fingerprint takes it.
|
|
33
|
+
def self.fingerprint(pem)
|
|
34
|
+
OpenSSL::Digest::SHA256.hexdigest(OpenSSL::X509::Certificate.new(pem).to_der).scan(/../).join(":").upcase
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def fingerprint(pem)
|
|
40
|
+
self.class.fingerprint(pem)
|
|
41
|
+
rescue OpenSSL::X509::CertificateError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# nil when the host has none or the role may not read it.
|
|
46
|
+
def certificate(ref)
|
|
47
|
+
pem = @client.call("host.get_server_certificate", ref).to_s
|
|
48
|
+
pem.empty? ? nil : pem
|
|
49
|
+
rescue Api::XapiError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
require_relative "base"
|
|
7
|
+
require_relative "disk_image"
|
|
8
|
+
require_relative "lookup"
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module Xapi
|
|
12
|
+
module Services
|
|
13
|
+
# Uploads a disk image into an existing VDI through /import_raw_vdi, on
|
|
14
|
+
# the host that has the VDI's SR attached.
|
|
15
|
+
#
|
|
16
|
+
# qcow2: whether XAPI can import it is asked once per session with an
|
|
17
|
+
# empty probe PUT on a small scratch VDI. XAPI rejects an unknown format
|
|
18
|
+
# before it reads the body; a supported format gets a 200. If qcow2 is
|
|
19
|
+
# not supported (or the probe gives no answer), or the image has
|
|
20
|
+
# compressed clusters or features XAPI's import does not take, the image
|
|
21
|
+
# is converted to VHD on the local machine with the converter (qemu-img).
|
|
22
|
+
class ImageUpload
|
|
23
|
+
include Helpers
|
|
24
|
+
|
|
25
|
+
PROBE_SIZE = 1024 * 1024
|
|
26
|
+
|
|
27
|
+
# @param converter [#call, nil] converter.call(src, dst): qcow2 -> VHD
|
|
28
|
+
def initialize(ctx, converter: nil)
|
|
29
|
+
@ctx = ctx
|
|
30
|
+
@client = ctx.client
|
|
31
|
+
@lookup = Lookup.new(ctx)
|
|
32
|
+
@converter = converter
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param format [String] vhd, qcow2 or raw
|
|
36
|
+
# @return [String] the VDI ref; a new one when the VDI had to be
|
|
37
|
+
# recreated for the VHD fallback
|
|
38
|
+
def upload(vdi, path, format, label:)
|
|
39
|
+
sr = @client.call("VDI.get_SR", vdi)
|
|
40
|
+
host = @lookup.transfer_host_for_sr(sr)
|
|
41
|
+
tmpdir = nil
|
|
42
|
+
if format == "qcow2"
|
|
43
|
+
obstacle = DiskImage.qcow2_import_obstacle(path)
|
|
44
|
+
if obstacle || !qcow2_supported?(sr, host)
|
|
45
|
+
tmpdir = Dir.mktmpdir("vagrant-xapi-convert-")
|
|
46
|
+
path = convert(path, tmpdir, reason: obstacle)
|
|
47
|
+
format = "vhd"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
begin
|
|
51
|
+
send_image(vdi, path, format, host, label)
|
|
52
|
+
rescue Api::HttpError, Api::ConnectionError, Api::XapiError => e
|
|
53
|
+
# The probe said yes, but this host refused qcow2 after all (e.g.
|
|
54
|
+
# another version during a rolling upgrade), or its import could
|
|
55
|
+
# not read this image: convert and retry.
|
|
56
|
+
raise unless format == "qcow2" && qcow2_refused?(e)
|
|
57
|
+
|
|
58
|
+
@ctx.logger&.info("qcow2 upload refused (#{e.message}); converting to VHD")
|
|
59
|
+
# Only an HTTP refusal of the format says something about the host.
|
|
60
|
+
@client.set_feature(feature_key(host), false) if e.is_a?(Api::HttpError)
|
|
61
|
+
tmpdir ||= Dir.mktmpdir("vagrant-xapi-convert-")
|
|
62
|
+
vhd = convert(path, tmpdir, reason: e.is_a?(Api::XapiError) ? :refused : nil, detail: e.message)
|
|
63
|
+
vdi = recreate(vdi)
|
|
64
|
+
begin
|
|
65
|
+
send_image(vdi, vhd, "vhd", host, label)
|
|
66
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
67
|
+
# The caller only knows the old (destroyed) ref; clean up here.
|
|
68
|
+
destroy_vdi(vdi)
|
|
69
|
+
raise
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
vdi
|
|
73
|
+
ensure
|
|
74
|
+
FileUtils.rm_rf(tmpdir) if tmpdir
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Asked once per session and transfer host: pool members may run
|
|
78
|
+
# different versions.
|
|
79
|
+
def qcow2_supported?(sr, host)
|
|
80
|
+
known = @client.features[feature_key(host)]
|
|
81
|
+
return known unless known.nil?
|
|
82
|
+
|
|
83
|
+
scratch = create_vdi(sr, "vagrant-xapi qcow2 probe", PROBE_SIZE, description: "temporary")
|
|
84
|
+
begin
|
|
85
|
+
status = @client.http.probe("/import_raw_vdi", { "vdi" => scratch, "format" => "qcow2" }, host: host)
|
|
86
|
+
ensure
|
|
87
|
+
destroy_vdi(scratch)
|
|
88
|
+
end
|
|
89
|
+
supported = status ? status.between?(200, 299) : false
|
|
90
|
+
@ctx.logger&.info("qcow2 import probe: HTTP #{status.inspect}; supported=#{supported}")
|
|
91
|
+
# Only remember a real answer; a timeout is asked again next time.
|
|
92
|
+
@client.set_feature(feature_key(host), supported) unless status.nil?
|
|
93
|
+
supported
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def feature_key(host)
|
|
99
|
+
:"qcow2_import@#{host || "master"}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# XAPI refuses a format it cannot import with a 4xx before it reads
|
|
103
|
+
# the body, and may close the connection while the body is still
|
|
104
|
+
# being sent (broken pipe, reset). Its qcow2 reader failing on this
|
|
105
|
+
# image is a VDI_IO_ERROR from qcow-stream-tool. Anything else (a
|
|
106
|
+
# timeout, an unreachable host, 401/403 for the session, 5xx) is no
|
|
107
|
+
# refusal and is raised as it is.
|
|
108
|
+
def qcow2_refused?(error)
|
|
109
|
+
case error
|
|
110
|
+
when Api::XapiError
|
|
111
|
+
error.code == "VDI_IO_ERROR" && error.params.join(" ").match?(/qcow/i)
|
|
112
|
+
when Api::HttpError
|
|
113
|
+
error.status.between?(400, 499) && ![401, 403].include?(error.status)
|
|
114
|
+
when Api::TimeoutError
|
|
115
|
+
false
|
|
116
|
+
when Api::ConnectionError
|
|
117
|
+
[Errno::EPIPE, Errno::ECONNRESET].any? { |klass| error.cause.is_a?(klass) }
|
|
118
|
+
else
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def send_image(vdi, path, format, host, label)
|
|
124
|
+
@client.http.upload("/import_raw_vdi", { "vdi" => vdi, "format" => format }, path,
|
|
125
|
+
host: host,
|
|
126
|
+
progress: @ctx.progress_proc(:uploading, file: File.basename(path)),
|
|
127
|
+
task_label: label, cancel: @ctx.cancel)
|
|
128
|
+
@ctx.ui&.clear_progress
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A refused upload may have written part of the image; start the VHD
|
|
132
|
+
# import on a fresh VDI with the same settings.
|
|
133
|
+
def recreate(vdi)
|
|
134
|
+
rec = @client.call("VDI.get_record", vdi)
|
|
135
|
+
destroy_vdi(vdi)
|
|
136
|
+
create_vdi(rec["SR"], rec["name_label"], rec["virtual_size"],
|
|
137
|
+
description: rec["name_description"].to_s, type: rec["type"] || "user",
|
|
138
|
+
other_config: rec["other_config"] || {}, tags: rec["tags"] || [])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param reason [Symbol, nil] why the image is converted: nil (the host
|
|
142
|
+
# cannot import qcow2), :compressed, :features or :refused
|
|
143
|
+
def convert(path, tmpdir, reason: nil, detail: nil)
|
|
144
|
+
unless @converter
|
|
145
|
+
raise Error.new(:qemu_img_missing) unless reason
|
|
146
|
+
|
|
147
|
+
raise Error.new(:"qemu_img_missing_#{reason}", file: File.basename(path), detail: detail.to_s)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
dst = File.join(tmpdir, "#{File.basename(path, ".*")}.vhd")
|
|
151
|
+
@ctx.ui&.info(:converting_image, file: File.basename(path))
|
|
152
|
+
@converter.call(path, dst)
|
|
153
|
+
dst
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The probe may leave XAPI busy with the VDI for a moment.
|
|
157
|
+
def destroy_vdi(vdi)
|
|
158
|
+
10.times do
|
|
159
|
+
@client.call("VDI.destroy", vdi)
|
|
160
|
+
return
|
|
161
|
+
rescue Api::XapiError => e
|
|
162
|
+
return if %w[HANDLE_INVALID UUID_INVALID].include?(e.code)
|
|
163
|
+
raise unless %w[VDI_IN_USE OTHER_OPERATION_IN_PROGRESS].include?(e.code)
|
|
164
|
+
|
|
165
|
+
sleep 1
|
|
166
|
+
end
|
|
167
|
+
@ctx.logger&.warn("could not remove the qcow2 probe VDI")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "template_cache"
|
|
7
|
+
|
|
8
|
+
module VagrantPlugins
|
|
9
|
+
module Xapi
|
|
10
|
+
module Services
|
|
11
|
+
# Overview and cleanup of what the plugin left in a pool: box templates
|
|
12
|
+
# (shared by all users) and orphaned VMs.
|
|
13
|
+
class Inventory
|
|
14
|
+
include Helpers
|
|
15
|
+
|
|
16
|
+
TemplateInfo = Struct.new(:ref, :uuid, :name, :box, :version, :provider, :sr_uuid, :sr_name, :size,
|
|
17
|
+
:clones, :clone_users, keyword_init: true)
|
|
18
|
+
VmInfo = Struct.new(:ref, :uuid, :name, :machine, :project, :user, :client, :power_state,
|
|
19
|
+
keyword_init: true)
|
|
20
|
+
# One object carrying a vagrant-xapi marking.
|
|
21
|
+
# kind: :vm, :snapshot, :template, :vdi, :network, :lock
|
|
22
|
+
Marked = Struct.new(:kind, :uuid, :name, :detail, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
KIND_ORDER = %i[vm snapshot template vdi network lock].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(ctx)
|
|
27
|
+
@ctx = ctx
|
|
28
|
+
@client = ctx.client
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<TemplateInfo>]
|
|
32
|
+
def templates
|
|
33
|
+
vms = marked_vms
|
|
34
|
+
TemplateCache.new(@ctx, digest_cache: nil).all_templates.map do |ref, rec|
|
|
35
|
+
oc = rec["other_config"] || {}
|
|
36
|
+
clones = vms.select { |v| v.dig(:oc, Marking::TEMPLATE_UUID) == rec["uuid"] }
|
|
37
|
+
TemplateInfo.new(
|
|
38
|
+
ref: ref, uuid: rec["uuid"], name: rec["name_label"],
|
|
39
|
+
box: oc[Marking::BOX], version: oc[Marking::BOX_VERSION], provider: oc[Marking::BOX_PROVIDER],
|
|
40
|
+
sr_uuid: oc[Marking::SR],
|
|
41
|
+
sr_name: sr_name(oc[Marking::SR]),
|
|
42
|
+
size: disk_vdis(ref).sum { |vdi| ignore_missing { @client.call("VDI.get_virtual_size", vdi).to_i } || 0 },
|
|
43
|
+
clones: clones.size,
|
|
44
|
+
clone_users: clones.map { |v| v.dig(:oc, Marking::USER) }.compact.uniq.sort
|
|
45
|
+
)
|
|
46
|
+
end.sort_by { |t| [t.box.to_s, version_key(t.version)] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Templates to remove so that per box only the newest `keep` versions
|
|
50
|
+
# remain (per SR).
|
|
51
|
+
def prune_candidates(keep)
|
|
52
|
+
# By SR UUID: in a pool every host's local SR is called "Local storage".
|
|
53
|
+
templates.group_by { |t| [t.box, t.provider, t.sr_uuid] }.flat_map do |_key, list|
|
|
54
|
+
sorted = list.sort_by { |t| version_key(t.version) }.reverse
|
|
55
|
+
sorted.drop(keep.to_i)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_template(name_or_uuid)
|
|
60
|
+
list = templates
|
|
61
|
+
hit = list.select { |t| [t.uuid, t.name, "#{t.box} #{t.version}"].include?(name_or_uuid.to_s) }
|
|
62
|
+
raise Error.new(:template_not_found, name: name_or_uuid.to_s) if hit.empty?
|
|
63
|
+
raise Error.new(:ambiguous_name, kind: "template", name: name_or_uuid.to_s, count: hit.size) if hit.size > 1
|
|
64
|
+
|
|
65
|
+
hit.first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Removing a template is safe while clones exist: the SM layer keeps
|
|
69
|
+
# the shared base VHD.
|
|
70
|
+
def remove_template(info)
|
|
71
|
+
rec = @client.call("VM.get_record", info.ref)
|
|
72
|
+
raise Error.new(:template_not_marked, name: info.name) unless (rec["other_config"] || {}).key?(Marking::BOX_DIGEST)
|
|
73
|
+
|
|
74
|
+
vdis = disk_vdis(info.ref)
|
|
75
|
+
@client.call("VM.destroy", info.ref)
|
|
76
|
+
vdis.each { |vdi| ignore_missing { @client.call("VDI.destroy", vdi) } }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# VMs of this Vagrant installation: same client_id, or (for VMs
|
|
80
|
+
# without one) the same user and local hostname.
|
|
81
|
+
# @return [Array<VmInfo>]
|
|
82
|
+
def my_vms(user: Marking.user, client: Marking.client_host, client_id: nil)
|
|
83
|
+
marked_vms.select { |v| mine?(v[:oc], user, client, client_id) }
|
|
84
|
+
.map do |v|
|
|
85
|
+
VmInfo.new(ref: v[:ref], uuid: v[:uuid], name: v[:name], machine: v.dig(:oc, Marking::MACHINE),
|
|
86
|
+
project: v.dig(:oc, Marking::PROJECT), user: v.dig(:oc, Marking::USER),
|
|
87
|
+
client: v.dig(:oc, Marking::CLIENT), power_state: v[:power_state])
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Everything that carries the plugin's marking, read through XAPI.
|
|
92
|
+
# With filters, an object is listed when it belongs to any of them:
|
|
93
|
+
# projects: VMs, snapshots and internal networks of these project
|
|
94
|
+
# directories
|
|
95
|
+
# vm_uuids: these VMs, their snapshots and the VDIs they own
|
|
96
|
+
# boxes: templates of these box names
|
|
97
|
+
# mine: client_id of this installation: its VMs (or, for VMs
|
|
98
|
+
# without client id, those of this user and hostname),
|
|
99
|
+
# their snapshots and disks, and the internal networks of
|
|
100
|
+
# their projects; shared templates are not "mine"
|
|
101
|
+
# @return [Array<Marked>]
|
|
102
|
+
def marked(projects: [], vm_uuids: [], boxes: [], mine: nil)
|
|
103
|
+
projects = projects.map(&:to_s)
|
|
104
|
+
vm_uuids = vm_uuids.map(&:to_s)
|
|
105
|
+
boxes = boxes.map(&:to_s)
|
|
106
|
+
filtered = !(projects.empty? && vm_uuids.empty? && boxes.empty? && mine.nil?)
|
|
107
|
+
result = []
|
|
108
|
+
records = @client.call("VM.get_all_records")
|
|
109
|
+
|
|
110
|
+
my_vms = []
|
|
111
|
+
my_projects = []
|
|
112
|
+
if mine
|
|
113
|
+
records.each_value do |rec|
|
|
114
|
+
oc = rec["other_config"] || {}
|
|
115
|
+
next if rec["is_a_template"] || !Marking.marked?(oc)
|
|
116
|
+
next unless mine?(oc, Marking.user, Marking.client_host, mine)
|
|
117
|
+
|
|
118
|
+
my_vms << rec["uuid"]
|
|
119
|
+
my_projects << oc[Marking::PROJECT]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
vm_uuids |= my_vms
|
|
123
|
+
network_projects = projects | my_projects
|
|
124
|
+
|
|
125
|
+
records.each_value do |rec|
|
|
126
|
+
oc = rec["other_config"] || {}
|
|
127
|
+
if rec["is_a_snapshot"] && oc.key?(Marking::SNAPSHOT_OF)
|
|
128
|
+
next if filtered && !vm_uuids.include?(oc[Marking::SNAPSHOT_OF]) && !projects.include?(oc[Marking::PROJECT])
|
|
129
|
+
|
|
130
|
+
result << Marked.new(kind: :snapshot, uuid: rec["uuid"], name: rec["name_label"],
|
|
131
|
+
detail: "of VM #{oc[Marking::SNAPSHOT_OF]}")
|
|
132
|
+
elsif rec["is_a_template"] && oc.key?(Marking::BOX_DIGEST)
|
|
133
|
+
next if filtered && !boxes.include?(oc[Marking::BOX])
|
|
134
|
+
|
|
135
|
+
result << Marked.new(kind: :template, uuid: rec["uuid"], name: rec["name_label"],
|
|
136
|
+
detail: "box #{oc[Marking::BOX]} #{oc[Marking::BOX_VERSION]}")
|
|
137
|
+
elsif !rec["is_a_template"] && Marking.marked?(oc)
|
|
138
|
+
next if filtered && !projects.include?(oc[Marking::PROJECT]) && !vm_uuids.include?(rec["uuid"])
|
|
139
|
+
|
|
140
|
+
result << Marked.new(kind: :vm, uuid: rec["uuid"], name: rec["name_label"],
|
|
141
|
+
detail: "#{oc[Marking::MACHINE]} in #{oc[Marking::PROJECT]}")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@client.call("VDI.get_all_records").each_value do |rec|
|
|
146
|
+
oc = rec["other_config"] || {}
|
|
147
|
+
owner = oc[Marking::OWNER_VM]
|
|
148
|
+
if owner
|
|
149
|
+
next if filtered && !vm_uuids.include?(owner)
|
|
150
|
+
|
|
151
|
+
result << Marked.new(kind: :vdi, uuid: rec["uuid"], name: rec["name_label"], detail: "of VM #{owner}")
|
|
152
|
+
elsif oc.key?(Marking::OF_TEMPLATE) && !filtered
|
|
153
|
+
result << Marked.new(kind: :vdi, uuid: rec["uuid"], name: rec["name_label"],
|
|
154
|
+
detail: "of template #{oc[Marking::OF_TEMPLATE]}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
@client.call("network.get_all_records").each_value do |rec|
|
|
159
|
+
oc = rec["other_config"] || {}
|
|
160
|
+
next unless oc.key?(Marking::NETWORK)
|
|
161
|
+
next if filtered && !network_projects.include?(oc[Marking::PROJECT])
|
|
162
|
+
|
|
163
|
+
result << Marked.new(kind: :network, uuid: rec["uuid"], name: rec["name_label"],
|
|
164
|
+
detail: "project #{oc[Marking::PROJECT]}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
unless filtered
|
|
168
|
+
pool = @client.call("pool.get_all").first
|
|
169
|
+
@client.call("pool.get_other_config", pool).each do |key, value|
|
|
170
|
+
next unless key.start_with?("#{Marking::PREFIX}lock.")
|
|
171
|
+
|
|
172
|
+
owner = begin
|
|
173
|
+
JSON.parse(value)["owner"]
|
|
174
|
+
rescue JSON::ParserError
|
|
175
|
+
value
|
|
176
|
+
end
|
|
177
|
+
result << Marked.new(kind: :lock, uuid: "", name: key.delete_prefix("#{Marking::PREFIX}lock."),
|
|
178
|
+
detail: "held by #{owner}")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
result.sort_by { |m| [KIND_ORDER.index(m.kind), m.name.to_s] }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def mine?(oc, user, client, client_id)
|
|
188
|
+
return oc[Marking::CLIENT_ID] == client_id.to_s if client_id && oc.key?(Marking::CLIENT_ID)
|
|
189
|
+
|
|
190
|
+
oc[Marking::USER] == user && oc[Marking::CLIENT] == client
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def marked_vms
|
|
194
|
+
@marked_vms ||= begin
|
|
195
|
+
recs = begin
|
|
196
|
+
@client.call("VM.get_all_records_where", 'field "is_a_template"="false"')
|
|
197
|
+
rescue Api::XapiError
|
|
198
|
+
@client.call("VM.get_all_records")
|
|
199
|
+
end
|
|
200
|
+
recs.filter_map do |ref, rec|
|
|
201
|
+
next if rec["is_a_template"] || rec["is_a_snapshot"] || rec["is_control_domain"]
|
|
202
|
+
|
|
203
|
+
oc = rec["other_config"] || {}
|
|
204
|
+
next unless Marking.marked?(oc)
|
|
205
|
+
|
|
206
|
+
{ ref: ref, uuid: rec["uuid"], name: rec["name_label"], oc: oc, power_state: rec["power_state"] }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def sr_name(uuid)
|
|
212
|
+
return nil if uuid.to_s.empty?
|
|
213
|
+
|
|
214
|
+
ignore_missing { @client.call("SR.get_name_label", @client.call("SR.get_by_uuid", uuid)) } || uuid
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def version_key(version)
|
|
218
|
+
Gem::Version.new(version.to_s)
|
|
219
|
+
rescue ArgumentError
|
|
220
|
+
Gem::Version.new("0")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Services
|
|
10
|
+
# Finds the guest's IP from the configured sources, in order:
|
|
11
|
+
# :guest_metrics VM_guest_metrics.networks "<dev>/ipv4/0", "<dev>/ip"
|
|
12
|
+
# :static ip: of the first VIF (set via cloud-init network-config)
|
|
13
|
+
# :himn network.assigned_ips of the HIMN VIF
|
|
14
|
+
class IpDetector
|
|
15
|
+
include Helpers
|
|
16
|
+
|
|
17
|
+
SOURCES = %i[guest_metrics static himn].freeze
|
|
18
|
+
|
|
19
|
+
# @param static_ip [String, nil] ip: of VIF device 0
|
|
20
|
+
def initialize(ctx, sources:, static_ip: nil, himn_enabled: false)
|
|
21
|
+
@ctx = ctx
|
|
22
|
+
@client = ctx.client
|
|
23
|
+
@sources = Array(sources).map(&:to_sym)
|
|
24
|
+
@static_ip = static_ip
|
|
25
|
+
@himn_enabled = himn_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# One look, no waiting.
|
|
29
|
+
# @return [Array(String, Symbol), nil] [ip, source]
|
|
30
|
+
def detect(vm)
|
|
31
|
+
@sources.each do |source|
|
|
32
|
+
ip = case source
|
|
33
|
+
when :guest_metrics then from_guest_metrics(vm)
|
|
34
|
+
when :static then @static_ip.to_s.empty? ? nil : @static_ip.to_s
|
|
35
|
+
when :himn then @himn_enabled ? from_himn(vm) : nil
|
|
36
|
+
end
|
|
37
|
+
return [ip, source] if ip
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Waits until an IP shows up or the timeout passes.
|
|
43
|
+
def wait(vm, timeout:, interval: 2)
|
|
44
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
45
|
+
loop do
|
|
46
|
+
found = detect(vm)
|
|
47
|
+
return found if found
|
|
48
|
+
raise Api::TaskCancelled, "waiting for IP" if @ctx.cancelled?
|
|
49
|
+
|
|
50
|
+
power = @client.call("VM.get_power_state", vm)
|
|
51
|
+
raise Error.new(:vm_stopped_while_waiting, state: power) unless power == "Running"
|
|
52
|
+
|
|
53
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
54
|
+
raise Error.new(:ip_timeout, timeout: timeout, sources: @sources.join(", "))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sleep interval
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def from_guest_metrics(vm, device: 0)
|
|
62
|
+
metrics = @client.call("VM.get_guest_metrics", vm)
|
|
63
|
+
return nil if null_ref?(metrics)
|
|
64
|
+
|
|
65
|
+
nets = ignore_missing { @client.call("VM_guest_metrics.get_networks", metrics) } || {}
|
|
66
|
+
candidate = nets["#{device}/ipv4/0"] || nets["#{device}/ip"]
|
|
67
|
+
usable?(candidate) ? candidate : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def from_himn(vm)
|
|
71
|
+
himn_vif = @client.call("VM.get_VIFs", vm).find do |vif|
|
|
72
|
+
net = @client.call("VIF.get_network", vif)
|
|
73
|
+
@client.call("network.get_other_config", net)["is_host_internal_management_network"].to_s == "true"
|
|
74
|
+
end
|
|
75
|
+
return nil unless himn_vif
|
|
76
|
+
|
|
77
|
+
net = @client.call("VIF.get_network", himn_vif)
|
|
78
|
+
ip = (@client.call("network.get_assigned_ips", net) || {})[himn_vif]
|
|
79
|
+
# HIMN leases are link-local (169.254.0.0/16) by design.
|
|
80
|
+
usable?(ip, allow_link_local: true) ? ip : nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Only a bare IPv4 or IPv6 literal is accepted. The value is written by
|
|
84
|
+
# the guest agent, so it is untrusted: it ends up in an SSH
|
|
85
|
+
# ProxyCommand (%h) and in NFS exports.
|
|
86
|
+
def self.valid_ip?(ip, allow_link_local: false)
|
|
87
|
+
s = ip.to_s
|
|
88
|
+
return false unless s.match?(/\A[0-9A-Fa-f:.]{2,45}\z/)
|
|
89
|
+
|
|
90
|
+
addr = IPAddr.new(s)
|
|
91
|
+
return false if addr.loopback? || addr.to_s == "0.0.0.0" || addr.to_s == "::"
|
|
92
|
+
return false if !allow_link_local && addr.link_local?
|
|
93
|
+
|
|
94
|
+
true
|
|
95
|
+
rescue IPAddr::Error
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def usable?(ip, allow_link_local: false)
|
|
102
|
+
self.class.valid_ip?(ip, allow_link_local: allow_link_local)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|