vagrant-qemu 0.4.1 → 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: 4cd40c0eae22a6a6d4f284004a86de09e0584e8887cc273e9acd4c0fa2f33e69
4
- data.tar.gz: 4caaa2706bbb8f221dcc67e5164e7b38879e3cb2d709d8414fc1f0a0ed0de45b
3
+ metadata.gz: 00ee57a8b6cb0315b36c0084239e92ed69633698845945e6290ba289d71e2889
4
+ data.tar.gz: 99985f04eabced2eabae1914eacb19f053f13613a4f18d40047ac57440bd4a52
5
5
  SHA512:
6
- metadata.gz: dd592ca7bf0a75c82b58db4ed8194f8a3e31b44834538ea0fe7ae7124032871422b9b4f2f6b49122964fefc0692f02d1be4a018bae2e4948f3f8aeeb9385eaaf
7
- data.tar.gz: 906b02b551b9f3c5082a3967edc33c656f9b642ae0f6bbabcd8bb40b944fa0e2d67f10c5b6b34af3291516c218d0d7657c47d9cf495d1dabceb63fa5acb4ac77
6
+ metadata.gz: ffc285799ee3f7be527da85b03d539c6f1f6b9278bc474c00b63eef41ea937bd8d5bf17aae125d801d3e1a9404967b1df9f0fa2b3173f3db452ed0fc60a12727
7
+ data.tar.gz: 43601507f85d2bb8e884e24795cdbb64c4d2d03f3842c1a8feb5cefe437241cdc0bcbbc531da4036540b3e19f6aaf831f9b35cad6c91c29c32da42be5f90abb0
data/CHANGELOG.md CHANGED
@@ -140,3 +140,32 @@
140
140
  * Warn when private_network is configured without `advanced_network`, when the
141
141
  network backend needs sudo, and when other unsupported network types are used
142
142
  * Add test suite: unit + acceptance + e2e (`rake spec:unit|acceptance|e2e`)
143
+
144
+ # 0.5.0 (2026-07-11)
145
+
146
+ * `vagrant package` is now supported: export a running QEMU VM into a
147
+ distributable libvirt-format box. The box disk is flattened from its qcow2
148
+ overlay into a standalone qcow2 with `qemu-img convert` (the live VM disk is
149
+ never modified). A single-disk VM produces `box.img`; a multi-disk VM
150
+ produces `box_1.img`..`box_N.img` plus a `disks[]` entry in `metadata.json`.
151
+ `metadata.json` is written with `provider: libvirt` / `format: qcow2` /
152
+ `virtual_size`, and a minimal default Vagrantfile declaring the box `arch` is
153
+ bundled. `--output`/`--include`/`--info`/`--vagrantfile` are handled by the
154
+ core packaging middleware. Environment-specific state — networking, MAC,
155
+ additional disks, DVDs/cloud-init seed ISOs, and firmware — is intentionally
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 |
@@ -0,0 +1,74 @@
1
+ require "json"
2
+ require "open3"
3
+ require "pathname"
4
+ require "log4r"
5
+
6
+ module VagrantPlugins
7
+ module QEMU
8
+ module Action
9
+ # Produces the box's disk artifacts and metadata.json in export.temp_dir:
10
+ # flattens each box-disk overlay into a standalone qcow2 and describes them
11
+ # in a libvirt-format metadata.json. Only the VM's box disks are packaged;
12
+ # additional disks, DVDs and cloud-init seed ISOs are left out.
13
+ class Export
14
+ def initialize(app, env)
15
+ @app = app
16
+ @logger = Log4r::Logger.new("vagrant_qemu::action::export")
17
+ end
18
+
19
+ def call(env)
20
+ @env = env
21
+
22
+ if env[:machine].state.id != :stopped
23
+ raise Vagrant::Errors::VMPowerOffToPackage
24
+ end
25
+
26
+ export
27
+ @app.call(env)
28
+ end
29
+
30
+ def export
31
+ driver = @env[:machine].provider.driver
32
+ temp_dir = Pathname.new(@env["export.temp_dir"])
33
+ disks = driver.box_disk_paths
34
+
35
+ if disks.empty?
36
+ raise Errors::BoxInvalid, name: @env[:machine].name, err: "No box disk found to package"
37
+ end
38
+
39
+ multi = disks.size > 1
40
+ disk_meta = []
41
+ disks.each_with_index do |src, i|
42
+ name = multi ? "box_#{i + 1}.img" : "box.img"
43
+ @env[:ui].info(I18n.t("vagrant_qemu.packaging_disk", name: name))
44
+ driver.convert_box_disk(src, temp_dir.join(name))
45
+ disk_meta << { "path" => name }
46
+ end
47
+
48
+ write_metadata(temp_dir, disk_meta, multi)
49
+ end
50
+
51
+ # libvirt box metadata: provider must be "libvirt" (the provider's
52
+ # box_format) or the packaged box won't match on `box add`.
53
+ def write_metadata(temp_dir, disk_meta, multi)
54
+ metadata = {
55
+ "provider" => "libvirt",
56
+ "format" => "qcow2",
57
+ "virtual_size" => virtual_size_gb(temp_dir.join(disk_meta.first["path"])),
58
+ }
59
+ metadata["disks"] = disk_meta if multi
60
+ File.write(temp_dir.join("metadata.json"), JSON.pretty_generate(metadata))
61
+ end
62
+
63
+ def virtual_size_gb(img)
64
+ stdout, stderr, status = Open3.capture3("qemu-img", "info", "--output=json", img.to_s)
65
+ if !status.success?
66
+ raise Errors::ExecuteError, command: "qemu-img info", stderr: stderr, stdout: stdout
67
+ end
68
+ bytes = JSON.parse(stdout)["virtual-size"]
69
+ (bytes.to_f / (1024**3)).ceil
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,37 @@
1
+ require "pathname"
2
+
3
+ module VagrantPlugins
4
+ module QEMU
5
+ module Action
6
+ # Writes a minimal default Vagrantfile into the box declaring only the
7
+ # architecture, so a consumer on a different host arch doesn't default to
8
+ # the wrong one. Networking, MAC and host-specific machine flags are left
9
+ # out on purpose — they are environment-specific and come from the
10
+ # consuming Vagrantfile. The core Package middleware appends the SSH
11
+ # private-key config to this same file afterwards.
12
+ class PackageVagrantfile
13
+ def initialize(app, env)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ @env = env
19
+ create_vagrantfile
20
+ @app.call(env)
21
+ end
22
+
23
+ def create_vagrantfile
24
+ arch = @env[:machine].provider_config.arch
25
+ path = Pathname.new(@env["export.temp_dir"]).join("Vagrantfile")
26
+ File.write(path, <<~VF)
27
+ Vagrant.configure("2") do |config|
28
+ config.vm.provider :qemu do |qe|
29
+ qe.arch = "#{arch}"
30
+ end
31
+ end
32
+ VF
33
+ end
34
+ end
35
+ end
36
+ end
37
+ 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
@@ -9,8 +9,21 @@ module VagrantPlugins
9
9
  include Vagrant::Action::Builtin
10
10
 
11
11
  def self.action_package
12
- lambda do |env|
13
- raise Errors::NotSupportedError
12
+ Vagrant::Action::Builder.new.tap do |b|
13
+ b.use ConfigValidate
14
+ b.use Call, IsState, :not_created do |env, b2|
15
+ if env[:result]
16
+ b2.use MessageNotCreated
17
+ next
18
+ end
19
+
20
+ b2.use Vagrant::Action::General::PackageSetupFolders
21
+ b2.use Vagrant::Action::General::PackageSetupFiles
22
+ b2.use StopInstance
23
+ b2.use Vagrant::Action::General::Package
24
+ b2.use Export
25
+ b2.use PackageVagrantfile
26
+ end
14
27
  end
15
28
  end
16
29
 
