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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0968c034faf465b2d51e1f7b785a7a1aed39a4d6c40ef96f6b34147e9ab8ebd
4
- data.tar.gz: fff8fc272c6c99abb85ccff761137f53ee6aabe8953f4b702ffca61163ea8d27
3
+ metadata.gz: 00ee57a8b6cb0315b36c0084239e92ed69633698845945e6290ba289d71e2889
4
+ data.tar.gz: 99985f04eabced2eabae1914eacb19f053f13613a4f18d40047ac57440bd4a52
5
5
  SHA512:
6
- metadata.gz: ba77c0b50cd06921799dcc154f8d987a3fe1e367fd4347bbd2a45381f0819815e9f832f29be04a76818fb5e5c24abb4f009fb33350aace09486f1a20f6250012
7
- data.tar.gz: 6438ed9c811e25f7393e59971028eea43b5f3d76ac9a11982dd2424c8b7cbd54181cbaa514875aebbb54f38520fcab12575929e5c62259f42c37cce83274c34a
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) — 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.
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 (or socket_vmnet) | None (QEMU >= 7.0) |
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
@@ -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 == "aarch64" ? "/opt/homebrew/share/qemu" : "/usr/local/share/qemu"
180
+ "#{homebrew_prefix(arch)}/share/qemu"
165
181
  else
166
182
  "/usr/share/qemu"
167
183
  end
@@ -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
@@ -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
@@ -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
  #
@@ -1,5 +1,5 @@
1
1
  module VagrantPlugins
2
2
  module QEMU
3
- VERSION = '0.5.0'
3
+ VERSION = '0.6.0'
4
4
  end
5
5
  end
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
@@ -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.5.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 00:00:00.000000000 Z
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