diff --git a/go.mod b/go.mod index 231b249..e982a03 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/jaypipes/ghw v0.12.0 github.com/joho/godotenv v1.5.1 - github.com/kairos-io/kairos-sdk v0.0.20 github.com/kairos-io/kcrypt v0.8.0 + github.com/kairos-io/kairos-sdk v0.0.20 github.com/labstack/echo/v4 v4.11.1 github.com/mitchellh/mapstructure v1.5.0 github.com/mudler/go-nodepair v0.0.0-20221223092639-ba399a66fdfb @@ -161,6 +161,7 @@ require ( github.com/swaggest/refl v1.3.0 // indirect github.com/tredoe/osutil/v2 v2.0.0-rc.16 // indirect github.com/ulikunitz/xz v0.5.11 // indirect + github.com/urfave/cli v1.22.14 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vbatts/tar-split v0.11.3 // indirect diff --git a/go.sum b/go.sum index c571078..4bef8d0 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= @@ -621,6 +622,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= @@ -639,6 +641,8 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/agent/hooks/kcrypt_uki.go b/internal/agent/hooks/kcrypt_uki.go index bf9f519..fec064b 100644 --- a/internal/agent/hooks/kcrypt_uki.go +++ b/internal/agent/hooks/kcrypt_uki.go @@ -9,12 +9,37 @@ import ( "github.com/kairos-io/kairos-sdk/machine" "github.com/kairos-io/kairos-sdk/utils" kcrypt "github.com/kairos-io/kcrypt/pkg/lib" + "strconv" + "strings" "time" ) type KcryptUKI struct{} func (k KcryptUKI) Run(c config.Config, _ v1.Spec) error { + // pre-check for systemd version, we need something higher or equal to 252 + run, err := utils.SH("systemctl --version | head -1 | awk '{ print $2}'") + systemdVersion := strings.TrimSpace(string(run)) + if err != nil { + c.Logger.Errorf("could not get systemd version: %s", err) + c.Logger.Errorf("could not get systemd version: %s", run) + return err + } + if systemdVersion == "" { + c.Logger.Errorf("could not get systemd version: %s", err) + return err + } + // Change systemdVersion to int value + systemdVersionInt, err := strconv.Atoi(systemdVersion) + if err != nil { + c.Logger.Errorf("could not convert systemd version to int: %s", err) + return err + } + // If systemd version is less than 252 return + if systemdVersionInt < 252 { + c.Logger.Infof("systemd version is %s, we need 252 or higher for encrypting partitions", systemdVersion) + return nil + } // We always encrypt OEM and PERSISTENT under UKI // If mounted, unmount it @@ -22,7 +47,7 @@ func (k KcryptUKI) Run(c config.Config, _ v1.Spec) error { _ = machine.Umount(constants.PersistentDir) //nolint:errcheck // Backup oem as we already copied files on there and on luksify it will be wiped - err := machine.Mount("COS_OEM", constants.OEMDir) + err = machine.Mount("COS_OEM", constants.OEMDir) if err != nil { return err } @@ -60,7 +85,6 @@ func (k KcryptUKI) Run(c config.Config, _ v1.Spec) error { c.Logger.Infof("Done encrypting %s", p) } - // Restore OEM err = kcrypt.UnlockAll(true) if err != nil { return err diff --git a/internal/agent/upgrade.go b/internal/agent/upgrade.go index 4f873cf..274caf4 100644 --- a/internal/agent/upgrade.go +++ b/internal/agent/upgrade.go @@ -12,6 +12,7 @@ import ( "github.com/kairos-io/kairos-agent/v2/pkg/action" config "github.com/kairos-io/kairos-agent/v2/pkg/config" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" + "github.com/kairos-io/kairos-agent/v2/pkg/uki" events "github.com/kairos-io/kairos-sdk/bus" "github.com/kairos-io/kairos-sdk/collector" "github.com/kairos-io/kairos-sdk/utils" @@ -185,3 +186,49 @@ func getReleasesFromProvider(includePrereleases bool) ([]string, error) { return result, nil } + +func UkiUpgrade(source string, dirs []string, strictValidations bool) error { + bus.Manager.Initialize() + + cliConf, err := generateUpgradeConfForCLIArgs(source, false) + if err != nil { + return err + } + + c, err := config.Scan(collector.Directories(dirs...), + collector.Readers(strings.NewReader(cliConf)), + collector.StrictValidation(strictValidations)) + if err != nil { + return err + } + + utils.SetEnv(c.Env) + + // Load the upgrade Config from the system + upgradeSpec, err := config.ReadUkiUpgradeFromConfig(c) + if err != nil { + return err + } + + err = upgradeSpec.Sanitize() + if err != nil { + return err + } + + upgradeAction := uki.NewUpgradeAction(c, upgradeSpec) + + err = upgradeAction.Run() + if err != nil { + return err + } + + if upgradeSpec.Reboot { + utils.Reboot() + } + + if upgradeSpec.PowerOff { + utils.PowerOFF() + } + + return hook.Run(*c, upgradeSpec, hook.AfterUpgrade...) +} diff --git a/main.go b/main.go index 4ab7f25..6a51be8 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/kairos-io/kairos-sdk/schema" "github.com/kairos-io/kairos-sdk/state" "github.com/kairos-io/kairos-sdk/versioneer" + "github.com/sanity-io/litter" "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -726,6 +727,12 @@ The validate command expects a configuration file as its only argument. Local fi Name: "device", }, }, + Before: func(c *cli.Context) error { + if c.String("device") == "" { + return fmt.Errorf("on uki, --device flag is required") + } + return nil + }, Action: func(c *cli.Context) error { config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs) if err != nil { @@ -757,22 +764,14 @@ The validate command expects a configuration file as its only argument. Local fi }, }, Before: func(c *cli.Context) error { - return fmt.Errorf("not implemented") + if c.String("source") == "" { + return fmt.Errorf("On uki, source is required") + } + return nil }, Action: func(c *cli.Context) error { - config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation"))) - if err != nil { - return err - } - - // Load the spec from the config - upgradeSpec, err := agentConfig.ReadUkiUpgradeFromConfig(config) - if err != nil { - return err - } - - upgradeAction := uki.NewUpgradeAction(config, upgradeSpec) - return upgradeAction.Run() + source := c.String("source") + return agent.UkiUpgrade(source, configScanDir, c.Bool("strict-validation")) }, }, { @@ -847,6 +846,9 @@ The kairos agent is a component to abstract away node ops, providing a common fe Before: func(c *cli.Context) error { // Set debug from here already, so it's loaded by the ReadConfigRun viper.Set("debug", c.Bool("debug")) + if c.Bool("debug") { + litter.Config.HidePrivateFields = false + } return nil }, Commands: cmds, diff --git a/pkg/config/spec.go b/pkg/config/spec.go index d154950..2a084f9 100644 --- a/pkg/config/spec.go +++ b/pkg/config/spec.go @@ -496,7 +496,7 @@ func NewUkiInstallSpec(cfg *Config) (*v1.InstallUkiSpec, error) { // Calculate the partitions afterwards so they use the image sizes for the final partition sizes spec.Partitions.EFI = &v1.Partition{ FilesystemLabel: constants.EfiLabel, - Size: constants.ImgSize, // TODO: Fix this and set proper size based on the source size + Size: constants.ImgSize * 2, // TODO: Fix this and set proper size based on the source size Name: constants.EfiPartName, FS: constants.EfiFs, MountPoint: constants.EfiDir, @@ -521,7 +521,7 @@ func NewUkiInstallSpec(cfg *Config) (*v1.InstallUkiSpec, error) { // 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 return spec, err } @@ -535,6 +535,28 @@ func ReadUkiInstallSpecFromConfig(c *Config) (*v1.InstallUkiSpec, error) { return installSpec, nil } +func NewUkiUpgradeSpec(cfg *Config) (*v1.UpgradeUkiSpec, error) { + spec := &v1.UpgradeUkiSpec{} + err := unmarshallFullSpec(cfg, "upgrade", spec) + // Get the actual source size to calculate the image size and partitions size + size, err := GetSourceSize(cfg, spec.Active.Source) + if err != nil { + cfg.Logger.Warnf("Failed to infer size for images: %s", err.Error()) + spec.Active.Size = constants.ImgSize + } else { + cfg.Logger.Infof("Setting image size to %dMb", size) + spec.Active.Size = uint(size) + } + + spec.EfiPartition = &v1.Partition{ + FilesystemLabel: constants.EfiLabel, + FS: constants.EfiFs, + Path: constants.UkiEfiDiskByLabel, + MountPoint: constants.UkiEfiDir, + } + return spec, err +} + // ReadUkiUpgradeFromConfig will return a proper v1.UpgradeUkiSpec based on an agent Config func ReadUkiUpgradeFromConfig(c *Config) (*v1.UpgradeUkiSpec, error) { sp, err := ReadSpecFromCloudConfig(c, "upgrade-uki") @@ -653,8 +675,7 @@ func ReadSpecFromCloudConfig(r *Config, spec string) (v1.Spec, error) { // TODO: Fill with proper defaults sp = &v1.ResetUkiSpec{} case "upgrade-uki": - // TODO: Fill with proper defaults - sp = &v1.UpgradeUkiSpec{} + sp, err = NewUkiUpgradeSpec(r) default: return nil, fmt.Errorf("spec not valid: %s", spec) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index d05a0b3..9701920 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -108,8 +108,10 @@ const ( Rsync = "rsync" - UkiSource = "/run/install/uki" - UkiCdromSource = "/run/install/cdrom" + UkiSource = "/run/install/uki" + UkiCdromSource = "/run/install/cdrom" + UkiEfiDir = "/efi" + UkiEfiDiskByLabel = `/dev/disk/by-label/` + EfiLabel ) func GetCloudInitPaths() []string { diff --git a/pkg/types/v1/common.go b/pkg/types/v1/common.go index eeb9302..0715fe0 100644 --- a/pkg/types/v1/common.go +++ b/pkg/types/v1/common.go @@ -35,7 +35,7 @@ const ( // ImageSource represents the source from where an image is created for easy identification type ImageSource struct { - source string + source string `yaml:"source"` srcType string } diff --git a/pkg/types/v1/config.go b/pkg/types/v1/config.go index fa2b186..c3ea08a 100644 --- a/pkg/types/v1/config.go +++ b/pkg/types/v1/config.go @@ -202,7 +202,6 @@ func (u *UpgradeSpec) Sanitize() error { } return nil } - func (u *UpgradeSpec) ShouldReboot() bool { return u.Reboot } func (u *UpgradeSpec) ShouldShutdown() bool { return u.PowerOff } @@ -526,8 +525,10 @@ func (i *InstallUkiSpec) GetPartitions() ElementalPartitions { return i.Partitio func (i *InstallUkiSpec) GetExtraPartitions() PartitionList { return i.ExtraPartitions } type UpgradeUkiSpec struct { - Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` - PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + Active Image `yaml:"system,omitempty" mapstructure:"system"` + Reboot bool `yaml:"reboot,omitempty" mapstructure:"reboot"` + PowerOff bool `yaml:"poweroff,omitempty" mapstructure:"poweroff"` + EfiPartition *Partition `yaml:"efi-partition,omitempty" mapstructure:"efi-partition"` } func (i *UpgradeUkiSpec) Sanitize() error { diff --git a/pkg/uki/upgrade.go b/pkg/uki/upgrade.go index 59cc123..28e639b 100644 --- a/pkg/uki/upgrade.go +++ b/pkg/uki/upgrade.go @@ -3,9 +3,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/constants" + "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 UpgradeAction struct { @@ -18,16 +21,30 @@ func NewUpgradeAction(cfg *config.Config, spec *v1.UpgradeUkiSpec) *UpgradeActio } func (i *UpgradeAction) Run() (err error) { + e := elemental.NewElemental(i.cfg) + cleanup := utils.NewCleanStack() + defer func() { err = cleanup.Cleanup(err) }() // Run pre-install stage _ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.pre") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.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 - + // REMOUNT /efi as RW (its RO by default) + umount, err := e.MountRWPartition(i.spec.EfiPartition) + if err != nil { + return err + } + cleanup.Push(umount) + // TODO: Check size of EFI partition to see if we can upgrade + // TODO: Check size of source to see if we can upgrade + // TODO: Check number of existing UKI files + // TODO: Load them, order them via semver + // TODO: Remove the latest one if its over the max number of entries + // Dump artifact to efi dir + _, err = e.DumpSource(constants.UkiEfiDir, i.spec.Active.Source) + if err != nil { + return err + } + _ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after") _ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck