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,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
require_relative "base"
|
|
7
|
+
require_relative "lock"
|
|
8
|
+
require_relative "lookup"
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module Xapi
|
|
12
|
+
module Services
|
|
13
|
+
# One VIF to create.
|
|
14
|
+
# kind: :public, :private (existing network), :internal (no PIF),
|
|
15
|
+
# :default (default_network), :himn
|
|
16
|
+
VifSpec = Struct.new(
|
|
17
|
+
:device, :kind, :network, :bridge, :internal_name, :mac, :mtu, :ip, :netmask,
|
|
18
|
+
:gateway, :locking_mode, :auto_config, keyword_init: true
|
|
19
|
+
) do
|
|
20
|
+
def static_ip?
|
|
21
|
+
!ip.to_s.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ipv6?
|
|
25
|
+
ip.to_s.include?(":")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# netmask may be a prefix length ("24", 64) or a dotted mask.
|
|
29
|
+
def prefix_length
|
|
30
|
+
return(ipv6? ? 64 : 24) if netmask.to_s.empty?
|
|
31
|
+
return netmask.to_i if netmask.to_s =~ /\A\d+\z/
|
|
32
|
+
|
|
33
|
+
IPAddr.new(netmask.to_s).to_i.to_s(2).count("1")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Turns Vagrant network definitions into VIFs and manages internal
|
|
38
|
+
# networks. XAPI has no NAT and no DHCP: every VIF hangs on a real
|
|
39
|
+
# network of the pool.
|
|
40
|
+
class Networks
|
|
41
|
+
include Helpers
|
|
42
|
+
|
|
43
|
+
def initialize(ctx, project:)
|
|
44
|
+
@ctx = ctx
|
|
45
|
+
@client = ctx.client
|
|
46
|
+
@lookup = Lookup.new(ctx)
|
|
47
|
+
@project = project.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.default_internal_name(project)
|
|
51
|
+
"vagrant-xapi-#{Digest::SHA256.hexdigest(project.to_s)[0, 10]}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Pure planning: the order of VIFs.
|
|
55
|
+
#
|
|
56
|
+
# @param networks [Array<[Symbol, Hash]>] config.vm.networks with
|
|
57
|
+
# provider-scoped options already applied
|
|
58
|
+
# @param default_network [String, nil]
|
|
59
|
+
# @param himn [Boolean] add a VIF on the host internal management network
|
|
60
|
+
# @return [Array<VifSpec>]
|
|
61
|
+
def self.plan(networks, default_network:, himn: false, project: "")
|
|
62
|
+
list = networks.reject { |type, _| type == :forwarded_port }
|
|
63
|
+
public_idx = list.index { |type, _| type == :public_network }
|
|
64
|
+
ordered = []
|
|
65
|
+
if public_idx
|
|
66
|
+
ordered << list[public_idx]
|
|
67
|
+
list = list.each_with_index.reject { |_, i| i == public_idx }.map(&:first)
|
|
68
|
+
else
|
|
69
|
+
ordered << [:default, { network: default_network }]
|
|
70
|
+
end
|
|
71
|
+
ordered.concat(list)
|
|
72
|
+
|
|
73
|
+
specs = ordered.each_with_index.map do |(type, opts), device|
|
|
74
|
+
opts ||= {}
|
|
75
|
+
kind = case type
|
|
76
|
+
when :default then :default
|
|
77
|
+
when :public_network then :public
|
|
78
|
+
when :private_network
|
|
79
|
+
opts[:network] || opts[:bridge] ? :private : :internal
|
|
80
|
+
else
|
|
81
|
+
next nil
|
|
82
|
+
end
|
|
83
|
+
VifSpec.new(
|
|
84
|
+
device: device,
|
|
85
|
+
kind: kind,
|
|
86
|
+
network: opts[:network],
|
|
87
|
+
bridge: opts[:bridge],
|
|
88
|
+
internal_name: kind == :internal ? (opts[:name] || default_internal_name(project)).to_s : nil,
|
|
89
|
+
mac: normalize_mac(opts[:mac]),
|
|
90
|
+
mtu: opts[:mtu],
|
|
91
|
+
ip: opts[:ip],
|
|
92
|
+
netmask: opts[:netmask],
|
|
93
|
+
gateway: opts[:gateway],
|
|
94
|
+
locking_mode: opts[:locking_mode],
|
|
95
|
+
# auto_config: false = the user configures the guest network.
|
|
96
|
+
auto_config: opts[:auto_config] != false
|
|
97
|
+
)
|
|
98
|
+
end.compact
|
|
99
|
+
specs.each_with_index { |s, i| s.device = i }
|
|
100
|
+
specs << VifSpec.new(device: specs.size, kind: :himn) if himn
|
|
101
|
+
specs
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.normalize_mac(mac)
|
|
105
|
+
return nil if mac.nil? || mac.to_s.empty?
|
|
106
|
+
|
|
107
|
+
hex = mac.to_s.delete(":-").downcase
|
|
108
|
+
return mac.to_s unless hex =~ /\A[0-9a-f]{12}\z/
|
|
109
|
+
|
|
110
|
+
hex.scan(/../).join(":")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Recreates all VIFs of a halted VM. Keeps the MAC address per device
|
|
114
|
+
# unless one is configured, so cloud-init network-config stays valid.
|
|
115
|
+
# Every network is resolved before a VIF is removed: a name that no
|
|
116
|
+
# longer resolves must not leave the VM without VIFs and lose their
|
|
117
|
+
# MACs.
|
|
118
|
+
# @return [Hash{Integer => String}] device => MAC
|
|
119
|
+
def apply(vm, specs)
|
|
120
|
+
resolved = specs.map do |spec|
|
|
121
|
+
network = resolve(spec)
|
|
122
|
+
[spec, network, spec.mtu ? spec.mtu.to_i : @client.call("network.get_MTU", network).to_i]
|
|
123
|
+
end
|
|
124
|
+
existing = @client.call("VM.get_VIFs", vm).map { |v| [v, @client.call("VIF.get_record", v)] }
|
|
125
|
+
old_macs = existing.to_h { |_ref, rec| [rec["device"].to_i, rec["MAC"]] }
|
|
126
|
+
existing.each { |ref, _| @client.call("VIF.destroy", ref) }
|
|
127
|
+
|
|
128
|
+
resolved.each_with_object({}) do |(spec, network, mtu), macs|
|
|
129
|
+
record = {
|
|
130
|
+
"device" => spec.device.to_s,
|
|
131
|
+
"network" => network,
|
|
132
|
+
"VM" => vm,
|
|
133
|
+
"MAC" => spec.mac || (spec.kind == :himn ? "" : old_macs[spec.device].to_s),
|
|
134
|
+
"MTU" => mtu,
|
|
135
|
+
"other_config" => {},
|
|
136
|
+
"qos_algorithm_type" => "",
|
|
137
|
+
"qos_algorithm_params" => {}
|
|
138
|
+
}
|
|
139
|
+
record["locking_mode"] = spec.locking_mode.to_s if spec.locking_mode
|
|
140
|
+
vif = @client.call("VIF.create", record)
|
|
141
|
+
macs[spec.device] = @client.call("VIF.get_MAC", vif)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [String] network ref
|
|
146
|
+
def resolve(spec)
|
|
147
|
+
case spec.kind
|
|
148
|
+
when :default
|
|
149
|
+
spec.network ? @lookup.network(spec.network) : @lookup.management_network
|
|
150
|
+
when :public, :private
|
|
151
|
+
if spec.network
|
|
152
|
+
@lookup.network(spec.network)
|
|
153
|
+
elsif spec.bridge
|
|
154
|
+
@lookup.network_by_bridge(spec.bridge)
|
|
155
|
+
else
|
|
156
|
+
@lookup.management_network
|
|
157
|
+
end
|
|
158
|
+
when :internal
|
|
159
|
+
ensure_internal(spec.internal_name)
|
|
160
|
+
when :himn
|
|
161
|
+
@lookup.himn_network
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Internal network without PIF, created when missing and marked with
|
|
166
|
+
# the project.
|
|
167
|
+
def ensure_internal(name)
|
|
168
|
+
found = find_internal(name)
|
|
169
|
+
return found if found
|
|
170
|
+
|
|
171
|
+
Lock.new(@ctx, "network.#{name}", ttl: 300, wait: 300).synchronize do
|
|
172
|
+
find_internal(name) || begin
|
|
173
|
+
@ctx.ui&.info(:creating_network, name: name)
|
|
174
|
+
@client.call("network.create", {
|
|
175
|
+
"name_label" => name,
|
|
176
|
+
"name_description" => "Internal network created by vagrant-xapi for #{@project}",
|
|
177
|
+
"MTU" => 1500,
|
|
178
|
+
"other_config" => { Marking::PROJECT => @project, Marking::NETWORK => name },
|
|
179
|
+
"tags" => []
|
|
180
|
+
})
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Only a network marked for this project is used. A network with the
|
|
186
|
+
# same name that belongs to another project (or to nobody) is never
|
|
187
|
+
# joined: that would connect two projects on one L2 segment.
|
|
188
|
+
def find_internal(name)
|
|
189
|
+
refs = @client.call("network.get_by_name_label", name)
|
|
190
|
+
return nil if refs.empty?
|
|
191
|
+
|
|
192
|
+
ours = refs.find { |r| Marking.ours?(@client.call("network.get_other_config", r), Marking::PROJECT, @project) }
|
|
193
|
+
raise Error.new(:internal_network_taken, name: name) unless ours
|
|
194
|
+
|
|
195
|
+
ours
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Internal networks (with our marking) the VM is attached to.
|
|
199
|
+
def internal_networks_of(vm)
|
|
200
|
+
@client.call("VM.get_VIFs", vm).filter_map do |vif|
|
|
201
|
+
net = @client.call("VIF.get_network", vif)
|
|
202
|
+
oc = @client.call("network.get_other_config", net)
|
|
203
|
+
net if oc.key?(Marking::NETWORK) && @client.call("network.get_PIFs", net).empty?
|
|
204
|
+
end.uniq
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Internal networks only exist per host, so all VMs on one must run on
|
|
208
|
+
# the same host: the host of a VM that already runs on one of them.
|
|
209
|
+
# @return [String, nil] host ref, nil when no other VM runs there
|
|
210
|
+
def running_peer_host(networks, except:)
|
|
211
|
+
networks.each do |net|
|
|
212
|
+
@client.call("network.get_VIFs", net).each do |vif|
|
|
213
|
+
peer = ignore_missing { @client.call("VIF.get_VM", vif) }
|
|
214
|
+
next if peer.nil? || peer == except
|
|
215
|
+
|
|
216
|
+
rec = ignore_missing { @client.call("VM.get_record", peer) }
|
|
217
|
+
next unless rec && rec["power_state"] == "Running" && !null_ref?(rec["resident_on"])
|
|
218
|
+
|
|
219
|
+
return rec["resident_on"]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Serializes VM starts of this project, so two machines on the same
|
|
226
|
+
# internal network cannot both start before either one runs.
|
|
227
|
+
def placement_lock(&block)
|
|
228
|
+
Lock.new(@ctx, "placement.#{Digest::SHA256.hexdigest(@project)[0, 16]}", ttl: 600, wait: 1800)
|
|
229
|
+
.synchronize(&block)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Removes internal networks of this project that no VIF uses anymore.
|
|
233
|
+
def cleanup_unused(candidates)
|
|
234
|
+
candidates.each do |net|
|
|
235
|
+
ignore_missing do
|
|
236
|
+
oc = @client.call("network.get_other_config", net)
|
|
237
|
+
next unless Marking.ours?(oc, Marking::PROJECT, @project) && oc.key?(Marking::NETWORK)
|
|
238
|
+
next unless @client.call("network.get_VIFs", net).empty?
|
|
239
|
+
|
|
240
|
+
@ctx.ui&.detail(:removing_network, name: @client.call("network.get_name_label", net))
|
|
241
|
+
@client.call("network.destroy", net)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Services
|
|
8
|
+
# Power operations, always through Async.* tasks.
|
|
9
|
+
class Power
|
|
10
|
+
include Helpers
|
|
11
|
+
|
|
12
|
+
STATES = {
|
|
13
|
+
"Halted" => :halted,
|
|
14
|
+
"Running" => :running,
|
|
15
|
+
"Suspended" => :suspended,
|
|
16
|
+
"Paused" => :paused
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(ctx)
|
|
20
|
+
@ctx = ctx
|
|
21
|
+
@client = ctx.client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Hash] :state (Symbol), :operations (Array<String>)
|
|
25
|
+
def state(vm)
|
|
26
|
+
rec = @client.call("VM.get_record", vm)
|
|
27
|
+
{
|
|
28
|
+
state: STATES.fetch(rec["power_state"], :unknown),
|
|
29
|
+
operations: (rec["current_operations"] || {}).values.map(&:to_s)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def power_state(vm)
|
|
34
|
+
STATES.fetch(@client.call("VM.get_power_state", vm), :unknown)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param host [String, nil] host ref to start on (internal networks)
|
|
38
|
+
def start(vm, host: nil)
|
|
39
|
+
@ctx.ui&.info(:starting_vm)
|
|
40
|
+
if host && !null_ref?(host)
|
|
41
|
+
@ctx.run_task("VM.start_on", vm, host, false, false, label: "start")
|
|
42
|
+
else
|
|
43
|
+
@ctx.run_task("VM.start", vm, false, false, label: "start")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# clean_shutdown, then hard_shutdown after `timeout` seconds.
|
|
48
|
+
def shutdown(vm, timeout:, force: false)
|
|
49
|
+
state = power_state(vm)
|
|
50
|
+
return if state == :halted
|
|
51
|
+
|
|
52
|
+
if force || state != :running
|
|
53
|
+
hard_shutdown(vm)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@ctx.ui&.info(:clean_shutdown)
|
|
58
|
+
begin
|
|
59
|
+
@ctx.client.run_task("VM.clean_shutdown", vm, label: "clean_shutdown",
|
|
60
|
+
timeout: timeout, cancel: @ctx.cancel)
|
|
61
|
+
rescue Api::TaskTimeout, Api::XapiError => e
|
|
62
|
+
@ctx.logger&.info("clean shutdown failed (#{e.message}); forcing")
|
|
63
|
+
end
|
|
64
|
+
hard_shutdown(vm) unless power_state(vm) == :halted
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def hard_shutdown(vm)
|
|
68
|
+
return if power_state(vm) == :halted
|
|
69
|
+
|
|
70
|
+
@ctx.ui&.info(:hard_shutdown)
|
|
71
|
+
@ctx.run_task("VM.hard_shutdown", vm, label: "hard_shutdown")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def suspend(vm)
|
|
75
|
+
@ctx.ui&.info(:suspending)
|
|
76
|
+
@ctx.run_task("VM.suspend", vm, label: "suspend", progress_key: :suspending_progress)
|
|
77
|
+
rescue Api::XapiError => e
|
|
78
|
+
case e.code
|
|
79
|
+
when "VM_MISSING_PV_DRIVERS", "VM_LACKS_FEATURE_SUSPEND", "VM_LACKS_FEATURE"
|
|
80
|
+
raise Error.new(:suspend_no_drivers)
|
|
81
|
+
when "VM_NO_SUSPEND_SR", "SR_NOT_FOUND"
|
|
82
|
+
raise Error.new(:suspend_no_sr)
|
|
83
|
+
else
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param host [String, nil] host ref to resume on (internal networks)
|
|
89
|
+
def resume(vm, host: nil)
|
|
90
|
+
@ctx.ui&.info(:resuming)
|
|
91
|
+
if host && !null_ref?(host)
|
|
92
|
+
@ctx.run_task("VM.resume_on", vm, host, false, false, label: "resume", progress_key: :resuming_progress)
|
|
93
|
+
else
|
|
94
|
+
@ctx.run_task("VM.resume", vm, false, false, label: "resume", progress_key: :resuming_progress)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def unpause(vm)
|
|
99
|
+
@ctx.run_task("VM.unpause", vm, label: "unpause")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def resident_host(vm)
|
|
103
|
+
@client.call("VM.get_resident_on", vm)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module VagrantPlugins
|
|
7
|
+
module Xapi
|
|
8
|
+
module Services
|
|
9
|
+
# The RFB (VNC) handshake of the console proxy. Towards the local
|
|
10
|
+
# viewer it asks for a one-time VNC password, so no other local user
|
|
11
|
+
# can watch the console through the port, and macOS Screen Sharing,
|
|
12
|
+
# which always wants a password, connects without asking (the password
|
|
13
|
+
# goes into the vnc:// URL). Towards XAPI, which controls access with
|
|
14
|
+
# the session and offers no authentication, it chooses "None". After
|
|
15
|
+
# both handshakes the bytes pass through unchanged.
|
|
16
|
+
module Rfb
|
|
17
|
+
NONE = 1
|
|
18
|
+
VNC_AUTH = 2
|
|
19
|
+
TIMEOUT = 30
|
|
20
|
+
VERSION_RE = /\ARFB (\d{3})\.(\d{3})\n\z/.freeze
|
|
21
|
+
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# VNC uses at most 8 characters.
|
|
27
|
+
def password
|
|
28
|
+
SecureRandom.alphanumeric(8)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The answer to a VNC challenge: DES-ECB with the password as key,
|
|
32
|
+
# the bits of every key byte reversed (as VNC does). OpenSSL 3 keeps
|
|
33
|
+
# single DES in its legacy provider; 3DES with the key three times is
|
|
34
|
+
# the same cipher and is in the default provider.
|
|
35
|
+
def vnc_response(password, challenge)
|
|
36
|
+
key = password.to_s.b[0, 8].ljust(8, "\0").bytes.map { |b| reverse_bits(b) }.pack("C*")
|
|
37
|
+
cipher = OpenSSL::Cipher.new("des-ede3")
|
|
38
|
+
cipher.encrypt
|
|
39
|
+
cipher.key = key * 3
|
|
40
|
+
cipher.padding = 0
|
|
41
|
+
cipher.update(challenge.b) + cipher.final
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether this Ruby/OpenSSL can do it (3DES may be missing, e.g. in a
|
|
45
|
+
# FIPS-only build).
|
|
46
|
+
def available?
|
|
47
|
+
vnc_response("x", "\0" * 16).bytesize == 16
|
|
48
|
+
rescue OpenSSL::Cipher::CipherError, RuntimeError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Towards the viewer: offers only VNC authentication.
|
|
53
|
+
# @return [Boolean] whether the viewer knew the password
|
|
54
|
+
def serve_viewer(io, password)
|
|
55
|
+
io.write("RFB 003.008\n")
|
|
56
|
+
minor = minor_version(read(io, 12))
|
|
57
|
+
if minor < 7
|
|
58
|
+
io.write([VNC_AUTH].pack("N"))
|
|
59
|
+
else
|
|
60
|
+
io.write([1, VNC_AUTH].pack("CC"))
|
|
61
|
+
chosen = read(io, 1).unpack1("C")
|
|
62
|
+
raise Error, "the viewer chose security type #{chosen}" unless chosen == VNC_AUTH
|
|
63
|
+
end
|
|
64
|
+
challenge = SecureRandom.random_bytes(16)
|
|
65
|
+
io.write(challenge)
|
|
66
|
+
ok = OpenSSL.fixed_length_secure_compare(read(io, 16), vnc_response(password, challenge))
|
|
67
|
+
io.write([ok ? 0 : 1].pack("N"))
|
|
68
|
+
io.write([14].pack("N") + "Wrong password") if !ok && minor >= 8
|
|
69
|
+
ok
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Towards XAPI: takes "None".
|
|
73
|
+
def join_server(io)
|
|
74
|
+
minor = minor_version(read(io, 12))
|
|
75
|
+
io.write(format("RFB 003.%03d\n", minor))
|
|
76
|
+
if minor < 7
|
|
77
|
+
type = read(io, 4).unpack1("N")
|
|
78
|
+
raise Error, reason(io) if type.zero?
|
|
79
|
+
raise Error, "the console asks for security type #{type}" unless type == NONE
|
|
80
|
+
else
|
|
81
|
+
count = read(io, 1).unpack1("C")
|
|
82
|
+
raise Error, reason(io) if count.zero?
|
|
83
|
+
raise Error, "the console offers no access without authentication" unless read(io, count).bytes.include?(NONE)
|
|
84
|
+
|
|
85
|
+
io.write([NONE].pack("C"))
|
|
86
|
+
# Only 3.8 sends a result for "None".
|
|
87
|
+
raise Error, reason(io) if minor >= 8 && !read(io, 4).unpack1("N").zero?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 3, 7 or 8: the protocol versions with different handshakes. Apple's
|
|
92
|
+
# 3.889 behaves as 3.8 here.
|
|
93
|
+
def minor_version(line)
|
|
94
|
+
m = VERSION_RE.match(line)
|
|
95
|
+
raise Error, "not an RFB greeting: #{line.inspect}" unless m && m[1] == "003"
|
|
96
|
+
|
|
97
|
+
minor = m[2].to_i
|
|
98
|
+
if minor >= 8 then 8
|
|
99
|
+
elsif minor == 7 then 7
|
|
100
|
+
else 3
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def reason(io)
|
|
105
|
+
read(io, [read(io, 4).unpack1("N"), 1024].min)
|
|
106
|
+
rescue Error
|
|
107
|
+
"refused"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def read(io, count)
|
|
111
|
+
data = +"".b
|
|
112
|
+
while data.bytesize < count
|
|
113
|
+
pending = io.respond_to?(:pending) && io.pending.positive?
|
|
114
|
+
raise Error, "timed out" unless pending || IO.select([io], nil, nil, TIMEOUT)
|
|
115
|
+
|
|
116
|
+
data << io.readpartial(count - data.bytesize)
|
|
117
|
+
end
|
|
118
|
+
data
|
|
119
|
+
rescue EOFError, IOError, SystemCallError
|
|
120
|
+
raise Error, "the connection closed during the handshake"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reverse_bits(byte)
|
|
124
|
+
format("%08b", byte).reverse.to_i(2)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module Xapi
|
|
7
|
+
module Services
|
|
8
|
+
# vagrant snapshot save/restore/delete/list (and therefore push/pop).
|
|
9
|
+
class Snapshots
|
|
10
|
+
include Helpers
|
|
11
|
+
|
|
12
|
+
def initialize(ctx)
|
|
13
|
+
@ctx = ctx
|
|
14
|
+
@client = ctx.client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Names, oldest first (push/pop depend on this order).
|
|
18
|
+
def list(vm)
|
|
19
|
+
records(vm).map { |_ref, rec| rec["name_label"] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# [[ref, record], ...] sorted by snapshot_time
|
|
23
|
+
def records(vm)
|
|
24
|
+
@client.call("VM.get_snapshots", vm)
|
|
25
|
+
.map { |ref| [ref, @client.call("VM.get_record", ref)] }
|
|
26
|
+
.sort_by { |_ref, rec| rec["snapshot_time"].to_s }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find(vm, name)
|
|
30
|
+
hits = records(vm).select { |_ref, rec| rec["name_label"] == name.to_s }
|
|
31
|
+
raise Error.new(:snapshot_not_found, name: name.to_s) if hits.empty?
|
|
32
|
+
|
|
33
|
+
hits.last.first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param memory [Boolean] checkpoint (with RAM) when running
|
|
37
|
+
def save(vm, name, memory: false)
|
|
38
|
+
raise Error.new(:snapshot_exists, name: name.to_s) if list(vm).include?(name.to_s)
|
|
39
|
+
|
|
40
|
+
vm_uuid = @client.call("VM.get_uuid", vm)
|
|
41
|
+
running = @client.call("VM.get_power_state", vm) == "Running"
|
|
42
|
+
method = memory && running ? "VM.checkpoint" : "VM.snapshot"
|
|
43
|
+
snap = @ctx.run_task(method, vm, name.to_s, label: "snapshot #{name}").ref
|
|
44
|
+
raise Error.new(:snapshot_failed, name: name.to_s) unless snap
|
|
45
|
+
|
|
46
|
+
set_oc("VM", snap, Marking::SNAPSHOT_OF, vm_uuid)
|
|
47
|
+
# Mark the snapshot's disks so destroy can find them.
|
|
48
|
+
disk_vdis(snap).each { |vdi| set_oc("VDI", vdi, Marking::OWNER_VM, vm_uuid) }
|
|
49
|
+
snap
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Symbol] power state after revert (:suspended for checkpoints)
|
|
53
|
+
def restore(vm, name)
|
|
54
|
+
snap = find(vm, name)
|
|
55
|
+
@ctx.run_task("VM.revert", snap, label: "revert #{name}")
|
|
56
|
+
Power::STATES.fetch(@client.call("VM.get_power_state", vm), :unknown)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def delete(vm, name)
|
|
60
|
+
destroy_snapshot(find(vm, name))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def delete_all(vm)
|
|
64
|
+
records(vm).each { |ref, _| destroy_snapshot(ref) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# VDIs first, then the snapshot object; the SM layer coalesces the
|
|
68
|
+
# VHD chain in the background.
|
|
69
|
+
def destroy_snapshot(snap)
|
|
70
|
+
vdis = vbds_of(snap).filter_map do |_vbd, rec|
|
|
71
|
+
rec["VDI"] if rec["type"] == "Disk" && !null_ref?(rec["VDI"])
|
|
72
|
+
end
|
|
73
|
+
suspend_vdi = ignore_missing { @client.call("VM.get_suspend_VDI", snap) }
|
|
74
|
+
vdis << suspend_vdi unless null_ref?(suspend_vdi)
|
|
75
|
+
vdis.uniq.each { |vdi| ignore_missing { @client.call("VDI.destroy", vdi) } }
|
|
76
|
+
ignore_missing { @client.call("VM.destroy", snap) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|