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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cd40c0eae22a6a6d4f284004a86de09e0584e8887cc273e9acd4c0fa2f33e69
4
- data.tar.gz: 4caaa2706bbb8f221dcc67e5164e7b38879e3cb2d709d8414fc1f0a0ed0de45b
3
+ metadata.gz: b0968c034faf465b2d51e1f7b785a7a1aed39a4d6c40ef96f6b34147e9ab8ebd
4
+ data.tar.gz: fff8fc272c6c99abb85ccff761137f53ee6aabe8953f4b702ffca61163ea8d27
5
5
  SHA512:
6
- metadata.gz: dd592ca7bf0a75c82b58db4ed8194f8a3e31b44834538ea0fe7ae7124032871422b9b4f2f6b49122964fefc0692f02d1be4a018bae2e4948f3f8aeeb9385eaaf
7
- data.tar.gz: 906b02b551b9f3c5082a3967edc33c656f9b642ae0f6bbabcd8bb40b944fa0e2d67f10c5b6b34af3291516c218d0d7657c47d9cf495d1dabceb63fa5acb4ac77
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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module VagrantPlugins
2
2
  module QEMU
3
- VERSION = '0.4.1'
3
+ VERSION = '0.5.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
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.1
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-06-22 00:00:00.000000000 Z
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