From 602d086ce49208adb0656498c28add26fcfbcf42 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 18 Jul 2022 22:02:49 +0000 Subject: [PATCH] art: Refactor agent code Move out cmd pieces and split into its own package. Also make sure we reload plugins before bootstrapping nodes. Also commons out the agent configuration in a specific YAML file. --- internal/agent/agent.go | 2 + internal/agent/config.go | 56 +++++++++++ internal/agent/install.go | 8 +- internal/agent/installer_suite_test.go | 13 +++ internal/agent/recovery.go | 134 +++++++++++++++++++++++++ internal/agent/recovery_linux.go | 12 +++ internal/agent/recovery_windows.go | 8 ++ internal/agent/reset.go | 81 +++++++++++++++ internal/agent/rotate.go | 40 ++++++++ internal/agent/upgrade.go | 51 ++++++++++ internal/bus/bus.go | 5 +- internal/cmd/utils.go | 9 +- 12 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 internal/agent/config.go create mode 100644 internal/agent/installer_suite_test.go create mode 100644 internal/agent/recovery.go create mode 100644 internal/agent/recovery_linux.go create mode 100644 internal/agent/recovery_windows.go create mode 100644 internal/agent/reset.go create mode 100644 internal/agent/rotate.go create mode 100644 internal/agent/upgrade.go diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 23626f3..ad2853d 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -50,6 +50,8 @@ func Run(apiAddress string, dir []string, force bool) error { return err } + // Re-load providers + bus.Manager.LoadProviders() machine.CreateSentinel("bundles") } diff --git a/internal/agent/config.go b/internal/agent/config.go new file mode 100644 index 0000000..92ed76f --- /dev/null +++ b/internal/agent/config.go @@ -0,0 +1,56 @@ +package agent + +import ( + "io/ioutil" + + "github.com/c3os-io/c3os/internal/c3os" + "gopkg.in/yaml.v2" +) + +type BrandingText struct { + Install string `yaml:"install"` + Reset string `yaml:"reset"` + Recovery string `yaml:"recovery"` +} + +type Config struct { + Branding BrandingText `yaml:"branding"` +} + +func LoadConfig(path ...string) (*Config, error) { + if len(path) == 0 { + path = append(path, "/etc/c3os/agent.yaml", "/etc/elemental/config.yaml") + } + + cfg := &Config{} + + for _, p := range path { + f, err := ioutil.ReadFile(p) + if err == nil { + yaml.Unmarshal(f, cfg) + } + } + + if cfg.Branding.Install == "" { + f, err := ioutil.ReadFile(c3os.BrandingFile("install_text")) + if err == nil { + cfg.Branding.Install = string(f) + } + } + + if cfg.Branding.Recovery == "" { + f, err := ioutil.ReadFile(c3os.BrandingFile("recovery_text")) + if err == nil { + cfg.Branding.Recovery = string(f) + } + } + + if cfg.Branding.Reset == "" { + f, err := ioutil.ReadFile(c3os.BrandingFile("reset_text")) + if err == nil { + cfg.Branding.Recovery = string(f) + } + } + + return cfg, nil +} diff --git a/internal/agent/install.go b/internal/agent/install.go index 678e13c..4895704 100644 --- a/internal/agent/install.go +++ b/internal/agent/install.go @@ -14,7 +14,6 @@ import ( config "github.com/c3os-io/c3os/pkg/config" "github.com/c3os-io/c3os/internal/bus" - "github.com/c3os-io/c3os/internal/c3os" "github.com/c3os-io/c3os/internal/cmd" "github.com/c3os-io/c3os/internal/utils" @@ -91,7 +90,12 @@ func Install(dir ...string) error { cmd.PrintBranding(DefaultBanner) - cmd.PrintTextFromFile(c3os.BrandingFile("install_text"), "Installation") + agentConfig, err := LoadConfig() + if err != nil { + return err + } + + cmd.PrintText(agentConfig.Branding.Install, "Installation") time.Sleep(5 * time.Second) diff --git a/internal/agent/installer_suite_test.go b/internal/agent/installer_suite_test.go new file mode 100644 index 0000000..d4afd21 --- /dev/null +++ b/internal/agent/installer_suite_test.go @@ -0,0 +1,13 @@ +package agent_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestInstaller(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Installer Suite") +} diff --git a/internal/agent/recovery.go b/internal/agent/recovery.go new file mode 100644 index 0000000..9297498 --- /dev/null +++ b/internal/agent/recovery.go @@ -0,0 +1,134 @@ +package agent + +import ( + "context" + "fmt" + "io" + "os/exec" + "time" + + "github.com/c3os-io/c3os/internal/cmd" + "github.com/c3os-io/c3os/internal/utils" + config "github.com/c3os-io/c3os/pkg/config" + "github.com/ipfs/go-log" + + machine "github.com/c3os-io/c3os/internal/machine" + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + "github.com/mudler/edgevpn/pkg/logger" + "github.com/mudler/edgevpn/pkg/node" + "github.com/mudler/edgevpn/pkg/services" + nodepair "github.com/mudler/go-nodepair" + qr "github.com/mudler/go-nodepair/qrcode" + "github.com/pterm/pterm" +) + +const recoveryAddr = "127.0.0.1:2222" + +func startRecoveryService(ctx context.Context, token, name, address, loglevel string) error { + + nc := config.Network(token, "", loglevel, "c3osrecovery0") + + lvl, err := log.LevelFromString(loglevel) + if err != nil { + lvl = log.LevelError + } + llger := logger.New(lvl) + + o, _, err := nc.ToOpts(llger) + if err != nil { + llger.Fatal(err.Error()) + } + + o = append(o, + services.Alive( + time.Duration(20)*time.Second, + time.Duration(10)*time.Second, + time.Duration(10)*time.Second)...) + + // opts, err := vpn.Register(vpnOpts...) + // if err != nil { + // return err + // } + o = append(o, services.RegisterService(llger, time.Duration(5*time.Second), name, address)...) + + e, err := node.New(o...) + if err != nil { + return err + } + + return e.Start(ctx) +} + +func Recovery() error { + + cmd.PrintBranding(DefaultBanner) + + agentConfig, err := LoadConfig() + if err != nil { + return err + } + + tk := nodepair.GenerateToken() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serviceUUID := utils.RandStringRunes(10) + generatedPassword := utils.RandStringRunes(7) + + startRecoveryService(ctx, tk, serviceUUID, recoveryAddr, "fatal") + + cmd.PrintText(agentConfig.Branding.Recovery, "Recovery") + + time.Sleep(5 * time.Second) + + pterm.Info.Printfln( + "starting ssh server on '%s', password: '%s' service: '%s' ", recoveryAddr, generatedPassword, serviceUUID) + + qr.Print(utils.EncodeRecoveryToken(tk, serviceUUID, generatedPassword)) + + go sshServer(recoveryAddr, generatedPassword) + + // Wait for user input and go back to shell + utils.Prompt("") + cancel() + // give tty1 back + svc, err := machine.Getty(1) + if err == nil { + svc.Start() + } + + return nil +} + +func sshServer(listenAdddr, password string) { + ssh.Handle(func(s ssh.Session) { + cmd := exec.Command("bash") + ptyReq, winCh, isPty := s.Pty() + if isPty { + cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) + f, err := pty.Start(cmd) + if err != nil { + pterm.Warning.Println("Failed reserving tty") + } + go func() { + for win := range winCh { + setWinsize(f, win.Width, win.Height) + } + }() + go func() { + io.Copy(f, s) // stdin + }() + io.Copy(s, f) // stdout + cmd.Wait() + } else { + io.WriteString(s, "No PTY requested.\n") + s.Exit(1) + } + }) + + pterm.Info.Println(ssh.ListenAndServe(listenAdddr, nil, ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool { + return pass == password + }), + )) +} diff --git a/internal/agent/recovery_linux.go b/internal/agent/recovery_linux.go new file mode 100644 index 0000000..71633d3 --- /dev/null +++ b/internal/agent/recovery_linux.go @@ -0,0 +1,12 @@ +package agent + +import ( + "os" + "syscall" + "unsafe" +) + +func setWinsize(f *os.File, w, h int) { + syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) +} diff --git a/internal/agent/recovery_windows.go b/internal/agent/recovery_windows.go new file mode 100644 index 0000000..5794e39 --- /dev/null +++ b/internal/agent/recovery_windows.go @@ -0,0 +1,8 @@ +package agent + +import ( + "os" +) + +func setWinsize(f *os.File, w, h int) { +} diff --git a/internal/agent/reset.go b/internal/agent/reset.go new file mode 100644 index 0000000..b2e1dee --- /dev/null +++ b/internal/agent/reset.go @@ -0,0 +1,81 @@ +package agent + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/c3os-io/c3os/internal/cmd" + "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/utils" + "github.com/pterm/pterm" +) + +func Reset() error { + + cmd.PrintBranding(DefaultBanner) + + agentConfig, err := LoadConfig() + if err != nil { + return err + } + + cmd.PrintText(agentConfig.Branding.Reset, "Reset") + + // We don't close the lock, as none of the following actions are expected to return + lock := sync.Mutex{} + go func() { + // Wait for user input and go back to shell + utils.Prompt("") + // give tty1 back + svc, err := machine.Getty(1) + if err == nil { + svc.Start() + } + + lock.Lock() + fmt.Println("Reset aborted") + panic(utils.Shell().Run()) + }() + + time.Sleep(60 * time.Second) + lock.Lock() + args := []string{"reset"} + args = append(args, "--reset-persistent") + + 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) + } + + pterm.Info.Println("Rebooting in 60 seconds, press Enter to abort...") + + // We don't close the lock, as none of the following actions are expected to return + lock2 := sync.Mutex{} + go func() { + // Wait for user input and go back to shell + utils.Prompt("") + // give tty1 back + svc, err := machine.Getty(1) + if err == nil { + svc.Start() + } + + lock2.Lock() + fmt.Println("Reboot aborted") + panic(utils.Shell().Run()) + }() + + time.Sleep(60 * time.Second) + lock2.Lock() + utils.Reboot() + + return nil +} diff --git a/internal/agent/rotate.go b/internal/agent/rotate.go new file mode 100644 index 0000000..a14a506 --- /dev/null +++ b/internal/agent/rotate.go @@ -0,0 +1,40 @@ +package agent + +import ( + machine "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/provider" + providerConfig "github.com/c3os-io/c3os/internal/provider/config" + config "github.com/c3os-io/c3os/pkg/config" +) + +func RotateToken(configDir []string, newToken, apiAddress, rootDir string, restart bool) error { + if err := config.ReplaceToken(configDir, newToken); err != nil { + return err + } + + c, err := config.Scan(config.Directories(configDir...)) + if err != nil { + return err + } + + providerCfg := &providerConfig.Config{} + err = c.Unmarshal(providerCfg) + if err != nil { + return err + } + + err = provider.SetupVPN(machine.EdgeVPNDefaultInstance, apiAddress, rootDir, false, providerCfg) + if err != nil { + return err + } + + if restart { + svc, err := machine.EdgeVPN(machine.EdgeVPNDefaultInstance, rootDir) + if err != nil { + return err + } + + return svc.Restart() + } + return nil +} diff --git a/internal/agent/upgrade.go b/internal/agent/upgrade.go new file mode 100644 index 0000000..1e75f71 --- /dev/null +++ b/internal/agent/upgrade.go @@ -0,0 +1,51 @@ +package agent + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + + "github.com/c3os-io/c3os/internal/github" + "github.com/c3os-io/c3os/internal/utils" +) + +func Upgrade(version, image string, force bool) error { + if version == "" && image == "" { + githubRepo, err := utils.OSRelease("GITHUB_REPO") + if err != nil { + return err + } + releases, _ := github.FindReleases(context.Background(), "", githubRepo) + version = releases[0] + fmt.Println("latest release is ", version) + } + + if utils.Version() == version && !force { + fmt.Println("latest version already installed. use --force to force upgrade") + return nil + } + + flavor := utils.Flavor() + if flavor == "" { + return errors.New("no flavor detected") + } + + registry, err := utils.OSRelease("IMAGE_REPO") + if err != nil { + return err + } + img := fmt.Sprintf("%s:%s-%s", registry, flavor, version) + if image != "" { + img = image + } + + args := []string{"upgrade", "--system.uri", fmt.Sprintf("docker:%s", img)} + cmd := exec.Command("elemental", args...) + cmd.Env = os.Environ() + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/internal/bus/bus.go b/internal/bus/bus.go index 8264146..96315b3 100644 --- a/internal/bus/bus.go +++ b/internal/bus/bus.go @@ -24,9 +24,12 @@ type Bus struct { *pluggable.Manager } -func (b *Bus) Initialize() { +func (b *Bus) LoadProviders() { b.Manager.Autoload("agent-provider", "/system/providers").Register() +} +func (b *Bus) Initialize() { + b.LoadProviders() for i := range b.Manager.Events { e := b.Manager.Events[i] b.Manager.Response(e, func(p *pluggable.Plugin, r *pluggable.EventResponse) { diff --git a/internal/cmd/utils.go b/internal/cmd/utils.go index 557a0af..51f7b90 100644 --- a/internal/cmd/utils.go +++ b/internal/cmd/utils.go @@ -10,14 +10,9 @@ import ( "github.com/pterm/pterm" ) -func PrintTextFromFile(f string, banner string) { - installText := "" - text, err := ioutil.ReadFile(f) - if err == nil { - installText = string(text) - } +func PrintText(f string, banner string) { pterm.DefaultBox.WithTitle(banner).WithTitleBottomRight().WithRightPadding(0).WithBottomPadding(0).Println( - installText) + f) } func PrintBranding(b []byte) {