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,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
require_relative "base"
|
|
7
|
+
require_relative "../services/differ"
|
|
8
|
+
|
|
9
|
+
module VagrantPlugins
|
|
10
|
+
module Xapi
|
|
11
|
+
module Command
|
|
12
|
+
# vagrant xapi diff [vm] (--from SNAPSHOT [--to SNAPSHOT] | --with MACHINE)
|
|
13
|
+
#
|
|
14
|
+
# Which files differ between a snapshot and the running guest, two
|
|
15
|
+
# snapshots, or two running machines. Snapshots are made with
|
|
16
|
+
# `vagrant snapshot save`; the names are those of `vagrant snapshot list`.
|
|
17
|
+
class Diff < Base
|
|
18
|
+
LETTERS = { added: "A", removed: "D", changed: "M", metadata: "m", type: "T" }.freeze
|
|
19
|
+
|
|
20
|
+
# For --plain and --json: stdout carries only the result. Warnings go
|
|
21
|
+
# to stderr, progress is dropped.
|
|
22
|
+
class StderrUi
|
|
23
|
+
def initialize(ui)
|
|
24
|
+
@ui = ui
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def info(*); end
|
|
28
|
+
|
|
29
|
+
def detail(*); end
|
|
30
|
+
|
|
31
|
+
def clear_line; end
|
|
32
|
+
|
|
33
|
+
def warn(message, *_opts)
|
|
34
|
+
@ui.warn(message, channel: :error)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def execute
|
|
39
|
+
options = { paths: [], excludes: [], checksum: false, default_excludes: true, style: :table }
|
|
40
|
+
opts = OptionParser.new do |o|
|
|
41
|
+
o.banner = "Usage: vagrant xapi diff [vm-name] (--from SNAPSHOT [--to SNAPSHOT] | --with MACHINE) [options]"
|
|
42
|
+
o.separator ""
|
|
43
|
+
o.separator "Lists the files that were added, removed or changed. The machine must be running."
|
|
44
|
+
o.separator ""
|
|
45
|
+
o.on("--from SNAPSHOT", "Compare this snapshot with the running machine (or with --to)") do |v|
|
|
46
|
+
options[:from] = v
|
|
47
|
+
end
|
|
48
|
+
o.on("--to SNAPSHOT", "Compare with this snapshot instead of the running machine") { |v| options[:to] = v }
|
|
49
|
+
o.on("--with MACHINE", "Compare with another running machine of this Vagrantfile") do |v|
|
|
50
|
+
options[:with] = v
|
|
51
|
+
end
|
|
52
|
+
o.on("--path DIR", "Only paths below DIR (repeatable)") { |v| options[:paths] << v }
|
|
53
|
+
o.on("--exclude PATTERN",
|
|
54
|
+
"Leave out paths matching this glob, and what is below them; without a leading / at any depth " \
|
|
55
|
+
"(repeatable)") do |v|
|
|
56
|
+
options[:excludes] << v
|
|
57
|
+
end
|
|
58
|
+
o.on("--no-default-excludes",
|
|
59
|
+
"Also compare #{Services::FileDiff::DEFAULT_EXCLUDES.join(", ")}") do
|
|
60
|
+
options[:default_excludes] = false
|
|
61
|
+
end
|
|
62
|
+
o.on("--checksum", "Compare the content of every file, not only of files whose time changed") do
|
|
63
|
+
options[:checksum] = true
|
|
64
|
+
end
|
|
65
|
+
o.on("--content", "Also show what changed in each file (unified diff; not with --plain)") do
|
|
66
|
+
options[:content] = true
|
|
67
|
+
end
|
|
68
|
+
o.on("--content-limit SIZE", "With --content: largest file to show (default 1M; K and M suffixes)") do |v|
|
|
69
|
+
options[:content_limit] = Services::ContentDiff.parse_size(v) || :invalid
|
|
70
|
+
end
|
|
71
|
+
o.on("--plain", "One change per line, tab-separated (for scripts)") { options[:style] = :plain }
|
|
72
|
+
o.on("--json", "JSON") { options[:style] = :json }
|
|
73
|
+
end
|
|
74
|
+
argv = parse_options(opts)
|
|
75
|
+
return 0 if argv.nil?
|
|
76
|
+
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp unless valid?(argv, options)
|
|
77
|
+
|
|
78
|
+
each_xapi_machine(argv, single: true) do |machine|
|
|
79
|
+
print_result(machine, run(machine, options), options[:style])
|
|
80
|
+
end
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def valid?(argv, options)
|
|
87
|
+
return false if argv.size > 1
|
|
88
|
+
return false unless options[:paths].all? { |p| p.start_with?("/") }
|
|
89
|
+
# --plain is one change per line; a diff would break that.
|
|
90
|
+
return false if options[:content] && options[:style] == :plain
|
|
91
|
+
return false if options[:content_limit] && (!options[:content] || options[:content_limit] == :invalid)
|
|
92
|
+
|
|
93
|
+
if options[:with]
|
|
94
|
+
options[:from].nil? && options[:to].nil?
|
|
95
|
+
else
|
|
96
|
+
!options[:from].nil?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run(machine, options)
|
|
101
|
+
check_running!(machine)
|
|
102
|
+
other = options[:with] && other_machine(machine, options[:with])
|
|
103
|
+
excludes = (options[:default_excludes] ? Services::FileDiff::DEFAULT_EXCLUDES : []) + options[:excludes]
|
|
104
|
+
filter = Services::FileDiff::Filter.new(paths: options[:paths], excludes: excludes)
|
|
105
|
+
ui = options[:style] == :table ? nil : StderrUi.new(machine.ui)
|
|
106
|
+
content = options[:content] ? (options[:content_limit] || Services::ContentDiff::DEFAULT_LIMIT) : nil
|
|
107
|
+
@content_limit = content
|
|
108
|
+
interruptible do |stopped|
|
|
109
|
+
ctx = Glue.context(machine, cancel: stopped, ui: ui)
|
|
110
|
+
Dir.mktmpdir("vagrant-xapi-diff-") do |tmp|
|
|
111
|
+
differ = Services::Differ.new(ctx, tmpdir: tmp)
|
|
112
|
+
if other
|
|
113
|
+
# The other machine may be on another pool: its own context.
|
|
114
|
+
other_ctx = Glue.context(other, cancel: stopped, ui: ui)
|
|
115
|
+
differ.machines(Glue::GuestShell.new(machine), Glue::GuestShell.new(other),
|
|
116
|
+
vms: [[ctx, Glue.vm_ref(machine, ctx)], [other_ctx, Glue.vm_ref(other, other_ctx)]],
|
|
117
|
+
names: [machine.name.to_s, other.name.to_s], filter: filter,
|
|
118
|
+
checksum: options[:checksum], content: content)
|
|
119
|
+
else
|
|
120
|
+
differ.snapshot(Glue.vm_ref(machine, ctx), Glue::GuestShell.new(machine),
|
|
121
|
+
from: options[:from], to: options[:to], filter: filter,
|
|
122
|
+
checksum: options[:checksum], content: content)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The files are read through the guest, so it must be running and
|
|
129
|
+
# reachable.
|
|
130
|
+
def check_running!(machine)
|
|
131
|
+
raise Vagrant::Errors::VMNotCreatedError unless machine.id
|
|
132
|
+
unless machine.state.id == :running
|
|
133
|
+
raise Errors::ServiceFailed.new(:diff_needs_running, machine: machine.name.to_s)
|
|
134
|
+
end
|
|
135
|
+
return if machine.communicate.ready?
|
|
136
|
+
|
|
137
|
+
raise Errors::ServiceFailed.new(:diff_guest_unreachable, machine: machine.name.to_s)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def other_machine(machine, name)
|
|
141
|
+
other = nil
|
|
142
|
+
with_target_vms([name], single_target: true) { |m| other = m }
|
|
143
|
+
raise Errors::ServiceFailed.new(:diff_same_machine, machine: name.to_s) if other.name == machine.name
|
|
144
|
+
unless Extensions.provider?(other.provider_name)
|
|
145
|
+
raise Errors::ServiceFailed.new(:diff_other_provider, machine: other.name.to_s,
|
|
146
|
+
provider: other.provider_name.to_s)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
check_running!(other)
|
|
150
|
+
other
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def print_result(machine, result, style)
|
|
154
|
+
changes = result.changes
|
|
155
|
+
case style
|
|
156
|
+
when :json
|
|
157
|
+
json = { "compared" => result.mountpoints.map { |m| escape(m) },
|
|
158
|
+
"read_errors" => result.read_errors,
|
|
159
|
+
"changes" => changes.map { |c| as_json(c).merge(content_json(result, c)) } }
|
|
160
|
+
@env.ui.info(JSON.pretty_generate(json), prefix: false)
|
|
161
|
+
when :plain
|
|
162
|
+
return if changes.empty?
|
|
163
|
+
|
|
164
|
+
@env.ui.info(changes.map { |c| [LETTERS[c.kind], escape(c.path), detail(c)].join("\t") }.join("\n"),
|
|
165
|
+
prefix: false)
|
|
166
|
+
else
|
|
167
|
+
machine.ui.info(t("diff_compared", mountpoints: result.mountpoints.map { |m| escape(m) }.join(", ")))
|
|
168
|
+
if changes.empty?
|
|
169
|
+
machine.ui.info(t("diff_no_changes"))
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
lines = changes.map do |c|
|
|
173
|
+
d = detail(c)
|
|
174
|
+
"#{LETTERS[c.kind]} #{escape(c.path)}#{d.empty? ? "" : " (#{d})"}"
|
|
175
|
+
end
|
|
176
|
+
@env.ui.info(lines.join("\n"), prefix: false)
|
|
177
|
+
counts = LETTERS.keys.to_h { |kind| [kind, changes.count { |c| c.kind == kind }] }
|
|
178
|
+
machine.ui.info(t("diff_summary", **counts))
|
|
179
|
+
print_contents(result) if result.contents
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The diffs of --content, in the order of the list. They are escaped
|
|
184
|
+
# already (ContentDiff); a skipped file gets one line why.
|
|
185
|
+
def print_contents(result)
|
|
186
|
+
blocks = result.changes.filter_map do |change|
|
|
187
|
+
content = result.contents[change.path]
|
|
188
|
+
next unless content
|
|
189
|
+
|
|
190
|
+
content[:diff] || t("diff_content_#{content[:skipped]}",
|
|
191
|
+
path: escape(change.path), size: content[:size].to_s, limit: @content_limit.to_s,
|
|
192
|
+
budget: human_size(Services::ContentDiff.budget(@content_limit)))
|
|
193
|
+
end
|
|
194
|
+
@env.ui.info("\n#{blocks.join("\n")}", prefix: false) unless blocks.empty?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def content_json(result, change)
|
|
198
|
+
content = result.contents && result.contents[change.path]
|
|
199
|
+
return {} unless content
|
|
200
|
+
return { "diff" => content[:diff] } if content[:diff]
|
|
201
|
+
|
|
202
|
+
json = { "diff_skipped" => content[:skipped].to_s }
|
|
203
|
+
json["size"] = content[:size] if content[:size]
|
|
204
|
+
json
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def detail(change)
|
|
208
|
+
a = change.from
|
|
209
|
+
b = change.to
|
|
210
|
+
case change.kind
|
|
211
|
+
when :added, :removed
|
|
212
|
+
entry = a || b
|
|
213
|
+
entry.type == "f" ? "" : type_name(entry)
|
|
214
|
+
when :type
|
|
215
|
+
t("diff_type_change", from: type_name(a), to: type_name(b))
|
|
216
|
+
else
|
|
217
|
+
parts = []
|
|
218
|
+
if change.kind == :changed
|
|
219
|
+
parts << if a.type == "l"
|
|
220
|
+
t("diff_link", from: escape(a.target), to: escape(b.target))
|
|
221
|
+
elsif change.unreadable
|
|
222
|
+
t("diff_unreadable")
|
|
223
|
+
else
|
|
224
|
+
t("diff_content")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
parts << t("diff_mode", from: fd.mode(a), to: fd.mode(b)) if a.mode != b.mode
|
|
228
|
+
parts << t("diff_owner", from: fd.owner(a), to: fd.owner(b)) if fd.owner(a) != fd.owner(b)
|
|
229
|
+
parts.join(", ")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def as_json(change)
|
|
234
|
+
a = change.from
|
|
235
|
+
b = change.to
|
|
236
|
+
json = { "change" => change.kind.to_s, "path" => escape(change.path),
|
|
237
|
+
"type" => fd::TYPE_NAMES.fetch((b || a).type, "other") }
|
|
238
|
+
return json if a.nil? || b.nil?
|
|
239
|
+
|
|
240
|
+
if change.kind == :type
|
|
241
|
+
json["type"] = { "from" => fd::TYPE_NAMES.fetch(a.type, "other"),
|
|
242
|
+
"to" => fd::TYPE_NAMES.fetch(b.type, "other") }
|
|
243
|
+
return json
|
|
244
|
+
end
|
|
245
|
+
json["content"] = change.kind == :changed
|
|
246
|
+
json["unreadable"] = true if change.unreadable
|
|
247
|
+
json["target"] = { "from" => escape(a.target), "to" => escape(b.target) } if a.type == "l" && a.target != b.target
|
|
248
|
+
json["mode"] = { "from" => fd.mode(a), "to" => fd.mode(b) } if a.mode != b.mode
|
|
249
|
+
json["owner"] = { "from" => fd.owner(a), "to" => fd.owner(b) } if fd.owner(a) != fd.owner(b)
|
|
250
|
+
json
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def type_name(entry)
|
|
254
|
+
t("diff_types.#{fd::TYPE_NAMES.fetch(entry.type, "other")}")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def escape(value)
|
|
258
|
+
fd.escape(value)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def fd
|
|
262
|
+
Services::FileDiff
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "../services/hosts"
|
|
7
|
+
|
|
8
|
+
module VagrantPlugins
|
|
9
|
+
module Xapi
|
|
10
|
+
module Command
|
|
11
|
+
# vagrant xapi hosts [--plain|--json|--pem]
|
|
12
|
+
#
|
|
13
|
+
# The pool's hosts with address, role and the SHA-256 fingerprint of
|
|
14
|
+
# their TLS certificate, as the master reports them.
|
|
15
|
+
class Hosts < Base
|
|
16
|
+
def execute
|
|
17
|
+
style = :table
|
|
18
|
+
opts = OptionParser.new do |o|
|
|
19
|
+
o.banner = "Usage: vagrant xapi hosts [--plain|--json|--pem]"
|
|
20
|
+
o.separator ""
|
|
21
|
+
o.separator "The pool's hosts as the master reports them. The fingerprints and"
|
|
22
|
+
o.separator "certificates come through the verified connection to the master."
|
|
23
|
+
o.separator ""
|
|
24
|
+
o.on("--plain", "One host per line, tab-separated, no header: name, address, role,",
|
|
25
|
+
"enabled, fingerprint (for scripts)") { style = :plain }
|
|
26
|
+
o.on("--json", "JSON") { style = :json }
|
|
27
|
+
o.on("--pem", "The hosts' certificates in PEM, e.g. for tls_ca_file") { style = :pem }
|
|
28
|
+
end
|
|
29
|
+
argv = parse_options(opts)
|
|
30
|
+
return 0 if argv.nil?
|
|
31
|
+
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp unless argv.empty?
|
|
32
|
+
|
|
33
|
+
machine = connection_machine
|
|
34
|
+
hosts = Errors.wrap { Services::Hosts.new(Glue.context(machine)).list }
|
|
35
|
+
print_hosts(hosts, style)
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def print_hosts(hosts, style)
|
|
42
|
+
text = case style
|
|
43
|
+
when :json then JSON.pretty_generate(hosts.map(&:to_h))
|
|
44
|
+
when :pem then hosts.filter_map(&:certificate).map(&:strip).join("\n")
|
|
45
|
+
when :plain
|
|
46
|
+
hosts.map { |h| [h.name, h.address, h.role, h.enabled, h.fingerprint].join("\t") }.join("\n")
|
|
47
|
+
else table(hosts)
|
|
48
|
+
end
|
|
49
|
+
@env.ui.info(text, prefix: false) unless text.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def table(hosts)
|
|
53
|
+
lines = [format("%-20s %-15s %-6s %-7s %s", "NAME", "ADDRESS", "ROLE", "ENABLED", "FINGERPRINT (SHA-256)")]
|
|
54
|
+
hosts.each do |h|
|
|
55
|
+
lines << format("%-20s %-15s %-6s %-7s %s", h.name.to_s[0, 20], h.address, h.role,
|
|
56
|
+
h.enabled ? "yes" : "no", h.fingerprint || "-")
|
|
57
|
+
end
|
|
58
|
+
lines.join("\n")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../services/lookup"
|
|
5
|
+
require_relative "../services/snapshots"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Command
|
|
10
|
+
# vagrant xapi info [vm]
|
|
11
|
+
class Info < Base
|
|
12
|
+
def execute
|
|
13
|
+
opts = OptionParser.new do |o|
|
|
14
|
+
o.banner = "Usage: vagrant xapi info [vm-name]"
|
|
15
|
+
end
|
|
16
|
+
argv = parse_options(opts)
|
|
17
|
+
return 0 if argv.nil?
|
|
18
|
+
|
|
19
|
+
each_xapi_machine(argv) do |machine|
|
|
20
|
+
ctx = Glue.context(machine)
|
|
21
|
+
vm = Glue.vm_ref(machine, ctx)
|
|
22
|
+
unless vm
|
|
23
|
+
machine.ui.info(t("not_created"))
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
machine.ui.info(describe(ctx, vm).join("\n"))
|
|
27
|
+
end
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def describe(ctx, vm)
|
|
34
|
+
c = ctx.client
|
|
35
|
+
rec = c.call("VM.get_record", vm)
|
|
36
|
+
oc = rec["other_config"] || {}
|
|
37
|
+
lines = []
|
|
38
|
+
lines << "UUID: #{rec["uuid"]}"
|
|
39
|
+
lines << "Name: #{rec["name_label"]}"
|
|
40
|
+
lines << "State: #{rec["power_state"]}"
|
|
41
|
+
host = rec["resident_on"]
|
|
42
|
+
lines << "Host: #{c.call("host.get_name_label", host)}" unless null?(host)
|
|
43
|
+
affinity = rec["affinity"]
|
|
44
|
+
lines << "Affinity: #{c.call("host.get_name_label", affinity)}" unless null?(affinity)
|
|
45
|
+
lines << "CPUs: #{rec["VCPUs_max"]}"
|
|
46
|
+
lines << "Memory: #{rec["memory_static_max"].to_i / 1024 / 1024} MiB"
|
|
47
|
+
lines << "Firmware: #{(rec["HVM_boot_params"] || {})["firmware"] || "bios"}"
|
|
48
|
+
lines << "Template: #{template_name(c, oc)}"
|
|
49
|
+
|
|
50
|
+
lines << "Disks:"
|
|
51
|
+
vbds = rec["VBDs"].map { |vbd| c.call("VBD.get_record", vbd) }
|
|
52
|
+
vbds.sort_by { |b| b["userdevice"].to_i }.each do |b|
|
|
53
|
+
if null?(b["VDI"])
|
|
54
|
+
lines << " #{b["userdevice"]}: #{b["type"]} (empty)"
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
vdi = c.call("VDI.get_record", b["VDI"])
|
|
58
|
+
sr = c.call("SR.get_name_label", vdi["SR"])
|
|
59
|
+
lines << " #{b["userdevice"]}: #{b["type"]} #{vdi["name_label"]} " \
|
|
60
|
+
"#{human_size(vdi["virtual_size"])} on #{sr} (#{vdi["uuid"]})"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
metrics = rec["guest_metrics"]
|
|
64
|
+
nets = null?(metrics) ? {} : (c.call("VM_guest_metrics.get_networks", metrics) rescue {}) # rubocop:disable Style/RescueModifier
|
|
65
|
+
lines << "VIFs:"
|
|
66
|
+
rec["VIFs"].map { |v| c.call("VIF.get_record", v) }.sort_by { |v| v["device"].to_i }.each do |v|
|
|
67
|
+
dev = v["device"]
|
|
68
|
+
ips = nets.select { |k, _| k.start_with?("#{dev}/") }.values.uniq
|
|
69
|
+
net = c.call("network.get_name_label", v["network"])
|
|
70
|
+
lines << " #{dev}: #{v["MAC"]} on #{net}#{ips.empty? ? "" : " #{ips.join(", ")}"}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
snaps = Services::Snapshots.new(ctx).list(vm)
|
|
74
|
+
lines << "Snapshots: #{snaps.empty? ? "(none)" : snaps.join(", ")}"
|
|
75
|
+
lines
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def template_name(client, oc)
|
|
79
|
+
uuid = oc[Services::Marking::TEMPLATE_UUID]
|
|
80
|
+
return "(unknown)" if uuid.to_s.empty?
|
|
81
|
+
|
|
82
|
+
ref = client.call("VM.get_by_uuid", uuid)
|
|
83
|
+
"#{client.call("VM.get_name_label", ref)} (#{uuid})"
|
|
84
|
+
rescue Api::XapiError
|
|
85
|
+
"#{uuid} (removed)"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def null?(ref)
|
|
89
|
+
Services::Helpers.null_ref?(ref)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require_relative "base"
|
|
7
|
+
require_relative "../services/inventory"
|
|
8
|
+
|
|
9
|
+
module VagrantPlugins
|
|
10
|
+
module Xapi
|
|
11
|
+
module Command
|
|
12
|
+
# vagrant xapi marked [--mine] [--project DIR] [--vm UUID] [--box NAME] [--plain|--json]
|
|
13
|
+
#
|
|
14
|
+
# Lists the objects in the pool that carry the plugin's marking, through
|
|
15
|
+
# XAPI with the normal connection settings (no dom0 access needed).
|
|
16
|
+
class Marked < Base
|
|
17
|
+
def execute
|
|
18
|
+
options = { projects: [], vm_uuids: [], boxes: [], mine: false, style: :table }
|
|
19
|
+
opts = OptionParser.new do |o|
|
|
20
|
+
o.banner = "Usage: vagrant xapi marked [--mine] [--project DIR] [--vm UUID] [--box NAME] [--plain|--json]"
|
|
21
|
+
o.separator ""
|
|
22
|
+
o.separator "Without filters: every marked object in the pool. Filters can be repeated;"
|
|
23
|
+
o.separator "an object is listed when it belongs to any of them."
|
|
24
|
+
o.separator ""
|
|
25
|
+
o.on("--mine", "VMs of this Vagrant installation, their snapshots, disks and internal networks") do
|
|
26
|
+
options[:mine] = true
|
|
27
|
+
end
|
|
28
|
+
o.on("--project DIR", "VMs, snapshots and internal networks of this project directory") do |d|
|
|
29
|
+
options[:projects].concat(project_paths(d))
|
|
30
|
+
end
|
|
31
|
+
o.on("--vm UUID", "This VM, its snapshots and the disks it owns") { |u| options[:vm_uuids] << u }
|
|
32
|
+
o.on("--box NAME", "Templates of this box") { |b| options[:boxes] << b }
|
|
33
|
+
o.on("--plain", "One object per line, tab-separated, no header (for scripts)") { options[:style] = :plain }
|
|
34
|
+
o.on("--json", "JSON") { options[:style] = :json }
|
|
35
|
+
end
|
|
36
|
+
argv = parse_options(opts)
|
|
37
|
+
return 0 if argv.nil?
|
|
38
|
+
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp unless argv.empty?
|
|
39
|
+
|
|
40
|
+
machine = connection_machine
|
|
41
|
+
objects = Errors.wrap do
|
|
42
|
+
Services::Inventory.new(Glue.context(machine))
|
|
43
|
+
.marked(projects: options[:projects], vm_uuids: options[:vm_uuids],
|
|
44
|
+
boxes: options[:boxes],
|
|
45
|
+
mine: options[:mine] ? Glue.client_id(@env) : nil)
|
|
46
|
+
end
|
|
47
|
+
print_objects(objects, options[:style])
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Vagrant marks the physical path (e.g. /private/var/... on macOS, where
|
|
54
|
+
# /var is a symlink); match both that and the path as given.
|
|
55
|
+
def project_paths(dir)
|
|
56
|
+
given = File.expand_path(dir)
|
|
57
|
+
real = File.exist?(given) ? File.realpath(given) : given
|
|
58
|
+
[given, real].uniq
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def print_objects(objects, style)
|
|
62
|
+
case style
|
|
63
|
+
when :json
|
|
64
|
+
@env.ui.info(JSON.pretty_generate(objects.map(&:to_h)), prefix: false)
|
|
65
|
+
when :plain
|
|
66
|
+
return if objects.empty?
|
|
67
|
+
|
|
68
|
+
@env.ui.info(objects.map { |m| [m.kind, m.uuid, m.name, m.detail].join("\t") }.join("\n"),
|
|
69
|
+
prefix: false)
|
|
70
|
+
else
|
|
71
|
+
if objects.empty?
|
|
72
|
+
@env.ui.info(t("nothing_marked"))
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
lines = [format("%-9s %-36s %-40s %s", "KIND", "UUID", "NAME", "DETAIL")]
|
|
76
|
+
objects.each do |m|
|
|
77
|
+
lines << format("%-9s %-36s %-40s %s", m.kind, m.uuid, m.name.to_s[0, 40], m.detail)
|
|
78
|
+
end
|
|
79
|
+
@env.ui.info(lines.join("\n"), prefix: false)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "../services/inventory"
|
|
7
|
+
require_relative "../services/destroyer"
|
|
8
|
+
require_relative "../services/networks"
|
|
9
|
+
require_relative "../services/power"
|
|
10
|
+
|
|
11
|
+
module VagrantPlugins
|
|
12
|
+
module Xapi
|
|
13
|
+
module Command
|
|
14
|
+
# vagrant xapi orphans [--destroy]: VMs of this Vagrant installation
|
|
15
|
+
# whose Vagrant machine no longer exists locally.
|
|
16
|
+
class Orphans < Base
|
|
17
|
+
def execute
|
|
18
|
+
options = { destroy: false, force: false }
|
|
19
|
+
opts = OptionParser.new do |o|
|
|
20
|
+
o.banner = "Usage: vagrant xapi orphans [--destroy] [--force]"
|
|
21
|
+
o.on("--destroy", "Destroy the orphaned VMs") { options[:destroy] = true }
|
|
22
|
+
o.on("-f", "--force", "Do not ask for confirmation") { options[:force] = true }
|
|
23
|
+
end
|
|
24
|
+
argv = parse_options(opts)
|
|
25
|
+
return 0 if argv.nil?
|
|
26
|
+
|
|
27
|
+
machine = connection_machine
|
|
28
|
+
Errors.wrap do
|
|
29
|
+
ctx = Glue.context(machine)
|
|
30
|
+
orphans = Services::Inventory.new(ctx).my_vms(client_id: Glue.client_id(@env))
|
|
31
|
+
.reject { |vm| machine_exists?(vm) }
|
|
32
|
+
if orphans.empty?
|
|
33
|
+
@env.ui.info(t("no_orphans"))
|
|
34
|
+
return 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
orphans.each do |vm|
|
|
38
|
+
@env.ui.info(t("orphan", name: vm.name, uuid: vm.uuid, machine: vm.machine,
|
|
39
|
+
project: vm.project, state: vm.power_state))
|
|
40
|
+
end
|
|
41
|
+
return options[:destroy] ? destroy(ctx, orphans, options[:force]) : 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# The machine still exists when some Vagrant machine (from Vagrant's
|
|
48
|
+
# machine index, which knows custom VAGRANT_DOTFILE_PATHs) or the
|
|
49
|
+
# project's default .vagrant directory has an id file with this
|
|
50
|
+
# VM's UUID.
|
|
51
|
+
def machine_exists?(vm)
|
|
52
|
+
return false if vm.machine.to_s.empty?
|
|
53
|
+
|
|
54
|
+
data_dirs = []
|
|
55
|
+
@env.machine_index.each do |entry|
|
|
56
|
+
next unless Extensions.provider?(entry.provider) && entry.name.to_s == vm.machine.to_s
|
|
57
|
+
next unless entry.local_data_path
|
|
58
|
+
|
|
59
|
+
data_dirs << Pathname.new(entry.local_data_path.to_s)
|
|
60
|
+
end
|
|
61
|
+
data_dirs << Pathname.new(vm.project).join(".vagrant") unless vm.project.to_s.empty?
|
|
62
|
+
data_dirs.uniq.product(Extensions.provider_names).any? do |dir, provider|
|
|
63
|
+
id_file = dir.join("machines", vm.machine, provider.to_s, "id")
|
|
64
|
+
id_file.file? && id_file.read.strip == vm.uuid
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A VM that cannot be shut down or removed (another operation on it,
|
|
69
|
+
# a lost connection) is reported and the others still go; the
|
|
70
|
+
# command then exits with 1.
|
|
71
|
+
# @return [Integer] exit status
|
|
72
|
+
def destroy(ctx, orphans, force)
|
|
73
|
+
return 0 unless confirm(t("confirm_orphans_destroy", count: orphans.size), force)
|
|
74
|
+
|
|
75
|
+
failed = orphans.count { |vm| !destroy_one(ctx, vm) }
|
|
76
|
+
failed.zero? ? 0 : 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def destroy_one(ctx, vm)
|
|
80
|
+
# As `vagrant destroy`: a task, so a slow shutdown is no API
|
|
81
|
+
# timeout, and only when the VM is not already halted.
|
|
82
|
+
Services::Power.new(ctx).hard_shutdown(vm.ref)
|
|
83
|
+
used = Services::Destroyer.new(ctx).destroy(vm.ref)
|
|
84
|
+
Services::Networks.new(ctx, project: vm.project).cleanup_unused(used)
|
|
85
|
+
@env.ui.success(t("orphan_destroyed", name: vm.name, uuid: vm.uuid))
|
|
86
|
+
true
|
|
87
|
+
rescue Api::TaskCancelled
|
|
88
|
+
raise
|
|
89
|
+
rescue Api::Error, Services::Error => e
|
|
90
|
+
@env.ui.error(t("orphan_destroy_failed", name: vm.name, uuid: vm.uuid, reason: Errors.translate(e).message))
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "../extensions"
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module Xapi
|
|
9
|
+
module Command
|
|
10
|
+
# vagrant xapi <subcommand>: what Vagrant core does not cover.
|
|
11
|
+
class Root < Vagrant.plugin("2", :command)
|
|
12
|
+
SUBCOMMANDS = {
|
|
13
|
+
info: ["info", "Info", "UUID, host, SR, template, VIFs with IPs, snapshots"],
|
|
14
|
+
console: ["console", "Console", "VNC console through the XAPI console tunnel"],
|
|
15
|
+
adopt: ["adopt", "Adopt", "attach an existing VM to this machine"],
|
|
16
|
+
templates: ["templates", "Templates", "list, prune or remove box templates"],
|
|
17
|
+
orphans: ["orphans", "Orphans", "VMs of this Vagrant installation whose machine is gone"],
|
|
18
|
+
marked: ["marked", "Marked", "everything in the pool that vagrant-xapi created"],
|
|
19
|
+
diff: ["diff", "Diff", "files that differ from a snapshot or another machine"],
|
|
20
|
+
hosts: ["hosts", "Hosts", "the pool's hosts with address, role and TLS fingerprint"]
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def self.synopsis
|
|
24
|
+
"manages XAPI-specific things (vagrant-xapi)"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute
|
|
28
|
+
main_args, sub_command, sub_args = split_main_and_subcommand(@argv)
|
|
29
|
+
if main_args.include?("-h") || main_args.include?("--help") || sub_command.nil?
|
|
30
|
+
return help
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
entry = subcommands[sub_command.to_sym]
|
|
34
|
+
return help unless entry
|
|
35
|
+
|
|
36
|
+
require_relative entry[0]
|
|
37
|
+
Command.const_get(entry[1]).new(sub_args, @env).execute
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def subcommands
|
|
41
|
+
SUBCOMMANDS.merge(Extensions.subcommands)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def help
|
|
45
|
+
lines = ["Usage: vagrant xapi <subcommand> [<args>]", "", "Subcommands:"]
|
|
46
|
+
subcommands.each { |name, (_file, _klass, desc)| lines << format(" %-10s %s", name, desc) }
|
|
47
|
+
lines << ""
|
|
48
|
+
lines << "For help on a subcommand run `vagrant xapi <subcommand> -h`."
|
|
49
|
+
@env.ui.info(lines.join("\n"), prefix: false)
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|