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,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module Xapi
|
|
12
|
+
module Api
|
|
13
|
+
# HTTPS transport to one XAPI host: TLS setup (CA, custom CA bundle or a
|
|
14
|
+
# SHA-256 fingerprint pin), a small pool of keep-alive connections, and
|
|
15
|
+
# translation of low-level network errors into Api errors.
|
|
16
|
+
#
|
|
17
|
+
# There is deliberately no way to disable certificate verification.
|
|
18
|
+
class Transport
|
|
19
|
+
NETWORK_ERRORS = [
|
|
20
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
|
|
21
|
+
Errno::ENETUNREACH, Errno::ETIMEDOUT, Errno::EPIPE, SocketError,
|
|
22
|
+
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, EOFError,
|
|
23
|
+
IOError
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
TIMEOUT_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ETIMEDOUT].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :host, :port
|
|
29
|
+
|
|
30
|
+
# @param tls [Hash] :verify (Boolean, must be true unless :fingerprint),
|
|
31
|
+
# :ca_file (String), :fingerprint (one or more SHA-256 pins, see
|
|
32
|
+
# normalize_fingerprints)
|
|
33
|
+
def initialize(host:, port: 443, tls: {}, timeout: 60)
|
|
34
|
+
@host = host
|
|
35
|
+
@port = port
|
|
36
|
+
@tls = tls || {}
|
|
37
|
+
@timeout = timeout
|
|
38
|
+
@fingerprints = self.class.normalize_fingerprints(@tls[:fingerprint])
|
|
39
|
+
@pool = []
|
|
40
|
+
@mutex = Mutex.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.normalize_fingerprint(value)
|
|
44
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
45
|
+
|
|
46
|
+
value.to_s.delete(":").strip.downcase
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Pins as an Array of lowercase hex strings. Takes one fingerprint,
|
|
50
|
+
# several separated by commas or whitespace, or an Array of them (e.g.
|
|
51
|
+
# the certificates of all hosts of a pool).
|
|
52
|
+
def self.normalize_fingerprints(value)
|
|
53
|
+
Array(value).flat_map { |v| v.to_s.split(/[\s,]+/) }.filter_map { |v| normalize_fingerprint(v) }.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Yields a started Net::HTTP. Connections are reused between calls.
|
|
57
|
+
def with_connection(read_timeout: nil)
|
|
58
|
+
# Only a handshake of this call may name a certificate in an error.
|
|
59
|
+
Thread.current[:vagrant_xapi_peer_cert] = nil
|
|
60
|
+
http = checkout
|
|
61
|
+
http.read_timeout = read_timeout || @timeout
|
|
62
|
+
ok = false
|
|
63
|
+
begin
|
|
64
|
+
result = yield http
|
|
65
|
+
ok = true
|
|
66
|
+
result
|
|
67
|
+
ensure
|
|
68
|
+
ok ? checkin(http) : discard(http)
|
|
69
|
+
end
|
|
70
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
71
|
+
raise TlsError.new(@host, tls_hint(e))
|
|
72
|
+
rescue *TIMEOUT_ERRORS => e
|
|
73
|
+
raise TimeoutError.new("#{@host}:#{@port}", e.message)
|
|
74
|
+
rescue *NETWORK_ERRORS => e
|
|
75
|
+
raise ConnectionError.new("#{@host}:#{@port}", e.message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A fresh, started connection for long transfers (not pooled).
|
|
79
|
+
def new_connection(read_timeout:)
|
|
80
|
+
http = build
|
|
81
|
+
http.read_timeout = read_timeout
|
|
82
|
+
http.write_timeout = read_timeout if http.respond_to?(:write_timeout=)
|
|
83
|
+
start(http)
|
|
84
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
85
|
+
raise TlsError.new(@host, tls_hint(e))
|
|
86
|
+
rescue *TIMEOUT_ERRORS => e
|
|
87
|
+
raise TimeoutError.new("#{@host}:#{@port}", e.message)
|
|
88
|
+
rescue *NETWORK_ERRORS => e
|
|
89
|
+
raise ConnectionError.new("#{@host}:#{@port}", e.message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Opens a raw TLS socket (used for the VNC console tunnel).
|
|
93
|
+
def open_tls_socket
|
|
94
|
+
Thread.current[:vagrant_xapi_peer_cert] = nil
|
|
95
|
+
tcp = Socket.tcp(@host, @port, connect_timeout: @timeout)
|
|
96
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
97
|
+
configure_context(ctx)
|
|
98
|
+
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
|
99
|
+
ssl.hostname = @host if ssl.respond_to?(:hostname=)
|
|
100
|
+
ssl.sync_close = true
|
|
101
|
+
ssl.connect
|
|
102
|
+
ssl.post_connection_check(@host) unless pinned?
|
|
103
|
+
check_pin!(ssl.peer_cert) if pinned?
|
|
104
|
+
ssl
|
|
105
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
106
|
+
raise TlsError.new(@host, tls_hint(e))
|
|
107
|
+
rescue *TIMEOUT_ERRORS => e
|
|
108
|
+
raise TimeoutError.new("#{@host}:#{@port}", e.message)
|
|
109
|
+
rescue *NETWORK_ERRORS => e
|
|
110
|
+
raise ConnectionError.new("#{@host}:#{@port}", e.message)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def shutdown
|
|
114
|
+
conns = @mutex.synchronize { @pool.dup.tap { @pool.clear } }
|
|
115
|
+
conns.each { |c| discard(c) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def pinned?
|
|
121
|
+
!@fingerprints.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def checkout
|
|
125
|
+
http = @mutex.synchronize { @pool.pop }
|
|
126
|
+
return http if http&.started?
|
|
127
|
+
|
|
128
|
+
start(build)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def checkin(http)
|
|
132
|
+
@mutex.synchronize do
|
|
133
|
+
if @pool.size < 8
|
|
134
|
+
@pool << http
|
|
135
|
+
else
|
|
136
|
+
discard(http)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def discard(http)
|
|
142
|
+
http.finish if http&.started?
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build
|
|
148
|
+
http = Net::HTTP.new(@host, @port)
|
|
149
|
+
http.use_ssl = true
|
|
150
|
+
http.open_timeout = @timeout
|
|
151
|
+
http.read_timeout = @timeout
|
|
152
|
+
http.keep_alive_timeout = 30
|
|
153
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
154
|
+
http.verify_callback = verify_callback
|
|
155
|
+
if pinned?
|
|
156
|
+
# The pin replaces CA and hostname checks; only the leaf counts.
|
|
157
|
+
http.verify_hostname = false if http.respond_to?(:verify_hostname=)
|
|
158
|
+
elsif @tls[:ca_file]
|
|
159
|
+
http.ca_file = @tls[:ca_file]
|
|
160
|
+
else
|
|
161
|
+
store = OpenSSL::X509::Store.new
|
|
162
|
+
store.set_default_paths
|
|
163
|
+
http.cert_store = store
|
|
164
|
+
end
|
|
165
|
+
http
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def configure_context(ctx)
|
|
169
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
170
|
+
ctx.verify_callback = verify_callback
|
|
171
|
+
if pinned?
|
|
172
|
+
nil # the callback checks the pins
|
|
173
|
+
elsif @tls[:ca_file]
|
|
174
|
+
ctx.ca_file = @tls[:ca_file]
|
|
175
|
+
else
|
|
176
|
+
store = OpenSSL::X509::Store.new
|
|
177
|
+
store.set_default_paths
|
|
178
|
+
ctx.cert_store = store
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Remembers the certificate the server presents (per thread, so a
|
|
183
|
+
# failed check can show its fingerprint). With pins it also decides:
|
|
184
|
+
# only the leaf counts, and it must match one of them; otherwise
|
|
185
|
+
# OpenSSL's own verdict (CA and hostname) stands.
|
|
186
|
+
def verify_callback
|
|
187
|
+
pins = pinned? ? @fingerprints : nil
|
|
188
|
+
lambda do |ok, store_ctx|
|
|
189
|
+
leaf = store_ctx.chain&.first || (store_ctx.error_depth.zero? ? store_ctx.current_cert : nil)
|
|
190
|
+
Thread.current[:vagrant_xapi_peer_cert] = leaf if leaf
|
|
191
|
+
next ok unless pins
|
|
192
|
+
next true if store_ctx.error_depth.positive?
|
|
193
|
+
|
|
194
|
+
pins.include?(OpenSSL::Digest::SHA256.hexdigest(store_ctx.current_cert.to_der))
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def start(http)
|
|
199
|
+
Thread.current[:vagrant_xapi_peer_cert] = nil
|
|
200
|
+
http.start
|
|
201
|
+
check_pin!(http.peer_cert) if pinned?
|
|
202
|
+
http
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The verify_callback already enforces the pin during the handshake;
|
|
206
|
+
# this is a second check once the peer certificate is known.
|
|
207
|
+
def check_pin!(cert)
|
|
208
|
+
return if cert.nil?
|
|
209
|
+
|
|
210
|
+
actual = OpenSSL::Digest::SHA256.hexdigest(cert.to_der)
|
|
211
|
+
return if @fingerprints.include?(actual)
|
|
212
|
+
|
|
213
|
+
configured = @fingerprints.map { |fp| format_fp(fp) }.join(", ")
|
|
214
|
+
raise TlsError.new(@host, "certificate fingerprint #{format_fp(actual)} does not match " \
|
|
215
|
+
"the configured tls_fingerprint #{configured}")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def format_fp(hex)
|
|
219
|
+
return "(none)" if hex.nil?
|
|
220
|
+
|
|
221
|
+
hex.scan(/../).join(":").upcase
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The error with what to do about it, and the SHA-256 fingerprint of
|
|
225
|
+
# the certificate the server presented: like SSH's first connection,
|
|
226
|
+
# to check against the host and pin it.
|
|
227
|
+
def tls_hint(error)
|
|
228
|
+
msg = error.message
|
|
229
|
+
# Other TLS errors (e.g. "unexpected eof" on a reused connection)
|
|
230
|
+
# are no certificate problem: no advice to pin anything.
|
|
231
|
+
return msg unless msg.include?("certificate verify failed")
|
|
232
|
+
|
|
233
|
+
seen = Thread.current[:vagrant_xapi_peer_cert]
|
|
234
|
+
fp = seen && format_fp(OpenSSL::Digest::SHA256.hexdigest(seen.to_der))
|
|
235
|
+
if pinned?
|
|
236
|
+
"#{msg} (the certificate#{" #{fp}" if fp} does not match tls_fingerprint)"
|
|
237
|
+
elsif fp
|
|
238
|
+
"#{msg}. The certificate's SHA-256 fingerprint is #{fp}. If that is the host's certificate " \
|
|
239
|
+
"(check it on the host), pin it with tls_fingerprint or VAGRANT_XAPI_TLS_FINGERPRINT, " \
|
|
240
|
+
"or use your own CA with tls_ca_file or VAGRANT_XAPI_TLS_CA_FILE"
|
|
241
|
+
else
|
|
242
|
+
"#{msg} (for a self-signed certificate set tls_fingerprint or VAGRANT_XAPI_TLS_FINGERPRINT, " \
|
|
243
|
+
"or tls_ca_file or VAGRANT_XAPI_TLS_CA_FILE)"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "glue"
|
|
7
|
+
require_relative "services/disks"
|
|
8
|
+
require_relative "services/box_source"
|
|
9
|
+
require_relative "services/snapshots"
|
|
10
|
+
require_relative "services/tunnels"
|
|
11
|
+
|
|
12
|
+
module VagrantPlugins
|
|
13
|
+
module Xapi
|
|
14
|
+
# Provider capabilities Vagrant core asks for.
|
|
15
|
+
module Cap
|
|
16
|
+
DISK_EXTS = %w[vhd qcow2 raw img].freeze
|
|
17
|
+
DEFAULT_DISK_EXT = "vhd"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def snapshot_list(machine)
|
|
22
|
+
return [] if machine.id.nil?
|
|
23
|
+
|
|
24
|
+
with_vm(machine) { |ctx, vm| Services::Snapshots.new(ctx).list(vm) } || []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# host port => guest port, for `vagrant port` and auto_correct
|
|
28
|
+
def forwarded_ports(machine)
|
|
29
|
+
return {} if machine.state.id != :running
|
|
30
|
+
|
|
31
|
+
Services::Tunnels.new(machine.data_dir).forwarded_ports
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def public_address(machine)
|
|
35
|
+
return nil if machine.state.id != :running
|
|
36
|
+
|
|
37
|
+
info = machine.ssh_info
|
|
38
|
+
info && info[:host]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Adapter number (1-based, like VirtualBox) => MAC without separators.
|
|
42
|
+
def nic_mac_addresses(machine)
|
|
43
|
+
with_vm(machine) do |ctx, vm|
|
|
44
|
+
ctx.client.call("VM.get_VIFs", vm).to_h do |vif|
|
|
45
|
+
rec = ctx.client.call("VIF.get_record", vif)
|
|
46
|
+
[rec["device"].to_i + 1, rec["MAC"].to_s.delete(":").upcase]
|
|
47
|
+
end
|
|
48
|
+
end || {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def configure_disks(machine, defined_disks)
|
|
52
|
+
return {} if defined_disks.empty?
|
|
53
|
+
|
|
54
|
+
with_vm(machine) do |ctx, vm|
|
|
55
|
+
Services::Disks.new(ctx, default_sr: machine.provider_config.sr, converter: Glue.method(:convert_qcow2),
|
|
56
|
+
digest_cache: Services::DigestCache.new(Glue.data_home(machine.env).join("digests.json")))
|
|
57
|
+
.configure(vm, defined_disks)
|
|
58
|
+
end || {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cleanup_disks(machine, defined_disks, disk_meta_file)
|
|
62
|
+
return if disk_meta_file.values.flatten.empty?
|
|
63
|
+
|
|
64
|
+
with_vm(machine) do |ctx, vm|
|
|
65
|
+
Services::Disks.new(ctx, default_sr: machine.provider_config.sr).cleanup(vm, defined_disks, disk_meta_file)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_disk_ext(_machine, disk_ext)
|
|
70
|
+
DISK_EXTS.include?(disk_ext.to_s.downcase)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def default_disk_exts(_machine)
|
|
74
|
+
DISK_EXTS
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_default_disk_ext(_machine)
|
|
78
|
+
DEFAULT_DISK_EXT
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def with_vm(machine)
|
|
82
|
+
ctx = Glue.context(machine)
|
|
83
|
+
Errors.wrap do
|
|
84
|
+
vm = Glue.vm_ref(machine, ctx)
|
|
85
|
+
vm ? yield(ctx, vm) : nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../services/adoption"
|
|
5
|
+
require_relative "../services/lookup"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Command
|
|
10
|
+
# vagrant xapi adopt [vm] UUID: attach an existing VM to this machine.
|
|
11
|
+
# A VM without our marking needs --force.
|
|
12
|
+
class Adopt < Base
|
|
13
|
+
def execute
|
|
14
|
+
options = { force: false }
|
|
15
|
+
opts = OptionParser.new do |o|
|
|
16
|
+
o.banner = "Usage: vagrant xapi adopt [vm-name] UUID [--force]"
|
|
17
|
+
o.on("-f", "--force", "Also adopt a VM without vagrant-xapi marking") { options[:force] = true }
|
|
18
|
+
end
|
|
19
|
+
argv = parse_options(opts)
|
|
20
|
+
return 0 if argv.nil?
|
|
21
|
+
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.empty? || argv.size > 2
|
|
22
|
+
|
|
23
|
+
uuid = argv.pop
|
|
24
|
+
each_xapi_machine(argv, single: true) do |machine|
|
|
25
|
+
# Adopting the machine's own VM again updates its marking (e.g.
|
|
26
|
+
# after the project directory moved). An id that is no VM UUID
|
|
27
|
+
# (vagrant-xenserver's OpaqueRef) is replaced.
|
|
28
|
+
if machine.id && machine.id != uuid && Services::Lookup.uuid?(machine.id)
|
|
29
|
+
raise Errors::ServiceFailed.new(:adopt_machine_exists, machine: machine.name.to_s, uuid: machine.id.to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
adopt(machine, uuid, options[:force])
|
|
33
|
+
end
|
|
34
|
+
0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Without --force only VMs of this Vagrant installation may be
|
|
40
|
+
# (re-)adopted: one that already belongs to this machine and project,
|
|
41
|
+
# or this machine's own VM after the project moved. The same machine
|
|
42
|
+
# name and project path alone are not enough: CI runners and
|
|
43
|
+
# colleagues often have the same checkout layout.
|
|
44
|
+
def own_vm?(machine, uuid, oc)
|
|
45
|
+
marking = Services::Marking
|
|
46
|
+
same_install = if oc.key?(marking::CLIENT_ID)
|
|
47
|
+
oc[marking::CLIENT_ID] == Glue.client_id(@env)
|
|
48
|
+
else
|
|
49
|
+
oc[marking::USER] == marking.user && oc[marking::CLIENT] == marking.client_host
|
|
50
|
+
end
|
|
51
|
+
return false unless same_install
|
|
52
|
+
|
|
53
|
+
same_place = oc[marking::MACHINE] == machine.name.to_s &&
|
|
54
|
+
oc[marking::PROJECT] == Glue.project_path(machine)
|
|
55
|
+
same_place || machine.id == uuid
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def adopt(machine, uuid, force)
|
|
59
|
+
ctx = Glue.context(machine)
|
|
60
|
+
client = ctx.client
|
|
61
|
+
vm = Services::Lookup.new(ctx).vm_by_uuid(uuid)
|
|
62
|
+
raise Errors::ServiceFailed.new(:vm_uuid_not_found, uuid: uuid) unless vm
|
|
63
|
+
|
|
64
|
+
rec = client.call("VM.get_record", vm)
|
|
65
|
+
if rec["is_a_template"] || rec["is_a_snapshot"] || rec["is_control_domain"]
|
|
66
|
+
raise Errors::ServiceFailed.new(:adopt_not_a_vm, uuid: uuid)
|
|
67
|
+
end
|
|
68
|
+
oc = rec["other_config"] || {}
|
|
69
|
+
if !Services::Marking.marked?(oc) && !force
|
|
70
|
+
raise Errors::ServiceFailed.new(:adopt_needs_force, uuid: uuid)
|
|
71
|
+
end
|
|
72
|
+
if Services::Marking.marked?(oc) && !force && !own_vm?(machine, uuid, oc)
|
|
73
|
+
raise Errors::ServiceFailed.new(:adopt_other_owner, uuid: uuid, machine: oc[Services::Marking::MACHINE].to_s,
|
|
74
|
+
project: oc[Services::Marking::PROJECT].to_s,
|
|
75
|
+
user: oc[Services::Marking::USER].to_s)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
marking = Services::Marking.vm_marking(machine_name: machine.name, project: Glue.project_path(machine),
|
|
79
|
+
client_id: Glue.client_id(@env))
|
|
80
|
+
adoption = Services::Adoption.new(ctx)
|
|
81
|
+
adoption.mark_vm(vm, marking)
|
|
82
|
+
# The user explicitly hands this VM to Vagrant: its own disks become
|
|
83
|
+
# ours. A disk that is sharable or also attached to another VM is
|
|
84
|
+
# not, so `vagrant destroy` never deletes it.
|
|
85
|
+
adoption.mark_disks(vm)
|
|
86
|
+
machine.id = rec["uuid"]
|
|
87
|
+
machine.ui.success(t("adopted", name: rec["name_label"], uuid: rec["uuid"]))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "vagrant/util/busy"
|
|
5
|
+
|
|
6
|
+
require_relative "../errors"
|
|
7
|
+
require_relative "../extensions"
|
|
8
|
+
require_relative "../glue"
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module Xapi
|
|
12
|
+
module Command
|
|
13
|
+
# Shared plumbing for the subcommands.
|
|
14
|
+
class Base < Vagrant.plugin("2", :command)
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
# Runs the block for each xapi machine, translating errors.
|
|
18
|
+
def each_xapi_machine(names, single: false)
|
|
19
|
+
with_target_vms(names, provider: target_provider, single_target: single) do |machine|
|
|
20
|
+
unless Extensions.provider?(machine.provider_name)
|
|
21
|
+
machine.ui.warn(I18n.t("vagrant_xapi.commands.not_xapi"))
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
Errors.wrap { yield machine }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A machine only used for its connection settings (templates, orphans).
|
|
29
|
+
def connection_machine
|
|
30
|
+
machine = nil
|
|
31
|
+
with_target_vms(nil, provider: target_provider) do |m|
|
|
32
|
+
machine ||= m if Extensions.provider?(m.provider_name)
|
|
33
|
+
end
|
|
34
|
+
raise Errors::ServiceFailed.new(:no_xapi_machine, {}) unless machine
|
|
35
|
+
|
|
36
|
+
machine
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Which of Extensions.provider_names to use: the one a machine of
|
|
40
|
+
# this project already runs with (Vagrant refuses another), else the
|
|
41
|
+
# first one the Vagrantfile configures, else :xapi.
|
|
42
|
+
def target_provider
|
|
43
|
+
active = @env.active_machines.map { |_name, provider| provider.to_sym }
|
|
44
|
+
names = Extensions.provider_names
|
|
45
|
+
configured = @env.vagrantfile.config.vm.__providers
|
|
46
|
+
(active & names).first || (configured & names).first || :xapi
|
|
47
|
+
rescue StandardError
|
|
48
|
+
:xapi
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sets a flag on Ctrl-C instead of killing the process.
|
|
52
|
+
def interruptible
|
|
53
|
+
interrupted = false
|
|
54
|
+
callback = -> { interrupted = true }
|
|
55
|
+
Vagrant::Util::Busy.busy(callback) { yield -> { interrupted } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def confirm(question, force)
|
|
59
|
+
return true if force
|
|
60
|
+
|
|
61
|
+
answer = @env.ui.ask("#{question} [y/N] ")
|
|
62
|
+
answer.to_s.strip.downcase.start_with?("y")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def t(key, **opts)
|
|
66
|
+
I18n.t("vagrant_xapi.commands.#{key}", **opts)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def human_size(bytes)
|
|
70
|
+
bytes = bytes.to_i
|
|
71
|
+
return "#{bytes} B" if bytes < 1024**2
|
|
72
|
+
return format("%.1f MiB", bytes / 1024.0**2) if bytes < 1024**3
|
|
73
|
+
|
|
74
|
+
format("%.1f GiB", bytes / 1024.0**3)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vagrant/util/platform"
|
|
4
|
+
require "vagrant/util/subprocess"
|
|
5
|
+
require "vagrant/util/which"
|
|
6
|
+
|
|
7
|
+
require_relative "base"
|
|
8
|
+
require_relative "../services/console"
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module Xapi
|
|
12
|
+
module Command
|
|
13
|
+
# vagrant xapi console [vm]: VNC console through the XAPI console
|
|
14
|
+
# tunnel on a local port. For when the VM boots but SSH does not work.
|
|
15
|
+
class Console < Base
|
|
16
|
+
def execute
|
|
17
|
+
options = { port: 0, open: true }
|
|
18
|
+
opts = OptionParser.new do |o|
|
|
19
|
+
o.banner = "Usage: vagrant xapi console [vm-name] [--port PORT] [--no-open]"
|
|
20
|
+
o.on("--port PORT", Integer, "Local port (default: a free one)") { |p| options[:port] = p }
|
|
21
|
+
o.on("--[no-]open", "Open the VNC viewer (default: yes)") { |v| options[:open] = v }
|
|
22
|
+
end
|
|
23
|
+
argv = parse_options(opts)
|
|
24
|
+
return 0 if argv.nil?
|
|
25
|
+
|
|
26
|
+
each_xapi_machine(argv, single: true) do |machine|
|
|
27
|
+
ctx = Glue.context(machine)
|
|
28
|
+
vm = Glue.vm_ref(machine, ctx)
|
|
29
|
+
raise Vagrant::Errors::VMNotCreatedError unless vm
|
|
30
|
+
|
|
31
|
+
password = console_password(machine)
|
|
32
|
+
console = Services::Console.new(ctx, password: password)
|
|
33
|
+
uri = console.location(vm)
|
|
34
|
+
server = console.listen(options[:port])
|
|
35
|
+
port = server.addr[1]
|
|
36
|
+
url = "vnc://127.0.0.1:#{port}"
|
|
37
|
+
if password
|
|
38
|
+
machine.ui.info(t("console_ready_password", url: url, password: password))
|
|
39
|
+
else
|
|
40
|
+
machine.ui.info(t("console_ready", url: url))
|
|
41
|
+
end
|
|
42
|
+
open_viewer(machine, viewer_url(url, password, port)) if options[:open]
|
|
43
|
+
interruptible do |stopped|
|
|
44
|
+
console.serve(server, uri, stop: stopped)
|
|
45
|
+
end
|
|
46
|
+
machine.ui.info(t("console_closed"))
|
|
47
|
+
end
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# A one-time VNC password, unless this Ruby/OpenSSL cannot do VNC
|
|
54
|
+
# authentication (no 3DES); then the console runs without one.
|
|
55
|
+
def console_password(machine)
|
|
56
|
+
return Services::Rfb.password if Services::Rfb.available?
|
|
57
|
+
|
|
58
|
+
machine.ui.warn(t("console_no_password"))
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# macOS Screen Sharing takes the password from the URL, and `open`
|
|
63
|
+
# exits at once. On Linux, xdg-open starts the viewer with the URL in
|
|
64
|
+
# its command line for the whole session, readable by every local
|
|
65
|
+
# user; there the viewer asks for the password the terminal shows.
|
|
66
|
+
def viewer_url(url, password, port)
|
|
67
|
+
return url unless password && Vagrant::Util::Platform.darwin?
|
|
68
|
+
|
|
69
|
+
"vnc://:#{password}@127.0.0.1:#{port}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def open_viewer(machine, url)
|
|
73
|
+
cmd = if Vagrant::Util::Platform.darwin?
|
|
74
|
+
"open"
|
|
75
|
+
elsif Vagrant::Util::Which.which("xdg-open")
|
|
76
|
+
"xdg-open"
|
|
77
|
+
end
|
|
78
|
+
if cmd
|
|
79
|
+
Vagrant::Util::Subprocess.execute(cmd, url)
|
|
80
|
+
else
|
|
81
|
+
machine.ui.detail(t("console_open_manually", url: url))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|