vagrant-qemu 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +39 -4
- data/lib/vagrant-qemu/action/cloud_init_network.rb +19 -4
- data/lib/vagrant-qemu/action/start_instance.rb +2 -0
- data/lib/vagrant-qemu/cap/disk.rb +14 -4
- data/lib/vagrant-qemu/config.rb +18 -2
- data/lib/vagrant-qemu/driver.rb +17 -0
- data/lib/vagrant-qemu/errors.rb +12 -0
- data/lib/vagrant-qemu/network/base.rb +17 -0
- data/lib/vagrant-qemu/network/socket_vmnet.rb +60 -0
- data/lib/vagrant-qemu/network.rb +32 -1
- data/lib/vagrant-qemu/version.rb +1 -1
- data/locales/en.yml +21 -0
- data/spec/e2e/disk_spec.rb +72 -0
- data/spec/e2e/helper.rb +1 -1
- data/spec/e2e/socket_vmnet_spec.rb +127 -0
- data/spec/e2e/support/qemu_no_stream_shim.sh +14 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/action/cloud_init_network_spec.rb +17 -2
- data/spec/unit/cap/disk_spec.rb +104 -0
- data/spec/unit/config_spec.rb +34 -0
- data/spec/unit/driver/start_cmd_socket_vmnet_spec.rb +144 -0
- data/spec/unit/network/backend_for_spec.rb +4 -0
- data/spec/unit/network/socket_vmnet_spec.rb +146 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7af3fcc9e9a9210bea30f44da695c87fbd15c24eed79de64540bb01ec5d4842
|
|
4
|
+
data.tar.gz: 805730ceb9574de982d6c20f4b8a7e04c37b13fbdc47ca902ddde6fcf844fef5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9613c43e9ea63da345bf0745b3825578cb9d32a8e9465b91a9beea36c571e085d9a325675d28e1a98fe655fbabffb0b29f8e6107e90a13dc504ba83e1b007c8
|
|
7
|
+
data.tar.gz: 96a64e28147f0a7c6c7994695d8b423088f7f3eb80c18288d2652120e24c5479d88d9d407455f10e0ab252a7cacbf0fb2f0c0ef2a22a403c625d3826d5002ec5
|
data/CHANGELOG.md
CHANGED
|
@@ -154,3 +154,35 @@
|
|
|
154
154
|
core packaging middleware. Environment-specific state — networking, MAC,
|
|
155
155
|
additional disks, DVDs/cloud-init seed ISOs, and firmware — is intentionally
|
|
156
156
|
not baked into the box.
|
|
157
|
+
|
|
158
|
+
# 0.6.0 (2026-07-21)
|
|
159
|
+
|
|
160
|
+
* `net_mode = :socket_vmnet` adds no-sudo private networking on macOS: the
|
|
161
|
+
[socket_vmnet](https://github.com/lima-vm/socket_vmnet) daemon holds the root
|
|
162
|
+
vmnet membership and QEMU connects to it as a normal user, so `vagrant up`
|
|
163
|
+
and the other lifecycle commands need no sudo. It reuses the advanced-network
|
|
164
|
+
dual-NIC + cloud-init path — NIC 0 user-mode (SSH + port forwarding), NIC 1
|
|
165
|
+
on socket_vmnet with the configured static IP — giving host↔VM and VM↔VM
|
|
166
|
+
connectivity. On QEMU >= 7.2 QEMU connects to the daemon directly
|
|
167
|
+
(`-netdev stream`); on older QEMU it falls back to the `socket_vmnet_client`
|
|
168
|
+
wrapper (`-netdev socket,fd=3`), chosen automatically by probing the QEMU
|
|
169
|
+
binary. New config: `socket_vmnet_socket` and `socket_vmnet_client` (default
|
|
170
|
+
to the Homebrew prefix). The static IP must fall within the daemon's subnet
|
|
171
|
+
(`--vmnet-gateway`, default `192.168.105.1/24`); see the README. (#41)
|
|
172
|
+
|
|
173
|
+
# 0.6.1 (2026-07-22)
|
|
174
|
+
|
|
175
|
+
* Fix `vagrant reload` (and any other same-process halt-then-start, e.g. the
|
|
176
|
+
`vagrant-reload` plugin) failing with a QEMU image write-lock error when a
|
|
177
|
+
custom disk (`config.vm.disk`) was configured. `configure_disks` re-runs on
|
|
178
|
+
every `action_start`, but the driver's attached-disk list only ever
|
|
179
|
+
accumulated, so a reload attached each custom disk twice on the same qemu
|
|
180
|
+
command line. (#41)
|
|
181
|
+
* Fix `qemu-img create` unconditionally recreating an already-existing custom
|
|
182
|
+
disk on every `configure_disks` call, which silently wiped the disk's
|
|
183
|
+
contents on every reload (independent of the bug above — resizing an
|
|
184
|
+
existing disk to a new configured size still isn't supported).
|
|
185
|
+
* Fix the cloud-init network seed ISO (used by `advanced_network` without a
|
|
186
|
+
core `cloud_init` user-data config, i.e. the `net_mode` examples above)
|
|
187
|
+
being re-registered as a duplicate disk on every same-process reload,
|
|
188
|
+
hitting the same QEMU write-lock error via a different disk.
|
data/README.md
CHANGED
|
@@ -108,11 +108,13 @@ This provider exposes a few provider-specific configuration options:
|
|
|
108
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
|
-
* `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`
|
|
111
|
+
* `net_mode` - Network backend: `:auto` (detect by platform), `:vmnet_shared`, `:vmnet_host`, `:vmnet_bridged` (macOS, sudo), `:socket_vmnet` (macOS, no sudo — via the socket_vmnet daemon), `:tap` (Linux), `:socket` (QEMU `socket` netdev — multicast or point-to-point, see `socket_opts`), default: `:auto`
|
|
112
112
|
* `vmnet_interface` - Physical interface for vmnet-bridged mode, default: `en0`
|
|
113
113
|
* `tap_device` - TAP device name for Linux tap backend, default: `nil` (uses `tap0`)
|
|
114
114
|
* `mcast_addr` - Convenience shortcut for the `:socket` backend's multicast address, default: `nil` (uses `230.0.0.1:1234`)
|
|
115
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)
|
|
116
|
+
* `socket_vmnet_socket` - Path to the socket_vmnet daemon's unix socket, used by the `:socket_vmnet` backend, default: `<homebrew-prefix>/var/run/socket_vmnet` (`HOMEBREW_PREFIX` env, else `/opt/homebrew` on Apple Silicon, `/usr/local` on Intel)
|
|
117
|
+
* `socket_vmnet_client` - Path to the `socket_vmnet_client` wrapper, used only on QEMU older than 7.2 (which lacks the native `stream` netdev), default: `<homebrew-prefix>/opt/socket_vmnet/bin/socket_vmnet_client`
|
|
116
118
|
|
|
117
119
|
### Usage
|
|
118
120
|
|
|
@@ -320,7 +322,7 @@ See the [QEMU Documentation](https://www.qemu.org/docs/master/devel/multiple-iot
|
|
|
320
322
|
|
|
321
323
|
12. Advanced networking with private_network
|
|
322
324
|
|
|
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.
|
|
325
|
+
Pick a backend with `net_mode`: QEMU's native vmnet.framework on macOS (`:vmnet_shared`/`_host`/`_bridged`, requires sudo), `:socket_vmnet` on macOS (full host↔VM and VM↔VM **without** sudo, via the [socket_vmnet](https://github.com/lima-vm/socket_vmnet) daemon), 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
326
|
|
|
325
327
|
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
328
|
|
|
@@ -366,6 +368,38 @@ Vagrant.configure("2") do |config|
|
|
|
366
368
|
end
|
|
367
369
|
```
|
|
368
370
|
|
|
371
|
+
Full networking on macOS **without sudo** with `:socket_vmnet` (host↔VM and VM↔VM, static IP):
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
Vagrant.configure("2") do |config|
|
|
375
|
+
config.vm.box = "perk/ubuntu-2204-arm64" # an aarch64 cloud-init box
|
|
376
|
+
config.vm.network "private_network", ip: "192.168.105.10"
|
|
377
|
+
|
|
378
|
+
config.vm.provider "qemu" do |qe|
|
|
379
|
+
qe.advanced_network = true
|
|
380
|
+
qe.net_mode = :socket_vmnet
|
|
381
|
+
# qe.socket_vmnet_socket = "/opt/homebrew/var/run/socket_vmnet" # default (Homebrew prefix)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Set up the daemon once (root, one-time), then run `vagrant up` as your normal user:
|
|
387
|
+
|
|
388
|
+
```sh
|
|
389
|
+
brew install socket_vmnet
|
|
390
|
+
# Reserve a static-IP range so cloud-init IPs don't collide with the DHCP pool:
|
|
391
|
+
sudo brew services start socket_vmnet # or run it with explicit flags, e.g.:
|
|
392
|
+
# sudo socket_vmnet --vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.100 \
|
|
393
|
+
# /opt/homebrew/var/run/socket_vmnet
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The `private_network` IP must sit inside the daemon's subnet (`--vmnet-gateway`, default
|
|
397
|
+
`192.168.105.1/24`) and, to avoid a DHCP clash, above `--vmnet-dhcp-end`. Unlike the native vmnet
|
|
398
|
+
backends, `:socket_vmnet` cannot tell the daemon which subnet to use — the daemon's launch flags are
|
|
399
|
+
authoritative, so align the IP to them. On QEMU ≥ 7.2 the plugin connects to the daemon directly
|
|
400
|
+
(`-netdev stream`); on older QEMU it automatically falls back to the `socket_vmnet_client` wrapper
|
|
401
|
+
(configurable via `qe.socket_vmnet_client`).
|
|
402
|
+
|
|
369
403
|
Notes:
|
|
370
404
|
* The guest image must include cloud-init, otherwise the static IP is silently not applied
|
|
371
405
|
* 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.
|
|
@@ -374,7 +408,7 @@ Notes:
|
|
|
374
408
|
sudo chown -R "$(id -un)":staff ~/.vagrant.d/boxes/<box> .vagrant
|
|
375
409
|
```
|
|
376
410
|
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)
|
|
411
|
+
* To avoid root (and this side effect) entirely on macOS, use `net_mode = :socket_vmnet` (see the example above): the [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) daemon holds the root vmnet membership and QEMU connects to it as a normal user (the approach Lima/Colima/minikube take), so `vagrant up` and the other lifecycle commands need no sudo. 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
412
|
* Without `advanced_network = true`, the `private_network` configuration is ignored with a warning
|
|
379
413
|
* When only one NIC is needed (no `private_network`), no cloud-init seed is attached, avoiding compatibility issues
|
|
380
414
|
* 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
|
|
@@ -387,7 +421,8 @@ Platform support:
|
|
|
387
421
|
|
|
388
422
|
| Platform | Backend (`net_mode`) | Host ↔ VM | VM ↔ VM | Root? | External dependency |
|
|
389
423
|
|----------|---------|:---------:|:-------:|:-----:|:-------------------:|
|
|
390
|
-
| macOS | `:vmnet_shared`/`_host`/`_bridged` | Yes | Yes | sudo
|
|
424
|
+
| macOS | `:vmnet_shared`/`_host`/`_bridged` | Yes | Yes | sudo | None (QEMU >= 7.0) |
|
|
425
|
+
| macOS | `:socket_vmnet` | Yes | Yes | No | socket_vmnet daemon (`brew install socket_vmnet`) |
|
|
391
426
|
| macOS | `:socket` (`listen`/`connect`) | No (use port forwarding) | Yes (2 VMs) | No | None |
|
|
392
427
|
| Linux | `:tap` + bridge | Yes | Yes | sudo | Pre-created tap device + bridge (`ip` command) |
|
|
393
428
|
| Linux | `:socket` (`mcast`) | No (use port forwarding) | Yes | No | None |
|
|
@@ -24,6 +24,10 @@ module VagrantPlugins
|
|
|
24
24
|
# user-data seed.
|
|
25
25
|
CORE_SEED_DISK_NAME = "vagrant-cloud_init-disk".freeze
|
|
26
26
|
|
|
27
|
+
# Disk name this action gives its own network-only seed (no core
|
|
28
|
+
# user-data seed present).
|
|
29
|
+
OWN_SEED_DISK_NAME = "vagrant-qemu-network-disk".freeze
|
|
30
|
+
|
|
27
31
|
def initialize(app, env)
|
|
28
32
|
@app = app
|
|
29
33
|
@logger = Log4r::Logger.new("vagrant_qemu::action::cloud_init_network")
|
|
@@ -55,13 +59,24 @@ module VagrantPlugins
|
|
|
55
59
|
private
|
|
56
60
|
|
|
57
61
|
# No core cloud-init seed: build our own network-only seed and attach
|
|
58
|
-
# it as a fresh :dvd disk.
|
|
62
|
+
# it as a fresh :dvd disk. This re-runs on every action_start
|
|
63
|
+
# (including a same-process reload), so a disk already registered
|
|
64
|
+
# from an earlier run is rebuilt in place rather than re-registered
|
|
65
|
+
# -- machine.config.vm.disks has no dedup of its own.
|
|
59
66
|
def attach_network_seed(machine, env, pn)
|
|
60
|
-
|
|
67
|
+
existing = machine.config.vm.disks.find do |d|
|
|
68
|
+
d.type == :dvd && d.name == OWN_SEED_DISK_NAME
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
iso_path = existing ? Pathname.new(existing.file) : machine.data_dir.join("vagrant-qemu-network.iso")
|
|
72
|
+
|
|
73
|
+
build_seed(machine, env, pn,
|
|
61
74
|
user_data: "#cloud-config\n",
|
|
62
|
-
file_destination:
|
|
75
|
+
file_destination: iso_path)
|
|
76
|
+
|
|
77
|
+
return if existing
|
|
63
78
|
|
|
64
|
-
machine.config.vm.disk :dvd, file: iso_path.to_s, name:
|
|
79
|
+
machine.config.vm.disk :dvd, file: iso_path.to_s, name: OWN_SEED_DISK_NAME
|
|
65
80
|
machine.config.vm.disks.each do |d|
|
|
66
81
|
d.finalize! if d.type == :dvd && d.file == iso_path.to_s
|
|
67
82
|
end
|
|
@@ -42,6 +42,8 @@ module VagrantPlugins
|
|
|
42
42
|
:tap_device => config.tap_device,
|
|
43
43
|
:mcast_addr => config.mcast_addr,
|
|
44
44
|
:socket_opts => config.socket_opts,
|
|
45
|
+
:socket_vmnet_socket => config.socket_vmnet_socket,
|
|
46
|
+
:socket_vmnet_client => config.socket_vmnet_client,
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
# Pick up SSH port that may have been corrected by HandleForwardedPortCollisions
|
|
@@ -32,6 +32,11 @@ module VagrantPlugins
|
|
|
32
32
|
# @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks
|
|
33
33
|
# @return [Hash] configured_disks - A hash of all the current configured disks
|
|
34
34
|
def self.configure_disks(machine, defined_disks)
|
|
35
|
+
# Vagrant re-invokes this on every action_start (including a
|
|
36
|
+
# same-process reload), always passing the complete current disk
|
|
37
|
+
# list -- reset before the empty check so a disk removed from the
|
|
38
|
+
# Vagrantfile doesn't leave a stale entry attached either.
|
|
39
|
+
machine.provider.driver.reset_attached_drives!
|
|
35
40
|
return {} if defined_disks.empty?
|
|
36
41
|
|
|
37
42
|
configured_disks = {disk: [], floppy: [], dvd: []}
|
|
@@ -79,12 +84,17 @@ module VagrantPlugins
|
|
|
79
84
|
def self.setup_disk(machine, disk)
|
|
80
85
|
disk_dir = machine.provider.driver.disk_dir
|
|
81
86
|
disk_path = disk_dir.join("#{disk.name}.#{disk.disk_ext}")
|
|
82
|
-
args = ["create", "-f", "qcow2"]
|
|
83
87
|
|
|
84
88
|
disk_provider_config = disk.provider_config[:qemu] if disk.provider_config
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
|
|
90
|
+
# configure_disks re-runs on every action_start (including a
|
|
91
|
+
# same-process reload); qemu-img create truncates an existing file
|
|
92
|
+
# unconditionally, so skip it once the disk already exists instead
|
|
93
|
+
# of silently wiping it. Resizing an existing disk isn't handled.
|
|
94
|
+
if !disk_path.file?
|
|
95
|
+
args = ["create", "-f", "qcow2", disk_path.to_s, "#{disk.size}"]
|
|
96
|
+
machine.provider.driver.execute("qemu-img", *args)
|
|
97
|
+
end
|
|
88
98
|
|
|
89
99
|
{UUID: disk.id, Name: disk.name, Path: disk_path.to_s, primary: !!disk.primary}
|
|
90
100
|
end
|
data/lib/vagrant-qemu/config.rb
CHANGED
|
@@ -29,7 +29,7 @@ module VagrantPlugins
|
|
|
29
29
|
attr_accessor :graceful_timeout # seconds to wait for guest shutdown before force kill
|
|
30
30
|
# Advanced networking options
|
|
31
31
|
attr_accessor :advanced_network # bool, opt-in for dual-NIC setup
|
|
32
|
-
attr_accessor :net_mode # :auto, :vmnet_shared, :vmnet_host, :vmnet_bridged, :tap, :socket
|
|
32
|
+
attr_accessor :net_mode # :auto, :vmnet_shared, :vmnet_host, :vmnet_bridged, :tap, :socket, :socket_vmnet
|
|
33
33
|
attr_accessor :vmnet_interface # physical interface for vmnet-bridged (e.g. "en0")
|
|
34
34
|
attr_accessor :tap_device # tap device name for Linux tap backend
|
|
35
35
|
attr_accessor :mcast_addr # convenience shortcut for the :socket backend's multicast address
|
|
@@ -39,6 +39,10 @@ module VagrantPlugins
|
|
|
39
39
|
# The mode (multicast vs point-to-point listen/connect) and any roles are
|
|
40
40
|
# entirely the user's choice. Overrides mcast_addr when set.
|
|
41
41
|
attr_accessor :socket_opts
|
|
42
|
+
# socket_vmnet backend (macOS): the daemon's unix socket, and the client
|
|
43
|
+
# wrapper used only on QEMU without the native `stream` netdev.
|
|
44
|
+
attr_accessor :socket_vmnet_socket
|
|
45
|
+
attr_accessor :socket_vmnet_client
|
|
42
46
|
|
|
43
47
|
def initialize
|
|
44
48
|
@ssh_host = UNSET_VALUE
|
|
@@ -71,6 +75,8 @@ module VagrantPlugins
|
|
|
71
75
|
@tap_device = UNSET_VALUE
|
|
72
76
|
@mcast_addr = UNSET_VALUE
|
|
73
77
|
@socket_opts = UNSET_VALUE
|
|
78
|
+
@socket_vmnet_socket = UNSET_VALUE
|
|
79
|
+
@socket_vmnet_client = UNSET_VALUE
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
#-------------------------------------------------------------------
|
|
@@ -123,6 +129,8 @@ module VagrantPlugins
|
|
|
123
129
|
@tap_device = nil if @tap_device == UNSET_VALUE
|
|
124
130
|
@mcast_addr = nil if @mcast_addr == UNSET_VALUE
|
|
125
131
|
@socket_opts = nil if @socket_opts == UNSET_VALUE
|
|
132
|
+
@socket_vmnet_socket = "#{homebrew_prefix}/var/run/socket_vmnet" if @socket_vmnet_socket == UNSET_VALUE
|
|
133
|
+
@socket_vmnet_client = "#{homebrew_prefix}/opt/socket_vmnet/bin/socket_vmnet_client" if @socket_vmnet_client == UNSET_VALUE
|
|
126
134
|
|
|
127
135
|
# TODO better error msg
|
|
128
136
|
@ssh_port = Integer(@ssh_port)
|
|
@@ -152,6 +160,14 @@ module VagrantPlugins
|
|
|
152
160
|
end
|
|
153
161
|
end
|
|
154
162
|
|
|
163
|
+
# Homebrew install prefix: env override, else the per-arch default
|
|
164
|
+
# (Apple Silicon vs Intel). Defaults to the host arch (socket_vmnet paths
|
|
165
|
+
# are host-side); default_qemu_dir passes the guest arch to keep its
|
|
166
|
+
# firmware-dir behavior.
|
|
167
|
+
def homebrew_prefix(arch = host_arch)
|
|
168
|
+
ENV["HOMEBREW_PREFIX"] || (arch == "aarch64" ? "/opt/homebrew" : "/usr/local")
|
|
169
|
+
end
|
|
170
|
+
|
|
155
171
|
# QEMU data dir (firmware images). Only actually consumed for aarch64
|
|
156
172
|
# firmware, but resolved generically: explicit env override, then the
|
|
157
173
|
# Homebrew prefix, then a per-platform default.
|
|
@@ -161,7 +177,7 @@ module VagrantPlugins
|
|
|
161
177
|
|
|
162
178
|
case RbConfig::CONFIG["host_os"]
|
|
163
179
|
when /darwin/
|
|
164
|
-
arch
|
|
180
|
+
"#{homebrew_prefix(arch)}/share/qemu"
|
|
165
181
|
else
|
|
166
182
|
"/usr/share/qemu"
|
|
167
183
|
end
|
data/lib/vagrant-qemu/driver.rb
CHANGED
|
@@ -114,6 +114,7 @@ module VagrantPlugins
|
|
|
114
114
|
cmd += %W(-m #{options[:memory]}) if !options[:memory].nil?
|
|
115
115
|
|
|
116
116
|
# network
|
|
117
|
+
launch_prefix = []
|
|
117
118
|
if !options[:net_device].nil?
|
|
118
119
|
private_networks = options[:private_networks] || []
|
|
119
120
|
use_advanced = options[:advanced_network] && !private_networks.empty?
|
|
@@ -139,8 +140,12 @@ module VagrantPlugins
|
|
|
139
140
|
# (the static-IP cloud-init seed is built and attached by the
|
|
140
141
|
# CloudInitNetwork action, not here)
|
|
141
142
|
backend = Network.backend_for(options[:net_mode])
|
|
143
|
+
backend.preflight!(options, qemu_binary)
|
|
142
144
|
cmd += %W(-device #{options[:net_device]},netdev=net1,mac=#{mac1})
|
|
143
145
|
cmd += backend.build_netdev_args("net1", options)
|
|
146
|
+
# launch_prefix is applied under the same gate as build_netdev_args
|
|
147
|
+
# (empty for every backend except socket_vmnet's wrapper route).
|
|
148
|
+
launch_prefix = backend.launch_prefix(options)
|
|
144
149
|
else
|
|
145
150
|
# Single NIC: user-mode only (original behavior, no cloud-init)
|
|
146
151
|
cmd += %W(-device #{options[:net_device]},netdev=net0)
|
|
@@ -206,6 +211,10 @@ module VagrantPlugins
|
|
|
206
211
|
# user-defined
|
|
207
212
|
cmd += options[:extra_qemu_args]
|
|
208
213
|
|
|
214
|
+
# socket_vmnet wrapper route: launch qemu under socket_vmnet_client
|
|
215
|
+
# (empty prefix otherwise -> zero difference from the current path).
|
|
216
|
+
cmd = launch_prefix + cmd
|
|
217
|
+
|
|
209
218
|
opts = {:detach => options[:no_daemonize]}
|
|
210
219
|
execute(*cmd, **opts)
|
|
211
220
|
end
|
|
@@ -463,6 +472,14 @@ module VagrantPlugins
|
|
|
463
472
|
end
|
|
464
473
|
end
|
|
465
474
|
|
|
475
|
+
# Vagrant's Disk middleware always passes the *complete* current disk
|
|
476
|
+
# list and re-runs on every action_start (including a same-process
|
|
477
|
+
# reload, which reuses this Driver instance) -- so attach_disk/attach_dvd
|
|
478
|
+
# must be rebuilt from a clean slate each time, not accumulated across calls.
|
|
479
|
+
def reset_attached_drives!
|
|
480
|
+
@attached_drives = {disk: [], floppy: [], dvd: []}
|
|
481
|
+
end
|
|
482
|
+
|
|
466
483
|
def attach_dvd(disk)
|
|
467
484
|
@attached_drives[:dvd] << disk
|
|
468
485
|
end
|
data/lib/vagrant-qemu/errors.rb
CHANGED
|
@@ -38,6 +38,18 @@ module VagrantPlugins
|
|
|
38
38
|
class DestroyError < VagrantQEMUError
|
|
39
39
|
error_key(:destroy_error)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
class SocketVmnetNotMacos < VagrantQEMUError
|
|
43
|
+
error_key(:socket_vmnet_not_macos)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class SocketVmnetSocketNotFound < VagrantQEMUError
|
|
47
|
+
error_key(:socket_vmnet_socket_not_found)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class SocketVmnetClientNotFound < VagrantQEMUError
|
|
51
|
+
error_key(:socket_vmnet_client_not_found)
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
@@ -15,6 +15,23 @@ module VagrantPlugins
|
|
|
15
15
|
def requires_sudo?
|
|
16
16
|
false
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# Tokens to prepend to the QEMU launch command (a wrapper program).
|
|
20
|
+
# Empty by default; only socket_vmnet's wrapper route is non-empty.
|
|
21
|
+
# The driver applies this under the same gate as build_netdev_args.
|
|
22
|
+
# @param options [Hash] provider config options
|
|
23
|
+
# @return [Array<String>]
|
|
24
|
+
def launch_prefix(options)
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validate preconditions and resolve any backend-specific options
|
|
29
|
+
# before the command is built. No-op by default; the driver calls it
|
|
30
|
+
# under the same gate as build_netdev_args.
|
|
31
|
+
# @param options [Hash] provider config options (may be mutated)
|
|
32
|
+
# @param qemu_binary [String] the resolved qemu-system-* binary
|
|
33
|
+
def preflight!(options, qemu_binary)
|
|
34
|
+
end
|
|
18
35
|
end
|
|
19
36
|
end
|
|
20
37
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
require_relative "../errors"
|
|
3
|
+
|
|
4
|
+
module VagrantPlugins
|
|
5
|
+
module QEMU
|
|
6
|
+
module Network
|
|
7
|
+
# socket_vmnet backend (macOS) -- connects the private NIC to a
|
|
8
|
+
# socket_vmnet daemon so QEMU runs without sudo (the daemon holds the
|
|
9
|
+
# root vmnet.framework membership).
|
|
10
|
+
#
|
|
11
|
+
# Two routes, chosen by whether the QEMU binary has the native `stream`
|
|
12
|
+
# netdev (options[:use_stream], set by the driver from a probe):
|
|
13
|
+
# stream (QEMU >= 7.2): QEMU connects to the daemon's unix socket
|
|
14
|
+
# directly -- a pure netdev backend, no launch wrapper.
|
|
15
|
+
# wrapper (older QEMU): QEMU is launched under `socket_vmnet_client
|
|
16
|
+
# <sock>`, which hands the connection in on fd 3.
|
|
17
|
+
#
|
|
18
|
+
# build_netdev_args and launch_prefix are driven by the same use_stream
|
|
19
|
+
# so they never disagree.
|
|
20
|
+
class SocketVmnet < Base
|
|
21
|
+
# Resolve the route and fail fast on missing preconditions. Sets
|
|
22
|
+
# options[:use_stream]: true when the qemu binary has the native
|
|
23
|
+
# `stream` netdev, false when it definitively lacks it, and true when
|
|
24
|
+
# the probe couldn't determine (rare) -- stream needs no extra dependency.
|
|
25
|
+
def preflight!(options, qemu_binary)
|
|
26
|
+
raise Errors::SocketVmnetNotMacos unless RbConfig::CONFIG["host_os"] =~ /darwin/
|
|
27
|
+
|
|
28
|
+
options[:use_stream] = Network.qemu_supports_stream?(qemu_binary) != false
|
|
29
|
+
|
|
30
|
+
socket = options[:socket_vmnet_socket]
|
|
31
|
+
unless socket && File.exist?(socket)
|
|
32
|
+
raise Errors::SocketVmnetSocketNotFound, socket: socket
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return if options[:use_stream]
|
|
36
|
+
|
|
37
|
+
client = options[:socket_vmnet_client]
|
|
38
|
+
unless client && (::Vagrant::Util::Which.which(client) || File.executable?(client))
|
|
39
|
+
raise Errors::SocketVmnetClientNotFound, client: client
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_netdev_args(id, options)
|
|
44
|
+
if options[:use_stream]
|
|
45
|
+
sock = options[:socket_vmnet_socket]
|
|
46
|
+
%W(-netdev stream,id=#{id},server=off,addr.type=unix,addr.path=#{sock})
|
|
47
|
+
else
|
|
48
|
+
%W(-netdev socket,id=#{id},fd=3)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def launch_prefix(options)
|
|
53
|
+
return [] if options[:use_stream]
|
|
54
|
+
|
|
55
|
+
%W(#{options[:socket_vmnet_client]} #{options[:socket_vmnet_socket]})
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/vagrant-qemu/network.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "network/base"
|
|
|
2
2
|
require_relative "network/vmnet"
|
|
3
3
|
require_relative "network/tap"
|
|
4
4
|
require_relative "network/socket"
|
|
5
|
+
require_relative "network/socket_vmnet"
|
|
5
6
|
|
|
6
7
|
module VagrantPlugins
|
|
7
8
|
module QEMU
|
|
@@ -9,7 +10,7 @@ module VagrantPlugins
|
|
|
9
10
|
# Select the appropriate network backend based on net_mode and platform.
|
|
10
11
|
#
|
|
11
12
|
# @param net_mode [Symbol] :auto, :vmnet_shared, :vmnet_host,
|
|
12
|
-
# :vmnet_bridged, :tap, :socket
|
|
13
|
+
# :vmnet_bridged, :tap, :socket, :socket_vmnet
|
|
13
14
|
# @return [Base] a network backend instance
|
|
14
15
|
def self.backend_for(net_mode)
|
|
15
16
|
case net_mode
|
|
@@ -19,6 +20,8 @@ module VagrantPlugins
|
|
|
19
20
|
Tap.new
|
|
20
21
|
when :socket
|
|
21
22
|
Socket.new
|
|
23
|
+
when :socket_vmnet
|
|
24
|
+
SocketVmnet.new
|
|
22
25
|
when :auto
|
|
23
26
|
auto_detect
|
|
24
27
|
else
|
|
@@ -39,6 +42,34 @@ module VagrantPlugins
|
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
# Whether the QEMU binary has the native `stream` netdev (QEMU >= 7.2),
|
|
46
|
+
# so socket_vmnet can connect to the daemon's unix socket directly
|
|
47
|
+
# instead of going through the socket_vmnet_client wrapper.
|
|
48
|
+
#
|
|
49
|
+
# Probes `<qemu> -M none -netdev help` and looks for a `stream` line on
|
|
50
|
+
# stdout. Cached per binary.
|
|
51
|
+
#
|
|
52
|
+
# Tri-state so the driver can honor the design's fallback direction:
|
|
53
|
+
# true -- probe ran, stream present -> use stream
|
|
54
|
+
# false -- probe ran, stream absent -> use the wrapper (old QEMU)
|
|
55
|
+
# nil -- probe could not run (rare) -> driver defaults to stream
|
|
56
|
+
#
|
|
57
|
+
# @param qemu_binary [String] path/name of the qemu-system-* binary
|
|
58
|
+
# @return [Boolean, nil]
|
|
59
|
+
def self.qemu_supports_stream?(qemu_binary)
|
|
60
|
+
@stream_support ||= {}
|
|
61
|
+
return @stream_support[qemu_binary] if @stream_support.key?(qemu_binary)
|
|
62
|
+
|
|
63
|
+
result =
|
|
64
|
+
begin
|
|
65
|
+
out = ::Vagrant::Util::Subprocess.execute(qemu_binary, "-M", "none", "-netdev", "help")
|
|
66
|
+
out.exit_code == 0 ? out.stdout.lines.any? { |l| l.strip == "stream" } : nil
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
@stream_support[qemu_binary] = result
|
|
71
|
+
end
|
|
72
|
+
|
|
42
73
|
# Generate a deterministic MAC address from vm_id and NIC index.
|
|
43
74
|
# Uses 52:54:00 prefix (QEMU's OUI).
|
|
44
75
|
#
|
data/lib/vagrant-qemu/version.rb
CHANGED
data/locales/en.yml
CHANGED
|
@@ -84,3 +84,24 @@ en:
|
|
|
84
84
|
|
|
85
85
|
The machine ID has been preserved. Fix the underlying issue and
|
|
86
86
|
run `vagrant destroy` again.
|
|
87
|
+
socket_vmnet_not_macos: |-
|
|
88
|
+
net_mode = :socket_vmnet is only supported on macOS (it relies on
|
|
89
|
+
the socket_vmnet daemon and vmnet.framework).
|
|
90
|
+
socket_vmnet_socket_not_found: |-
|
|
91
|
+
The socket_vmnet daemon socket was not found:
|
|
92
|
+
|
|
93
|
+
%{socket}
|
|
94
|
+
|
|
95
|
+
Install and start the daemon first, e.g.:
|
|
96
|
+
brew install socket_vmnet
|
|
97
|
+
sudo brew services start socket_vmnet
|
|
98
|
+
|
|
99
|
+
Or point qe.socket_vmnet_socket at the daemon's socket path.
|
|
100
|
+
socket_vmnet_client_not_found: |-
|
|
101
|
+
This QEMU lacks the native `stream` netdev (QEMU < 7.2), so the
|
|
102
|
+
socket_vmnet_client wrapper is required but was not found:
|
|
103
|
+
|
|
104
|
+
%{client}
|
|
105
|
+
|
|
106
|
+
Install socket_vmnet (which ships the client), upgrade QEMU to >= 7.2,
|
|
107
|
+
or point qe.socket_vmnet_client at the wrapper binary.
|
data/spec/e2e/disk_spec.rb
CHANGED
|
@@ -32,4 +32,76 @@ describe "extra disk attachment end-to-end", :requires_qemu do
|
|
|
32
32
|
expect(ssh[:exit_code]).to eq 0
|
|
33
33
|
expect(ssh[:stdout].lines.last.to_s.strip.to_i).to be >= 2
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
it "reload with an extra disk does not hit a QEMU image-lock error" do
|
|
37
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
38
|
+
Vagrant.configure("2") do |config|
|
|
39
|
+
config.vm.box = "#{test_box}"
|
|
40
|
+
config.vm.box_check_update = false
|
|
41
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
42
|
+
config.vm.disk :disk, name: "extra", size: "1GB"
|
|
43
|
+
config.vm.provider "qemu" do |qe|
|
|
44
|
+
qe.memory = "2G"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
|
|
49
|
+
result = vagrant_up(@work_dir, timeout: 600)
|
|
50
|
+
expect(result[:exit_code]).to eq 0
|
|
51
|
+
|
|
52
|
+
# This is the exact same-process halt->start path a same-process reload
|
|
53
|
+
# takes (issue #41: reload used to re-attach the extra disk a second
|
|
54
|
+
# time, so QEMU refused its own duplicate -drive with a write-lock error).
|
|
55
|
+
reload = vagrant_reload(@work_dir, timeout: 600)
|
|
56
|
+
expect(reload[:exit_code]).to eq 0
|
|
57
|
+
expect(reload[:stderr]).not_to match(/Failed to get "write" lock/)
|
|
58
|
+
|
|
59
|
+
ssh = vagrant_ssh(@work_dir, command: %q{lsblk -d -n -o TYPE | grep -c '^disk$'})
|
|
60
|
+
expect(ssh[:exit_code]).to eq 0
|
|
61
|
+
expect(ssh[:stdout].lines.last.to_s.strip.to_i).to be >= 2
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "reload does not wipe data already written to an extra disk" do
|
|
65
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
66
|
+
Vagrant.configure("2") do |config|
|
|
67
|
+
config.vm.box = "#{test_box}"
|
|
68
|
+
config.vm.box_check_update = false
|
|
69
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
70
|
+
config.vm.disk :disk, name: "extra", size: "1GB"
|
|
71
|
+
config.vm.provider "qemu" do |qe|
|
|
72
|
+
qe.memory = "2G"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
RUBY
|
|
76
|
+
|
|
77
|
+
result = vagrant_up(@work_dir, timeout: 600)
|
|
78
|
+
expect(result[:exit_code]).to eq 0
|
|
79
|
+
|
|
80
|
+
setup = vagrant_ssh(@work_dir, command: <<~SH)
|
|
81
|
+
set -e
|
|
82
|
+
dev=$(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}' | grep -v vda | head -1)
|
|
83
|
+
sudo mkfs.ext4 -F /dev/$dev
|
|
84
|
+
sudo mkdir -p /mnt/extra
|
|
85
|
+
sudo mount /dev/$dev /mnt/extra
|
|
86
|
+
echo marker-should-survive-reload | sudo tee /mnt/extra/marker.txt
|
|
87
|
+
sudo umount /mnt/extra
|
|
88
|
+
SH
|
|
89
|
+
expect(setup[:exit_code]).to eq 0
|
|
90
|
+
|
|
91
|
+
# configure_disks unconditionally ran `qemu-img create` on every
|
|
92
|
+
# action_start (including this same-process reload), silently
|
|
93
|
+
# truncating the extra disk every time -- this is the regression test.
|
|
94
|
+
reload = vagrant_reload(@work_dir, timeout: 600)
|
|
95
|
+
expect(reload[:exit_code]).to eq 0
|
|
96
|
+
|
|
97
|
+
check = vagrant_ssh(@work_dir, command: <<~SH)
|
|
98
|
+
set -e
|
|
99
|
+
dev=$(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}' | grep -v vda | head -1)
|
|
100
|
+
sudo mkdir -p /mnt/extra
|
|
101
|
+
sudo mount /dev/$dev /mnt/extra
|
|
102
|
+
cat /mnt/extra/marker.txt
|
|
103
|
+
SH
|
|
104
|
+
expect(check[:exit_code]).to eq 0
|
|
105
|
+
expect(check[:stdout]).to include("marker-should-survive-reload")
|
|
106
|
+
end
|
|
35
107
|
end
|
data/spec/e2e/helper.rb
CHANGED
|
@@ -130,7 +130,7 @@ RSpec.configure do |config|
|
|
|
130
130
|
# e2e runs the INSTALLED plugin (see note above) — fail fast when it
|
|
131
131
|
# doesn't match the source tree, instead of silently testing a stale gem.
|
|
132
132
|
config.before(:suite) do
|
|
133
|
-
next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"]
|
|
133
|
+
next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"] || ENV["TEST_SOCKET_VMNET"]
|
|
134
134
|
|
|
135
135
|
require "vagrant-qemu/version"
|
|
136
136
|
expected = VagrantPlugins::QEMU::VERSION
|