vagrant-qemu 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0968c034faf465b2d51e1f7b785a7a1aed39a4d6c40ef96f6b34147e9ab8ebd
4
- data.tar.gz: fff8fc272c6c99abb85ccff761137f53ee6aabe8953f4b702ffca61163ea8d27
3
+ metadata.gz: a7af3fcc9e9a9210bea30f44da695c87fbd15c24eed79de64540bb01ec5d4842
4
+ data.tar.gz: 805730ceb9574de982d6c20f4b8a7e04c37b13fbdc47ca902ddde6fcf844fef5
5
5
  SHA512:
6
- metadata.gz: ba77c0b50cd06921799dcc154f8d987a3fe1e367fd4347bbd2a45381f0819815e9f832f29be04a76818fb5e5c24abb4f009fb33350aace09486f1a20f6250012
7
- data.tar.gz: 6438ed9c811e25f7393e59971028eea43b5f3d76ac9a11982dd2424c8b7cbd54181cbaa514875aebbb54f38520fcab12575929e5c62259f42c37cce83274c34a
6
+ metadata.gz: d9613c43e9ea63da345bf0745b3825578cb9d32a8e9465b91a9beea36c571e085d9a325675d28e1a98fe655fbabffb0b29f8e6107e90a13dc504ba83e1b007c8
7
+ data.tar.gz: 96a64e28147f0a7c6c7994695d8b423088f7f3eb80c18288d2652120e24c5479d88d9d407455f10e0ab252a7cacbf0fb2f0c0ef2a22a403c625d3826d5002ec5
data/CHANGELOG.md CHANGED
@@ -154,3 +154,35 @@
154
154
  core packaging middleware. Environment-specific state — networking, MAC,
155
155
  additional disks, DVDs/cloud-init seed ISOs, and firmware — is intentionally
156
156
  not baked into the box.
