Repository: osbuild/osbuild
Pull request #2143 (2025-07-10 08:55:23)

stages/vagrant: allow configuring synced folders (HMS-6116)
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`.


Patch:
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",


Test patch:
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}'