Add reset for uki (#221)

This commit is contained in:
Itxaka 2024-02-02 13:20:06 +01:00 committed by GitHub
parent 7efda3d50e
commit 33c8e8f29c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 219 additions and 91 deletions

View File

@ -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 {

View File

@ -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
if resetOem {
r.Reset.ResetOem = true
}
utils.SetEnv(c.Env)
// 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"
r.Reset.ResetOem = o == "true"
}
}
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)
// 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 c, err
}
// Set strict validation from the event
if len(optionsFromEvent) > 0 {
if s := optionsFromEvent["strict"]; s != "" {
c.Strict = s == "true"
}
}
// Override with flags
if reboot {
resetSpec.Reboot = reboot
}
utils.SetEnv(c.Env)
err = resetSpec.Sanitize()
if err != nil {
return err
}
resetAction := action.NewResetAction(c, resetSpec)
if err := resetAction.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
bus.Manager.Publish(sdk.EventAfterReset, sdk.EventPayload{}) //nolint:errcheck
return hook.Run(*c, resetSpec, hook.AfterReset...)
return c, nil
}
// 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"`
}

View File

@ -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"`
}

View File

@ -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: `

View File

@ -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:

View File

@ -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...)
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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) {