diff --git a/cmd/config/config.go b/cmd/config/config.go index d364871b67c..9d483f337d4 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -253,6 +253,24 @@ func ReadInitSpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.InitSpec, error) { return init, err } +func ReadMountSpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.MountSpec, error) { + mount := config.NewMountSpec() + vp := viper.Sub("mount") + if vp == nil { + vp = viper.New() + } + // Bind install cmd flags + bindGivenFlags(vp, flags) + // Bind install env vars + viperReadEnv(vp, "MOUNT", constants.GetInitKeyEnvMap()) + + err := vp.Unmarshal(mount, setDecoder, decodeHook) + if err != nil { + r.Logger.Warnf("error unmarshalling MountSpec: %s", err) + } + return mount, err +} + func ReadResetSpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.ResetSpec, error) { reset, err := config.NewResetSpec(r.Config) if err != nil { diff --git a/cmd/mount.go b/cmd/mount.go new file mode 100644 index 00000000000..99191562748 --- /dev/null +++ b/cmd/mount.go @@ -0,0 +1,59 @@ +/* +Copyright © 2022 - 2023 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/mount-utils" + + "github.com/rancher/elemental-toolkit/cmd/config" + "github.com/rancher/elemental-toolkit/pkg/action" + "github.com/rancher/elemental-toolkit/pkg/constants" + elementalError "github.com/rancher/elemental-toolkit/pkg/error" +) + +func MountCmd(root *cobra.Command) *cobra.Command { + c := &cobra.Command{ + Use: "mount DEVICE SYSROOT", + Short: "Mount an elemental system from a device into the specified sysroot", + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + mounter := mount.New(constants.MountBinary) + + cfg, err := config.ReadConfigRun(viper.GetString("config-dir"), cmd.Flags(), mounter) + if err != nil { + cfg.Logger.Errorf("Error reading config: %s\n", err) + return elementalError.NewFromError(err, elementalError.ReadingRunConfig) + } + + cmd.SilenceUsage = true + spec, err := config.ReadMountSpec(cfg, cmd.Flags()) + if err != nil { + cfg.Logger.Errorf("Error reading spec: %s\n", err) + return elementalError.NewFromError(err, elementalError.ReadingSpecConfig) + } + + cfg.Logger.Infof("Mounting system...") + return action.RunMount(cfg, spec) + }, + } + root.AddCommand(c) + return c +} + +var _ = MountCmd(rootCmd) diff --git a/config.yaml.example b/config.yaml.example index a31204e3e08..4db688ce981 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -124,6 +124,27 @@ upgrade: # grub menu entry, this is the string that will be displayed grub-entry-name: Elemental +# configuration used for the 'mount' command +mount: + read-kernel-cmdline: true # read and parse /proc/cmdline for arguments + sysroot: /sysroot + write-fstab: true + write-sentinel: true + rw-paths: + - /var + - /etc + - /srv + persistent: + mode: overlay # overlay|bind + paths: + - /etc/systemd + - /etc/ssh + - /home + - /opt + - /root + - /usr/libexec + - /var/log + # use cosign to validate images from container registries cosign: true # cosign key to used for validation diff --git a/pkg/action/mount.go b/pkg/action/mount.go new file mode 100644 index 00000000000..59c26e505fe --- /dev/null +++ b/pkg/action/mount.go @@ -0,0 +1,126 @@ +/* +Copyright © 2022 - 2023 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/rancher/elemental-toolkit/pkg/constants" + "github.com/rancher/elemental-toolkit/pkg/elemental" + v1 "github.com/rancher/elemental-toolkit/pkg/types/v1" + "github.com/rancher/elemental-toolkit/pkg/utils" +) + +func RunMount(cfg *v1.RunConfig, spec *v1.MountSpec) error { + cfg.Logger.Info("Running mount command") + + e := elemental.NewElemental(&cfg.Config) + + err := e.MountPartitions(spec.Partitions.PartitionsByMountPoint(false)) + if err != nil { + return err + } + + if err := e.MountImage(spec.Image); err != nil { + cfg.Logger.Errorf("Error mounting image %s: %s", spec.Image.File, err.Error()) + return err + } + + for _, path := range spec.RwPaths { + cfg.Logger.Debugf("Mounting path %s into %s", path, spec.Sysroot) + if err := MountRwPath(cfg, spec.Sysroot, path); err != nil { + cfg.Logger.Errorf("Error mounting path %s: %s", path, err.Error()) + return err + } + } + + if err := WriteFstab(cfg, spec); err != nil { + cfg.Logger.Errorf("Error writing new fstab: %s", err.Error()) + return err + } + + cfg.Logger.Info("Mount command finished successfully") + return nil +} + +func MountRwPath(cfg *v1.RunConfig, sysroot, path string) error { + cfg.Logger.Debugf("Mounting Path") + + lower := filepath.Join(sysroot, path) + if err := utils.MkdirAll(cfg.Config.Fs, lower, constants.DirPerm); err != nil { + cfg.Logger.Errorf("Error creating directory %s: %s", path, err.Error()) + return err + } + + trimmed := strings.TrimPrefix(path, "/") + pathName := strings.ReplaceAll(trimmed, "/", "-") + upper := fmt.Sprintf("%s/%s/upper", constants.OverlayDir, pathName) + if err := utils.MkdirAll(cfg.Config.Fs, upper, constants.DirPerm); err != nil { + cfg.Logger.Errorf("Error creating upperdir %s: %s", upper, err.Error()) + return err + } + + work := fmt.Sprintf("%s/%s/work", constants.OverlayDir, pathName) + if err := utils.MkdirAll(cfg.Config.Fs, work, constants.DirPerm); err != nil { + cfg.Logger.Errorf("Error creating workdir %s: %s", work, err.Error()) + return err + } + + cfg.Logger.Debugf("Mounting overlay %s", lower) + options := []string{"defaults"} + options = append(options, fmt.Sprintf("lowerdir=%s", lower)) + options = append(options, fmt.Sprintf("upperdir=%s", upper)) + options = append(options, fmt.Sprintf("workdir=%s", work)) + + if err := cfg.Mounter.Mount("overlay", lower, "overlay", options); err != nil { + cfg.Logger.Errorf("Error mounting overlay: %s", err.Error()) + return err + } + + return nil +} + +func WriteFstab(cfg *v1.RunConfig, spec *v1.MountSpec) error { + if !spec.WriteFstab { + cfg.Logger.Debug("Skipping writing fstab") + return nil + } + + data := fmt.Sprintf("%s\t/\tauto\tro\t0 0\n", spec.Image.LoopDevice) + + for _, part := range spec.Partitions.PartitionsByMountPoint(false) { + data = data + fmt.Sprintf("%s\t%s\t%s\t%s\n", part.Path, part.MountPoint, part.FS, "") + } + + for _, rw := range spec.RwPaths { + trimmed := strings.TrimPrefix(rw, "/") + pathName := strings.ReplaceAll(trimmed, "/", "-") + upper := fmt.Sprintf("%s/%s/upper", constants.OverlayDir, pathName) + work := fmt.Sprintf("%s/%s/work", constants.OverlayDir, pathName) + + options := []string{"defaults"} + options = append(options, fmt.Sprintf("lowerdir=%s", rw)) + options = append(options, fmt.Sprintf("upperdir=%s", upper)) + options = append(options, fmt.Sprintf("workdir=%s", work)) + options = append(options, fmt.Sprintf("x-systemd.requires-mounts-for=%s", constants.OverlayDir)) + data = data + fmt.Sprintf("%s\t%s\t%s\t%s\n", "overlay", rw, "overlay", strings.Join(options, ",")) + } + + return cfg.Config.Fs.WriteFile(filepath.Join(spec.Sysroot, "/etc/fstab"), []byte(data), 0644) +} diff --git a/pkg/action/mount_test.go b/pkg/action/mount_test.go new file mode 100644 index 00000000000..dcfddcb68b4 --- /dev/null +++ b/pkg/action/mount_test.go @@ -0,0 +1,71 @@ +package action_test + +import ( + "bytes" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/twpayne/go-vfs" + "github.com/twpayne/go-vfs/vfst" + "k8s.io/mount-utils" + + "github.com/rancher/elemental-toolkit/pkg/action" + "github.com/rancher/elemental-toolkit/pkg/config" + "github.com/rancher/elemental-toolkit/pkg/constants" + v1 "github.com/rancher/elemental-toolkit/pkg/types/v1" + "github.com/rancher/elemental-toolkit/pkg/utils" +) + +var _ = Describe("Mount Action", func() { + var cfg *v1.RunConfig + var mounter *mount.FakeMounter + var fs vfs.FS + var logger v1.Logger + var cleanup func() + var memLog *bytes.Buffer + + BeforeEach(func() { + mounter = &mount.FakeMounter{} + memLog = &bytes.Buffer{} + logger = v1.NewBufferLogger(memLog) + logger.SetLevel(logrus.DebugLevel) + fs, cleanup, _ = vfst.NewTestFS(map[string]interface{}{}) + cfg = config.NewRunConfig( + config.WithFs(fs), + config.WithMounter(mounter), + config.WithLogger(logger), + ) + }) + AfterEach(func() { + cleanup() + }) + Describe("Write fstab", Label("mount", "fstab"), func() { + It("Writes a simple fstab", func() { + spec := &v1.MountSpec{ + WriteFstab: true, + Image: &v1.Image{ + LoopDevice: "/dev/loop0", + }, + } + utils.MkdirAll(fs, filepath.Join(spec.Sysroot, "/etc"), constants.DirPerm) + err := action.WriteFstab(cfg, spec) + Expect(err).To(BeNil()) + + fstab, err := cfg.Config.Fs.ReadFile(filepath.Join(spec.Sysroot, "/etc/fstab")) + Expect(err).To(BeNil()) + Expect(string(fstab)).To(Equal("/dev/loop0\t/\tauto\tro\t0 0\n")) + }) + }) + // Describe("Mount image", Label("mount", "image"), func() { + // It("Mounts an image", func() { + // spec := &v1.MountSpec{ + // Image: &v1.Image{}, + // } + + // err := action.RunMount(cfg, spec) + // Expect(err).To(BeNil()) + // }) + // }) +}) diff --git a/pkg/config/config.go b/pkg/config/config.go index 7846013f9eb..c38b18ba321 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -222,6 +222,48 @@ func NewInitSpec() *v1.InitSpec { } } +func NewMountSpec() *v1.MountSpec { + return &v1.MountSpec{ + Sysroot: "/sysroot", + WriteFstab: true, + WriteSentinel: true, + Image: &v1.Image{ + Label: constants.ActiveLabel, + FS: constants.LinuxImgFs, + File: filepath.Join(constants.RunningStateDir, "cOS", constants.ActiveImgFile), + Source: v1.NewFileSrc(filepath.Join(constants.RunningStateDir, "cOS", constants.ActiveImgFile)), + MountPoint: "/sysroot", + }, + Partitions: v1.ElementalPartitions{ + State: &v1.Partition{ + FilesystemLabel: constants.StateLabel, + Size: constants.StateSize, + Name: constants.StatePartName, + FS: constants.LinuxFs, + MountPoint: constants.RunningStateDir, + Flags: []string{}, + }, + Persistent: &v1.Partition{ + FilesystemLabel: constants.PersistentLabel, + Size: constants.PersistentSize, + Name: constants.PersistentPartName, + FS: constants.LinuxFs, + MountPoint: constants.PersistentDir, + Flags: []string{}, + }, + OEM: &v1.Partition{ + FilesystemLabel: constants.OEMLabel, + Size: constants.OEMSize, + Name: constants.OEMPartName, + FS: constants.LinuxFs, + MountPoint: constants.OEMPath, + Flags: []string{}, + }, + }, + RwPaths: []string{"/var", "/etc", "/srv"}, + } +} + func NewInstallElementalPartitions() v1.ElementalPartitions { partitions := v1.ElementalPartitions{} partitions.OEM = &v1.Partition{ diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 1885ad615d6..191a0470e06 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -59,7 +59,6 @@ const ( ImgSize = uint(0) ImgOverhead = uint(256) HTTPTimeout = 60 - CosSetup = "/usr/bin/cos-setup" GPT = "gpt" BuildImgName = "elemental" UsrLocalPath = "/usr/local" @@ -67,15 +66,16 @@ const ( ConfigDir = "/etc/elemental" // Mountpoints of images and partitions - RecoveryDir = "/run/cos/recovery" - StateDir = "/run/cos/state" - OEMDir = "/run/cos/oem" - PersistentDir = "/run/cos/persistent" - ActiveDir = "/run/cos/active" - TransitionDir = "/run/cos/transition" - EfiDir = "/run/cos/efi" - ImgSrcDir = "/run/cos/imgsrc" - WorkingImgDir = "/run/cos/workingtree" + RecoveryDir = "/run/elemental/recovery" + StateDir = "/run/elemental/state" + OEMDir = "/run/elemental/oem" + PersistentDir = "/run/elemental/persistent" + ActiveDir = "/run/elemental/active" + TransitionDir = "/run/elemental/transition" + EfiDir = "/run/elemental/efi" + ImgSrcDir = "/run/elemental/imgsrc" + WorkingImgDir = "/run/elemental/workingtree" + OverlayDir = "/run/elemental/overlay" RunningStateDir = "/run/initramfs/cos-state" // TODO: converge this constant with StateDir/RecoveryDir in dracut module from cos-toolkit // Live image mountpoints diff --git a/pkg/features/embedded/elemental-rootfs/etc/dracut.conf.d/02-elemental-rootfs.conf b/pkg/features/embedded/elemental-rootfs/etc/dracut.conf.d/02-elemental-rootfs.conf new file mode 100644 index 00000000000..97e30f2527c --- /dev/null +++ b/pkg/features/embedded/elemental-rootfs/etc/dracut.conf.d/02-elemental-rootfs.conf @@ -0,0 +1 @@ +add_dracutmodules+=" elemental-rootfs " diff --git a/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-generator.sh b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-generator.sh new file mode 100755 index 00000000000..7630a59f937 --- /dev/null +++ b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-generator.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh + +if [ ! -e "$GENERATOR_DIR/initrd-root-fs.target.requires/sysroot.mount" ]; then + mkdir -p "$GENERATOR_DIR"/initrd-root-fs.target.requires + ln -s "$GENERATOR_DIR"/sysroot.mount \ + "$GENERATOR_DIR"/initrd-root-fs.target.requires/sysroot.mount +fi diff --git a/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-rootfs.service b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-rootfs.service new file mode 100644 index 00000000000..20caa16124a --- /dev/null +++ b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/elemental-rootfs.service @@ -0,0 +1,12 @@ +[Unit] +Description=Elemental system rootfs mounts +DefaultDependencies=no +After=initrd-root-fs.target elemental-setup-rootfs.service +Requires=initrd-root-fs.target +Before=initrd-fs.target +Conflicts=initrd-switch-root.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/elemental mount --debug diff --git a/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/module-setup.sh b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/module-setup.sh new file mode 100755 index 00000000000..ef0379299d2 --- /dev/null +++ b/pkg/features/embedded/elemental-rootfs/usr/lib/dracut/modules.d/30elemental-rootfs/module-setup.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# called by dracut +check() { + require_binaries "$systemdutildir"/systemd || return 1 + return 255 +} + +# called by dracut +depends() { + echo systemd rootfs-block dm fs-lib + return 0 +} + +# called by dracut +installkernel() { + instmods overlay +} + +# called by dracut +install() { + declare moddir=${moddir} + declare systemdutildir=${systemdutildir} + declare systemdsystemunitdir=${systemdsystemunitdir} + + inst_multiple \ + mount mountpoint sort rmdir findmnt rsync cut realpath basename lsblk + + inst_simple "/etc/elemental/config.yaml" + + # Include utilities required for elemental-setup services, + # probably a devoted dracut module makes sense + inst_multiple -o \ + "$systemdutildir"/systemd-fsck partprobe sync udevadm parted mkfs.ext2 mkfs.ext3 mkfs.ext4 mkfs.vfat mkfs.fat mkfs.xfs blkid e2fsck resize2fs mount xfs_growfs umount sgdisk elemental + inst_script "${moddir}/elemental-generator.sh" \ + "${systemdutildir}/system-generators/dracut-elemental-generator" + inst_simple "${moddir}/elemental-rootfs.service" \ + "${systemdsystemunitdir}/elemental-rootfs.service" + mkdir -p "${initdir}/${systemdsystemunitdir}/initrd-fs.target.requires" + ln_r "../elemental-rootfs.service" \ + "${systemdsystemunitdir}/initrd-fs.target.requires/elemental-rootfs.service" + ln_r "$systemdutildir"/systemd-fsck \ + "/sbin/systemd-fsck" + dracut_need_initqueue +} diff --git a/pkg/features/features.go b/pkg/features/features.go index 639be2b3cea..392ff8bc812 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -36,6 +36,7 @@ const ( embeddedRoot = "embedded" FeatureImmutableRootfs = "immutable-rootfs" + FeatureElementalRootfs = "elemental-rootfs" FeatureGrubConfig = "grub-config" FeatureGrubDefaultBootargs = "grub-default-bootargs" FeatureElementalSetup = "elemental-setup" @@ -134,6 +135,8 @@ func Get(names []string) ([]*Feature, error) { features = append(features, New(name, nil)) case FeatureImmutableRootfs: features = append(features, New(name, nil)) + case FeatureElementalRootfs: + features = append(features, New(name, nil)) case FeatureDracutConfig: features = append(features, New(name, nil)) case FeatureGrubConfig: diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index c10c8fd7149..b3444946a30 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -229,6 +229,7 @@ func (i *InstallSpec) Sanitize() error { return i.Partitions.SetFirmwarePartitions(i.Firmware, i.PartTable) } +// InitSpec struct represents all the init action details type InitSpec struct { Mkinitrd bool `yaml:"mkinitrd,omitempty" mapstructure:"mkinitrd"` Force bool `yaml:"force,omitempty" mapstructure:"force"` @@ -236,6 +237,25 @@ type InitSpec struct { Features []string `yaml:"features,omitempty" mapstructure:"features"` } +// MountSpec struct represents all the mount action details +type MountSpec struct { + ReadKernelCmdline bool `yaml:"read-kernel-cmdline,omitempty" mapstructure:"read-kernel-cmdline"` + WriteFstab bool `yaml:"write-fstab,omitempty" mapstructure:"write-fstab"` + WriteSentinel bool `yaml:"write-sentinel,omitempty" mapstructure:"write-sentinel"` + Sysroot string `yaml:"sysroot,omitempty" mapstructure:"sysroot"` + Image *Image `yaml:"image,omitempty" mapstructure:"image"` + Partitions ElementalPartitions + RwPaths []string `yaml:"rw-paths,omitempty" mapstructure:"rw-paths"` + Persistent PersistentMounts `yaml:"persistent,omitempty" mapstructure:"persistent"` +} + +// PersistentMounts struct contains settings for which paths to mount as +// persistent +type PersistentMounts struct { + Mode string `yaml:"mode,omitempty" mapstructure:"mode"` + Paths []string `yaml:"paths,omitempty" mapstructure:"paths"` +} + // ResetSpec struct represents all the reset action details type ResetSpec struct { FormatPersistent bool `yaml:"reset-persistent,omitempty" mapstructure:"reset-persistent"` diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 25aaa56a79c..a37441ca8d9 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -132,7 +132,7 @@ func ConcatFiles(fs v1.FS, sources []string, target string) (err error) { // CreateDirStructure creates essentials directories under the root tree that might not be present // within a container image (/dev, /run, etc.) func CreateDirStructure(fs v1.FS, target string) error { - for _, dir := range []string{"/run", "/dev", "/boot", "/usr/local", "/oem", "/system", "/etc/elemental/config.d"} { + for _, dir := range []string{"/run", "/dev", "/boot", "/oem", "/system", "/etc/elemental/config.d"} { err := MkdirAll(fs, filepath.Join(target, dir), cnst.DirPerm) if err != nil { return err diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 0527a6172bf..fafc62e165b 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -431,7 +431,7 @@ var _ = Describe("Utils", Label("utils"), func() { }) Describe("CreateDirStructure", Label("CreateDirStructure"), func() { It("Creates essential directories", func() { - dirList := []string{"sys", "proc", "dev", "tmp", "boot", "usr/local", "oem"} + dirList := []string{"sys", "proc", "dev", "tmp", "boot", "oem"} for _, dir := range dirList { _, err := fs.Stat(fmt.Sprintf("/my/root/%s", dir)) Expect(err).NotTo(BeNil())