elemental-toolkit/pkg/action/reset.go

345 lines
11 KiB
Go

/*
Copyright © 2022 - 2025 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"
"time"
"github.com/rancher/elemental-toolkit/v2/pkg/bootloader"
"github.com/rancher/elemental-toolkit/v2/pkg/constants"
"github.com/rancher/elemental-toolkit/v2/pkg/elemental"
elementalError "github.com/rancher/elemental-toolkit/v2/pkg/error"
"github.com/rancher/elemental-toolkit/v2/pkg/snapshotter"
"github.com/rancher/elemental-toolkit/v2/pkg/types"
"github.com/rancher/elemental-toolkit/v2/pkg/utils"
)
// resetHook runs the given hook without chroot. Moreover if the hook is 'after-reset'
// it appends defined cloud init paths rooted to the deployed root. This way any
// 'after-reset' hook provided by the deployed system image is also taken into account.
func (r *ResetAction) resetHook(hook string) error {
cIPaths := r.cfg.CloudInitPaths
if hook == constants.AfterResetHook {
cIPaths = append(cIPaths, utils.PreAppendRoot(constants.WorkingImgDir, r.cfg.CloudInitPaths...)...)
}
return Hook(&r.cfg.Config, hook, r.cfg.Strict, cIPaths...)
}
func (r *ResetAction) resetChrootHook(hook string, root string) error {
extraMounts := map[string]string{}
persistent := r.spec.Partitions.Persistent
if persistent != nil && persistent.MountPoint != "" {
extraMounts[persistent.MountPoint] = constants.PersistentPath
}
oem := r.spec.Partitions.OEM
if oem != nil && oem.MountPoint != "" {
extraMounts[oem.MountPoint] = constants.OEMPath
}
efi := r.spec.Partitions.Boot
if efi != nil && efi.MountPoint != "" {
extraMounts[efi.MountPoint] = constants.BootDir
}
return ChrootHook(&r.cfg.Config, hook, r.cfg.Strict, root, extraMounts, r.cfg.CloudInitPaths...)
}
type ResetActionOption func(r *ResetAction) error
func WithResetBootloader(bootloader types.Bootloader) func(r *ResetAction) error {
return func(i *ResetAction) error {
i.bootloader = bootloader
return nil
}
}
type ResetAction struct {
cfg *types.RunConfig
spec *types.ResetSpec
bootloader types.Bootloader
snapshotter types.Snapshotter
snapshot *types.Snapshot
}
func NewResetAction(cfg *types.RunConfig, spec *types.ResetSpec, opts ...ResetActionOption) (*ResetAction, error) {
var err error
r := &ResetAction{cfg: cfg, spec: spec}
for _, o := range opts {
err = o(r)
if err != nil {
cfg.Logger.Errorf("error applying config option: %s", err.Error())
return nil, err
}
}
if r.bootloader == nil {
r.bootloader = bootloader.NewGrub(
&cfg.Config,
bootloader.WithGrubDisableBootEntry(r.spec.DisableBootEntry),
bootloader.WithGrubAutoDisableBootEntry(),
bootloader.WithGrubClearBootEntry(false),
)
}
if r.snapshotter == nil {
r.snapshotter, err = snapshotter.NewSnapshotter(cfg.Config, cfg.Snapshotter, r.bootloader)
if err != nil {
cfg.Logger.Errorf("error initializing snapshotter of type '%s'", cfg.Snapshotter.Type)
return nil, err
}
}
if r.cfg.Snapshotter.Type == constants.BtrfsSnapshotterType {
if spec.Partitions.State.FS != constants.Btrfs {
cfg.Logger.Warning("Btrfs snapshotter type, forcing btrfs filesystem on state partition")
spec.Partitions.State.FS = constants.Btrfs
}
}
return r, nil
}
func (r *ResetAction) updateInstallState(cleanup *utils.CleanStack) error {
if r.spec.Partitions.Recovery == nil || r.spec.Partitions.State == nil {
return fmt.Errorf("undefined state or recovery partition")
}
if r.snapshot == nil {
return fmt.Errorf("undefined reset snapshot")
}
// Reuse recovery source and digest if system points to recovery
src := r.spec.System
if src.IsFile() && strings.HasSuffix(src.Value(), constants.RecoveryImgFile) {
if r.spec.State != nil && r.spec.State.Partitions[constants.RecoveryPartName] != nil &&
r.spec.State.Partitions[constants.RecoveryPartName].RecoveryImage != nil {
src = r.spec.State.Partitions[constants.RecoveryPartName].RecoveryImage.Source
src.SetDigest(r.spec.State.Partitions[constants.RecoveryPartName].RecoveryImage.Digest)
}
}
date := time.Now().Format(time.RFC3339)
installState := &types.InstallState{
Date: date,
Snapshotter: r.cfg.Snapshotter,
Partitions: map[string]*types.PartitionState{
constants.StatePartName: {
FSLabel: r.spec.Partitions.State.FilesystemLabel,
Snapshots: map[int]*types.SystemState{
r.snapshot.ID: {
Source: src,
Digest: src.GetDigest(),
Active: true,
Labels: r.spec.SnapshotLabels,
Date: date,
FromAction: constants.ActionReset,
},
},
},
},
}
if r.spec.Partitions.OEM != nil {
installState.Partitions[constants.OEMPartName] = &types.PartitionState{
FSLabel: r.spec.Partitions.OEM.FilesystemLabel,
}
}
if r.spec.Partitions.Persistent != nil {
installState.Partitions[constants.PersistentPartName] = &types.PartitionState{
FSLabel: r.spec.Partitions.Persistent.FilesystemLabel,
}
}
if r.spec.State != nil && r.spec.State.Partitions != nil {
installState.Partitions[constants.RecoveryPartName] = r.spec.State.Partitions[constants.RecoveryPartName]
}
umount, err := elemental.MountRWPartition(r.cfg.Config, r.spec.Partitions.Recovery)
if err != nil {
return err
}
cleanup.Push(umount)
return r.cfg.WriteInstallState(
installState,
filepath.Join(r.spec.Partitions.State.MountPoint, constants.InstallStateFile),
filepath.Join(r.spec.Partitions.Recovery.MountPoint, constants.InstallStateFile),
)
}
// ResetRun will reset the cos system to by following several steps
func (r ResetAction) Run() (err error) {
cleanup := utils.NewCleanStack()
defer func() { err = cleanup.Cleanup(err) }()
// Unmount partitions if any is already mounted before formatting
err = elemental.UnmountPartitions(r.cfg.Config, r.spec.Partitions.PartitionsByMountPoint(true, r.spec.Partitions.Recovery))
if err != nil {
return elementalError.NewFromError(err, elementalError.UnmountPartitions)
}
// Reformat state partition
err = elemental.FormatPartition(r.cfg.Config, r.spec.Partitions.State)
if err != nil {
return elementalError.NewFromError(err, elementalError.FormatPartitions)
}
// Reformat persistent partition
if r.spec.FormatPersistent {
persistent := r.spec.Partitions.Persistent
if persistent != nil {
err = elemental.FormatPartition(r.cfg.Config, persistent)
if err != nil {
return elementalError.NewFromError(err, elementalError.FormatPartitions)
}
}
}
// Reformat OEM
if r.spec.FormatOEM {
oem := r.spec.Partitions.OEM
if oem != nil {
err = elemental.FormatPartition(r.cfg.Config, oem)
if err != nil {
return elementalError.NewFromError(err, elementalError.FormatPartitions)
}
}
}
// Mount configured partitions
err = elemental.MountPartitions(r.cfg.Config, r.spec.Partitions.PartitionsByMountPoint(false, r.spec.Partitions.Recovery), "rw")
if err != nil {
return elementalError.NewFromError(err, elementalError.MountPartitions)
}
cleanup.Push(func() error {
return elemental.UnmountPartitions(r.cfg.Config, r.spec.Partitions.PartitionsByMountPoint(true, r.spec.Partitions.Recovery))
})
// Init snapshotter
err = r.snapshotter.InitSnapshotter(r.spec.Partitions.State, r.spec.Partitions.Boot.MountPoint)
if err != nil {
return elementalError.NewFromError(err, elementalError.SnapshotterInit)
}
// Before reset hook happens once partitions are aready and before deploying the OS image
err = r.resetHook(constants.BeforeResetHook)
if err != nil {
return elementalError.NewFromError(err, elementalError.HookBeforeReset)
}
// Starting snapshotter transaction
r.cfg.Logger.Info("Starting snapshotter transaction")
r.snapshot, err = r.snapshotter.StartTransaction()
if err != nil {
r.cfg.Logger.Errorf("failed to start snapshotter transaction")
return elementalError.NewFromError(err, elementalError.SnapshotterStart)
}
cleanup.PushErrorOnly(func() error { return r.snapshotter.CloseTransactionOnError(r.snapshot) })
// Deploy system image
err = elemental.MirrorRoot(r.cfg.Config, r.snapshot.WorkDir, r.spec.System)
if err != nil {
r.cfg.Logger.Errorf("failed deploying source: %s", r.spec.System.String())
return elementalError.NewFromError(err, elementalError.DumpSource)
}
// Fine tune the dumped tree
r.cfg.Logger.Info("Fine tune the dumped root tree")
err = r.refineDeployment()
if err != nil {
r.cfg.Logger.Error("failed refining system root tree")
return err
}
// Closing snapshotter transaction
r.cfg.Logger.Info("Closing snapshotter transaction")
err = r.snapshotter.CloseTransaction(r.snapshot)
if err != nil {
r.cfg.Logger.Errorf("failed closing snapshot transaction: %v", err)
return err
}
err = r.resetHook(constants.PostResetHook)
if err != nil {
return elementalError.NewFromError(err, elementalError.HookPostReset)
}
err = r.updateInstallState(cleanup)
if err != nil {
return elementalError.NewFromError(err, elementalError.CreateFile)
}
// Do not reboot/poweroff on cleanup errors
err = cleanup.Cleanup(err)
if err != nil {
return elementalError.NewFromError(err, elementalError.Cleanup)
}
return PowerAction(r.cfg)
}
func (r *ResetAction) refineDeployment() error { //nolint:dupl
// Copy cloud-init if any
err := elemental.CopyCloudConfig(r.cfg.Config, r.spec.Partitions.GetConfigStorage(), r.spec.CloudInit)
if err != nil {
return elementalError.NewFromError(err, elementalError.CopyFile)
}
// Install grub
err = r.bootloader.Install(
r.snapshot.WorkDir,
r.spec.Partitions.Boot.MountPoint,
)
if err != nil {
r.cfg.Logger.Errorf("failed installing grub: %v", err)
return elementalError.NewFromError(err, elementalError.InstallGrub)
}
err = r.resetChrootHook(constants.AfterResetChrootHook, constants.WorkingImgDir)
if err != nil {
r.cfg.Logger.Errorf("failed after-reset-chroot hook: %v", err)
return elementalError.NewFromError(err, elementalError.HookAfterResetChroot)
}
err = r.resetHook(constants.AfterResetHook)
if err != nil {
r.cfg.Logger.Errorf("failed after-reset hook: %v", err)
return elementalError.NewFromError(err, elementalError.HookAfterReset)
}
grubVars := r.spec.GetGrubLabels()
err = r.bootloader.SetPersistentVariables(
filepath.Join(r.spec.Partitions.Boot.MountPoint, constants.GrubOEMEnv),
grubVars,
)
if err != nil {
r.cfg.Logger.Error("Error setting GRUB labels: %s", err)
return elementalError.NewFromError(err, elementalError.SetGrubVariables)
}
// Installation rebrand (only grub for now)
err = r.bootloader.SetDefaultEntry(
r.spec.Partitions.Boot.MountPoint,
constants.WorkingImgDir,
r.spec.GrubDefEntry,
)
if err != nil {
r.cfg.Logger.Errorf("failed setting defaut GRUB entry: %v", err)
return elementalError.NewFromError(err, elementalError.SetDefaultGrubEntry)
}
return nil
}