vagrant-qemu 0.4.0 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35de3b54145ee419e9cada4bd6b879fa0e9f7faef93e27da8e750346dcd6c0b7
4
- data.tar.gz: 8e0069a3cce24d59ab4723108bd54199cb6019a8a47c587601795bd6a9cc91a1
3
+ metadata.gz: 4cd40c0eae22a6a6d4f284004a86de09e0584e8887cc273e9acd4c0fa2f33e69
4
+ data.tar.gz: 4caaa2706bbb8f221dcc67e5164e7b38879e3cb2d709d8414fc1f0a0ed0de45b
5
5
  SHA512:
6
- metadata.gz: de578f059ba6ee08339738a906e366f8b8f33acc7b6389b05d68af4d365ceb6829e6d2ac7cf84214d7ff5f7af8510d657228ebd08fa0f444d40a962e23001cae
7
- data.tar.gz: 5f74816cd5c18bf5ad492e546fea36d972b32585ebb0f360f1bd6cf2bbe70e475ea69365f6ff228addb5d8179feaa7944d74bfd80f4982dbd80eb7d2c7605a1b
6
+ metadata.gz: dd592ca7bf0a75c82b58db4ed8194f8a3e31b44834538ea0fe7ae7124032871422b9b4f2f6b49122964fefc0692f02d1be4a018bae2e4948f3f8aeeb9385eaaf
7
+ data.tar.gz: 906b02b551b9f3c5082a3967edc33c656f9b642ae0f6bbabcd8bb40b944fa0e2d67f10c5b6b34af3291516c218d0d7657c47d9cf495d1dabceb63fa5acb4ac77
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`
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 for the guest to shut down on `vagrant halt` before the QEMU process is force-killed, default: `60`
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`
@@ -212,10 +212,27 @@ module VagrantPlugins
212
212
  end
213
213
 
214
214
  def stop(options)
215
- if running?
216
- send_powerdown(with_persisted_control_port(options))
217
- wait_for_shutdown(options[:graceful_timeout] || 60)
218
- end
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
- def send_powerdown(options)
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 "system_powerdown\n"
259
+ sock.print "#{command}\n"
239
260
  sock.close_write
240
261
  sock.read rescue nil
241
262
  end
242
263
  else
243
- id_tmp_dir = @tmp_dir.join(@vm_id)
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 "system_powerdown\n"
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
- def wait_for_shutdown(timeout)
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
@@ -1,5 +1,5 @@
1
1
  module VagrantPlugins
2
2
  module QEMU
3
- VERSION = '0.4.0'
3
+ VERSION = '0.4.1'
4
4
  end
5
5
  end
@@ -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
@@ -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
- expect(subject).not_to receive(:send_powerdown)
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 powerdown when running" do
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(:send_powerdown)
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(:send_powerdown)
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 "returns when VM shuts down within timeout" do
33
- call_count = 0
34
- allow(subject).to receive(:running?) do
35
- call_count += 1
36
- call_count <= 1 # running on first call, stopped on second
37
- end
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: 5)
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 when VM does not shut down within timeout" do
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
- expect { subject.stop(control_port: nil, graceful_timeout: 1) }.not_to raise_error
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(:send_powerdown)
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(:send_powerdown)
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(:send_powerdown)
81
+ allow(subject).to receive(:send_monitor)
74
82
 
75
83
  subject.stop(control_port: 33333)
76
84
 
77
- expect(subject).to have_received(:send_powerdown)
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vagrant-qemu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ppggff
@@ -59,6 +59,7 @@ files:
59
59
  - spec/e2e/advanced_network_spec.rb
60
60
  - spec/e2e/disk_spec.rb
61
61
  - spec/e2e/forwarded_port_spec.rb
62
+ - spec/e2e/halt_spec.rb
62
63
  - spec/e2e/helper.rb
63
64
  - spec/e2e/provision_spec.rb
64
65
  - spec/e2e/reload_spec.rb