2022-07-17 08:42:12 +00:00
package agent
import (
2023-03-08 17:13:36 +00:00
"context"
2022-07-17 08:42:12 +00:00
"encoding/json"
"errors"
"fmt"
2023-07-25 13:21:34 +00:00
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
2023-08-01 10:33:40 +00:00
"github.com/sanity-io/litter"
2023-03-08 17:13:36 +00:00
"net/url"
2022-07-17 08:42:12 +00:00
"os"
2023-08-01 10:33:40 +00:00
"path/filepath"
2023-01-08 20:49:23 +00:00
"strings"
2022-07-17 08:42:12 +00:00
"syscall"
"time"
2023-07-10 12:39:48 +00:00
hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks"
"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"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils"
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"
"gopkg.in/yaml.v2"
)
2023-01-08 20:49:23 +00:00
func displayInfo ( agentConfig * Config ) {
fmt . Println ( "--------------------------" )
fmt . Println ( "No providers found, dropping to a shell. \n -- For instructions on how to install manually, see: https://kairos.io/docs/installation/manual/" )
if ! agentConfig . WebUI . Disable {
if ! agentConfig . WebUI . HasAddress ( ) {
ips := machine . LocalIPs ( )
if len ( ips ) > 0 {
fmt . Print ( "WebUI installer running at : " )
for _ , ip := range ips {
fmt . Printf ( "%s%s " , ip , config . DefaultWebUIListenAddress )
}
fmt . Print ( "\n" )
}
} else {
fmt . Printf ( "WebUI installer running at : %s\n" , agentConfig . WebUI . ListenAddress )
}
ifaces := machine . Interfaces ( )
fmt . Printf ( "Network Interfaces: %s\n" , strings . Join ( ifaces , " " ) )
}
}
2023-07-25 07:12:39 +00:00
func ManualInstall ( c , device string , reboot , poweroff , strictValidations bool ) error {
2023-03-08 17:13:36 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2023-02-03 17:41:35 +00:00
2023-03-08 17:13:36 +00:00
source , err := prepareConfiguration ( ctx , c )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
2023-08-03 17:46:13 +00:00
cc , err := config . Scan ( collector . Directories ( source ) , collector . MergeBootLine , collector . StrictValidation ( strictValidations ) , collector . NoLogs )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
2023-07-25 07:12:39 +00:00
if reboot {
// Override from flags!
cc . Install . Reboot = true
2023-03-29 14:25:38 +00:00
}
2023-07-25 07:12:39 +00:00
if poweroff {
// Override from flags!
cc . Install . Poweroff = true
}
if device != "" {
// Override from flags!
cc . Install . Device = device
2023-03-08 17:13:36 +00:00
}
2023-07-25 07:12:39 +00:00
return RunInstall ( cc )
2022-09-10 13:01:03 +00:00
}
2023-06-21 07:42:11 +00:00
func Install ( 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
// dump data into a dir so the collector can pick it up properly
cloudConfig , exists := r [ "cc" ]
if exists {
tmpdir , err := os . MkdirTemp ( "" , "kairos-install-" )
if err == nil {
err = os . WriteFile ( filepath . Join ( tmpdir , "kairos-event-install-data.yaml" ) , [ ] byte ( cloudConfig ) , os . ModePerm )
if err != nil {
fmt . Printf ( "could not write event cloud init: %s\n" , err . Error ( ) )
}
// Append to default dirs so we read from all sources
dir = append ( dir , tmpdir )
// override cc with our new config object from the scan, so it's updated for the RunInstall function
cc , _ = config . Scan ( collector . Directories ( dir ... ) , collector . MergeBootLine , collector . NoLogs )
} else {
fmt . Printf ( "could not create temp dir: %s\n" , err . Error ( ) )
}
}
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
2022-07-17 08:42:12 +00:00
// Reads config, and if present and offline is defined,
// runs the installation
2023-08-01 10:33:40 +00:00
cc , err = config . Scan ( collector . Directories ( dir ... ) , collector . MergeBootLine )
2022-07-17 08:42:12 +00:00
if err == nil && cc . Install != nil && cc . Install . Auto {
2023-07-25 07:12:39 +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
2023-07-25 07:12:39 +00:00
if cc . Install . Reboot == false && cc . Install . Poweroff == false {
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 )
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 )
}
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-07-25 07:12:39 +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 {
2023-08-01 10:33:40 +00:00
pterm . Info . Println ( "Installation completed, rebooting in 5 seconds." )
2022-07-17 08:42:12 +00:00
}
2023-07-25 07:12:39 +00:00
if cc . Install . Poweroff {
2023-08-01 10:33:40 +00:00
pterm . Info . Println ( "Installation completed, powering 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
2023-07-25 07:12:39 +00:00
if cc . Install . Reboot == false && cc . Install . Poweroff == false {
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-07-25 07:12:39 +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 )
2023-07-25 07:12:39 +00:00
if c . Install . Device == "" || c . Install . Device == "auto" {
c . Install . Device = detectDevice ( )
2023-05-31 09:42:44 +00:00
}
2023-05-05 16:43:21 +00:00
2023-07-25 13:21:34 +00:00
// 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-07-25 13:21:34 +00:00
f , err := fsutils . TempFile ( c . Fs , "" , "kairos-install-config-xxx.yaml" )
2023-07-20 13:53:48 +00:00
if err != nil {
2023-07-25 13:21:34 +00:00
c . Logger . Error ( "Error creating temporary file for install config: %s\n" , err . Error ( ) )
2023-07-20 13:53:48 +00:00
return err
}
defer os . RemoveAll ( f . Name ( ) )
2023-07-25 07:12:39 +00:00
ccstring , err := c . String ( )
if err != nil {
return err
}
err = os . WriteFile ( f . Name ( ) , [ ] byte ( ccstring ) , os . ModePerm )
2023-07-20 13:53:48 +00:00
if err != nil {
fmt . Printf ( "could not write cloud init to %s: %s\n" , f . Name ( ) , err . Error ( ) )
return err
}
2023-06-07 09:28:37 +00:00
2023-07-25 13:21:34 +00:00
// TODO: This should not be neccessary
2023-06-07 09:28:37 +00:00
installSpec . NoFormat = c . Install . NoFormat
2023-05-05 16:43:21 +00:00
// Set our cloud-init to the file we just created
installSpec . CloudInit = append ( installSpec . CloudInit , f . Name ( ) )
// Get the source of the installation if we are overriding it
if c . Install . Image != "" {
imgSource , err := v1 . NewSrcFromURI ( c . Install . Image )
if err != nil {
return err
}
installSpec . Active . Source = imgSource
}
// 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
// Run pre-install stage
2023-07-25 13:21:34 +00:00
_ = elementalUtils . RunStage ( c , "kairos-install.pre" )
2023-06-07 09:28:37 +00:00
events . RunHookScript ( "/usr/bin/kairos-agent.install.pre.hook" ) //nolint:errcheck
2023-05-05 16:43:21 +00:00
// Create the action
2023-07-25 13:21:34 +00:00
installAction := action . NewInstallAction ( c , installSpec )
2023-05-05 16:43:21 +00:00
// Run it
if err := installAction . Run ( ) ; err != nil {
2022-07-17 08:42:12 +00:00
fmt . Println ( err )
os . Exit ( 1 )
}
2023-07-25 13:21:34 +00:00
_ = elementalUtils . RunStage ( c , "kairos-install.after" )
events . RunHookScript ( "/usr/bin/kairos-agent.install.after.hook" ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
2023-07-24 10:28:59 +00:00
return hook . Run ( * c , installSpec , hook . AfterInstall ... )
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
func prepareConfiguration ( ctx context . Context , source string ) ( string , error ) {
// if the source is not an url it is already a configuration path
if u , err := url . Parse ( source ) ; err != nil || u . Scheme == "" {
return source , nil
}
// create a configuration file with the source referenced
f , err := os . CreateTemp ( os . TempDir ( ) , "kairos-install-*.yaml" )
if err != nil {
return "" , err
}
// defer cleanup until after parent is done
go func ( ) {
<- ctx . Done ( )
_ = os . RemoveAll ( f . Name ( ) )
} ( )
cfg := config . Config {
ConfigURL : source ,
}
if err = yaml . NewEncoder ( f ) . Encode ( cfg ) ; err != nil {
return "" , err
}
return f . Name ( ) , nil
}