From fd6801e380a87fb0026c1c73af7e17614fa6efb8 Mon Sep 17 00:00:00 2001 From: Simon de Vlieger Date: Thu, 10 Jul 2025 10:53:05 +0200 Subject: [PATCH] stages/vagrant: allow configuring synced folders When guest additions are not installed in VirtualBox Vagrant boxes then the default shared `/vagrant` directory must be set to `rsync`, otherwise Vagrant fails to start as the shared directory cannot be mounted with the `vboxfs` filesystem. Let's expand the schema to allow for `synced-folders` (currently only accepted under the `virtualbox` variant of the schema) to allow setting the bare subset of relevant options to configure this from `images`. Signed-off-by: Simon de Vlieger --- stages/org.osbuild.vagrant | 3 + stages/org.osbuild.vagrant.meta.json | 19 ++++++ stages/test/test_vagrant.py | 93 +++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/stages/org.osbuild.vagrant b/stages/org.osbuild.vagrant index 6719d69d..da157140 100755 --- a/stages/org.osbuild.vagrant +++ b/stages/org.osbuild.vagrant @@ -59,6 +59,9 @@ def main(tree, options, inputs): if provider == "virtualbox": vagrant_content = VAGRANTFILE_VIRTUALBOX.format(mac_address=options["virtualbox"]["mac_address"]) + for path, data in options.get("synced_folders", {}).items(): + vagrant_content += f' config.vm.synced_folder ".", "{path}", type: "{data["type"]}"\n' + with open(f"{tree}/Vagrantfile", "w", encoding="utf8") as fp: fp.write(VAGRANTFILE.format(content=vagrant_content)) diff --git a/stages/org.osbuild.vagrant.meta.json b/stages/org.osbuild.vagrant.meta.json index 5c304354..5aca4983 100644 --- a/stages/org.osbuild.vagrant.meta.json +++ b/stages/org.osbuild.vagrant.meta.json @@ -41,6 +41,25 @@ "virtualbox" ] }, + "synced_folders": { + "type": "object", + "additionalProperties": false, + "properties": { + "/vagrant": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "vboxfs", + "rsync" + ] + } + } + } + } + }, "virtualbox": { "type": "object", "description": "VirtualBox specific settings", diff --git a/stages/test/test_vagrant.py b/stages/test/test_vagrant.py index 1506c016..f8b39781 100644 --- a/stages/test/test_vagrant.py +++ b/stages/test/test_vagrant.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +from unittest import mock import pytest @@ -15,9 +16,11 @@ STAGE_NAME = "org.osbuild.vagrant" ({"provider": "virtualbox"}, "not valid under any of the given schemas"), ({"provider": "virtualbox", "virtualbox": {}}, "not valid under any of the given schemas"), ({"provider": "libvirt", "virtualbox": {"mac_address": "1"}}, "not valid under any of the given schemas"), + ({"provider": "libvirt", "synced_folders": {"/vagrant": {"type": "vboxfs"}}}, "not valid under any of the given schemas"), # Good API parameters ({"provider": "libvirt"}, ""), - ({"provider": "virtualbox", "virtualbox": {"mac_address": "000000000000"}}, ""), + ({"provider": "virtualbox", "virtualbox": {"mac_address": "000000000000"}, + "synced_folders": {"/vagrant": {"type": "rsync"}}}, ""), ]) # This test validates only API calls using correct and incorrect queries def test_schema_validation_vagrant(stage_schema, test_data, expected_err): @@ -39,3 +42,91 @@ def test_schema_validation_vagrant(stage_schema, test_data, expected_err): else: assert res.valid is False, f"err: {[e.as_dict() for e in res.errors]}" testutil.assert_jsonschema_error_contains(res, expected_err) + + +@mock.patch("subprocess.run") +def test_vagrant_writes_file_without_rsync_when_guest_additions(_mock_run, tmp_path, stage_module): + treepath = tmp_path / "tree" + treepath.mkdir() + + vagrantfile = treepath / "Vagrantfile" + metadatafile = treepath / "metadata.json" + + options = { + "provider": "virtualbox", + "virtualbox": { + "mac_address": "000000000000", + }, + "synced_folders": { + "/vagrant": { + "type": "rsync", + }, + } + } + + inputs = { + "image": { + "path": "/foo", + "data": { + "files": { + "image.vmdk": {"path": "foo.vmdk"} + } + } + } + } + + stage_module.main(treepath, options, inputs) + + assert vagrantfile.exists() + assert metadatafile.exists() + + assert vagrantfile.read_text() == """Vagrant.configure("2") do |config| + config.vm.base_mac = "000000000000" + config.vm.synced_folder ".", "/vagrant", type: "rsync" + +end +""" + + assert metadatafile.read_text() == '{"provider": "virtualbox"}' + + +@mock.patch("subprocess.run") +@mock.patch("subprocess.check_output") +def test_vagrant_writes_file_for_libvirt(mock_check_output, _mock_run, tmp_path, stage_module): + mock_check_output.return_value = '{"virtual-size": 1000000000}' + + treepath = tmp_path / "tree" + treepath.mkdir() + + vagrantfile = treepath / "Vagrantfile" + metadatafile = treepath / "metadata.json" + + options = { + "provider": "libvirt", + } + + inputs = { + "image": { + "path": "/foo", + "data": { + "files": { + "image.vmdk": {"path": "foo.vmdk"} + } + } + } + } + + stage_module.main(treepath, options, inputs) + + assert vagrantfile.exists() + assert metadatafile.exists() + + assert vagrantfile.read_text() == """Vagrant.configure("2") do |config| + config.vm.provider :libvirt do |libvirt| + libvirt.driver = "kvm" +end + +end +""" + + assert metadatafile.read_text() == '{"provider": "libvirt", "format": "qcow2", "virtual_size": 1}'