2023-10-03 09:15:17 +00:00
package uki
import (
2024-02-12 17:20:45 +00:00
"fmt"
2024-01-26 16:41:23 +00:00
"os"
"path/filepath"
"strings"
2024-03-29 11:49:07 +00:00
"github.com/kairos-io/kairos-agent/v2/pkg/action"
2023-10-03 09:15:17 +00:00
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"
"github.com/kairos-io/kairos-agent/v2/pkg/utils"
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
events "github.com/kairos-io/kairos-sdk/bus"
2024-02-12 17:20:45 +00:00
sdkutils "github.com/kairos-io/kairos-sdk/utils"
2023-10-03 09:15:17 +00:00
)
type InstallAction struct {
cfg * config . Config
spec * v1 . InstallUkiSpec
}
func NewInstallAction ( cfg * config . Config , spec * v1 . InstallUkiSpec ) * InstallAction {
return & InstallAction { cfg : cfg , spec : spec }
}
func ( i * InstallAction ) Run ( ) ( err error ) {
e := elemental . NewElemental ( i . cfg )
cleanup := utils . NewCleanStack ( )
defer func ( ) { err = cleanup . Cleanup ( err ) } ( )
// Run pre-install stage
2024-03-15 09:53:37 +00:00
if err = utils . RunStage ( i . cfg , "kairos-uki-install.pre" ) ; err != nil {
i . cfg . Logger . Errorf ( "running kairos-uki-install.pre stage: %s" , err . Error ( ) )
}
if err = events . RunHookScript ( "/usr/bin/kairos-agent.uki.install.pre.hook" ) ; err != nil {
i . cfg . Logger . Errorf ( "running kairos-uki-install.pre hook script: %s" , err . Error ( ) )
}
2023-10-03 09:15:17 +00:00
2024-04-08 12:58:36 +00:00
if i . spec . NoFormat {
i . cfg . Logger . Infof ( "NoFormat is true, skipping format and partitioning" )
if i . spec . Target == "" || i . spec . Target == "auto" {
// This needs to run after the pre-install stage to give the user the
// opportunity to prepare the target disk in the pre-install stage.
device , err := config . DetectPreConfiguredDevice ( i . cfg . Logger )
if err != nil {
return fmt . Errorf ( "no target device specified and no device found: %s" , err )
}
i . cfg . Logger . Infof ( "No target device specified, using pre-configured device: %s" , device )
i . spec . Target = device
}
} else {
// Deactivate any active volume on target
err = e . DeactivateDevices ( )
if err != nil {
i . cfg . Logger . Errorf ( "deactivating devices: %s" , err . Error ( ) )
return err
}
// Partition device
err = e . PartitionAndFormatDevice ( i . spec )
if err != nil {
i . cfg . Logger . Errorf ( "partitioning and formating devices: %s" , err . Error ( ) )
return err
}
2023-10-03 09:15:17 +00:00
}
err = e . MountPartitions ( i . spec . GetPartitions ( ) . PartitionsByMountPoint ( false ) )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "mounting partitions: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
cleanup . Push ( func ( ) error {
return e . UnmountPartitions ( i . spec . GetPartitions ( ) . PartitionsByMountPoint ( true ) )
} )
// Before install hook happens after partitioning but before the image OS is applied (this is for compatibility with normal install, so users can reuse their configs)
err = Hook ( i . cfg , constants . BeforeInstallHook )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "running before install hook: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
2024-08-21 07:25:10 +00:00
// Check if we should fail the installation by checking the sentinel file FailInstallationFileSentinel
if toFail , err := utils . CheckFailedInstallation ( constants . FailInstallationFileSentinel ) ; toFail {
return err
}
2023-10-03 09:15:17 +00:00
// Store cloud-config in TPM or copy it to COS_OEM?
// Copy cloud-init if any
err = e . CopyCloudConfig ( i . spec . CloudInit )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "copying cloud config: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
// Create dir structure
// - /EFI/Kairos/ -> Store our older efi images ?
// - /EFI/BOOT/ -> Default fallback dir (efi search for bootaa64.efi or bootx64.efi if no entries in the boot manager)
err = fsutils . MkdirAll ( i . cfg . Fs , filepath . Join ( constants . EfiDir , "EFI" , "BOOT" ) , constants . DirPerm )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "creating efi directories: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
2024-02-16 15:37:40 +00:00
// TODO: Check if the size of the files we are going to copy, will fit in the
// partition. If not stop here.
2023-10-03 09:15:17 +00:00
// Copy the efi file into the proper dir
2024-02-12 09:35:36 +00:00
_ , err = e . DumpSource ( i . spec . Partitions . EFI . MountPoint , i . spec . Active . Source )
2023-10-03 09:15:17 +00:00
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "dumping source: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
2024-01-26 16:41:23 +00:00
// Remove entries
// Read all confs
2024-02-12 17:20:45 +00:00
i . cfg . Logger . Debugf ( "Parsing efi partition files (skip SkipEntries, replace placeholders etc)" )
err = fsutils . WalkDirFs ( i . cfg . Fs , filepath . Join ( i . spec . Partitions . EFI . MountPoint ) , func ( path string , info os . DirEntry , err error ) error {
filename := info . Name ( )
2024-02-12 08:19:17 +00:00
if err != nil {
2024-02-12 17:20:45 +00:00
i . cfg . Logger . Errorf ( "Error walking path: %s, %s" , filename , err . Error ( ) )
2024-02-12 08:19:17 +00:00
return err
}
2024-01-26 16:41:23 +00:00
i . cfg . Logger . Debugf ( "Checking file %s" , path )
if info . IsDir ( ) {
return nil
}
2024-02-12 08:19:17 +00:00
2024-02-12 17:20:45 +00:00
if filepath . Ext ( filename ) == ".conf" {
// Extract the values
conf , err := sdkutils . SystemdBootConfReader ( path )
if err != nil {
i . cfg . Logger . Errorf ( "Error reading conf file to extract values %s: %s" , path , err )
return err
}
if len ( conf [ "cmdline" ] ) == 0 {
return nil
}
// Check if the cmdline matches any of the entries in the skip list
skip := false
for _ , entry := range i . spec . SkipEntries {
if strings . Contains ( conf [ "cmdline" ] , entry ) {
i . cfg . Logger . Debugf ( "Found match for %s in %s" , entry , path )
skip = true
break
}
}
if skip {
return i . SkipEntry ( path , conf )
}
2024-01-26 16:41:23 +00:00
}
2024-02-12 08:19:17 +00:00
2024-02-12 17:20:45 +00:00
return nil
2024-01-26 16:41:23 +00:00
} )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "error happened: %s" , err . Error ( ) )
2024-01-26 16:41:23 +00:00
return err
}
2024-03-29 15:12:23 +00:00
for _ , role := range [ ] string { "active" , "passive" , "recovery" , "statereset" } {
2024-02-16 15:37:40 +00:00
if err = copyArtifactSetRole ( i . cfg . Fs , i . spec . Partitions . EFI . MountPoint , UnassignedArtifactRole , role , i . cfg . Logger ) ; err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "installing the new artifact set as %s: %s" , role , err . Error ( ) )
2024-02-16 15:37:40 +00:00
return fmt . Errorf ( "installing the new artifact set as %s: %w" , role , err )
2024-02-15 16:48:12 +00:00
}
}
2024-02-12 17:20:45 +00:00
loaderConfPath := filepath . Join ( i . spec . Partitions . EFI . MountPoint , "loader" , "loader.conf" )
2024-02-16 15:37:40 +00:00
if err = replaceRoleInKey ( loaderConfPath , "default" , UnassignedArtifactRole , "active" , i . cfg . Logger ) ; err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "replacing role in key %s: %s" , "default" , err . Error ( ) )
2024-02-12 17:20:45 +00:00
return err
}
2024-02-16 15:37:40 +00:00
if err = removeArtifactSetWithRole ( i . cfg . Fs , i . spec . Partitions . EFI . MountPoint , UnassignedArtifactRole ) ; err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "removing artifact set with role %s: %s" , UnassignedArtifactRole , err . Error ( ) )
2024-02-16 15:37:40 +00:00
return fmt . Errorf ( "removing artifact set with role %s: %w" , UnassignedArtifactRole , err )
2024-02-15 16:48:12 +00:00
}
2024-11-27 14:18:50 +00:00
// add sort key to all files
err = AddSystemdConfSortKey ( i . cfg . Fs , i . spec . Partitions . EFI . MountPoint , i . cfg . Logger )
if err != nil {
i . cfg . Logger . Warnf ( "adding sort key: %s" , err . Error ( ) )
}
2024-11-27 10:16:56 +00:00
// Add boot assessment to files by appending +3 to the name
err = utils . AddBootAssessment ( i . cfg . Fs , i . spec . Partitions . EFI . MountPoint , i . cfg . Logger )
if err != nil {
i . cfg . Logger . Warnf ( "adding boot assesment: %s" , err . Error ( ) )
}
2024-03-15 14:32:17 +00:00
// SelectBootEntry sets the default boot entry to the selected entry
2024-03-16 15:27:29 +00:00
err = action . SelectBootEntry ( i . cfg , "cos" )
2024-03-15 14:32:17 +00:00
if err != nil {
i . cfg . Logger . Warnf ( "selecting active boot entry: %s" , err . Error ( ) )
}
2025-03-24 15:05:39 +00:00
err = hook . Run ( * i . cfg , i . spec , hook . PostInstall ... )
2024-02-27 16:25:20 +00:00
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "running uki encryption hooks: %s" , err . Error ( ) )
2024-02-27 16:25:20 +00:00
return err
}
2023-10-03 09:15:17 +00:00
// after install hook happens after install (this is for compatibility with normal install, so users can reuse their configs)
err = Hook ( i . cfg , constants . AfterInstallHook )
if err != nil {
2024-03-15 09:53:37 +00:00
i . cfg . Logger . Errorf ( "running after install hook: %s" , err . Error ( ) )
2023-10-03 09:15:17 +00:00
return err
}
// Remove all boot manager entries?
// Create boot manager entry
// Set default entry to the one we just created
// Probably copy efi utils, like the Mokmanager and even the shim or grub efi to help with troubleshooting?
2024-03-15 09:53:37 +00:00
if err = utils . RunStage ( i . cfg , "kairos-uki-install.after" ) ; err != nil {
i . cfg . Logger . Errorf ( "running kairos-uki-install.after stage: %s" , err . Error ( ) )
}
if err = events . RunHookScript ( "/usr/bin/kairos-agent.uki.install.after.hook" ) ; err != nil {
i . cfg . Logger . Errorf ( "running kairos-uki-install.after hook script: %s" , err . Error ( ) )
}
2023-10-03 09:15:17 +00:00
2025-03-24 15:05:39 +00:00
return hook . Run ( * i . cfg , i . spec , hook . FinishUKIInstall ... )
2023-10-03 09:15:17 +00:00
}
2024-02-12 17:20:45 +00:00
func ( i * InstallAction ) SkipEntry ( path string , conf map [ string ] string ) ( err error ) {
// If match, get the efi file and remove it
if conf [ "efi" ] != "" {
i . cfg . Logger . Debugf ( "Removing efi file %s" , conf [ "efi" ] )
// First remove the efi file
err = i . cfg . Fs . Remove ( filepath . Join ( i . spec . Partitions . EFI . MountPoint , conf [ "efi" ] ) )
if err != nil {
i . cfg . Logger . Errorf ( "Error removing efi file %s: %s" , conf [ "efi" ] , err )
return err
2024-02-12 08:19:17 +00:00
}
2024-02-12 17:20:45 +00:00
// Then remove the conf file
i . cfg . Logger . Debugf ( "Removing conf file %s" , path )
err = i . cfg . Fs . Remove ( path )
if err != nil {
i . cfg . Logger . Errorf ( "Error removing conf file %s: %s" , path , err )
return err
}
// Do not continue checking the conf file, we already done all we needed
2024-02-12 08:19:17 +00:00
}
return err
}
2023-10-03 09:15:17 +00:00
// Hook is RunStage wrapper that only adds logic to ignore errors
// in case v1.Config.Strict is set to false
func Hook ( config * config . Config , hook string ) error {
config . Logger . Infof ( "Running %s hook" , hook )
err := utils . RunStage ( config , hook )
if ! config . Strict {
err = nil
}
return err
}