@@ -173,6 +186,8 @@ module VagrantPlugins
173
186
  autoload :Import, action_root.join("import")
174
187
  autoload :StartInstance, action_root.join("start_instance")
175
188
  autoload :StopInstance, action_root.join("stop_instance")
189
+ autoload :Export, action_root.join("export")
190
+ autoload :PackageVagrantfile, action_root.join("package_vagrantfile")
176
191
  autoload :Destroy, action_root.join("destroy")
177
192
  autoload :CloudInitNetwork, action_root.join("cloud_init_network")
178
193
  autoload :TimedProvision, action_root.join("timed_provision") # some plugins now expect this action to exist
@@ -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
@@ -474,6 +483,25 @@ module VagrantPlugins
474
483
  def disk_dir
475
484
  @data_dir.join(@vm_id)
476
485
  end
486
+
487
+ # Ordered paths of the box's disk overlays, reconstructed by index to
488
+ # match start/import (glob only counts; names rebuild by index so a
489
+ # two-digit suffix can't sort ahead of a single digit).
490
+ def box_disk_paths
491
+ id_dir = disk_dir
492
+ count = id_dir.glob("linked-box*.img").count
493
+ (0...count).map do |i|
494
+ suffix = i > 0 ? "-#{i}" : ""
495
+ id_dir.join("linked-box#{suffix}.img")
496
+ end
497
+ end
498
+
499
+ # Flatten a box disk overlay into a standalone qcow2 at dst. convert reads
500
+ # the whole backing chain and writes a fresh file, so the source overlay
501
+ # (the live VM disk) is never modified.
502
+ def convert_box_disk(src, dst)
503
+ execute("qemu-img", "convert", "-O", "qcow2", src.to_s, dst.to_s)
504
+ end
477
505
  end
478
506
  end
479
507
  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.4.1'
3
+ VERSION = '0.6.0'
4
4
  end
5
5
  end
data/locales/en.yml CHANGED
@@ -15,6 +15,8 @@ en:
15
15
  Stopping the instance...
16
16
  destroying: |-
17
17
  Destroying the instance...
18
+ packaging_disk: |-
19
+ Exporting box disk: %{name}
18
20
  warn_networks: |-
19
21
  Warning! The QEMU provider doesn't support any of the Vagrant
20
22
  high-level network configurations (`config.vm.network`). They
@@ -82,3 +84,24 @@ en:
82
84
 
83
85
  The machine ID has been preserved. Fix the underlying issue and
84
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
@@ -73,6 +73,18 @@ module E2EHelper
73
73
  run_vagrant_cmd("status", "--machine-readable", cwd: cwd, timeout: 15)
74
74
  end
75
75
 
76
+ def vagrant_package(cwd, output:, timeout: 600)
77
+ run_vagrant_cmd("package", "--output", output.to_s, cwd: cwd, timeout: timeout)
78
+ end
79
+
80
+ def vagrant_box_add(name, path, cwd:, timeout: 120)
81
+ run_vagrant_cmd("box", "add", name, path.to_s, "--force", cwd: cwd, timeout: timeout)
82
+ end
83
+
84
+ def vagrant_box_remove(name, cwd:, timeout: 60)
85
+ run_vagrant_cmd("box", "remove", name, "--force", cwd: cwd, timeout: timeout)
86
+ end
87
+
76
88
  private
77
89
 
78
90
  def run_vagrant_cmd(*args, cwd:, timeout: 120)
@@ -118,7 +130,7 @@ RSpec.configure do |config|
118
130
  # e2e runs the INSTALLED plugin (see note above) — fail fast when it
119
131
  # doesn't match the source tree, instead of silently testing a stale gem.
120
132
  config.before(:suite) do
121
- next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"]
133
+ next unless ENV["TEST_QEMU"] || ENV["TEST_VMNET"] || ENV["TEST_SOCKET_VMNET"]
122
134
 
123
135
  require "vagrant-qemu/version"
124
136
  expected = VagrantPlugins::QEMU::VERSION