Skip to content

Commit

Permalink
feat(init): generate filesystems if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
water-sucks committed Feb 17, 2025
1 parent 43602c5 commit 76b3c6a
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 10 deletions.
235 changes: 232 additions & 3 deletions cmd/init/filesystems.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"syscall"

Expand All @@ -14,10 +15,22 @@ import (
)

const (
swapDeviceListFilename = "/proc/swaps"
swapDeviceListFilename = "/proc/swaps"
mountedFilesystemListFilename = "/proc/self/mountinfo"
)

type SwapDevice struct{}
type Filesystem struct {
Mountpoint string
DevicePath string
FSType string
Options []string
LUKSInformation *LUKSInformation
}

type LUKSInformation struct {
Name string
DevicePath string
}

func findSwapDevices(log *logger.Logger) []string {
swapDevices := []string{}
Expand Down Expand Up @@ -51,6 +64,158 @@ func findSwapDevices(log *logger.Logger) []string {
return swapDevices
}

func findFilesystems(log *logger.Logger, rootDir string) []Filesystem {
filesystems := []Filesystem{}

foundFileystems := make(map[string]string, 0)
foundLuksDevices := make(map[string]struct{}, 0)

mountList, err := os.Open(mountedFilesystemListFilename)
if err != nil {
log.Warnf("failed to open swap device list %v: %v", mountedFilesystemListFilename, err)
return filesystems
}
defer mountList.Close()

s := bufio.NewScanner(mountList)
s.Split(bufio.ScanLines)

for s.Scan() {
fields := strings.Fields(s.Text())

mountID := fields[2]
path := fields[3]
if path == "/" {
path = ""
}

absoluteMountpoint := strings.ReplaceAll(fields[4], "\\040", "")

if stat, err := os.Stat(absoluteMountpoint); err != nil || !stat.IsDir() {
continue
}

if !isSubdir(absoluteMountpoint, rootDir) {
continue
}

var mountpoint string
if absoluteMountpoint == rootDir {
mountpoint = "/"
} else {
mountpoint = absoluteMountpoint[len(rootDir):]
}

mountOptions := strings.Split(fields[5], ",")

if isSubdir(mountpoint, "/proc") || isSubdir(mountpoint, "/sys") || isSubdir(mountpoint, "/dev") || isSubdir(mountpoint, "/run") {
continue
} else if mountpoint == "/var/lib/nfs/rpc_pipefs" {
continue
}

// Skip irrelevant fields
n := 6
for ; n < len(fields); n++ {
if fields[n] == "-" {
n++
break
}
}

// Sanity check. If the mount entry is malformed, we should not attempt
// to access the rest of the fields, lest we risk an OOB.
if n > len(fields)-3 {
log.Warnf("malformed mount entry: %v", s.Text())
continue
}

fsType := fields[n]

devicePath := fields[n+1]
devicePath = strings.ReplaceAll(devicePath, "\\040", "")
devicePath = strings.ReplaceAll(devicePath, "\\011", "\t")

superblockOptions := strings.Split(fields[n+2], ",")

// Skip read-only Nix store bind mount
if mountpoint == "/nix/store" && slices.Contains(superblockOptions, "rw") && slices.Contains(mountOptions, "ro") {
continue
}

if fsType == "fuse" || fsType == "fuseblk" {
log.Warnf("don't know how to emit `fileSystem` option for FUSE filesystem '%v'", mountpoint)
continue
}

if mountpoint == "/tmp" && fsType == "tmpfs" {
continue
}

if existingFsPath, ok := foundFileystems[mountID]; ok {
// TODO: check if filesystem is a btrfs subvolume

filesystems = append(filesystems, Filesystem{
Mountpoint: mountpoint,
DevicePath: filepath.Join(existingFsPath, path),
FSType: fsType,
Options: []string{"bind"},
})

continue
}

foundFileystems[mountID] = path

extraOptions := []string{}

if strings.HasPrefix(devicePath, "/dev/loop") {
startIndex := len("/dev/loop")
endIndex := strings.Index(devicePath[startIndex:], "/")
if endIndex == -1 {
endIndex = len(devicePath)
}
loopNumber := devicePath[startIndex:endIndex]

backerFilename := fmt.Sprintf("/sys/block/loop%s/loop/backing_file", loopNumber)

if backer, err := os.ReadFile(backerFilename); err == nil {
devicePath = string(backer)
extraOptions = append(extraOptions, "loop")
}
}

// Preserve umask for FAT filesystems in order to preserve
// EFI system partition security.
if fsType == "vfat" {
for _, o := range superblockOptions {
if o == "fmask" || o == "dmask" {
extraOptions = append(extraOptions, o)
}
}
}

// TODO: check if filesystem is a btrfs subvolume

// TODO: check if Stratis pool

filesystemToAdd := Filesystem{
Mountpoint: mountpoint,
DevicePath: findStableDevPath(devicePath),
FSType: fsType,
Options: extraOptions,
}

deviceName := filepath.Base(devicePath)
filesystemToAdd.LUKSInformation = queryLUKSInformation(deviceName, foundLuksDevices)

filesystems = append(filesystems, filesystemToAdd)

}

return filesystems
}

func lvmDevicesExist(s system.CommandRunner, log *logger.Logger) bool {
cmd := system.NewCommand("lsblk", "-o", "TYPE")

Expand Down Expand Up @@ -136,7 +301,6 @@ func checkDirForEqualDevice(deviceRdev uint64, dirname string) (string, bool) {

for _, entry := range entries {
devicePath := filepath.Join(dirname, entry.Name())
fmt.Println("checking device path", devicePath)
rdev, err := getRdev(devicePath)
if err != nil {
continue
Expand All @@ -149,3 +313,68 @@ func checkDirForEqualDevice(deviceRdev uint64, dirname string) (string, bool) {

return "", false
}

func isSubdir(subdir string, dir string) bool {
if len(dir) == 0 || dir == "/" {
return true
}

if dir == subdir {
return true
}

if len(subdir) <= len(dir)+1 {
return false
}

return strings.Index(subdir, dir) == 0 && subdir[len(dir)] == '/'
}

func queryLUKSInformation(deviceName string, foundLuksDevices map[string]struct{}) *LUKSInformation {
// Check if the device in question is a LUKS device.
uuidFilename := fmt.Sprintf("/sys/class/block/%s/dm/uuid", deviceName)
uuidFileContents, err := os.ReadFile(uuidFilename)
if err != nil {
return nil
}
if !strings.HasPrefix(string(uuidFileContents), "CRYPT_LUKS") {
return nil
}

// Then, make sure it has a single slave device. These are the only types of
// supported LUKS devices for filesystem generation.
slaveDeviceDirname := fmt.Sprintf("/sys/class/block/%s/slaves", deviceName)
slaveDeviceEntries, err := os.ReadDir(slaveDeviceDirname)
if err != nil {
return nil
}

if len(slaveDeviceEntries) != 1 {
return nil
}

// Get the real name of the device that LUKS is using, and attempt to find
// a stable device path for it.
slaveName := slaveDeviceEntries[0].Name()
slaveDeviceName := filepath.Join("/dev", slaveName)
dmNameFilename := fmt.Sprintf("/sys/class/block/%s/dm/name", slaveDeviceName)

dmNameFileContents, err := os.ReadFile(dmNameFilename)
if err != nil {
return nil
}
dmName := strings.TrimSpace(string(dmNameFileContents))

realDevicePath := findStableDevPath(dmName)

// Check if the device has already been found.
if _, ok := foundLuksDevices[dmName]; ok {
return nil
}
foundLuksDevices[dmName] = struct{}{}

return &LUKSInformation{
Name: dmName,
DevicePath: realDevicePath,
}
}
78 changes: 73 additions & 5 deletions cmd/init/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
_ "embed"
"fmt"
"os"
"path/filepath"
"strings"

buildOpts "github.com/water-sucks/nixos/internal/build"
cmdTypes "github.com/water-sucks/nixos/internal/cmd/types"
"github.com/water-sucks/nixos/internal/config"
"github.com/water-sucks/nixos/internal/logger"
"github.com/water-sucks/nixos/internal/system"
Expand All @@ -22,7 +24,7 @@ var configurationNixTemplate string
//go:embed flake.nix.txt
var flakeNixTemplate string

func generateHwConfigNix(s system.CommandRunner, log *logger.Logger, cfg *config.Config, virtType VirtualisationType, scanFilesystems bool) (string, error) {
func generateHwConfigNix(s system.CommandRunner, log *logger.Logger, cfg *config.Config, virtType VirtualisationType, opts *cmdTypes.InitOpts) (string, error) {
imports := []string{}
initrdAvailableModules := []string{}
initrdModules := []string{}
Expand All @@ -42,7 +44,6 @@ func generateHwConfigNix(s system.CommandRunner, log *logger.Logger, cfg *config
ModulePackages: &modulePackages,
Attrs: &extraAttrs,
}
_ = hwConfigSettings

if cfg.Init.ExtraAttrs != nil {
for k, v := range cfg.Init.ExtraAttrs {
Expand Down Expand Up @@ -127,21 +128,41 @@ func generateHwConfigNix(s system.CommandRunner, log *logger.Logger, cfg *config
%v
];`, strings.Join(swapDeviceStrings, "\n "))

// TODO: find filesystems

extraAttrLines := make([]string, len(extraAttrs))
for i, attr := range extraAttrs {
extraAttrLines[i] = fmt.Sprintf(" %v = %v;", attr.Key, attr.Value)
}

rootDirectory, err := filepath.EvalSymlinks(opts.Root)
if err != nil {
log.Errorf("failed to resolve root directory: %v", err)
return "", err
}
if rootDirectory == "/" {
rootDirectory = ""
}

var filesystems []Filesystem
if opts.NoFSGeneration {
filesystems = []Filesystem{}
} else {
filesystems = findFilesystems(log, rootDirectory)
}

fsStrB := strings.Builder{}
for _, fs := range filesystems {
_, _ = fsStrB.WriteString(generateFilesystemAttrset(&fs))
_, _ = fsStrB.WriteString("\n")
}

return fmt.Sprintf(
hardwareConfigurationNixTemplate,
strings.Join(imports, "\n "),
nixStringList(initrdAvailableModules),
nixStringList(initrdModules),
nixStringList(kernelModules),
strings.Join(modulePackages, " "),
"", // TODO: filesystems
fsStrB.String(),
swapDevicesStr,
strings.Join(networkInterfaceLines, "\n")+"\n",
strings.Join(extraAttrLines, "\n"),
Expand Down Expand Up @@ -257,3 +278,50 @@ func nixStringList(s []string) string {

return strings.Join(quotedItems, " ")
}

const (
fileSystemEntryKeyTemplate = ` fileSystems."%s" = {` + "\n"
fileSystemDeviceTemplate = ` device = "%s";` + "\n"
fileSystemTypeTemplate = ` type = "%s";` + "\n"
fileSystemOptionTemplate = ` options = [%s];` + "\n"

fileSystemLuksTemplate = ` boot.initrd.luks.devices."%s".device = "%s";` + "\n\n"
)

func generateFilesystemAttrset(filesystem *Filesystem) string {
fsStr := strings.Builder{}

_, _ = fsStr.WriteString(fmt.Sprintf(fileSystemEntryKeyTemplate, filesystem.Mountpoint))
_, _ = fsStr.WriteString(fmt.Sprintf(fileSystemDeviceTemplate, filesystem.DevicePath))
_, _ = fsStr.WriteString(fmt.Sprintf(fileSystemTypeTemplate, filesystem.FSType))

if len(filesystem.Options) > 0 {
optionStr := fmt.Sprintf(fileSystemOptionTemplate, nixStringList(uniqueStringsInSlice(filesystem.Options)))
_, _ = fsStr.WriteString(optionStr)
}

fsStr.WriteString(" }\n")

luks := filesystem.LUKSInformation
if luks != nil {
_, _ = fsStr.WriteString(fmt.Sprintf(fileSystemLuksTemplate, luks.Name, luks.DevicePath))
}

return fsStr.String()
}

func uniqueStringsInSlice(s []string) []string {
visited := make(map[string]bool, len(s))

result := make([]string, 0, len(s))

for _, v := range s {
_, ok := visited[v]
if !ok {
visited[v] = true
result = append(result, v)
}
}

return result
}
Loading

0 comments on commit 76b3c6a

Please sign in to comment.