vagrant-qemu 0.4.0 → 0.5.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 +22 -0
- data/README.md +1 -1
- data/lib/vagrant-qemu/action/export.rb +74 -0
- data/lib/vagrant-qemu/action/package_vagrantfile.rb +37 -0
- data/lib/vagrant-qemu/action.rb +17 -2
- data/lib/vagrant-qemu/driver.rb +55 -17
- data/lib/vagrant-qemu/version.rb +1 -1
- data/locales/en.yml +2 -0
- data/spec/e2e/halt_spec.rb +48 -0
- data/spec/e2e/helper.rb +12 -0
- data/spec/e2e/package_spec.rb +95 -0
- data/spec/unit/action/export_spec.rb +103 -0
- data/spec/unit/action/package_spec.rb +22 -0
- data/spec/unit/action/package_vagrantfile_spec.rb +41 -0
- data/spec/unit/driver/package_spec.rb +51 -0
- data/spec/unit/driver/stop_spec.rb +35 -28
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0968c034faf465b2d51e1f7b785a7a1aed39a4d6c40ef96f6b34147e9ab8ebd
|
|
4
|
+
data.tar.gz: fff8fc272c6c99abb85ccff761137f53ee6aabe8953f4b702ffca61163ea8d27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba77c0b50cd06921799dcc154f8d987a3fe1e367fd4347bbd2a45381f0819815e9f832f29be04a76818fb5e5c24abb4f009fb33350aace09486f1a20f6250012
|
|
7
|
+
data.tar.gz: 6438ed9c811e25f7393e59971028eea43b5f3d76ac9a11982dd2424c8b7cbd54181cbaa514875aebbb54f38520fcab12575929e5c62259f42c37cce83274c34a
|
data/CHANGELOG.md
CHANGED
|
@@ -101,6 +101,14 @@
|
|
|
101
101
|
* Add support for cloud-init and disks
|
|
102
102
|
* Add support for resizing disk on vm setup
|
|
103
103
|
|
|
104
|
+
# 0.4.1 (2026-06-22)
|
|
105
|
+
|
|
106
|
+
* `vagrant halt` now reaps QEMU even when the guest was already halted from
|
|
107
|
+
inside (e.g. `sudo systemctl halt`), where the ACPI `system_powerdown` is a
|
|
108
|
+
no-op. Halt escalates: `system_powerdown` -> wait `graceful_timeout` -> QEMU
|
|
109
|
+
`quit` monitor command (clean: flushes and closes the disk images) -> wait
|
|
110
|
+
`graceful_timeout` -> SIGKILL as a last resort (#79)
|
|
111
|
+
|
|
104
112
|
# 0.4.0 (2026-06-12)
|
|
105
113
|
|
|
106
114
|
* Advanced networking (opt-in, `advanced_network = true`): dual-NIC `private_network`
|
|
@@ -132,3 +140,17 @@
|
|
|
132
140
|
* Warn when private_network is configured without `advanced_network`, when the
|
|
133
141
|
network backend needs sudo, and when other unsupported network types are used
|
|
134
142
|
* Add test suite: unit + acceptance + e2e (`rake spec:unit|acceptance|e2e`)
|
|
143
|
+
|
|
144
|
+
# 0.5.0 (2026-07-11)
|
|
145
|
+
|
|
146
|
+
* `vagrant package` is now supported: export a running QEMU VM into a
|
|
147
|
+
distributable libvirt-format box. The box disk is flattened from its qcow2
|
|
148
|
+
overlay into a standalone qcow2 with `qemu-img convert` (the live VM disk is
|
|
149
|
+
never modified). A single-disk VM produces `box.img`; a multi-disk VM
|
|
150
|
+
produces `box_1.img`..`box_N.img` plus a `disks[]` entry in `metadata.json`.
|
|
151
|
+
`metadata.json` is written with `provider: libvirt` / `format: qcow2` /
|
|
152
|
+
`virtual_size`, and a minimal default Vagrantfile declaring the box `arch` is
|
|
153
|
+
bundled. `--output`/`--include`/`--info`/`--vagrantfile` are handled by the
|
|
154
|
+
core packaging middleware. Environment-specific state — networking, MAC,
|
|
155
|
+
additional disks, DVDs/cloud-init seed ISOs, and firmware — is intentionally
|
|
156
|
+
not baked into the box.
|
data/README.md
CHANGED
|
@@ -105,7 +105,7 @@ This provider exposes a few provider-specific configuration options:
|
|
|
105
105
|
* `firmware_format` - The format of aarch64 firmware images (`edk2-aarch64-code.fd` and `edk2-arm-vars.fd`) loaded from `qemu_dir`, default: `raw`
|
|
106
106
|
* `other_default` - The other default arguments used by this plugin, default: `%W(-parallel null -monitor none -display none -vga none)`
|
|
107
107
|
* `extra_image_opts` - Options passed via `-o` to `qemu-img` when the base qcow2 images are created, default: `[]`
|
|
108
|
-
* `graceful_timeout` - Seconds to wait
|
|
108
|
+
* `graceful_timeout` - Seconds to wait at each `vagrant halt` stage before escalating, default: `60`. Halt sends ACPI `system_powerdown`, waits up to this long, then sends QEMU's `quit` monitor command (a clean shutdown that flushes and closes the disk images), waits again, and finally SIGKILLs QEMU as a last resort — so halt still completes even when the guest was already halted from inside (e.g. `sudo systemctl halt`), where `system_powerdown` is a no-op
|
|
109
109
|
* advanced networking (requires `advanced_network = true`)
|
|
110
110
|
* `advanced_network` - Enable dual-NIC advanced networking with `private_network` support, default: `false`
|
|
111
111
|
* `net_mode` - Network backend: `:auto` (detect by platform), `:vmnet_shared`, `:vmnet_host`, `:vmnet_bridged` (macOS), `:tap` (Linux), `:socket` (QEMU `socket` netdev — multicast or point-to-point, see `socket_opts`), default: `:auto`
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "log4r"
|
|
5
|
+
|
|
6
|
+
module VagrantPlugins
|
|
7
|
+
module QEMU
|
|
8
|
+
module Action
|
|
9
|
+
# Produces the box's disk artifacts and metadata.json in export.temp_dir:
|
|
10
|
+
# flattens each box-disk overlay into a standalone qcow2 and describes them
|
|
11
|
+
# in a libvirt-format metadata.json. Only the VM's box disks are packaged;
|
|
12
|
+
# additional disks, DVDs and cloud-init seed ISOs are left out.
|
|
13
|
+
class Export
|
|
14
|
+
def initialize(app, env)
|
|
15
|
+
@app = app
|
|
16
|
+
@logger = Log4r::Logger.new("vagrant_qemu::action::export")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(env)
|
|
20
|
+
@env = env
|
|
21
|
+
|
|
22
|
+
if env[:machine].state.id != :stopped
|
|
23
|
+
raise Vagrant::Errors::VMPowerOffToPackage
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
export
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def export
|
|
31
|
+
driver = @env[:machine].provider.driver
|
|
32
|
+
temp_dir = Pathname.new(@env["export.temp_dir"])
|
|
33
|
+
disks = driver.box_disk_paths
|
|
34
|
+
|
|
35
|
+
if disks.empty?
|
|
36
|
+
raise Errors::BoxInvalid, name: @env[:machine].name, err: "No box disk found to package"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
multi = disks.size > 1
|
|
40
|
+
disk_meta = []
|
|
41
|
+
disks.each_with_index do |src, i|
|
|
42
|
+
name = multi ? "box_#{i + 1}.img" : "box.img"
|
|
43
|
+
@env[:ui].info(I18n.t("vagrant_qemu.packaging_disk", name: name))
|
|
44
|
+
driver.convert_box_disk(src, temp_dir.join(name))
|
|
45
|
+
disk_meta << { "path" => name }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
write_metadata(temp_dir, disk_meta, multi)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# libvirt box metadata: provider must be "libvirt" (the provider's
|
|
52
|
+
# box_format) or the packaged box won't match on `box add`.
|
|
53
|
+
def write_metadata(temp_dir, disk_meta, multi)
|
|
54
|
+
metadata = {
|
|
55
|
+
"provider" => "libvirt",
|
|
56
|
+
"format" => "qcow2",
|
|
57
|
+
"virtual_size" => virtual_size_gb(temp_dir.join(disk_meta.first["path"])),
|
|
58
|
+
}
|
|
59
|
+
metadata["disks"] = disk_meta if multi
|
|
60
|
+
File.write(temp_dir.join("metadata.json"), JSON.pretty_generate(metadata))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def virtual_size_gb(img)
|
|
64
|
+
stdout, stderr, status = Open3.capture3("qemu-img", "info", "--output=json", img.to_s)
|
|
65
|
+
if !status.success?
|
|
66
|
+
raise Errors::ExecuteError, command: "qemu-img info", stderr: stderr, stdout: stdout
|
|
67
|
+
end
|
|
68
|
+
bytes = JSON.parse(stdout)["virtual-size"]
|
|
69
|
+
(bytes.to_f / (1024**3)).ceil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module VagrantPlugins
|
|
4
|
+
module QEMU
|
|
5
|
+
module Action
|
|
6
|
+
# Writes a minimal default Vagrantfile into the box declaring only the
|
|
7
|
+
# architecture, so a consumer on a different host arch doesn't default to
|
|
8
|
+
# the wrong one. Networking, MAC and host-specific machine flags are left
|
|
9
|
+
# out on purpose — they are environment-specific and come from the
|
|
10
|
+
# consuming Vagrantfile. The core Package middleware appends the SSH
|
|
11
|
+
# private-key config to this same file afterwards.
|
|
12
|
+
class PackageVagrantfile
|
|
13
|
+
def initialize(app, env)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
@env = env
|
|
19
|
+
create_vagrantfile
|
|
20
|
+
@app.call(env)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_vagrantfile
|
|
24
|
+
arch = @env[:machine].provider_config.arch
|
|
25
|
+
path = Pathname.new(@env["export.temp_dir"]).join("Vagrantfile")
|
|
26
|
+
File.write(path, <<~VF)
|
|
27
|
+
Vagrant.configure("2") do |config|
|
|
28
|
+
config.vm.provider :qemu do |qe|
|
|
29
|
+
qe.arch = "#{arch}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
VF
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/vagrant-qemu/action.rb
CHANGED
|
@@ -9,8 +9,21 @@ module VagrantPlugins
|
|
|
9
9
|
include Vagrant::Action::Builtin
|
|
10
10
|
|
|
11
11
|
def self.action_package
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
13
|
+
b.use ConfigValidate
|
|
14
|
+
b.use Call, IsState, :not_created do |env, b2|
|
|
15
|
+
if env[:result]
|
|
16
|
+
b2.use MessageNotCreated
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
b2.use Vagrant::Action::General::PackageSetupFolders
|
|
21
|
+
b2.use Vagrant::Action::General::PackageSetupFiles
|
|
22
|
+
b2.use StopInstance
|
|
23
|
+
b2.use Vagrant::Action::General::Package
|
|
24
|
+
b2.use Export
|
|
25
|
+
b2.use PackageVagrantfile
|
|
26
|
+
end
|
|
14
27
|
end
|
|
15
28
|
end
|
|
16
29
|
|
|
@@ -173,6 +186,8 @@ module VagrantPlugins
|
|
|
173
186
|
autoload :Import, action_root.join("import")
|
|
174
187
|
autoload :StartInstance, action_root.join("start_instance")
|
|
175
188
|
autoload :StopInstance, action_root.join("stop_instance")
|
|
189
|
+
autoload :Export, action_root.join("export")
|
|
190
|
+
autoload :PackageVagrantfile, action_root.join("package_vagrantfile")
|
|
176
191
|
autoload :Destroy, action_root.join("destroy")
|
|
177
192
|
autoload :CloudInitNetwork, action_root.join("cloud_init_network")
|
|
178
193
|
autoload :TimedProvision, action_root.join("timed_provision") # some plugins now expect this action to exist
|
data/lib/vagrant-qemu/driver.rb
CHANGED
|
@@ -212,10 +212,27 @@ module VagrantPlugins
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
def stop(options)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
return unless running?
|
|
216
|
+
|
|
217
|
+
opts = with_persisted_control_port(options)
|
|
218
|
+
timeout = options[:graceful_timeout] || 60
|
|
219
|
+
|
|
220
|
+
# 1. ACPI power button (graceful). Only works if a live guest OS is
|
|
221
|
+
# there to act on it -- e.g. NOT after `systemctl halt`, which leaves
|
|
222
|
+
# QEMU running with a halted, unresponsive guest.
|
|
223
|
+
send_monitor(opts, "system_powerdown")
|
|
224
|
+
return unless still_running_after?(timeout)
|
|
225
|
+
|
|
226
|
+
# 2. Powerdown didn't take. Ask QEMU itself to quit: it stops the VM,
|
|
227
|
+
# flushes and closes the qcow2 images, then exits cleanly. Does not
|
|
228
|
+
# depend on the guest, only on the monitor being reachable.
|
|
229
|
+
@logger.warn("VM did not power down within #{timeout}s; sending 'quit' to QEMU")
|
|
230
|
+
send_monitor(opts, "quit")
|
|
231
|
+
return unless still_running_after?(timeout)
|
|
232
|
+
|
|
233
|
+
# 3. Last resort: SIGKILL the QEMU process (no flush/cleanup).
|
|
234
|
+
@logger.warn("VM still running after 'quit'; forcing kill")
|
|
235
|
+
force_kill
|
|
219
236
|
end
|
|
220
237
|
|
|
221
238
|
private
|
|
@@ -232,35 +249,37 @@ module VagrantPlugins
|
|
|
232
249
|
options.merge(:control_port => persisted[:control_port])
|
|
233
250
|
end
|
|
234
251
|
|
|
235
|
-
|
|
252
|
+
# Send a single QEMU monitor command over the control channel (TCP
|
|
253
|
+
# control_port if set, else the unix monitor socket). Best-effort: the
|
|
254
|
+
# socket may already be gone (e.g. QEMU exited from a prior command), so
|
|
255
|
+
# connection errors are swallowed.
|
|
256
|
+
def send_monitor(options, command)
|
|
236
257
|
if !options[:control_port].nil?
|
|
237
258
|
Socket.tcp("localhost", options[:control_port], connect_timeout: 5) do |sock|
|
|
238
|
-
sock.print "
|
|
259
|
+
sock.print "#{command}\n"
|
|
239
260
|
sock.close_write
|
|
240
261
|
sock.read rescue nil
|
|
241
262
|
end
|
|
242
263
|
else
|
|
243
|
-
|
|
244
|
-
unix_socket_path = id_tmp_dir.join("qemu_socket").to_s
|
|
264
|
+
unix_socket_path = @tmp_dir.join(@vm_id).join("qemu_socket").to_s
|
|
245
265
|
Socket.unix(unix_socket_path) do |sock|
|
|
246
|
-
sock.print "
|
|
266
|
+
sock.print "#{command}\n"
|
|
247
267
|
sock.close_write
|
|
248
268
|
sock.read rescue nil
|
|
249
269
|
end
|
|
250
270
|
end
|
|
271
|
+
rescue => e
|
|
272
|
+
@logger.debug("monitor command '#{command}' failed: #{e}") if @logger
|
|
251
273
|
end
|
|
252
274
|
|
|
253
|
-
|
|
275
|
+
# Poll for up to `timeout` seconds. Return false as soon as the VM has
|
|
276
|
+
# stopped; return true if it is still running when the timeout elapses.
|
|
277
|
+
def still_running_after?(timeout)
|
|
254
278
|
timeout.times do
|
|
255
|
-
return unless running?
|
|
279
|
+
return false unless running?
|
|
256
280
|
sleep 1
|
|
257
281
|
end
|
|
258
|
-
|
|
259
|
-
# Still running after timeout, force kill
|
|
260
|
-
if running?
|
|
261
|
-
@logger.warn("VM did not shut down within #{timeout}s, forcing kill")
|
|
262
|
-
force_kill
|
|
263
|
-
end
|
|
282
|
+
running?
|
|
264
283
|
end
|
|
265
284
|
|
|
266
285
|
def force_kill
|
|
@@ -455,6 +474,25 @@ module VagrantPlugins
|
|
|
455
474
|
def disk_dir
|
|
456
475
|
@data_dir.join(@vm_id)
|
|
457
476
|
end
|
|
477
|
+
|
|
478
|
+
# Ordered paths of the box's disk overlays, reconstructed by index to
|
|
479
|
+
# match start/import (glob only counts; names rebuild by index so a
|
|
480
|
+
# two-digit suffix can't sort ahead of a single digit).
|
|
481
|
+
def box_disk_paths
|
|
482
|
+
id_dir = disk_dir
|
|
483
|
+
count = id_dir.glob("linked-box*.img").count
|
|
484
|
+
(0...count).map do |i|
|
|
485
|
+
suffix = i > 0 ? "-#{i}" : ""
|
|
486
|
+
id_dir.join("linked-box#{suffix}.img")
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Flatten a box disk overlay into a standalone qcow2 at dst. convert reads
|
|
491
|
+
# the whole backing chain and writes a fresh file, so the source overlay
|
|
492
|
+
# (the live VM disk) is never modified.
|
|
493
|
+
def convert_box_disk(src, dst)
|
|
494
|
+
execute("qemu-img", "convert", "-O", "qcow2", src.to_s, dst.to_s)
|
|
495
|
+
end
|
|
458
496
|
end
|
|
459
497
|
end
|
|
460
498
|
end
|
data/lib/vagrant-qemu/version.rb
CHANGED
data/locales/en.yml
CHANGED
|
@@ -15,6 +15,8 @@ en:
|
|
|
15
15
|
Stopping the instance...
|
|
16
16
|
destroying: |-
|
|
17
17
|
Destroying the instance...
|
|
18
|
+
packaging_disk: |-
|
|
19
|
+
Exporting box disk: %{name}
|
|
18
20
|
warn_networks: |-
|
|
19
21
|
Warning! The QEMU provider doesn't support any of the Vagrant
|
|
20
22
|
high-level network configurations (`config.vm.network`). They
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Regression for #79: `vagrant halt` must actually reap QEMU even when the
|
|
4
|
+
# guest OS was already halted from inside (e.g. `sudo systemctl halt`). In that
|
|
5
|
+
# state the ACPI `system_powerdown` is a no-op (no live guest to act on it), so
|
|
6
|
+
# halt has to escalate to QEMU's `quit` monitor command and, failing that, to
|
|
7
|
+
# SIGKILL. A short graceful_timeout keeps the escalation fast.
|
|
8
|
+
describe "halt reaps QEMU when the guest is already halted (#79)", :requires_qemu do
|
|
9
|
+
around(:each) do |example|
|
|
10
|
+
with_temp_dir do |dir|
|
|
11
|
+
@work_dir = dir.join("project")
|
|
12
|
+
FileUtils.mkdir_p(@work_dir)
|
|
13
|
+
|
|
14
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
15
|
+
Vagrant.configure("2") do |config|
|
|
16
|
+
config.vm.box = "#{test_box}"
|
|
17
|
+
config.vm.box_check_update = false
|
|
18
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
19
|
+
config.vm.provider "qemu" do |qe|
|
|
20
|
+
qe.memory = "2G"
|
|
21
|
+
qe.graceful_timeout = 5
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
RUBY
|
|
25
|
+
|
|
26
|
+
example.run
|
|
27
|
+
|
|
28
|
+
vagrant_destroy(@work_dir) rescue nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "stops the VM after the guest was halted from inside" do
|
|
33
|
+
vagrant_up(@work_dir)
|
|
34
|
+
|
|
35
|
+
# Halt the guest OS from inside; the SSH connection drops as it goes down,
|
|
36
|
+
# so ignore the result. After this, system_powerdown can no longer work.
|
|
37
|
+
vagrant_ssh(@work_dir, command: "sudo systemctl halt", timeout: 30) rescue nil
|
|
38
|
+
sleep 3
|
|
39
|
+
|
|
40
|
+
# Without the escalation, QEMU would linger and the VM would still report
|
|
41
|
+
# running; with it, halt reaps the process within ~graceful_timeout.
|
|
42
|
+
result = vagrant_halt(@work_dir, timeout: 60)
|
|
43
|
+
expect(result[:exit_code]).to eq 0
|
|
44
|
+
|
|
45
|
+
status = vagrant_status(@work_dir)
|
|
46
|
+
expect(status[:stdout]).to include(",state,stopped")
|
|
47
|
+
end
|
|
48
|
+
end
|
data/spec/e2e/helper.rb
CHANGED
|
@@ -73,6 +73,18 @@ module E2EHelper
|
|
|
73
73
|
run_vagrant_cmd("status", "--machine-readable", cwd: cwd, timeout: 15)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
def vagrant_package(cwd, output:, timeout: 600)
|
|
77
|
+
run_vagrant_cmd("package", "--output", output.to_s, cwd: cwd, timeout: timeout)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def vagrant_box_add(name, path, cwd:, timeout: 120)
|
|
81
|
+
run_vagrant_cmd("box", "add", name, path.to_s, "--force", cwd: cwd, timeout: timeout)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def vagrant_box_remove(name, cwd:, timeout: 60)
|
|
85
|
+
run_vagrant_cmd("box", "remove", name, "--force", cwd: cwd, timeout: timeout)
|
|
86
|
+
end
|
|
87
|
+
|
|
76
88
|
private
|
|
77
89
|
|
|
78
90
|
def run_vagrant_cmd(*args, cwd:, timeout: 120)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# End-to-end round-trip for `vagrant package`. Exercises the INSTALLED plugin
|
|
4
|
+
# (see helper.rb), so rebuild + reinstall before running:
|
|
5
|
+
# bundle exec rake build && vagrant plugin install ./pkg/vagrant-qemu-<ver>.gem
|
|
6
|
+
# TEST_QEMU=1 bundle exec rake spec:e2e
|
|
7
|
+
describe "vagrant package end-to-end", :requires_qemu do
|
|
8
|
+
around(:each) do |example|
|
|
9
|
+
with_temp_dir do |dir|
|
|
10
|
+
@work_dir = dir.join("project")
|
|
11
|
+
@consume_dir = dir.join("consumer")
|
|
12
|
+
@box_path = dir.join("packaged.box")
|
|
13
|
+
FileUtils.mkdir_p(@work_dir)
|
|
14
|
+
FileUtils.mkdir_p(@consume_dir)
|
|
15
|
+
example.run
|
|
16
|
+
vagrant_destroy(@consume_dir) rescue nil
|
|
17
|
+
vagrant_destroy(@work_dir) rescue nil
|
|
18
|
+
vagrant_box_remove("e2e-packaged", cwd: @work_dir) rescue nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_source_vagrantfile
|
|
23
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
24
|
+
Vagrant.configure("2") do |config|
|
|
25
|
+
config.vm.box = "#{test_box}"
|
|
26
|
+
config.vm.box_check_update = false
|
|
27
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
28
|
+
config.vm.provider "qemu" do |qe|
|
|
29
|
+
qe.memory = "2G"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "packages a running VM into a box that boots again (round-trip)" do
|
|
36
|
+
write_source_vagrantfile
|
|
37
|
+
expect(vagrant_up(@work_dir)[:exit_code]).to eq 0
|
|
38
|
+
|
|
39
|
+
result = vagrant_package(@work_dir, output: @box_path)
|
|
40
|
+
expect(result[:exit_code]).to eq 0
|
|
41
|
+
expect(File.exist?(@box_path)).to be true
|
|
42
|
+
|
|
43
|
+
# Box layout: box.img + metadata.json + Vagrantfile.
|
|
44
|
+
listing = `tar tzf #{@box_path}`
|
|
45
|
+
expect(listing).to include("box.img")
|
|
46
|
+
expect(listing).to include("metadata.json")
|
|
47
|
+
expect(listing).to include("Vagrantfile")
|
|
48
|
+
|
|
49
|
+
# box.img must be a self-contained qcow2 (overlay flattened, no backing).
|
|
50
|
+
Dir.mktmpdir do |ex|
|
|
51
|
+
system("tar xzf #{@box_path} -C #{ex} box.img")
|
|
52
|
+
info = `qemu-img info --output=json #{File.join(ex, "box.img")}`
|
|
53
|
+
expect(info).to include('"format": "qcow2"')
|
|
54
|
+
expect(info).not_to match(/backing-filename/)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Consume the packaged box in a fresh project and boot it.
|
|
58
|
+
expect(vagrant_box_add("e2e-packaged", @box_path, cwd: @work_dir)[:exit_code]).to eq 0
|
|
59
|
+
File.write(@consume_dir.join("Vagrantfile"), <<~RUBY)
|
|
60
|
+
Vagrant.configure("2") do |config|
|
|
61
|
+
config.vm.box = "e2e-packaged"
|
|
62
|
+
config.vm.box_check_update = false
|
|
63
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
64
|
+
config.vm.provider "qemu" do |qe|
|
|
65
|
+
qe.memory = "2G"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
expect(vagrant_up(@consume_dir)[:exit_code]).to eq 0
|
|
70
|
+
expect(vagrant_ssh(@consume_dir, command: "echo roundtrip-ok")[:stdout]).to include("roundtrip-ok")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "does not bake environment-specific network config into the box" do
|
|
74
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
75
|
+
Vagrant.configure("2") do |config|
|
|
76
|
+
config.vm.box = "#{test_box}"
|
|
77
|
+
config.vm.box_check_update = false
|
|
78
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
79
|
+
config.vm.network "forwarded_port", guest: 80, host: 8080, auto_correct: true
|
|
80
|
+
config.vm.provider "qemu" do |qe|
|
|
81
|
+
qe.memory = "2G"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
expect(vagrant_up(@work_dir)[:exit_code]).to eq 0
|
|
86
|
+
expect(vagrant_package(@work_dir, output: @box_path)[:exit_code]).to eq 0
|
|
87
|
+
|
|
88
|
+
Dir.mktmpdir do |ex|
|
|
89
|
+
system("tar xzf #{@box_path} -C #{ex} Vagrantfile")
|
|
90
|
+
vf = File.read(File.join(ex, "Vagrantfile"))
|
|
91
|
+
expect(vf).to include("qe.arch")
|
|
92
|
+
expect(vf).not_to match(/forwarded_port|network|8080/)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "json"
|
|
3
|
+
require "vagrant-qemu/action/export"
|
|
4
|
+
|
|
5
|
+
describe VagrantPlugins::QEMU::Action::Export do
|
|
6
|
+
let(:vm_id) { "vq_export" }
|
|
7
|
+
let(:app) { lambda { |env| } }
|
|
8
|
+
let(:ui) { double("ui", info: nil, detail: nil) }
|
|
9
|
+
|
|
10
|
+
around(:each) do |example|
|
|
11
|
+
with_temp_dir do |dir|
|
|
12
|
+
@data_dir = dir.join("data")
|
|
13
|
+
@tmp_base = dir.join("tmp")
|
|
14
|
+
@export_dir = dir.join("export")
|
|
15
|
+
FileUtils.mkdir_p(@data_dir.join(vm_id))
|
|
16
|
+
FileUtils.mkdir_p(@tmp_base)
|
|
17
|
+
FileUtils.mkdir_p(@export_dir)
|
|
18
|
+
example.run
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:driver) { VagrantPlugins::QEMU::Driver.new(vm_id, @data_dir, @tmp_base) }
|
|
23
|
+
let(:provider) { double("provider", driver: driver) }
|
|
24
|
+
let(:state) { double("state", id: :stopped) }
|
|
25
|
+
let(:machine) do
|
|
26
|
+
c = VagrantPlugins::QEMU::Config.new
|
|
27
|
+
c.arch = "aarch64"
|
|
28
|
+
c.finalize!
|
|
29
|
+
double("machine", provider: provider, provider_config: c, name: "default", state: state)
|
|
30
|
+
end
|
|
31
|
+
let(:env) { { machine: machine, ui: ui, "export.temp_dir" => @export_dir.to_s } }
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
VagrantPlugins::QEMU::Plugin.setup_i18n
|
|
35
|
+
# Don't invoke real qemu-img convert; just record calls.
|
|
36
|
+
allow(driver).to receive(:convert_box_disk)
|
|
37
|
+
# virtual-size from qemu-img info: 1 GiB + 1 byte → ceil to 2 GB.
|
|
38
|
+
status = double("status", success?: true)
|
|
39
|
+
allow(Open3).to receive(:capture3)
|
|
40
|
+
.and_return([JSON.generate("virtual-size" => (1024**3) + 1), "", status])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def metadata
|
|
44
|
+
JSON.parse(File.read(@export_dir.join("metadata.json")))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "single box disk (v1)" do
|
|
48
|
+
before { FileUtils.touch(@data_dir.join(vm_id, "linked-box.img")) }
|
|
49
|
+
|
|
50
|
+
it "flattens the disk to box.img and writes libvirt v1 metadata (no disks[])" do
|
|
51
|
+
expect(driver).to receive(:convert_box_disk)
|
|
52
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box.img"))
|
|
53
|
+
described_class.new(app, env).call(env)
|
|
54
|
+
|
|
55
|
+
expect(metadata["provider"]).to eq "libvirt"
|
|
56
|
+
expect(metadata["format"]).to eq "qcow2"
|
|
57
|
+
expect(metadata["virtual_size"]).to eq 2
|
|
58
|
+
expect(metadata).not_to have_key("disks")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context "multiple box disks (v2)" do
|
|
63
|
+
before do
|
|
64
|
+
%w[linked-box.img linked-box-1.img].each { |n| FileUtils.touch(@data_dir.join(vm_id, n)) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "flattens each disk to box_N.img and writes disks[]" do
|
|
68
|
+
expect(driver).to receive(:convert_box_disk)
|
|
69
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box_1.img"))
|
|
70
|
+
expect(driver).to receive(:convert_box_disk)
|
|
71
|
+
.with(@data_dir.join(vm_id, "linked-box-1.img"), @export_dir.join("box_2.img"))
|
|
72
|
+
described_class.new(app, env).call(env)
|
|
73
|
+
|
|
74
|
+
expect(metadata["disks"]).to eq([{ "path" => "box_1.img" }, { "path" => "box_2.img" }])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "environment-specific artifacts present" do
|
|
79
|
+
before do
|
|
80
|
+
FileUtils.touch(@data_dir.join(vm_id, "linked-box.img"))
|
|
81
|
+
FileUtils.touch(@data_dir.join(vm_id, "extra-data.qcow2"))
|
|
82
|
+
FileUtils.touch(@data_dir.join(vm_id, "vagrant-qemu-network.iso"))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "packages only the box disk, not additional disks or ISOs" do
|
|
86
|
+
expect(driver).to receive(:convert_box_disk).once
|
|
87
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box.img"))
|
|
88
|
+
described_class.new(app, env).call(env)
|
|
89
|
+
expect(metadata).not_to have_key("disks")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "VM not powered off" do
|
|
94
|
+
let(:state) { double("state", id: :running) }
|
|
95
|
+
before { FileUtils.touch(@data_dir.join(vm_id, "linked-box.img")) }
|
|
96
|
+
|
|
97
|
+
it "raises and does not convert" do
|
|
98
|
+
expect(driver).not_to receive(:convert_box_disk)
|
|
99
|
+
expect { described_class.new(app, env).call(env) }
|
|
100
|
+
.to raise_error(Vagrant::Errors::VMPowerOffToPackage)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Action, "#action_package" do
|
|
4
|
+
it "returns a runnable builder instead of raising NotSupportedError" do
|
|
5
|
+
expect(described_class.action_package).to be_a(Vagrant::Action::Builder)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "no longer raises (package is supported now)" do
|
|
9
|
+
expect { described_class.action_package }.not_to raise_error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "validates config and branches on machine state at the top of the chain" do
|
|
13
|
+
klasses = described_class.action_package.stack.map(&:first)
|
|
14
|
+
expect(klasses).to include(Vagrant::Action::Builtin::ConfigValidate)
|
|
15
|
+
expect(klasses).to include(Vagrant::Action::Builtin::Call)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "autoloads the Export and PackageVagrantfile actions" do
|
|
19
|
+
expect { described_class::Export }.not_to raise_error
|
|
20
|
+
expect { described_class::PackageVagrantfile }.not_to raise_error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "vagrant-qemu/action/package_vagrantfile"
|
|
3
|
+
|
|
4
|
+
describe VagrantPlugins::QEMU::Action::PackageVagrantfile do
|
|
5
|
+
let(:app) { lambda { |env| } }
|
|
6
|
+
let(:ui) { double("ui", info: nil) }
|
|
7
|
+
|
|
8
|
+
around(:each) do |example|
|
|
9
|
+
with_temp_dir do |dir|
|
|
10
|
+
@export_dir = dir.join("export")
|
|
11
|
+
FileUtils.mkdir_p(@export_dir)
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def machine_with(arch)
|
|
17
|
+
c = VagrantPlugins::QEMU::Config.new
|
|
18
|
+
c.arch = arch
|
|
19
|
+
c.finalize!
|
|
20
|
+
double("machine", provider_config: c)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(arch)
|
|
24
|
+
env = { machine: machine_with(arch), ui: ui, "export.temp_dir" => @export_dir.to_s }
|
|
25
|
+
described_class.new(app, env).call(env)
|
|
26
|
+
File.read(@export_dir.join("Vagrantfile"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "writes the VM arch into the default Vagrantfile" do
|
|
30
|
+
expect(run("aarch64")).to include('qe.arch = "aarch64"')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "carries a different arch through" do
|
|
34
|
+
expect(run("x86_64")).to include('qe.arch = "x86_64"')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "does not bake environment-specific network/MAC/machine config" do
|
|
38
|
+
vf = run("aarch64")
|
|
39
|
+
expect(vf).not_to match(/network|base_mac|accel|machine/i)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Driver, "package helpers" do
|
|
4
|
+
let(:vm_id) { "vq_pkgtest" }
|
|
5
|
+
|
|
6
|
+
around(:each) do |example|
|
|
7
|
+
with_temp_dir do |dir|
|
|
8
|
+
@data_dir = dir.join("data")
|
|
9
|
+
@tmp_base = dir.join("tmp")
|
|
10
|
+
FileUtils.mkdir_p(@data_dir.join(vm_id))
|
|
11
|
+
FileUtils.mkdir_p(@tmp_base)
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subject { described_class.new(vm_id, @data_dir, @tmp_base) }
|
|
17
|
+
|
|
18
|
+
describe "#box_disk_paths" do
|
|
19
|
+
it "enumerates box disks by index, matching start/import order (not glob lexical)" do
|
|
20
|
+
id_dir = @data_dir.join(vm_id)
|
|
21
|
+
# Create out of lexical order incl. a two-digit index to catch lexical-sort bugs.
|
|
22
|
+
%w[linked-box.img linked-box-1.img linked-box-2.img].each { |n| FileUtils.touch(id_dir.join(n)) }
|
|
23
|
+
|
|
24
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) })
|
|
25
|
+
.to eq %w[linked-box.img linked-box-1.img linked-box-2.img]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns a single box.img overlay for a single-disk VM" do
|
|
29
|
+
FileUtils.touch(@data_dir.join(vm_id, "linked-box.img"))
|
|
30
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) }).to eq %w[linked-box.img]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "excludes additional disks and ISOs (only linked-box*.img are box disks)" do
|
|
34
|
+
id_dir = @data_dir.join(vm_id)
|
|
35
|
+
FileUtils.touch(id_dir.join("linked-box.img"))
|
|
36
|
+
FileUtils.touch(id_dir.join("extra-data.qcow2"))
|
|
37
|
+
FileUtils.touch(id_dir.join("vagrant-qemu-network.iso"))
|
|
38
|
+
|
|
39
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) }).to eq %w[linked-box.img]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#convert_box_disk" do
|
|
44
|
+
it "flattens via qemu-img convert to a fresh qcow2 (never mutates the source)" do
|
|
45
|
+
src = @data_dir.join(vm_id, "linked-box.img")
|
|
46
|
+
dst = @data_dir.join(vm_id, "box.img")
|
|
47
|
+
expect(subject).to receive(:execute).with("qemu-img", "convert", "-O", "qcow2", src.to_s, dst.to_s)
|
|
48
|
+
subject.convert_box_disk(src, dst)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -16,47 +16,55 @@ describe VagrantPlugins::QEMU::Driver, "#stop" do
|
|
|
16
16
|
subject { described_class.new(vm_id, @data_dir, @tmp_base) }
|
|
17
17
|
|
|
18
18
|
it "does nothing when not running" do
|
|
19
|
-
|
|
19
|
+
allow(subject).to receive(:running?).and_return(false)
|
|
20
|
+
expect(subject).not_to receive(:send_monitor)
|
|
20
21
|
subject.stop(control_port: nil)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
it "sends
|
|
24
|
+
it "sends system_powerdown first and stops there when the guest powers off" do
|
|
25
|
+
# running on the initial check, stopped on the first poll
|
|
24
26
|
allow(subject).to receive(:running?).and_return(true, false)
|
|
25
|
-
allow(subject).to receive(:
|
|
27
|
+
allow(subject).to receive(:send_monitor)
|
|
26
28
|
|
|
27
|
-
subject.stop(control_port: nil)
|
|
29
|
+
subject.stop(control_port: nil, graceful_timeout: 5)
|
|
28
30
|
|
|
29
|
-
expect(subject).to have_received(:
|
|
31
|
+
expect(subject).to have_received(:send_monitor).with(anything, "system_powerdown")
|
|
32
|
+
expect(subject).not_to have_received(:send_monitor).with(anything, "quit")
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
it "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
allow(subject).to receive(:send_powerdown)
|
|
35
|
+
it "escalates to 'quit' when powerdown does not stop the VM" do
|
|
36
|
+
allow(subject).to receive(:sleep)
|
|
37
|
+
# stays up through the powerdown wait, then stops during the quit wait
|
|
38
|
+
allow(subject).to receive(:running?).and_return(true, true, true, false)
|
|
39
|
+
allow(subject).to receive(:send_monitor)
|
|
40
|
+
expect(subject).not_to receive(:force_kill)
|
|
39
41
|
|
|
40
|
-
subject.stop(control_port: nil, graceful_timeout:
|
|
42
|
+
subject.stop(control_port: nil, graceful_timeout: 1)
|
|
43
|
+
|
|
44
|
+
expect(subject).to have_received(:send_monitor).with(anything, "system_powerdown").ordered
|
|
45
|
+
expect(subject).to have_received(:send_monitor).with(anything, "quit").ordered
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
it "force kills
|
|
44
|
-
allow(subject).to receive(:running?).and_return(true)
|
|
45
|
-
allow(subject).to receive(:send_powerdown)
|
|
48
|
+
it "force kills only after both powerdown and 'quit' fail" do
|
|
46
49
|
allow(subject).to receive(:sleep)
|
|
50
|
+
allow(subject).to receive(:running?).and_return(true) # never stops
|
|
51
|
+
allow(subject).to receive(:send_monitor)
|
|
47
52
|
|
|
48
53
|
pid_dir = @tmp_base.join("vagrant-qemu", vm_id)
|
|
49
54
|
FileUtils.mkdir_p(pid_dir)
|
|
50
55
|
File.write(pid_dir.join("qemu.pid"), "999999999")
|
|
51
|
-
|
|
52
56
|
allow(Process).to receive(:kill).with("KILL", 999999999).and_raise(Errno::ESRCH)
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
subject.stop(control_port: nil, graceful_timeout: 1)
|
|
59
|
+
|
|
60
|
+
expect(subject).to have_received(:send_monitor).with(anything, "system_powerdown")
|
|
61
|
+
expect(subject).to have_received(:send_monitor).with(anything, "quit")
|
|
62
|
+
expect(Process).to have_received(:kill).with("KILL", 999999999)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
it "prefers the persisted control_port over the configured one" do
|
|
58
66
|
allow(subject).to receive(:running?).and_return(true, false)
|
|
59
|
-
allow(subject).to receive(:
|
|
67
|
+
allow(subject).to receive(:send_monitor)
|
|
60
68
|
|
|
61
69
|
opts_dir = @tmp_base.join("vagrant-qemu", vm_id)
|
|
62
70
|
FileUtils.mkdir_p(opts_dir)
|
|
@@ -64,29 +72,28 @@ describe VagrantPlugins::QEMU::Driver, "#stop" do
|
|
|
64
72
|
|
|
65
73
|
subject.stop(control_port: 33333)
|
|
66
74
|
|
|
67
|
-
expect(subject).to have_received(:
|
|
68
|
-
.with(hash_including(control_port: 44444))
|
|
75
|
+
expect(subject).to have_received(:send_monitor)
|
|
76
|
+
.with(hash_including(control_port: 44444), "system_powerdown")
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
it "keeps the configured control_port when nothing is persisted" do
|
|
72
80
|
allow(subject).to receive(:running?).and_return(true, false)
|
|
73
|
-
allow(subject).to receive(:
|
|
81
|
+
allow(subject).to receive(:send_monitor)
|
|
74
82
|
|
|
75
83
|
subject.stop(control_port: 33333)
|
|
76
84
|
|
|
77
|
-
expect(subject).to have_received(:
|
|
78
|
-
.with(hash_including(control_port: 33333))
|
|
85
|
+
expect(subject).to have_received(:send_monitor)
|
|
86
|
+
.with(hash_including(control_port: 33333), "system_powerdown")
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
it "swallows ESRCH on force_kill when process already gone" do
|
|
82
|
-
allow(subject).to receive(:running?).and_return(true)
|
|
83
|
-
allow(subject).to receive(:send_powerdown)
|
|
89
|
+
it "swallows ESRCH on force_kill when the process is already gone" do
|
|
84
90
|
allow(subject).to receive(:sleep)
|
|
91
|
+
allow(subject).to receive(:running?).and_return(true)
|
|
92
|
+
allow(subject).to receive(:send_monitor)
|
|
85
93
|
|
|
86
94
|
pid_dir = @tmp_base.join("vagrant-qemu", vm_id)
|
|
87
95
|
FileUtils.mkdir_p(pid_dir)
|
|
88
96
|
File.write(pid_dir.join("qemu.pid"), "999999999")
|
|
89
|
-
|
|
90
97
|
allow(Process).to receive(:kill).and_raise(Errno::ESRCH)
|
|
91
98
|
|
|
92
99
|
expect { subject.stop(control_port: nil, graceful_timeout: 1) }.not_to raise_error
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-qemu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ppggff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Enables Vagrant to manage machines with QEMU.
|
|
14
14
|
email: pgf00a@gmail.com
|
|
@@ -27,10 +27,12 @@ files:
|
|
|
27
27
|
- lib/vagrant-qemu/action.rb
|
|
28
28
|
- lib/vagrant-qemu/action/cloud_init_network.rb
|
|
29
29
|
- lib/vagrant-qemu/action/destroy.rb
|
|
30
|
+
- lib/vagrant-qemu/action/export.rb
|
|
30
31
|
- lib/vagrant-qemu/action/import.rb
|
|
31
32
|
- lib/vagrant-qemu/action/message_already_created.rb
|
|
32
33
|
- lib/vagrant-qemu/action/message_not_created.rb
|
|
33
34
|
- lib/vagrant-qemu/action/message_will_not_destroy.rb
|
|
35
|
+
- lib/vagrant-qemu/action/package_vagrantfile.rb
|
|
34
36
|
- lib/vagrant-qemu/action/prepare_forwarded_port_collision_params.rb
|
|
35
37
|
- lib/vagrant-qemu/action/read_state.rb
|
|
36
38
|
- lib/vagrant-qemu/action/start_instance.rb
|
|
@@ -59,7 +61,9 @@ files:
|
|
|
59
61
|
- spec/e2e/advanced_network_spec.rb
|
|
60
62
|
- spec/e2e/disk_spec.rb
|
|
61
63
|
- spec/e2e/forwarded_port_spec.rb
|
|
64
|
+
- spec/e2e/halt_spec.rb
|
|
62
65
|
- spec/e2e/helper.rb
|
|
66
|
+
- spec/e2e/package_spec.rb
|
|
63
67
|
- spec/e2e/provision_spec.rb
|
|
64
68
|
- spec/e2e/reload_spec.rb
|
|
65
69
|
- spec/e2e/smoke_spec.rb
|
|
@@ -67,7 +71,10 @@ files:
|
|
|
67
71
|
- spec/spec_helper.rb
|
|
68
72
|
- spec/unit/action/cloud_init_network_spec.rb
|
|
69
73
|
- spec/unit/action/destroy_spec.rb
|
|
74
|
+
- spec/unit/action/export_spec.rb
|
|
70
75
|
- spec/unit/action/import_spec.rb
|
|
76
|
+
- spec/unit/action/package_spec.rb
|
|
77
|
+
- spec/unit/action/package_vagrantfile_spec.rb
|
|
71
78
|
- spec/unit/action/prepare_forwarded_port_collision_params_spec.rb
|
|
72
79
|
- spec/unit/action/read_state_spec.rb
|
|
73
80
|
- spec/unit/action/start_instance_port_spec.rb
|
|
@@ -76,6 +83,7 @@ files:
|
|
|
76
83
|
- spec/unit/config_spec.rb
|
|
77
84
|
- spec/unit/driver/delete_spec.rb
|
|
78
85
|
- spec/unit/driver/options_yaml_spec.rb
|
|
86
|
+
- spec/unit/driver/package_spec.rb
|
|
79
87
|
- spec/unit/driver/ssh_port_spec.rb
|
|
80
88
|
- spec/unit/driver/start_cmd_advanced_spec.rb
|
|
81
89
|
- spec/unit/driver/start_cmd_order_spec.rb
|