157
+
158
+ # 0.6.0 (2026-07-21)
159
+
160
+ * `net_mode = :socket_vmnet` adds no-sudo private networking on macOS: the
161
+ [socket_vmnet](https://github.com/lima-vm/socket_vmnet) daemon holds the root
162
+ vmnet membership and QEMU connects to it as a normal user, so `vagrant up`
163
+ and the other lifecycle commands need no sudo. It reuses the advanced-network
164
+ dual-NIC + cloud-init path — NIC 0 user-mode (SSH + port forwarding), NIC 1
165
+ on socket_vmnet with the configured static IP — giving host↔VM and VM↔VM
166
+ connectivity. On QEMU >= 7.2 QEMU connects to the daemon directly
167
+ (`-netdev stream`); on older QEMU it falls back to the `socket_vmnet_client`
168
+ wrapper (`-netdev socket,fd=3`), chosen automatically by probing the QEMU
169
+ binary. New config: `socket_vmnet_socket` and `socket_vmnet_client` (default
170
+ to the Homebrew prefix). The static IP must fall within the daemon's subnet
171
+ (`--vmnet-gateway`, default `192.168.105.1/24`); see the README. (#41)
172
+
173
+ # 0.6.1 (2026-07-22)
174
+
175
+ * Fix `vagrant reload` (and any other same-process halt-then-start, e.g. the
176
+ `vagrant-reload` plugin) failing with a QEMU image write-lock error when a
177
+ custom disk (`config.vm.disk`) was configured. `configure_disks` re-runs on
178
+ every `action_start`, but the driver's attached-disk list only ever
179
+ accumulated, so a reload attached each custom disk twice on the same qemu
180
+ command line. (#41)
181
+ * Fix `qemu-img create` unconditionally recreating an already-existing custom
182
+ disk on every `configure_disks` call, which silently wiped the disk's
183
+ contents on every reload (independent of the bug above — resizing an
184
+ existing disk to a new configured size still isn't supported).
185
+ * Fix the cloud-init network seed ISO (used by `advanced_network` without a
186
+ core `cloud_init` user-data config, i.e. the `net_mode` examples above)
187
+ being re-registered as a duplicate disk on every same-process reload,
188
+ hitting the same QEMU write-lock error via a different disk.
data/README.md CHANGED
@@ -108,11 +108,13 @@ This provider exposes a few provider-specific configuration options:
108
108
  * `graceful_timeout` - Seconds to wait at each `vagrant halt` stage before escalating, default: `60`. Halt sends ACPI `system_powerdown`, waits up to this long, then sends QEMU's `quit` monitor command (a clean shutdown that flushes and closes the disk images), waits again, and finally SIGKILLs QEMU as a last resort — so halt still completes even when the guest was already halted from inside (e.g. `sudo systemctl halt`), where `system_powerdown` is a no-op
109
109
  * advanced networking (requires `advanced_network = true`)
110
110
  * `advanced_network` - Enable dual-NIC advanced networking with `private_network` support, default: `false`
111
- * `net_mode` - Network backend: `:auto` (detect by platform), `:vmnet_shared`, `:vmnet_host`, `:vmnet_bridged` (macOS), `:tap` (Linux), `:socket` (QEMU `socket` netdev — multicast or point-to-point, see `socket_opts`), default: `:auto`
111
+ * `net_mode` - Network backend: `:auto` (detect by platform), `:vmnet_shared`, `:vmnet_host`, `:vmnet_bridged` (macOS, sudo), `:socket_vmnet` (macOS, no sudo — via the socket_vmnet daemon), `:tap` (Linux), `:socket` (QEMU `socket` netdev — multicast or point-to-point, see `socket_opts`), default: `:auto`
112
112
  * `vmnet_interface` - Physical interface for vmnet-bridged mode, default: `en0`
113
113
  * `tap_device` - TAP device name for Linux tap backend, default: `nil` (uses `tap0`)
114
114
  * `mcast_addr` - Convenience shortcut for the `:socket` backend's multicast address, default: `nil` (uses `230.0.0.1:1234`)
115
115
  * `socket_opts` - Raw options for the `:socket` netdev, emitted verbatim as `-netdev socket,id=netN,<socket_opts>`. You pick the mode: `"mcast=230.0.0.1:1234"` (multicast, N-way), `"listen=:1234"` / `"connect=127.0.0.1:1234"` (point-to-point; you decide which VM listens and which connects — the no-root, macOS-friendly path). Overrides `mcast_addr`. Default: `nil` (falls back to multicast)
116
+ * `socket_vmnet_socket` - Path to the socket_vmnet daemon's unix socket, used by the `:socket_vmnet` backend, default: `<homebrew-prefix>/var/run/socket_vmnet` (`HOMEBREW_PREFIX` env, else `/opt/homebrew` on Apple Silicon, `/usr/local` on Intel)
117
+ * `socket_vmnet_client` - Path to the `socket_vmnet_client` wrapper, used only on QEMU older than 7.2 (which lacks the native `stream` netdev), default: `<homebrew-prefix>/opt/socket_vmnet/bin/socket_vmnet_client`
116
118
 
117
119
  ### Usage
118
120
 
@@ -320,7 +322,7 @@ See the [QEMU Documentation](https://www.qemu.org/docs/master/devel/multiple-iot
320
322
 
321
323
  12. Advanced networking with private_network
322
324
 
323
- Pick a backend with `net_mode`: QEMU's native vmnet.framework on macOS (requires sudo), TAP on Linux, or the `socket` netdev. The `:socket` backend is a thin wrapper around QEMU's `socket` netdev — you choose the mode in `socket_opts`: `mcast=` (multicast, N-way, Linux/Windows) or `listen=`/`connect=` (point-to-point, no root, works on macOS). The plugin creates two NICs: NIC 0 (user-mode for SSH and port forwarding) and NIC 1 (platform backend for VM networking). The static IP is delivered via a cloud-init NoCloud seed ISO that the plugin builds and attaches automatically; the NICs are matched by MAC address, never by interface order.
325
+ Pick a backend with `net_mode`: QEMU's native vmnet.framework on macOS (`:vmnet_shared`/`_host`/`_bridged`, requires sudo), `:socket_vmnet` on macOS (full host↔VM and VM↔VM **without** sudo, via the [socket_vmnet](https://github.com/lima-vm/socket_vmnet) daemon), TAP on Linux, or the `socket` netdev. The `:socket` backend is a thin wrapper around QEMU's `socket` netdev — you choose the mode in `socket_opts`: `mcast=` (multicast, N-way, Linux/Windows) or `listen=`/`connect=` (point-to-point, no root, works on macOS). The plugin creates two NICs: NIC 0 (user-mode for SSH and port forwarding) and NIC 1 (platform backend for VM networking). The static IP is delivered via a cloud-init NoCloud seed ISO that the plugin builds and attaches automatically; the NICs are matched by MAC address, never by interface order.
324
326
 
325
327
  For VM-to-VM networking on macOS without sudo, use `:socket` with a `listen`/`connect` pair — you decide which VM listens and which connects:
326
328
 
@@ -366,6 +368,38 @@ Vagrant.configure("2") do |config|
366
368
  end
367
369
  ```
368
370
 
371
+ Full networking on macOS **without sudo** with `:socket_vmnet` (host↔VM and VM↔VM, static IP):
372
+
373
+ ```ruby
374
+ Vagrant.configure("2") do |config|
375
+ config.vm.box = "perk/ubuntu-2204-arm64" # an aarch64 cloud-init box
376
+ config.vm.network "private_network", ip: "192.168.105.10"
377
+
378
+ config.vm.provider "qemu" do |qe|
379
+ qe.advanced_network = true
380
+ qe.net_mode = :socket_vmnet
381
+ # qe.socket_vmnet_socket = "/opt/homebrew/var/run/socket_vmnet" # default (Homebrew prefix)
382
+ end
383
+ end
384
+ ```
385
+
386
+ Set up the daemon once (root, one-time), then run `vagrant up` as your normal user:
387
+
388
+ ```sh
389
+ brew install socket_vmnet
390
+ # Reserve a static-IP range so cloud-init IPs don't collide with the DHCP pool:
391
+ sudo brew services start socket_vmnet # or run it with explicit flags, e.g.:
392
+ # sudo socket_vmnet --vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.100 \
393
+ # /opt/homebrew/var/run/socket_vmnet
394
+ ```
395
+
396
+ The `private_network` IP must sit inside the daemon's subnet (`--vmnet-gateway`, default
397
+ `192.168.105.1/24`) and, to avoid a DHCP clash, above `--vmnet-dhcp-end`. Unlike the native vmnet
398
+ backends, `:socket_vmnet` cannot tell the daemon which subnet to use — the daemon's launch flags are
399
+ authoritative, so align the IP to them. On QEMU ≥ 7.2 the plugin connects to the daemon directly
400
+ (`-netdev stream`); on older QEMU it automatically falls back to the `socket_vmnet_client` wrapper
401
+ (configurable via `qe.socket_vmnet_client`).
402
+
369
403
  Notes:
370
404
  * The guest image must include cloud-init, otherwise the static IP is silently not applied
371
405
  * On macOS, vmnet requires root: run `sudo vagrant up` (and the other lifecycle commands such as `halt`/`reload`/`destroy`), because the plugin launches QEMU as a child of the Vagrant process and does not elevate it on its own. The plugin warns when vmnet is selected and Vagrant is not running as root.
@@ -374,7 +408,7 @@ Notes:
374
408
  sudo chown -R "$(id -un)":staff ~/.vagrant.d/boxes/<box> .vagrant
375
409
  ```
376
410
  Pre-adding boxes as your normal user (`vagrant box add <box>`) before the first `sudo vagrant up` also avoids the box ending up root-owned.
377
- * To avoid root (and this side effect) entirely on macOS, use [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) — 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 |
@@ -24,6 +24,10 @@ module VagrantPlugins
24
24
  # user-data seed.
25
25
  CORE_SEED_DISK_NAME = "vagrant-cloud_init-disk".freeze
26
26
 
27
+ # Disk name this action gives its own network-only seed (no core
28
+ # user-data seed present).
29
+ OWN_SEED_DISK_NAME = "vagrant-qemu-network-disk".freeze
30
+
27
31
  def initialize(app, env)
28
32
  @app = app
29
33
  @logger = Log4r::Logger.new("vagrant_qemu::action::cloud_init_network")
@@ -55,13 +59,24 @@ module VagrantPlugins
55
59
  private
56
60
 
57
61
  # No core cloud-init seed: build our own network-only seed and attach
58
- # it as a fresh :dvd disk.
62
+ # it as a fresh :dvd disk. This re-runs on every action_start
63
+ # (including a same-process reload), so a disk already registered
64
+ # from an earlier run is rebuilt in place rather than re-registered
65
+ # -- machine.config.vm.disks has no dedup of its own.
59
66
  def attach_network_seed(machine, env, pn)
60
- iso_path = build_seed(machine, env, pn,
67
+ existing = machine.config.vm.disks.find do |d|
68
+ d.type == :dvd && d.name == OWN_SEED_DISK_NAME
69
+ end
70
+
71
+ iso_path = existing ? Pathname.new(existing.file) : machine.data_dir.join("vagrant-qemu-network.iso")
72
+
73
+ build_seed(machine, env, pn,
61
74
  user_data: "#cloud-config\n",
62
- file_destination: machine.data_dir.join("vagrant-qemu-network.iso"))
75
+ file_destination: iso_path)
76
+
77
+ return if existing
63
78
 
64
- machine.config.vm.disk :dvd, file: iso_path.to_s, name: "vagrant-qemu-network-disk"
79
+ machine.config.vm.disk :dvd, file: iso_path.to_s, name: OWN_SEED_DISK_NAME
65
80
  machine.config.vm.disks.each do |d|
66
81
  d.finalize! if d.type == :dvd && d.file == iso_path.to_s
67
82
  end
@@ -42,6 +42,8 @@ module VagrantPlugins
42
42
  :tap_device => config.tap_device,
43
43
  :mcast_addr => config.mcast_addr,
44
44
  :socket_opts => config.socket_opts,
45
+ :socket_vmnet_socket => config.socket_vmnet_socket,
46
+ :socket_vmnet_client => config.socket_vmnet_client,
45
47
  }
46
48
 
47
49
  # Pick up SSH port that may have been corrected by HandleForwardedPortCollisions
@@ -32,6 +32,11 @@ module VagrantPlugins
32
32
  # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks
33
33
  # @return [Hash] configured_disks - A hash of all the current configured disks
34
34
  def self.configure_disks(machine, defined_disks)
35
+ # Vagrant re-invokes this on every action_start (including a
36
+ # same-process reload), always passing the complete current disk
37
+ # list -- reset before the empty check so a disk removed from the
38
+ # Vagrantfile doesn't leave a stale entry attached either.
39
+ machine.provider.driver.reset_attached_drives!
35
40
  return {} if defined_disks.empty?
36
41
 
37
42
  configured_disks = {disk: [], floppy: [], dvd: []}
@@ -79,12 +84,17 @@ module VagrantPlugins
79
84
  def self.setup_disk(machine, disk)
80
85
  disk_dir = machine.provider.driver.disk_dir
81
86
  disk_path = disk_dir.join("#{disk.name}.#{disk.disk_ext}")
82
- args = ["create", "-f", "qcow2"]
83
87
 
84
88
  disk_provider_config = disk.provider_config[:qemu] if disk.provider_config
85
- args.push(disk_path.to_s)
86
- args.push("#{disk.size}")
87
- machine.provider.driver.execute("qemu-img", *args)
89
+
90
+ # configure_disks re-runs on every action_start (including a
91
+ # same-process reload); qemu-img create truncates an existing file
92
+ # unconditionally, so skip it once the disk already exists instead
93
+ # of silently wiping it. Resizing an existing disk isn't handled.
94
+ if !disk_path.file?
95
+ args = ["create", "-f", "qcow2", disk_path.to_s, "#{disk.size}"]
96
+ machine.provider.driver.execute("qemu-img", *args)
97
+ end
88
98
 
89
99
  {UUID: disk.id, Name: disk.name, Path: disk_path.to_s, primary: !!disk.primary}
90
100
  end
@@ -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
@@ -463,6 +472,14 @@ module VagrantPlugins
463
472
  end
464
473
  end
465
474
 
475
+ # Vagrant's Disk middleware always passes the *complete* current disk
476
+ # list and re-runs on every action_start (including a same-process
477
+ # reload, which reuses this Driver instance) -- so attach_disk/attach_dvd
478
+ # must be rebuilt from a clean slate each time, not accumulated across calls.
479
+ def reset_attached_drives!
480
+ @attached_drives = {disk: [], floppy: [], dvd: []}
481
+ end
482
+
466
483
  def attach_dvd(disk)
467
484
  @attached_drives[:dvd] << disk
468
485
  end
@@ -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.1'
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.
@@ -32,4 +32,76 @@ describe "extra disk attachment end-to-end", :requires_qemu do
32
32
  expect(ssh[:exit_code]).to eq 0
33
33
  expect(ssh[:stdout].lines.last.to_s.strip.to_i).to be >= 2
34
34
  end
35
+
36
+ it "reload with an extra disk does not hit a QEMU image-lock error" do
37
+ File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
38
+ Vagrant.configure("2") do |config|
39
+ config.vm.box = "#{test_box}"
40
+ config.vm.box_check_update = false
41
+ config.vm.synced_folder ".", "/vagrant", disabled: true
42
+ config.vm.disk :disk, name: "extra", size: "1GB"
43
+ config.vm.provider "qemu" do |qe|
44
+ qe.memory = "2G"
45
+ end
46
+ end
47
+ RUBY
48
+
49
+ result = vagrant_up(@work_dir, timeout: 600)
50
+ expect(result[:exit_code]).to eq 0
51
+
52
+ # This is the exact same-process halt->start path a same-process reload
53
+ # takes (issue #41: reload used to re-attach the extra disk a second
54
+ # time, so QEMU refused its own duplicate -drive with a write-lock error).
55
+ reload = vagrant_reload(@work_dir, timeout: 600)
56
+ expect(reload[:exit_code]).to eq 0
57
+ expect(reload[:stderr]).not_to match(/Failed to get "write" lock/)
58
+
59
+ ssh = vagrant_ssh(@work_dir, command: %q{lsblk -d -n -o TYPE | grep -c '^disk$'})
60
+ expect(ssh[:exit_code]).to eq 0
61
+ expect(ssh[:stdout].lines.last.to_s.strip.to_i).to be >= 2
62
+ end
63
+
64
+ it "reload does not wipe data already written to an extra disk" do
65
+ File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
66
+ Vagrant.configure("2") do |config|
67
+ config.vm.box = "#{test_box}"
68
+ config.vm.box_check_update = false
69
+ config.vm.synced_folder ".", "/vagrant", disabled: true
70
+ config.vm.disk :disk, name: "extra", size: "1GB"
71
+ config.vm.provider "qemu" do |qe|
72
+ qe.memory = "2G"
73
+ end
74
+ end
75
+ RUBY
76
+
77
+ result = vagrant_up(@work_dir, timeout: 600)
78
+ expect(result[:exit_code]).to eq 0
79
+
80
+ setup = vagrant_ssh(@work_dir, command: <<~SH)
81
+ set -e
82
+ dev=$(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}' | grep -v vda | head -1)
83
+ sudo mkfs.ext4 -F /dev/$dev
84
+ sudo mkdir -p /mnt/extra
85
+ sudo mount /dev/$dev /mnt/extra
86
+ echo marker-should-survive-reload | sudo tee /mnt/extra/marker.txt
87
+ sudo umount /mnt/extra
88
+ SH
89
+ expect(setup[:exit_code]).to eq 0
90
+
91
+ # configure_disks unconditionally ran `qemu-img create` on every
92
+ # action_start (including this same-process reload), silently
93
+ # truncating the extra disk every time -- this is the regression test.
94
+ reload = vagrant_reload(@work_dir, timeout: 600)
95
+ expect(reload[:exit_code]).to eq 0
96
+
97
+ check = vagrant_ssh(@work_dir, command: <<~SH)
98
+ set -e
99
+ dev=$(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}' | grep -v vda | head -1)
100
+ sudo mkdir -p /mnt/extra
101
+ sudo mount /dev/$dev /mnt/extra
102
+ cat /mnt/extra/marker.txt
103
+ SH
104
+ expect(check[:exit_code]).to eq 0
105
+ expect(check[:stdout]).to include("marker-should-survive-reload")
106
+ end
35
107
  end
data/spec/e2e/helper.rb CHANGED
@@ -130,7 +130,7 @@ RSpec.configure do |config|
130
130
  # e2e runs the INSTALLED plugin (see note above) — fail fast when it
131
131
  # doesn't match the source tree, instead of silently testing a stale gem.
132
132
  config.before(:suite) do
133
- next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"]
133
+ next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"] || ENV["TEST_SOCKET_VMNET"]
134
134
 
135
135
  require "vagrant-qemu/version"
136
136
  expected = VagrantPlugins::QEMU::VERSION