2023-05-05 16:43:21 +00:00
/ *
Copyright © 2022 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 .
* /
2023-07-25 13:21:34 +00:00
package config
2023-05-05 16:43:21 +00:00
import (
"fmt"
2023-12-18 15:09:55 +00:00
"github.com/google/go-containerregistry/pkg/crane"
"golang.org/x/sys/unix"
2023-08-08 08:44:42 +00:00
"io/fs"
2023-09-27 14:25:34 +00:00
"os"
2023-05-05 16:43:21 +00:00
"path/filepath"
"reflect"
"strings"
2023-07-10 12:39:48 +00:00
"github.com/kairos-io/kairos-agent/v2/internal/common"
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
2023-09-27 14:38:55 +00:00
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
2023-08-08 08:44:42 +00:00
"github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions"
2023-05-05 16:43:21 +00:00
"github.com/mitchellh/mapstructure"
"github.com/sanity-io/litter"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
// NewInstallSpec returns an InstallSpec struct all based on defaults and basic host checks (e.g. EFI vs BIOS)
2023-09-29 12:49:36 +00:00
func NewInstallSpec ( cfg * Config ) ( * v1 . InstallSpec , error ) {
2023-05-05 16:43:21 +00:00
var firmware string
var recoveryImg , activeImg , passiveImg v1 . Image
recoveryImgFile := filepath . Join ( constants . LiveDir , constants . RecoverySquashFile )
// Check if current host has EFI firmware
2023-07-25 13:21:34 +00:00
efiExists , _ := fsutils . Exists ( cfg . Fs , constants . EfiDevice )
2023-05-05 16:43:21 +00:00
// Check the default ISO installation media is available
2023-07-25 13:21:34 +00:00
isoRootExists , _ := fsutils . Exists ( cfg . Fs , constants . IsoBaseTree )
2023-05-05 16:43:21 +00:00
// Check the default ISO recovery installation media is available)
2023-07-25 13:21:34 +00:00
recoveryExists , _ := fsutils . Exists ( cfg . Fs , recoveryImgFile )
2023-05-05 16:43:21 +00:00
if efiExists {
firmware = v1 . EFI
} else {
firmware = v1 . BIOS
}
activeImg . Label = constants . ActiveLabel
activeImg . Size = constants . ImgSize
activeImg . File = filepath . Join ( constants . StateDir , "cOS" , constants . ActiveImgFile )
activeImg . FS = constants . LinuxImgFs
activeImg . MountPoint = constants . ActiveDir
2023-10-02 09:28:33 +00:00
2024-01-23 16:05:54 +00:00
// First try to use the install media source
2023-05-05 16:43:21 +00:00
if isoRootExists {
activeImg . Source = v1 . NewDirSrc ( constants . IsoBaseTree )
2024-01-23 16:05:54 +00:00
}
// Then any user provided source
if cfg . Install . Source != "" {
activeImg . Source , _ = v1 . NewSrcFromURI ( cfg . Install . Source )
}
// If we dont have any just an empty source so the sanitation fails
// TODO: Should we directly fail here if we got no source instead of waiting for the Sanitize() to fail?
if ! isoRootExists && cfg . Install . Source == "" {
2023-05-05 16:43:21 +00:00
activeImg . Source = v1 . NewEmptySrc ( )
}
if recoveryExists {
recoveryImg . Source = v1 . NewFileSrc ( recoveryImgFile )
recoveryImg . FS = constants . SquashFs
recoveryImg . File = filepath . Join ( constants . RecoveryDir , "cOS" , constants . RecoverySquashFile )
2023-07-20 13:53:48 +00:00
recoveryImg . Size = constants . ImgSize
2023-05-05 16:43:21 +00:00
} else {
recoveryImg . Source = v1 . NewFileSrc ( activeImg . File )
recoveryImg . FS = constants . LinuxImgFs
recoveryImg . Label = constants . SystemLabel
recoveryImg . File = filepath . Join ( constants . RecoveryDir , "cOS" , constants . RecoveryImgFile )
2023-07-20 13:53:48 +00:00
recoveryImg . Size = constants . ImgSize
2023-05-05 16:43:21 +00:00
}
passiveImg = v1 . Image {
File : filepath . Join ( constants . StateDir , "cOS" , constants . PassiveImgFile ) ,
Label : constants . PassiveLabel ,
Source : v1 . NewFileSrc ( activeImg . File ) ,
FS : constants . LinuxImgFs ,
2023-07-20 13:53:48 +00:00
Size : constants . ImgSize ,
2023-05-05 16:43:21 +00:00
}
2023-08-07 12:35:15 +00:00
spec := & v1 . InstallSpec {
Target : cfg . Install . Device ,
Firmware : firmware ,
PartTable : v1 . GPT ,
GrubConf : constants . GrubConf ,
Tty : constants . DefaultTty ,
Active : activeImg ,
Recovery : recoveryImg ,
Passive : passiveImg ,
2023-05-05 16:43:21 +00:00
}
2023-08-08 08:44:42 +00:00
// 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 ( ) )
} else {
cfg . Logger . Infof ( "Setting image size to %dMb" , size )
spec . Active . Size = uint ( size )
spec . Passive . Size = uint ( size )
spec . Recovery . Size = uint ( size )
}
2023-10-02 09:28:33 +00:00
err = unmarshallFullSpec ( cfg , "install" , spec )
if err != nil {
return nil , fmt . Errorf ( "failed unmarshalling the full spec: %w" , err )
}
2023-08-08 08:44:42 +00:00
// Calculate the partitions afterwards so they use the image sizes for the final partition sizes
2024-01-19 11:25:45 +00:00
spec . Partitions = NewInstallElementalPartitions ( cfg . Logger , spec )
2023-08-07 12:35:15 +00:00
2023-09-29 12:49:36 +00:00
return spec , nil
2023-05-05 16:43:21 +00:00
}
2024-01-19 11:25:45 +00:00
func NewInstallElementalPartitions ( log v1 . Logger , spec * v1 . InstallSpec ) v1 . ElementalPartitions {
2023-07-25 13:21:34 +00:00
pt := v1 . ElementalPartitions { }
2024-01-19 11:25:45 +00:00
var oemSize uint
if spec . Partitions . OEM != nil && spec . Partitions . OEM . Size != 0 {
oemSize = spec . Partitions . OEM . Size
} else {
oemSize = constants . OEMSize
}
2023-07-25 13:21:34 +00:00
pt . OEM = & v1 . Partition {
2023-05-05 16:43:21 +00:00
FilesystemLabel : constants . OEMLabel ,
2024-01-19 11:25:45 +00:00
Size : oemSize ,
2023-05-05 16:43:21 +00:00
Name : constants . OEMPartName ,
FS : constants . LinuxFs ,
MountPoint : constants . OEMDir ,
Flags : [ ] string { } ,
}
2024-01-19 11:25:45 +00:00
log . Infof ( "Setting OEM partition size to %dMb" , oemSize )
2023-08-18 10:18:10 +00:00
// Double the space for recovery, as upgrades use the recovery partition to create the transition image for upgrades
// so we need twice the space to do a proper upgrade
2024-01-19 11:25:45 +00:00
// Check if the default/user provided values are enough to fit the images sizes
var recoverySize uint
if spec . Partitions . Recovery == nil { // This means its not configured by user so use the default
recoverySize = ( spec . Recovery . Size * 2 ) + 200
} else {
if spec . Partitions . Recovery . Size < ( spec . Recovery . Size * 2 ) + 200 { // Configured by user but not enough space
// If we had the logger here we could log a message saying that space is not enough and we are auto increasing it
recoverySize = ( spec . Recovery . Size * 2 ) + 200
log . Warnf ( "Not enough space set for recovery partition(%dMb), increasing it to fit the recovery images(%dMb)" , spec . Partitions . Recovery . Size , recoverySize )
} else {
recoverySize = spec . Partitions . Recovery . Size
}
}
log . Infof ( "Setting recovery partition size to %dMb" , recoverySize )
2023-07-25 13:21:34 +00:00
pt . Recovery = & v1 . Partition {
2023-05-05 16:43:21 +00:00
FilesystemLabel : constants . RecoveryLabel ,
2024-01-19 11:25:45 +00:00
Size : recoverySize ,
2023-05-05 16:43:21 +00:00
Name : constants . RecoveryPartName ,
FS : constants . LinuxFs ,
MountPoint : constants . RecoveryDir ,
Flags : [ ] string { } ,
}
2023-08-17 11:53:45 +00:00
// Add 1 Gb to the partition so images can grow a bit, otherwise you are stuck with the smallest space possible and
// there is no coming back from that
2023-08-18 10:18:10 +00:00
// Also multiply the space for active, as upgrades use the state partition to create the transition image for upgrades
// so we need twice the space to do a proper upgrade
2024-01-19 11:25:45 +00:00
// Check if the default/user provided values are enough to fit the images sizes
var stateSize uint
if spec . Partitions . State == nil { // This means its not configured by user so use the default
stateSize = ( spec . Active . Size * 2 ) + spec . Passive . Size + 1000
} else {
if spec . Partitions . State . Size < ( spec . Active . Size * 2 ) + spec . Passive . Size + 1000 { // Configured by user but not enough space
stateSize = ( spec . Active . Size * 2 ) + spec . Passive . Size + 1000
log . Warnf ( "Not enough space set for state partition(%dMb), increasing it to fit the state images(%dMb)" , spec . Partitions . State . Size , stateSize )
} else {
stateSize = spec . Partitions . State . Size
}
}
log . Infof ( "Setting state partition size to %dMb" , stateSize )
2023-07-25 13:21:34 +00:00
pt . State = & v1 . Partition {
2023-05-05 16:43:21 +00:00
FilesystemLabel : constants . StateLabel ,
2024-01-19 11:25:45 +00:00
Size : stateSize ,
2023-05-05 16:43:21 +00:00
Name : constants . StatePartName ,
FS : constants . LinuxFs ,
MountPoint : constants . StateDir ,
Flags : [ ] string { } ,
}
2024-01-19 11:25:45 +00:00
var persistentSize uint
if spec . Partitions . Persistent == nil { // This means its not configured by user so use the default
persistentSize = constants . PersistentSize
} else {
persistentSize = spec . Partitions . Persistent . Size
}
2023-07-25 13:21:34 +00:00
pt . Persistent = & v1 . Partition {
2023-05-05 16:43:21 +00:00
FilesystemLabel : constants . PersistentLabel ,
2024-01-19 11:25:45 +00:00
Size : persistentSize ,
2023-05-05 16:43:21 +00:00
Name : constants . PersistentPartName ,
FS : constants . LinuxFs ,
MountPoint : constants . PersistentDir ,
Flags : [ ] string { } ,
}
2024-01-19 11:25:45 +00:00
log . Infof ( "Setting persistent partition size to %dMb" , persistentSize )
2023-07-25 13:21:34 +00:00
return pt
2023-05-05 16:43:21 +00:00
}
// NewUpgradeSpec returns an UpgradeSpec struct all based on defaults and current host state
2023-07-25 13:21:34 +00:00
func NewUpgradeSpec ( cfg * Config ) ( * v1 . UpgradeSpec , error ) {
2023-05-05 16:43:21 +00:00
var recLabel , recFs , recMnt string
var active , passive , recovery v1 . Image
installState , err := cfg . LoadInstallState ( )
if err != nil {
cfg . Logger . Warnf ( "failed reading installation state: %s" , err . Error ( ) )
}
2023-07-25 13:21:34 +00:00
parts , err := partitions . GetAllPartitions ( )
2023-05-05 16:43:21 +00:00
if err != nil {
2023-06-23 12:49:38 +00:00
return nil , fmt . Errorf ( "could not read host partitions" )
2023-05-05 16:43:21 +00:00
}
ep := v1 . NewElementalPartitionsFromList ( parts )
2023-07-07 15:35:41 +00:00
if ep . Recovery == nil {
// We could have recovery in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . Recovery = partitions . GetPartitionViaDM ( cfg . Fs , constants . RecoveryLabel )
2023-07-07 15:35:41 +00:00
}
if ep . OEM == nil {
// We could have OEM in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . OEM = partitions . GetPartitionViaDM ( cfg . Fs , constants . OEMLabel )
2023-07-07 15:35:41 +00:00
}
if ep . Persistent == nil {
// We could have persistent encrypted or in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . Persistent = partitions . GetPartitionViaDM ( cfg . Fs , constants . PersistentLabel )
2023-07-07 15:35:41 +00:00
}
2023-05-05 16:43:21 +00:00
if ep . Recovery != nil {
if ep . Recovery . MountPoint == "" {
ep . Recovery . MountPoint = constants . RecoveryDir
}
2023-07-25 13:21:34 +00:00
squashedRec , err := hasSquashedRecovery ( cfg , ep . Recovery )
2023-05-05 16:43:21 +00:00
if err != nil {
2023-09-28 13:44:10 +00:00
return nil , fmt . Errorf ( "failed checking for squashed recovery: %w" , err )
2023-05-05 16:43:21 +00:00
}
if squashedRec {
recFs = constants . SquashFs
} else {
recLabel = constants . SystemLabel
recFs = constants . LinuxImgFs
recMnt = constants . TransitionDir
}
recovery = v1 . Image {
File : filepath . Join ( ep . Recovery . MountPoint , "cOS" , constants . TransitionImgFile ) ,
Size : constants . ImgSize ,
Label : recLabel ,
FS : recFs ,
MountPoint : recMnt ,
Source : v1 . NewEmptySrc ( ) ,
}
}
if ep . State != nil {
if ep . State . MountPoint == "" {
ep . State . MountPoint = constants . StateDir
}
active = v1 . Image {
File : filepath . Join ( ep . State . MountPoint , "cOS" , constants . TransitionImgFile ) ,
Size : constants . ImgSize ,
Label : constants . ActiveLabel ,
FS : constants . LinuxImgFs ,
MountPoint : constants . TransitionDir ,
Source : v1 . NewEmptySrc ( ) ,
}
passive = v1 . Image {
File : filepath . Join ( ep . State . MountPoint , "cOS" , constants . PassiveImgFile ) ,
Label : constants . PassiveLabel ,
2023-07-20 13:53:48 +00:00
Size : constants . ImgSize ,
2023-05-05 16:43:21 +00:00
Source : v1 . NewFileSrc ( active . File ) ,
FS : active . FS ,
}
}
2023-07-19 07:46:30 +00:00
// If we have oem in the system, but it has no mountpoint
if ep . OEM != nil && ep . OEM . MountPoint == "" {
// Add the default mountpoint for it in case the chroot stages want to bind mount it
ep . OEM . MountPoint = constants . OEMPath
}
2023-05-05 16:43:21 +00:00
// This is needed if we want to use the persistent as tmpdir for the upgrade images
// as tmpfs is 25% of the total RAM, we cannot rely on the tmp dir having enough space for our image
// This enables upgrades on low ram devices
if ep . Persistent != nil {
if ep . Persistent . MountPoint == "" {
ep . Persistent . MountPoint = constants . PersistentDir
}
}
2023-08-08 08:44:42 +00:00
spec := & v1 . UpgradeSpec {
2023-05-05 16:43:21 +00:00
Active : active ,
Recovery : recovery ,
Passive : passive ,
Partitions : ep ,
State : installState ,
2023-08-08 08:44:42 +00:00
}
2023-10-02 09:28:33 +00:00
setUpgradeSourceSize ( cfg , spec )
2023-09-29 12:49:36 +00:00
err = unmarshallFullSpec ( cfg , "upgrade" , spec )
2023-09-29 07:12:12 +00:00
if err != nil {
2023-09-29 12:49:36 +00:00
return nil , fmt . Errorf ( "failed unmarshalling the full spec: %w" , err )
2023-09-29 07:12:12 +00:00
}
2023-08-08 08:44:42 +00:00
return spec , nil
2023-05-05 16:43:21 +00:00
}
2023-09-29 12:49:36 +00:00
func setUpgradeSourceSize ( cfg * Config , spec * v1 . UpgradeSpec ) error {
var size int64
var err error
var targetSpec * v1 . Image
if spec . RecoveryUpgrade {
targetSpec = & ( spec . Recovery )
} else {
targetSpec = & ( spec . Active )
}
if targetSpec . Source . IsEmpty ( ) {
return nil
}
size , err = GetSourceSize ( cfg , targetSpec . Source )
if err != nil {
return err
}
cfg . Logger . Infof ( "Setting image size to %dMb" , size )
targetSpec . Size = uint ( size )
return nil
}
2023-05-05 16:43:21 +00:00
// NewResetSpec returns a ResetSpec struct all based on defaults and current host state
2023-07-25 13:21:34 +00:00
func NewResetSpec ( cfg * Config ) ( * v1 . ResetSpec , error ) {
2023-05-05 16:43:21 +00:00
var imgSource * v1 . ImageSource
//TODO find a way to pre-load current state values such as labels
2023-07-25 13:21:34 +00:00
if ! BootedFrom ( cfg . Runner , constants . RecoverySquashFile ) &&
! BootedFrom ( cfg . Runner , constants . SystemLabel ) {
2023-05-05 16:43:21 +00:00
return nil , fmt . Errorf ( "reset can only be called from the recovery system" )
}
2023-07-25 13:21:34 +00:00
efiExists , _ := fsutils . Exists ( cfg . Fs , constants . EfiDevice )
2023-05-05 16:43:21 +00:00
installState , err := cfg . LoadInstallState ( )
if err != nil {
cfg . Logger . Warnf ( "failed reading installation state: %s" , err . Error ( ) )
}
2023-07-25 13:21:34 +00:00
parts , err := partitions . GetAllPartitions ( )
2023-05-05 16:43:21 +00:00
if err != nil {
2023-06-23 12:49:38 +00:00
return nil , fmt . Errorf ( "could not read host partitions" )
2023-05-05 16:43:21 +00:00
}
ep := v1 . NewElementalPartitionsFromList ( parts )
if efiExists {
if ep . EFI == nil {
return nil , fmt . Errorf ( "EFI partition not found" )
}
if ep . EFI . MountPoint == "" {
ep . EFI . MountPoint = constants . EfiDir
}
ep . EFI . Name = constants . EfiPartName
}
if ep . State == nil {
return nil , fmt . Errorf ( "state partition not found" )
}
if ep . State . MountPoint == "" {
ep . State . MountPoint = constants . StateDir
}
ep . State . Name = constants . StatePartName
if ep . Recovery == nil {
2023-06-23 12:49:38 +00:00
// We could have recovery in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . Recovery = partitions . GetPartitionViaDM ( cfg . Fs , constants . RecoveryLabel )
2023-06-23 12:49:38 +00:00
if ep . Recovery == nil {
return nil , fmt . Errorf ( "recovery partition not found" )
}
2023-05-05 16:43:21 +00:00
}
if ep . Recovery . MountPoint == "" {
ep . Recovery . MountPoint = constants . RecoveryDir
}
target := ep . State . Disk
// OEM partition is not a hard requirement
if ep . OEM != nil {
if ep . OEM . MountPoint == "" {
ep . OEM . MountPoint = constants . OEMDir
}
ep . OEM . Name = constants . OEMPartName
} else {
2023-06-23 12:49:38 +00:00
// We could have oem in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . OEM = partitions . GetPartitionViaDM ( cfg . Fs , constants . OEMLabel )
2023-06-23 12:49:38 +00:00
}
if ep . OEM == nil {
2023-05-05 16:43:21 +00:00
cfg . Logger . Warnf ( "no OEM partition found" )
}
// Persistent partition is not a hard requirement
if ep . Persistent != nil {
if ep . Persistent . MountPoint == "" {
ep . Persistent . MountPoint = constants . PersistentDir
}
ep . Persistent . Name = constants . PersistentPartName
} else {
2023-06-23 12:49:38 +00:00
// We could have persistent encrypted or in lvm which won't appear in ghw list
2023-07-25 13:21:34 +00:00
ep . Persistent = partitions . GetPartitionViaDM ( cfg . Fs , constants . PersistentLabel )
2023-05-05 16:43:21 +00:00
}
if ep . Persistent == nil {
cfg . Logger . Warnf ( "no Persistent partition found" )
}
recoveryImg := filepath . Join ( constants . RunningStateDir , "cOS" , constants . RecoveryImgFile )
recoveryImg2 := filepath . Join ( constants . RunningRecoveryStateDir , "cOS" , constants . RecoveryImgFile )
2023-07-25 13:21:34 +00:00
if exists , _ := fsutils . Exists ( cfg . Fs , recoveryImg ) ; exists {
2023-05-05 16:43:21 +00:00
imgSource = v1 . NewFileSrc ( recoveryImg )
2023-07-25 13:21:34 +00:00
} else if exists , _ = fsutils . Exists ( cfg . Fs , recoveryImg2 ) ; exists {
2023-05-05 16:43:21 +00:00
imgSource = v1 . NewFileSrc ( recoveryImg2 )
2023-07-25 13:21:34 +00:00
} else if exists , _ = fsutils . Exists ( cfg . Fs , constants . IsoBaseTree ) ; exists {
2023-05-05 16:43:21 +00:00
imgSource = v1 . NewDirSrc ( constants . IsoBaseTree )
} else {
imgSource = v1 . NewEmptySrc ( )
}
activeFile := filepath . Join ( ep . State . MountPoint , "cOS" , constants . ActiveImgFile )
2023-08-08 08:44:42 +00:00
spec := & v1 . ResetSpec {
2023-07-21 08:37:53 +00:00
Target : target ,
Partitions : ep ,
Efi : efiExists ,
GrubDefEntry : constants . GrubDefEntry ,
GrubConf : constants . GrubConf ,
Tty : constants . DefaultTty ,
FormatPersistent : true ,
2023-05-05 16:43:21 +00:00
Active : v1 . Image {
Label : constants . ActiveLabel ,
Size : constants . ImgSize ,
File : activeFile ,
FS : constants . LinuxImgFs ,
Source : imgSource ,
MountPoint : constants . ActiveDir ,
} ,
Passive : v1 . Image {
File : filepath . Join ( ep . State . MountPoint , "cOS" , constants . PassiveImgFile ) ,
Label : constants . PassiveLabel ,
2023-07-20 13:53:48 +00:00
Size : constants . ImgSize ,
2023-05-05 16:43:21 +00:00
Source : v1 . NewFileSrc ( activeFile ) ,
FS : constants . LinuxImgFs ,
} ,
State : installState ,
2023-08-08 08:44:42 +00:00
}
// 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 ( ) )
} else {
cfg . Logger . Infof ( "Setting image size to %dMb" , size )
spec . Active . Size = uint ( size )
spec . Passive . Size = uint ( size )
}
2023-10-02 09:28:33 +00:00
err = unmarshallFullSpec ( cfg , "reset" , spec )
if err != nil {
return nil , fmt . Errorf ( "failed unmarshalling the full spec: %w" , err )
}
2023-08-08 08:44:42 +00:00
return spec , nil
2023-05-05 16:43:21 +00:00
}
2023-07-25 13:21:34 +00:00
// ReadResetSpecFromConfig will return a proper v1.ResetSpec based on an agent Config
func ReadResetSpecFromConfig ( c * Config ) ( * v1 . ResetSpec , error ) {
sp , err := ReadSpecFromCloudConfig ( c , "reset" )
if err != nil {
return & v1 . ResetSpec { } , err
}
resetSpec := sp . ( * v1 . ResetSpec )
return resetSpec , nil
}
2023-05-05 16:43:21 +00:00
2024-02-02 12:20:06 +00:00
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
}
2023-07-25 13:21:34 +00:00
// ReadInstallSpecFromConfig will return a proper v1.InstallSpec based on an agent Config
func ReadInstallSpecFromConfig ( c * Config ) ( * v1 . InstallSpec , error ) {
sp , err := ReadSpecFromCloudConfig ( c , "install" )
2023-07-20 13:53:48 +00:00
if err != nil {
2023-07-25 13:21:34 +00:00
return & v1 . InstallSpec { } , err
2023-05-05 16:43:21 +00:00
}
2023-07-25 13:21:34 +00:00
installSpec := sp . ( * v1 . InstallSpec )
2023-07-25 22:22:14 +00:00
// Workaround!
// If we set the "auto" for the device in the cloudconfig the value will be proper in the Config.Install.Device
// But on the cloud-config it will still appear as "auto" as we dont modify that
// Unfortunately as we load the full cloud-config and unmarshall it into our spec, we cannot infer from there
// What device was choosen, and re-choosing again could lead to different results
// So instead we do the check here and override the installSpec.Target with the Config.Install.Device
// as its the soonest we have access to both
if installSpec . Target == "auto" {
installSpec . Target = c . Install . Device
}
2023-07-25 13:21:34 +00:00
return installSpec , nil
}
2023-05-05 16:43:21 +00:00
2023-10-03 09:15:17 +00:00
// ReadUkiResetSpecFromConfig will return a proper v1.ResetUkiSpec based on an agent Config
func ReadUkiResetSpecFromConfig ( c * Config ) ( * v1 . ResetUkiSpec , error ) {
sp , err := ReadSpecFromCloudConfig ( c , "reset-uki" )
if err != nil {
return & v1 . ResetUkiSpec { } , err
}
resetSpec := sp . ( * v1 . ResetUkiSpec )
return resetSpec , nil
}
func NewUkiInstallSpec ( cfg * Config ) ( * v1 . InstallUkiSpec , error ) {
spec := & v1 . InstallUkiSpec {
Target : cfg . Install . Device ,
}
// Calculate the partitions afterwards so they use the image sizes for the final partition sizes
spec . Partitions . EFI = & v1 . Partition {
FilesystemLabel : constants . EfiLabel ,
2024-01-30 09:35:10 +00:00
Size : constants . ImgSize * 5 , // 15Gb for the EFI partition as default
2023-10-03 09:15:17 +00:00
Name : constants . EfiPartName ,
FS : constants . EfiFs ,
MountPoint : constants . EfiDir ,
Flags : [ ] string { "esp" } ,
}
spec . Partitions . OEM = & v1 . Partition {
FilesystemLabel : constants . OEMLabel ,
Size : constants . OEMSize ,
Name : constants . OEMPartName ,
FS : constants . LinuxFs ,
MountPoint : constants . OEMDir ,
Flags : [ ] string { } ,
}
spec . Partitions . Persistent = & v1 . Partition {
FilesystemLabel : constants . PersistentLabel ,
Size : constants . PersistentSize ,
Name : constants . PersistentPartName ,
FS : constants . LinuxFs ,
MountPoint : constants . PersistentDir ,
Flags : [ ] string { } ,
}
err := unmarshallFullSpec ( cfg , "install" , spec )
2023-12-18 10:38:26 +00:00
// TODO: Get the actual source size to calculate the image size and partitions size for at least 3 UKI images
2024-01-26 16:41:23 +00:00
// Add default values for the skip partitions for our default entries
spec . SkipEntries = append ( spec . SkipEntries , constants . UkiDefaultSkipEntries ( ) ... )
2023-10-03 09:15:17 +00:00
return spec , err
}
// ReadUkiInstallSpecFromConfig will return a proper v1.InstallUkiSpec based on an agent Config
func ReadUkiInstallSpecFromConfig ( c * Config ) ( * v1 . InstallUkiSpec , error ) {
sp , err := ReadSpecFromCloudConfig ( c , "install-uki" )
if err != nil {
return & v1 . InstallUkiSpec { } , err
}
installSpec := sp . ( * v1 . InstallUkiSpec )
2024-01-24 09:44:19 +00:00
// Workaround!
// If we set the "auto" for the device in the cloudconfig the value will be proper in the Config.Install.Device
// But on the cloud-config it will still appear as "auto" as we dont modify that
// Unfortunately as we load the full cloud-config and unmarshall it into our spec, we cannot infer from there
// What device was choosen, and re-choosing again could lead to different results
// So instead we do the check here and override the installSpec.Target with the Config.Install.Device
// as its the soonest we have access to both
if installSpec . Target == "auto" {
installSpec . Target = c . Install . Device
}
2023-10-03 09:15:17 +00:00
return installSpec , nil
}
2023-12-18 10:38:26 +00:00
func NewUkiUpgradeSpec ( cfg * Config ) ( * v1 . UpgradeUkiSpec , error ) {
spec := & v1 . UpgradeUkiSpec { }
err := unmarshallFullSpec ( cfg , "upgrade" , spec )
2023-12-18 15:09:55 +00:00
// TODO: Use this everywhere?
cfg . Logger . Infof ( "Checking if OCI image %s exists" , spec . Active . Source . Value ( ) )
if spec . Active . Source . IsDocker ( ) {
_ , err := crane . Manifest ( spec . Active . Source . Value ( ) )
if err != nil {
if strings . Contains ( err . Error ( ) , "MANIFEST_UNKNOWN" ) {
return nil , fmt . Errorf ( "oci image %s does not exist" , spec . Active . Source . Value ( ) )
}
return nil , err
}
}
2023-12-18 10:38:26 +00:00
// 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 )
}
2023-12-18 15:09:55 +00:00
// Get EFI partition
parts , err := partitions . GetAllPartitions ( )
if err != nil {
return spec , fmt . Errorf ( "could not read host partitions" )
}
for _ , p := range parts {
if p . FilesystemLabel == constants . EfiLabel {
spec . EfiPartition = p
break
}
}
// Get free size of partition
var stat unix . Statfs_t
_ = unix . Statfs ( spec . EfiPartition . MountPoint , & stat )
freeSize := stat . Bfree * uint64 ( stat . Bsize ) / 1000 / 1000
cfg . Logger . Debugf ( "Partition on mountpoint %s has %dMb free" , spec . EfiPartition . MountPoint , freeSize )
// Check if the source is over the free size
if spec . Active . Size > uint ( freeSize ) {
return spec , fmt . Errorf ( "source size(%d) is bigger than the free space(%d) on the EFI partition(%s)" , spec . Active . Size , freeSize , spec . EfiPartition . MountPoint )
2023-12-18 10:38:26 +00:00
}
2023-12-18 15:09:55 +00:00
2023-12-18 10:38:26 +00:00
return spec , err
}
2023-10-03 09:15:17 +00:00
// 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" )
if err != nil {
return & v1 . UpgradeUkiSpec { } , err
}
upgradeSpec := sp . ( * v1 . UpgradeUkiSpec )
return upgradeSpec , nil
}
2023-10-23 14:35:19 +00:00
// getSize will calculate the size of a file or symlink and will do nothing with directories
2023-10-19 20:25:57 +00:00
// fileList: keeps track of the files visited to avoid counting a file more than once if it's a symlink. It could also be used as a way to filter some files
// size: will be the memory that adds up all the files sizes. Meaning it could be initialized with a value greater than 0 if needed.
2023-10-23 14:35:19 +00:00
func getSize ( size * int64 , fileList map [ string ] bool , path string , d fs . DirEntry , err error ) error {
2023-10-19 20:11:13 +00:00
if err != nil {
return err
}
2023-10-20 09:49:35 +00:00
if d . IsDir ( ) {
return nil
}
actualFilePath := path
2023-10-19 20:11:13 +00:00
if d . Type ( ) & fs . ModeSymlink != 0 {
// If it's a symlink, get its target and calculate its size.
2023-10-20 09:49:35 +00:00
var err error
actualFilePath , err = os . Readlink ( path )
2023-10-19 20:11:13 +00:00
if err != nil {
return err
}
2023-10-20 09:49:35 +00:00
if ! filepath . IsAbs ( actualFilePath ) {
2023-10-19 20:11:13 +00:00
// If it's a relative path, join it with the base directory path.
2023-10-20 09:49:35 +00:00
actualFilePath = filepath . Join ( filepath . Dir ( path ) , actualFilePath )
2023-10-19 20:11:13 +00:00
}
2023-10-20 09:49:35 +00:00
}
2023-10-19 20:11:13 +00:00
2023-10-23 10:55:54 +00:00
fileInfo , err := os . Stat ( actualFilePath )
2023-10-20 09:49:35 +00:00
if os . IsNotExist ( err ) || fileList [ actualFilePath ] {
return nil
}
if err != nil {
return err
2023-10-19 20:11:13 +00:00
}
2023-10-20 09:49:35 +00:00
* size += fileInfo . Size ( )
fileList [ actualFilePath ] = true
2023-10-19 20:11:13 +00:00
return nil
}
2023-08-08 08:44:42 +00:00
// GetSourceSize will try to gather the actual size of the source
// Useful to create the exact size of images and by side effect the partition size
// This helps adjust the size to be juuuuust right.
// It can still be manually override from the cloud config by setting all values manually
// But by default it should adjust the sizes properly
func GetSourceSize ( config * Config , source * v1 . ImageSource ) ( int64 , error ) {
var size int64
var err error
2023-10-19 20:11:13 +00:00
var filesVisited map [ string ] bool
2023-08-08 08:44:42 +00:00
switch {
case source . IsDocker ( ) :
2023-08-17 11:53:45 +00:00
// Docker size is uncompressed! So we double it to be sure that we have enough space
// Otherwise we would need to READ all the layers uncompressed to calculate the image size which would
// double the download size and slow down everything
2023-08-08 08:44:42 +00:00
size , err = config . ImageExtractor . GetOCIImageSize ( source . Value ( ) , config . Platform . String ( ) )
2023-08-17 11:53:45 +00:00
size = int64 ( float64 ( size ) * 2.5 )
2023-08-08 08:44:42 +00:00
case source . IsDir ( ) :
2023-10-23 14:34:57 +00:00
filesVisited = make ( map [ string ] bool , 30000 ) // An Ubuntu system has around 27k files. This improves performance by not having to resize the map for every file visited
2023-09-27 14:25:34 +00:00
2023-10-19 20:11:13 +00:00
err = fsutils . WalkDirFs ( config . Fs , source . Value ( ) , func ( path string , d fs . DirEntry , err error ) error {
2023-10-23 14:35:19 +00:00
v := getSize ( & size , filesVisited , path , d , err )
2023-10-20 09:49:35 +00:00
return v
2023-08-08 08:44:42 +00:00
} )
case source . IsFile ( ) :
file , err := config . Fs . Stat ( source . Value ( ) )
if err == nil {
size = file . Size ( )
}
}
// Normalize size to Mb before returning and add 100Mb to round the size from bytes to mb+extra files like grub stuff
if size != 0 {
size = ( size / 1000 / 1000 ) + 100
}
return size , err
}
2023-07-25 13:21:34 +00:00
// ReadUpgradeSpecFromConfig will return a proper v1.UpgradeSpec based on an agent Config
func ReadUpgradeSpecFromConfig ( c * Config ) ( * v1 . UpgradeSpec , error ) {
2023-07-25 20:15:46 +00:00
sp , err := ReadSpecFromCloudConfig ( c , "upgrade" )
2023-05-05 16:43:21 +00:00
if err != nil {
2023-07-25 13:21:34 +00:00
return & v1 . UpgradeSpec { } , err
}
upgradeSpec := sp . ( * v1 . UpgradeSpec )
return upgradeSpec , nil
2023-05-05 16:43:21 +00:00
}
2023-07-21 15:02:37 +00:00
// ReadSpecFromCloudConfig returns a v1.Spec for the given spec
2023-07-25 13:21:34 +00:00
func ReadSpecFromCloudConfig ( r * Config , spec string ) ( v1 . Spec , error ) {
2023-07-20 13:53:48 +00:00
var sp v1 . Spec
var err error
switch spec {
case "install" :
2023-09-29 12:49:36 +00:00
sp , err = NewInstallSpec ( r )
2023-07-20 13:53:48 +00:00
case "upgrade" :
2023-07-25 09:08:27 +00:00
sp , err = NewUpgradeSpec ( r )
2023-07-20 13:53:48 +00:00
case "reset" :
2023-07-25 09:08:27 +00:00
sp , err = NewResetSpec ( r )
2023-10-03 09:15:17 +00:00
case "install-uki" :
sp , err = NewUkiInstallSpec ( r )
case "reset-uki" :
2024-02-02 12:20:06 +00:00
sp , err = NewUkiResetSpec ( r )
2023-10-03 09:15:17 +00:00
case "upgrade-uki" :
2023-12-18 10:38:26 +00:00
sp , err = NewUkiUpgradeSpec ( r )
2023-07-20 13:53:48 +00:00
default :
return nil , fmt . Errorf ( "spec not valid: %s" , spec )
}
if err != nil {
2023-07-24 09:44:21 +00:00
return nil , fmt . Errorf ( "failed initializing spec: %v" , err )
2023-07-20 13:53:48 +00:00
}
2023-10-02 07:57:11 +00:00
err = sp . Sanitize ( )
if err != nil {
return sp , fmt . Errorf ( "sanitizing the %s spec: %w" , spec , err )
}
2023-08-16 20:59:50 +00:00
r . Logger . Debugf ( "Loaded %s spec: %s" , spec , litter . Sdump ( sp ) )
2023-07-25 17:36:01 +00:00
return sp , nil
2023-05-05 16:43:21 +00:00
}
func configLogger ( log v1 . Logger , vfs v1 . FS ) {
// Set debug level
if viper . GetBool ( "debug" ) {
log . SetLevel ( v1 . DebugLevel ( ) )
}
// Set formatter so both file and stdout format are equal
log . SetFormatter ( & logrus . TextFormatter {
ForceColors : true ,
DisableColors : false ,
DisableTimestamp : false ,
FullTimestamp : true ,
} )
// Logfile
2023-07-24 09:44:21 +00:00
// Not being used for now, disable it until we plug it again in our cli
/ *
logfile := viper . GetString ( "logfile" )
if logfile != "" {
o , err := vfs . OpenFile ( logfile , os . O_APPEND | os . O_CREATE | os . O_WRONLY , fs . ModePerm )
if err != nil {
log . Errorf ( "Could not open %s for logging to file: %s" , logfile , err . Error ( ) )
}
if viper . GetBool ( "quiet" ) { // if quiet is set, only set the log to the file
log . SetOutput ( o )
} else { // else set it to both stdout and the file
mw := io . MultiWriter ( os . Stdout , o )
log . SetOutput ( mw )
}
} else { // no logfile
if viper . GetBool ( "quiet" ) { // quiet is enabled so discard all logging
log . SetOutput ( io . Discard )
} else { // default to stdout
log . SetOutput ( os . Stdout )
}
2023-05-05 16:43:21 +00:00
}
2023-07-24 09:44:21 +00:00
* /
2023-05-05 16:43:21 +00:00
v := common . GetVersion ( )
2023-05-18 14:23:18 +00:00
log . Infof ( "kairos-agent version %s" , v )
2023-05-05 16:43:21 +00:00
}
var decodeHook = viper . DecodeHook (
mapstructure . ComposeDecodeHookFunc (
UnmarshalerHook ( ) ,
mapstructure . StringToTimeDurationHookFunc ( ) ,
mapstructure . StringToSliceHookFunc ( "," ) ,
) ,
)
type Unmarshaler interface {
CustomUnmarshal ( interface { } ) ( bool , error )
}
func UnmarshalerHook ( ) mapstructure . DecodeHookFunc {
return func ( from reflect . Value , to reflect . Value ) ( interface { } , error ) {
// get the destination object address if it is not passed by reference
if to . CanAddr ( ) {
to = to . Addr ( )
}
// If the destination implements the unmarshaling interface
u , ok := to . Interface ( ) . ( Unmarshaler )
if ! ok {
return from . Interface ( ) , nil
}
// If it is nil and a pointer, create and assign the target value first
if to . IsNil ( ) && to . Type ( ) . Kind ( ) == reflect . Ptr {
to . Set ( reflect . New ( to . Type ( ) . Elem ( ) ) )
u = to . Interface ( ) . ( Unmarshaler )
}
// Call the custom unmarshaling method
cont , err := u . CustomUnmarshal ( from . Interface ( ) )
if cont {
// Continue with the decoding stack
return from . Interface ( ) , err
}
// Decoding finalized
return to . Interface ( ) , err
}
}
func setDecoder ( config * mapstructure . DecoderConfig ) {
// Make sure we zero fields before applying them, this is relevant for slices
// so we do not merge with any already present value and directly apply whatever
// we got form configs.
config . ZeroFields = true
}
2023-07-25 13:21:34 +00:00
// BootedFrom will check if we are booting from the given label
func BootedFrom ( runner v1 . Runner , label string ) bool {
out , _ := runner . Run ( "cat" , "/proc/cmdline" )
return strings . Contains ( string ( out ) , label )
}
// HasSquashedRecovery returns true if a squashed recovery image is found in the system
func hasSquashedRecovery ( config * Config , recovery * v1 . Partition ) ( squashed bool , err error ) {
mountPoint := recovery . MountPoint
if mnt , _ := isMounted ( config , recovery ) ; ! mnt {
tmpMountDir , err := fsutils . TempDir ( config . Fs , "" , "elemental" )
if err != nil {
config . Logger . Errorf ( "failed creating temporary dir: %v" , err )
return false , err
}
defer config . Fs . RemoveAll ( tmpMountDir ) // nolint:errcheck
err = config . Mounter . Mount ( recovery . Path , tmpMountDir , "auto" , [ ] string { } )
if err != nil {
config . Logger . Errorf ( "failed mounting recovery partition: %v" , err )
return false , err
}
mountPoint = tmpMountDir
defer func ( ) {
err = config . Mounter . Unmount ( tmpMountDir )
if err != nil {
squashed = false
}
} ( )
}
return fsutils . Exists ( config . Fs , filepath . Join ( mountPoint , "cOS" , constants . RecoverySquashFile ) )
}
func isMounted ( config * Config , part * v1 . Partition ) ( bool , error ) {
if part == nil {
return false , fmt . Errorf ( "nil partition" )
}
if part . MountPoint == "" {
return false , nil
}
// Using IsLikelyNotMountPoint seams to be safe as we are not checking
// for bind mounts here
notMnt , err := config . Mounter . IsLikelyNotMountPoint ( part . MountPoint )
if err != nil {
return false , err
}
return ! notMnt , nil
}
2023-09-29 07:12:12 +00:00
2023-09-29 12:49:36 +00:00
func unmarshallFullSpec ( r * Config , subkey string , sp v1 . Spec ) error {
// Load the config into viper from the raw cloud config string
ccString , err := r . String ( )
if err != nil {
return fmt . Errorf ( "failed initializing spec: %w" , err )
}
viper . SetConfigType ( "yaml" )
viper . ReadConfig ( strings . NewReader ( ccString ) )
vp := viper . Sub ( subkey )
if vp == nil {
vp = viper . New ( )
2023-09-29 07:12:12 +00:00
}
2023-09-29 12:49:36 +00:00
err = vp . Unmarshal ( sp , setDecoder , decodeHook )
2023-09-29 07:12:12 +00:00
if err != nil {
2023-09-29 12:49:36 +00:00
return fmt . Errorf ( "error unmarshalling %s Spec: %w" , subkey , err )
2023-09-29 07:12:12 +00:00
}
return nil
}