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