immucore/internal/utils/mounts.go
Itxaka aa5939da89
Rework workflow (#77)
This makes immucore run more in parallel rather than block everything
else. We just tell sysroot.mount that eventually it will be mounted and
to wait for a bit.

This allows us to be more flexible where to run and run in parallel in
cases like cdrom in which we may do things but we need the sysroot to be
mounted already but not from us.

Also adds the initramfs stage directly in immucore and merges all the dracut config into one

    Dont create sysroot, just add a timeout override so it waits for us
    Dont block on the service, just make sure to finish before
    initrd.target
    Fix mounts from cmdline
    More proper log
    Store logs under the /run/immucore dir
    Store rootfs and initramfs logs separated
    Do not log the full stages in INFO level
    Run initramfs stage in immucore directly on boot and cd/netboot
    Drop systemd requirement from dracut module

Signed-off-by: Itxaka itxaka.garcia@spectrocloud.com
2023-03-08 11:45:11 +01:00

264 lines
7.3 KiB
Go

package utils
import (
"fmt"
"os"
"strconv"
"strings"
"syscall"
"github.com/containerd/containerd/mount"
"github.com/deniswernert/go-fstab"
"github.com/kairos-io/kairos/sdk/state"
)
// https://github.com/kairos-io/packages/blob/7c3581a8ba6371e5ce10c3a98bae54fde6a505af/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L58
// ParseMount will return a proper full disk path based on UUID or LABEL given
// input: LABEL=FOO:/mount
// output: /dev/disk...:/mount .
func ParseMount(s string) string {
switch {
case strings.Contains(s, "UUID="):
dat := strings.Split(s, "UUID=")
return fmt.Sprintf("/dev/disk/by-uuid/%s", dat[1])
case strings.Contains(s, "LABEL="):
dat := strings.Split(s, "LABEL=")
return fmt.Sprintf("/dev/disk/by-label/%s", dat[1])
default:
return s
}
}
// ReadCMDLineArg will return the pair of arg=value for a given arg if it was passed on the cmdline
// TODO: Split this into GetBool and GetValue to return decent defaults.
func ReadCMDLineArg(arg string) []string {
cmdLine, err := os.ReadFile(GetHostProcCmdline())
if err != nil {
return []string{}
}
res := []string{}
fields := strings.Fields(string(cmdLine))
for _, f := range fields {
if strings.HasPrefix(f, arg) {
dat := strings.Split(f, arg)
// For stanzas that have no value, we should return something better than an empty value
// Otherwise anything can easily clean the value
if dat[1] == "" {
res = append(res, "")
} else {
res = append(res, dat[1])
}
}
}
return res
}
// IsMounted lets us know if the given device is currently mounted.
func IsMounted(dev string) bool {
_, err := CommandWithPath(fmt.Sprintf("findmnt %s", dev))
return err == nil
}
// DiskFSType will return the FS type for a given disk
// Does NOT need to be mounted
// Needs full path so either /dev/sda1 or /dev/disk/by-{label,uuid}/{label,uuid} .
func DiskFSType(s string) string {
out, e := CommandWithPath(fmt.Sprintf("blkid %s -s TYPE -o value", s))
if e != nil {
Log.Err(e).Msg("blkid")
}
out = strings.Trim(strings.Trim(out, " "), "\n")
Log.Debug().Str("what", s).Str("type", out).Msg("Partition FS type")
return out
}
// SyncState will rsync source into destination. Useful for Bind mounts.
func SyncState(src, dst string) error {
_, err := CommandWithPath(fmt.Sprintf("rsync -aqAX %s %s", src, dst))
return err
}
// AppendSlash it's in the name. Appends a slash.
func AppendSlash(path string) string {
if !strings.HasSuffix(path, "/") {
return fmt.Sprintf("%s/", path)
}
return path
}
// MountToFstab transforms a mount.Mount into a fstab.Mount so we can transform existing mounts into the fstab format.
func MountToFstab(m mount.Mount) *fstab.Mount {
opts := map[string]string{}
for _, o := range m.Options {
if strings.Contains(o, "=") {
dat := strings.Split(o, "=")
key := dat[0]
value := dat[1]
opts[key] = value
} else {
opts[o] = ""
}
}
return &fstab.Mount{
Spec: m.Source,
VfsType: m.Type,
MntOps: opts,
Freq: 0,
PassNo: 0,
}
}
// CleanSysrootForFstab will clean up the pesky sysroot dir from entries to make them
// suitable to be written in the fstab
// As we mount on /sysroot during initramfs but the fstab file is for the real init process, we need to remove
// Any mentions to /sysroot from the fstab lines, otherwise they won't work
// Special care for the root (/sysroot) path as we can't just simple remove that path and call it a day
// as that will return an empty mountpoint which will break fstab mounting.
func CleanSysrootForFstab(path string) string {
if IsUKI() {
return path
}
cleaned := strings.ReplaceAll(path, "/sysroot", "")
if cleaned == "" {
cleaned = "/"
}
return cleaned
}
// Fsck will run fsck over the device
// options are set on cmdline, but they are for systemd-fsck,
// so we need to interpret ourselves.
func Fsck(device string) error {
if device == "tmpfs" {
return nil
}
mode := CleanupSlice(ReadCMDLineArg("fsck.mode="))
repair := CleanupSlice(ReadCMDLineArg("fsck.repair="))
// Be safe with defaults
if len(mode) == 0 {
mode = []string{"auto"}
}
if len(repair) == 0 {
repair = []string{"preen"}
}
args := []string{"fsck", device}
// Check the mode
// skip means just skip the fsck
// force means force even if fs is deemed clean
// auto or others means normal fsck call
switch mode[0] {
case "skip":
return nil
case "force":
args = append(args, "-f")
}
// Check repair type
// preen means try to fix automatically
// yes means say yes to everything (potentially destructive)
// no means say no to everything
switch repair[0] {
case "preen":
args = append(args, "-a")
case "yes":
args = append(args, "-y")
case "no":
args = append(args, "-n")
}
cmd := strings.Join(args, " ")
Log.Debug().Str("cmd", cmd).Msg("fsck command")
out, e := CommandWithPath(cmd)
if e != nil {
Log.Debug().Err(e).Str("out", out).Str("what", device).Msg("fsck")
}
return e
}
// MountProc will mount /proc
// For now proc is needed to read the cmdline fully in uki mode
// in normal modes this should already be done by the initramfs process, so we can skip this.
func MountProc() {
_ = os.MkdirAll("/proc", 0755)
if !IsMounted("/proc") {
_ = syscall.Mount("proc", "/proc", "proc", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, "")
}
}
// GetOemTimeout parses the cmdline to get the oem timeout to use. Defaults to 5 (converted into seconds afterwards).
func GetOemTimeout() int {
var time []string
// Pick both stanzas until we deprecate the cos ones
timeCos := CleanupSlice(ReadCMDLineArg("rd.cos.oemtimeout="))
timeImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.oemtimeout="))
if len(timeCos) != 0 {
time = timeCos
}
if len(timeImmucore) != 0 {
time = timeImmucore
}
if len(time) == 0 {
return 5
}
converted, err := strconv.Atoi(time[0])
if err != nil {
return 5
}
return converted
}
// GetOverlayBase parses the cdmline and gets the overlay config
// Format is rd.cos.overlay=tmpfs:20% or rd.cos.overlay=LABEL=$LABEL or rd.cos.overlay=UUID=$UUID
// Notice that this can be later override by the config coming from cos-layout.env .
func GetOverlayBase() string {
var overlayConfig []string
// Pick both stanzas until we deprecate the cos ones
// Clean up the slice in case the values are empty
overlayConfigCos := CleanupSlice(ReadCMDLineArg("rd.cos.overlay="))
overlayConfigImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.overlay="))
if len(overlayConfigCos) != 0 {
overlayConfig = overlayConfigCos
}
if len(overlayConfigImmucore) != 0 {
overlayConfig = overlayConfigImmucore
}
if len(overlayConfig) == 0 {
return "tmpfs:20%"
}
return overlayConfig[0]
}
// GetOemLabel will ge the oem label to mount, first from the cmdline and if that fails, from the runtime
// This way users can override the oem label.
func GetOemLabel() string {
var oemLabel string
// Pick both stanzas until we deprecate the cos ones
oemLabelCos := CleanupSlice(ReadCMDLineArg("rd.cos.oemlabel="))
oemLabelImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.oemlabel="))
if len(oemLabelCos) != 0 {
oemLabel = oemLabelCos[0]
}
if len(oemLabelImmucore) != 0 {
oemLabel = oemLabelImmucore[0]
}
if oemLabel != "" {
return oemLabel
}
// We could not get it from the cmdline so get it from the runtime
runtime, err := state.NewRuntime()
if err != nil {
Log.Debug().Err(err).Msg("runtime")
}
return runtime.OEM.Label
}