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,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