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