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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +39 -4
- data/lib/vagrant-qemu/action/export.rb +74 -0
- data/lib/vagrant-qemu/action/package_vagrantfile.rb +37 -0
- data/lib/vagrant-qemu/action/start_instance.rb +2 -0
- data/lib/vagrant-qemu/action.rb +17 -2
- data/lib/vagrant-qemu/config.rb +18 -2
- data/lib/vagrant-qemu/driver.rb +28 -0
- data/lib/vagrant-qemu/errors.rb +12 -0
- data/lib/vagrant-qemu/network/base.rb +17 -0
- data/lib/vagrant-qemu/network/socket_vmnet.rb +60 -0
- data/lib/vagrant-qemu/network.rb +32 -1
- data/lib/vagrant-qemu/version.rb +1 -1
- data/locales/en.yml +23 -0
- data/spec/e2e/helper.rb +13 -1
- data/spec/e2e/package_spec.rb +95 -0
- data/spec/e2e/socket_vmnet_spec.rb +127 -0
- data/spec/e2e/support/qemu_no_stream_shim.sh +14 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/action/export_spec.rb +103 -0
- data/spec/unit/action/package_spec.rb +22 -0
- data/spec/unit/action/package_vagrantfile_spec.rb +41 -0
- data/spec/unit/config_spec.rb +34 -0
- data/spec/unit/driver/package_spec.rb +51 -0
- data/spec/unit/driver/start_cmd_socket_vmnet_spec.rb +144 -0
- data/spec/unit/network/backend_for_spec.rb +4 -0
- data/spec/unit/network/socket_vmnet_spec.rb +146 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ee57a8b6cb0315b36c0084239e92ed69633698845945e6290ba289d71e2889
|
|
4
|
+
data.tar.gz: 99985f04eabced2eabae1914eacb19f053f13613a4f18d40047ac57440bd4a52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffc285799ee3f7be527da85b03d539c6f1f6b9278bc474c00b63eef41ea937bd8d5bf17aae125d801d3e1a9404967b1df9f0fa2b3173f3db452ed0fc60a12727
|
|
7
|
+
data.tar.gz: 43601507f85d2bb8e884e24795cdbb64c4d2d03f3842c1a8feb5cefe437241cdc0bcbbc531da4036540b3e19f6aaf831f9b35cad6c91c29c32da42be5f90abb0
|
data/CHANGELOG.md
CHANGED
|
@@ -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)
|
|
411
|
+
* To avoid root (and this side effect) entirely on macOS, use `net_mode = :socket_vmnet` (see the example above): the [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) daemon holds the root vmnet membership and QEMU connects to it as a normal user (the approach Lima/Colima/minikube take), so `vagrant up` and the other lifecycle commands need no sudo. The `com.apple.developer.networking.vmnet` entitlement could also bypass root in principle, but it is a *restricted* Apple entitlement that requires an Apple-provisioned signing certificate and cannot be ad-hoc / self-signed onto Homebrew's QEMU, so it is not a practical option for individual users.
|
|
378
412
|
* Without `advanced_network = true`, the `private_network` configuration is ignored with a warning
|
|
379
413
|
* When only one NIC is needed (no `private_network`), no cloud-init seed is attached, avoiding compatibility issues
|
|
380
414
|
* Combining `advanced_network` with `config.vm.cloud_init` is supported: the plugin merges your user-data and the generated network-config into a single NoCloud seed
|
|
@@ -387,7 +421,8 @@ Platform support:
|
|
|
387
421
|
|
|
388
422
|
| Platform | Backend (`net_mode`) | Host ↔ VM | VM ↔ VM | Root? | External dependency |
|
|
389
423
|
|----------|---------|:---------:|:-------:|:-----:|:-------------------:|
|
|
390
|
-
| macOS | `:vmnet_shared`/`_host`/`_bridged` | Yes | Yes | sudo
|
|
424
|
+
| macOS | `:vmnet_shared`/`_host`/`_bridged` | Yes | Yes | sudo | None (QEMU >= 7.0) |
|
|
425
|
+
| macOS | `:socket_vmnet` | Yes | Yes | No | socket_vmnet daemon (`brew install socket_vmnet`) |
|
|
391
426
|
| macOS | `:socket` (`listen`/`connect`) | No (use port forwarding) | Yes (2 VMs) | No | None |
|
|
392
427
|
| Linux | `:tap` + bridge | Yes | Yes | sudo | Pre-created tap device + bridge (`ip` command) |
|
|
393
428
|
| Linux | `:socket` (`mcast`) | No (use port forwarding) | Yes | No | None |
|
|
@@ -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
|
data/lib/vagrant-qemu/action.rb
CHANGED
|
@@ -9,8 +9,21 @@ module VagrantPlugins
|
|
|
9
9
|
include Vagrant::Action::Builtin
|
|
10
10
|
|
|
11
11
|
def self.action_package
|
|
12
|
-
|
|
13
|
-
|
|
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
|
data/lib/vagrant-qemu/config.rb
CHANGED
|
@@ -29,7 +29,7 @@ module VagrantPlugins
|
|
|
29
29
|
attr_accessor :graceful_timeout # seconds to wait for guest shutdown before force kill
|
|
30
30
|
# Advanced networking options
|
|
31
31
|
attr_accessor :advanced_network # bool, opt-in for dual-NIC setup
|
|
32
|
-
attr_accessor :net_mode # :auto, :vmnet_shared, :vmnet_host, :vmnet_bridged, :tap, :socket
|
|
32
|
+
attr_accessor :net_mode # :auto, :vmnet_shared, :vmnet_host, :vmnet_bridged, :tap, :socket, :socket_vmnet
|
|
33
33
|
attr_accessor :vmnet_interface # physical interface for vmnet-bridged (e.g. "en0")
|
|
34
34
|
attr_accessor :tap_device # tap device name for Linux tap backend
|
|
35
35
|
attr_accessor :mcast_addr # convenience shortcut for the :socket backend's multicast address
|
|
@@ -39,6 +39,10 @@ module VagrantPlugins
|
|
|
39
39
|
# The mode (multicast vs point-to-point listen/connect) and any roles are
|
|
40
40
|
# entirely the user's choice. Overrides mcast_addr when set.
|
|
41
41
|
attr_accessor :socket_opts
|
|
42
|
+
# socket_vmnet backend (macOS): the daemon's unix socket, and the client
|
|
43
|
+
# wrapper used only on QEMU without the native `stream` netdev.
|
|
44
|
+
attr_accessor :socket_vmnet_socket
|
|
45
|
+
attr_accessor :socket_vmnet_client
|
|
42
46
|
|
|
43
47
|
def initialize
|
|
44
48
|
@ssh_host = UNSET_VALUE
|
|
@@ -71,6 +75,8 @@ module VagrantPlugins
|
|
|
71
75
|
@tap_device = UNSET_VALUE
|
|
72
76
|
@mcast_addr = UNSET_VALUE
|
|
73
77
|
@socket_opts = UNSET_VALUE
|
|
78
|
+
@socket_vmnet_socket = UNSET_VALUE
|
|
79
|
+
@socket_vmnet_client = UNSET_VALUE
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
#-------------------------------------------------------------------
|
|
@@ -123,6 +129,8 @@ module VagrantPlugins
|
|
|
123
129
|
@tap_device = nil if @tap_device == UNSET_VALUE
|
|
124
130
|
@mcast_addr = nil if @mcast_addr == UNSET_VALUE
|
|
125
131
|
@socket_opts = nil if @socket_opts == UNSET_VALUE
|
|
132
|
+
@socket_vmnet_socket = "#{homebrew_prefix}/var/run/socket_vmnet" if @socket_vmnet_socket == UNSET_VALUE
|
|
133
|
+
@socket_vmnet_client = "#{homebrew_prefix}/opt/socket_vmnet/bin/socket_vmnet_client" if @socket_vmnet_client == UNSET_VALUE
|
|
126
134
|
|
|
127
135
|
# TODO better error msg
|
|
128
136
|
@ssh_port = Integer(@ssh_port)
|
|
@@ -152,6 +160,14 @@ module VagrantPlugins
|
|
|
152
160
|
end
|
|
153
161
|
end
|
|
154
162
|
|
|
163
|
+
# Homebrew install prefix: env override, else the per-arch default
|
|
164
|
+
# (Apple Silicon vs Intel). Defaults to the host arch (socket_vmnet paths
|
|
165
|
+
# are host-side); default_qemu_dir passes the guest arch to keep its
|
|
166
|
+
# firmware-dir behavior.
|
|
167
|
+
def homebrew_prefix(arch = host_arch)
|
|
168
|
+
ENV["HOMEBREW_PREFIX"] || (arch == "aarch64" ? "/opt/homebrew" : "/usr/local")
|
|
169
|
+
end
|
|
170
|
+
|
|
155
171
|
# QEMU data dir (firmware images). Only actually consumed for aarch64
|
|
156
172
|
# firmware, but resolved generically: explicit env override, then the
|
|
157
173
|
# Homebrew prefix, then a per-platform default.
|
|
@@ -161,7 +177,7 @@ module VagrantPlugins
|
|
|
161
177
|
|
|
162
178
|
case RbConfig::CONFIG["host_os"]
|
|
163
179
|
when /darwin/
|
|
164
|
-
arch
|
|
180
|
+
"#{homebrew_prefix(arch)}/share/qemu"
|
|
165
181
|
else
|
|
166
182
|
"/usr/share/qemu"
|
|
167
183
|
end
|
data/lib/vagrant-qemu/driver.rb
CHANGED
|
@@ -114,6 +114,7 @@ module VagrantPlugins
|
|
|
114
114
|
cmd += %W(-m #{options[:memory]}) if !options[:memory].nil?
|
|
115
115
|
|
|
116
116
|
# network
|
|
117
|
+
launch_prefix = []
|
|
117
118
|
if !options[:net_device].nil?
|
|
118
119
|
private_networks = options[:private_networks] || []
|
|
119
120
|
use_advanced = options[:advanced_network] && !private_networks.empty?
|
|
@@ -139,8 +140,12 @@ module VagrantPlugins
|
|
|
139
140
|
# (the static-IP cloud-init seed is built and attached by the
|
|
140
141
|
# CloudInitNetwork action, not here)
|
|
141
142
|
backend = Network.backend_for(options[:net_mode])
|
|
143
|
+
backend.preflight!(options, qemu_binary)
|
|
142
144
|
cmd += %W(-device #{options[:net_device]},netdev=net1,mac=#{mac1})
|
|
143
145
|
cmd += backend.build_netdev_args("net1", options)
|
|
146
|
+
# launch_prefix is applied under the same gate as build_netdev_args
|
|
147
|
+
# (empty for every backend except socket_vmnet's wrapper route).
|
|
148
|
+
launch_prefix = backend.launch_prefix(options)
|
|
144
149
|
else
|
|
145
150
|
# Single NIC: user-mode only (original behavior, no cloud-init)
|
|
146
151
|
cmd += %W(-device #{options[:net_device]},netdev=net0)
|
|
@@ -206,6 +211,10 @@ module VagrantPlugins
|
|
|
206
211
|
# user-defined
|
|
207
212
|
cmd += options[:extra_qemu_args]
|
|
208
213
|
|
|
214
|
+
# socket_vmnet wrapper route: launch qemu under socket_vmnet_client
|
|
215
|
+
# (empty prefix otherwise -> zero difference from the current path).
|
|
216
|
+
cmd = launch_prefix + cmd
|
|
217
|
+
|
|
209
218
|
opts = {:detach => options[:no_daemonize]}
|
|
210
219
|
execute(*cmd, **opts)
|
|
211
220
|
end
|
|
@@ -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
|
data/lib/vagrant-qemu/errors.rb
CHANGED
|
@@ -38,6 +38,18 @@ module VagrantPlugins
|
|
|
38
38
|
class DestroyError < VagrantQEMUError
|
|
39
39
|
error_key(:destroy_error)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
class SocketVmnetNotMacos < VagrantQEMUError
|
|
43
|
+
error_key(:socket_vmnet_not_macos)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class SocketVmnetSocketNotFound < VagrantQEMUError
|
|
47
|
+
error_key(:socket_vmnet_socket_not_found)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class SocketVmnetClientNotFound < VagrantQEMUError
|
|
51
|
+
error_key(:socket_vmnet_client_not_found)
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
@@ -15,6 +15,23 @@ module VagrantPlugins
|
|
|
15
15
|
def requires_sudo?
|
|
16
16
|
false
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# Tokens to prepend to the QEMU launch command (a wrapper program).
|
|
20
|
+
# Empty by default; only socket_vmnet's wrapper route is non-empty.
|
|
21
|
+
# The driver applies this under the same gate as build_netdev_args.
|
|
22
|
+
# @param options [Hash] provider config options
|
|
23
|
+
# @return [Array<String>]
|
|
24
|
+
def launch_prefix(options)
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validate preconditions and resolve any backend-specific options
|
|
29
|
+
# before the command is built. No-op by default; the driver calls it
|
|
30
|
+
# under the same gate as build_netdev_args.
|
|
31
|
+
# @param options [Hash] provider config options (may be mutated)
|
|
32
|
+
# @param qemu_binary [String] the resolved qemu-system-* binary
|
|
33
|
+
def preflight!(options, qemu_binary)
|
|
34
|
+
end
|
|
18
35
|
end
|
|
19
36
|
end
|
|
20
37
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
require_relative "../errors"
|
|
3
|
+
|
|
4
|
+
module VagrantPlugins
|
|
5
|
+
module QEMU
|
|
6
|
+
module Network
|
|
7
|
+
# socket_vmnet backend (macOS) -- connects the private NIC to a
|
|
8
|
+
# socket_vmnet daemon so QEMU runs without sudo (the daemon holds the
|
|
9
|
+
# root vmnet.framework membership).
|
|
10
|
+
#
|
|
11
|
+
# Two routes, chosen by whether the QEMU binary has the native `stream`
|
|
12
|
+
# netdev (options[:use_stream], set by the driver from a probe):
|
|
13
|
+
# stream (QEMU >= 7.2): QEMU connects to the daemon's unix socket
|
|
14
|
+
# directly -- a pure netdev backend, no launch wrapper.
|
|
15
|
+
# wrapper (older QEMU): QEMU is launched under `socket_vmnet_client
|
|
16
|
+
# <sock>`, which hands the connection in on fd 3.
|
|
17
|
+
#
|
|
18
|
+
# build_netdev_args and launch_prefix are driven by the same use_stream
|
|
19
|
+
# so they never disagree.
|
|
20
|
+
class SocketVmnet < Base
|
|
21
|
+
# Resolve the route and fail fast on missing preconditions. Sets
|
|
22
|
+
# options[:use_stream]: true when the qemu binary has the native
|
|
23
|
+
# `stream` netdev, false when it definitively lacks it, and true when
|
|
24
|
+
# the probe couldn't determine (rare) -- stream needs no extra dependency.
|
|
25
|
+
def preflight!(options, qemu_binary)
|
|
26
|
+
raise Errors::SocketVmnetNotMacos unless RbConfig::CONFIG["host_os"] =~ /darwin/
|
|
27
|
+
|
|
28
|
+
options[:use_stream] = Network.qemu_supports_stream?(qemu_binary) != false
|
|
29
|
+
|
|
30
|
+
socket = options[:socket_vmnet_socket]
|
|
31
|
+
unless socket && File.exist?(socket)
|
|
32
|
+
raise Errors::SocketVmnetSocketNotFound, socket: socket
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return if options[:use_stream]
|
|
36
|
+
|
|
37
|
+
client = options[:socket_vmnet_client]
|
|
38
|
+
unless client && (::Vagrant::Util::Which.which(client) || File.executable?(client))
|
|
39
|
+
raise Errors::SocketVmnetClientNotFound, client: client
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_netdev_args(id, options)
|
|
44
|
+
if options[:use_stream]
|
|
45
|
+
sock = options[:socket_vmnet_socket]
|
|
46
|
+
%W(-netdev stream,id=#{id},server=off,addr.type=unix,addr.path=#{sock})
|
|
47
|
+
else
|
|
48
|
+
%W(-netdev socket,id=#{id},fd=3)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def launch_prefix(options)
|
|
53
|
+
return [] if options[:use_stream]
|
|
54
|
+
|
|
55
|
+
%W(#{options[:socket_vmnet_client]} #{options[:socket_vmnet_socket]})
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/vagrant-qemu/network.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "network/base"
|
|
|
2
2
|
require_relative "network/vmnet"
|
|
3
3
|
require_relative "network/tap"
|
|
4
4
|
require_relative "network/socket"
|
|
5
|
+
require_relative "network/socket_vmnet"
|
|
5
6
|
|
|
6
7
|
module VagrantPlugins
|
|
7
8
|
module QEMU
|
|
@@ -9,7 +10,7 @@ module VagrantPlugins
|
|
|
9
10
|
# Select the appropriate network backend based on net_mode and platform.
|
|
10
11
|
#
|
|
11
12
|
# @param net_mode [Symbol] :auto, :vmnet_shared, :vmnet_host,
|
|
12
|
-
# :vmnet_bridged, :tap, :socket
|
|
13
|
+
# :vmnet_bridged, :tap, :socket, :socket_vmnet
|
|
13
14
|
# @return [Base] a network backend instance
|
|
14
15
|
def self.backend_for(net_mode)
|
|
15
16
|
case net_mode
|
|
@@ -19,6 +20,8 @@ module VagrantPlugins
|
|
|
19
20
|
Tap.new
|
|
20
21
|
when :socket
|
|
21
22
|
Socket.new
|
|
23
|
+
when :socket_vmnet
|
|
24
|
+
SocketVmnet.new
|
|
22
25
|
when :auto
|
|
23
26
|
auto_detect
|
|
24
27
|
else
|
|
@@ -39,6 +42,34 @@ module VagrantPlugins
|
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
# Whether the QEMU binary has the native `stream` netdev (QEMU >= 7.2),
|
|
46
|
+
# so socket_vmnet can connect to the daemon's unix socket directly
|
|
47
|
+
# instead of going through the socket_vmnet_client wrapper.
|
|
48
|
+
#
|
|
49
|
+
# Probes `<qemu> -M none -netdev help` and looks for a `stream` line on
|
|
50
|
+
# stdout. Cached per binary.
|
|
51
|
+
#
|
|
52
|
+
# Tri-state so the driver can honor the design's fallback direction:
|
|
53
|
+
# true -- probe ran, stream present -> use stream
|
|
54
|
+
# false -- probe ran, stream absent -> use the wrapper (old QEMU)
|
|
55
|
+
# nil -- probe could not run (rare) -> driver defaults to stream
|
|
56
|
+
#
|
|
57
|
+
# @param qemu_binary [String] path/name of the qemu-system-* binary
|
|
58
|
+
# @return [Boolean, nil]
|
|
59
|
+
def self.qemu_supports_stream?(qemu_binary)
|
|
60
|
+
@stream_support ||= {}
|
|
61
|
+
return @stream_support[qemu_binary] if @stream_support.key?(qemu_binary)
|
|
62
|
+
|
|
63
|
+
result =
|
|
64
|
+
begin
|
|
65
|
+
out = ::Vagrant::Util::Subprocess.execute(qemu_binary, "-M", "none", "-netdev", "help")
|
|
66
|
+
out.exit_code == 0 ? out.stdout.lines.any? { |l| l.strip == "stream" } : nil
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
@stream_support[qemu_binary] = result
|
|
71
|
+
end
|
|
72
|
+
|
|
42
73
|
# Generate a deterministic MAC address from vm_id and NIC index.
|
|
43
74
|
# Uses 52:54:00 prefix (QEMU's OUI).
|
|
44
75
|
#
|
data/lib/vagrant-qemu/version.rb
CHANGED
data/locales/en.yml
CHANGED
|
@@ -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
|