vagrant-qemu 0.5.0 → 0.6.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 +15 -0
- data/README.md +39 -4
- data/lib/vagrant-qemu/action/start_instance.rb +2 -0
- data/lib/vagrant-qemu/config.rb +18 -2
- data/lib/vagrant-qemu/driver.rb +9 -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/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 +1 -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 +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ee57a8b6cb0315b36c0084239e92ed69633698845945e6290ba289d71e2889
|
|
4
|
+
data.tar.gz: 99985f04eabced2eabae1914eacb19f053f13613a4f18d40047ac57440bd4a52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffc285799ee3f7be527da85b03d539c6f1f6b9278bc474c00b63eef41ea937bd8d5bf17aae125d801d3e1a9404967b1df9f0fa2b3173f3db452ed0fc60a12727
|
|
7
|
+
data.tar.gz: 43601507f85d2bb8e884e24795cdbb64c4d2d03f3842c1a8feb5cefe437241cdc0bcbbc531da4036540b3e19f6aaf831f9b35cad6c91c29c32da42be5f90abb0
|
data/CHANGELOG.md
CHANGED
|
@@ -154,3 +154,18 @@
|
|
|
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)
|
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 |
|
|
@@ -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
|
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
|
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/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
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Requires the socket_vmnet daemon running (no sudo for vagrant itself):
|
|
4
|
+
# brew install socket_vmnet && sudo brew services start socket_vmnet
|
|
5
|
+
# Run with: TEST_SOCKET_VMNET=1 bundle exec rspec spec/e2e/socket_vmnet_spec.rb
|
|
6
|
+
describe "socket_vmnet advanced networking end-to-end", :requires_socket_vmnet do
|
|
7
|
+
around(:each) do |example|
|
|
8
|
+
with_temp_dir do |dir|
|
|
9
|
+
@work_dir = dir.join("project")
|
|
10
|
+
FileUtils.mkdir_p(@work_dir)
|
|
11
|
+
example.run
|
|
12
|
+
vagrant_destroy(@work_dir) rescue nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "VM gets the configured static IP (no sudo)" do
|
|
17
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
18
|
+
Vagrant.configure("2") do |config|
|
|
19
|
+
config.vm.box = "#{test_box_cloudinit}"
|
|
20
|
+
config.vm.box_check_update = false
|
|
21
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
22
|
+
config.vm.network "private_network", ip: "192.168.105.10"
|
|
23
|
+
config.vm.provider "qemu" do |qe|
|
|
24
|
+
qe.memory = "2G"
|
|
25
|
+
qe.advanced_network = true
|
|
26
|
+
qe.net_mode = :socket_vmnet
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
|
|
31
|
+
vagrant_up(@work_dir)
|
|
32
|
+
result = vagrant_ssh(@work_dir, command: "ip addr show")
|
|
33
|
+
expect(result[:stdout]).to include("192.168.105.10")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "host can ping the VM IP" do
|
|
37
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
38
|
+
Vagrant.configure("2") do |config|
|
|
39
|
+
config.vm.box = "#{test_box_cloudinit}"
|
|
40
|
+
config.vm.box_check_update = false
|
|
41
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
42
|
+
config.vm.network "private_network", ip: "192.168.105.11"
|
|
43
|
+
config.vm.provider "qemu" do |qe|
|
|
44
|
+
qe.memory = "2G"
|
|
45
|
+
qe.advanced_network = true
|
|
46
|
+
qe.net_mode = :socket_vmnet
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
RUBY
|
|
50
|
+
|
|
51
|
+
vagrant_up(@work_dir)
|
|
52
|
+
`ping -c 1 -t 5 192.168.105.11 2>&1`
|
|
53
|
+
expect($?.exitstatus).to eq 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "two VMs can communicate via the socket_vmnet private network" do
|
|
57
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
58
|
+
Vagrant.configure("2") do |config|
|
|
59
|
+
config.vm.define "vm1" do |c|
|
|
60
|
+
c.vm.box = "#{test_box_cloudinit}"
|
|
61
|
+
c.vm.box_check_update = false
|
|
62
|
+
c.vm.synced_folder ".", "/vagrant", disabled: true
|
|
63
|
+
c.vm.network "private_network", ip: "192.168.105.20"
|
|
64
|
+
c.vm.provider "qemu" do |qe|
|
|
65
|
+
qe.memory = "2G"
|
|
66
|
+
qe.advanced_network = true
|
|
67
|
+
qe.net_mode = :socket_vmnet
|
|
68
|
+
qe.ssh_auto_correct = true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
config.vm.define "vm2" do |c|
|
|
73
|
+
c.vm.box = "#{test_box_cloudinit}"
|
|
74
|
+
c.vm.box_check_update = false
|
|
75
|
+
c.vm.synced_folder ".", "/vagrant", disabled: true
|
|
76
|
+
c.vm.network "private_network", ip: "192.168.105.21"
|
|
77
|
+
c.vm.provider "qemu" do |qe|
|
|
78
|
+
qe.memory = "2G"
|
|
79
|
+
qe.advanced_network = true
|
|
80
|
+
qe.net_mode = :socket_vmnet
|
|
81
|
+
qe.ssh_auto_correct = true
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
RUBY
|
|
86
|
+
|
|
87
|
+
vagrant_up(@work_dir, timeout: 600)
|
|
88
|
+
result = vagrant_ssh(@work_dir, machine: "vm1", command: "ping -c 1 -W 5 192.168.105.21")
|
|
89
|
+
expect(result[:exit_code]).to eq 0
|
|
90
|
+
expect(result[:stdout]).to include(" 0% packet loss")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# On QEMU >= 7.2 the plugin picks the stream route; a shim that hides the
|
|
94
|
+
# `stream` netdev from the probe forces the wrapper route (socket_vmnet_client
|
|
95
|
+
# + `-netdev socket,fd=3`) so it too gets exercised end-to-end -- in
|
|
96
|
+
# particular that the fd-3 convention survives the plugin's ChildProcess
|
|
97
|
+
# launcher. socket_vmnet_client itself connects on whatever fd `socket()`
|
|
98
|
+
# returns, so this proves nothing extra is leaked into fd 3.
|
|
99
|
+
it "wrapper route (forced via a no-stream shim) connects over fd 3" do
|
|
100
|
+
shim = File.expand_path("support/qemu_no_stream_shim.sh", __dir__)
|
|
101
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
102
|
+
Vagrant.configure("2") do |config|
|
|
103
|
+
config.vm.box = "#{test_box_cloudinit}"
|
|
104
|
+
config.vm.box_check_update = false
|
|
105
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
106
|
+
config.vm.network "private_network", ip: "192.168.105.13"
|
|
107
|
+
config.vm.provider "qemu" do |qe|
|
|
108
|
+
qe.memory = "2G"
|
|
109
|
+
qe.advanced_network = true
|
|
110
|
+
qe.net_mode = :socket_vmnet
|
|
111
|
+
qe.qemu_bin = "#{shim}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
RUBY
|
|
115
|
+
|
|
116
|
+
vagrant_up(@work_dir)
|
|
117
|
+
|
|
118
|
+
# The wrapper route was genuinely taken (not stream): the running QEMU
|
|
119
|
+
# carries the fd-3 socket netdev, not a stream netdev.
|
|
120
|
+
qemu_args = `ps -Ao args 2>/dev/null`
|
|
121
|
+
expect(qemu_args).to include("socket,id=net1,fd=3")
|
|
122
|
+
expect(qemu_args).not_to include("-netdev stream")
|
|
123
|
+
|
|
124
|
+
result = vagrant_ssh(@work_dir, command: "ip addr show")
|
|
125
|
+
expect(result[:stdout]).to include("192.168.105.13")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Test shim: forces vagrant-qemu's socket_vmnet backend down the wrapper route
|
|
3
|
+
# on a modern QEMU by hiding the native `stream` netdev from the plugin's probe
|
|
4
|
+
# (`-M none -netdev help`). Every other invocation execs the real QEMU verbatim,
|
|
5
|
+
# so the fd 3 handed in by socket_vmnet_client is preserved into QEMU.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
REAL="${REAL_QEMU:-$(command -v qemu-system-aarch64 || true)}"
|
|
8
|
+
[ -x "$REAL" ] || REAL=/opt/homebrew/bin/qemu-system-aarch64
|
|
9
|
+
|
|
10
|
+
if [ "$*" = "-M none -netdev help" ]; then
|
|
11
|
+
"$REAL" "$@" | grep -v '^stream$'
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
exec "$REAL" "$@"
|
data/spec/spec_helper.rb
CHANGED
|
@@ -33,6 +33,7 @@ RSpec.configure do |config|
|
|
|
33
33
|
config.order = :defined
|
|
34
34
|
config.filter_run_excluding :requires_qemu unless ENV["TEST_QEMU"]
|
|
35
35
|
config.filter_run_excluding :requires_vmnet unless ENV["TEST_VMNET"]
|
|
36
|
+
config.filter_run_excluding :requires_socket_vmnet unless ENV["TEST_SOCKET_VMNET"]
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Helper to create a temporary directory that is cleaned up after the test
|
data/spec/unit/config_spec.rb
CHANGED
|
@@ -140,4 +140,38 @@ describe VagrantPlugins::QEMU::Config do
|
|
|
140
140
|
expect(config.net_mode).to eq :vmnet_shared
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
|
+
|
|
144
|
+
describe "socket_vmnet paths" do
|
|
145
|
+
it "defaults to Apple Silicon Homebrew prefix when HOMEBREW_PREFIX is unset" do
|
|
146
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
147
|
+
allow(ENV).to receive(:[]).with("HOMEBREW_PREFIX").and_return(nil)
|
|
148
|
+
config = finalized(host_arch: "aarch64", host_accel: "hvf")
|
|
149
|
+
expect(config.socket_vmnet_socket).to eq "/opt/homebrew/var/run/socket_vmnet"
|
|
150
|
+
expect(config.socket_vmnet_client).to eq "/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet_client"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "defaults to Intel Homebrew prefix when HOMEBREW_PREFIX is unset" do
|
|
154
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
155
|
+
allow(ENV).to receive(:[]).with("HOMEBREW_PREFIX").and_return(nil)
|
|
156
|
+
config = finalized(host_arch: "x86_64", host_accel: "hvf")
|
|
157
|
+
expect(config.socket_vmnet_socket).to eq "/usr/local/var/run/socket_vmnet"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "derives from HOMEBREW_PREFIX when set" do
|
|
161
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
162
|
+
allow(ENV).to receive(:[]).with("HOMEBREW_PREFIX").and_return("/opt/hb")
|
|
163
|
+
config = finalized(host_arch: "aarch64", host_accel: "hvf")
|
|
164
|
+
expect(config.socket_vmnet_socket).to eq "/opt/hb/var/run/socket_vmnet"
|
|
165
|
+
expect(config.socket_vmnet_client).to eq "/opt/hb/opt/socket_vmnet/bin/socket_vmnet_client"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "preserves user-specified paths" do
|
|
169
|
+
config = described_class.new
|
|
170
|
+
config.socket_vmnet_socket = "/tmp/sv.sock"
|
|
171
|
+
config.socket_vmnet_client = "/tmp/sv_client"
|
|
172
|
+
config.finalize!
|
|
173
|
+
expect(config.socket_vmnet_socket).to eq "/tmp/sv.sock"
|
|
174
|
+
expect(config.socket_vmnet_client).to eq "/tmp/sv_client"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
143
177
|
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Driver, "start command line (socket_vmnet)" do
|
|
4
|
+
let(:vm_id) { "vq_svtest00042" }
|
|
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)
|
|
11
|
+
FileUtils.mkdir_p(@tmp_base)
|
|
12
|
+
|
|
13
|
+
id_dir = @data_dir.join(vm_id)
|
|
14
|
+
FileUtils.mkdir_p(id_dir)
|
|
15
|
+
FileUtils.touch(id_dir.join("linked-box.img"))
|
|
16
|
+
FileUtils.touch(id_dir.join("edk2-aarch64-code.fd"))
|
|
17
|
+
FileUtils.touch(id_dir.join("edk2-arm-vars.fd"))
|
|
18
|
+
|
|
19
|
+
# Real socket + client files so File.exist?/executable? preconditions pass.
|
|
20
|
+
@sock = @data_dir.join("socket_vmnet").to_s
|
|
21
|
+
FileUtils.touch(@sock)
|
|
22
|
+
@client = @data_dir.join("socket_vmnet_client").to_s
|
|
23
|
+
FileUtils.touch(@client)
|
|
24
|
+
File.chmod(0o755, @client)
|
|
25
|
+
|
|
26
|
+
example.run
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
subject { described_class.new(vm_id, @data_dir, @tmp_base) }
|
|
31
|
+
|
|
32
|
+
let(:base_options) do
|
|
33
|
+
{
|
|
34
|
+
ssh_host: "127.0.0.1", ssh_port: 50022,
|
|
35
|
+
arch: "aarch64", machine: "virt,accel=hvf,highmem=on",
|
|
36
|
+
cpu: "host", smp: "2", memory: "4G",
|
|
37
|
+
net_device: "virtio-net-device", drive_interface: "virtio",
|
|
38
|
+
qemu_bin: nil, extra_qemu_args: [], extra_netdev_args: nil,
|
|
39
|
+
extra_drive_args: nil, ports: [], control_port: nil,
|
|
40
|
+
debug_port: nil, no_daemonize: false, firmware_format: "raw",
|
|
41
|
+
other_default: %w(-parallel null -monitor none -display none -vga none),
|
|
42
|
+
extra_image_opts: nil,
|
|
43
|
+
advanced_network: true, net_mode: :socket_vmnet,
|
|
44
|
+
private_networks: [{ ip: "192.168.105.10", netmask: "255.255.255.0" }],
|
|
45
|
+
vmnet_interface: "en0", tap_device: nil, mcast_addr: nil,
|
|
46
|
+
socket_vmnet_socket: nil, socket_vmnet_client: nil,
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
@captured_cmd = nil
|
|
52
|
+
allow(subject).to receive(:execute) do |*cmd, **opts|
|
|
53
|
+
@captured_cmd = cmd
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
allow(subject).to receive(:running?).and_return(false)
|
|
57
|
+
# qemu binary resolves; client path is checked via File.executable? (Which -> nil).
|
|
58
|
+
allow(Vagrant::Util::Which).to receive(:which).and_return(nil)
|
|
59
|
+
allow(Vagrant::Util::Which).to receive(:which)
|
|
60
|
+
.with("qemu-system-aarch64").and_return("/usr/bin/qemu-system-aarch64")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def options(overrides = {})
|
|
64
|
+
base_options.merge(socket_vmnet_socket: @sock, socket_vmnet_client: @client).merge(overrides)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context "stream route (QEMU supports stream)" do
|
|
68
|
+
before { allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(true) }
|
|
69
|
+
|
|
70
|
+
it "attaches NIC 1 to the daemon socket via a native stream netdev" do
|
|
71
|
+
subject.start(options)
|
|
72
|
+
cmd_str = @captured_cmd.join(" ")
|
|
73
|
+
expect(cmd_str).to include("-netdev stream,id=net1,server=off,addr.type=unix,addr.path=#{@sock}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "does not wrap qemu (command still begins with the qemu binary)" do
|
|
77
|
+
subject.start(options)
|
|
78
|
+
expect(@captured_cmd.first).to eq "qemu-system-aarch64"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "keeps NIC 0 user-mode with SSH hostfwd" do
|
|
82
|
+
subject.start(options)
|
|
83
|
+
expect(@captured_cmd.join(" ")).to include("-netdev user,id=net0,hostfwd=tcp::50022-:22")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "wrapper route (QEMU lacks stream)" do
|
|
88
|
+
before { allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(false) }
|
|
89
|
+
|
|
90
|
+
it "prepends the socket_vmnet_client wrapper with the socket path" do
|
|
91
|
+
subject.start(options)
|
|
92
|
+
expect(@captured_cmd[0, 3]).to eq [@client, @sock, "qemu-system-aarch64"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "attaches NIC 1 via a socket netdev on fd 3" do
|
|
96
|
+
subject.start(options)
|
|
97
|
+
expect(@captured_cmd.join(" ")).to include("-netdev socket,id=net1,fd=3")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "probe could not run (nil) -> optimistic stream" do
|
|
102
|
+
before { allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(nil) }
|
|
103
|
+
|
|
104
|
+
it "uses the stream route and no wrapper" do
|
|
105
|
+
subject.start(options)
|
|
106
|
+
expect(@captured_cmd.first).to eq "qemu-system-aarch64"
|
|
107
|
+
expect(@captured_cmd.join(" ")).to include("-netdev stream,id=net1")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context "gating: net_mode set but advanced_network off" do
|
|
112
|
+
it "does not wrap qemu and does not add NIC 1 (matches pre-existing warn behavior)" do
|
|
113
|
+
# No probe should even happen; single-NIC user-mode path.
|
|
114
|
+
expect(VagrantPlugins::QEMU::Network).not_to receive(:qemu_supports_stream?)
|
|
115
|
+
subject.start(options(advanced_network: false))
|
|
116
|
+
cmd_str = @captured_cmd.join(" ")
|
|
117
|
+
expect(@captured_cmd.first).to eq "qemu-system-aarch64"
|
|
118
|
+
expect(cmd_str).not_to include("netdev=net1")
|
|
119
|
+
expect(cmd_str).not_to include("-netdev stream")
|
|
120
|
+
expect(cmd_str).not_to include("fd=3")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context "preconditions (fail-fast)" do
|
|
125
|
+
it "raises when the daemon socket is missing" do
|
|
126
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(true)
|
|
127
|
+
expect { subject.start(options(socket_vmnet_socket: "/nonexistent/sock")) }
|
|
128
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetSocketNotFound)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "raises on the wrapper route when the client is missing" do
|
|
132
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(false)
|
|
133
|
+
expect { subject.start(options(socket_vmnet_client: "/nonexistent/client")) }
|
|
134
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetClientNotFound)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "raises on non-macOS hosts" do
|
|
138
|
+
allow(RbConfig::CONFIG).to receive(:[]).and_call_original
|
|
139
|
+
allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("linux-gnu")
|
|
140
|
+
expect { subject.start(options) }
|
|
141
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetNotMacos)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -21,6 +21,10 @@ describe VagrantPlugins::QEMU::Network, ".backend_for" do
|
|
|
21
21
|
expect(described_class.backend_for(:socket)).to be_a(VagrantPlugins::QEMU::Network::Socket)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
it "returns SocketVmnet for :socket_vmnet" do
|
|
25
|
+
expect(described_class.backend_for(:socket_vmnet)).to be_a(VagrantPlugins::QEMU::Network::SocketVmnet)
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
it "raises ConfigError for unknown net_mode" do
|
|
25
29
|
expect { described_class.backend_for(:bogus) }.to raise_error(VagrantPlugins::QEMU::Errors::ConfigError)
|
|
26
30
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Network::SocketVmnet do
|
|
4
|
+
subject { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:sock) { "/opt/homebrew/var/run/socket_vmnet" }
|
|
7
|
+
let(:client) { "/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet_client" }
|
|
8
|
+
|
|
9
|
+
context "stream route (use_stream: true)" do
|
|
10
|
+
let(:opts) { { use_stream: true, socket_vmnet_socket: sock, socket_vmnet_client: client } }
|
|
11
|
+
|
|
12
|
+
it "builds a native stream netdev connecting to the daemon socket" do
|
|
13
|
+
expect(subject.build_netdev_args("net1", opts))
|
|
14
|
+
.to eq %W(-netdev stream,id=net1,server=off,addr.type=unix,addr.path=#{sock})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "needs no launch wrapper" do
|
|
18
|
+
expect(subject.launch_prefix(opts)).to eq []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "wrapper route (use_stream: false)" do
|
|
23
|
+
let(:opts) { { use_stream: false, socket_vmnet_socket: sock, socket_vmnet_client: client } }
|
|
24
|
+
|
|
25
|
+
it "builds a socket netdev on fd 3" do
|
|
26
|
+
expect(subject.build_netdev_args("net1", opts)).to eq %w(-netdev socket,id=net1,fd=3)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "prepends the socket_vmnet_client wrapper with the socket path" do
|
|
30
|
+
expect(subject.launch_prefix(opts)).to eq [client, sock]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "never requires sudo (the daemon holds the root vmnet membership)" do
|
|
35
|
+
expect(subject.requires_sudo?).to eq false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "#preflight!" do
|
|
39
|
+
around(:each) { |ex| with_temp_dir { |dir| @dir = dir; ex.run } }
|
|
40
|
+
|
|
41
|
+
def opts(overrides = {})
|
|
42
|
+
sock = @dir.join("sock").to_s
|
|
43
|
+
FileUtils.touch(sock)
|
|
44
|
+
client = @dir.join("client").to_s
|
|
45
|
+
FileUtils.touch(client)
|
|
46
|
+
File.chmod(0o755, client)
|
|
47
|
+
{ socket_vmnet_socket: sock, socket_vmnet_client: client }.merge(overrides)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
allow(RbConfig::CONFIG).to receive(:[]).and_call_original
|
|
52
|
+
allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("darwin23")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "sets use_stream=true when the probe reports stream" do
|
|
56
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(true)
|
|
57
|
+
o = opts
|
|
58
|
+
subject.preflight!(o, "qemu-x")
|
|
59
|
+
expect(o[:use_stream]).to eq true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "sets use_stream=false when the probe definitively lacks stream" do
|
|
63
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(false)
|
|
64
|
+
o = opts
|
|
65
|
+
subject.preflight!(o, "qemu-x")
|
|
66
|
+
expect(o[:use_stream]).to eq false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "defaults use_stream=true when the probe is unknown (nil)" do
|
|
70
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(nil)
|
|
71
|
+
o = opts
|
|
72
|
+
subject.preflight!(o, "qemu-x")
|
|
73
|
+
expect(o[:use_stream]).to eq true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "raises on non-macOS hosts" do
|
|
77
|
+
allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("linux-gnu")
|
|
78
|
+
expect { subject.preflight!(opts, "qemu-x") }
|
|
79
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetNotMacos)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "raises when the daemon socket is missing" do
|
|
83
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(true)
|
|
84
|
+
expect { subject.preflight!(opts(socket_vmnet_socket: "/nope/sock"), "qemu-x") }
|
|
85
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetSocketNotFound)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "raises on the wrapper route when the client is missing" do
|
|
89
|
+
allow(VagrantPlugins::QEMU::Network).to receive(:qemu_supports_stream?).and_return(false)
|
|
90
|
+
allow(::Vagrant::Util::Which).to receive(:which).and_return(nil)
|
|
91
|
+
expect { subject.preflight!(opts(socket_vmnet_client: "/nope/client"), "qemu-x") }
|
|
92
|
+
.to raise_error(VagrantPlugins::QEMU::Errors::SocketVmnetClientNotFound)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe VagrantPlugins::QEMU::Network::Base do
|
|
98
|
+
it "#launch_prefix defaults to an empty prefix (other backends launch qemu directly)" do
|
|
99
|
+
expect(described_class.new.launch_prefix({})).to eq []
|
|
100
|
+
expect(VagrantPlugins::QEMU::Network::Vmnet.new.launch_prefix({})).to eq []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "#preflight! is a no-op by default (other backends need no preconditions)" do
|
|
104
|
+
o = { some: "opt" }
|
|
105
|
+
expect { described_class.new.preflight!(o, "qemu-x") }.not_to raise_error
|
|
106
|
+
expect(o).to eq({ some: "opt" })
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe VagrantPlugins::QEMU::Network, ".qemu_supports_stream?" do
|
|
111
|
+
# Reset the memoized cache between examples.
|
|
112
|
+
before { described_class.instance_variable_set(:@stream_support, nil) }
|
|
113
|
+
|
|
114
|
+
def stub_probe(stdout:, exit_code: 0)
|
|
115
|
+
result = ::Vagrant::Util::Subprocess::Result.new(exit_code, stdout, "")
|
|
116
|
+
allow(::Vagrant::Util::Subprocess).to receive(:execute)
|
|
117
|
+
.with("qemu-x", "-M", "none", "-netdev", "help").and_return(result)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "is true when the netdev list contains a stream line" do
|
|
121
|
+
stub_probe(stdout: "socket\nstream\ndgram\nuser\n")
|
|
122
|
+
expect(described_class.qemu_supports_stream?("qemu-x")).to eq true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "is false when the list lacks stream (old QEMU)" do
|
|
126
|
+
stub_probe(stdout: "socket\nuser\ntap\n")
|
|
127
|
+
expect(described_class.qemu_supports_stream?("qemu-x")).to eq false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "is nil (unknown) when the probe exits non-zero" do
|
|
131
|
+
stub_probe(stdout: "stream\n", exit_code: 1)
|
|
132
|
+
expect(described_class.qemu_supports_stream?("qemu-x")).to be_nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "is nil (not an exception) when the probe cannot run" do
|
|
136
|
+
allow(::Vagrant::Util::Subprocess).to receive(:execute).and_raise(StandardError.new("boom"))
|
|
137
|
+
expect(described_class.qemu_supports_stream?("qemu-x")).to be_nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "caches the result per binary (probes once)" do
|
|
141
|
+
stub_probe(stdout: "stream\n")
|
|
142
|
+
described_class.qemu_supports_stream?("qemu-x")
|
|
143
|
+
described_class.qemu_supports_stream?("qemu-x")
|
|
144
|
+
expect(::Vagrant::Util::Subprocess).to have_received(:execute).once
|
|
145
|
+
end
|
|
146
|
+
end
|
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.6.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-07-
|
|
11
|
+
date: 2026-07-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Enables Vagrant to manage machines with QEMU.
|
|
14
14
|
email: pgf00a@gmail.com
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/vagrant-qemu/network.rb
|
|
47
47
|
- lib/vagrant-qemu/network/base.rb
|
|
48
48
|
- lib/vagrant-qemu/network/socket.rb
|
|
49
|
+
- lib/vagrant-qemu/network/socket_vmnet.rb
|
|
49
50
|
- lib/vagrant-qemu/network/tap.rb
|
|
50
51
|
- lib/vagrant-qemu/network/vmnet.rb
|
|
51
52
|
- lib/vagrant-qemu/plugin.rb
|
|
@@ -68,6 +69,8 @@ files:
|
|
|
68
69
|
- spec/e2e/reload_spec.rb
|
|
69
70
|
- spec/e2e/smoke_spec.rb
|
|
70
71
|
- spec/e2e/socket_network_spec.rb
|
|
72
|
+
- spec/e2e/socket_vmnet_spec.rb
|
|
73
|
+
- spec/e2e/support/qemu_no_stream_shim.sh
|
|
71
74
|
- spec/spec_helper.rb
|
|
72
75
|
- spec/unit/action/cloud_init_network_spec.rb
|
|
73
76
|
- spec/unit/action/destroy_spec.rb
|
|
@@ -87,6 +90,7 @@ files:
|
|
|
87
90
|
- spec/unit/driver/ssh_port_spec.rb
|
|
88
91
|
- spec/unit/driver/start_cmd_advanced_spec.rb
|
|
89
92
|
- spec/unit/driver/start_cmd_order_spec.rb
|
|
93
|
+
- spec/unit/driver/start_cmd_socket_vmnet_spec.rb
|
|
90
94
|
- spec/unit/driver/start_cmd_spec.rb
|
|
91
95
|
- spec/unit/driver/start_edge_cases_spec.rb
|
|
92
96
|
- spec/unit/driver/state_spec.rb
|
|
@@ -95,6 +99,7 @@ files:
|
|
|
95
99
|
- spec/unit/network/build_network_config_spec.rb
|
|
96
100
|
- spec/unit/network/generate_mac_spec.rb
|
|
97
101
|
- spec/unit/network/socket_spec.rb
|
|
102
|
+
- spec/unit/network/socket_vmnet_spec.rb
|
|
98
103
|
- spec/unit/network/tap_spec.rb
|
|
99
104
|
- spec/unit/network/vmnet_spec.rb
|
|
100
105
|
- vagrant-qemu.gemspec
|