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