Skip to content

Commit

Permalink
Implement elemental-register upgrade
Browse files Browse the repository at this point in the history
Signed-off-by: Andrea Mazzotti <andrea.mazzotti@suse.com>
  • Loading branch information
anmazzotti committed Oct 11, 2024
1 parent fad205a commit 456636a
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/register/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func main() {
cmd.AddCommand(
newVersionCommand(),
newDumpDataCommand(),
newUpgradeCommand(),
)
if err := cmd.Execute(); err != nil {
log.Fatalf("FATAL: %s", err)
Expand Down
131 changes: 131 additions & 0 deletions cmd/register/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
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 main

import (
"errors"
"fmt"
"os"
"os/exec"

"github.com/rancher/elemental-operator/pkg/elementalcli"
"github.com/rancher/elemental-operator/pkg/install"
"github.com/rancher/elemental-operator/pkg/log"
"github.com/spf13/cobra"
"github.com/twpayne/go-vfs"
)

var (
ErrRebooting = errors.New("Machine needs reboot after upgrade")
ErrAlreadyShuttingDown = errors.New("System is already shutting down")
)

func newUpgradeCommand() *cobra.Command {
var hostDir string
var cloudConfigPath string
var recovery bool
var recoveryOnly bool
var debug bool
var system string
var correlationID string

cmd := &cobra.Command{
Use: "upgrade",
Short: "Upgrades the machine",
RunE: func(_ *cobra.Command, _ []string) error {
// If the system is shutting down, return an error so we can try again on next reboot.
alreadyShuttingDown, err := isSystemShuttingDown()
if err != nil {
return fmt.Errorf("determining if system is running: %w", err)
}
if alreadyShuttingDown {
return ErrAlreadyShuttingDown
}

// If system is not shutting down we can proceed.
upgradeConfig := elementalcli.UpgradeConfig{
Debug: debug,
Recovery: recovery,
RecoveryOnly: recoveryOnly,
System: system,
Bootloader: true,
}
upgradeContext := install.UpgradeContext{
Config: upgradeConfig,
HostDir: hostDir,
CloudConfigPath: cloudConfigPath,
CorrelationID: correlationID,
}

log.Infof("Upgrade context: %+v", upgradeContext)

installer := install.NewInstaller(vfs.OSFS, nil, nil)

needsReboot, err := installer.UpgradeElemental(upgradeContext)
// If the upgrade could not be applied or verified,
// then this command will fail but the machine will not reboot.
if err != nil {
return fmt.Errorf("upgrading machine: %w", err)
}
// If the machine needs a reboot after an upgrade has been applied,
// so that consumers can try again after reboot to validate the upgrade has been applied successfully.
if needsReboot {
log.Infof("Rebooting machine after %s upgrade", correlationID)
reboot()
return ErrRebooting
}
// Upgrade has been applied successfully, nothing to do.
log.Infof("Upgrade %s applied successfully", correlationID)
return nil
},
}

cmd.Flags().StringVar(&hostDir, "host-dir", "/host", "The machine root directory where to apply the upgrade")
cmd.Flags().StringVar(&cloudConfigPath, "cloud-config", "/run/data/cloud-config", "The path of a cloud-config file to install on the machine during upgrade")
cmd.Flags().StringVar(&system, "system", "dir:/", "The system image uri or filesystem location to upgrade to")
cmd.Flags().StringVar(&correlationID, "correlation-id", "", "A correlationID to label the upgrade snapshot with")
cmd.Flags().BoolVar(&recovery, "recovery", false, "Upgrades the recovery partition together with the system")
cmd.Flags().BoolVar(&recoveryOnly, "recovery-only", false, "Upgrades the recovery partition only")
cmd.Flags().BoolVar(&debug, "debug", true, "Prints debug logs when performing upgrade")
return cmd
}

func isSystemShuttingDown() (bool, error) {
cmd := exec.Command("nsenter")
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Args = []string{"-i", "-m", "-t", "1", "--", "systemctl is-system-running"}
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("running: systemctl is-system-running: %w", err)
}
if string(output) == "stopping" {
return true, nil
}
return false, nil
}

func reboot() {
cmd := exec.Command("nsenter")
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Args = []string{"-i", "-m", "-t", "1", "--", "reboot"}
if err := cmd.Run(); err != nil {
log.Errorf("Could not reboot: %s", err)
}
}
106 changes: 105 additions & 1 deletion pkg/elementalcli/elementalcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,46 @@ import (
"os/exec"
"strconv"
"strings"
"time"

elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/pkg/log"
"gopkg.in/yaml.v3"
)

const TempCloudInitDir = "/tmp/elemental/cloud-init"
const (
TempCloudInitDir = "/tmp/elemental/cloud-init"
UpgradeLockFile = "/run/elemental/upgrade.lock"
UpgradeLockTimeout = 60 * time.Second
)

type UpgradeConfig struct {
Debug bool
Recovery bool
RecoveryOnly bool
System string
Bootloader bool
SnapshotLabels map[string]string
}

type State struct {
StatePartition PartitionState `yaml:"state,omitempty"`
}

type PartitionState struct {
Snapshots map[int]*Snapshot `yaml:"snapshots,omitempty"`
}

type Snapshot struct {
Active bool `yaml:"active,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
}

type Runner interface {
Install(elementalv1.Install) error
Reset(elementalv1.Reset) error
Upgrade(UpgradeConfig) error
GetState() (State, error)
}

func NewRunner() Runner {
Expand Down Expand Up @@ -86,6 +116,59 @@ func (r *runner) Reset(conf elementalv1.Reset) error {
return cmd.Run()
}

func (r *runner) Upgrade(conf UpgradeConfig) error {
installerOpts := []string{"elemental"}
// There are no env var bindings in elemental-cli for elemental root options
// so root flags should be passed within the command line
if conf.Debug {
installerOpts = append(installerOpts, "--debug")
}

// Actual subcommand
if conf.RecoveryOnly {
installerOpts = append(installerOpts, "upgrade-recovery")
} else {
installerOpts = append(installerOpts, "upgrade")
}

if conf.Bootloader {
installerOpts = append(installerOpts, "--bootloader")
}

cmd := exec.Command("elemental")
environmentVariables := mapToUpgradeEnv(conf)
cmd.Env = append(os.Environ(), environmentVariables...)
cmd.Stdout = os.Stdout
cmd.Args = installerOpts
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
log.Debugf("running: %s\n with ENV:\n%s", strings.Join(installerOpts, " "), strings.Join(environmentVariables, "\n"))
return cmd.Run()
}

func (r *runner) GetState() (State, error) {
state := State{}

log.Debug("Getting elemental state")
installerOpts := []string{"elemental", "state"}
cmd := exec.Command("elemental")
cmd.Args = installerOpts
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
log.Debugf("running: %s", strings.Join(installerOpts, " "))

var commandOutput []byte
var err error
if commandOutput, err = cmd.Output(); err != nil {
return state, fmt.Errorf("running elemental state: %w", err)
}
if err := yaml.Unmarshal(commandOutput, &state); err != nil {
return state, fmt.Errorf("unmarshalling elemental state: %w", err)
}

return state, nil
}

func mapToInstallEnv(conf elementalv1.Install) []string {
var variables []string
// See GetInstallKeyEnvMap() in https://github.com/rancher/elemental-toolkit/blob/main/pkg/constants/constants.go
Expand Down Expand Up @@ -120,6 +203,27 @@ func mapToResetEnv(conf elementalv1.Reset) []string {
return variables
}

func mapToUpgradeEnv(conf UpgradeConfig) []string {
var variables []string
// See GetUpgradeKeyEnvMap() in https://github.com/rancher/elemental-toolkit/blob/main/pkg/constants/constants.go
variables = append(variables, formatEV("ELEMENTAL_UPGRADE_RECOVERY", strconv.FormatBool(conf.Recovery)))
variables = append(variables, formatEV("ELEMENTAL_UPGRADE_SNAPSHOT_LABELS", formatSnapshotLabels(conf.SnapshotLabels)))
if conf.RecoveryOnly {
variables = append(variables, formatEV("ELEMENTAL_UPGRADE_RECOVERY_SYSTEM", conf.System))
} else {
variables = append(variables, formatEV("ELEMENTAL_UPGRADE_SYSTEM", conf.System))
}
return variables
}

func formatEV(key string, value string) string {
return fmt.Sprintf("%s=%s", key, value)
}

func formatSnapshotLabels(labels map[string]string) string {
formattedLabels := []string{}
for key, value := range labels {
formattedLabels = append(formattedLabels, fmt.Sprintf("%s=%s", key, value))
}
return strings.Join(formattedLabels, ",")
}
30 changes: 30 additions & 0 deletions pkg/elementalcli/mocks/elementalcli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 456636a

Please sign in to comment.