2022-07-17 08:42:12 +00:00
package agent
import (
2024-04-12 08:40:11 +00:00
"bytes"
2022-07-17 08:42:12 +00:00
"encoding/json"
"errors"
"fmt"
2024-04-12 08:40:11 +00:00
"io"
2024-09-11 13:57:53 +00:00
"net/http"
2023-03-08 17:13:36 +00:00
"net/url"
2022-07-17 08:42:12 +00:00
"os"
2023-01-08 20:49:23 +00:00
"strings"
2022-07-17 08:42:12 +00:00
"syscall"
"time"
2024-02-12 17:20:45 +00:00
"github.com/kairos-io/kairos-agent/v2/pkg/uki"
internalutils "github.com/kairos-io/kairos-agent/v2/pkg/utils"
2023-09-14 12:35:44 +00:00
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
"github.com/sanity-io/litter"
2023-07-10 12:39:48 +00:00
"github.com/kairos-io/kairos-agent/v2/internal/bus"
"github.com/kairos-io/kairos-agent/v2/internal/cmd"
"github.com/kairos-io/kairos-agent/v2/pkg/action"
"github.com/kairos-io/kairos-agent/v2/pkg/config"
2023-07-20 13:53:48 +00:00
events "github.com/kairos-io/kairos-sdk/bus"
"github.com/kairos-io/kairos-sdk/collector"
"github.com/kairos-io/kairos-sdk/machine"
"github.com/kairos-io/kairos-sdk/utils"
2022-07-17 08:42:12 +00:00
qr "github.com/mudler/go-nodepair/qrcode"
"github.com/mudler/go-pluggable"
"github.com/pterm/pterm"
)
2023-01-08 20:49:23 +00:00
func displayInfo ( agentConfig * Config ) {
if ! agentConfig . WebUI . Disable {
2024-09-10 13:50:16 +00:00
ifaces := machine . Interfaces ( )
message := fmt . Sprintf ( "Interfaces: %s" , strings . Join ( ifaces , " " ) )
2023-01-08 20:49:23 +00:00
if ! agentConfig . WebUI . HasAddress ( ) {
ips := machine . LocalIPs ( )
if len ( ips ) > 0 {
2024-09-10 13:50:16 +00:00
messageIps := " - WebUI installer: "
2023-01-08 20:49:23 +00:00
for _ , ip := range ips {
2024-09-10 13:50:16 +00:00
// Skip printing local ips, makes no sense
if strings . Contains ( "127.0.0.1" , ip ) || strings . Contains ( "::1" , ip ) {
continue
}
messageIps = messageIps + fmt . Sprintf ( "%s%s " , ip , config . DefaultWebUIListenAddress )
2023-01-08 20:49:23 +00:00
}
2024-09-10 13:50:16 +00:00
message = message + messageIps
2023-01-08 20:49:23 +00:00
}
} else {
2024-09-10 13:50:16 +00:00
message = message + fmt . Sprintf ( " - WebUI installer: %s" , agentConfig . WebUI . ListenAddress )
2023-01-08 20:49:23 +00:00
}
2024-09-10 13:50:16 +00:00
fmt . Println ( message )
2023-01-08 20:49:23 +00:00
}
}
2023-09-29 09:50:34 +00:00
func ManualInstall ( c , sourceImgURL , device string , reboot , poweroff , strictValidations bool ) error {
2024-04-12 08:40:11 +00:00
configSource , err := prepareConfiguration ( c )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
2023-09-29 09:50:34 +00:00
cliConf := generateInstallConfForCLIArgs ( sourceImgURL )
2023-12-05 14:46:11 +00:00
cliConfManualArgs := generateInstallConfForManualCLIArgs ( device , reboot , poweroff )
2023-09-28 12:55:14 +00:00
2024-04-12 08:40:11 +00:00
cc , err := config . Scan (
collector . Readers ( configSource , strings . NewReader ( cliConf ) , strings . NewReader ( cliConfManualArgs ) ) ,
2023-09-28 12:55:14 +00:00
collector . MergeBootLine ,
collector . StrictValidation ( strictValidations ) , collector . NoLogs )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
2023-12-07 11:32:41 +00:00
2023-09-28 12:55:14 +00:00
return RunInstall ( cc )
2022-09-10 13:01:03 +00:00
}
2023-09-29 09:50:34 +00:00
func Install ( sourceImgURL string , dir ... string ) error {
2023-08-01 10:33:40 +00:00
var cc * config . Config
var err error
bus . Manager . Initialize ( )
2022-07-17 08:42:12 +00:00
utils . OnSignal ( func ( ) {
svc , err := machine . Getty ( 1 )
if err == nil {
2022-07-25 22:26:10 +00:00
svc . Start ( ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
}
} , syscall . SIGINT , syscall . SIGTERM )
tk := ""
r := map [ string ] string { }
bus . Manager . Response ( events . EventChallenge , func ( p * pluggable . Plugin , r * pluggable . EventResponse ) {
tk = r . Data
} )
2023-03-14 14:44:49 +00:00
2022-07-17 08:42:12 +00:00
bus . Manager . Response ( events . EventInstall , func ( p * pluggable . Plugin , resp * pluggable . EventResponse ) {
err := json . Unmarshal ( [ ] byte ( resp . Data ) , & r )
if err != nil {
fmt . Println ( err )
}
2023-08-01 10:33:40 +00:00
cloudConfig , exists := r [ "cc" ]
if exists {
2023-09-26 07:38:58 +00:00
// Re-read the full config and add the config coming from the event
2024-05-03 16:26:57 +00:00
cc , _ = config . Scan ( collector . Directories ( dir ... ) , collector . Overwrites ( cloudConfig ) , collector . MergeBootLine , collector . NoLogs )
2023-08-01 10:33:40 +00:00
}
2022-07-17 08:42:12 +00:00
} )
2023-01-30 15:12:11 +00:00
ensureDataSourceReady ( )
2022-07-21 21:38:07 +00:00
2023-09-29 09:50:34 +00:00
cliConf := generateInstallConfForCLIArgs ( sourceImgURL )
2023-09-28 12:55:14 +00:00
// Reads config, and if present and offline is defined, runs the installation
cc , err = config . Scan ( collector . Directories ( dir ... ) ,
collector . Readers ( strings . NewReader ( cliConf ) ) ,
collector . MergeBootLine )
2022-07-17 08:42:12 +00:00
if err == nil && cc . Install != nil && cc . Install . Auto {
2023-09-28 12:55:14 +00:00
err = RunInstall ( cc )
2023-03-29 14:25:38 +00:00
if err != nil {
return err
}
2022-07-17 08:42:12 +00:00
2024-02-28 06:51:06 +00:00
if ! cc . Install . Reboot && ! cc . Install . Poweroff {
2023-07-25 07:12:39 +00:00
pterm . DefaultInteractiveContinue . Show ( "Installation completed, press enter to go back to the shell." )
svc , err := machine . Getty ( 1 )
if err == nil {
svc . Start ( ) //nolint:errcheck
}
2022-07-17 08:42:12 +00:00
}
return nil
}
2022-10-24 14:57:02 +00:00
if err != nil {
fmt . Printf ( "- config not found in the system: %s" , err . Error ( ) )
}
2023-01-08 20:49:23 +00:00
agentConfig , err := LoadConfig ( )
2022-07-17 08:42:12 +00:00
if err != nil {
return err
}
2023-01-08 20:49:23 +00:00
// try to clear screen
cmd . ClearScreen ( )
2022-07-17 08:42:12 +00:00
cmd . PrintBranding ( DefaultBanner )
2023-03-14 14:44:49 +00:00
// If there are no providers registered, we enter a shell for manual installation
// and print information about the webUI
2023-01-08 20:49:23 +00:00
if ! bus . Manager . HasRegisteredPlugins ( ) {
displayInfo ( agentConfig )
2024-09-10 13:50:16 +00:00
fmt . Println ( "No providers found, dropping to a shell. \n -- For instructions on how to install manually, see: https://kairos.io/docs/installation/manual/" )
2023-01-08 20:49:23 +00:00
return utils . Shell ( ) . Run ( )
}
2023-03-29 14:25:38 +00:00
configStr , err := cc . String ( )
if err != nil {
return err
}
_ , err = bus . Manager . Publish ( events . EventChallenge , events . EventPayload { Config : configStr } )
2022-07-18 22:02:49 +00:00
if err != nil {
return err
}
cmd . PrintText ( agentConfig . Branding . Install , "Installation" )
2022-07-17 08:42:12 +00:00
2022-10-01 00:23:10 +00:00
if ! agentConfig . Fast {
time . Sleep ( 5 * time . Second )
}
2022-07-17 08:42:12 +00:00
if tk != "" {
qr . Print ( tk )
2024-09-10 13:50:16 +00:00
displayInfo ( agentConfig )
2022-07-17 08:42:12 +00:00
}
2023-03-29 14:25:38 +00:00
if _ , err := bus . Manager . Publish ( events . EventInstall , events . InstallPayload { Token : tk , Config : configStr } ) ; err != nil {
2022-07-17 08:42:12 +00:00
return err
}
if len ( r ) == 0 {
2023-08-01 10:33:40 +00:00
// This means there is no config in the system AND no config was obtained from events
2022-07-17 08:42:12 +00:00
return errors . New ( "no configuration, stopping installation" )
}
pterm . Info . Println ( "Starting installation" )
2023-08-01 10:33:40 +00:00
cc . Logger . Debugf ( "Runinstall with cc: %s\n" , litter . Sdump ( cc ) )
2023-09-28 12:55:14 +00:00
if err := RunInstall ( cc ) ; err != nil {
2022-07-25 22:26:10 +00:00
return err
}
2022-07-17 08:42:12 +00:00
2023-07-25 07:12:39 +00:00
if cc . Install . Reboot {
2024-05-13 14:27:47 +00:00
pterm . Info . Println ( "Installation completed, starting reboot in 5 seconds." )
2022-07-17 08:42:12 +00:00
}
2023-07-25 07:12:39 +00:00
if cc . Install . Poweroff {
2024-05-13 14:27:47 +00:00
pterm . Info . Println ( "Installation completed, starting power off in 5 seconds." )
2022-07-17 08:42:12 +00:00
}
2023-08-01 10:33:40 +00:00
// If neither reboot and poweroff are enabled let the user insert enter to go back to a new shell
// This is helpful to see the installation messages instead of just cleaning the screen with a new tty
2024-02-28 06:51:06 +00:00
if ! cc . Install . Reboot && ! cc . Install . Poweroff {
2023-07-25 07:12:39 +00:00
pterm . DefaultInteractiveContinue . Show ( "Installation completed, press enter to go back to the shell." )
utils . Prompt ( "" ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
2023-07-25 07:12:39 +00:00
// give tty1 back
svc , err := machine . Getty ( 1 )
if err == nil {
svc . Start ( ) //nolint: errcheck
}
2022-09-08 13:39:26 +00:00
}
2023-01-05 13:15:05 +00:00
2023-07-25 07:12:39 +00:00
return nil
}
2022-10-18 05:45:07 +00:00
2023-09-28 12:55:14 +00:00
func RunInstall ( c * config . Config ) error {
2023-08-08 16:52:04 +00:00
utils . SetEnv ( c . Env )
utils . SetEnv ( c . Install . Env )
2024-01-09 14:10:04 +00:00
// UKI path. Check if we are on UKI AND if we are running off a cd, otherwise it makes no sense to run the install
// From the installed system
2024-02-21 09:44:32 +00:00
if internalutils . IsUkiWithFs ( c . Fs ) {
2024-01-10 09:38:31 +00:00
c . Logger . Debugf ( "UKI mode: %s\n" , internalutils . UkiBootMode ( ) )
if internalutils . UkiBootMode ( ) == internalutils . UkiRemovableMedia {
return runInstallUki ( c )
}
c . Logger . Warnf ( "UKI boot mode is not removable media, skipping install" )
return nil
2024-01-09 14:10:04 +00:00
} else { // Non-uki path
return runInstall ( c )
2023-07-20 13:53:48 +00:00
}
2024-01-09 14:10:04 +00:00
}
2023-07-20 13:53:48 +00:00
2024-01-09 14:10:04 +00:00
// runInstallUki runs the UKI path install
func runInstallUki ( c * config . Config ) error {
// Load the spec from the config
installSpec , err := config . ReadUkiInstallSpecFromConfig ( c )
2023-07-20 13:53:48 +00:00
if err != nil {
return err
}
2024-01-09 14:10:04 +00:00
// Set our cloud-init to the file we just created
f , err := dumpCCStringToFile ( c )
if err == nil {
installSpec . CloudInit = append ( installSpec . CloudInit , f )
}
// Check if values are correct
err = installSpec . Sanitize ( )
2023-07-25 07:12:39 +00:00
if err != nil {
return err
}
2024-01-09 14:10:04 +00:00
// Add user's cloud-config (to run user defined "before-install" stages)
c . CloudInitPaths = append ( c . CloudInitPaths , installSpec . CloudInit ... )
installAction := uki . NewInstallAction ( c , installSpec )
return installAction . Run ( )
}
// runInstall runs the non-UKI path install
func runInstall ( c * config . Config ) error {
// Load the installation spec from the Config
installSpec , err := config . ReadInstallSpecFromConfig ( c )
2023-07-20 13:53:48 +00:00
if err != nil {
return err
}
2023-06-07 09:28:37 +00:00
2023-05-05 16:43:21 +00:00
// Set our cloud-init to the file we just created
2024-01-09 14:10:04 +00:00
f , err := dumpCCStringToFile ( c )
if err == nil {
installSpec . CloudInit = append ( installSpec . CloudInit , f )
2023-05-05 16:43:21 +00:00
}
// Check if values are correct
err = installSpec . Sanitize ( )
if err != nil {
return err
}
2023-06-07 09:28:37 +00:00
// Add user's cloud-config (to run user defined "before-install" stages)
2023-07-25 13:21:34 +00:00
c . CloudInitPaths = append ( c . CloudInitPaths , installSpec . CloudInit ... )
2023-06-07 09:28:37 +00:00
2023-07-25 13:21:34 +00:00
installAction := action . NewInstallAction ( c , installSpec )
2024-01-09 14:10:04 +00:00
return installAction . Run ( )
}
// dumpCCStringToFile dumps the cloud-init string to a file and returns the path of the file
func dumpCCStringToFile ( c * config . Config ) ( string , error ) {
f , err := fsutils . TempFile ( c . Fs , "" , "kairos-install-config-xxx.yaml" )
if err != nil {
2024-03-01 11:27:26 +00:00
c . Logger . Errorf ( "Error creating temporary file for install config: %s" , err . Error ( ) )
2024-01-09 14:10:04 +00:00
return "" , err
2022-07-17 08:42:12 +00:00
}
2024-01-10 09:38:31 +00:00
defer func ( f * os . File ) {
_ = f . Close ( )
} ( f )
2024-01-09 14:10:04 +00:00
ccstring , err := c . String ( )
if err != nil {
return "" , err
}
err = os . WriteFile ( f . Name ( ) , [ ] byte ( ccstring ) , os . ModePerm )
if err != nil {
fmt . Printf ( "could not write cloud init to %s: %s\n" , f . Name ( ) , err . Error ( ) )
return "" , err
}
return f . Name ( ) , nil
2022-07-17 08:42:12 +00:00
}
2023-01-30 15:12:11 +00:00
func ensureDataSourceReady ( ) {
timeout := time . NewTimer ( 5 * time . Minute )
ticker := time . NewTicker ( 500 * time . Millisecond )
defer timeout . Stop ( )
defer ticker . Stop ( )
for {
select {
case <- timeout . C :
fmt . Println ( "userdata configuration failed to load after 5m, ignoring." )
return
case <- ticker . C :
if _ , err := os . Stat ( "/run/.userdata_load" ) ; os . IsNotExist ( err ) {
return
}
fmt . Println ( "userdata configuration has not yet completed. (waiting for /run/.userdata_load to be deleted)" )
}
}
}
2023-03-08 17:13:36 +00:00
2024-04-12 08:40:11 +00:00
func prepareConfiguration ( source string ) ( io . Reader , error ) {
var cfg io . Reader
// source can be either a file in the system or an url
// We need to differentiate between the two
// If its a local file, we just read it and return it
// If its a url, we need to create a configuration with the url and let the config.Scan handle it
2023-03-08 17:13:36 +00:00
// if the source is not an url it is already a configuration path
if u , err := url . Parse ( source ) ; err != nil || u . Scheme == "" {
2024-04-12 08:40:11 +00:00
file , err := os . ReadFile ( source )
if err != nil {
return cfg , err
}
cfg = bytes . NewReader ( file )
return cfg , nil
2023-03-08 17:13:36 +00:00
}
2024-09-11 13:57:53 +00:00
// Its a remote url
// Check if it actually exists and fail if it doesn't
resp , err := http . Head ( source )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
if resp . StatusCode == http . StatusNotFound {
return nil , errors . New ( "configuration file not found in remote address" )
} else {
return nil , errors . New ( resp . Status )
}
}
2023-03-08 17:13:36 +00:00
2024-04-12 08:40:11 +00:00
cfgUrl := fmt . Sprintf ( ` config_url: %s ` , source )
cfg = strings . NewReader ( cfgUrl )
2023-03-08 17:13:36 +00:00
2024-04-12 08:40:11 +00:00
return cfg , nil
2023-03-08 17:13:36 +00:00
}
2023-09-28 12:55:14 +00:00
2023-09-29 09:50:34 +00:00
func generateInstallConfForCLIArgs ( sourceImageURL string ) string {
if sourceImageURL == "" {
2023-09-28 12:55:14 +00:00
return ""
}
return fmt . Sprintf ( ` install :
2024-01-23 16:05:54 +00:00
source : % s
2023-09-29 09:50:34 +00:00
` , sourceImageURL )
2023-09-28 12:55:14 +00:00
}
2023-12-05 14:46:11 +00:00
// generateInstallConfForManualCLIArgs creates a kairos configuration for flags passed via manual install
func generateInstallConfForManualCLIArgs ( device string , reboot , poweroff bool ) string {
cfg := fmt . Sprintf ( ` install :
reboot : % t
poweroff : % t
` , reboot , poweroff )
if device != "" {
cfg += fmt . Sprintf ( `
device : % s
` , device )
}
return cfg
}