375 lines
12 KiB
Go
375 lines
12 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"
|
|
"time"
|
|
|
|
"github.com/rancher/elemental-toolkit/v2/pkg/bootloader"
|
|
cnst "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"
|
|
)
|
|
|
|
type InstallAction struct {
|
|
cfg *types.RunConfig
|
|
spec *types.InstallSpec
|
|
bootloader types.Bootloader
|
|
snapshotter types.Snapshotter
|
|
snapshot *types.Snapshot
|
|
}
|
|
|
|
type InstallActionOption func(i *InstallAction) error
|
|
|
|
func WithInstallBootloader(bootloader types.Bootloader) func(i *InstallAction) error {
|
|
return func(i *InstallAction) error {
|
|
i.bootloader = bootloader
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func NewInstallAction(cfg *types.RunConfig, spec *types.InstallSpec, opts ...InstallActionOption) (*InstallAction, error) {
|
|
var err error
|
|
|
|
i := &InstallAction{cfg: cfg, spec: spec}
|
|
|
|
for _, o := range opts {
|
|
err = o(i)
|
|
if err != nil {
|
|
cfg.Logger.Errorf("error applying config option: %s", err.Error())
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if i.bootloader == nil {
|
|
i.bootloader = bootloader.NewGrub(&cfg.Config,
|
|
bootloader.WithGrubDisableBootEntry(i.spec.DisableBootEntry),
|
|
bootloader.WithGrubAutoDisableBootEntry(),
|
|
)
|
|
}
|
|
|
|
if i.snapshotter == nil {
|
|
i.snapshotter, err = snapshotter.NewSnapshotter(cfg.Config, cfg.Snapshotter, i.bootloader)
|
|
}
|
|
|
|
if i.cfg.Snapshotter.Type == cnst.BtrfsSnapshotterType {
|
|
if spec.Partitions.State.FS != cnst.Btrfs {
|
|
cfg.Logger.Warning("Btrfs snapshotter type, forcing btrfs filesystem on state partition")
|
|
spec.Partitions.State.FS = cnst.Btrfs
|
|
}
|
|
}
|
|
|
|
return i, err
|
|
}
|
|
|
|
// installHook runs the given hook without chroot. Moreover if the hook is 'after-install'
|
|
// it appends defiled cloud init paths rooted to the deployed root. This way any
|
|
// 'after-install' hook provided by the deployed system image is also taken into account.
|
|
func (i *InstallAction) installHook(hook string) error {
|
|
cIPaths := i.cfg.CloudInitPaths
|
|
if hook == cnst.AfterInstallHook {
|
|
cIPaths = append(cIPaths, utils.PreAppendRoot(cnst.WorkingImgDir, i.cfg.CloudInitPaths...)...)
|
|
}
|
|
return Hook(&i.cfg.Config, hook, i.cfg.Strict, cIPaths...)
|
|
}
|
|
|
|
func (i *InstallAction) installChrootHook(hook string, root string) error {
|
|
extraMounts := map[string]string{}
|
|
persistent := i.spec.Partitions.Persistent
|
|
if persistent != nil && persistent.MountPoint != "" {
|
|
extraMounts[persistent.MountPoint] = cnst.PersistentPath
|
|
}
|
|
oem := i.spec.Partitions.OEM
|
|
if oem != nil && oem.MountPoint != "" {
|
|
extraMounts[oem.MountPoint] = cnst.OEMPath
|
|
}
|
|
efi := i.spec.Partitions.Boot
|
|
if efi != nil && efi.MountPoint != "" {
|
|
extraMounts[efi.MountPoint] = cnst.BootDir
|
|
}
|
|
return ChrootHook(&i.cfg.Config, hook, i.cfg.Strict, root, extraMounts, i.cfg.CloudInitPaths...)
|
|
}
|
|
|
|
func (i *InstallAction) createInstallStateYaml() error {
|
|
if i.spec.Partitions.State == nil || i.spec.Partitions.Recovery == nil {
|
|
return fmt.Errorf("undefined state or recovery partition")
|
|
}
|
|
|
|
if i.snapshot == nil {
|
|
return fmt.Errorf("undefined installed snapshot")
|
|
}
|
|
|
|
date := time.Now().Format(time.RFC3339)
|
|
|
|
installState := &types.InstallState{
|
|
Date: date,
|
|
Snapshotter: i.cfg.Snapshotter,
|
|
Partitions: map[string]*types.PartitionState{
|
|
cnst.StatePartName: {
|
|
FSLabel: i.spec.Partitions.State.FilesystemLabel,
|
|
Snapshots: map[int]*types.SystemState{
|
|
i.snapshot.ID: {
|
|
Source: i.spec.System,
|
|
Digest: i.spec.System.GetDigest(),
|
|
Active: true,
|
|
Labels: i.spec.SnapshotLabels,
|
|
Date: date,
|
|
FromAction: cnst.ActionInstall,
|
|
},
|
|
},
|
|
},
|
|
cnst.RecoveryPartName: {
|
|
FSLabel: i.spec.Partitions.Recovery.FilesystemLabel,
|
|
RecoveryImage: &types.SystemState{
|
|
Source: i.spec.RecoverySystem.Source,
|
|
Digest: i.spec.RecoverySystem.Source.GetDigest(),
|
|
Label: i.spec.RecoverySystem.Label,
|
|
FS: i.spec.RecoverySystem.FS,
|
|
Labels: i.spec.SnapshotLabels,
|
|
Date: date,
|
|
FromAction: cnst.ActionInstall,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if i.spec.Partitions.OEM != nil {
|
|
installState.Partitions[cnst.OEMPartName] = &types.PartitionState{
|
|
FSLabel: i.spec.Partitions.OEM.FilesystemLabel,
|
|
}
|
|
}
|
|
if i.spec.Partitions.Persistent != nil {
|
|
installState.Partitions[cnst.PersistentPartName] = &types.PartitionState{
|
|
FSLabel: i.spec.Partitions.Persistent.FilesystemLabel,
|
|
}
|
|
}
|
|
if i.spec.Partitions.Boot != nil {
|
|
installState.Partitions[cnst.BootPartName] = &types.PartitionState{
|
|
FSLabel: i.spec.Partitions.Boot.FilesystemLabel,
|
|
}
|
|
}
|
|
|
|
return i.cfg.WriteInstallState(
|
|
installState,
|
|
filepath.Join(i.spec.Partitions.State.MountPoint, cnst.InstallStateFile),
|
|
filepath.Join(i.spec.Partitions.Recovery.MountPoint, cnst.InstallStateFile),
|
|
)
|
|
}
|
|
|
|
// InstallRun will install the system from a given configuration
|
|
func (i InstallAction) Run() (err error) {
|
|
cleanup := utils.NewCleanStack()
|
|
defer func() { err = cleanup.Cleanup(err) }()
|
|
|
|
// Set installation sources from a downloaded ISO
|
|
if i.spec.Iso != "" {
|
|
isoSrc, isoCleaner, err := elemental.SourceFormISO(i.cfg.Config, i.spec.Iso)
|
|
cleanup.Push(isoCleaner)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.Unknown)
|
|
}
|
|
i.spec.System = isoSrc
|
|
}
|
|
|
|
// Partition and format device if needed
|
|
err = i.prepareDevice()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = elemental.MountPartitions(i.cfg.Config, i.spec.Partitions.PartitionsByMountPoint(false), "rw")
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed mounting partitions")
|
|
return elementalError.NewFromError(err, elementalError.MountPartitions)
|
|
}
|
|
cleanup.Push(func() error {
|
|
return elemental.UnmountPartitions(i.cfg.Config, i.spec.Partitions.PartitionsByMountPoint(true))
|
|
})
|
|
|
|
err = i.snapshotter.InitSnapshotter(i.spec.Partitions.State, i.spec.Partitions.Boot.MountPoint)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed initializing snapshotter")
|
|
return elementalError.NewFromError(err, elementalError.SnapshotterInit)
|
|
}
|
|
|
|
// Before install hook happens after partitioning but before the image OS is applied
|
|
err = i.installHook(cnst.BeforeInstallHook)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.HookBeforeInstall)
|
|
}
|
|
|
|
// Starting snapshotter transaction
|
|
i.cfg.Logger.Info("Starting snapshotter transaction")
|
|
i.snapshot, err = i.snapshotter.StartTransaction()
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed to start snapshotter transaction")
|
|
return elementalError.NewFromError(err, elementalError.SnapshotterStart)
|
|
}
|
|
cleanup.PushErrorOnly(func() error { return i.snapshotter.CloseTransactionOnError(i.snapshot) })
|
|
|
|
// Deploy system image
|
|
err = elemental.MirrorRoot(i.cfg.Config, i.snapshot.WorkDir, i.spec.System)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed deploying source: %s", i.spec.System.String())
|
|
return elementalError.NewFromError(err, elementalError.DumpSource)
|
|
}
|
|
|
|
// Fine tune the dumped tree
|
|
i.cfg.Logger.Info("Fine tune the dumped root tree")
|
|
err = i.refineDeployment()
|
|
if err != nil {
|
|
i.cfg.Logger.Error("failed refining system root tree")
|
|
return err
|
|
}
|
|
|
|
// Closing snapshotter transaction
|
|
i.cfg.Logger.Info("Closing snapshotter transaction")
|
|
err = i.snapshotter.CloseTransaction(i.snapshot)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed closing snapshot transaction: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Install recovery
|
|
recoveryBootDir := filepath.Join(i.spec.Partitions.Recovery.MountPoint, "boot")
|
|
err = utils.MkdirAll(i.cfg.Fs, recoveryBootDir, cnst.DirPerm)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed creating recovery boot dir: %v", err)
|
|
return err
|
|
}
|
|
|
|
recoverySystem := i.spec.RecoverySystem
|
|
i.cfg.Logger.Info("Deploying recovery system")
|
|
if recoverySystem.Source.String() == i.spec.System.String() {
|
|
// Reuse already deployed root-tree from active snapshot
|
|
recoverySystem.Source, err = i.snapshotter.SnapshotToImageSource(i.snapshot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recoverySystem.Source.SetDigest(i.spec.System.GetDigest())
|
|
}
|
|
err = elemental.DeployRecoverySystem(i.cfg.Config, &recoverySystem)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("Failed deploying recovery image: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.DeployImage)
|
|
}
|
|
|
|
err = i.installHook(cnst.PostInstallHook)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.HookPostInstall)
|
|
}
|
|
|
|
// Add state.yaml file on state and recovery partitions
|
|
i.cfg.Logger.Info("Creating installation state files")
|
|
err = i.createInstallStateYaml()
|
|
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)
|
|
}
|
|
|
|
// If we want to eject the cd, create the required executable so the cd is ejected at shutdown
|
|
if i.cfg.EjectCD && utils.BootedFrom(i.cfg.Runner, "cdroot") {
|
|
i.cfg.Logger.Infof("Writing eject script")
|
|
err = i.cfg.Fs.WriteFile("/usr/lib/systemd/system-shutdown/eject", []byte(cnst.EjectScript), 0744)
|
|
if err != nil {
|
|
i.cfg.Logger.Warnf("Could not write eject script, cdrom wont be ejected automatically: %s", err)
|
|
}
|
|
}
|
|
|
|
return PowerAction(i.cfg)
|
|
}
|
|
|
|
func (i *InstallAction) prepareDevice() error {
|
|
if i.spec.NoFormat {
|
|
if elemental.CheckActiveDeployment(i.cfg.Config) && !i.spec.Force {
|
|
return elementalError.New("use `force` flag to run an installation over the current running deployment", elementalError.AlreadyInstalled)
|
|
}
|
|
} else {
|
|
// Deactivate any active volume on target
|
|
err := elemental.DeactivateDevices(i.cfg.Config)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.DeactivatingDevices)
|
|
}
|
|
// Partition device
|
|
err = elemental.PartitionAndFormatDevice(i.cfg.Config, i.spec)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.PartitioningDevice)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *InstallAction) refineDeployment() error { //nolint:dupl
|
|
// Copy cloud-init if any
|
|
err := elemental.CopyCloudConfig(i.cfg.Config, i.spec.Partitions.GetConfigStorage(), i.spec.CloudInit)
|
|
if err != nil {
|
|
return elementalError.NewFromError(err, elementalError.CopyFile)
|
|
}
|
|
// Install grub
|
|
err = i.bootloader.Install(
|
|
i.snapshot.WorkDir,
|
|
i.spec.Partitions.Boot.MountPoint,
|
|
)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed installing grub: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.InstallGrub)
|
|
}
|
|
|
|
err = i.installChrootHook(cnst.AfterInstallChrootHook, cnst.WorkingImgDir)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed after-install-chroot hook: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.HookAfterInstallChroot)
|
|
}
|
|
err = i.installHook(cnst.AfterInstallHook)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed after-install hook: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.HookAfterInstall)
|
|
}
|
|
|
|
grubVars := i.spec.GetGrubLabels()
|
|
err = i.bootloader.SetPersistentVariables(
|
|
filepath.Join(i.spec.Partitions.Boot.MountPoint, cnst.GrubOEMEnv),
|
|
grubVars,
|
|
)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed setting GRUB labels: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.SetGrubVariables)
|
|
}
|
|
|
|
// Installation rebrand (only grub for now)
|
|
err = i.bootloader.SetDefaultEntry(
|
|
i.spec.Partitions.Boot.MountPoint,
|
|
cnst.WorkingImgDir,
|
|
i.spec.GrubDefEntry,
|
|
)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("failed setting defaut GRUB entry: %v", err)
|
|
return elementalError.NewFromError(err, elementalError.SetDefaultGrubEntry)
|
|
}
|
|
return nil
|
|
}
|