mkfs.ext4 zeroes the entire inode table and journal at format time unless told otherwise. On an 8 GiB work disk that's roughly 500-700ms of host CPU/IO per 'banger vm create', for a one-time small per-write penalty inside the guest the first time it touches an unwritten inode that nobody can perceive. Centralise the canonical mkfs -E option list as system.MkfsExtraOptions and use it everywhere banger calls mkfs.ext4 on a VM-internal image: the no-seed work disk, MaterializeWorkDisk, BuildWorkSeedImage, and the imagepull rootfs builder. The work-disk paths feed vm create directly; the others are one-off but still benefit from the faster format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
package imagepull
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"banger/internal/system"
|
|
)
|
|
|
|
// MinExt4Size is the smallest ext4 image we'll create. mkfs.ext4 needs a
|
|
// few megabytes for its bookkeeping; for a real rootfs the staging tree
|
|
// will dominate anyway.
|
|
const MinExt4Size int64 = 1 << 20 * 64 // 64 MiB
|
|
|
|
// BuildExt4 creates outFile as a sparse ext4 image of sizeBytes and
|
|
// populates it from srcDir using `mkfs.ext4 -F -d`. No mount, no sudo.
|
|
//
|
|
// sizeBytes must be at least MinExt4Size. Callers size the file with
|
|
// headroom over the staged tree (the daemon orchestrator does this;
|
|
// this function only enforces a sanity floor).
|
|
//
|
|
// The filesystem itself is root-owned via `-E root_owner=0:0`, but
|
|
// the per-file uid/gid/mode inside srcDir are the runner's — Go's
|
|
// unprivileged tar extraction can't preserve them. The pipeline's
|
|
// next step, ApplyOwnership, restores the tar-header values.
|
|
func BuildExt4(ctx context.Context, runner system.CommandRunner, srcDir, outFile string, sizeBytes int64) error {
|
|
if sizeBytes < MinExt4Size {
|
|
return fmt.Errorf("ext4 size %d below minimum %d", sizeBytes, MinExt4Size)
|
|
}
|
|
info, err := os.Stat(srcDir)
|
|
if err != nil {
|
|
return fmt.Errorf("stat source: %w", err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("%s is not a directory", srcDir)
|
|
}
|
|
|
|
if err := os.Remove(outFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := f.Truncate(sizeBytes); err != nil {
|
|
_ = f.Close()
|
|
_ = os.Remove(outFile)
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
_ = os.Remove(outFile)
|
|
return err
|
|
}
|
|
|
|
// mkfs.ext4's positional `fs-size` is documented in 1 KiB units
|
|
// (NOT the filesystem's 4 KiB block size), so dividing by 4096
|
|
// produces a filesystem 1/4 the intended size. Omit the positional
|
|
// entirely — the file was truncated to sizeBytes above, and mkfs
|
|
// with no fs-size arg uses the whole device.
|
|
out, runErr := runner.Run(ctx, "mkfs.ext4",
|
|
"-F",
|
|
"-q",
|
|
"-d", srcDir,
|
|
"-L", "banger-rootfs",
|
|
"-E", system.MkfsExtraOptions,
|
|
outFile,
|
|
)
|
|
if runErr != nil {
|
|
_ = os.Remove(outFile)
|
|
return fmt.Errorf("mkfs.ext4 -d: %w: %s", runErr, string(out))
|
|
}
|
|
return nil
|
|
}
|