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-03-08 17:13:36 +00:00
"net/url"
2022-07-17 08:42:12 +00:00
"os"
"os/exec"
2023-01-08 20:49:23 +00:00
"strings"
2022-07-17 08:42:12 +00:00
"syscall"
"time"
2022-09-17 16:43:51 +00:00
events "github.com/kairos-io/kairos/sdk/bus"
2022-08-09 06:01:54 +00:00
2022-09-17 16:43:51 +00:00
config "github.com/kairos-io/kairos/pkg/config"
2022-07-17 08:42:12 +00:00
2022-09-17 16:43:51 +00:00
hook "github.com/kairos-io/kairos/internal/agent/hooks"
"github.com/kairos-io/kairos/internal/bus"
2022-08-10 16:56:07 +00:00
2022-09-17 16:43:51 +00:00
"github.com/kairos-io/kairos/internal/cmd"
"github.com/kairos-io/kairos/pkg/utils"
2022-07-17 08:42:12 +00:00
2022-09-17 16:43:51 +00:00
machine "github.com/kairos-io/kairos/pkg/machine"
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"
)
func optsToArgs ( options map [ string ] string ) ( res [ ] string ) {
for k , v := range options {
if k != "device" && k != "cc" && k != "reboot" && k != "poweroff" {
res = append ( res , fmt . Sprintf ( "--%s" , k ) )
2022-09-23 15:20:23 +00:00
if v != "" {
res = append ( res , v )
}
2022-07-17 08:42:12 +00:00
}
}
return
}
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-02-14 15:15:13 +00:00
func ManualInstall ( c string , options map [ string ] string , 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-03-08 17:13:36 +00:00
cc , err := config . Scan ( config . Directories ( source ) , config . MergeBootLine , config . StrictValidation ( strictValidations ) )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
options [ "cc" ] = cc . String ( )
2022-09-10 13:01:03 +00:00
2023-03-08 17:13:36 +00:00
if options [ "device" ] == "" {
options [ "device" ] = cc . Install . Device
}
2022-09-10 13:01:03 +00:00
return RunInstall ( options )
}
2022-07-17 08:42:12 +00:00
func Install ( dir ... string ) error {
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 { }
mergeOption := func ( cloudConfig string ) {
c := & config . Config { }
2022-07-25 22:26:10 +00:00
yaml . Unmarshal ( [ ] byte ( cloudConfig ) , c ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
for k , v := range c . Options {
if k == "cc" {
continue
}
r [ k ] = v
}
}
bus . Manager . Response ( events . EventChallenge , func ( p * pluggable . Plugin , r * pluggable . EventResponse ) {
tk = r . Data
} )
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-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-01-08 20:49:23 +00:00
cc , err := config . Scan ( config . Directories ( dir ... ) , config . MergeBootLine , config . NoLogs )
2022-07-17 08:42:12 +00:00
if err == nil && cc . Install != nil && cc . Install . Auto {
r [ "cc" ] = cc . String ( )
r [ "device" ] = cc . Install . Device
mergeOption ( cc . String ( ) )
2022-07-25 22:26:10 +00:00
err = RunInstall ( r )
if err != nil {
return err
}
2022-07-17 08:42:12 +00:00
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
}
return nil
}
2022-10-24 14:57:02 +00:00
if err != nil {
fmt . Printf ( "- config not found in the system: %s" , err . Error ( ) )
}
2022-07-17 08:42:12 +00:00
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-01-08 20:49:23 +00:00
// If there are no providers registered, we enter a shell for manual installation and print information about
// the webUI
if ! bus . Manager . HasRegisteredPlugins ( ) {
displayInfo ( agentConfig )
return utils . Shell ( ) . Run ( )
}
_ , err = bus . Manager . Publish ( events . EventChallenge , events . EventPayload { Config : cc . String ( ) } )
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 )
}
if _ , err := bus . Manager . Publish ( events . EventInstall , events . InstallPayload { Token : tk , Config : cc . String ( ) } ) ; err != nil {
return err
}
if len ( r ) == 0 {
return errors . New ( "no configuration, stopping installation" )
}
// we receive a cloud config at this point
cloudConfig , exists := r [ "cc" ]
// merge any options defined in it
mergeOption ( cloudConfig )
// now merge cloud config from system and the one received from the agent-provider
ccData := map [ string ] interface { } { }
2023-01-20 18:14:53 +00:00
// make sure the config we write has at least the #cloud-config header, if any other was defined beforeahead
header := "#cloud-config"
2022-07-17 08:42:12 +00:00
if hasHeader , head := config . HasHeader ( cc . String ( ) , "" ) ; hasHeader {
header = head
}
2022-07-25 22:26:10 +00:00
// What we receive take precedence over the one in the system. best-effort
yaml . Unmarshal ( [ ] byte ( cc . String ( ) ) , & ccData ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
if exists {
2022-07-25 22:26:10 +00:00
yaml . Unmarshal ( [ ] byte ( cloudConfig ) , & ccData ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
if hasHeader , head := config . HasHeader ( cloudConfig , "" ) ; hasHeader {
header = head
}
}
out , err := yaml . Marshal ( ccData )
if err != nil {
return fmt . Errorf ( "failed marshalling cc: %w" , err )
}
r [ "cc" ] = config . AddHeader ( header , string ( out ) )
pterm . Info . Println ( "Starting installation" )
2022-07-25 22:26:10 +00:00
if err := RunInstall ( r ) ; err != nil {
return err
}
2022-07-17 08:42:12 +00:00
pterm . Info . Println ( "Installation completed, press enter to go back to the shell." )
2022-07-25 22:26:10 +00:00
utils . Prompt ( "" ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
// give tty1 back
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
}
return nil
}
func RunInstall ( options map [ string ] string ) error {
2022-09-17 16:43:51 +00:00
utils . SH ( "elemental run-stage kairos-install.pre" ) //nolint:errcheck
events . RunHookScript ( "/usr/bin/kairos-agent.install.pre.hook" ) //nolint:errcheck
2022-09-10 13:01:03 +00:00
2022-11-21 17:11:03 +00:00
f , _ := os . CreateTemp ( "" , "xxxx" )
2022-07-17 08:42:12 +00:00
defer os . RemoveAll ( f . Name ( ) )
device , ok := options [ "device" ]
if ! ok {
fmt . Println ( "device must be specified among options" )
os . Exit ( 1 )
}
2022-09-10 13:01:24 +00:00
if device == "auto" {
device = detectDevice ( )
}
2022-07-17 08:42:12 +00:00
cloudInit , ok := options [ "cc" ]
if ! ok {
fmt . Println ( "cloudInit must be specified among options" )
os . Exit ( 1 )
}
c := & config . Config { }
2022-07-25 22:26:10 +00:00
yaml . Unmarshal ( [ ] byte ( cloudInit ) , c ) //nolint:errcheck
2022-07-17 08:42:12 +00:00
_ , reboot := options [ "reboot" ]
_ , poweroff := options [ "poweroff" ]
2022-09-08 13:39:26 +00:00
if c . Install == nil {
c . Install = & config . Install { }
}
if poweroff {
c . Install . Poweroff = true
}
if reboot {
c . Install . Reboot = true
}
2022-07-17 08:42:12 +00:00
2023-01-05 13:15:05 +00:00
if c . Install . Image != "" {
options [ "system.uri" ] = c . Install . Image
}
2022-10-24 11:13:23 +00:00
env := append ( c . Install . Env , c . Env ... )
utils . SetEnv ( env )
2022-10-18 05:45:07 +00:00
2022-11-21 17:11:03 +00:00
err := os . WriteFile ( f . Name ( ) , [ ] byte ( cloudInit ) , os . ModePerm )
2022-07-25 22:26:10 +00:00
if err != nil {
fmt . Printf ( "could not write cloud init: %s\n" , err . Error ( ) )
os . Exit ( 1 )
}
2022-07-17 08:42:12 +00:00
args := [ ] string { "install" }
args = append ( args , optsToArgs ( options ) ... )
2022-07-25 22:26:10 +00:00
args = append ( args , "-c" , f . Name ( ) , device )
2022-07-17 08:42:12 +00:00
cmd := exec . Command ( "elemental" , args ... )
2022-10-24 11:13:23 +00:00
cmd . Env = os . Environ ( )
2022-07-17 08:42:12 +00:00
cmd . Stdout = os . Stdout
cmd . Stdin = os . Stdin
cmd . Stderr = os . Stderr
if err := cmd . Run ( ) ; err != nil {
fmt . Println ( err )
os . Exit ( 1 )
}
2023-01-25 12:38:56 +00:00
if err := hook . Run ( * c , hook . AfterInstall ... ) ; err != nil {
2022-09-08 13:39:26 +00:00
return err
2022-07-17 08:42:12 +00:00
}
return nil
}
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
}