From 3246717d70916b7f6b6567bd5664cb870c331a92 Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Mon, 26 Feb 2024 13:45:40 +0100 Subject: [PATCH] Implement to upgrade-recovery separate command Signed-off-by: Andrea Mazzotti --- cmd/config/config.go | 23 ++++++ cmd/upgrade-recovery.go | 85 ++++++++++++++++++++++ pkg/action/upgrade-recovery.go | 126 +++++++++++++++++++++++++++------ pkg/action/upgrade.go | 34 ++++----- pkg/constants/constants.go | 13 ++++ pkg/types/v1/config.go | 40 ++++++++--- 6 files changed, 274 insertions(+), 47 deletions(-) create mode 100644 cmd/upgrade-recovery.go diff --git a/cmd/config/config.go b/cmd/config/config.go index 3af436b925f..220579ab1ab 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -452,6 +452,29 @@ func ReadUpgradeSpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.UpgradeSpec, er return upgrade, err } +func ReadUpgradeRecoverySpec(r *v1.RunConfig, flags *pflag.FlagSet) (*v1.UpgradeSpec, error) { + upgrade, err := config.NewUpgradeSpec(r.Config) + if err != nil { + return nil, fmt.Errorf("failed initializing upgrade recovery spec: %v", err) + } + vp := viper.Sub("upgrade-recovery") + if vp == nil { + vp = viper.New() + } + // Bind upgrade-recovery cmd flags + bindGivenFlags(vp, flags) + // Bind upgrade-recovery env vars + viperReadEnv(vp, "UPGRADE_RECOVERY", constants.GetUpgradeRecoveryKeyEnvMap()) + + err = vp.Unmarshal(upgrade, setDecoder, decodeHook) + if err != nil { + r.Logger.Warnf("error unmarshalling UpgradeSpec: %s", err) + } + err = upgrade.SanitizeForRecoveryOnly() + r.Logger.Debugf("Loaded upgrade UpgradeSpec: %s", litter.Sdump(upgrade)) + return upgrade, err +} + func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error) { iso := config.NewISO() vp := viper.Sub("iso") diff --git a/cmd/upgrade-recovery.go b/cmd/upgrade-recovery.go new file mode 100644 index 00000000000..f4fea50bae4 --- /dev/null +++ b/cmd/upgrade-recovery.go @@ -0,0 +1,85 @@ +/* +Copyright © 2022 - 2024 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 ( + "os/exec" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/rancher/elemental-toolkit/cmd/config" + "github.com/rancher/elemental-toolkit/pkg/action" + elementalError "github.com/rancher/elemental-toolkit/pkg/error" + v1 "github.com/rancher/elemental-toolkit/pkg/types/v1" +) + +// NewUpgradeCmd returns a new instance of the upgrade subcommand and appends it to +// the root command. requireRoot is to initiate it with or without the CheckRoot +// pre-run check. This method is mostly used for testing purposes. +func NewUpgradeRecoveryCmd(root *cobra.Command, addCheckRoot bool) *cobra.Command { + c := &cobra.Command{ + Use: "upgrade-recovery", + Short: "Upgrade the Recovery system", + Args: cobra.ExactArgs(0), + PreRunE: func(_ *cobra.Command, _ []string) error { + if addCheckRoot { + return CheckRoot() + } + return nil + }, + + RunE: func(cmd *cobra.Command, _ []string) error { + path, err := exec.LookPath("mount") + if err != nil { + return err + } + mounter := v1.NewMounter(path) + + 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) + } + + // Set this after parsing of the flags, so it fails on parsing and prints usage properly + cmd.SilenceUsage = true + cmd.SilenceErrors = true // Do not propagate errors down the line, we control them + + spec, err := config.ReadUpgradeRecoverySpec(cfg, cmd.Flags()) + if err != nil { + cfg.Logger.Errorf("Invalid upgrade-recovery command setup %v", err) + return elementalError.NewFromError(err, elementalError.ReadingSpecConfig) + } + + cfg.Logger.Infof("Upgrade Recovery called") + upgrade, err := action.NewUpgradeRecoveryAction(cfg, spec, action.WithUpdateInstallState(true)) + if err != nil { + cfg.Logger.Errorf("failed to initialize upgrade-recovery action: %v", err) + return err + } + + return upgrade.Run() + }, + } + root.AddCommand(c) + addRecoverySystemFlag(c) + return c +} + +// register the subcommand into rootCmd +var _ = NewUpgradeRecoveryCmd(rootCmd, true) diff --git a/pkg/action/upgrade-recovery.go b/pkg/action/upgrade-recovery.go index 9baafb6b03d..27c2bc28378 100644 --- a/pkg/action/upgrade-recovery.go +++ b/pkg/action/upgrade-recovery.go @@ -19,6 +19,7 @@ package action import ( "fmt" "path/filepath" + "time" "github.com/rancher/elemental-toolkit/pkg/constants" "github.com/rancher/elemental-toolkit/pkg/elemental" @@ -27,14 +28,41 @@ import ( "github.com/rancher/elemental-toolkit/pkg/utils" ) -// UpgradeRecoveryAction represents the struct that will run the upgrade from start to finish +// UpgradeRecoveryAction represents the struct that will run the recovery upgrade from start to finish type UpgradeRecoveryAction struct { - cfg *v1.RunConfig - spec *v1.UpgradeSpec + cfg *v1.RunConfig + spec *v1.UpgradeSpec + updateInstallState bool } -func NewUpgradeRecoveryAction(config *v1.RunConfig, spec *v1.UpgradeSpec) *UpgradeRecoveryAction { - return &UpgradeRecoveryAction{cfg: config, spec: spec} +type UpgradeRecoveryActionOption func(r *UpgradeRecoveryAction) error + +func WithUpdateInstallState(updateInstallState bool) func(u *UpgradeRecoveryAction) error { + return func(u *UpgradeRecoveryAction) error { + u.updateInstallState = updateInstallState + return nil + } +} + +func NewUpgradeRecoveryAction(config *v1.RunConfig, spec *v1.UpgradeSpec, opts ...UpgradeRecoveryActionOption) (*UpgradeRecoveryAction, error) { + var err error + + u := &UpgradeRecoveryAction{cfg: config, spec: spec} + + for _, o := range opts { + err = o(u) + if err != nil { + config.Logger.Errorf("error applying config option: %s", err.Error()) + return nil, err + } + } + + if elemental.IsRecoveryMode(config.Config) { + config.Logger.Errorf("Upgrading recovery image from the recovery system itself is not supported") + return nil, fmt.Errorf("Not supported") + } + + return u, nil } func (u UpgradeRecoveryAction) Info(s string, args ...interface{}) { @@ -54,6 +82,14 @@ func (u *UpgradeRecoveryAction) mountRWPartitions(cleanup *utils.CleanStack) err return fmt.Errorf("Can not upgrade recovery from recovery partition") } + if u.updateInstallState { + umount, err := elemental.MountRWPartition(u.cfg.Config, u.spec.Partitions.State) + if err != nil { + return elementalError.NewFromError(err, elementalError.MountStatePartition) + } + cleanup.Push(umount) + } + umount, err := elemental.MountRWPartition(u.cfg.Config, u.spec.Partitions.Recovery) if err != nil { return elementalError.NewFromError(err, elementalError.MountRecoveryPartition) @@ -63,6 +99,47 @@ func (u *UpgradeRecoveryAction) mountRWPartitions(cleanup *utils.CleanStack) err return nil } +func (u *UpgradeRecoveryAction) upgradeInstallStateYaml() error { + if u.spec.Partitions.Recovery == nil || u.spec.Partitions.State == nil { + return fmt.Errorf("undefined state or recovery partition") + } + + // A nil State should never be the case. + // However if it happens we need to abort, we we can't recreate + // a correct install state when upgrading recovery only. + if u.spec.State == nil { + return fmt.Errorf("Could not load current install state") + } + + u.spec.State.Date = time.Now().Format(time.RFC3339) + + recoveryPart := u.spec.State.Partitions[constants.RecoveryPartName] + if recoveryPart == nil { + recoveryPart = &v1.PartitionState{ + FSLabel: u.spec.Partitions.Recovery.FilesystemLabel, + RecoveryImage: &v1.SystemState{ + FS: u.spec.RecoverySystem.FS, + Label: u.spec.RecoverySystem.Label, + Source: u.spec.RecoverySystem.Source, + Digest: u.spec.RecoverySystem.Source.GetDigest(), + }, + } + u.spec.State.Partitions[constants.RecoveryPartName] = recoveryPart + } + + // Hack to ensure we are not using / or /.snapshots mountpoints. Btrfs based deployments + // mount state partition into multiple locations + statePath := filepath.Join(u.spec.Partitions.State.MountPoint, constants.InstallStateFile) + if u.spec.Partitions.State.MountPoint == "/" || u.spec.Partitions.State.MountPoint == "/.snapshots" { + statePath = filepath.Join(constants.RunningStateDir, constants.InstallStateFile) + } + + return u.cfg.WriteInstallState( + u.spec.State, statePath, + filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.InstallStateFile), + ) +} + func (u *UpgradeRecoveryAction) Run() (err error) { cleanup := utils.NewCleanStack() defer func() { @@ -76,27 +153,32 @@ func (u *UpgradeRecoveryAction) Run() (err error) { } // Upgrade recovery - if u.spec.RecoveryUpgrade { - err = elemental.DeployImage(u.cfg.Config, &u.spec.RecoverySystem) - if err != nil { - u.cfg.Logger.Error("failed deploying recovery image") - return elementalError.NewFromError(err, elementalError.DeployImage) - } - recoveryFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.RecoveryImgFile) - transitionFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.TransitionImgFile) - if ok, _ := utils.Exists(u.cfg.Fs, recoveryFile); ok { - err = u.cfg.Fs.Remove(recoveryFile) - if err != nil { - u.Error("failed removing old recovery image") - return err - } - } - err = u.cfg.Fs.Rename(transitionFile, recoveryFile) + err = elemental.DeployImage(u.cfg.Config, &u.spec.RecoverySystem) + if err != nil { + u.cfg.Logger.Error("failed deploying recovery image") + return elementalError.NewFromError(err, elementalError.DeployImage) + } + recoveryFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.RecoveryImgFile) + transitionFile := filepath.Join(u.spec.Partitions.Recovery.MountPoint, constants.TransitionImgFile) + if ok, _ := utils.Exists(u.cfg.Fs, recoveryFile); ok { + err = u.cfg.Fs.Remove(recoveryFile) if err != nil { - u.Error("failed renaming transition recovery image") + u.Error("failed removing old recovery image") return err } } + err = u.cfg.Fs.Rename(transitionFile, recoveryFile) + if err != nil { + u.Error("failed renaming transition recovery image") + return err + } + + // Update state.yaml file on recovery and state partitions + err = u.upgradeInstallStateYaml() + if err != nil { + u.Error("failed upgrading installation metadata") + return err + } u.Info("Recovery upgrade completed") diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index face8b9836f..5523e515c88 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -264,20 +264,18 @@ func (u *UpgradeAction) Run() (err error) { cleanup.PushErrorOnly(func() error { return u.snapshotter.CloseTransactionOnError(u.snapshot) }) // Deploy system image - if !u.spec.RecoveryOnlyUpgrade { - err = elemental.DumpSource(u.cfg.Config, u.snapshot.WorkDir, u.spec.System) - if err != nil { - u.cfg.Logger.Errorf("failed deploying source: %s", u.spec.System.String()) - return elementalError.NewFromError(err, elementalError.DumpSource) - } + err = elemental.DumpSource(u.cfg.Config, u.snapshot.WorkDir, u.spec.System) + if err != nil { + u.cfg.Logger.Errorf("failed deploying source: %s", u.spec.System.String()) + return elementalError.NewFromError(err, elementalError.DumpSource) + } - // Fine tune the dumped tree - u.cfg.Logger.Info("Fine tune the dumped root tree") - err = u.refineDeployment() - if err != nil { - u.cfg.Logger.Error("failed refining system root tree") - return err - } + // Fine tune the dumped tree + u.cfg.Logger.Info("Fine tune the dumped root tree") + err = u.refineDeployment() + if err != nil { + u.cfg.Logger.Error("failed refining system root tree") + return err } // Manage legacy recovery. This logic should be removed once toolkit v1.1 gets unsupported. @@ -301,7 +299,7 @@ func (u *UpgradeAction) Run() (err error) { } // Upgrade recovery - if u.spec.RecoveryUpgrade || u.spec.RecoveryOnlyUpgrade { + if u.spec.RecoveryUpgrade { recoverySystem := &u.spec.RecoverySystem u.cfg.Logger.Info("Deploying recovery system") if recoverySystem.Source.String() == u.spec.System.String() { @@ -312,9 +310,13 @@ func (u *UpgradeAction) Run() (err error) { } recoverySystem.Source.SetDigest(u.spec.System.GetDigest()) } - upgradeRecoveryAction := NewUpgradeRecoveryAction(u.cfg, u.spec) + upgradeRecoveryAction, err := NewUpgradeRecoveryAction(u.cfg, u.spec, WithUpdateInstallState(false)) + if err != nil { + u.Error("Could not initialize Recovery upgrade: %s", err) + return elementalError.NewFromError(err, elementalError.UpgradeRecovery) + } if err := upgradeRecoveryAction.Run(); err != nil { - u.Error("Upgrading Recovery: %s", err) + u.Error("Could not upgrade Recovery: %s", err) return elementalError.NewFromError(err, elementalError.UpgradeRecovery) } } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 1198909e947..ecfeaadf254 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -333,9 +333,22 @@ func GetResetKeyEnvMap() map[string]string { // GetUpgradeKeyEnvMap returns environment variable bindings to UpgradeSpec data func GetUpgradeKeyEnvMap() map[string]string { return map[string]string{ +<<<<<<< HEAD "recovery": "RECOVERY", "system": "SYSTEM", "recovery-system.uri": "RECOVERY_SYSTEM", +======= + "recovery": "RECOVERY", + "system": "SYSTEM", + "recovery-system": "RECOVERY_SYSTEM", +>>>>>>> 49451b499 (Implement to upgrade-recovery separate command) + } +} + +// GetUpgradeRecoveryKeyEnvMap returns environment variable bindings to UpgradeSpec data +func GetUpgradeRecoveryKeyEnvMap() map[string]string { + return map[string]string{ + "recovery-system": "RECOVERY_SYSTEM", } } diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index a7171eced3a..89fded5b8f0 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -359,14 +359,13 @@ func (r *ResetSpec) Sanitize() error { } type UpgradeSpec struct { - RecoveryOnlyUpgrade bool `yaml:"recovery-only,omitempty" mapstructure:"recovery-only"` - RecoveryUpgrade bool `yaml:"recovery,omitempty" mapstructure:"recovery"` - System *ImageSource `yaml:"system,omitempty" mapstructure:"system"` - RecoverySystem Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` - GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` - BootloaderUpgrade bool `yaml:"bootloader,omitempty" mapstructure:"bootloader"` - Partitions ElementalPartitions - State *InstallState + RecoveryUpgrade bool `yaml:"recovery,omitempty" mapstructure:"recovery"` + System *ImageSource `yaml:"system,omitempty" mapstructure:"system"` + RecoverySystem Image `yaml:"recovery-system,omitempty" mapstructure:"recovery-system"` + GrubDefEntry string `yaml:"grub-entry-name,omitempty" mapstructure:"grub-entry-name"` + BootloaderUpgrade bool `yaml:"bootloader,omitempty" mapstructure:"bootloader"` + Partitions ElementalPartitions + State *InstallState } // Sanitize checks the consistency of the struct, returns error @@ -379,7 +378,7 @@ func (u *UpgradeSpec) Sanitize() error { return fmt.Errorf("undefined upgrade source") } - if u.RecoveryUpgrade || u.RecoveryOnlyUpgrade { + if u.RecoveryUpgrade { if u.Partitions.Recovery == nil || u.Partitions.Recovery.MountPoint == "" { return fmt.Errorf("undefined recovery partition") } @@ -404,6 +403,29 @@ func (u *UpgradeSpec) Sanitize() error { return nil } +// SanitizeForRecoveryOnly sanitizes UpgradeSpec when upgrading recovery only. +func (u *UpgradeSpec) SanitizeForRecoveryOnly() error { + if u.Partitions.State == nil || u.Partitions.State.MountPoint == "" { + return fmt.Errorf("undefined state partition") + } + + if u.Partitions.Recovery == nil || u.Partitions.Recovery.MountPoint == "" { + return fmt.Errorf("undefined recovery partition") + } + if u.RecoverySystem.Source.IsEmpty() { + return fmt.Errorf("undefined upgrade-recovery source") + } + + // Set default label for non squashfs images + if u.RecoverySystem.FS != constants.SquashFs && u.RecoverySystem.Label == "" { + u.RecoverySystem.Label = constants.SystemLabel + } else if u.RecoverySystem.FS == constants.SquashFs { + u.RecoverySystem.Label = "" + } + + return nil +} + // Partition struct represents a partition with its commonly configurable values, size in MiB type Partition struct { Name string