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,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "tmpdir"
6
+ require "yaml"
7
+ require "vagrant/action/builtin/cloud_init_setup"
8
+
9
+ require_relative "../errors"
10
+ require_relative "../glue"
11
+
12
+ module VagrantPlugins
13
+ module Xapi
14
+ module Action
15
+ # The built-in CloudInitSetup plus a `network-config` for a static IP on
16
+ # VIF device 0 (XAPI has no DHCP). Also builds the NoCloud ISO when there
17
+ # is only network config and no user-data, so boxes without a guest
18
+ # agent (e.g. libvirt boxes) still get their address.
19
+ #
20
+ # The ISO is attached by configure_disks as a :dvd disk named
21
+ # "vagrant-cloud_init-disk"; when its content changes, the VDI is
22
+ # replaced.
23
+ class CloudInitSetup < Vagrant::Action::Builtin::CloudInitSetup
24
+ ISO_SENTINEL = "xapi_cloud_init_iso"
25
+
26
+ def call(env)
27
+ catch(:complete) do
28
+ machine = env[:machine]
29
+ sentinel_path = machine.data_dir.join("action_cloud_init")
30
+ if sentinel_path.file?
31
+ if sentinel_path.read.chomp == machine.id.to_s
32
+ throw :complete if machine.config.vm.cloud_init_first_boot_only
33
+ end
34
+ sentinel_path.unlink
35
+ end
36
+
37
+ user_data_configs = machine.config.vm.cloud_init_configs.select { |c| c.type == :user_data }
38
+ network_config = Errors.wrap { build_network_config(env) }
39
+ throw :complete if user_data_configs.empty? && network_config.nil?
40
+
41
+ user_data = if user_data_configs.empty?
42
+ "#cloud-config\n{}\n"
43
+ else
44
+ setup_user_data(machine, env, user_data_configs).to_s
45
+ end
46
+ meta_data = { "instance-id" => "i-#{machine.id.split("-").join}" }
47
+ iso = write_iso(machine, env, user_data, meta_data, network_config)
48
+ attach_disk_config(machine, env, iso)
49
+ end
50
+
51
+ @app.call(env)
52
+ end
53
+
54
+ # cloud-init network-config v2, matched on MAC.
55
+ # @return [Hash, nil]
56
+ def build_network_config(env)
57
+ machine = env[:machine]
58
+ plan = Glue.vif_plan(machine)
59
+ first = plan.first
60
+ return nil unless first&.static_ip? && first.auto_config
61
+
62
+ client = env[:xapi] || Glue.client(machine)
63
+ ref = client.call("VM.get_by_uuid", machine.id)
64
+ macs = client.call("VM.get_VIFs", ref).to_h do |vif|
65
+ rec = client.call("VIF.get_record", vif)
66
+ [rec["device"].to_i, rec["MAC"].to_s.downcase]
67
+ end
68
+
69
+ ethernets = {}
70
+ plan.each do |spec|
71
+ mac = macs[spec.device]
72
+ next if mac.nil? || mac.empty?
73
+
74
+ name = "vif#{spec.device}"
75
+ if spec.device.zero?
76
+ entry = { "match" => { "macaddress" => mac }, "dhcp4" => false,
77
+ "addresses" => ["#{spec.ip}/#{spec.prefix_length}"] }
78
+ entry["routes"] = [{ "to" => "default", "via" => spec.gateway.to_s }] if spec.gateway
79
+ dns = Glue.networks(machine).find { |t, _| t == :public_network }&.last&.dig(:nameservers)
80
+ entry["nameservers"] = { "addresses" => Array(dns).map(&:to_s) } if dns
81
+ ethernets[name] = entry
82
+ elsif !spec.static_ip? && spec.kind != :internal
83
+ # cloud-init's fallback DHCP is off once network-config exists.
84
+ ethernets[name] = { "match" => { "macaddress" => mac }, "dhcp4" => true, "optional" => true }
85
+ end
86
+ end
87
+ { "version" => 2, "ethernets" => ethernets }
88
+ end
89
+
90
+ private
91
+
92
+ def write_iso(machine, env, user_data, meta_data, network_config)
93
+ raise Vagrant::Errors::CreateIsoHostCapNotFound unless env[:env].host.capability?(:create_iso)
94
+
95
+ files = { "user-data" => user_data, "meta-data" => meta_data.to_yaml }
96
+ files["network-config"] = network_config.to_yaml if network_config
97
+ checksum = Digest::SHA256.hexdigest(files.sort.map { |k, v| "#{k}\0#{v}" }.join("\0"))
98
+
99
+ sentinel = machine.data_dir.join(ISO_SENTINEL)
100
+ if sentinel.file?
101
+ sum, path = sentinel.read.chomp.split(":", 2)
102
+ return Pathname.new(path) if sum == checksum && File.file?(path.to_s)
103
+
104
+ FileUtils.rm_f(path.to_s) if path.to_s.include?("vagrant-xapi-cidata")
105
+ end
106
+
107
+ source_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX))
108
+ begin
109
+ files.each { |name, content| File.write(source_dir.join(name), content) }
110
+ dest = machine.data_dir.join("vagrant-xapi-cidata-#{checksum[0, 12]}.iso")
111
+ path = env[:env].host.capability(:create_iso, source_dir, volume_id: "cidata",
112
+ file_destination: dest.to_s)
113
+ sentinel.write("#{checksum}:#{path}")
114
+ Pathname.new(path.to_s)
115
+ ensure
116
+ FileUtils.remove_entry(source_dir)
117
+ end
118
+ end
119
+
120
+ def attach_disk_config(machine, _env, iso_path)
121
+ path = iso_path.to_s
122
+ return if machine.config.vm.disks.any? { |d| d.type == :dvd && d.file == path }
123
+
124
+ machine.config.vm.disk(:dvd, file: path, name: "vagrant-cloud_init-disk")
125
+ machine.config.vm.disks.each { |d| d.finalize! if d.type == :dvd && d.file == path }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vagrant/util/subprocess"
4
+ require "vagrant/util/which"
5
+
6
+ require_relative "base"
7
+ require_relative "../services/box_source"
8
+ require_relative "../services/template_cache"
9
+ require_relative "../services/vm_builder"
10
+ require_relative "../services/networks"
11
+ require_relative "../services/destroyer"
12
+ require_relative "../services/lookup"
13
+ require_relative "../services/disks"
14
+
15
+ module VagrantPlugins
16
+ module Xapi
17
+ module Action
18
+ # Makes sure the box exists as a template on template_sr
19
+ # (env[:xapi_template]).
20
+ class ImportTemplate < Base
21
+ def call(env)
22
+ machine = env[:machine]
23
+ box = machine.box
24
+ raise Vagrant::Errors::BoxNotFound, name: machine.config.vm.box.to_s if box.nil?
25
+
26
+ wrap do
27
+ c = ctx(env)
28
+ cfg = config(env)
29
+ source = Services::BoxSource.new(
30
+ name: box.name, version: box.version, provider: box.provider,
31
+ directory: box.directory, xva_url: cfg.xva_url || legacy_xva_url(machine, box)
32
+ )
33
+ warn_architecture(env, source)
34
+ sr = Services::Lookup.new(c).sr(cfg.template_sr || cfg.sr)
35
+ cache = Services::TemplateCache.new(
36
+ c,
37
+ digest_cache: Services::DigestCache.new(digest_path(machine)),
38
+ # Testing aid: forces a fresh template for the same box content.
39
+ salt: ENV["VAGRANT_XAPI_TEMPLATE_SALT"],
40
+ converter: Glue.method(:convert_qcow2),
41
+ # Longer than the lock's TTL, so a lock of a crashed process
42
+ # expires while we still wait and can be taken over.
43
+ lock_wait: (2 * cfg.task_timeout) + 1500,
44
+ task_timeout: cfg.task_timeout
45
+ )
46
+ env[:xapi_template] = cache.ensure_template(source, sr)
47
+ end
48
+ @app.call(env)
49
+ end
50
+
51
+ private
52
+
53
+ def digest_path(machine)
54
+ Glue.data_home(machine.env).join("digests.json")
55
+ end
56
+
57
+ # Boxes of vagrant-xenserver may only contain a Vagrantfile with
58
+ # `xs.xva_url = "..."` in a :xenserver provider block.
59
+ def legacy_xva_url(machine, box)
60
+ url = begin
61
+ cfg = machine.config.vm.get_provider_config(:xenserver)
62
+ cfg.instance_variable_get(:@xva_url)
63
+ rescue StandardError
64
+ nil
65
+ end
66
+ return url if url.is_a?(String) && !url.empty?
67
+
68
+ vagrantfile = box.directory.join("Vagrantfile")
69
+ return nil unless vagrantfile.file?
70
+
71
+ vagrantfile.read[/xva_url\s*=\s*["']([^"']+)["']/, 1]
72
+ end
73
+
74
+ def warn_architecture(env, source)
75
+ arch = source.architecture
76
+ return if arch.nil? || %w[amd64 x86_64].include?(arch.to_s)
77
+
78
+ env[:ui].warn(I18n.t("vagrant_xapi.messages.box_architecture", arch: arch))
79
+ end
80
+ end
81
+
82
+ # Clones (or copies) the template into a new VM, applies the config and
83
+ # creates the VIFs. All or nothing: a failure removes the VM again.
84
+ class CreateVm < Base
85
+ def call(env)
86
+ machine = env[:machine]
87
+ wrap do
88
+ c = ctx(env)
89
+ cfg = config(env)
90
+ lookup = Services::Lookup.new(c)
91
+ sr = cfg.clone_mode == :full ? lookup.sr(cfg.sr) : nil
92
+ vm = Services::VmBuilder.new(c).create(env[:xapi_template], Glue.vm_spec(machine),
93
+ clone_mode: cfg.clone_mode, sr_ref: sr)
94
+ networks = Services::Networks.new(c, project: Glue.project_path(machine))
95
+ begin
96
+ networks.apply(vm, Glue.vif_plan(machine))
97
+ Services::Disks.new(c).sync_isos(vm, cfg.isos)
98
+ rescue Exception # rubocop:disable Lint/RescueException
99
+ used = Services::Destroyer.new(c).destroy(vm, require_marking: false)
100
+ networks.cleanup_unused(used)
101
+ raise
102
+ end
103
+ machine.id = c.client.call("VM.get_uuid", vm)
104
+ end
105
+ @app.call(env)
106
+ end
107
+ end
108
+
109
+ # Re-applies CPU, memory, firmware, platform and VIFs to a halted VM
110
+ # (reload).
111
+ class ConfigureVm < Base
112
+ def call(env)
113
+ machine = env[:machine]
114
+ wrap do
115
+ c = ctx(env)
116
+ ref = vm(env)
117
+ env[:ui].info(I18n.t("vagrant_xapi.messages.applying_config"))
118
+ Services::VmBuilder.new(c).apply(ref, Glue.vm_spec(machine))
119
+ Services::Networks.new(c, project: Glue.project_path(machine)).apply(ref, Glue.vif_plan(machine))
120
+ Services::Disks.new(c).sync_isos(ref, config(env).isos)
121
+ end
122
+ @app.call(env)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ require_relative "base"
6
+ require_relative "network"
7
+ require_relative "../services/destroyer"
8
+ require_relative "../services/networks"
9
+
10
+ module VagrantPlugins
11
+ module Xapi
12
+ module Action
13
+ # Refuses to go on unless the VM is marked for this machine and project.
14
+ # Runs before anything halts or changes the VM.
15
+ class CheckOwner < Base
16
+ def call(env)
17
+ machine = env[:machine]
18
+ wrap do
19
+ ref = Glue.vm_ref(machine, ctx(env))
20
+ if ref
21
+ Services::Destroyer.new(ctx(env)).check_owner!(
22
+ ref, { machine: machine.name, project: Glue.project_path(machine) }
23
+ )
24
+ end
25
+ end
26
+ @app.call(env)
27
+ end
28
+ end
29
+
30
+ # Removes the VM, its snapshots, its own VDIs (never template VDIs),
31
+ # unused internal networks of the project, and our files in data_dir.
32
+ class DestroyVm < Base
33
+ OWN_FILES = %w[forwarded_ports.json tunnels.log disk_meta action_cloud_init
34
+ action_cloud_init_iso xapi_cloud_init_iso].freeze
35
+
36
+ def call(env)
37
+ machine = env[:machine]
38
+ Action.tunnels(machine).stop
39
+ wrap do
40
+ c = ctx(env)
41
+ ref = Glue.vm_ref(machine, c)
42
+ if ref
43
+ Extensions.before_destroy(machine, c, ref)
44
+ used = Services::Destroyer.new(c).destroy(
45
+ ref, owner: { machine: machine.name, project: Glue.project_path(machine) }
46
+ )
47
+ Services::Networks.new(c, project: Glue.project_path(machine)).cleanup_unused(used)
48
+ end
49
+ end
50
+ cleanup_files(machine)
51
+ machine.id = nil
52
+ @app.call(env)
53
+ end
54
+
55
+ private
56
+
57
+ def cleanup_files(machine)
58
+ iso_sentinel = machine.data_dir.join("xapi_cloud_init_iso")
59
+ if iso_sentinel.file?
60
+ path = iso_sentinel.read.split(":", 2).last.to_s
61
+ FileUtils.rm_f(path) if path.include?("vagrant-xapi-cidata")
62
+ end
63
+ OWN_FILES.each { |f| FileUtils.rm_f(machine.data_dir.join(f)) }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "vagrant/action/builtin/mixin_synced_folders"
5
+ require "vagrant/util/is_port_open"
6
+
7
+ require_relative "base"
8
+ require_relative "../services/tunnels"
9
+ require_relative "../services/inventory"
10
+
11
+ module VagrantPlugins
12
+ module Xapi
13
+ module Action
14
+ def self.tunnels(machine)
15
+ Services::Tunnels.new(machine.data_dir, logger: Glue::LOGGER)
16
+ end
17
+
18
+ # Starts one SSH tunnel per forwarded_port (after WaitForCommunicator).
19
+ class StartTunnels < Base
20
+ def call(env)
21
+ machine = env[:machine]
22
+ forwards = Glue.forwarded_ports(machine)
23
+ unless forwards.empty?
24
+ info = machine.ssh_info
25
+ raise Vagrant::Errors::SSHNotReady if info.nil?
26
+
27
+ announce(env, forwards)
28
+ wrap { Action.tunnels(machine).start(forwards, ssh_options(info)) }
29
+ end
30
+ @app.call(env)
31
+ end
32
+
33
+ private
34
+
35
+ def announce(env, forwards)
36
+ env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.forwarding"))
37
+ forwards.each do |f|
38
+ env[:ui].detail(I18n.t("vagrant_xapi.messages.forwarding_port",
39
+ host_ip: f.host_ip, host: f.host, guest: f.guest))
40
+ end
41
+ end
42
+
43
+ def ssh_options(info)
44
+ {
45
+ host: info[:host], port: info[:port], username: info[:username],
46
+ private_key_path: info[:private_key_path], proxy_command: info[:proxy_command]
47
+ }
48
+ end
49
+ end
50
+
51
+ # On `vagrant up` of a running (or paused) VM: bring the tunnels in line
52
+ # with the Vagrantfile. Tunnels that died (e.g. after the local machine
53
+ # rebooted) or whose forward changed are restarted, tunnels of removed
54
+ # forwards are stopped, and the others are left alone. A forward with
55
+ # auto_correct whose host port is unchanged keeps the port it was
56
+ # auto-corrected to at boot.
57
+ #
58
+ # Vagrant's collision check only runs at boot, so new host ports are
59
+ # checked here, before any tunnel is stopped: a taken port moves to a
60
+ # free one with auto_correct, and is Vagrant's collision error without.
61
+ class EnsureTunnels < StartTunnels
62
+ def call(env)
63
+ machine = env[:machine]
64
+ tunnels = Action.tunnels(machine)
65
+ plan = tunnels.plan(tunnels.with_saved_ports(Glue.forwarded_ports(machine)))
66
+ return @app.call(env) if plan.noop?
67
+
68
+ info = machine.ssh_info
69
+ if info.nil? && !plan.start.empty?
70
+ # No IP yet (e.g. the guest agent has not reported): stop what
71
+ # must go, which needs no SSH, but do not fail `vagrant up` of a
72
+ # running VM over the tunnels that cannot start yet.
73
+ wrap { tunnels.sync(Services::Tunnels::Plan.new(keep: plan.keep, stop: plan.stop, start: []), {}) }
74
+ env[:ui].warn(I18n.t("vagrant_xapi.messages.tunnels_need_ip"))
75
+ return @app.call(env)
76
+ end
77
+
78
+ plan.start = free_ports(env, plan)
79
+ announce(env, plan.start) unless plan.start.empty?
80
+ wrap { tunnels.sync(plan, info ? ssh_options(info) : {}) }
81
+ @app.call(env)
82
+ end
83
+
84
+ private
85
+
86
+ def free_ports(env, plan)
87
+ machine = env[:machine]
88
+ checker = env[:port_collision_port_check] || ->(ip, port) { port_taken?(machine, ip, port) }
89
+ # Ports of tunnels about to be stopped are ours, so free.
90
+ ours = plan.stop.map { |t| t["host"].to_i }
91
+ used = plan.keep.map { |t| t["host"].to_i } + plan.start.map(&:host)
92
+ plan.start.map do |fwd|
93
+ next fwd if ours.include?(fwd.host) || !checker.call(fwd.host_ip, fwd.host)
94
+
95
+ unless fwd.auto_correct
96
+ raise Vagrant::Errors::ForwardPortCollision, guest_port: fwd.guest.to_s, host_port: fwd.host.to_s
97
+ end
98
+
99
+ port = machine.config.vm.usable_port_range.find do |p|
100
+ !used.include?(p) && (ours.include?(p) || !checker.call(fwd.host_ip, p))
101
+ end
102
+ unless port
103
+ raise Vagrant::Errors::ForwardPortAutolistEmpty,
104
+ vm_name: machine.name, guest_port: fwd.guest.to_s, host_port: fwd.host.to_s
105
+ end
106
+
107
+ used << port
108
+ env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.fixed_collision",
109
+ host_port: fwd.host.to_s, guest_port: fwd.guest.to_s, new_port: port.to_s))
110
+ fwd.dup.tap { |f| f.host = port }
111
+ end
112
+ end
113
+
114
+ # The same check Vagrant's HandleForwardedPortCollisions does.
115
+ def port_taken?(machine, host_ip, port)
116
+ collisions = Vagrant::Action::Builtin::HandleForwardedPortCollisions
117
+ return collisions.port_check(machine, host_ip, port) if collisions.respond_to?(:port_check)
118
+
119
+ Vagrant::Util::IsPortOpen.is_port_open?(host_ip, port)
120
+ end
121
+ end
122
+
123
+ class StopTunnels < Base
124
+ def call(env)
125
+ Action.tunnels(env[:machine]).stop
126
+ @app.call(env)
127
+ end
128
+ end
129
+
130
+ # Prepares HandleForwardedPortCollisions at boot and resume. Runs after
131
+ # StopTunnels, so no port is held by a tunnel of this machine.
132
+ # Remembers each forward's host port from the Vagrantfile, because
133
+ # collision repair overwrites options[:host].
134
+ #
135
+ # Vagrant's default SSH forward (id "ssh", 22 => 2222) gets no tunnel
136
+ # unless forward_ssh_port is set, so it is marked disabled: it must not
137
+ # collide with anything on the local machine (that made `resume` fail
138
+ # when port 2222 was taken), and `vagrant port` should not show it.
139
+ class PrepareForwardedPortCollisionParams < Base
140
+ def call(env)
141
+ machine = env[:machine]
142
+ env[:port_collision_extra_in_use] = {}
143
+ machine.config.vm.networks.each do |type, options|
144
+ next unless type == :forwarded_port
145
+
146
+ options[:disabled] = true if options[:id].to_s == "ssh" && !machine.provider_config.forward_ssh_port
147
+ options[Glue::CONFIGURED_HOST] ||= options[:host]
148
+ end
149
+ @app.call(env)
150
+ end
151
+ end
152
+
153
+ # NFS exports of machines not in this list are pruned, so it must hold
154
+ # every live machine of this installation: VMs marked with our client
155
+ # id on this pool, plus the VM UUIDs of all xapi machines Vagrant knows
156
+ # (other pools, older markings).
157
+ #
158
+ # Building it reads every VM record of the pool, and only NFS uses it,
159
+ # so it is built only for a machine with NFS folders, now or at its
160
+ # last boot (Vagrant's cache). Without the list, Vagrant's NFS cleanup
161
+ # hook prunes nothing, as for any provider that gives none.
162
+ class PrepareNFSValidIds < Base
163
+ include Vagrant::Action::Builtin::MixinSyncedFolders
164
+
165
+ def call(env)
166
+ machine = env[:machine]
167
+ env[:nfs_valid_ids] = valid_ids(env, machine) if nfs?(machine)
168
+ @app.call(env)
169
+ end
170
+
171
+ private
172
+
173
+ def nfs?(machine)
174
+ synced_folders(machine).key?(:nfs) || synced_folders(machine, cached: true).key?(:nfs)
175
+ end
176
+
177
+ def valid_ids(env, machine)
178
+ ids = wrap do
179
+ Services::Inventory.new(ctx(env)).my_vms(client_id: Glue.client_id(machine.env)).map(&:uuid)
180
+ end
181
+ ids |= Glue.indexed_vm_ids(machine.env)
182
+ ids |= [machine.id] if machine.id
183
+ ids
184
+ end
185
+ end
186
+
187
+ # NFS: the host IP is the source address of the route to the guest,
188
+ # found with a UDP socket in Ruby (no `ip route` or `route get`).
189
+ class PrepareNFSSettings < Base
190
+ include Vagrant::Action::Builtin::MixinSyncedFolders
191
+
192
+ def call(env)
193
+ @app.call(env)
194
+
195
+ machine = env[:machine]
196
+ opts = {
197
+ cached: !!env[:synced_folders_cached],
198
+ config: env[:synced_folders_config],
199
+ disable_usable_check: !!env[:test]
200
+ }
201
+ folders = synced_folders(machine, **opts)
202
+ return unless folders.key?(:nfs)
203
+
204
+ info = machine.ssh_info
205
+ raise Vagrant::Errors::NFSNoGuestIP if info.nil? || info[:host].to_s.empty?
206
+
207
+ guest_ip = info[:host].to_s
208
+ env[:nfs_machine_ip] = guest_ip
209
+ env[:nfs_host_ip] = self.class.source_address_for(guest_ip)
210
+ raise Vagrant::Errors::NFSNoHostonlyNetwork if env[:nfs_host_ip].nil?
211
+ end
212
+
213
+ def self.source_address_for(ip)
214
+ family = ip.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
215
+ sock = UDPSocket.new(family)
216
+ begin
217
+ # UDP connect sends nothing; it only selects a route.
218
+ sock.connect(ip, 9)
219
+ sock.local_address.ip_address
220
+ ensure
221
+ sock.close
222
+ end
223
+ rescue SystemCallError, SocketError
224
+ nil
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "vagrant/action/general/package"
5
+ require "vagrant/action/general/package_setup_files"
6
+ require "vagrant/action/general/package_setup_folders"
7
+ require "vagrant/util/safe_chdir"
8
+ require "vagrant/util/subprocess"
9
+
10
+ require_relative "base"
11
+ require_relative "../services/diff_disks"
12
+ require_relative "../services/exporter"
13
+
14
+ module VagrantPlugins
15
+ module Xapi
16
+ module Action
17
+ class PackageSetupFolders < Vagrant::Action::General::PackageSetupFolders; end
18
+ class PackageSetupFiles < Vagrant::Action::General::PackageSetupFiles; end
19
+
20
+ # Vagrant's compress ignores the exit status of bsdtar, so a failure
21
+ # (e.g. a full disk) left a truncated box behind without an error, and
22
+ # only `vagrant box add` noticed. Same command, with the check.
23
+ class Package < Vagrant::Action::General::Package
24
+ def compress
25
+ output_path = fullpath.to_s
26
+ Vagrant::Util::SafeChdir.safe_chdir(@env["package.directory"]) do
27
+ files = Dir.glob(File.join(".", "*"))
28
+ result = Vagrant::Util::Subprocess.execute("bsdtar", "-czf", output_path, *files)
29
+ unless result.exit_code.zero?
30
+ FileUtils.rm_f(output_path)
31
+ raise Errors::ServiceFailed.new(:package_compress_failed, path: output_path,
32
+ detail: result.stderr.to_s.strip)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # Exports the halted VM into env["export.temp_dir"] as box.<format> plus
39
+ # metadata.json (provider "xapi", format, architecture "amd64").
40
+ class PackageExport < Base
41
+ def call(env)
42
+ machine = env[:machine]
43
+ if machine.state.id != :halted
44
+ raise Errors::ServiceFailed.new(:package_needs_halt, {})
45
+ end
46
+
47
+ wrap do
48
+ disks = Services::DiffDisks.new(ctx(env))
49
+ # A box must not take along the disks of an interrupted diff; one
50
+ # that cannot be removed stops the export. Under the diff lock of
51
+ # the VM, export included: a diff still running on it keeps its
52
+ # disks, and none is attached while the box is made.
53
+ disks.synchronize(vm(env), wait: Services::DiffDisks::LEFTOVER_LOCK_WAIT) do
54
+ disks.cleanup_leftovers(vm(env), unattached: false, required: true)
55
+ env[:xapi_package_metadata] = Services::Exporter.new(ctx(env)).export(
56
+ vm(env), env["export.temp_dir"], format: config(env).package_format
57
+ )
58
+ end
59
+ end
60
+ @app.call(env)
61
+ end
62
+ end
63
+
64
+ # Vagrantfile in the box with firmware, cpus and memory as provider
65
+ # defaults. --vagrantfile and --include are handled by Vagrant core.
66
+ class PackageVagrantfile < Base
67
+ def call(env)
68
+ values = wrap do
69
+ client = ctx(env).client
70
+ rec = client.call("VM.get_record", vm(env))
71
+ {
72
+ firmware: (rec["HVM_boot_params"] || {})["firmware"] || "bios",
73
+ cpus: rec["VCPUs_max"].to_i,
74
+ memory: rec["memory_static_max"].to_i / (1024 * 1024)
75
+ }
76
+ end
77
+ File.write(File.join(env["export.temp_dir"], "Vagrantfile"), render(values))
78
+ @app.call(env)
79
+ end
80
+
81
+ def render(values)
82
+ <<~RUBY
83
+ # Generated by vagrant-xapi #{VERSION} (vagrant package).
84
+ Vagrant.configure("2") do |config|
85
+ config.vm.box_architecture = "amd64"
86
+ config.vm.allowed_synced_folder_types = [:rsync, :nfs, :smb]
87
+ config.vm.provider :xapi do |xapi|
88
+ xapi.firmware = :#{values[:firmware] == "uefi" ? "uefi" : "bios"}
89
+ xapi.cpus = #{values[:cpus].to_i}
90
+ xapi.memory = #{values[:memory].to_i}
91
+ end
92
+ end
93
+
94
+ # Load the Vagrantfile given with `vagrant package --vagrantfile`
95
+ # after these defaults, so it can override them.
96
+ include_vagrantfile = File.expand_path("../include/_Vagrantfile", __FILE__)
97
+ load include_vagrantfile if File.exist?(include_vagrantfile)
98
+ RUBY
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end