From 33c8e8f29ce930672367e08f37e3444120ab05b9 Mon Sep 17 00:00:00 2001 From: Itxaka Date: Fri, 2 Feb 2024 13:20:06 +0100 Subject: [PATCH] Add reset for uki (#221) --- internal/agent/hooks/hook.go | 6 -- internal/agent/reset.go | 138 +++++++++++++++++++++++++++-------- internal/agent/upgrade.go | 49 +++++-------- main.go | 7 +- pkg/config/spec.go | 31 +++++++- pkg/elemental/elemental.go | 9 ++- pkg/types/v1/config.go | 7 +- pkg/uki/reset.go | 52 ++++++++++--- pkg/uki/upgrade.go | 3 +- 9 files changed, 215 insertions(+), 87 deletions(-) diff --git a/internal/agent/hooks/hook.go b/internal/agent/hooks/hook.go index a4e47326..1957eba1 100644 --- a/internal/agent/hooks/hook.go +++ b/internal/agent/hooks/hook.go @@ -35,12 +35,6 @@ var AfterUkiInstall = []Interface{ &KcryptUKI{}, } -// AfterUkiReset sets which Hooks to run after uki runs the install action -var AfterUkiReset = []Interface{} - -// AfterUkiUpgrade sets which Hooks to run after uki runs the install action -var AfterUkiUpgrade = []Interface{} - func Run(c config.Config, spec v1.Spec, hooks ...Interface) error { for _, h := range hooks { if err := h.Run(c, spec); err != nil { diff --git a/internal/agent/reset.go b/internal/agent/reset.go index e6aba47d..9ac43f44 100644 --- a/internal/agent/reset.go +++ b/internal/agent/reset.go @@ -3,7 +3,9 @@ package agent import ( "encoding/json" "fmt" - "os" + "github.com/kairos-io/kairos-agent/v2/pkg/uki" + internalutils "github.com/kairos-io/kairos-agent/v2/pkg/utils" + "strings" "sync" "time" @@ -20,13 +22,81 @@ import ( "github.com/mudler/go-pluggable" ) -func Reset(reboot, unattended bool, dir ...string) error { +func Reset(reboot, unattended, resetOem bool, dir ...string) error { + // In both cases we want + if internalutils.UkiBootMode() == internalutils.UkiHDD { + return resetUki(reboot, unattended, resetOem, dir...) + } else if internalutils.UkiBootMode() == internalutils.UkiRemovableMedia { + return fmt.Errorf("reset is not supported on removable media, please run reset from the installed system recovery entry") + } else { + return reset(reboot, unattended, resetOem, dir...) + } +} + +func reset(reboot, unattended, resetOem bool, dir ...string) error { + cfg, err := sharedReset(reboot, unattended, resetOem, dir...) + if err != nil { + return err + } + // Load the installation Config from the cloud-config data + resetSpec, err := config.ReadResetSpecFromConfig(cfg) + if err != nil { + return err + } + + err = resetSpec.Sanitize() + if err != nil { + return err + } + + resetAction := action.NewResetAction(cfg, resetSpec) + if err = resetAction.Run(); err != nil { + cfg.Logger.Errorf("failed to reset: %s", err) + return err + } + + bus.Manager.Publish(sdk.EventAfterReset, sdk.EventPayload{}) //nolint:errcheck + + return hook.Run(*cfg, resetSpec, hook.AfterReset...) +} + +func resetUki(reboot, unattended, resetOem bool, dir ...string) error { + cfg, err := sharedReset(reboot, unattended, resetOem, dir...) + if err != nil { + return err + } + // Load the installation Config from the cloud-config data + resetSpec, err := config.ReadUkiResetSpecFromConfig(cfg) + if err != nil { + return err + } + + err = resetSpec.Sanitize() + if err != nil { + return err + } + + resetAction := uki.NewResetAction(cfg, resetSpec) + if err = resetAction.Run(); err != nil { + cfg.Logger.Errorf("failed to reset uki: %s", err) + return err + } + + bus.Manager.Publish(sdk.EventAfterReset, sdk.EventPayload{}) //nolint:errcheck + + return hook.Run(*cfg, resetSpec, hook.AfterReset...) +} + +// sharedReset is the common reset code for both uki and non-uki +// sets the config, runs the event handler, publish the envent and gets the config +func sharedReset(reboot, unattended, resetOem bool, dir ...string) (c *config.Config, err error) { bus.Manager.Initialize() + var optionsFromEvent map[string]string // This config is only for reset branding. agentConfig, err := LoadConfig() if err != nil { - return err + return c, err } if !unattended { @@ -58,8 +128,6 @@ func Reset(reboot, unattended bool, dir ...string) error { ensureDataSourceReady() - optionsFromEvent := map[string]string{} - // This gets the options from an event that can be sent by anyone. // This should override the default config as it's much more dynamic bus.Manager.Response(sdk.EventBeforeReset, func(p *pluggable.Plugin, r *pluggable.EventResponse) { @@ -71,49 +139,59 @@ func Reset(reboot, unattended bool, dir ...string) error { bus.Manager.Publish(sdk.EventBeforeReset, sdk.EventPayload{}) //nolint:errcheck - c, err := config.Scan(collector.Directories(dir...)) - if err != nil { - return err - } + // Prepare a config from the cli flags + r := ExtraConfigReset{} + r.Reset.ResetOem = resetOem - utils.SetEnv(c.Env) + if resetOem { + r.Reset.ResetOem = true + } - // Load the installation Config from the cloud-config data - resetSpec, err := config.ReadResetSpecFromConfig(c) - if err != nil { - return err + if reboot { + r.Reset.Reboot = true } + // Override the config with the event options // Go over the possible options sent via event if len(optionsFromEvent) > 0 { if p := optionsFromEvent["reset-persistent"]; p != "" { - resetSpec.FormatPersistent = p == "true" + r.Reset.ResetPersistent = p == "true" } if o := optionsFromEvent["reset-oem"]; o != "" { - resetSpec.FormatOEM = o == "true" - } - if s := optionsFromEvent["strict"]; s != "" { - c.Strict = s == "true" + r.Reset.ResetOem = o == "true" } } - // Override with flags - if reboot { - resetSpec.Reboot = reboot + d, err := json.Marshal(r) + if err != nil { + c.Logger.Errorf("failed to marshal reset cmdline flags/event options: %s", err) + return c, err } + cliConf := string(d) - err = resetSpec.Sanitize() + // cliconf goes last so it can override the rest of the config files + c, err = config.Scan(collector.Directories(dir...), collector.Readers(strings.NewReader(cliConf))) if err != nil { - return err + return c, err } - resetAction := action.NewResetAction(c, resetSpec) - if err := resetAction.Run(); err != nil { - fmt.Println(err) - os.Exit(1) + // Set strict validation from the event + if len(optionsFromEvent) > 0 { + if s := optionsFromEvent["strict"]; s != "" { + c.Strict = s == "true" + } } - bus.Manager.Publish(sdk.EventAfterReset, sdk.EventPayload{}) //nolint:errcheck + utils.SetEnv(c.Env) + + return c, nil +} - return hook.Run(*c, resetSpec, hook.AfterReset...) +// ExtraConfigReset is the struct that holds the reset options that come from flags and events +type ExtraConfigReset struct { + Reset struct { + ResetOem bool `json:"reset-oem,omitempty"` + ResetPersistent bool `json:"reset-persistent,omitempty"` + Reboot bool `json:"reboot,omitempty"` + } `json:"reset"` } diff --git a/internal/agent/upgrade.go b/internal/agent/upgrade.go index 440f245f..a7e81306 100644 --- a/internal/agent/upgrade.go +++ b/internal/agent/upgrade.go @@ -93,14 +93,6 @@ func upgrade(source string, force, strictValidations bool, dirs []string, preRel return err } - if upgradeSpec.Reboot { - utils.Reboot() - } - - if upgradeSpec.PowerOff { - utils.PowerOFF() - } - return hook.Run(*c, upgradeSpec, hook.AfterUpgrade...) } @@ -138,24 +130,16 @@ func newerReleases() (versioneer.TagList, error) { if err != nil { return tagList, err } - //fmt.Printf("tagList.OtherAnyVersion() = %#v\n", tagList.OtherAnyVersion().Tags) - //fmt.Printf("tagList.Images() = %#v\n", tagList.Images().Tags) - // fmt.Println("Tags") - // tagList.NewerAnyVersion().Print() - // fmt.Println("---------------------------") - return tagList.NewerAnyVersion().RSorted(), nil } // generateUpgradeConfForCLIArgs creates a kairos configuration for `--source` and `--recovery` // command line arguments. It will be added to the rest of the configurations. func generateUpgradeConfForCLIArgs(source string, upgradeRecovery bool) (string, error) { - upgrade := map[string](map[string]interface{}){ - "upgrade": {}, - } + upgradeConfig := ExtraConfigUpgrade{} if upgradeRecovery { - upgrade["upgrade"]["recovery"] = "true" + upgradeConfig.Upgrade.Recovery = true } // Set uri both for active and recovery because we don't know what we are @@ -164,12 +148,8 @@ func generateUpgradeConfForCLIArgs(source string, upgradeRecovery bool) (string, // have access to that yet, we just set both uri values which shouldn't matter // anyway, the right one will be used later in the process. if source != "" { - upgrade["upgrade"]["recovery-system"] = map[string]string{ - "uri": source, - } - upgrade["upgrade"]["system"] = map[string]string{ - "uri": source, - } + upgradeConfig.Upgrade.RecoverySystem.URI = source + upgradeConfig.Upgrade.System.URI = source } d, err := json.Marshal(upgrade) @@ -254,13 +234,18 @@ func upgradeUki(source string, dirs []string, strictValidations bool) error { return err } - if upgradeSpec.Reboot { - utils.Reboot() - } - - if upgradeSpec.PowerOff { - utils.PowerOFF() - } - return hook.Run(*c, upgradeSpec, hook.AfterUpgrade...) } + +// ExtraConfigUpgrade is the struct that holds the upgrade options that come from flags and events +type ExtraConfigUpgrade struct { + Upgrade struct { + Recovery bool `json:"recovery,omitempty"` + RecoverySystem struct { + URI string `json:"uri,omitempty"` + } `json:"recovery-system,omitempty"` + System struct { + URI string `json:"uri,omitempty"` + } `json:"system,omitempty"` + } `json:"upgrade,omitempty"` +} diff --git a/main.go b/main.go index a81f7026..418bb282 100644 --- a/main.go +++ b/main.go @@ -564,6 +564,10 @@ This command is meant to be used from the boot GRUB menu, but can likely be used Name: "unattended", Usage: "Do not wait for user input and provide ttys after reset. Also sets the fast mode (do not wait 60 seconds before reset)", }, + &cli.BoolFlag{ + Name: "reset-oem", + Usage: "Reset the OEM partition. Warning: this will delete any persistent data on the OEM partition.", + }, }, Before: func(c *cli.Context) error { return checkRoot() @@ -571,8 +575,9 @@ This command is meant to be used from the boot GRUB menu, but can likely be used Action: func(c *cli.Context) error { reboot := c.Bool("reboot") unattended := c.Bool("unattended") + resetOem := c.Bool("reset-oem") - return agent.Reset(reboot, unattended, configScanDir...) + return agent.Reset(reboot, unattended, resetOem, configScanDir...) }, Usage: "Starts kairos reset mode", Description: ` diff --git a/pkg/config/spec.go b/pkg/config/spec.go index ac58a0fc..05ef4c76 100644 --- a/pkg/config/spec.go +++ b/pkg/config/spec.go @@ -507,6 +507,33 @@ func ReadResetSpecFromConfig(c *Config) (*v1.ResetSpec, error) { return resetSpec, nil } +func NewUkiResetSpec(cfg *Config) (spec *v1.ResetUkiSpec, err error) { + spec = &v1.ResetUkiSpec{ + FormatPersistent: true, // Persistent is formatted by default + Partitions: v1.ElementalPartitions{}, + } + + _, ukiBootMode := cfg.Fs.Stat("/run/cos/uki_boot_mode") + if !BootedFrom(cfg.Runner, "rd.immucore.uki") && ukiBootMode == nil { + return spec, fmt.Errorf("uki reset can only be called from the recovery installed system") + } + + // Fill persistent partition + spec.Partitions.Persistent = partitions.GetPartitionViaDM(cfg.Fs, constants.PersistentLabel) + spec.Partitions.OEM = partitions.GetPartitionViaDM(cfg.Fs, constants.OEMLabel) + + if spec.Partitions.Persistent == nil { + return spec, fmt.Errorf("persistent partition not found") + } + if spec.Partitions.OEM == nil { + return spec, fmt.Errorf("oem partition not found") + } + + // Fill oem partition + err = unmarshallFullSpec(cfg, "reset", spec) + return spec, err +} + // ReadInstallSpecFromConfig will return a proper v1.InstallSpec based on an agent Config func ReadInstallSpecFromConfig(c *Config) (*v1.InstallSpec, error) { sp, err := ReadSpecFromCloudConfig(c, "install") @@ -568,7 +595,6 @@ func NewUkiInstallSpec(cfg *Config) (*v1.InstallUkiSpec, error) { Flags: []string{}, } - // TODO: Which key to use? install or install-uki? err := unmarshallFullSpec(cfg, "install", spec) // TODO: Get the actual source size to calculate the image size and partitions size for at least 3 UKI images // Add default values for the skip partitions for our default entries @@ -760,8 +786,7 @@ func ReadSpecFromCloudConfig(r *Config, spec string) (v1.Spec, error) { case "install-uki": sp, err = NewUkiInstallSpec(r) case "reset-uki": - // TODO: Fill with proper defaults - sp = &v1.ResetUkiSpec{} + sp, err = NewUkiResetSpec(r) case "upgrade-uki": sp, err = NewUkiUpgradeSpec(r) default: diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index 8592b85a..80d65fce 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -43,7 +43,14 @@ func NewElemental(config *agentConfig.Config) *Elemental { // FormatPartition will format an already existing partition func (e *Elemental) FormatPartition(part *v1.Partition, opts ...string) error { - e.config.Logger.Infof("Formatting '%s' partition", part.Name) + var name string + // Nice display name for logs + if part.Name == "" { + name = part.FilesystemLabel + } else { + name = part.Name + } + e.config.Logger.Infof("Formatting '%s' partition", name) return partitioner.FormatDevice(e.config.Runner, part.Path, part.FS, part.FilesystemLabel, opts...) } diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index 906d670a..ecbf2a1c 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -541,8 +541,11 @@ func (i *UpgradeUkiSpec) ShouldReboot() bool { return i.Reboot } func (i *UpgradeUkiSpec) ShouldShutdown() bool { return i.PowerOff } type ResetUkiSpec struct { - Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` - PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + FormatPersistent bool `yaml:"reset-persistent,omitempty" mapstructure:"reset-persistent"` + FormatOEM bool `yaml:"reset-oem,omitempty" mapstructure:"reset-oem"` + Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` + PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + Partitions ElementalPartitions } func (i *ResetUkiSpec) Sanitize() error { diff --git a/pkg/uki/reset.go b/pkg/uki/reset.go index e405a6e6..a8933fd6 100644 --- a/pkg/uki/reset.go +++ b/pkg/uki/reset.go @@ -1,11 +1,12 @@ package uki import ( - hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" "github.com/kairos-io/kairos-agent/v2/pkg/config" + "github.com/kairos-io/kairos-agent/v2/pkg/elemental" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils" events "github.com/kairos-io/kairos-sdk/bus" + "github.com/kairos-io/kairos-sdk/utils" ) type ResetAction struct { @@ -17,19 +18,50 @@ func NewResetAction(cfg *config.Config, spec *v1.ResetUkiSpec) *ResetAction { return &ResetAction{cfg: cfg, spec: spec} } -func (i *ResetAction) Run() (err error) { +func (r *ResetAction) Run() (err error) { // Run pre-install stage - _ = elementalUtils.RunStage(i.cfg, "kairos-uki-reset.pre") + _ = elementalUtils.RunStage(r.cfg, "kairos-uki-reset.pre") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.reset.pre.hook") - // Get source (from spec?) - // Copy the efi file into the proper dir - // Remove all boot manager entries? - // Create boot manager entry - // Set default entry to the one we just created + e := elemental.NewElemental(r.cfg) + cleanup := utils.NewCleanStack() + defer func() { err = cleanup.Cleanup(err) }() - _ = elementalUtils.RunStage(i.cfg, "kairos-uki-reset.after") + // Unmount partitions if any is already mounted before formatting + err = e.UnmountPartitions(r.spec.Partitions.PartitionsByMountPoint(true)) + if err != nil { + return err + } + + // Reformat persistent partition + if r.spec.FormatPersistent { + persistent := r.spec.Partitions.Persistent + if persistent != nil { + err = e.FormatPartition(persistent) + if err != nil { + return err + } + } + } + + // Reformat OEM + if r.spec.FormatOEM { + oem := r.spec.Partitions.OEM + if oem != nil { + err = e.FormatPartition(oem) + if err != nil { + return err + } + } + } + + _ = elementalUtils.RunStage(r.cfg, "kairos-uki-reset.after") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.reset.after.hook") //nolint:errcheck - return hook.Run(*i.cfg, i.spec, hook.AfterUkiReset...) + // Do not reboot/poweroff on cleanup errors + err = cleanup.Cleanup(err) + if err != nil { + return err + } + return nil } diff --git a/pkg/uki/upgrade.go b/pkg/uki/upgrade.go index e1b60272..ee293a01 100644 --- a/pkg/uki/upgrade.go +++ b/pkg/uki/upgrade.go @@ -2,7 +2,6 @@ package uki import ( "github.com/Masterminds/semver/v3" - hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" "github.com/kairos-io/kairos-agent/v2/pkg/config" "github.com/kairos-io/kairos-agent/v2/pkg/constants" "github.com/kairos-io/kairos-agent/v2/pkg/elemental" @@ -81,7 +80,7 @@ func (i *UpgradeAction) Run() (err error) { _ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck - return hook.Run(*i.cfg, i.spec, hook.AfterUkiUpgrade...) + return nil } func (i *UpgradeAction) getEfiFiles() ([]string, error) {