kairos-agent/internal/agent/install.go
Ettore Di Giacinto 09b41735f4 gear: Be sure to run datasource before install
Seems in certain condition the datasource is kicking late and serving the datasource not fast as wanted.

We make sure to pull it before install, so we also give chance to convoluted setups to pull configs.

Also wires up autoinstall tests to CI
2022-07-21 21:38:07 +00:00

219 lines
5.1 KiB
Go

package agent
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"syscall"
"time"
events "github.com/c3os-io/c3os/pkg/bus"
config "github.com/c3os-io/c3os/pkg/config"
"github.com/c3os-io/c3os/internal/bus"
"github.com/c3os-io/c3os/internal/cmd"
"github.com/c3os-io/c3os/internal/utils"
machine "github.com/c3os-io/c3os/internal/machine"
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))
res = append(res, fmt.Sprintf("%s", v))
}
}
return
}
func Install(dir ...string) error {
utils.OnSignal(func() {
svc, err := machine.Getty(1)
if err == nil {
svc.Start()
}
}, syscall.SIGINT, syscall.SIGTERM)
tk := ""
r := map[string]string{}
mergeOption := func(cloudConfig string) {
c := &config.Config{}
yaml.Unmarshal([]byte(cloudConfig), c)
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)
}
})
// Try to pull userdata once more. best-effort
if _, err := os.Stat("/oem/userdata"); err != nil {
if err := machine.ExecuteCloudConfig("/system/oem/00_datasource.yaml", "rootfs.before"); err != nil {
fmt.Println("Warning: Failed pulling from datasources")
}
}
// Reads config, and if present and offline is defined,
// runs the installation
cc, err := config.Scan(config.Directories(dir...), config.MergeBootLine)
if err == nil && cc.Install != nil && cc.Install.Auto {
r["cc"] = cc.String()
r["device"] = cc.Install.Device
mergeOption(cc.String())
RunInstall(r)
svc, err := machine.Getty(1)
if err == nil {
svc.Start()
}
return nil
}
_, err = bus.Manager.Publish(events.EventChallenge, events.EventPayload{Config: cc.String()})
if err != nil {
return err
}
cmd.PrintBranding(DefaultBanner)
agentConfig, err := LoadConfig()
if err != nil {
return err
}
cmd.PrintText(agentConfig.Branding.Install, "Installation")
time.Sleep(5 * time.Second)
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{}{}
// make sure the config we write has at least the #node-config header, if any other was defined beforeahead
header := "#node-config"
if hasHeader, head := config.HasHeader(cc.String(), ""); hasHeader {
header = head
}
// What we receive take precedence over the one in the system
yaml.Unmarshal([]byte(cc.String()), &ccData)
if exists {
yaml.Unmarshal([]byte(cloudConfig), &ccData)
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")
utils.SH("elemental run-stage c3os-install.pre")
bus.RunHookScript("/usr/bin/c3os-agent.install.pre.hook")
RunInstall(r)
pterm.Info.Println("Installation completed, press enter to go back to the shell.")
utils.Prompt("")
// give tty1 back
svc, err := machine.Getty(1)
if err == nil {
svc.Start()
}
return nil
}
func RunInstall(options map[string]string) error {
f, _ := ioutil.TempFile("", "xxxx")
defer os.RemoveAll(f.Name())
device, ok := options["device"]
if !ok {
fmt.Println("device must be specified among options")
os.Exit(1)
}
cloudInit, ok := options["cc"]
if !ok {
fmt.Println("cloudInit must be specified among options")
os.Exit(1)
}
c := &config.Config{}
yaml.Unmarshal([]byte(cloudInit), c)
_, reboot := options["reboot"]
_, poweroff := options["poweroff"]
ioutil.WriteFile(f.Name(), []byte(cloudInit), os.ModePerm)
args := []string{"install"}
args = append(args, optsToArgs(options)...)
args = append(args, "-c", f.Name(), fmt.Sprintf("%s", device))
cmd := exec.Command("elemental", args...)
cmd.Env = os.Environ()
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
utils.SH("elemental run-stage c3os-install.after")
bus.RunHookScript("/usr/bin/c3os-agent.install.after.hook")
if reboot || c.Install != nil && c.Install.Reboot {
utils.Reboot()
}
if poweroff || c.Install != nil && c.Install.Poweroff {
utils.PowerOFF()
}
return nil
}