additionalDisks: support custom mountPoint and optional mounting

Additional disks were always mounted at the hardcoded path
/mnt/lima-<name>, with no way to change that location or to attach a disk
without Lima mounting it. Issue #4065 asks for both: a custom mount point
and an attach-only mode so the guest can manage the disk itself (a
flexibility Colima can benefit from).

Add two optional, backwards-compatible fields to each additionalDisks
entry:
- mountPoint: absolute guest path to mount at (default /mnt/lima-<name>)
- mount: whether Lima auto-mounts the disk (default true; false attaches
  the disk without mounting it)

A separate `mount` boolean is used rather than overloading an empty
mountPoint, per the discussion in #4065. nil for either field preserves
the current behavior. Values are passed to the guest via
LIMA_CIDATA_DISK_<i>_MOUNT / _MOUNTPOINT and honored by 05-lima-disks.sh
(non-swap disks only). mountPoint is validated to be an absolute guest
path (path.IsAbs, not the host-dependent filepath.IsAbs) and neither a
critical system path nor the reserved guest home directory.

Fixes #4065

Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
This commit is contained in:
Eugene Kalinin committed 2026-10-02 15:51:20 +03:00
1 parent 3d1c397aa6
commit 1bf6ec2ee8
12 files changed
+155 -19

No files matched your search

