vagrant-qemu 0.3.12 → 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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/CLAUDE.md +45 -0
  4. data/README.md +185 -17
  5. data/Rakefile +24 -7
  6. data/lib/vagrant-qemu/action/cloud_init_network.rb +123 -0
  7. data/lib/vagrant-qemu/action/destroy.rb +7 -1
  8. data/lib/vagrant-qemu/action/import.rb +10 -5
  9. data/lib/vagrant-qemu/action/read_state.rb +2 -2
  10. data/lib/vagrant-qemu/action/start_instance.rb +44 -28
  11. data/lib/vagrant-qemu/action/stop_instance.rb +2 -1
  12. data/lib/vagrant-qemu/action/warn_networks.rb +21 -1
  13. data/lib/vagrant-qemu/action.rb +2 -0
  14. data/lib/vagrant-qemu/config.rb +75 -5
  15. data/lib/vagrant-qemu/driver.rb +145 -34
  16. data/lib/vagrant-qemu/errors.rb +8 -0
  17. data/lib/vagrant-qemu/network/base.rb +21 -0
  18. data/lib/vagrant-qemu/network/socket.rb +28 -0
  19. data/lib/vagrant-qemu/network/tap.rb +20 -0
  20. data/lib/vagrant-qemu/network/vmnet.rb +53 -0
  21. data/lib/vagrant-qemu/network.rb +99 -0
  22. data/lib/vagrant-qemu/provider.rb +1 -1
  23. data/lib/vagrant-qemu/version.rb +1 -1
  24. data/lib/vagrant-qemu.rb +1 -0
  25. data/locales/en.yml +22 -0
  26. data/spec/acceptance/basic_up_spec.rb +83 -0
  27. data/spec/acceptance/halt_destroy_spec.rb +69 -0
  28. data/spec/acceptance/helper.rb +69 -0
  29. data/spec/acceptance/network_spec.rb +134 -0
  30. data/spec/acceptance/port_collision_spec.rb +53 -0
  31. data/spec/e2e/advanced_network_spec.rb +117 -0
  32. data/spec/e2e/disk_spec.rb +35 -0
  33. data/spec/e2e/forwarded_port_spec.rb +107 -0
  34. data/spec/e2e/halt_spec.rb +48 -0
  35. data/spec/e2e/helper.rb +137 -0
  36. data/spec/e2e/provision_spec.rb +35 -0
  37. data/spec/e2e/reload_spec.rb +58 -0
  38. data/spec/e2e/smoke_spec.rb +60 -0
  39. data/spec/e2e/socket_network_spec.rb +178 -0
  40. data/spec/spec_helper.rb +75 -0
  41. data/spec/unit/action/cloud_init_network_spec.rb +162 -0
  42. data/spec/unit/action/destroy_spec.rb +46 -0
  43. data/spec/unit/action/import_spec.rb +52 -0
  44. data/spec/unit/action/prepare_forwarded_port_collision_params_spec.rb +72 -0
  45. data/spec/unit/action/read_state_spec.rb +50 -0
  46. data/spec/unit/action/start_instance_port_spec.rb +77 -0
  47. data/spec/unit/action/start_instance_spec.rb +64 -0
  48. data/spec/unit/action/warn_networks_spec.rb +38 -0
  49. data/spec/unit/config_spec.rb +143 -0
  50. data/spec/unit/driver/delete_spec.rb +39 -0
  51. data/spec/unit/driver/options_yaml_spec.rb +49 -0
  52. data/spec/unit/driver/ssh_port_spec.rb +46 -0
  53. data/spec/unit/driver/start_cmd_advanced_spec.rb +109 -0
  54. data/spec/unit/driver/start_cmd_order_spec.rb +67 -0
  55. data/spec/unit/driver/start_cmd_spec.rb +161 -0
  56. data/spec/unit/driver/start_edge_cases_spec.rb +72 -0
  57. data/spec/unit/driver/state_spec.rb +60 -0
  58. data/spec/unit/driver/stop_spec.rb +101 -0
  59. data/spec/unit/network/backend_for_spec.rb +27 -0
  60. data/spec/unit/network/build_network_config_spec.rb +40 -0
  61. data/spec/unit/network/generate_mac_spec.rb +26 -0
  62. data/spec/unit/network/socket_spec.rb +44 -0
  63. data/spec/unit/network/tap_spec.rb +15 -0
  64. data/spec/unit/network/vmnet_spec.rb +58 -0
  65. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22e76d0b973e4e459a0780c79265d30b6cd0b48d4e94d28fdc82ef9cccfca37f
4
- data.tar.gz: '049fb94522b7197b4d77975da222b033755761d0db699d90c6a98cb1fe49cd33'
3
+ metadata.gz: 4cd40c0eae22a6a6d4f284004a86de09e0584e8887cc273e9acd4c0fa2f33e69
4
+ data.tar.gz: 4caaa2706bbb8f221dcc67e5164e7b38879e3cb2d709d8414fc1f0a0ed0de45b
5
5
  SHA512:
6
- metadata.gz: '06901f39eac546967a57ca981739ca160caae09ec28ae9ade2c809bae6862bdeaf61ec8ac8f8cea6fbaba37146323ca08f1bb60bab53433381ac5619bf9f3517'
7
- data.tar.gz: c815fe0029b54a7e092543fddbb5769add49393ef5fdd4e84584b8c893a164f88e1d56aebe5251b891dac7bee4a13037366988e22abdbb3189f96b979f338680
6
+ metadata.gz: dd592ca7bf0a75c82b58db4ed8194f8a3e31b44834538ea0fe7ae7124032871422b9b4f2f6b49122964fefc0692f02d1be4a018bae2e4948f3f8aeeb9385eaaf
7
+ data.tar.gz: 906b02b551b9f3c5082a3967edc33c656f9b642ae0f6bbabcd8bb40b944fa0e2d67f10c5b6b34af3291516c218d0d7657c47d9cf495d1dabceb63fa5acb4ac77
data/CHANGELOG.md CHANGED
@@ -94,9 +94,49 @@
94
94
 
95
95
  * Re-publish with repacked gem
96
96
 
97
- # 0.3.22 (2025-05-19)
97
+ # 0.3.12 (2025-05-19)
98
98
 
99
99
  * Add support for extra `-drive` arguments
100
100
  * Add `extra_image_opts` to customize image creation
101
101
  * Add support for cloud-init and disks
