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-05-24 08:39:17 +00:00
"github.com/sirupsen/logrus"
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"
2023-03-15 14:45:00 +00:00
events "github.com/kairos-io/kairos-sdk/bus"
2023-03-18 09:27:18 +00:00
"github.com/kairos-io/kairos-sdk/machine"
"github.com/kairos-io/kairos-sdk/utils"
2023-03-30 11:18:53 +00:00
hook "github.com/kairos-io/kairos/v2/internal/agent/hooks"
"github.com/kairos-io/kairos/v2/internal/bus"
"github.com/kairos-io/kairos/v2/internal/cmd"
2023-05-05 16:43:21 +00:00
"github.com/kairos-io/kairos/v2/pkg/action"
"github.com/kairos-io/kairos/v2/pkg/config"
2023-03-30 11:18:53 +00:00
"github.com/kairos-io/kairos/v2/pkg/config/collector"
2023-05-05 16:43:21 +00:00
"github.com/kairos-io/kairos/v2/pkg/elementalConfig"
v1 "github.com/kairos-io/kairos/v2/pkg/types/v1"
2023-05-16 16:06:49 +00:00
elementalUtils "github.com/kairos-io/kairos/v2/pkg/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"
)
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-03-14 14:44:49 +00:00
func mergeOption ( cloudConfig string , r map [ string ] string ) {
c := & config . Config { }
yaml . Unmarshal ( [ ] byte ( cloudConfig ) , c ) //nolint:errcheck
for k , v := range c . Options {
if k == "cc" {
continue
}
r [ k ] = v
}
}
2023-05-24 08:39:17 +00:00
func ManualInstall ( c string , options map [ string ] string , strictValidations , debug 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-29 14:25:38 +00:00
cc , err := config . Scan ( collector . Directories ( source ) , collector . MergeBootLine , collector . StrictValidation ( strictValidations ) )
2023-02-03 17:41:35 +00:00
if err != nil {
return err
}
2023-03-29 14:25:38 +00:00
configStr , err := cc . String ( )
if err != nil {
return err
}
options [ "cc" ] = configStr
2023-03-14 14:44:49 +00:00
// unlike Install device is already set
// options["device"] = cc.Install.Device
2023-03-29 14:25:38 +00:00
mergeOption ( configStr , options )
2022-09-10 13:01:03 +00:00
2023-03-08 17:13:36 +00:00
if options [ "device" ] == "" {
2023-05-24 08:39:17 +00:00
if cc . Install . Device == "" {
options [ "device" ] = detectDevice ( )
} else {
options [ "device" ] = cc . Install . Device
}
2023-03-08 17:13:36 +00:00
}
2023-05-24 08:39:17 +00:00
return RunInstall ( debug , options )
2022-09-10 13:01:03 +00:00
}
2023-05-24 08:39:17 +00:00
func Install ( debug bool , dir ... string ) error {
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-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-03-29 14:25:38 +00:00
cc , err := config . Scan ( collector . Directories ( dir ... ) , collector . MergeBootLine , collector . NoLogs )
2022-07-17 08:42:12 +00:00
if err == nil && cc . Install != nil && cc . Install . Auto {
2023-03-29 14:25:38 +00:00
configStr , err := cc . String ( )
if err != nil {
return err
}
r [ "cc" ] = configStr
2022-07-17 08:42:12 +00:00
r [ "device" ] = cc . Install . Device
2023-03-29 14:25:38 +00:00
mergeOption ( configStr , r )
2022-07-17 08:42:12 +00:00
2023-05-24 08:39:17 +00:00
err = RunInstall ( debug , r )
2022-07-25 22:26:10 +00:00
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-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 {
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
2023-03-14 14:44:49 +00:00
mergeOption ( cloudConfig , r )
2022-07-17 08:42:12 +00:00
2023-03-14 14:44:49 +00:00
// now merge cloud config from system and
// the one received from the agent-provider
2022-07-17 08:42:12 +00:00
ccData := map [ string ] interface { } { }
2023-03-14 14:44:49 +00:00
// make sure the config we write has at least the #cloud-config header,
// if any other was defined beforeahead
2023-01-20 18:14:53 +00:00
header := "#cloud-config"
2023-03-29 14:25:38 +00:00
if hasHeader , head := config . HasHeader ( configStr , "" ) ; hasHeader {
2022-07-17 08:42:12 +00:00
header = head
}
2022-07-25 22:26:10 +00:00
// What we receive take precedence over the one in the system. best-effort
2023-03-29 14:25:38 +00:00
yaml . Unmarshal ( [ ] byte ( configStr ) , & 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" )
2023-05-24 08:39:17 +00:00
if err := RunInstall ( debug , r ) ; err != nil {
2022-07-25 22:26:10 +00:00
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
}
2023-05-24 08:39:17 +00:00
func RunInstall ( debug bool , options map [ string ] string ) error {
2023-05-16 16:06:49 +00:00
// Load the installation Config from the system
installConfig , err := elementalConfig . ReadConfigRun ( "/etc/elemental" )
if err != nil {
return err
}
2023-05-24 08:39:17 +00:00
if debug {
installConfig . Logger . SetLevel ( logrus . DebugLevel )
}
2023-05-16 16:06:49 +00:00
// Run pre-install stage
_ = elementalUtils . RunStage ( & installConfig . Config , "kairos-install.pre" , installConfig . Strict , installConfig . CloudInitPaths ... )
2022-09-17 16:43:51 +00:00
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 ( ) )
cloudInit , ok := options [ "cc" ]
if ! ok {
fmt . Println ( "cloudInit must be specified among options" )
os . Exit ( 1 )
}
2023-05-05 16:43:21 +00:00
// TODO: Drop this and make a more straighforward way of getting the cloud-init and options?
2022-07-17 08:42:12 +00:00
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
2022-09-08 13:39:26 +00:00
if c . Install == nil {
c . Install = & config . Install { }
}
2023-01-05 13:15:05 +00:00
2023-05-05 16:43:21 +00:00
// TODO: Im guessing this was used to try to override elemental values from env vars
// Does it make sense anymore? We can now expose the whole options of elemental directly
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
2023-05-16 16:06:49 +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
2023-05-05 16:43:21 +00:00
_ , reboot := options [ "reboot" ]
_ , poweroff := options [ "poweroff" ]
installConfig . Reboot = reboot
installConfig . PowerOff = poweroff
// Generate the installation spec
installSpec , err := elementalConfig . ReadInstallSpec ( installConfig )
if err != nil {
return err
}
// 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
}
// Set the target device
device , ok := options [ "device" ]
if ! ok {
fmt . Println ( "device must be specified among options" )
os . Exit ( 1 )
}
if device == "auto" {
device = detectDevice ( )
}
installSpec . Target = device
// Check if values are correct
err = installSpec . Sanitize ( )
if err != nil {
return err
}
// Create the action
installAction := action . NewInstallAction ( installConfig , installSpec )
// Run it
if err := installAction . Run ( ) ; err != nil {
2022-07-17 08:42:12 +00:00
fmt . Println ( err )
os . Exit ( 1 )
}
2023-03-30 14:42:25 +00:00
return hook . Run ( * c , 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
}