@@ -18,9 +18,13 @@ for i in $(seq 0 $((LIMA_CIDATA_DISKS - 1))); do
FORMAT_DISK="$(get_disk_var "$i" "FORMAT")"
FORMAT_FSTYPE="$(get_disk_var "$i" "FSTYPE")"
FORMAT_FSARGS="$(get_disk_var "$i" "FSARGS")"
MOUNT_DISK="$(get_disk_var "$i" "MOUNT")"
MOUNT_POINT="$(get_disk_var "$i" "MOUNTPOINT")"
test -n "$FORMAT_DISK" || FORMAT_DISK=true
test -n "$FORMAT_FSTYPE" || FORMAT_FSTYPE=ext4
test -n "$MOUNT_DISK" || MOUNT_DISK=true
test -n "$MOUNT_POINT" || MOUNT_POINT="/mnt/lima-${DISK_NAME}"
# first time setup
if [[ ! -b "/dev/disk/by-label/lima-${DISK_NAME}" ]]; then
@@ -39,9 +43,9 @@ for i in $(seq 0 $((LIMA_CIDATA_DISKS - 1))); do
if [ "$FORMAT_FSTYPE" == "swap" ]; then
swapon "/dev/${DEVICE_NAME}1"
else
mkdir -p "/mnt/lima-${DISK_NAME}"
mount -t "$FORMAT_FSTYPE" "/dev/${DEVICE_NAME}1" "/mnt/lima-${DISK_NAME}"
elif $MOUNT_DISK; then
mkdir -p "$MOUNT_POINT"
mount -t "$FORMAT_FSTYPE" "/dev/${DEVICE_NAME}1" "$MOUNT_POINT"
fi
if command -v growpart >/dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then
growpart "/dev/${DEVICE_NAME}" 1 || true
+2
View File
@@ -19,6 +19,8 @@ LIMA_CIDATA_DISK_{{$i}}_DEVICE={{$disk.Device}}
LIMA_CIDATA_DISK_{{$i}}_FORMAT={{$disk.Format}}
LIMA_CIDATA_DISK_{{$i}}_FSTYPE={{$disk.FSType}}
LIMA_CIDATA_DISK_{{$i}}_FSARGS={{range $j, $arg := $disk.FSArgs}}{{if $j}} {{end}}{{$arg}}{{end}}
LIMA_CIDATA_DISK_{{$i}}_MOUNT={{$disk.Mount}}
LIMA_CIDATA_DISK_{{$i}}_MOUNTPOINT={{$disk.MountPoint}}
{{- end}}
{{- range $dataFile := .DataFiles}}
LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OVERWRITE={{$dataFile.Overwrite}}
+15 -5
View File
@@ -283,12 +283,22 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
if d.FSType != nil {
fstype = *d.FSType
}
mount := true
if d.Mount != nil {
mount = *d.Mount
}
mountPoint := ""
if d.MountPoint != nil {
mountPoint = *d.MountPoint
}
args.Disks = append(args.Disks, Disk{
Name: d.Name,
Device: diskDeviceNameFromOrder(i),
Format: format,
FSType: fstype,
FSArgs: d.FSArgs,
Name: d.Name,
Device: diskDeviceNameFromOrder(i),
Format: format,
FSType: fstype,
FSArgs: d.FSArgs,
Mount: mount,
MountPoint: mountPoint,
})
}
+7 -5
View File
@@ -82,11 +82,13 @@ type YQProvision struct {
}
type Disk struct {
Name string
Device string
Format bool
FSType string
FSArgs []string
Name string
Device string
Format bool
FSType string
FSArgs []string
Mount bool
MountPoint string
}
type TemplateArgs struct {
Debug bool
+39
View File
@@ -141,6 +141,45 @@ func TestTemplate(t *testing.T) {
}
}
func TestTemplateDisks(t *testing.T) {
args := &TemplateArgs{
Name: "default",
User: "foo",
UID: 501,
Home: "/home/foo.guest",
Shell: "/bin/bash",
SSHPubKeys: []string{
"ssh-rsa dummy foo@example.com",
},
MountType: "reverse-sshfs",
CACerts: CACerts{
RemoveDefaults: &defaultRemoveDefaults,
},
Disks: []Disk{
{Name: "data", Device: "vdb", Format: true, FSType: "ext4", Mount: true, MountPoint: "/data"},
{Name: "extra", Device: "vdc", Format: false, Mount: false},
},
}
layout, err := ExecuteTemplateCIDataISO(args)
assert.NilError(t, err)
var found bool
for _, f := range layout {
if f.Path != "lima.env" {
continue
}
found = true
b, err := io.ReadAll(f.Reader)
assert.NilError(t, err)
env := string(b)
assert.Assert(t, strings.Contains(env, "LIMA_CIDATA_DISK_0_MOUNT=true"), env)
assert.Assert(t, strings.Contains(env, "LIMA_CIDATA_DISK_0_MOUNTPOINT=/data"), env)
assert.Assert(t, strings.Contains(env, "LIMA_CIDATA_DISK_1_MOUNT=false"), env)
// A nil MountPoint renders as empty; the guest script falls back to /mnt/lima-<name>.
assert.Assert(t, strings.Contains(env, "LIMA_CIDATA_DISK_1_MOUNTPOINT=\n"), env)
}
assert.Assert(t, found, "lima.env not found in layout")
}
func TestTemplate9p(t *testing.T) {
args := &TemplateArgs{
Name: "default",
+10
View File
@@ -403,6 +403,16 @@ func (tmpl *Template) combineAdditionalDisks() {
tmpl.copyListEntryField(additionalDisks, dst, src, "fsArgs")
dest.FSArgs = disk.FSArgs
}
if dest.Mount == nil && disk.Mount != nil {
upgradeDiskToMap()
tmpl.copyListEntryField(additionalDisks, dst, src, "mount")
dest.Mount = disk.Mount
}
if dest.MountPoint == nil && disk.MountPoint != nil {
upgradeDiskToMap()
tmpl.copyListEntryField(additionalDisks, dst, src, "mountPoint")
dest.MountPoint = disk.MountPoint
}
// TODO: Is there a good reason not to copy comments from wildcard entries?
if disk.Name != "*" {
tmpl.copyListEntryComments(additionalDisks, dst, src, "")
+6
View File
@@ -144,6 +144,12 @@ additionalDisks: # comment
"additionalDisks: [{name: disk2},{name: disk1, format: true}]",
"additionalDisks: [{name: disk1, format: true},{name: disk2}]",
},
{
"additionalDisks merge mount and mountPoint on shared name",
"additionalDisks: [{name: disk1}]",
"additionalDisks: [{name: disk2},{name: disk1, mount: false, mountPoint: /data}]",
"additionalDisks: [{name: disk1, mount: false, mountPoint: /data},{name: disk2}]",
},
{
// This test fails because there are 2 spurious newlines in the merged output
"TODO mounts append, but merge fields on shared mountPoint",
+6 -4
View File
@@ -161,10 +161,12 @@ type Image struct {
}
type Disk struct {
Name string `yaml:"name" json:"name"` // REQUIRED
Format *bool `yaml:"format,omitempty" json:"format,omitempty"`
FSType *string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
FSArgs []string `yaml:"fsArgs,omitempty" json:"fsArgs,omitempty"`
Name string `yaml:"name" json:"name"` // REQUIRED
Format *bool `yaml:"format,omitempty" json:"format,omitempty"`
FSType *string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
FSArgs []string `yaml:"fsArgs,omitempty" json:"fsArgs,omitempty"`
Mount *bool `yaml:"mount,omitempty" json:"mount,omitempty"`
MountPoint *string `yaml:"mountPoint,omitempty" json:"mountPoint,omitempty"`
}
type Mount struct {
+15
View File
@@ -121,6 +121,21 @@ func Validate(y *limatype.LimaYAML, warn bool) error {
if err := identifiers.Validate(disk.Name); err != nil {
errs = errors.Join(errs, fmt.Errorf("field `additionalDisks[%d].name is invalid`: %w", i, err))
}
if disk.MountPoint != nil {
// This is a guest path, so use path.IsAbs (forward-slash) rather than the
// host-OS-dependent filepath.IsAbs. There is no tilde-expansion for guest
// filenames, so the mount point must be an absolute path.
if !path.IsAbs(*disk.MountPoint) {
errs = errors.Join(errs, fmt.Errorf("field `additionalDisks[%d].mountPoint` must be an absolute path, got %#q", i, *disk.MountPoint))
}
switch *disk.MountPoint {
case "/", "/bin", "/dev", "/etc", "/home", "/opt", "/sbin", "/tmp", "/usr", "/var":
errs = errors.Join(errs, fmt.Errorf("field `additionalDisks[%d].mountPoint` must not be a critical system path such as /etc or /usr", i))
// home directory defined in "cidata.iso:/user-data"
case *y.User.Home:
errs = errors.Join(errs, fmt.Errorf("field `additionalDisks[%d].mountPoint` is the reserved internal home directory %#q", i, *y.User.Home))
}
}
}
for i, f := range y.Mounts {
+37
View File
@@ -360,6 +360,43 @@ func TestValidateBlockDevicesRejectsOverlappingStorage(t *testing.T) {
}
}
func TestValidateAdditionalDisksMountPoint(t *testing.T) {
images := `images: [{"location": "/"}]`
validMountPoint := `
additionalDisks:
- name: "disk1"
mountPoint: "/data"
- name: "disk2"
mount: false
- name: "disk3"
mountPoint: "/var/lib/data"
`
y, err := Load(t.Context(), []byte(validMountPoint+"\n"+images), "lima.yaml")
assert.NilError(t, err)
assert.NilError(t, Validate(y, false))
for _, tc := range []struct {
mountPoint string
expected string
}{
{"data", "field `additionalDisks[0].mountPoint` must be an absolute path, got `data`"},
{"~/data", "field `additionalDisks[0].mountPoint` must be an absolute path, got `~/data`"},
{"/etc", "field `additionalDisks[0].mountPoint` must not be a critical system path such as /etc or /usr"},
{"/home", "field `additionalDisks[0].mountPoint` must not be a critical system path such as /etc or /usr"},
} {
invalid := "additionalDisks:\n - name: \"disk1\"\n mountPoint: \"" + tc.mountPoint + "\"\n"
y, err = Load(t.Context(), []byte(invalid+"\n"+images), "lima.yaml")
assert.NilError(t, err)
assert.Error(t, Validate(y, false), tc.expected)
}
reservedHome := "user: {home: \"/home/foo.guest\"}\nadditionalDisks:\n - name: \"disk1\"\n mountPoint: \"/home/foo.guest\"\n"
y, err = Load(t.Context(), []byte(reservedHome+"\n"+images), "lima.yaml")
assert.NilError(t, err)
assert.Error(t, Validate(y, false), "field `additionalDisks[0].mountPoint` is the reserved internal home directory `/home/foo.guest`")
}
func TestValidateParamName(t *testing.T) {
images := `images: [{"location": "/"}]`
validProvision := `provision: [{"script": "echo $PARAM_name $PARAM_NAME $PARAM_Name_123"}]`
+9 -2
View File
@@ -122,8 +122,9 @@ mountInotify: null
# Lima disks to attach to the instance. The disks will be accessible from inside the
# instance, labeled by name. (e.g. if the disk is named "data", it will be labeled
# "lima-data" inside the instance). The disk will be mounted inside the instance at
# `/mnt/lima-${VOLUME}`.
# "lima-data" inside the instance). By default the disk is mounted inside the instance
# at `/mnt/lima-${VOLUME}`; use "mountPoint" to change this, or set "mount" to false to
# attach the disk without mounting it (e.g. to manage it yourself inside the guest).
# 🟢 Builtin default: []
additionalDisks:
# disks should either be a list of disk name strings, for example:
@@ -132,6 +133,12 @@ additionalDisks:
# - name: "data"
# format: true
# fsType: "ext4"
# # Whether Lima should auto-mount the disk inside the guest.
# # 🟢 Builtin default: true
# mount: true
# # Absolute path inside the guest to mount the disk at.
# # 🟢 Builtin default: /mnt/lima-${name}
# mountPoint: "/mnt/lima-data"
# Attach host block devices (currently macOS with builtin vmType: vz only).
# The helper accepts disk device paths like /dev/disk4 or /dev/rdisk4s1
+2
View File
@@ -211,6 +211,8 @@ The volume label is "cidata", as defined by [cloud-init NoCloud](https://docs.cl
- `LIMA_CIDATA_DISK_%d_FORMAT`: set to "true" when the N-th additional disk should be formatted on first boot.
- `LIMA_CIDATA_DISK_%d_FSTYPE`: the filesystem type to format the N-th additional disk with (e.g. `ext4`).
- `LIMA_CIDATA_DISK_%d_FSARGS`: extra arguments passed to mkfs for the N-th additional disk (space-separated).
- `LIMA_CIDATA_DISK_%d_MOUNT`: set to "true" when the N-th additional disk should be mounted by the guest (default "true").
- `LIMA_CIDATA_DISK_%d_MOUNTPOINT`: the guest path to mount the N-th additional disk at. Empty means the default `/mnt/lima-<name>`.
- `LIMA_CIDATA_DATAFILE_%08d_OVERWRITE`: set to "true" if the datafile should be overwritten if it already exists.
- `LIMA_CIDATA_DATAFILE_%08d_OWNER`: set to the owner of the datafile.
- `LIMA_CIDATA_DATAFILE_%08d_PATH`: set to the path the datafile should be copied to.