102
102
  * Add support for resizing disk on vm setup
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
+
112
+ # 0.4.0 (2026-06-12)
113
+
114
+ * Advanced networking (opt-in, `advanced_network = true`): dual-NIC `private_network`
115
+ support via vmnet (macOS), TAP (Linux), or the QEMU `socket` netdev, with
116
+ deterministic MAC addresses; static IP delivered through a plugin-built cloud-init
117
+ NoCloud seed ISO (MAC-matched, requires cloud-init in the guest). When
118
+ `config.vm.cloud_init` is also set, its user-data and the generated
119
+ network-config are merged into a single seed
120
+ * `net_mode = :socket` is now a thin wrapper around QEMU's `socket` netdev: the new
121
+ `socket_opts` option is emitted verbatim, so you pick the mode yourself —
122
+ `"mcast=230.0.0.1:1234"` (multicast, N-way) or `"listen=:1234"` / `"connect=host:1234"`
123
+ (point-to-point, no root, works on macOS where multicast does not — QEMU binds the
124
+ socket to the multicast group address, which Darwin rejects for sending). For
125
+ listen/connect you decide which VM listens and which connects (it is a 1:1 link,
126
+ not a hub). `mcast_addr` remains as a shortcut for the multicast address
127
+ * Fix SSH port not updated after forwarded-port collision auto-correction
128
+ * Persist only needed runtime state in options.yml; harden YAML loading
129
+ (`safe_load`); `vagrant halt` reads back the persisted control_port
130
+ * Graceful shutdown on halt with configurable `graceful_timeout` (default 60s),
131
+ force kill as fallback
132
+ * Host-aware defaults for `arch`/`machine`/`cpu`/`net_device`/`qemu_dir`: detect
133
+ the host arch (Apple Silicon vs Intel) and OS, default to native acceleration
134
+ (`hvf`/`kvm`/`whpx`) with `cpu=host` when guest arch matches the host, and to
135
+ `accel=tcg`/`cpu=max` when emulating; resolve `qemu_dir` from
136
+ `QEMU_DIR`/`HOMEBREW_PREFIX`/per-host path; skip the `qemu_dir` check for
137
+ x86_64 (SeaBIOS) so Intel Macs no longer fail with "Invalid qemu dir" (#59, #50)
138
+ * Validate the QEMU binary exists before starting the VM
139
+ * Destroy failures now surface the underlying error and preserve the machine ID
140
+ * Warn when private_network is configured without `advanced_network`, when the
141
+ network backend needs sudo, and when other unsupported network types are used
142
+ * Add test suite: unit + acceptance + e2e (`rake spec:unit|acceptance|e2e`)
data/CLAUDE.md ADDED
@@ -0,0 +1,45 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ vagrant-qemu is a Vagrant provider plugin that manages virtual machines using QEMU. It is primarily designed for Apple Silicon (aarch64) but also supports x86_64. The plugin is distributed as a Ruby gem.
8
+
9
+ ## Build & Development Commands
10
+
11
+ ```bash
12
+ # Install dependencies (first time setup)
13
+ bundle config set --local path 'vendor/bundle'
14
+ bundle install
15
+
16
+ # Build the gem (outputs to pkg/)
17
+ bundle exec rake build
18
+
19
+ # Install locally in Vagrant
20
+ vagrant plugin install ./pkg/vagrant-qemu-<version>.gem
21
+
22
+ # Verify installation
23
+ vagrant plugin list | grep vagrant-qemu
24
+ ```
25
+
26
+ Tests (rspec) are defined in the Gemfile but currently commented out in the Rakefile. There is no active test suite.
27
+
28
+ ## Architecture
29
+
30
+ This is a standard Vagrant provider plugin following the Vagrant plugin v2 API. All source lives under `lib/vagrant-qemu/`.
31
+
32
+ **Key components:**
33
+
34
+ - **`plugin.rb`** — Registers the provider with Vagrant (name `:qemu`, box format `libvirt`), declares config and provider capabilities (disk management). Sets up i18n and logging.
35
+ - **`config.rb`** — All provider-specific config options (`qe.memory`, `qe.arch`, `qe.cpu`, etc.). Defaults target Apple Silicon (aarch64, HVF acceleration, cortex-a72 CPU). Options set to `nil` cause the corresponding QEMU arguments to be skipped entirely.
36
+ - **`provider.rb`** — Thin adapter between Vagrant and the Driver. Delegates actions to `Action.action_<name>`, exposes SSH info and machine state. NFS is explicitly disabled.
37
+ - **`driver.rb`** — Core QEMU interaction: builds the `qemu-system-*` command line, manages VM lifecycle via PID files and control sockets (unix socket or TCP port). Handles import (creates qcow2 overlay images via `qemu-img create`), start, stop (sends `system_powerdown` over control socket), and destroy.
38
+ - **`action.rb`** — Defines Vagrant action chains (up, halt, destroy, provision, reload, ssh) using `Vagrant::Action::Builder`. The `action_up` chain: HandleBox → ConfigValidate → Import (if not created) → CloudInit → Disk → Provision → Networking → StartInstance → WaitForCommunicator.
39
+ - **`cap/disk.rb`** — Provider capability for Vagrant's disk management (qcow2 and iso formats). Creates additional disks via `qemu-img create` and attaches them to the driver.
40
+
41
+ **VM state management:** State is determined by checking if the data directory exists (created?) and if the PID file references a running process (running?). VM data lives in `<data_dir>/<vm_id>/`, temp files in `~/.vagrant.d/tmp/vagrant-qemu/<vm_id>/`.
42
+
43
+ **Versioning:** Single source of truth in `lib/vagrant-qemu/version.rb`.
44
+
45
+ **I18n:** Locale strings in `locales/en.yml`.
data/README.md CHANGED
@@ -39,6 +39,7 @@ Others:
39
39
  * Basic suport to forwarded ports, see [vagrant doc](https://www.vagrantup.com/docs/networking/forwarded_ports) for details
40
40
  * Support Cloud-init, see [vagrant doc](https://developer.hashicorp.com/vagrant/docs/cloud-init/usage) for details
41
41
  * Support Disks, see [vagrant doc](https://developer.hashicorp.com/vagrant/docs/disks/usage) for details
42
+ * Advanced networking (opt-in): dual-NIC with `private_network` support via QEMU native vmnet (macOS), TAP (Linux), or a `socket` netdev — multicast (Linux/Windows) or point-to-point listen/connect (no root, works on macOS)
42
43
 
43
44
  ## Usage
44
45
 
@@ -81,20 +82,20 @@ This provider exposes a few provider-specific configuration options:
81
82
 
82
83
  * basic
83
84
  * `ssh_port` - The SSH port number used to access VM, default: `50022`
84
- * `arch` - The architecture of VM, default: `aarch64`
85
- * `machine` - The machine type of VM, default: `virt,accel=hvf,highmem=off`
86
- * `cpu` - The cpu model of VM, default: `cortex-a72`
85
+ * `arch` - The architecture of VM, default: auto-detected from the host (`aarch64` on Apple Silicon, `x86_64` on Intel)
86
+ * `machine` - The machine type of VM, default: auto-detected from host OS + arch. For native virtualization (guest arch == host arch): `virt,highmem=on,accel=hvf` (arm64) / `q35,accel=hvf` (x86_64) on macOS, `accel=kvm` on Linux, `accel=whpx` on Windows. When emulating a non-host arch it uses `accel=tcg`.
87
+ * `cpu` - The cpu model of VM, default: `host` for native virtualization, `max` when emulating a non-host arch
87
88
  * `smp` - The smp setting (Simulate an SMP system with n CPUs) of VM, default: `2`
88
89
  * `memory` - The memory setting of VM, default: `4G`
89
90
  * `disk_resize` - The target disk size of the primary disk, requires resizing of filesystem inside of VM, default: `nil`.
90
91
  * debug/expert
91
92
  * `ssh_host` - The SSH IP used to access VM, default: `127.0.0.1`
92
93
  * `ssh_auto_correct` - Auto correct port collisions for ssh port, default: `false`
93
- * `net_device` - The network device, default: `virtio-net-device`
94
+ * `net_device` - The network device, default: auto-detected — `virtio-net-device` (arm64 `virt`) or `virtio-net-pci` (x86_64 `q35`)
94
95
  * `drive_interface` - The interface type for the main drive, default `virtio`
95
96
  * `image_path` - The path (or array of paths) to qcow2 image for box-less VM, default is nil value
96
97
  * `qemu_bin` - Path to an alternative QEMU binary, default: autodetected
97
- * `qemu_dir` - The path to QEMU's install dir, default: `/opt/homebrew/share/qemu`
98
+ * `qemu_dir` - The path to QEMU's data/firmware dir. Default resolution order: `ENV["QEMU_DIR"]` → `${HOMEBREW_PREFIX}/share/qemu` → per-host default (`/opt/homebrew/share/qemu` on Apple Silicon, `/usr/local/share/qemu` on Intel macOS, `/usr/share/qemu` on Linux). Only consumed for aarch64 firmware; ignored (and not validated) for x86_64.
98
99
  * `extra_qemu_args` - The raw list of additional arguments to pass to QEMU. Use with extreme caution. (see "Force Multicore" below as example)
99
100
  * `extra_netdev_args` - extra, comma-separated arguments to pass to the -netdev parameter. Use with caution. (see "Force Local IP" below as example)
100
101
  * `extra_drive_args` - Add optional extra arguments to each drive attached, default: `[]`
@@ -104,6 +105,14 @@ This provider exposes a few provider-specific configuration options:
104
105
  * `firmware_format` - The format of aarch64 firmware images (`edk2-aarch64-code.fd` and `edk2-arm-vars.fd`) loaded from `qemu_dir`, default: `raw`
105
106
  * `other_default` - The other default arguments used by this plugin, default: `%W(-parallel null -monitor none -display none -vga none)`
106
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 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
+ * advanced networking (requires `advanced_network = true`)
110
+ * `advanced_network` - Enable dual-NIC advanced networking with `private_network` support, default: `false`
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`
112
+ * `vmnet_interface` - Physical interface for vmnet-bridged mode, default: `en0`
113
+ * `tap_device` - TAP device name for Linux tap backend, default: `nil` (uses `tap0`)
114
+ * `mcast_addr` - Convenience shortcut for the `:socket` backend's multicast address, default: `nil` (uses `230.0.0.1:1234`)
115
+ * `socket_opts` - Raw options for the `:socket` netdev, emitted verbatim as `-netdev socket,id=netN,<socket_opts>`. You pick the mode: `"mcast=230.0.0.1:1234"` (multicast, N-way), `"listen=:1234"` / `"connect=127.0.0.1:1234"` (point-to-point; you decide which VM listens and which connects — the no-root, macOS-friendly path). Overrides `mcast_addr`. Default: `nil` (falls back to multicast)
107
116
 
108
117
  ### Usage
109
118
 
@@ -174,6 +183,11 @@ end
174
183
 
175
184
  4. Work with a x86_64 box (basic config)
176
185
 
186
+ On an **Intel Mac** (or Linux x86_64 host) these defaults are now auto-detected,
187
+ so a plain `config.vm.box = "..."` with no provider overrides is usually enough.
188
+ The explicit settings below are only needed to **emulate** x86_64 on an Apple
189
+ Silicon host (cross-arch → TCG):
190
+
177
191
  ```
178
192
  Vagrant.configure(2) do |config|
179
193
  config.vm.box = "centos/7"
@@ -304,6 +318,83 @@ end
304
318
 
305
319
  See the [QEMU Documentation](https://www.qemu.org/docs/master/devel/multiple-iothreads.html) and [heiko-sieger.info/tuning-vm-disk-performance/](https://www.heiko-sieger.info/tuning-vm-disk-performance/) for more details.
306
320
 
321
+ 12. Advanced networking with private_network
322
+
323
+ Pick a backend with `net_mode`: QEMU's native vmnet.framework on macOS (requires sudo), TAP on Linux, or the `socket` netdev. The `:socket` backend is a thin wrapper around QEMU's `socket` netdev — you choose the mode in `socket_opts`: `mcast=` (multicast, N-way, Linux/Windows) or `listen=`/`connect=` (point-to-point, no root, works on macOS). The plugin creates two NICs: NIC 0 (user-mode for SSH and port forwarding) and NIC 1 (platform backend for VM networking). The static IP is delivered via a cloud-init NoCloud seed ISO that the plugin builds and attaches automatically; the NICs are matched by MAC address, never by interface order.
324
+
325
+ For VM-to-VM networking on macOS without sudo, use `:socket` with a `listen`/`connect` pair — you decide which VM listens and which connects:
326
+
327
+ ```ruby
328
+ Vagrant.configure("2") do |config|
329
+ PORT = 12399
330
+ # vm1 listens; define it first so it is up before vm2 connects.
331
+ config.vm.define "vm1" do |c|
332
+ c.vm.box = "perk/ubuntu-2204-arm64" # an aarch64 cloud-init box
333
+ c.vm.network "private_network", ip: "192.168.105.51"
334
+ c.vm.provider "qemu" do |qe|
335
+ qe.advanced_network = true
336
+ qe.net_mode = :socket
337
+ qe.socket_opts = "listen=127.0.0.1:#{PORT}"
338
+ qe.ssh_auto_correct = true
339
+ end
340
+ end
341
+ # vm2 connects to vm1.
342
+ config.vm.define "vm2" do |c|
343
+ c.vm.box = "perk/ubuntu-2204-arm64"
344
+ c.vm.network "private_network", ip: "192.168.105.52"
345
+ c.vm.provider "qemu" do |qe|
346
+ qe.advanced_network = true
347
+ qe.net_mode = :socket
348
+ qe.socket_opts = "connect=127.0.0.1:#{PORT}"
349
+ qe.ssh_auto_correct = true
350
+ end
351
+ end
352
+ end
353
+ ```
354
+
355
+ A single VM with a static IP (vmnet is the default backend on macOS when `net_mode` is `:auto`):
356
+
357
+ ```ruby
358
+ Vagrant.configure("2") do |config|
359
+ config.vm.box = "ppggff/centos-7-aarch64-2009-4K"
360
+ config.vm.network "private_network", ip: "192.168.105.10"
361
+
362
+ config.vm.provider "qemu" do |qe|
363
+ qe.advanced_network = true
364
+ # qe.net_mode = :vmnet_shared # default on macOS when :auto
365
+ end
366
+ end
367
+ ```
368
+
369
+ Notes:
370
+ * The guest image must include cloud-init, otherwise the static IP is silently not applied
371
+ * On macOS, vmnet requires root: run `sudo vagrant up` (and the other lifecycle commands such as `halt`/`reload`/`destroy`), because the plugin launches QEMU as a child of the Vagrant process and does not elevate it on its own. The plugin warns when vmnet is selected and Vagrant is not running as root.
372
+ * Side effect of running under `sudo`: QEMU and everything it writes become **root-owned** — the per-VM data directory (`.vagrant/machines/<name>/qemu/` in your project) and any box Vagrant downloads while elevated (`~/.vagrant.d/boxes/<box>/`). A later command run **without** `sudo` then fails with `EACCES` — e.g. a plain `vagrant status`/`up`, an unprivileged test run, or switching to a rootless backend — often on the box's `box_update_check` file. To handle it, either keep using `sudo` consistently for that environment, or restore ownership:
373
+ ```sh
374
+ sudo chown -R "$(id -un)":staff ~/.vagrant.d/boxes/<box> .vagrant
375
+ ```
376
+ Pre-adding boxes as your normal user (`vagrant box add <box>`) before the first `sudo vagrant up` also avoids the box ending up root-owned.
377
+ * To avoid root (and this side effect) entirely on macOS, use [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) — a small root helper daemon you install once, which QEMU then connects to as a normal user (the approach Lima/Colima/minikube take). The `com.apple.developer.networking.vmnet` entitlement could also bypass root in principle, but it is a *restricted* Apple entitlement that requires an Apple-provisioned signing certificate and cannot be ad-hoc / self-signed onto Homebrew's QEMU, so it is not a practical option for individual users.
378
+ * Without `advanced_network = true`, the `private_network` configuration is ignored with a warning
379
+ * When only one NIC is needed (no `private_network`), no cloud-init seed is attached, avoiding compatibility issues
380
+ * Combining `advanced_network` with `config.vm.cloud_init` is supported: the plugin merges your user-data and the generated network-config into a single NoCloud seed
381
+ * The Linux `:tap` backend expects a pre-created tap device attached to a bridge, e.g.:
382
+ `sudo ip tuntap add tap0 mode tap && sudo ip link set tap0 master br0 && sudo ip link set tap0 up`
383
+ * `socket_opts = "mcast=..."` gives N-way VM-to-VM on Linux/Windows, but does **not** work on macOS: QEMU binds the netdev socket to the multicast group address, which the Darwin socket stack refuses to send from (`EADDRNOTAVAIL`). On macOS use a `listen`/`connect` pair (no root) or vmnet (sudo).
384
+ * `socket_opts = "listen=..."` / `"connect=..."` is a point-to-point QEMU TCP link and connects **exactly two** VMs (QEMU's listening socket accepts a single connection — it is not a hub). You choose which VM listens and which connects. The listener must be running before the connector starts, so define the listener first and bring the environment up together (`vagrant up`); starting a connector alone, or reloading the listener, drops the link.
385
+
386
+ Platform support:
387
+
388
+ | Platform | Backend (`net_mode`) | Host ↔ VM | VM ↔ VM | Root? | External dependency |
389
+ |----------|---------|:---------:|:-------:|:-----:|:-------------------:|
390
+ | macOS | `:vmnet_shared`/`_host`/`_bridged` | Yes | Yes | sudo (or socket_vmnet) | None (QEMU >= 7.0) |
391
+ | macOS | `:socket` (`listen`/`connect`) | No (use port forwarding) | Yes (2 VMs) | No | None |
392
+ | Linux | `:tap` + bridge | Yes | Yes | sudo | Pre-created tap device + bridge (`ip` command) |
393
+ | Linux | `:socket` (`mcast`) | No (use port forwarding) | Yes | No | None |
394
+ | Windows | `:socket` (`mcast`) | No (use port forwarding) | Yes | No | None |
395
+
396
+ (`socket_opts = "mcast=..."` is not usable on macOS — see the note above; use a `listen`/`connect` pair there.)
397
+
307
398
  ## Debug
308
399
 
309
400
  Serial port is exported to unix socket: `<user_home>/.vagrant.d/tmp/vagrant-qemu/<id>/qemu_socket_serial`, or `debug_port`.
@@ -325,17 +416,84 @@ To send ctrl+c to GuestOS from `nc`, try:
325
416
 
326
417
  ## Build
327
418
 
328
- To build the `vagrant-qemu` plugin, clone this repository out, and use
329
- [Bundler](http://gembundler.com) to get the dependencies:
419
+ To build the `vagrant-qemu` plugin
330
420
 
331
- ```
332
- bundle
333
- ```
421
+ **Development Environment:**
422
+
423
+ Ensure your development environment has the necessary tools installed, such as:
424
+
425
+ * **Ruby**:
426
+ * [Ruby installation](https://www.ruby-lang.org/en/documentation/installation/)
427
+ * [Ruby Version Manager (RVM)](https://rvm.io/rvm/install)
428
+ * [Ruby Installer for Windows](https://rubyinstaller.org/)
429
+ * [Bundler](https://bundler.io/):
430
+ ```sh
431
+ gem install bundler
432
+ ```
433
+ * [Rake](https://github.com/ruby/rake)
434
+ ```sh
435
+ gem install rake
436
+ ```
437
+
438
+ 1. Clone this repository:
439
+ ```sh
440
+ git clone https://github.com/ppggff/vagrant-qemu.git
441
+ cd vagrant-qemu
442
+ ```
443
+
444
+ 2. Use [bundler](http://gembundler.com) to install the necessary dependencies to ensure all required Ruby gems are available for buidling the plugin out
445
+ ```sh
446
+ bundle config set --local path 'vendor/bundle'
447
+ bundle install
448
+ ```
449
+ > This command tells Bundler to install gems in the vendor/bundle directory within your project.
450
+
451
+ 3. Use `rake` to build the plugin. This command will package your changes into a gem file:
452
+
453
+ ```sh
454
+ bundle exec rake build
455
+ ```
456
+ > After running this command, you should see a `.gem` file created in the `pkg` directory within the repository. This file represents your built plugin.
334
457
 
335
- Once you have the dependencies, build with `rake`:
458
+ 4. Use `vagrant plugin install` to install the plugin from the local `.gem` file. This ensures that Vagrant uses the locally built version.
459
+
460
+ ```sh
461
+ vagrant plugin install ./pkg/vagrant-qemu-<version>.gem
462
+ ```
336
463
 
464
+ > Replace `<version>` with the actual version number of the locally built `.gem` file
465
+
466
+ ### Check Installed Plugins
467
+
468
+ After installation, verify that the locally built `vagrant-qemu` plugin is installed by running:
469
+
470
+ ```sh
471
+ vagrant plugin list | grep vagrant-qemu
337
472
  ```
473
+
474
+ > This command will list all installed plugins, and you should see the vagrant-qemu plugin with the locally built version.
475
+
476
+ ### Running Tests
477
+
478
+ ```sh
479
+ # Unit tests (fast, no QEMU needed)
480
+ bundle exec rake spec:unit
481
+
482
+ # Acceptance tests (mock QEMU, no real VM)
483
+ bundle exec rake spec:acceptance
484
+
485
+ # End-to-end tests (requires QEMU and a box image). e2e exercises the
486
+ # INSTALLED plugin — rebuild and reinstall first (the suite fails fast on
487
+ # a version mismatch):
338
488
  bundle exec rake build
489
+ vagrant plugin install ./pkg/vagrant-qemu-<version>.gem
490
+ TEST_QEMU=1 bundle exec rake spec:e2e
491
+
492
+ # End-to-end with vmnet (requires sudo + macOS; needs an aarch64 cloud-init box)
493
+ TEST_QEMU=1 TEST_VMNET=1 TEST_BOX_CLOUDINIT=perk/ubuntu-2204-arm64 sudo -E bundle exec rake spec:e2e
494
+
495
+ # All tests
496
+ bundle exec rake spec
339
497
  ```
340
498
 
341
499
  ## Known issue / Troubleshooting
@@ -398,25 +556,35 @@ If you get this error when running `vagrant up`
398
556
 
399
557
  ### 4. The box you're using with the QEMU provider ('default') is invalid
400
558
 
401
- This may cause by invalid default qemu dir (`/opt/homebrew/share/qemu`).
402
-
403
- You can find the correct one by:
559
+ `qemu_dir` is auto-detected (Homebrew prefix / per-host default) and is only
560
+ needed for **aarch64** firmware — x86_64 boots on SeaBIOS and no longer
561
+ validates it. If detection still picks the wrong path for an aarch64 box
562
+ (e.g. a MacPorts or custom QEMU install), set it explicitly. Find the correct
563
+ one with:
404
564
  ```
405
565
  echo `brew --prefix`/share/qemu
406
566
  ```
407
567
 
408
- And then set it (for example `/usr/local/share/qemu`) in the `Vagrantfile` as:
568
+ Then either export `QEMU_DIR` / `HOMEBREW_PREFIX`, or set it in the `Vagrantfile`:
409
569
  ```
410
570
  config.vm.provider "qemu" do |qe|
411
571
  qe.qemu_dir = "/usr/local/share/qemu"
412
572
  end
413
573
  ```
414
574
 
575
+ ### 5. `conflicting dependencies logger (= 1.6.0) and logger (= 1.6.1)` when installing the plugin
576
+
577
+ This is a Vagrant 2.4.2 packaging bug (bundled `logger` gem version conflict),
578
+ not a problem with this plugin — see
579
+ [hashicorp/vagrant#13534](https://github.com/hashicorp/vagrant/issues/13534).
580
+ Upgrade Vagrant to **2.4.3 or newer** (the Homebrew cask may lag; install the
581
+ official build from [vagrantup.com](https://www.vagrantup.com/downloads) if
582
+ needed). Do **not** work around it by pinning `logger` in a Gemfile — that tends
583
+ to deepen the conflict.
584
+
415
585
  ## TODO
416
586
 
417
587
  * Support NFS shared folder
418
588
  * Support package VM to box
419
589
  * More configures
420
- * Better error messages
421
- * Network
422
590
  * GUI mode
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
- # require 'rspec/core/rake_task'
3
+ require 'rspec/core/rake_task'
4
4
 
5
5
  # Immediately sync all stdout so that tools like buildbot can
6
6
  # immediately load in the output.
@@ -14,12 +14,29 @@ Dir.chdir(File.expand_path("../", __FILE__))
14
14
  # publishing.
15
15
  Bundler::GemHelper.install_tasks
16
16
 
17
- # Install the `spec` task so that we can run tests.
18
- # RSpec::Core::RakeTask.new(:spec) do |t|
19
- # t.rspec_opts = "--order defined"
20
- # end
21
- # Default task is to run the unit tests
22
- # task :default => :spec
17
+ # Test tasks
18
+ namespace :spec do
19
+ RSpec::Core::RakeTask.new(:unit) do |t|
20
+ t.pattern = "spec/unit/**/*_spec.rb"
21
+ t.rspec_opts = "--order defined"
22
+ end
23
+
24
+ RSpec::Core::RakeTask.new(:acceptance) do |t|
25
+ t.pattern = "spec/acceptance/**/*_spec.rb"
26
+ t.rspec_opts = "--order defined"
27
+ end
28
+
29
+ RSpec::Core::RakeTask.new(:e2e) do |t|
30
+ t.pattern = "spec/e2e/**/*_spec.rb"
31
+ t.rspec_opts = "--order defined"
32
+ end
33
+ end
34
+
35
+ desc "Run all specs"
36
+ RSpec::Core::RakeTask.new(:spec) do |t|
37
+ t.pattern = "spec/**/*_spec.rb"
38
+ t.rspec_opts = "--order defined"
39
+ end
23
40
 
24
41
  # build
25
42
  task :default => :build
@@ -0,0 +1,123 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require "yaml"
4
+
5
+ require "vagrant/action/builtin/cloud_init_setup"
6
+
7
+ require_relative "../network"
8
+
9
+ module VagrantPlugins
10
+ module QEMU
11
+ module Action
12
+ # Carries the cloud-init network-config for the advanced-network private
13
+ # NIC into a NoCloud cidata seed. The ISO build is delegated to the
14
+ # :create_iso host capability and the attach goes through the provider
15
+ # disk capability (cap/disk.rb).
16
+ #
17
+ # NoCloud reads user-data, meta-data and network-config from a single
18
+ # filesystem labelled "cidata"; two cidata volumes are ambiguous. So when
19
+ # core CloudInitSetup (which runs earlier in the chain) has already built
20
+ # a user-data seed, we rebuild that same ISO in place with network-config
21
+ # added instead of attaching a second seed.
22
+ class CloudInitNetwork
23
+ # Disk name core Vagrant::Action::Builtin::CloudInitSetup gives its
24
+ # user-data seed.
25
+ CORE_SEED_DISK_NAME = "vagrant-cloud_init-disk".freeze
26
+
27
+ def initialize(app, env)
28
+ @app = app
29
+ @logger = Log4r::Logger.new("vagrant_qemu::action::cloud_init_network")
30
+ end
31
+
32
+ def call(env)
33
+ machine = env[:machine]
34
+
35
+ pn = machine.config.vm.networks
36
+ .select { |t, _| t == :private_network }
37
+ .map { |_, opts| opts }
38
+ .first
39
+
40
+ if machine.provider_config.advanced_network && pn && pn[:ip]
41
+ existing = machine.config.vm.disks.find do |d|
42
+ d.type == :dvd && d.name == CORE_SEED_DISK_NAME
43
+ end
44
+
45
+ if existing
46
+ merge_into_seed(machine, env, pn, existing.file)
47
+ else
48
+ attach_network_seed(machine, env, pn)
49
+ end
50
+ end
51
+
52
+ @app.call(env)
53
+ end
54
+
55
+ private
56
+
57
+ # No core cloud-init seed: build our own network-only seed and attach
58
+ # it as a fresh :dvd disk.
59
+ def attach_network_seed(machine, env, pn)
60
+ iso_path = build_seed(machine, env, pn,
61
+ user_data: "#cloud-config\n",
62
+ file_destination: machine.data_dir.join("vagrant-qemu-network.iso"))
63
+
64
+ machine.config.vm.disk :dvd, file: iso_path.to_s, name: "vagrant-qemu-network-disk"
65
+ machine.config.vm.disks.each do |d|
66
+ d.finalize! if d.type == :dvd && d.file == iso_path.to_s
67
+ end
68
+ @logger.info("Attached cloud-init network seed ISO at #{iso_path}")
69
+ end
70
+
71
+ # Core CloudInitSetup already built a user-data seed and attached it.
72
+ # Rebuild that same ISO in place, carrying both the user-data and our
73
+ # network-config in one cidata volume. No second disk is registered.
74
+ def merge_into_seed(machine, env, pn, iso_path)
75
+ ud_cfgs = machine.config.vm.cloud_init_configs
76
+ .select { |c| c.type == :user_data }
77
+ setup = Vagrant::Action::Builtin::CloudInitSetup.new(->(_) {}, env)
78
+ user_data = setup.setup_user_data(machine, env, ud_cfgs).to_s
79
+
80
+ FileUtils.rm_f(iso_path)
81
+ build_seed(machine, env, pn,
82
+ user_data: user_data,
83
+ file_destination: Pathname.new(iso_path))
84
+ @logger.info("Merged cloud-init network seed into #{iso_path}")
85
+ end
86
+
87
+ # Write a NoCloud cidata seed (network-config + meta-data + user-data)
88
+ # and build the ISO via the :create_iso host capability. Returns the
89
+ # ISO path.
90
+ def build_seed(machine, env, pn, user_data:, file_destination:)
91
+ if !env[:env].host.capability?(:create_iso)
92
+ raise Vagrant::Errors::CreateIsoHostCapNotFound
93
+ end
94
+
95
+ mac0, mac1 = Network.nic_macs(machine.id, pn)
96
+ network_config = Network.build_network_config(
97
+ mac0: mac0,
98
+ mac1: mac1,
99
+ ip: pn[:ip],
100
+ netmask: pn[:netmask] || "255.255.255.0"
101
+ )
102
+
103
+ source_dir = Pathname.new(Dir.mktmpdir("vagrant-qemu-network-seed"))
104
+ begin
105
+ File.write(source_dir.join("network-config"), network_config)
106
+ File.write(source_dir.join("meta-data"),
107
+ { "instance-id" => "i-#{machine.id.to_s.split("-").join}" }.to_yaml)
108
+ File.write(source_dir.join("user-data"), user_data)
109
+
110
+ env[:env].host.capability(
111
+ :create_iso,
112
+ source_dir,
113
+ file_destination: file_destination,
114
+ volume_id: "cidata"
115
+ )
116
+ ensure
117
+ FileUtils.remove_entry(source_dir)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -8,7 +8,13 @@ module VagrantPlugins
8
8
 
9
9
  def call(env)
10
10
  env[:ui].info(I18n.t("vagrant_qemu.destroying"))
11
- env[:machine].provider.driver.delete
11
+ begin
12
+ env[:machine].provider.driver.delete
13
+ rescue => e
14
+ # Vagrant errors only render error_key translations; a bare
15
+ # String here would be silently dropped.
16
+ raise Errors::DestroyError, message: e.message
17
+ end
12
18
  env[:machine].id = nil
13
19
 
14
20
  @app.call(env)
@@ -61,12 +61,17 @@ module VagrantPlugins
61
61
  @logger.info("Found box image path: #{img_info}")
62
62
  end
63
63
 
64
+ # qemu_dir holds the firmware images, which are only consumed for the
65
+ # aarch64 target (see driver.rb). x86_64 boots on SeaBIOS and never
66
+ # touches qemu_dir, so don't let a missing dir block it.
64
67
  qemu_dir = Pathname.new(env[:machine].provider_config.qemu_dir)
65
- if !qemu_dir.directory?
66
- @logger.error("Invalid qemu dir: #{qemu_dir}")
67
- raise Errors::ConfigError, err: "Invalid qemu dir: #{qemu_dir}"
68
- else
69
- @logger.info("Found qemu dir: #{qemu_dir}")
68
+ if env[:machine].provider_config.arch == "aarch64"
69
+ if !qemu_dir.directory?
70
+ @logger.error("Invalid qemu dir: #{qemu_dir}")
71
+ raise Errors::ConfigError, err: "Invalid qemu dir: #{qemu_dir}"
72
+ else
73
+ @logger.info("Found qemu dir: #{qemu_dir}")
74
+ end
70
75
  end
71
76
 
72
77
  env[:ui].output("Importing a QEMU instance")
@@ -24,9 +24,9 @@ module VagrantPlugins
24
24
  env[:machine_state_id] = :not_created
25
25
  end
26
26
 
27
- # Update ssh_port if needed
27
+ # Update driver's runtime ssh_port from persisted options
28
28
  if env[:machine_state_id] == :running
29
- env[:machine].provider_config.ssh_port = env[:machine].provider.driver.get_ssh_port(env[:machine].provider_config.ssh_port)
29
+ env[:machine].provider.driver.get_ssh_port(env[:machine].provider_config.ssh_port)
30
30
  end
31
31
  @app.call(env)
32
32
  end