Files
immucore/pkg/mount/mount.go
Itxaka 2aee7ab5a0 Symlink /sysroot/oem to /oem so we can run rootfs stage
Looks like we need to have the oem partition loaded for some cases, and
during rootfs we should pick it up.

Currently we only mount it under /sysroot/oem. IIRC in
cos-immutable-rootfs it was mounted under /oem directly, the rootfs
stage service run and then when cos-layout was launched it made sure to
unmount /oem so it could mount it under /sysroot/oem

In our case its a bit more complicated, we may need to mount it under
/oem before the rootfs run, then unmount it and mount it in the proper
place. Im hoping we can get away with just creating the symlink

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-02-14 09:51:51 +01:00

499 lines
14 KiB
Go

package mount
import (
"context"
"errors"
"fmt"
"github.com/kairos-io/immucore/internal/constants"
"os"
"path/filepath"
"strings"
"time"
"github.com/containerd/containerd/mount"
"github.com/deniswernert/go-fstab"
"github.com/hashicorp/go-multierror"
internalUtils "github.com/kairos-io/immucore/internal/utils"
"github.com/kairos-io/kairos/pkg/utils"
"github.com/kairos-io/kairos/sdk/state"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spectrocloud-labs/herd"
)
type State struct {
Logger zerolog.Logger
Rootdir string // e.g. /sysroot inside initrd with pivot, / with nopivot
TargetImage string // e.g. /cOS/active.img
TargetLabel string // e.g. COS_ACTIVE
// /run/cos-layout.env (different!)
OverlayDirs []string // e.g. /var
BindMounts []string // e.g. /etc/kubernetes
CustomMounts map[string]string // e.g. diskid : mountpoint
StateDir string // e.g. "/usr/local/.state"
MountRoot bool // e.g. if true, it tries to find the image to loopback mount
fstabs []*fstab.Mount
}
const (
opCustomMounts = "custom-mount"
opDiscoverState = "discover-state"
opMountState = "mount-state"
opMountBind = "mount-bind"
opMountRoot = "mount-root"
opOverlayMount = "overlay-mount"
opWriteFstab = "write-fstab"
opMountBaseOverlay = "mount-base-overlay"
opMountOEM = "mount-oem"
opRootfsHook = "rootfs-hook"
opLoadConfig = "load-config"
)
func (s *State) path(p ...string) string {
return filepath.Join(append([]string{s.Rootdir}, p...)...)
}
func (s *State) WriteFstab(fstabFile string) func(context.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Logger()
return func(ctx context.Context) error {
for _, fst := range s.fstabs {
select {
case <-ctx.Done():
default:
f, err := os.OpenFile(fstabFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
if _, err := f.WriteString(fmt.Sprintf("%s\n", fst.String())); err != nil {
_ = f.Close()
return err
}
_ = f.Close()
}
}
return nil
}
}
func (s *State) RunStageOp(stage string) func(context.Context) error {
return func(ctx context.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Logger()
if stage == "rootfs" {
if _, err := os.Stat("/system"); os.IsNotExist(err) {
err = os.Symlink("/sysroot/system", "/system")
if err != nil {
s.Logger.Err(err).Msg("creating symlink")
return err
}
}
if _, err := os.Stat("/oem"); os.IsNotExist(err) {
err = os.Symlink("/sysroot/oem", "/oem")
if err != nil {
s.Logger.Err(err).Msg("creating symlink")
return err
}
}
}
cmd := fmt.Sprintf("elemental run-stage %s", stage)
// If we set the level to debug, also call elemental with debug
if log.Logger.GetLevel() == zerolog.DebugLevel {
cmd = fmt.Sprintf("%s --debug", cmd)
}
output, err := utils.SH(cmd)
log.Debug().Msg(output)
return err
}
}
func (s *State) MountOP(what, where, t string, options []string, timeout time.Duration) func(context.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Str("what", what).Str("where", where).Str("type", t).Strs("options", options).Logger()
return func(c context.Context) error {
cc := time.After(timeout)
for {
select {
default:
err := internalUtils.CreateIfNotExists(where)
if err != nil {
log.Logger.Err(err).Msg("Creating dir")
continue
}
time.Sleep(1 * time.Second)
mountPoint := mount.Mount{
Type: t,
Source: what,
Options: options,
}
tmpFstab := internalUtils.MountToFstab(mountPoint)
tmpFstab.File = internalUtils.CleanSysrootForFstab(where)
op := mountOperation{
MountOption: mountPoint,
FstabEntry: *tmpFstab,
Target: where,
}
err = op.run()
if err == nil {
s.fstabs = append(s.fstabs, tmpFstab)
}
// only continue the loop if it's an error and not an already mounted error
if err != nil && !errors.Is(err, constants.ErrAlreadyMounted) {
continue
}
return nil
case <-c.Done():
e := fmt.Errorf("context canceled")
log.Logger.Err(e).Msg("mount canceled")
return e
case <-cc:
e := fmt.Errorf("timeout exhausted")
log.Logger.Err(e).Msg("Mount timeout")
return e
}
}
}
}
func (s *State) WriteDAG(g *herd.Graph) (out string) {
for i, layer := range g.Analyze() {
out += fmt.Sprintf("%d.\n", i+1)
for _, op := range layer {
if op.Error != nil {
out += fmt.Sprintf(" <%s> (error: %s) (background: %t) (weak: %t)\n", op.Name, op.Error.Error(), op.Background, op.WeakDeps)
} else {
out += fmt.Sprintf(" <%s> (background: %t) (weak: %t)\n", op.Name, op.Background, op.WeakDeps)
}
}
}
return
}
func (s *State) Register(g *herd.Graph) error {
var err error
runtime, err := state.NewRuntime()
if err != nil {
s.Logger.Debug().Err(err).Msg("runtime")
return err
}
s.Logger.Debug().Interface("runtime", runtime).Msg("Current runtime")
// TODO: add hooks, fstab (might have missed some), systemd compat
// TODO: We should also set tmpfs here (not -related)
// All of this below need to run after rootfs stage runs (so the layout file is created)
// This is legacy - in UKI we don't need to found the img, this needs to run in a conditional
if s.MountRoot {
// setup loopback mount for the image target for booting
err = g.Add(opDiscoverState,
herd.WithDeps(opMountState),
herd.WithCallback(
func(ctx context.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Logger()
// Check if loop device is mounted by checking the existance of the target label
if internalUtils.IsMountedByLabel(s.TargetLabel) {
log.Logger.Debug().Str("targetImage", s.TargetImage).Str("path", s.Rootdir).Str("TargetLabel", s.TargetLabel).Msg("Not mounting loop, already mounted")
return nil
}
// TODO: squashfs recovery image?
cmd := fmt.Sprintf("losetup --show -f %s", s.path("/run/initramfs/cos-state", s.TargetImage))
_, err := utils.SH(cmd)
if err != nil {
log.Logger.Debug().Err(err).Msg("")
}
return err
},
))
if err != nil {
s.Logger.Err(err).Send()
}
// mount the state partition so to find the loopback device
stateName := runtime.State.Name
stateFs := runtime.State.Type
// Recovery is a different partition
if internalUtils.IsRecovery() {
stateName = runtime.Recovery.Name
stateFs = runtime.Recovery.Type
}
err = g.Add(opMountState,
herd.WithCallback(
s.MountOP(
stateName,
s.path("/run/initramfs/cos-state"),
stateFs,
[]string{
"ro", // or rw
}, 60*time.Second),
),
)
if err != nil {
s.Logger.Err(err).Send()
}
// mount the loopback device as root of the fs
err = g.Add(opMountRoot,
herd.WithDeps(opDiscoverState),
herd.WithCallback(
s.MountOP(
fmt.Sprintf("/dev/disk/by-label/%s", s.TargetLabel),
s.Rootdir,
"ext4", // are images always ext2?
[]string{
"ro", // or rw
"suid",
"dev",
"exec",
// "auto",
//"nouser",
"async",
}, 10*time.Second),
),
)
if err != nil {
s.Logger.Err(err).Send()
}
}
// depending on /run/cos-layout.env
// This is building the mountRoot dependendency if it was enabled
mountRootCondition := herd.ConditionalOption(func() bool { return s.MountRoot }, herd.WithDeps(opMountRoot))
// this needs to be run after sysroot, so we can link to /sysroot/system/oem and after /oem mounted
err = g.Add(opRootfsHook, mountRootCondition, herd.WithDeps(opMountRoot, opMountOEM), herd.WithCallback(s.RunStageOp("rootfs")))
if err != nil {
s.Logger.Err(err).Msg("running rootfs stage")
}
// /run/cos/cos-layout.env
// populate state bindmounts, overlaymounts, custommounts
err = g.Add(opLoadConfig,
herd.WithDeps(opRootfsHook),
herd.WithCallback(func(ctx context.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Logger()
if s.CustomMounts == nil {
s.CustomMounts = map[string]string{}
}
env, err := internalUtils.ReadEnv("/run/cos/cos-layout.env")
if err != nil {
log.Logger.Err(err).Msg("Reading env")
return err
}
// populate from env here
s.OverlayDirs = internalUtils.CleanupSlice(strings.Split(env["RW_PATHS"], " "))
// Append default RW_Paths if Dirs are empty
if len(s.OverlayDirs) == 0 {
s.OverlayDirs = constants.DefaultRWPaths()
}
// Remove any duplicates
s.OverlayDirs = internalUtils.UniqueSlice(s.OverlayDirs)
s.BindMounts = internalUtils.CleanupSlice(strings.Split(env["PERSISTENT_STATE_PATHS"], " "))
// Remove any duplicates
s.BindMounts = internalUtils.UniqueSlice(s.BindMounts)
s.StateDir = env["PERSISTENT_STATE_TARGET"]
if s.StateDir == "" {
s.StateDir = constants.PersistentStateTarget
}
addLine := func(d string) {
dat := strings.Split(d, ":")
if len(dat) == 2 {
disk := dat[0]
path := dat[1]
s.CustomMounts[disk] = path
}
}
// Parse custom mounts also from cmdline (rd.cos.mount=)
// Parse custom mounts also from env file (VOLUMES)
for _, v := range append(internalUtils.ReadCMDLineArg("rd.cos.mount="), strings.Split(env["VOLUMES"], " ")...) {
addLine(internalUtils.ParseMount(v))
}
return nil
}))
if err != nil {
s.Logger.Err(err).Send()
}
// end sysroot mount
// overlay mount start
if internalUtils.DiskFSType(s.Rootdir) != "overlay" {
err = g.Add(opMountBaseOverlay,
herd.WithCallback(
func(ctx context.Context) error {
op, err := baseOverlay(Overlay{
Base: "/run/overlay",
BackingBase: "tmpfs:20%",
})
if err != nil {
return err
}
err2 := op.run()
// No error, add fstab
if err2 == nil {
s.fstabs = append(s.fstabs, &op.FstabEntry)
return nil
}
// Error but its already mounted error, dont add fstab but dont return error
if err2 != nil && errors.Is(err2, constants.ErrAlreadyMounted) {
return nil
}
return err2
},
),
)
if err != nil {
s.Logger.Err(err).Send()
}
}
overlayCondition := herd.ConditionalOption(func() bool { return internalUtils.DiskFSType(s.Rootdir) != "overlay" }, herd.WithDeps(opMountBaseOverlay))
// TODO: Add fsck
// mount overlay
err = g.Add(
opOverlayMount,
overlayCondition,
herd.WithDeps(opLoadConfig),
mountRootCondition,
herd.WithCallback(
func(ctx context.Context) error {
var multierr *multierror.Error
s.Logger.Debug().Strs("dirs", s.OverlayDirs).Msg("Mounting overlays")
for _, p := range s.OverlayDirs {
op := mountWithBaseOverlay(p, s.Rootdir, "/run/overlay")
err := op.run()
// Append to errors only if it's not an already mounted error
if err != nil && !errors.Is(err, constants.ErrAlreadyMounted) {
log.Logger.Err(err).Msg("overlay mount")
multierr = multierror.Append(multierr, err)
continue
}
s.fstabs = append(s.fstabs, &op.FstabEntry)
}
return multierr.ErrorOrNil()
},
),
)
if err != nil {
s.Logger.Err(err).Send()
}
err = g.Add(
opCustomMounts,
mountRootCondition,
overlayCondition,
herd.WithDeps(opLoadConfig),
herd.WithCallback(func(ctx context.Context) error {
var err *multierror.Error
for what, where := range s.CustomMounts {
// TODO: scan for the custom mount disk to know the underlying fs and set it proper
fstype := "ext4"
mountOptions := []string{"ro"}
// Translate label to disk for COS_PERSISTENT
// Persistent needs to be RW
if strings.Contains(what, "COS_PERSISTENT") {
fstype = runtime.Persistent.Type
mountOptions = []string{"rw"}
}
err = multierror.Append(err, s.MountOP(
what,
s.path(where),
fstype,
mountOptions,
10*time.Second,
)(ctx))
}
s.Logger.Err(err.ErrorOrNil()).Send()
return err.ErrorOrNil()
}),
)
if err != nil {
s.Logger.Err(err).Send()
}
// mount state is defined over a custom mount (/usr/local/.state for instance, needs to be mounted over a device)
err = g.Add(
opMountBind,
overlayCondition,
mountRootCondition,
herd.WithDeps(opCustomMounts, opLoadConfig),
herd.WithCallback(
func(ctx context.Context) error {
var err *multierror.Error
s.Logger.Debug().Strs("mounts", s.BindMounts).Msg("Mounting binds")
for _, p := range s.BindMounts {
op := mountBind(p, s.Rootdir, s.StateDir)
err2 := op.run()
if err2 == nil {
// Only append to fstabs if there was no error, otherwise we will try to mount it after switch_root
s.fstabs = append(s.fstabs, &op.FstabEntry)
}
// Append to errors only if it's not an already mounted error
if err2 != nil && !errors.Is(err2, constants.ErrAlreadyMounted) {
log.Logger.Err(err2).Send()
err = multierror.Append(err, err2)
}
}
log.Logger.Err(err.ErrorOrNil()).Send()
return err.ErrorOrNil()
},
),
)
if err != nil {
s.Logger.Err(err).Send()
}
// overlay mount end
err = g.Add(opMountOEM,
overlayCondition,
mountRootCondition,
herd.WithCallback(
s.MountOP(
fmt.Sprintf("/dev/disk/by-label/%s", runtime.OEM.Label),
s.path("/oem"),
runtime.OEM.Type,
[]string{
"rw",
"suid",
"dev",
"exec",
//"noauto",
//"nouser",
"async",
}, 10*time.Second),
),
)
if err != nil {
s.Logger.Err(err).Send()
}
err = g.Add(opWriteFstab,
overlayCondition,
mountRootCondition,
herd.WithDeps(opMountOEM, opCustomMounts, opMountBind, opOverlayMount),
herd.WeakDeps,
herd.WithCallback(s.WriteFstab(s.path("/etc/fstab"))))
if err != nil {
s.Logger.Err(err).Send()
}
return err
}