mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-04-27 19:28:59 +00:00
302 lines
6.4 KiB
Go
302 lines
6.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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/config"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/elementalConfig"
|
|
|
|
events "github.com/kairos-io/kairos-sdk/bus"
|
|
"github.com/kairos-io/kairos-sdk/unstructured"
|
|
|
|
"github.com/erikgeiser/promptkit/textinput"
|
|
"github.com/jaypipes/ghw"
|
|
"github.com/kairos-io/kairos-sdk/utils"
|
|
"github.com/mudler/go-pluggable"
|
|
"github.com/mudler/yip/pkg/schema"
|
|
"github.com/pterm/pterm"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const (
|
|
canBeEmpty = "Unset"
|
|
yesNo = "[y]es/[N]o"
|
|
)
|
|
|
|
func prompt(prompt, initialValue, placeHolder string, canBeEmpty, hidden bool) (string, error) {
|
|
input := textinput.New(prompt)
|
|
input.InitialValue = initialValue
|
|
input.Placeholder = placeHolder
|
|
if canBeEmpty {
|
|
input.Validate = func(s string) error { return nil }
|
|
}
|
|
input.Hidden = hidden
|
|
|
|
return input.RunPrompt()
|
|
}
|
|
|
|
func isYes(s string) bool {
|
|
i := strings.ToLower(s)
|
|
if i == "y" || i == "yes" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const (
|
|
_ = 1 << (10 * iota)
|
|
KiB
|
|
MiB
|
|
GiB
|
|
TiB
|
|
)
|
|
|
|
func promptBool(p events.YAMLPrompt) (string, error) {
|
|
def := "n"
|
|
if p.Default != "" {
|
|
def = p.Default
|
|
}
|
|
val, err := prompt(p.Prompt, def, yesNo, true, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if isYes(val) {
|
|
val = "true"
|
|
} else {
|
|
val = "false"
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
func promptText(p events.YAMLPrompt) (string, error) {
|
|
def := ""
|
|
if p.Default != "" {
|
|
def = p.Default
|
|
}
|
|
return prompt(p.Prompt, def, p.PlaceHolder, true, false)
|
|
}
|
|
|
|
func promptToUnstructured(p events.YAMLPrompt, unstructuredYAML map[string]interface{}) (map[string]interface{}, error) {
|
|
var res string
|
|
if p.AskFirst {
|
|
ask, err := prompt(p.AskPrompt, "n", yesNo, true, false)
|
|
if err == nil && !isYes(ask) {
|
|
return unstructuredYAML, nil
|
|
}
|
|
}
|
|
if p.Bool {
|
|
val, err := promptBool(p)
|
|
if err != nil {
|
|
return unstructuredYAML, err
|
|
}
|
|
unstructuredYAML[p.YAMLSection] = val
|
|
res = val
|
|
} else {
|
|
val, err := promptText(p)
|
|
if err != nil {
|
|
return unstructuredYAML, err
|
|
}
|
|
unstructuredYAML[p.YAMLSection] = val
|
|
res = val
|
|
}
|
|
|
|
if res == "" && p.IfEmpty != "" {
|
|
res = p.IfEmpty
|
|
unstructuredYAML[p.YAMLSection] = res
|
|
}
|
|
return unstructuredYAML, nil
|
|
}
|
|
|
|
func detectDevice() string {
|
|
preferedDevice := "/dev/sda"
|
|
maxSize := float64(0)
|
|
|
|
block, err := ghw.Block()
|
|
if err == nil {
|
|
for _, disk := range block.Disks {
|
|
size := float64(disk.SizeBytes) / float64(GiB)
|
|
if size > maxSize {
|
|
maxSize = size
|
|
preferedDevice = "/dev/" + disk.Name
|
|
}
|
|
}
|
|
}
|
|
return preferedDevice
|
|
}
|
|
|
|
func InteractiveInstall(debug, spawnShell bool) error {
|
|
var sshUsers []string
|
|
bus.Manager.Initialize()
|
|
|
|
cmd.PrintBranding(DefaultBanner)
|
|
|
|
agentConfig, err := LoadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.PrintText(agentConfig.Branding.InteractiveInstall, "Installation")
|
|
|
|
disks := []string{}
|
|
maxSize := float64(0)
|
|
preferedDevice := "/dev/sda"
|
|
|
|
block, err := ghw.Block()
|
|
if err == nil {
|
|
for _, disk := range block.Disks {
|
|
size := float64(disk.SizeBytes) / float64(GiB)
|
|
if size > maxSize {
|
|
maxSize = size
|
|
preferedDevice = "/dev/" + disk.Name
|
|
}
|
|
disks = append(disks, fmt.Sprintf("/dev/%s: %s (%.2f GiB) ", disk.Name, disk.Model, float64(disk.SizeBytes)/float64(GiB)))
|
|
}
|
|
}
|
|
|
|
pterm.Info.Println("Available Disks:")
|
|
for _, d := range disks {
|
|
pterm.Info.Println(" " + d)
|
|
}
|
|
|
|
device, err := prompt("What's the target install device?", preferedDevice, "Cannot be empty", false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userName, err := prompt("User to setup", "kairos", canBeEmpty, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userPassword, err := prompt("Password", "", canBeEmpty, true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if userPassword == "" {
|
|
userPassword = "!"
|
|
}
|
|
|
|
users, err := prompt("SSH access (rsakey, github/gitlab supported, comma-separated)", "github:someuser,github:someuser2", canBeEmpty, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Cleanup the users if we selected the default values as they are not valid users
|
|
if users == "github:someuser,github:someuser2" {
|
|
users = ""
|
|
}
|
|
if users != "" {
|
|
sshUsers = strings.Split(users, ",")
|
|
}
|
|
|
|
// Prompt the user by prompts defined by the provider
|
|
r := []events.YAMLPrompt{}
|
|
|
|
bus.Manager.Response(events.EventInteractiveInstall, func(p *pluggable.Plugin, resp *pluggable.EventResponse) {
|
|
err := json.Unmarshal([]byte(resp.Data), &r)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
})
|
|
|
|
_, err = bus.Manager.Publish(events.EventInteractiveInstall, events.EventPayload{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
unstructuredYAML := map[string]interface{}{}
|
|
for _, p := range r {
|
|
unstructuredYAML, err = promptToUnstructured(p, unstructuredYAML)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
result, err := unstructured.ToYAMLMap(unstructuredYAML)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
allGood, err := prompt("Are settings ok?", "n", yesNo, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isYes(allGood) {
|
|
return InteractiveInstall(debug, spawnShell)
|
|
}
|
|
|
|
c := &config.Config{
|
|
Install: &config.Install{
|
|
Device: device,
|
|
},
|
|
}
|
|
|
|
usersToSet := map[string]schema.User{}
|
|
|
|
stage := config.NetworkStage.String()
|
|
|
|
if userName != "" {
|
|
user := schema.User{
|
|
Name: userName,
|
|
PasswordHash: userPassword,
|
|
Groups: []string{"admin"},
|
|
SSHAuthorizedKeys: sshUsers,
|
|
}
|
|
|
|
// If we got no ssh keys, we don't need network, do the user as soon as possible
|
|
if len(sshUsers) == 0 {
|
|
stage = config.InitramfsStage.String()
|
|
}
|
|
|
|
usersToSet = map[string]schema.User{
|
|
userName: user,
|
|
}
|
|
}
|
|
|
|
cloudConfig := schema.YipConfig{Name: "Config generated by the installer",
|
|
Stages: map[string][]schema.Stage{stage: {
|
|
{
|
|
Users: usersToSet,
|
|
},
|
|
}}}
|
|
|
|
dat, err := config.MergeYAML(cloudConfig, c, result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
finalCloudConfig := config.AddHeader("#cloud-config", string(dat))
|
|
|
|
pterm.Info.Println("Starting installation")
|
|
pterm.Info.Println(finalCloudConfig)
|
|
|
|
// Set debug from here already, so it's loaded by the ReadConfigRun
|
|
viper.Set("debug", debug)
|
|
|
|
// Load the installation Config from the system
|
|
installConfig, err := elementalConfig.ReadConfigRun("/etc/elemental")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = RunInstall(installConfig, map[string]string{
|
|
"device": device,
|
|
"cc": finalCloudConfig,
|
|
})
|
|
if err != nil {
|
|
pterm.Error.Println(err.Error())
|
|
}
|
|
|
|
if spawnShell {
|
|
return utils.Shell().Run()
|
|
}
|
|
return err
|
|
}
|