From 5e6a67a4222430ab7283d851996a2f64566289d4 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 17 Apr 2020 17:58:05 +0000 Subject: [PATCH] devshell: Print status as we're booting It's better than seconds of silence, and if e.g. we get stuck in the initramfs then it's more obvious. Closes: https://github.com/coreos/coreos-assembler/issues/1367 --- mantle/cmd/kola/devshell.go | 59 +++++++++++++++++++++++++++++++------ mantle/platform/qemu.go | 45 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/mantle/cmd/kola/devshell.go b/mantle/cmd/kola/devshell.go index 45b302bf4c..b2872bd98a 100644 --- a/mantle/cmd/kola/devshell.go +++ b/mantle/cmd/kola/devshell.go @@ -19,6 +19,7 @@ package main import ( "bufio" "fmt" + "io" "io/ioutil" "net" "os" @@ -28,6 +29,8 @@ import ( "syscall" "time" + "golang.org/x/crypto/ssh/terminal" + "github.com/coreos/mantle/util" "github.com/pkg/errors" @@ -64,6 +67,10 @@ func readTrimmedLine(r *bufio.Reader) (string, error) { } func runDevShellSSH(builder *platform.QemuBuilder, conf *v3types.Config) error { + if !terminal.IsTerminal(0) { + return fmt.Errorf("stdin is not a tty") + } + tmpd, err := ioutil.TempDir("", "kola-devshell") if err != nil { return err @@ -151,6 +158,17 @@ WantedBy=multi-user.target`, readinessSignalChan) return errors.Wrapf(err, "rendering config") } + serialPipe, err := builder.SerialPipe() + if err != nil { + return err + } + serialLog, err := ioutil.TempFile("", "cosa-run-serial") + if err != nil { + return err + } + os.Remove(serialLog.Name()) + serialTee := io.TeeReader(serialPipe, serialLog) + builder.InheritConsole = false inst, err := builder.Exec() if err != nil { @@ -158,9 +176,15 @@ WantedBy=multi-user.target`, readinessSignalChan) } defer inst.Destroy() + statusChan, statusErrChan := inst.ParseSerialConsoleState(serialTee) qemuWaitChan := make(chan error) errchan := make(chan error) readychan := make(chan struct{}) + go func() { + // Just proxy this one + err := <-statusErrChan + errchan <- err + }() go func() { buf, err := inst.WaitIgnitionError() if err != nil { @@ -187,18 +211,35 @@ WantedBy=multi-user.target`, readinessSignalChan) readychan <- s }() - select { - case err := <-errchan: - if err == platform.ErrInitramfsEmergency { - return fmt.Errorf("instance failed in initramfs; try rerunning with --devshell-console") +loop: + for { + select { + case err := <-errchan: + if err == platform.ErrInitramfsEmergency { + return fmt.Errorf("instance failed in initramfs; try rerunning with --devshell-console") + } + return err + case err := <-qemuWaitChan: + return errors.Wrapf(err, "qemu exited before setup") + case status := <-statusChan: + fmt.Printf("\033[2K\rstate: %s", status) + case _ = <-readychan: + fmt.Printf("\033[2K\rvirtio: connected\n") + break loop } - return err - case err := <-qemuWaitChan: - return errors.Wrapf(err, "qemu exited before setup") - case _ = <-readychan: - fmt.Println("virtio: connected") } + // Ignore other status messages, and just print errors for now + go func() { + for { + select { + case _ = <-statusChan: + case err := <-errchan: + fmt.Fprintf(os.Stderr, "errchan: %v", err) + } + } + }() + var ip string err = util.Retry(6, 5*time.Second, func() error { var err error diff --git a/mantle/platform/qemu.go b/mantle/platform/qemu.go index d2a9b73408..fb7f178369 100644 --- a/mantle/platform/qemu.go +++ b/mantle/platform/qemu.go @@ -42,6 +42,13 @@ var ( ErrInitramfsEmergency = errors.New("entered emergency.target in initramfs") ) +const ( + StateInitial = "qemu" + StateKernel = "kernel" + StateInitramfs = "initramfs" + StateTargetRoot = "root" +) + type MachineOptions struct { AdditionalDisks []Disk } @@ -144,6 +151,44 @@ func (inst *QemuInstance) Wait() error { return nil } +func runParseSerialConsoleState(r io.Reader, schan chan<- string, echan chan<- error) { + bufr := bufio.NewReader(r) + state := StateInitial + for { + buf, _, err := bufr.ReadLine() + if err != nil { + echan <- err + break + } + line := string(buf) + switch state { + case StateInitial: + // Yes, all this is heuristic. But it's just informational. + if strings.Contains(line, "Hypervisor detected: KVM") { + state = StateKernel + schan <- state + } + case StateKernel: + if strings.Contains(line, "Running in initial RAM disk.") { + state = StateInitramfs + schan <- state + } + case StateInitramfs: + if strings.Contains(line, "initrd-switch-root.service: Succeeded.") { + state = StateTargetRoot + schan <- state + } + } + } +} + +func (inst *QemuInstance) ParseSerialConsoleState(r io.Reader) (<-chan string, <-chan error) { + schan := make(chan string) + echan := make(chan error) + go runParseSerialConsoleState(r, schan, echan) + return schan, echan +} + // WaitIgnitionError will only return if the instance // failed inside the initramfs. The resulting string will // be a newline-delimited stream of JSON strings, as returned