vagrant-qemu 0.4.1 → 0.5.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 +14 -0
- data/lib/vagrant-qemu/action/export.rb +74 -0
- data/lib/vagrant-qemu/action/package_vagrantfile.rb +37 -0
- data/lib/vagrant-qemu/action.rb +17 -2
- data/lib/vagrant-qemu/driver.rb +19 -0
- data/lib/vagrant-qemu/version.rb +1 -1
- data/locales/en.yml +2 -0
- data/spec/e2e/helper.rb +12 -0
- data/spec/e2e/package_spec.rb +95 -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/driver/package_spec.rb +51 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0968c034faf465b2d51e1f7b785a7a1aed39a4d6c40ef96f6b34147e9ab8ebd
|
|
4
|
+
data.tar.gz: fff8fc272c6c99abb85ccff761137f53ee6aabe8953f4b702ffca61163ea8d27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba77c0b50cd06921799dcc154f8d987a3fe1e367fd4347bbd2a45381f0819815e9f832f29be04a76818fb5e5c24abb4f009fb33350aace09486f1a20f6250012
|
|
7
|
+
data.tar.gz: 6438ed9c811e25f7393e59971028eea43b5f3d76ac9a11982dd2424c8b7cbd54181cbaa514875aebbb54f38520fcab12575929e5c62259f42c37cce83274c34a
|
data/CHANGELOG.md
CHANGED
|
@@ -140,3 +140,17 @@
|
|
|
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.
|
|
@@ -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
|
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/driver.rb
CHANGED
|
@@ -474,6 +474,25 @@ module VagrantPlugins
|
|
|
474
474
|
def disk_dir
|
|
475
475
|
@data_dir.join(@vm_id)
|
|
476
476
|
end
|
|
477
|
+
|
|
478
|
+
# Ordered paths of the box's disk overlays, reconstructed by index to
|
|
479
|
+
# match start/import (glob only counts; names rebuild by index so a
|
|
480
|
+
# two-digit suffix can't sort ahead of a single digit).
|
|
481
|
+
def box_disk_paths
|
|
482
|
+
id_dir = disk_dir
|
|
483
|
+
count = id_dir.glob("linked-box*.img").count
|
|
484
|
+
(0...count).map do |i|
|
|
485
|
+
suffix = i > 0 ? "-#{i}" : ""
|
|
486
|
+
id_dir.join("linked-box#{suffix}.img")
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Flatten a box disk overlay into a standalone qcow2 at dst. convert reads
|
|
491
|
+
# the whole backing chain and writes a fresh file, so the source overlay
|
|
492
|
+
# (the live VM disk) is never modified.
|
|
493
|
+
def convert_box_disk(src, dst)
|
|
494
|
+
execute("qemu-img", "convert", "-O", "qcow2", src.to_s, dst.to_s)
|
|
495
|
+
end
|
|
477
496
|
end
|
|
478
497
|
end
|
|
479
498
|
end
|
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
|
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)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# End-to-end round-trip for `vagrant package`. Exercises the INSTALLED plugin
|
|
4
|
+
# (see helper.rb), so rebuild + reinstall before running:
|
|
5
|
+
# bundle exec rake build && vagrant plugin install ./pkg/vagrant-qemu-<ver>.gem
|
|
6
|
+
# TEST_QEMU=1 bundle exec rake spec:e2e
|
|
7
|
+
describe "vagrant package end-to-end", :requires_qemu do
|
|
8
|
+
around(:each) do |example|
|
|
9
|
+
with_temp_dir do |dir|
|
|
10
|
+
@work_dir = dir.join("project")
|
|
11
|
+
@consume_dir = dir.join("consumer")
|
|
12
|
+
@box_path = dir.join("packaged.box")
|
|
13
|
+
FileUtils.mkdir_p(@work_dir)
|
|
14
|
+
FileUtils.mkdir_p(@consume_dir)
|
|
15
|
+
example.run
|
|
16
|
+
vagrant_destroy(@consume_dir) rescue nil
|
|
17
|
+
vagrant_destroy(@work_dir) rescue nil
|
|
18
|
+
vagrant_box_remove("e2e-packaged", cwd: @work_dir) rescue nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_source_vagrantfile
|
|
23
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
24
|
+
Vagrant.configure("2") do |config|
|
|
25
|
+
config.vm.box = "#{test_box}"
|
|
26
|
+
config.vm.box_check_update = false
|
|
27
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
28
|
+
config.vm.provider "qemu" do |qe|
|
|
29
|
+
qe.memory = "2G"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "packages a running VM into a box that boots again (round-trip)" do
|
|
36
|
+
write_source_vagrantfile
|
|
37
|
+
expect(vagrant_up(@work_dir)[:exit_code]).to eq 0
|
|
38
|
+
|
|
39
|
+
result = vagrant_package(@work_dir, output: @box_path)
|
|
40
|
+
expect(result[:exit_code]).to eq 0
|
|
41
|
+
expect(File.exist?(@box_path)).to be true
|
|
42
|
+
|
|
43
|
+
# Box layout: box.img + metadata.json + Vagrantfile.
|
|
44
|
+
listing = `tar tzf #{@box_path}`
|
|
45
|
+
expect(listing).to include("box.img")
|
|
46
|
+
expect(listing).to include("metadata.json")
|
|
47
|
+
expect(listing).to include("Vagrantfile")
|
|
48
|
+
|
|
49
|
+
# box.img must be a self-contained qcow2 (overlay flattened, no backing).
|
|
50
|
+
Dir.mktmpdir do |ex|
|
|
51
|
+
system("tar xzf #{@box_path} -C #{ex} box.img")
|
|
52
|
+
info = `qemu-img info --output=json #{File.join(ex, "box.img")}`
|
|
53
|
+
expect(info).to include('"format": "qcow2"')
|
|
54
|
+
expect(info).not_to match(/backing-filename/)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Consume the packaged box in a fresh project and boot it.
|
|
58
|
+
expect(vagrant_box_add("e2e-packaged", @box_path, cwd: @work_dir)[:exit_code]).to eq 0
|
|
59
|
+
File.write(@consume_dir.join("Vagrantfile"), <<~RUBY)
|
|
60
|
+
Vagrant.configure("2") do |config|
|
|
61
|
+
config.vm.box = "e2e-packaged"
|
|
62
|
+
config.vm.box_check_update = false
|
|
63
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
64
|
+
config.vm.provider "qemu" do |qe|
|
|
65
|
+
qe.memory = "2G"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
expect(vagrant_up(@consume_dir)[:exit_code]).to eq 0
|
|
70
|
+
expect(vagrant_ssh(@consume_dir, command: "echo roundtrip-ok")[:stdout]).to include("roundtrip-ok")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "does not bake environment-specific network config into the box" do
|
|
74
|
+
File.write(@work_dir.join("Vagrantfile"), <<~RUBY)
|
|
75
|
+
Vagrant.configure("2") do |config|
|
|
76
|
+
config.vm.box = "#{test_box}"
|
|
77
|
+
config.vm.box_check_update = false
|
|
78
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
|
79
|
+
config.vm.network "forwarded_port", guest: 80, host: 8080, auto_correct: true
|
|
80
|
+
config.vm.provider "qemu" do |qe|
|
|
81
|
+
qe.memory = "2G"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
expect(vagrant_up(@work_dir)[:exit_code]).to eq 0
|
|
86
|
+
expect(vagrant_package(@work_dir, output: @box_path)[:exit_code]).to eq 0
|
|
87
|
+
|
|
88
|
+
Dir.mktmpdir do |ex|
|
|
89
|
+
system("tar xzf #{@box_path} -C #{ex} Vagrantfile")
|
|
90
|
+
vf = File.read(File.join(ex, "Vagrantfile"))
|
|
91
|
+
expect(vf).to include("qe.arch")
|
|
92
|
+
expect(vf).not_to match(/forwarded_port|network|8080/)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "json"
|
|
3
|
+
require "vagrant-qemu/action/export"
|
|
4
|
+
|
|
5
|
+
describe VagrantPlugins::QEMU::Action::Export do
|
|
6
|
+
let(:vm_id) { "vq_export" }
|
|
7
|
+
let(:app) { lambda { |env| } }
|
|
8
|
+
let(:ui) { double("ui", info: nil, detail: nil) }
|
|
9
|
+
|
|
10
|
+
around(:each) do |example|
|
|
11
|
+
with_temp_dir do |dir|
|
|
12
|
+
@data_dir = dir.join("data")
|
|
13
|
+
@tmp_base = dir.join("tmp")
|
|
14
|
+
@export_dir = dir.join("export")
|
|
15
|
+
FileUtils.mkdir_p(@data_dir.join(vm_id))
|
|
16
|
+
FileUtils.mkdir_p(@tmp_base)
|
|
17
|
+
FileUtils.mkdir_p(@export_dir)
|
|
18
|
+
example.run
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:driver) { VagrantPlugins::QEMU::Driver.new(vm_id, @data_dir, @tmp_base) }
|
|
23
|
+
let(:provider) { double("provider", driver: driver) }
|
|
24
|
+
let(:state) { double("state", id: :stopped) }
|
|
25
|
+
let(:machine) do
|
|
26
|
+
c = VagrantPlugins::QEMU::Config.new
|
|
27
|
+
c.arch = "aarch64"
|
|
28
|
+
c.finalize!
|
|
29
|
+
double("machine", provider: provider, provider_config: c, name: "default", state: state)
|
|
30
|
+
end
|
|
31
|
+
let(:env) { { machine: machine, ui: ui, "export.temp_dir" => @export_dir.to_s } }
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
VagrantPlugins::QEMU::Plugin.setup_i18n
|
|
35
|
+
# Don't invoke real qemu-img convert; just record calls.
|
|
36
|
+
allow(driver).to receive(:convert_box_disk)
|
|
37
|
+
# virtual-size from qemu-img info: 1 GiB + 1 byte → ceil to 2 GB.
|
|
38
|
+
status = double("status", success?: true)
|
|
39
|
+
allow(Open3).to receive(:capture3)
|
|
40
|
+
.and_return([JSON.generate("virtual-size" => (1024**3) + 1), "", status])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def metadata
|
|
44
|
+
JSON.parse(File.read(@export_dir.join("metadata.json")))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "single box disk (v1)" do
|
|
48
|
+
before { FileUtils.touch(@data_dir.join(vm_id, "linked-box.img")) }
|
|
49
|
+
|
|
50
|
+
it "flattens the disk to box.img and writes libvirt v1 metadata (no disks[])" do
|
|
51
|
+
expect(driver).to receive(:convert_box_disk)
|
|
52
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box.img"))
|
|
53
|
+
described_class.new(app, env).call(env)
|
|
54
|
+
|
|
55
|
+
expect(metadata["provider"]).to eq "libvirt"
|
|
56
|
+
expect(metadata["format"]).to eq "qcow2"
|
|
57
|
+
expect(metadata["virtual_size"]).to eq 2
|
|
58
|
+
expect(metadata).not_to have_key("disks")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context "multiple box disks (v2)" do
|
|
63
|
+
before do
|
|
64
|
+
%w[linked-box.img linked-box-1.img].each { |n| FileUtils.touch(@data_dir.join(vm_id, n)) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "flattens each disk to box_N.img and writes disks[]" do
|
|
68
|
+
expect(driver).to receive(:convert_box_disk)
|
|
69
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box_1.img"))
|
|
70
|
+
expect(driver).to receive(:convert_box_disk)
|
|
71
|
+
.with(@data_dir.join(vm_id, "linked-box-1.img"), @export_dir.join("box_2.img"))
|
|
72
|
+
described_class.new(app, env).call(env)
|
|
73
|
+
|
|
74
|
+
expect(metadata["disks"]).to eq([{ "path" => "box_1.img" }, { "path" => "box_2.img" }])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "environment-specific artifacts present" do
|
|
79
|
+
before do
|
|
80
|
+
FileUtils.touch(@data_dir.join(vm_id, "linked-box.img"))
|
|
81
|
+
FileUtils.touch(@data_dir.join(vm_id, "extra-data.qcow2"))
|
|
82
|
+
FileUtils.touch(@data_dir.join(vm_id, "vagrant-qemu-network.iso"))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "packages only the box disk, not additional disks or ISOs" do
|
|
86
|
+
expect(driver).to receive(:convert_box_disk).once
|
|
87
|
+
.with(@data_dir.join(vm_id, "linked-box.img"), @export_dir.join("box.img"))
|
|
88
|
+
described_class.new(app, env).call(env)
|
|
89
|
+
expect(metadata).not_to have_key("disks")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "VM not powered off" do
|
|
94
|
+
let(:state) { double("state", id: :running) }
|
|
95
|
+
before { FileUtils.touch(@data_dir.join(vm_id, "linked-box.img")) }
|
|
96
|
+
|
|
97
|
+
it "raises and does not convert" do
|
|
98
|
+
expect(driver).not_to receive(:convert_box_disk)
|
|
99
|
+
expect { described_class.new(app, env).call(env) }
|
|
100
|
+
.to raise_error(Vagrant::Errors::VMPowerOffToPackage)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Action, "#action_package" do
|
|
4
|
+
it "returns a runnable builder instead of raising NotSupportedError" do
|
|
5
|
+
expect(described_class.action_package).to be_a(Vagrant::Action::Builder)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "no longer raises (package is supported now)" do
|
|
9
|
+
expect { described_class.action_package }.not_to raise_error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "validates config and branches on machine state at the top of the chain" do
|
|
13
|
+
klasses = described_class.action_package.stack.map(&:first)
|
|
14
|
+
expect(klasses).to include(Vagrant::Action::Builtin::ConfigValidate)
|
|
15
|
+
expect(klasses).to include(Vagrant::Action::Builtin::Call)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "autoloads the Export and PackageVagrantfile actions" do
|
|
19
|
+
expect { described_class::Export }.not_to raise_error
|
|
20
|
+
expect { described_class::PackageVagrantfile }.not_to raise_error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "vagrant-qemu/action/package_vagrantfile"
|
|
3
|
+
|
|
4
|
+
describe VagrantPlugins::QEMU::Action::PackageVagrantfile do
|
|
5
|
+
let(:app) { lambda { |env| } }
|
|
6
|
+
let(:ui) { double("ui", info: nil) }
|
|
7
|
+
|
|
8
|
+
around(:each) do |example|
|
|
9
|
+
with_temp_dir do |dir|
|
|
10
|
+
@export_dir = dir.join("export")
|
|
11
|
+
FileUtils.mkdir_p(@export_dir)
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def machine_with(arch)
|
|
17
|
+
c = VagrantPlugins::QEMU::Config.new
|
|
18
|
+
c.arch = arch
|
|
19
|
+
c.finalize!
|
|
20
|
+
double("machine", provider_config: c)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(arch)
|
|
24
|
+
env = { machine: machine_with(arch), ui: ui, "export.temp_dir" => @export_dir.to_s }
|
|
25
|
+
described_class.new(app, env).call(env)
|
|
26
|
+
File.read(@export_dir.join("Vagrantfile"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "writes the VM arch into the default Vagrantfile" do
|
|
30
|
+
expect(run("aarch64")).to include('qe.arch = "aarch64"')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "carries a different arch through" do
|
|
34
|
+
expect(run("x86_64")).to include('qe.arch = "x86_64"')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "does not bake environment-specific network/MAC/machine config" do
|
|
38
|
+
vf = run("aarch64")
|
|
39
|
+
expect(vf).not_to match(/network|base_mac|accel|machine/i)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe VagrantPlugins::QEMU::Driver, "package helpers" do
|
|
4
|
+
let(:vm_id) { "vq_pkgtest" }
|
|
5
|
+
|
|
6
|
+
around(:each) do |example|
|
|
7
|
+
with_temp_dir do |dir|
|
|
8
|
+
@data_dir = dir.join("data")
|
|
9
|
+
@tmp_base = dir.join("tmp")
|
|
10
|
+
FileUtils.mkdir_p(@data_dir.join(vm_id))
|
|
11
|
+
FileUtils.mkdir_p(@tmp_base)
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subject { described_class.new(vm_id, @data_dir, @tmp_base) }
|
|
17
|
+
|
|
18
|
+
describe "#box_disk_paths" do
|
|
19
|
+
it "enumerates box disks by index, matching start/import order (not glob lexical)" do
|
|
20
|
+
id_dir = @data_dir.join(vm_id)
|
|
21
|
+
# Create out of lexical order incl. a two-digit index to catch lexical-sort bugs.
|
|
22
|
+
%w[linked-box.img linked-box-1.img linked-box-2.img].each { |n| FileUtils.touch(id_dir.join(n)) }
|
|
23
|
+
|
|
24
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) })
|
|
25
|
+
.to eq %w[linked-box.img linked-box-1.img linked-box-2.img]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns a single box.img overlay for a single-disk VM" do
|
|
29
|
+
FileUtils.touch(@data_dir.join(vm_id, "linked-box.img"))
|
|
30
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) }).to eq %w[linked-box.img]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "excludes additional disks and ISOs (only linked-box*.img are box disks)" do
|
|
34
|
+
id_dir = @data_dir.join(vm_id)
|
|
35
|
+
FileUtils.touch(id_dir.join("linked-box.img"))
|
|
36
|
+
FileUtils.touch(id_dir.join("extra-data.qcow2"))
|
|
37
|
+
FileUtils.touch(id_dir.join("vagrant-qemu-network.iso"))
|
|
38
|
+
|
|
39
|
+
expect(subject.box_disk_paths.map { |p| File.basename(p) }).to eq %w[linked-box.img]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#convert_box_disk" do
|
|
44
|
+
it "flattens via qemu-img convert to a fresh qcow2 (never mutates the source)" do
|
|
45
|
+
src = @data_dir.join(vm_id, "linked-box.img")
|
|
46
|
+
dst = @data_dir.join(vm_id, "box.img")
|
|
47
|
+
expect(subject).to receive(:execute).with("qemu-img", "convert", "-O", "qcow2", src.to_s, dst.to_s)
|
|
48
|
+
subject.convert_box_disk(src, dst)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-qemu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ppggff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Enables Vagrant to manage machines with QEMU.
|
|
14
14
|
email: pgf00a@gmail.com
|
|
@@ -27,10 +27,12 @@ files:
|
|
|
27
27
|
- lib/vagrant-qemu/action.rb
|
|
28
28
|
- lib/vagrant-qemu/action/cloud_init_network.rb
|
|
29
29
|
- lib/vagrant-qemu/action/destroy.rb
|
|
30
|
+
- lib/vagrant-qemu/action/export.rb
|
|
30
31
|
- lib/vagrant-qemu/action/import.rb
|
|
31
32
|
- lib/vagrant-qemu/action/message_already_created.rb
|
|
32
33
|
- lib/vagrant-qemu/action/message_not_created.rb
|
|
33
34
|
- lib/vagrant-qemu/action/message_will_not_destroy.rb
|
|
35
|
+
- lib/vagrant-qemu/action/package_vagrantfile.rb
|
|
34
36
|
- lib/vagrant-qemu/action/prepare_forwarded_port_collision_params.rb
|
|
35
37
|
- lib/vagrant-qemu/action/read_state.rb
|
|
36
38
|
- lib/vagrant-qemu/action/start_instance.rb
|
|
@@ -61,6 +63,7 @@ files:
|
|
|
61
63
|
- spec/e2e/forwarded_port_spec.rb
|
|
62
64
|
- spec/e2e/halt_spec.rb
|
|
63
65
|
- spec/e2e/helper.rb
|
|
66
|
+
- spec/e2e/package_spec.rb
|
|
64
67
|
- spec/e2e/provision_spec.rb
|
|
65
68
|
- spec/e2e/reload_spec.rb
|
|
66
69
|
- spec/e2e/smoke_spec.rb
|
|
@@ -68,7 +71,10 @@ files:
|
|
|
68
71
|
- spec/spec_helper.rb
|
|
69
72
|
- spec/unit/action/cloud_init_network_spec.rb
|
|
70
73
|
- spec/unit/action/destroy_spec.rb
|
|
74
|
+
- spec/unit/action/export_spec.rb
|
|
71
75
|
- spec/unit/action/import_spec.rb
|
|
76
|
+
- spec/unit/action/package_spec.rb
|
|
77
|
+
- spec/unit/action/package_vagrantfile_spec.rb
|
|
72
78
|
- spec/unit/action/prepare_forwarded_port_collision_params_spec.rb
|
|
73
79
|
- spec/unit/action/read_state_spec.rb
|
|
74
80
|
- spec/unit/action/start_instance_port_spec.rb
|
|
@@ -77,6 +83,7 @@ files:
|
|
|
77
83
|
- spec/unit/config_spec.rb
|
|
78
84
|
- spec/unit/driver/delete_spec.rb
|
|
79
85
|
- spec/unit/driver/options_yaml_spec.rb
|
|
86
|
+
- spec/unit/driver/package_spec.rb
|
|
80
87
|
- spec/unit/driver/ssh_port_spec.rb
|
|
81
88
|
- spec/unit/driver/start_cmd_advanced_spec.rb
|
|
82
89
|
- spec/unit/driver/start_cmd_order_spec.rb
|