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,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "log4r"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "vagrant/util/scoped_hash_override"
|
|
8
|
+
require "vagrant/util/subprocess"
|
|
9
|
+
require "vagrant/util/which"
|
|
10
|
+
|
|
11
|
+
require_relative "api/session_registry"
|
|
12
|
+
require_relative "errors"
|
|
13
|
+
require_relative "services/base"
|
|
14
|
+
require_relative "services/lookup"
|
|
15
|
+
require_relative "services/networks"
|
|
16
|
+
require_relative "services/ip_detector"
|
|
17
|
+
require_relative "services/vm_builder"
|
|
18
|
+
require_relative "services/tunnels"
|
|
19
|
+
|
|
20
|
+
module VagrantPlugins
|
|
21
|
+
module Xapi
|
|
22
|
+
# Translates between Vagrant (machine, config, ui) and the services.
|
|
23
|
+
# This is the only place besides actions/caps/commands that knows both.
|
|
24
|
+
module Glue
|
|
25
|
+
extend Vagrant::Util::ScopedHashOverride
|
|
26
|
+
|
|
27
|
+
# Under "vagrantplugins", so VAGRANT_LOG shows it (Vagrant only sets up
|
|
28
|
+
# the "vagrant" and "vagrantplugins" loggers). Lines appear as "xapi: ...".
|
|
29
|
+
LOGGER = Log4r::Logger.new("vagrantplugins::xapi")
|
|
30
|
+
|
|
31
|
+
# Maps service message keys to Vagrant UI output, with progress on one
|
|
32
|
+
# rewritten line.
|
|
33
|
+
class UiAdapter
|
|
34
|
+
def initialize(ui)
|
|
35
|
+
@ui = ui
|
|
36
|
+
@last = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def info(key, **params)
|
|
40
|
+
@ui&.info(t(key, **params))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detail(key, **params)
|
|
44
|
+
@ui&.detail(t(key, **params))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def warn(key, **params)
|
|
48
|
+
@ui&.warn(t(key, **params))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def progress(key, done, total, **params)
|
|
52
|
+
return unless @ui
|
|
53
|
+
|
|
54
|
+
pct = total.to_f.positive? ? (done.to_f / total * 100).floor : nil
|
|
55
|
+
text = pct ? "#{t(key, **params)} #{pct}%" : "#{t(key, **params)} #{format_bytes(done)}"
|
|
56
|
+
return if text == @last
|
|
57
|
+
|
|
58
|
+
@last = text
|
|
59
|
+
@ui.clear_line
|
|
60
|
+
@ui.detail(text, new_line: false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def clear_progress
|
|
64
|
+
return unless @ui && @last
|
|
65
|
+
|
|
66
|
+
@ui.clear_line
|
|
67
|
+
@last = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def t(key, **params)
|
|
73
|
+
Api::Redactor.scrub(I18n.t("vagrant_xapi.messages.#{key}", **params))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format_bytes(n)
|
|
77
|
+
return "#{n} B" if n < 1024
|
|
78
|
+
return format("%.1f MiB", n / 1024.0**2) if n < 1024**3
|
|
79
|
+
|
|
80
|
+
format("%.2f GiB", n / 1024.0**3)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
module_function
|
|
85
|
+
|
|
86
|
+
def client(machine)
|
|
87
|
+
cfg = machine.provider_config
|
|
88
|
+
missing = { host: cfg.host, username: cfg.username, password: cfg.password }
|
|
89
|
+
.select { |_k, v| v.nil? || v.to_s.empty? }.keys
|
|
90
|
+
raise Errors::ServiceFailed.new(:missing_credentials, options: missing.join(", ")) unless missing.empty?
|
|
91
|
+
|
|
92
|
+
Errors.wrap do
|
|
93
|
+
Api::SessionRegistry.fetch(
|
|
94
|
+
host: cfg.host, port: cfg.port, username: cfg.username, password: cfg.password,
|
|
95
|
+
tls: tls(cfg, machine.env.root_path), api_timeout: cfg.api_timeout, task_timeout: cfg.task_timeout,
|
|
96
|
+
logger: LOGGER
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# tls_ca_file is relative to the Vagrantfile's directory, like the
|
|
102
|
+
# check in Config#validate (Vagrant may run from a subdirectory).
|
|
103
|
+
def tls(cfg, root = nil)
|
|
104
|
+
{
|
|
105
|
+
verify: true,
|
|
106
|
+
ca_file: cfg.tls_ca_file ? File.expand_path(cfg.tls_ca_file.to_s, root ? root.to_s : Dir.pwd) : nil,
|
|
107
|
+
fingerprint: cfg.tls_fingerprint
|
|
108
|
+
}.compact
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Runs commands in a machine's guest for the services (as root, through
|
|
112
|
+
# Vagrant's communicator), see Services::GuestFiles.
|
|
113
|
+
class GuestShell
|
|
114
|
+
def initialize(machine)
|
|
115
|
+
@machine = machine
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return [Array(Integer, String, String)] exit status, stdout, stderr
|
|
119
|
+
def run(command)
|
|
120
|
+
out = +""
|
|
121
|
+
err = +""
|
|
122
|
+
status = @machine.communicate.execute(command, sudo: true, error_check: false) do |type, data|
|
|
123
|
+
(type == :stdout ? out : err) << data.to_s.b
|
|
124
|
+
end
|
|
125
|
+
[status.to_i, out, err]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def upload(local, remote)
|
|
129
|
+
@machine.communicate.upload(local, remote)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def download(remote, local)
|
|
133
|
+
@machine.communicate.download(remote, local)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @param env [Hash, nil] action env (for env[:interrupted] and env[:ui])
|
|
138
|
+
# @param cancel [#call, nil] Ctrl-C flag of a command
|
|
139
|
+
# @param ui [Vagrant::UI::Interface, nil] instead of the machine's UI
|
|
140
|
+
def context(machine, env = nil, client: nil, cancel: nil, ui: nil)
|
|
141
|
+
ui ||= (env && env[:ui]) || machine.ui
|
|
142
|
+
cancel ||= if env
|
|
143
|
+
-> { env[:interrupted] ? true : false }
|
|
144
|
+
else
|
|
145
|
+
Thread.current[:vagrant_xapi_cancel]
|
|
146
|
+
end
|
|
147
|
+
Services::Context.new(
|
|
148
|
+
client: client || self.client(machine),
|
|
149
|
+
ui: UiAdapter.new(ui),
|
|
150
|
+
cancel: cancel,
|
|
151
|
+
logger: LOGGER
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [String, nil] VM ref, nil when the machine is not created
|
|
156
|
+
def vm_ref(machine, ctx)
|
|
157
|
+
return nil if machine.id.nil?
|
|
158
|
+
|
|
159
|
+
Errors.wrap { Services::Lookup.new(ctx).vm_by_uuid(machine.id) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def project_path(machine)
|
|
163
|
+
machine.env.root_path.to_s
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def name_label(machine)
|
|
167
|
+
machine.provider_config.name_label ||
|
|
168
|
+
"#{File.basename(machine.env.root_path.to_s)}_#{machine.name}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def vm_spec(machine)
|
|
172
|
+
cfg = machine.provider_config
|
|
173
|
+
Services::VmSpec.new(
|
|
174
|
+
name_label: name_label(machine),
|
|
175
|
+
description: "Vagrant machine '#{machine.name}' of #{project_path(machine)} (vagrant-xapi)",
|
|
176
|
+
cpus: cfg.cpus,
|
|
177
|
+
cores_per_socket: cfg.cores_per_socket,
|
|
178
|
+
memory: cfg.memory,
|
|
179
|
+
memory_dynamic_min: cfg.memory_dynamic_min,
|
|
180
|
+
firmware: cfg.firmware,
|
|
181
|
+
# BIOS cannot boot securely: a VM switched from :uefi to :bios
|
|
182
|
+
# loses platform:secureboot. Without firmware the template decides.
|
|
183
|
+
secure_boot: { uefi: cfg.secure_boot, bios: false }[cfg.firmware],
|
|
184
|
+
vtpm: cfg.vtpm,
|
|
185
|
+
affinity_host: cfg.affinity_host,
|
|
186
|
+
tags: cfg.tags,
|
|
187
|
+
platform: cfg.platform,
|
|
188
|
+
other_config: cfg.other_config,
|
|
189
|
+
marking: Services::Marking.vm_marking(machine_name: machine.name, project: project_path(machine),
|
|
190
|
+
client_id: client_id(machine.env))
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Vagrant networks with xapi__ options applied.
|
|
195
|
+
def networks(machine)
|
|
196
|
+
machine.config.vm.networks.map do |type, opts|
|
|
197
|
+
[type, scoped_hash_override(opts || {}, :xapi)]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def himn?(machine)
|
|
202
|
+
cfg = machine.provider_config
|
|
203
|
+
cfg.ip_detection.include?(:himn) && cfg.ssh_via_host
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def vif_plan(machine)
|
|
207
|
+
Services::Networks.plan(networks(machine),
|
|
208
|
+
default_network: machine.provider_config.default_network,
|
|
209
|
+
himn: himn?(machine),
|
|
210
|
+
project: project_path(machine))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ip: of VIF device 0, if static
|
|
214
|
+
def static_ip(machine)
|
|
215
|
+
first = vif_plan(machine).first
|
|
216
|
+
first&.static_ip? ? first.ip.to_s : nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def ip_detector(machine, ctx)
|
|
220
|
+
Services::IpDetector.new(ctx, sources: machine.provider_config.ip_detection,
|
|
221
|
+
static_ip: static_ip(machine), himn_enabled: himn?(machine))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Option key under which PrepareForwardedPortCollisionParams keeps the
|
|
225
|
+
# Vagrantfile's host port, before HandleForwardedPortCollisions
|
|
226
|
+
# overwrites options[:host] with an auto-corrected one.
|
|
227
|
+
CONFIGURED_HOST = :xapi_configured_host
|
|
228
|
+
|
|
229
|
+
def forwarded_ports(machine)
|
|
230
|
+
cfg = machine.provider_config
|
|
231
|
+
machine.config.vm.networks.filter_map do |type, opts|
|
|
232
|
+
next unless type == :forwarded_port
|
|
233
|
+
next if opts[:id] == "ssh" && !cfg.forward_ssh_port
|
|
234
|
+
next if opts[:disabled]
|
|
235
|
+
|
|
236
|
+
Services::Tunnels::Forward.new(
|
|
237
|
+
id: opts[:id], host_ip: opts[:host_ip] || "127.0.0.1", host: opts[:host].to_i,
|
|
238
|
+
guest_ip: opts[:guest_ip] || "127.0.0.1", guest: opts[:guest].to_i,
|
|
239
|
+
configured_host: (opts[CONFIGURED_HOST] || opts[:host]).to_i, auto_correct: !!opts[:auto_correct]
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A random id for this Vagrant installation (machine and user), stored
|
|
245
|
+
# in the Vagrant home (~/.vagrant.d). Hostname and login name alone
|
|
246
|
+
# collide across containers, CI runners and machines with the same names.
|
|
247
|
+
CLIENT_ID_MUTEX = Mutex.new
|
|
248
|
+
|
|
249
|
+
def client_id(env)
|
|
250
|
+
path = data_home(env).join("client-id")
|
|
251
|
+
CLIENT_ID_MUTEX.synchronize do
|
|
252
|
+
existing = read_client_id(path)
|
|
253
|
+
return existing if existing
|
|
254
|
+
|
|
255
|
+
# Write a temp file and hard-link it into place: link fails when
|
|
256
|
+
# another process was first, and nobody ever sees a half file.
|
|
257
|
+
FileUtils.mkdir_p(path.dirname)
|
|
258
|
+
tmp = path.sub_ext(".#{Process.pid}.#{SecureRandom.hex(4)}.tmp")
|
|
259
|
+
File.write(tmp, SecureRandom.uuid)
|
|
260
|
+
begin
|
|
261
|
+
File.link(tmp, path)
|
|
262
|
+
rescue Errno::EEXIST
|
|
263
|
+
nil
|
|
264
|
+
ensure
|
|
265
|
+
FileUtils.rm_f(tmp)
|
|
266
|
+
end
|
|
267
|
+
read_client_id(path) || raise(Errors::ServiceFailed.new(:client_id_unreadable, path: path.to_s))
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def read_client_id(path)
|
|
272
|
+
return nil unless path.file?
|
|
273
|
+
|
|
274
|
+
id = path.read.strip
|
|
275
|
+
id.empty? ? nil : id
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def data_path(machine)
|
|
279
|
+
machine.data_dir
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# VM UUIDs of all machines of this plugin in Vagrant's machine index
|
|
283
|
+
# (any project, any dotfile path, any of Extensions.provider_names).
|
|
284
|
+
def indexed_vm_ids(env)
|
|
285
|
+
ids = []
|
|
286
|
+
env.machine_index.each do |entry|
|
|
287
|
+
id = indexed_vm_id(entry)
|
|
288
|
+
ids << id if id
|
|
289
|
+
end
|
|
290
|
+
ids
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
LOGGER.warn("could not read Vagrant's machine index: #{e.message}")
|
|
293
|
+
ids || []
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# The VM UUID of one index entry, or nil. An entry that cannot be read
|
|
297
|
+
# (e.g. during a parallel destroy) is skipped, not the ones after it:
|
|
298
|
+
# the caller keeps the NFS exports of every machine it gets.
|
|
299
|
+
def indexed_vm_id(entry)
|
|
300
|
+
return nil unless Extensions.provider?(entry.provider) && entry.local_data_path
|
|
301
|
+
|
|
302
|
+
id_file = Pathname.new(entry.local_data_path.to_s).join("machines", entry.name.to_s, entry.provider.to_s, "id")
|
|
303
|
+
id = id_file.file? ? id_file.read.strip : ""
|
|
304
|
+
id.empty? ? nil : id
|
|
305
|
+
rescue StandardError => e
|
|
306
|
+
LOGGER.warn("could not read the id of machine #{entry.name}: #{e.message}")
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Our directory in the Vagrant home (~/.vagrant.d/data/vagrant-xapi).
|
|
311
|
+
def data_home(env)
|
|
312
|
+
Pathname.new(env.home_path.to_s).join("data", "vagrant-xapi")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# qcow2 -> VHD with qemu-img (argv array, no shell), for hosts that
|
|
316
|
+
# cannot import qcow2.
|
|
317
|
+
def convert_qcow2(src, dst)
|
|
318
|
+
raise Services::Error.new(:qemu_img_missing) unless Vagrant::Util::Which.which("qemu-img")
|
|
319
|
+
|
|
320
|
+
result = Vagrant::Util::Subprocess.execute(
|
|
321
|
+
# -f qcow2: never let qemu-img guess the input format.
|
|
322
|
+
"qemu-img", "convert", "-f", "qcow2", "-O", "vpc", "-o", "subformat=dynamic", src.to_s, dst.to_s
|
|
323
|
+
)
|
|
324
|
+
return if result.exit_code.zero?
|
|
325
|
+
|
|
326
|
+
raise Services::Error.new(:qemu_img_failed, detail: result.stderr.to_s.strip[0, 500])
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Temporary: a stand-in for XenApi::Client, the client of the `xenapi` gem
|
|
4
|
+
# that vagrant-xenserver brought along, so triggers in Vagrantfiles written
|
|
5
|
+
# for it that `require "xenapi"` keep working for now (xenapi.rb, loaded
|
|
6
|
+
# through lib/xenapi.rb). It says so once per run when a trigger uses it.
|
|
7
|
+
#
|
|
8
|
+
# Only this directory, lib/xenapi.rb and locales/legacy.en.yml belong to it;
|
|
9
|
+
# docs/architecture.md section 17 lists what to delete to remove it. The
|
|
10
|
+
# compatibility with vagrant-xenserver itself (lib/vagrant-xapi/xenserver)
|
|
11
|
+
# is permanent and does not depend on it.
|
|
12
|
+
|
|
13
|
+
require "pathname"
|
|
14
|
+
|
|
15
|
+
require_relative "../extensions"
|
|
16
|
+
|
|
17
|
+
module VagrantPlugins
|
|
18
|
+
module Xapi
|
|
19
|
+
module Legacy
|
|
20
|
+
# Port, timeout and TLS settings per host, from the provider configs of
|
|
21
|
+
# this run, for XenApi::Client, which only gets a URI.
|
|
22
|
+
def self.connections
|
|
23
|
+
@connections ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.remember(config)
|
|
27
|
+
return if config.host.nil? || config.host.to_s.empty?
|
|
28
|
+
|
|
29
|
+
ca_file = config.tls_ca_file && File.expand_path(config.tls_ca_file.to_s, project_root)
|
|
30
|
+
connections[config.host.to_s] = {
|
|
31
|
+
port: config.port, api_timeout: config.api_timeout,
|
|
32
|
+
tls: { verify: true, ca_file: ca_file, fingerprint: config.tls_fingerprint }.compact
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The project root as Vagrant finds it (the directory of the
|
|
37
|
+
# Vagrantfile, from VAGRANT_CWD or upwards from the current one), so a
|
|
38
|
+
# relative tls_ca_file means the same as for the provider.
|
|
39
|
+
def self.project_root
|
|
40
|
+
name = ENV["VAGRANT_VAGRANTFILE"].to_s.empty? ? "Vagrantfile" : ENV["VAGRANT_VAGRANTFILE"]
|
|
41
|
+
dir = Pathname.new(File.expand_path(ENV["VAGRANT_CWD"].to_s.empty? ? Dir.pwd : ENV["VAGRANT_CWD"]))
|
|
42
|
+
dir.ascend { |d| return d.to_s if d.join(name).file? }
|
|
43
|
+
Dir.pwd
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Once per process, the first time a trigger creates a XenApi::Client.
|
|
47
|
+
# Straight to stderr: Kernel#warn prints nothing when Ruby runs without
|
|
48
|
+
# warnings ($VERBOSE nil), as Vagrant's installer does.
|
|
49
|
+
def self.xenapi_notice
|
|
50
|
+
return if @noticed
|
|
51
|
+
|
|
52
|
+
@noticed = true
|
|
53
|
+
$stderr.puts I18n.t("vagrant_xapi.legacy.xenapi_notice")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Extensions.on_config_finalized { |config| Legacy.remember(config) }
|
|
58
|
+
|
|
59
|
+
I18n.load_path << File.expand_path("../../../locales/legacy.en.yml", __dir__)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require_relative "../api/client"
|
|
6
|
+
require_relative "plugin"
|
|
7
|
+
|
|
8
|
+
# Temporary: a stand-in for the client of the `xenapi` gem that
|
|
9
|
+
# vagrant-xenserver brought along, so triggers in Vagrantfiles written for it
|
|
10
|
+
# keep working (see legacy/plugin.rb). It says once per run that it is
|
|
11
|
+
# temporary.
|
|
12
|
+
#
|
|
13
|
+
# xc = XenApi::Client.new("https://host:443/", 60)
|
|
14
|
+
# xc.login_with_password(user, password)
|
|
15
|
+
# xc.VM.get_VBDs(vm) # any XAPI call: xc.<Class>.<method>(...)
|
|
16
|
+
# xc.session.logout
|
|
17
|
+
#
|
|
18
|
+
#
|
|
19
|
+
# It runs on vagrant-xapi's own client: always HTTPS, with the port and TLS
|
|
20
|
+
# settings of the provider config for that host (or VAGRANT_XAPI_TLS_*),
|
|
21
|
+
# its own session, and HOST_IS_SLAVE handling. XAPI errors become
|
|
22
|
+
# XenApi::Errors::<CamelCaseCode> (all StandardError).
|
|
23
|
+
#
|
|
24
|
+
# Machine ids of vagrant-xapi are VM UUIDs, where vagrant-xenserver used the
|
|
25
|
+
# OpaqueRef. A UUID is passed on as it is, so XAPI answers HANDLE_INVALID to
|
|
26
|
+
# code that uses machine.id as a reference; trigger code written for
|
|
27
|
+
# vagrant-xenserver then falls back to looking the VM up by name. Turning the
|
|
28
|
+
# UUID into a ref here would not be enough: such code also puts it into
|
|
29
|
+
# records and compares it with refs XAPI returns.
|
|
30
|
+
module XenApi
|
|
31
|
+
module Errors
|
|
32
|
+
class GenericError < StandardError
|
|
33
|
+
attr_reader :code, :params
|
|
34
|
+
|
|
35
|
+
def initialize(code = "INTERNAL_ERROR", params = [])
|
|
36
|
+
@code = code.to_s
|
|
37
|
+
@params = Array(params)
|
|
38
|
+
super([@code, *@params].join(" "))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.class_for(code)
|
|
43
|
+
name = code.to_s.downcase.split(/[^a-z0-9]+/).map(&:capitalize).join
|
|
44
|
+
return GenericError unless name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
|
|
45
|
+
return const_get(name, false) if const_defined?(name, false)
|
|
46
|
+
|
|
47
|
+
const_set(name, Class.new(GenericError))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The ones code written for vagrant-xenserver may rescue by name.
|
|
51
|
+
class_for("SESSION_AUTHENTICATION_FAILED")
|
|
52
|
+
class_for("HANDLE_INVALID")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Client
|
|
56
|
+
# One namespace of calls: xc.VM, xc.pool, xc.session, ... A BasicObject,
|
|
57
|
+
# so that calls named like methods of Object (VM.clone, VM.hash) reach
|
|
58
|
+
# XAPI too.
|
|
59
|
+
class Namespace < BasicObject
|
|
60
|
+
def initialize(client, klass)
|
|
61
|
+
@client = client
|
|
62
|
+
@klass = klass
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_missing(name, *args)
|
|
66
|
+
@client.call("#{@klass}.#{name}", *args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def respond_to_missing?(*)
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def initialize(uri, timeout = 60, *_rest)
|
|
75
|
+
VagrantPlugins::Xapi::Legacy.xenapi_notice
|
|
76
|
+
@host = URI(uri.to_s).host
|
|
77
|
+
@settings = connection_settings(@host, timeout)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def login_with_password(username, password, *_rest)
|
|
81
|
+
@client = VagrantPlugins::Xapi::Api::Client.new(
|
|
82
|
+
host: @host, port: @settings[:port], username: username, password: password,
|
|
83
|
+
tls: @settings[:tls], api_timeout: @settings[:api_timeout]
|
|
84
|
+
)
|
|
85
|
+
translate_errors { @client.login.session_ref }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def call(method, *args)
|
|
89
|
+
raise Errors::GenericError.new("SESSION_INVALID", ["not logged in"]) unless @client
|
|
90
|
+
return logout if method == "session.logout"
|
|
91
|
+
|
|
92
|
+
translate_errors { @client.call(method, *args) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def logout
|
|
96
|
+
@client&.logout
|
|
97
|
+
""
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def method_missing(name, *args)
|
|
101
|
+
return super unless args.empty? && name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
102
|
+
|
|
103
|
+
Namespace.new(self, name.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def respond_to_missing?(name, include_private = false)
|
|
107
|
+
name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) || super
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def connection_settings(host, timeout)
|
|
113
|
+
known = VagrantPlugins::Xapi::Legacy.connections[host.to_s]
|
|
114
|
+
return known if known
|
|
115
|
+
|
|
116
|
+
env = ->(name) { ENV[name].to_s.empty? ? nil : ENV[name] }
|
|
117
|
+
ca_file = env.call("VAGRANT_XAPI_TLS_CA_FILE")
|
|
118
|
+
{ port: 443, api_timeout: timeout.to_i.positive? ? timeout.to_i : 60,
|
|
119
|
+
tls: { verify: true, ca_file: ca_file && File.expand_path(ca_file),
|
|
120
|
+
fingerprint: env.call("VAGRANT_XAPI_TLS_FINGERPRINT") }.compact }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def translate_errors
|
|
124
|
+
yield
|
|
125
|
+
rescue VagrantPlugins::Xapi::Api::XapiError => e
|
|
126
|
+
raise Errors.class_for(e.code).new(e.code, e.params)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "vagrant"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise "The vagrant-xapi plugin must be run within Vagrant."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "extensions"
|
|
10
|
+
|
|
11
|
+
module VagrantPlugins
|
|
12
|
+
module Xapi
|
|
13
|
+
# Registrations only; every component is loaded lazily in its block, so
|
|
14
|
+
# this file always loads.
|
|
15
|
+
class Plugin < Vagrant.plugin("2")
|
|
16
|
+
name "xapi"
|
|
17
|
+
description <<~DESC
|
|
18
|
+
Manages VMs on XenServer 8 and XCP-ng through XAPI.
|
|
19
|
+
DESC
|
|
20
|
+
|
|
21
|
+
# Only chosen when a Vagrantfile configures :xapi or --provider=xapi
|
|
22
|
+
# is given; it never becomes the default by accident.
|
|
23
|
+
provider(:xapi, parallel: true, defaultable: false, priority: 5,
|
|
24
|
+
box_format: %w[xapi xenserver libvirt]) do
|
|
25
|
+
require_relative "provider"
|
|
26
|
+
Provider
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
config(:xapi, :provider) do
|
|
30
|
+
require_relative "config"
|
|
31
|
+
Config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
CAPABILITIES = %i[snapshot_list forwarded_ports public_address nic_mac_addresses configure_disks
|
|
35
|
+
cleanup_disks validate_disk_ext default_disk_exts set_default_disk_ext].freeze
|
|
36
|
+
|
|
37
|
+
CAPABILITIES.each do |cap|
|
|
38
|
+
provider_capability(:xapi, cap) do
|
|
39
|
+
require_relative "cap"
|
|
40
|
+
Cap
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
command("xapi", primary: false) do
|
|
45
|
+
require_relative "command/root"
|
|
46
|
+
Command::Root
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Compatibility with Vagrantfiles of vagrant-xenserver (provider :xenserver).
|
|
53
|
+
require_relative "xenserver/plugin"
|
|
54
|
+
|
|
55
|
+
# Temporary: the XenApi::Client stand-in for triggers in those Vagrantfiles.
|
|
56
|
+
# To remove it, delete this line and lib/vagrant-xapi/legacy (see
|
|
57
|
+
# docs/architecture.md, section 17).
|
|
58
|
+
require_relative "legacy/plugin"
|