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,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "log4r"
|
|
4
|
+
require "vagrant"
|
|
5
|
+
|
|
6
|
+
require_relative "action"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "extensions"
|
|
9
|
+
require_relative "glue"
|
|
10
|
+
require_relative "services/power"
|
|
11
|
+
require_relative "services/lookup"
|
|
12
|
+
|
|
13
|
+
module VagrantPlugins
|
|
14
|
+
module Xapi
|
|
15
|
+
class Provider < Vagrant.plugin("2", :provider)
|
|
16
|
+
SAFE_HOST = /\A[A-Za-z0-9.:\-\[\]]+\z/.freeze
|
|
17
|
+
SAFE_USER = /\A[A-Za-z0-9._\-]+\z/.freeze
|
|
18
|
+
|
|
19
|
+
# Default synced folder types, in this order, when the Vagrantfile does
|
|
20
|
+
# not choose (config.vm.allowed_synced_folder_types).
|
|
21
|
+
SYNCED_FOLDER_TYPES = %i[rsync nfs smb].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(machine)
|
|
24
|
+
super
|
|
25
|
+
@machine = machine
|
|
26
|
+
@logger = Log4r::Logger.new("vagrantplugins::xapi_provider")
|
|
27
|
+
prefer_rsync
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def action(name)
|
|
31
|
+
method = "action_#{name}"
|
|
32
|
+
return Action.public_send(method) if Action.respond_to?(method)
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The machine id is the VM UUID. A VM that cannot be found is
|
|
38
|
+
# :not_created, which makes Vagrant clear the data_dir. An id that is
|
|
39
|
+
# not a UUID goes to the extensions first (Extensions.on_foreign_id).
|
|
40
|
+
def state
|
|
41
|
+
return not_created if @machine.id.nil?
|
|
42
|
+
|
|
43
|
+
ctx = Glue.context(@machine)
|
|
44
|
+
unless Services::Lookup.uuid?(@machine.id)
|
|
45
|
+
Errors.wrap { Extensions.resolve_foreign_id(@machine, ctx) }
|
|
46
|
+
return not_created if @machine.id.nil?
|
|
47
|
+
end
|
|
48
|
+
info = Errors.wrap do
|
|
49
|
+
ref = Services::Lookup.new(ctx).vm_by_uuid(@machine.id)
|
|
50
|
+
ref ? Services::Power.new(ctx).state(ref) : nil
|
|
51
|
+
end
|
|
52
|
+
return not_created unless info
|
|
53
|
+
|
|
54
|
+
id = info[:state]
|
|
55
|
+
short = I18n.t("vagrant_xapi.states.short_#{id}", default: id.to_s)
|
|
56
|
+
long = I18n.t("vagrant_xapi.states.long_#{id}", default: short)
|
|
57
|
+
unless info[:operations].empty?
|
|
58
|
+
long = "#{long}\n\n#{I18n.t("vagrant_xapi.states.operations", operations: info[:operations].join(", "))}"
|
|
59
|
+
end
|
|
60
|
+
Vagrant::MachineState.new(id, short, long)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# nil until the guest has an IP.
|
|
64
|
+
def ssh_info
|
|
65
|
+
return nil if @machine.id.nil?
|
|
66
|
+
|
|
67
|
+
ctx = Glue.context(@machine)
|
|
68
|
+
Errors.wrap do
|
|
69
|
+
ref = Services::Lookup.new(ctx).vm_by_uuid(@machine.id)
|
|
70
|
+
return nil unless ref
|
|
71
|
+
return nil unless ctx.client.call("VM.get_power_state", ref) == "Running"
|
|
72
|
+
|
|
73
|
+
found = Glue.ip_detector(@machine, ctx).detect(ref)
|
|
74
|
+
return nil unless found
|
|
75
|
+
|
|
76
|
+
info = { host: found.first, port: @machine.config.ssh.guest_port || 22 }
|
|
77
|
+
proxy = proxy_command(ctx, ref)
|
|
78
|
+
info[:proxy_command] = proxy if proxy
|
|
79
|
+
info
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Nothing to install locally: the plugin talks to XAPI over HTTPS.
|
|
84
|
+
def self.usable?(_raise_error = false)
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.installed?
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def machine_id_changed; end
|
|
93
|
+
|
|
94
|
+
def to_s
|
|
95
|
+
id = @machine.id ? @machine.id.to_s : "new VM"
|
|
96
|
+
"XAPI (#{id})"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# The VM runs on a remote hypervisor. Without a choice in the
|
|
102
|
+
# Vagrantfile, Vagrant takes the usable type with the highest priority,
|
|
103
|
+
# which on macOS and Windows is SMB: it asks for credentials on every
|
|
104
|
+
# `up` (failing without a terminal) and needs the guest to reach this
|
|
105
|
+
# machine. rsync needs neither, so it comes first. Only the default
|
|
106
|
+
# changes: a folder with an explicit type keeps it.
|
|
107
|
+
def prefer_rsync
|
|
108
|
+
vm = @machine.config&.vm
|
|
109
|
+
vm.allowed_synced_folder_types ||= SYNCED_FOLDER_TYPES.dup if vm
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def not_created
|
|
113
|
+
Vagrant::MachineState.new(:not_created,
|
|
114
|
+
I18n.t("vagrant_xapi.states.short_not_created"),
|
|
115
|
+
I18n.t("vagrant_xapi.states.long_not_created"))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ProxyCommand through dom0 of the host the VM runs on, with normal
|
|
119
|
+
# host key checking (the old plugin turned it off).
|
|
120
|
+
def proxy_command(ctx, ref)
|
|
121
|
+
cfg = @machine.provider_config
|
|
122
|
+
return nil unless cfg.ssh_via_host
|
|
123
|
+
|
|
124
|
+
host = ctx.client.call("VM.get_resident_on", ref)
|
|
125
|
+
address = ctx.client.call("host.get_address", host)
|
|
126
|
+
user = cfg.host_ssh_user.to_s
|
|
127
|
+
unless SAFE_HOST.match?(address.to_s) && SAFE_USER.match?(user)
|
|
128
|
+
raise Errors::ServiceFailed.new(:unsafe_proxy, address: address.to_s, user: user)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
"ssh -W %h:%p -l #{user} -o BatchMode=yes #{address}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Services
|
|
8
|
+
# Hands an existing VM to a Vagrant machine: the VM gets the machine's
|
|
9
|
+
# marking, and its own disks are marked as the VM's, so that
|
|
10
|
+
# `vagrant destroy` removes them.
|
|
11
|
+
class Adoption
|
|
12
|
+
def initialize(ctx)
|
|
13
|
+
@client = ctx.client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Replaces any older marking of the VM with this one and tags it. Its
|
|
17
|
+
# vTPM no longer counts as made by the plugin, so turning `vtpm` off
|
|
18
|
+
# does not remove it.
|
|
19
|
+
def mark_vm(vm, marking)
|
|
20
|
+
begin
|
|
21
|
+
@client.call("VM.remove_from_other_config", vm, Marking::VTPM)
|
|
22
|
+
rescue Api::XapiError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
marking.each do |key, value|
|
|
26
|
+
begin
|
|
27
|
+
@client.call("VM.remove_from_other_config", vm, key)
|
|
28
|
+
rescue Api::XapiError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
@client.call("VM.add_to_other_config", vm, key, value)
|
|
32
|
+
end
|
|
33
|
+
tags = @client.call("VM.get_record", vm)["tags"] || []
|
|
34
|
+
@client.call("VM.add_tags", vm, Marking::TAG) unless tags.include?(Marking::TAG)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Marks the VM's exclusive disks (Disk VBDs whose VDI is not sharable
|
|
38
|
+
# and attached to no other VM) as its own. A disk that is not
|
|
39
|
+
# exclusive (any more) loses the VM's marking, so `vagrant destroy`
|
|
40
|
+
# never deletes it.
|
|
41
|
+
def mark_disks(vm)
|
|
42
|
+
vm_uuid = @client.call("VM.get_uuid", vm)
|
|
43
|
+
@client.call("VM.get_VBDs", vm).each do |vbd|
|
|
44
|
+
rec = @client.call("VBD.get_record", vbd)
|
|
45
|
+
vdi = rec["VDI"].to_s
|
|
46
|
+
next if rec["type"] != "Disk" || vdi.empty? || vdi == "OpaqueRef:NULL"
|
|
47
|
+
|
|
48
|
+
if exclusive_disk?(vdi, vm)
|
|
49
|
+
remove_owner(vdi)
|
|
50
|
+
@client.call("VDI.add_to_other_config", vdi, Marking::OWNER_VM, vm_uuid)
|
|
51
|
+
elsif @client.call("VDI.get_other_config", vdi)[Marking::OWNER_VM] == vm_uuid
|
|
52
|
+
remove_owner(vdi)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def exclusive_disk?(vdi, vm)
|
|
60
|
+
return false if @client.call("VDI.get_sharable", vdi)
|
|
61
|
+
|
|
62
|
+
@client.call("VDI.get_VBDs", vdi).all? { |vbd| @client.call("VBD.get_VM", vbd) == vm }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def remove_owner(vdi)
|
|
66
|
+
@client.call("VDI.remove_from_other_config", vdi, Marking::OWNER_VM)
|
|
67
|
+
rescue Api::XapiError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "etc"
|
|
5
|
+
|
|
6
|
+
require_relative "../api/errors"
|
|
7
|
+
require_relative "../version"
|
|
8
|
+
|
|
9
|
+
module VagrantPlugins
|
|
10
|
+
module Xapi
|
|
11
|
+
module Services
|
|
12
|
+
# A service-level error. `key` is a message key under
|
|
13
|
+
# vagrant_xapi.errors in locales/en.yml; the integration layer turns it
|
|
14
|
+
# into a Vagrant error with readable text.
|
|
15
|
+
class Error < StandardError
|
|
16
|
+
attr_reader :key, :params
|
|
17
|
+
|
|
18
|
+
def initialize(key, **params)
|
|
19
|
+
@key = key.to_sym
|
|
20
|
+
@params = params
|
|
21
|
+
super("#{key} #{params.inspect}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Everything a service needs to talk to the pool and the user.
|
|
26
|
+
#
|
|
27
|
+
# ui responds to:
|
|
28
|
+
# info(key, **params), detail(key, **params), warn(key, **params),
|
|
29
|
+
# progress(key, done, total, **params), clear_progress
|
|
30
|
+
# cancel: a callable returning true once the user pressed Ctrl-C.
|
|
31
|
+
Context = Struct.new(:client, :ui, :cancel, :logger, keyword_init: true) do
|
|
32
|
+
def cancelled?
|
|
33
|
+
cancel ? cancel.call : false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def progress_proc(key, **params)
|
|
37
|
+
return nil unless ui
|
|
38
|
+
|
|
39
|
+
->(done, total = 1.0) { ui.progress(key, done, total, **params) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# For XAPI task progress (0.0..1.0).
|
|
43
|
+
def task_progress(key, **params)
|
|
44
|
+
return nil unless ui
|
|
45
|
+
|
|
46
|
+
->(fraction) { ui.progress(key, (fraction * 100).round, 100, **params) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_task(method, *args, label: nil, progress_key: nil, timeout: nil, **params)
|
|
50
|
+
result = client.run_task(method, *args,
|
|
51
|
+
label: label || method,
|
|
52
|
+
timeout: timeout,
|
|
53
|
+
progress: progress_key ? task_progress(progress_key, **params) : nil,
|
|
54
|
+
cancel: cancel)
|
|
55
|
+
ui&.clear_progress if progress_key
|
|
56
|
+
result
|
|
57
|
+
rescue StandardError
|
|
58
|
+
ui&.clear_progress if progress_key
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Small XAPI helpers shared by the services. Expects @client.
|
|
64
|
+
module Helpers
|
|
65
|
+
NULL_REF = "OpaqueRef:NULL"
|
|
66
|
+
|
|
67
|
+
# Also usable without including Helpers (commands).
|
|
68
|
+
def self.null_ref?(ref)
|
|
69
|
+
ref.nil? || ref.to_s.empty? || ref.to_s == NULL_REF
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def null_ref?(ref)
|
|
73
|
+
Helpers.null_ref?(ref)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Sets one other_config key (XAPI has no "set key", only add/remove).
|
|
77
|
+
def set_oc(klass, ref, key, value)
|
|
78
|
+
remove_oc(klass, ref, key)
|
|
79
|
+
@client.call("#{klass}.add_to_other_config", ref, key, value.to_s)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def remove_oc(klass, ref, key)
|
|
83
|
+
@client.call("#{klass}.remove_from_other_config", ref, key)
|
|
84
|
+
rescue Api::XapiError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# [[vbd_ref, vbd_record], ...] for a VM
|
|
89
|
+
def vbds_of(vm)
|
|
90
|
+
@client.call("VM.get_VBDs", vm).map { |vbd| [vbd, @client.call("VBD.get_record", vbd)] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# VDI refs of the VM's Disk VBDs
|
|
94
|
+
def disk_vdis(vm)
|
|
95
|
+
vbds_of(vm).filter_map do |_vbd, rec|
|
|
96
|
+
next if rec["type"] != "Disk" || null_ref?(rec["VDI"])
|
|
97
|
+
|
|
98
|
+
rec["VDI"]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ignore_missing
|
|
103
|
+
yield
|
|
104
|
+
rescue Api::XapiError => e
|
|
105
|
+
raise unless %w[HANDLE_INVALID UUID_INVALID].include?(e.code)
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A new, empty VDI; every VDI the plugin creates comes from here.
|
|
111
|
+
def create_vdi(sr, name, size, description:, read_only: false, type: "user", other_config: {}, tags: [])
|
|
112
|
+
@client.call("VDI.create", {
|
|
113
|
+
"name_label" => name, "name_description" => description, "SR" => sr,
|
|
114
|
+
"virtual_size" => size.to_i, "type" => type, "sharable" => false, "read_only" => read_only,
|
|
115
|
+
"other_config" => other_config, "xenstore_data" => {}, "sm_config" => {}, "tags" => tags
|
|
116
|
+
})
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Device slots in which a guest sees a CD drive. Xen emulates four IDE
|
|
120
|
+
# devices for an HVM guest, in slots 0-3. XAPI accepts a CD drive in a
|
|
121
|
+
# later slot, but the guest never sees it: its PV drivers leave CD
|
|
122
|
+
# drives to the emulation.
|
|
123
|
+
CD_SLOTS = %w[0 1 2 3].freeze
|
|
124
|
+
|
|
125
|
+
# A VBD on the VM's first free device; a CD only in a CD slot.
|
|
126
|
+
def create_vbd(vm, vdi, mode:, type:, other_config: {})
|
|
127
|
+
allowed = @client.call("VM.get_allowed_VBD_devices", vm).map(&:to_s)
|
|
128
|
+
if type == "CD"
|
|
129
|
+
allowed &= CD_SLOTS
|
|
130
|
+
raise Error.new(:no_free_cd_slot, in_use: cd_slots_in_use(vm)) if allowed.empty?
|
|
131
|
+
end
|
|
132
|
+
raise Error.new(:no_free_device) if allowed.empty?
|
|
133
|
+
|
|
134
|
+
@client.call("VBD.create", {
|
|
135
|
+
"VM" => vm, "VDI" => vdi, "userdevice" => allowed.first, "bootable" => false,
|
|
136
|
+
"mode" => mode, "type" => type, "unpluggable" => true, "empty" => false,
|
|
137
|
+
"other_config" => other_config, "qos_algorithm_type" => "", "qos_algorithm_params" => {}
|
|
138
|
+
})
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# "0: Disk, 3: CD (empty)": what takes the CD slots, for messages.
|
|
142
|
+
def cd_slots_in_use(vm)
|
|
143
|
+
vbds_of(vm).map { |_vbd, rec| [rec["userdevice"].to_s, rec] }
|
|
144
|
+
.select { |slot, _rec| CD_SLOTS.include?(slot) }
|
|
145
|
+
.sort_by { |slot, _rec| slot.to_i }
|
|
146
|
+
.map { |slot, rec| "#{slot}: #{rec["type"]}#{" (empty)" if null_ref?(rec["VDI"])}" }
|
|
147
|
+
.join(", ")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A string literal for the expressions of get_all_records_where. Block
|
|
151
|
+
# form: in a replacement string "\\" would mean one backslash.
|
|
152
|
+
def self.where_value(value)
|
|
153
|
+
value.to_s.gsub("\\") { "\\\\" }.gsub('"') { '\\"' }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Markings put on every object the plugin creates. Cleanup only ever
|
|
158
|
+
# touches objects that carry them.
|
|
159
|
+
module Marking
|
|
160
|
+
PREFIX = "vagrant-xapi."
|
|
161
|
+
TAG = "vagrant"
|
|
162
|
+
|
|
163
|
+
# VM keys
|
|
164
|
+
MACHINE = "#{PREFIX}machine"
|
|
165
|
+
PROJECT = "#{PREFIX}project"
|
|
166
|
+
USER = "#{PREFIX}user"
|
|
167
|
+
CLIENT = "#{PREFIX}client"
|
|
168
|
+
CLIENT_ID = "#{PREFIX}client_id"
|
|
169
|
+
VERSION_KEY = "#{PREFIX}version"
|
|
170
|
+
TEMPLATE_UUID = "#{PREFIX}template"
|
|
171
|
+
# The plugin created the VM's vTPM (provider option `vtpm`), so it may
|
|
172
|
+
# remove it again when the option is turned off.
|
|
173
|
+
VTPM = "#{PREFIX}vtpm"
|
|
174
|
+
|
|
175
|
+
# Template keys
|
|
176
|
+
BOX = "#{PREFIX}box"
|
|
177
|
+
BOX_VERSION = "#{PREFIX}box_version"
|
|
178
|
+
BOX_PROVIDER = "#{PREFIX}box_provider"
|
|
179
|
+
BOX_DIGEST = "#{PREFIX}box_digest"
|
|
180
|
+
SR = "#{PREFIX}sr"
|
|
181
|
+
TEMPLATE_KEYS = [BOX, BOX_VERSION, BOX_PROVIDER, BOX_DIGEST, SR].freeze
|
|
182
|
+
|
|
183
|
+
# VDI keys
|
|
184
|
+
OWNER_VM = "#{PREFIX}vm"
|
|
185
|
+
DISK_NAME = "#{PREFIX}disk"
|
|
186
|
+
CLOUD_INIT = "#{PREFIX}cloud_init"
|
|
187
|
+
CONTENT_SHA256 = "#{PREFIX}sha256"
|
|
188
|
+
OF_TEMPLATE = "#{PREFIX}of_template"
|
|
189
|
+
|
|
190
|
+
# VBD key: CD drive for an ISO from an ISO SR (provider option `isos`)
|
|
191
|
+
ISO = "#{PREFIX}iso"
|
|
192
|
+
|
|
193
|
+
# VDI and VBD key: throwaway snapshot disk of `vagrant xapi diff`
|
|
194
|
+
# (value: the id of the run)
|
|
195
|
+
DIFF = "#{PREFIX}diff"
|
|
196
|
+
|
|
197
|
+
# Snapshot key
|
|
198
|
+
SNAPSHOT_OF = "#{PREFIX}snapshot_of"
|
|
199
|
+
|
|
200
|
+
# Network keys
|
|
201
|
+
NETWORK = "#{PREFIX}network"
|
|
202
|
+
|
|
203
|
+
module_function
|
|
204
|
+
|
|
205
|
+
def user
|
|
206
|
+
Etc.getlogin || ENV["USER"] || ENV["USERNAME"] || "unknown"
|
|
207
|
+
rescue StandardError
|
|
208
|
+
ENV["USER"] || "unknown"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def client_host
|
|
212
|
+
Socket.gethostname
|
|
213
|
+
rescue StandardError
|
|
214
|
+
"unknown"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @param machine_name [String] Vagrant machine name
|
|
218
|
+
# @param project [String] path of the Vagrantfile directory
|
|
219
|
+
def vm_marking(machine_name:, project:, client_id: nil)
|
|
220
|
+
marking = {
|
|
221
|
+
MACHINE => machine_name.to_s,
|
|
222
|
+
PROJECT => project.to_s,
|
|
223
|
+
USER => user,
|
|
224
|
+
CLIENT => client_host,
|
|
225
|
+
VERSION_KEY => Xapi::VERSION
|
|
226
|
+
}
|
|
227
|
+
marking[CLIENT_ID] = client_id.to_s if client_id
|
|
228
|
+
marking
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def marked?(other_config)
|
|
232
|
+
oc = other_config || {}
|
|
233
|
+
oc.key?(MACHINE) && oc.key?(PROJECT)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def ours?(other_config, key, value)
|
|
237
|
+
(other_config || {})[key].to_s == value.to_s && !value.to_s.empty?
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
|
|
9
|
+
require_relative "base"
|
|
10
|
+
require_relative "disk_image"
|
|
11
|
+
|
|
12
|
+
module VagrantPlugins
|
|
13
|
+
module Xapi
|
|
14
|
+
module Services
|
|
15
|
+
# Describes what to import for a box: an XVA, an XVA URL, or one or more
|
|
16
|
+
# disk images. Understands three box providers: xapi, xenserver (boxes of
|
|
17
|
+
# the old vagrant-xenserver plugin) and libvirt.
|
|
18
|
+
class BoxSource
|
|
19
|
+
SUPPORTED_PROVIDERS = %w[xapi xenserver libvirt].freeze
|
|
20
|
+
DISK_FORMATS = %w[vhd qcow2 raw img].freeze
|
|
21
|
+
|
|
22
|
+
Disk = Struct.new(:path, :format, :virtual_size, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
attr_reader :name, :version, :provider, :kind, :xva_path, :xva_url, :disks, :directory
|
|
25
|
+
|
|
26
|
+
# @param directory [Pathname] unpacked box directory
|
|
27
|
+
# @param xva_url [String, nil] from provider config (xapi or xenserver)
|
|
28
|
+
def initialize(name:, version:, provider:, directory:, xva_url: nil)
|
|
29
|
+
@name = name.to_s
|
|
30
|
+
@version = version.to_s
|
|
31
|
+
@provider = provider.to_s
|
|
32
|
+
@directory = Pathname.new(directory.to_s)
|
|
33
|
+
@xva_url_config = xva_url
|
|
34
|
+
@disks = []
|
|
35
|
+
raise Error.new(:box_provider_unsupported, provider: @provider) unless SUPPORTED_PROVIDERS.include?(@provider)
|
|
36
|
+
|
|
37
|
+
detect
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def metadata
|
|
41
|
+
@metadata ||= begin
|
|
42
|
+
path = @directory.join("metadata.json")
|
|
43
|
+
path.file? ? JSON.parse(path.read) : {}
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
raise Error.new(:box_metadata_invalid, path: path.to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def architecture
|
|
50
|
+
metadata["architecture"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Files that make up the box content, in import order.
|
|
54
|
+
def content_files
|
|
55
|
+
case @kind
|
|
56
|
+
when :xva then [@xva_path]
|
|
57
|
+
when :disks then @disks.map(&:path)
|
|
58
|
+
else []
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# SHA-256 identifying the box content (cached on disk).
|
|
63
|
+
def digest(cache)
|
|
64
|
+
return Digest::SHA256.hexdigest("xva_url:#{@xva_url}") if @kind == :xva_url
|
|
65
|
+
|
|
66
|
+
files = content_files
|
|
67
|
+
return cache.digest(files.first) if files.size == 1
|
|
68
|
+
|
|
69
|
+
Digest::SHA256.hexdigest(files.map { |f| cache.digest(f) }.join(":"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def description
|
|
73
|
+
case @kind
|
|
74
|
+
when :xva then "XVA #{File.basename(@xva_path)}"
|
|
75
|
+
when :xva_url then "XVA from #{@xva_url}"
|
|
76
|
+
else @disks.map { |d| "#{File.basename(d.path)} (#{d.format})" }.join(", ")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def detect
|
|
83
|
+
case @provider
|
|
84
|
+
when "xapi" then detect_xapi
|
|
85
|
+
when "xenserver" then detect_xenserver
|
|
86
|
+
when "libvirt" then detect_libvirt
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def detect_xapi
|
|
91
|
+
format = metadata["format"].to_s
|
|
92
|
+
if format.empty?
|
|
93
|
+
# Tolerate boxes without format: look for a box.* file.
|
|
94
|
+
found = %w[xva vhd qcow2 raw img].find { |f| @directory.join("box.#{f}").file? }
|
|
95
|
+
return use_xva_url_or_fail if found.nil?
|
|
96
|
+
|
|
97
|
+
format = found
|
|
98
|
+
end
|
|
99
|
+
case format
|
|
100
|
+
when "xva"
|
|
101
|
+
use_xva(@directory.join("box.xva"))
|
|
102
|
+
when "vhd", "qcow2", "raw", "img"
|
|
103
|
+
use_disk(@directory.join("box.#{format}"))
|
|
104
|
+
else
|
|
105
|
+
raise Error.new(:box_format_unsupported, image_format: format)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def detect_xenserver
|
|
110
|
+
if @directory.join("box.xva").file?
|
|
111
|
+
use_xva(@directory.join("box.xva"))
|
|
112
|
+
elsif @directory.join("box.vhd").file?
|
|
113
|
+
use_disk(@directory.join("box.vhd"))
|
|
114
|
+
else
|
|
115
|
+
use_xva_url_or_fail
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def detect_libvirt
|
|
120
|
+
if metadata["disks"].is_a?(Array) && !metadata["disks"].empty?
|
|
121
|
+
metadata["disks"].each_with_index do |d, i|
|
|
122
|
+
path = d.is_a?(Hash) && d["path"] ? d["path"] : (i.zero? ? "box.img" : "box_#{i}.img")
|
|
123
|
+
use_disk(@directory.join(path.to_s))
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
use_disk(@directory.join("box.img"))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def use_xva(path)
|
|
131
|
+
@kind = :xva
|
|
132
|
+
@xva_path = inside_box!(path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def use_xva_url_or_fail
|
|
136
|
+
url = @xva_url_config.to_s
|
|
137
|
+
raise Error.new(:box_empty, directory: @directory.to_s) if url.empty?
|
|
138
|
+
|
|
139
|
+
@kind = :xva_url
|
|
140
|
+
@xva_url = url
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def use_disk(path)
|
|
144
|
+
real = inside_box!(path)
|
|
145
|
+
info = DiskImage.inspect_file(real)
|
|
146
|
+
@kind = :disks
|
|
147
|
+
@disks << Disk.new(path: real, format: info.format, virtual_size: info.virtual_size)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Box content is not trusted: every file it names must be a regular
|
|
151
|
+
# file inside the box directory, also after following symlinks, so a
|
|
152
|
+
# box cannot make the plugin upload files from the local machine.
|
|
153
|
+
# @return [String] the resolved path
|
|
154
|
+
def inside_box!(path)
|
|
155
|
+
raise Error.new(:box_file_missing, path: path.to_s) unless File.file?(path.to_s)
|
|
156
|
+
|
|
157
|
+
real = File.realpath(path.to_s)
|
|
158
|
+
root = File.realpath(@directory.to_s)
|
|
159
|
+
unless real.start_with?(root + File::SEPARATOR) && File.file?(real)
|
|
160
|
+
raise Error.new(:box_path_outside, path: path.to_s, directory: @directory.to_s)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
real
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# SHA-256 digests of box files, cached on path, size and mtime in
|
|
168
|
+
# ~/.vagrant.d/data/vagrant-xapi/digests.json.
|
|
169
|
+
class DigestCache
|
|
170
|
+
# One mutex for all instances: parallel machines each build their own
|
|
171
|
+
# DigestCache for the same file.
|
|
172
|
+
MUTEX = Mutex.new
|
|
173
|
+
|
|
174
|
+
def initialize(path)
|
|
175
|
+
@path = Pathname.new(path.to_s)
|
|
176
|
+
@mutex = MUTEX
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def digest(file)
|
|
180
|
+
stat = File.stat(file)
|
|
181
|
+
key = File.expand_path(file)
|
|
182
|
+
@mutex.synchronize do
|
|
183
|
+
entry = load[key]
|
|
184
|
+
if entry && entry["size"] == stat.size && entry["mtime"] == stat.mtime.to_i
|
|
185
|
+
return entry["sha256"]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
sha = Digest::SHA256.file(file).hexdigest
|
|
190
|
+
@mutex.synchronize do
|
|
191
|
+
data = load
|
|
192
|
+
data[key] = { "size" => stat.size, "mtime" => stat.mtime.to_i, "sha256" => sha }
|
|
193
|
+
save(data)
|
|
194
|
+
end
|
|
195
|
+
sha
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def load
|
|
201
|
+
return {} unless @path.file?
|
|
202
|
+
|
|
203
|
+
JSON.parse(@path.read)
|
|
204
|
+
rescue JSON::ParserError
|
|
205
|
+
{}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def save(data)
|
|
209
|
+
FileUtils.mkdir_p(@path.dirname)
|
|
210
|
+
# Unique per writer, so processes never rename each other's file.
|
|
211
|
+
tmp = @path.sub_ext(".#{Process.pid}.#{SecureRandom.hex(4)}.tmp")
|
|
212
|
+
File.write(tmp, JSON.pretty_generate(data))
|
|
213
|
+
File.rename(tmp, @path)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|