console-watcher: use console watcher to monitor guest console outputs

Import new console watcher to monitor guest console outputs, and will be
only effective when we turn on enable_debug option.
Guest console outputs may include guest kernel debug info, agent debug info,
etc.

Fixes: #389

Signed-off-by: Penny Zheng penny.zheng@arm.com
This commit is contained in:
Penny Zheng 2020-07-15 05:44:02 +00:00
parent 1099a28830
commit 7f3e8959c5
11 changed files with 183 additions and 117 deletions

View File

@ -254,7 +254,7 @@ func (a *Acrn) buildDevices(imagePath string) ([]Device, error) {
return nil, fmt.Errorf("Image Path should not be empty: %s", imagePath) return nil, fmt.Errorf("Image Path should not be empty: %s", imagePath)
} }
console, err := a.getSandboxConsole(a.id) _, console, err := a.getSandboxConsole(a.id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -643,11 +643,16 @@ func (a *Acrn) addDevice(devInfo interface{}, devType deviceType) error {
// getSandboxConsole builds the path of the console where we can read // getSandboxConsole builds the path of the console where we can read
// logs coming from the sandbox. // logs coming from the sandbox.
func (a *Acrn) getSandboxConsole(id string) (string, error) { func (a *Acrn) getSandboxConsole(id string) (string, string, error) {
span, _ := a.trace("getSandboxConsole") span, _ := a.trace("getSandboxConsole")
defer span.Finish() defer span.Finish()
return utils.BuildSocketPath(a.store.RunVMStoragePath(), id, acrnConsoleSocket) consoleURL, err := utils.BuildSocketPath(a.store.RunVMStoragePath(), id, acrnConsoleSocket)
if err != nil {
return consoleProtoUnix, "", err
}
return consoleProtoUnix, consoleURL, nil
} }
func (a *Acrn) saveSandbox() error { func (a *Acrn) saveSandbox() error {

View File

@ -205,9 +205,10 @@ func TestAcrnGetSandboxConsole(t *testing.T) {
sandboxID := "testSandboxID" sandboxID := "testSandboxID"
expected := filepath.Join(a.store.RunVMStoragePath(), sandboxID, consoleSocket) expected := filepath.Join(a.store.RunVMStoragePath(), sandboxID, consoleSocket)
result, err := a.getSandboxConsole(sandboxID) proto, result, err := a.getSandboxConsole(sandboxID)
assert.NoError(err) assert.NoError(err)
assert.Equal(result, expected) assert.Equal(result, expected)
assert.Equal(proto, consoleProtoUnix)
} }
func TestAcrnCreateSandbox(t *testing.T) { func TestAcrnCreateSandbox(t *testing.T) {

View File

@ -9,8 +9,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -21,6 +19,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/containerd/console"
persistapi "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/api" persistapi "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/api"
chclient "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/cloud-hypervisor/client" chclient "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/cloud-hypervisor/client"
"github.com/opencontainers/selinux/go-selinux/label" "github.com/opencontainers/selinux/go-selinux/label"
@ -123,6 +122,7 @@ type cloudHypervisor struct {
vmconfig chclient.VmConfig vmconfig chclient.VmConfig
virtiofsd Virtiofsd virtiofsd Virtiofsd
store persistapi.PersistDriver store persistapi.PersistDriver
console console.Console
} }
var clhKernelParams = []Param{ var clhKernelParams = []Param{
@ -357,13 +357,12 @@ func (clh *cloudHypervisor) startSandbox(timeout int) error {
return errors.New("cloud-hypervisor only supports virtio based file sharing") return errors.New("cloud-hypervisor only supports virtio based file sharing")
} }
var strErr string pid, err := clh.LaunchClh()
strErr, pid, err := clh.LaunchClh()
if err != nil { if err != nil {
if shutdownErr := clh.virtiofsd.Stop(); shutdownErr != nil { if shutdownErr := clh.virtiofsd.Stop(); shutdownErr != nil {
clh.Logger().WithField("error", shutdownErr).Warn("error shutting down Virtiofsd") clh.Logger().WithField("error", shutdownErr).Warn("error shutting down Virtiofsd")
} }
return fmt.Errorf("failed to launch cloud-hypervisor: %q, hypervisor output:\n%s", err, strErr) return fmt.Errorf("failed to launch cloud-hypervisor: %q", err)
} }
clh.state.PID = pid clh.state.PID = pid
@ -377,9 +376,16 @@ func (clh *cloudHypervisor) startSandbox(timeout int) error {
// getSandboxConsole builds the path of the console where we can read // getSandboxConsole builds the path of the console where we can read
// logs coming from the sandbox. // logs coming from the sandbox.
func (clh *cloudHypervisor) getSandboxConsole(id string) (string, error) { func (clh *cloudHypervisor) getSandboxConsole(id string) (string, string, error) {
clh.Logger().WithField("function", "getSandboxConsole").WithField("id", id).Info("Get Sandbox Console") clh.Logger().WithField("function", "getSandboxConsole").WithField("id", id).Info("Get Sandbox Console")
return "", nil master, slave, err := console.NewPty()
if err != nil {
clh.Logger().Debugf("Error create pseudo tty: %v", err)
return consoleProtoPty, "", err
}
clh.console = master
return consoleProtoPty, slave, nil
} }
func (clh *cloudHypervisor) disconnect() { func (clh *cloudHypervisor) disconnect() {
@ -885,11 +891,11 @@ func (clh *cloudHypervisor) getAvailableVersion() error {
} }
func (clh *cloudHypervisor) LaunchClh() (string, int, error) { func (clh *cloudHypervisor) LaunchClh() (int, error) {
clhPath, err := clh.clhPath() clhPath, err := clh.clhPath()
if err != nil { if err != nil {
return "", -1, err return -1, err
} }
args := []string{cscAPIsocket, clh.state.apiSocket} args := []string{cscAPIsocket, clh.state.apiSocket}
@ -913,14 +919,12 @@ func (clh *cloudHypervisor) LaunchClh() (string, int, error) {
clh.Logger().WithField("args", strings.Join(args, " ")).Info() clh.Logger().WithField("args", strings.Join(args, " ")).Info()
cmdHypervisor := exec.Command(clhPath, args...) cmdHypervisor := exec.Command(clhPath, args...)
var hypervisorOutput io.ReadCloser
if clh.config.Debug { if clh.config.Debug {
cmdHypervisor.Env = os.Environ() cmdHypervisor.Env = os.Environ()
cmdHypervisor.Env = append(cmdHypervisor.Env, "RUST_BACKTRACE=full") cmdHypervisor.Env = append(cmdHypervisor.Env, "RUST_BACKTRACE=full")
// Get StdoutPipe only for debug, without debug golang will redirect to /dev/null if clh.console != nil {
hypervisorOutput, err = cmdHypervisor.StdoutPipe() cmdHypervisor.Stderr = clh.console
if err != nil { cmdHypervisor.Stdout = clh.console
return "", -1, err
} }
} }
@ -928,37 +932,15 @@ func (clh *cloudHypervisor) LaunchClh() (string, int, error) {
err = utils.StartCmd(cmdHypervisor) err = utils.StartCmd(cmdHypervisor)
if err != nil { if err != nil {
return "", -1, err return -1, err
} }
if err := clh.waitVMM(clhTimeout); err != nil { if err := clh.waitVMM(clhTimeout); err != nil {
clh.Logger().WithField("error", err).Warn("cloud-hypervisor init failed") clh.Logger().WithField("error", err).Warn("cloud-hypervisor init failed")
var output string return -1, err
if hypervisorOutput != nil {
b, errRead := ioutil.ReadAll(hypervisorOutput)
if errRead != nil {
output = "failed to read hypervisor output to get error information"
} else {
output = string(b)
}
} else {
output = "Please enable hypervisor logging to get stdout information"
} }
return output, -1, err return cmdHypervisor.Process.Pid, nil
}
if clh.config.Debug {
cmdLogger := utils.NewProgramLogger("kata-hypervisor")
clh.Logger().Debugf("Starting process logger(%s) for hypervisor", cmdLogger)
if err := cmdLogger.StartLogger(hypervisorOutput); err != nil {
// Not critical to run a container, but output wont be logged
clh.Logger().Warnf("Failed start process logger(%s) %s", cmdLogger, err)
}
}
return "", cmdHypervisor.Process.Pid, nil
} }
//########################################################################### //###########################################################################

View File

@ -10,7 +10,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -25,7 +24,6 @@ import (
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/device/config" "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/device/config"
persistapi "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/api" persistapi "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/api"
kataclient "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/agent/protocols/client"
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client" "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client"
models "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client/models" models "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client/models"
ops "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client/operations" ops "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/firecracker/client/operations"
@ -154,6 +152,8 @@ type firecracker struct {
fcConfigPath string fcConfigPath string
fcConfig *types.FcConfig // Parameters configured before VM starts fcConfig *types.FcConfig // Parameters configured before VM starts
console console.Console
} }
type firecrackerDevice struct { type firecrackerDevice struct {
@ -398,13 +398,8 @@ func (fc *firecracker) fcInit(timeout int) error {
} }
if fc.config.Debug { if fc.config.Debug {
stdin, err := fc.watchConsole() cmd.Stderr = fc.console
if err != nil { cmd.Stdout = fc.console
return err
}
cmd.Stderr = stdin
cmd.Stdout = stdin
} }
fc.Logger().WithField("hypervisor args", args).Debug() fc.Logger().WithField("hypervisor args", args).Debug()
@ -1101,8 +1096,15 @@ func (fc *firecracker) hotplugRemoveDevice(devInfo interface{}, devType deviceTy
// getSandboxConsole builds the path of the console where we can read // getSandboxConsole builds the path of the console where we can read
// logs coming from the sandbox. // logs coming from the sandbox.
func (fc *firecracker) getSandboxConsole(id string) (string, error) { func (fc *firecracker) getSandboxConsole(id string) (string, string, error) {
return fmt.Sprintf("%s://%s:%d", kataclient.HybridVSockScheme, filepath.Join(fc.jailerRoot, defaultHybridVSocketName), vSockLogsPort), nil master, slave, err := console.NewPty()
if err != nil {
fc.Logger().Debugf("Error create pseudo tty: %v", err)
return consoleProtoPty, "", err
}
fc.console = master
return consoleProtoPty, slave, nil
} }
func (fc *firecracker) disconnect() { func (fc *firecracker) disconnect() {
@ -1214,40 +1216,6 @@ func (fc *firecracker) generateSocket(id string) (interface{}, error) {
}, nil }, nil
} }
func (fc *firecracker) watchConsole() (*os.File, error) {
master, slave, err := console.NewPty()
if err != nil {
fc.Logger().WithField("Error create pseudo tty", err).Debug()
return nil, err
}
stdio, err := os.OpenFile(slave, syscall.O_RDWR, 0700)
if err != nil {
fc.Logger().WithError(err).Debugf("open pseudo tty %s", slave)
return nil, err
}
go func() {
scanner := bufio.NewScanner(master)
for scanner.Scan() {
fc.Logger().WithFields(logrus.Fields{
"sandbox": fc.id,
"vmconsole": scanner.Text(),
}).Infof("reading guest console")
}
if err := scanner.Err(); err != nil {
if err == io.EOF {
fc.Logger().Info("console watcher quits")
} else {
fc.Logger().WithError(err).Error("Failed to read guest console")
}
}
}()
return stdio, nil
}
func (fc *firecracker) isRateLimiterBuiltin() bool { func (fc *firecracker) isRateLimiterBuiltin() bool {
return true return true
} }

View File

@ -764,7 +764,7 @@ type hypervisor interface {
hotplugRemoveDevice(devInfo interface{}, devType deviceType) (interface{}, error) hotplugRemoveDevice(devInfo interface{}, devType deviceType) (interface{}, error)
resizeMemory(memMB uint32, memoryBlockSizeMB uint32, probe bool) (uint32, memoryDevice, error) resizeMemory(memMB uint32, memoryBlockSizeMB uint32, probe bool) (uint32, memoryDevice, error)
resizeVCPUs(vcpus uint32) (uint32, uint32, error) resizeVCPUs(vcpus uint32) (uint32, uint32, error)
getSandboxConsole(sandboxID string) (string, error) getSandboxConsole(sandboxID string) (string, string, error)
disconnect() disconnect()
capabilities() types.Capabilities capabilities() types.Capabilities
hypervisorConfig() HypervisorConfig hypervisorConfig() HypervisorConfig

View File

@ -82,8 +82,8 @@ func (m *mockHypervisor) hotplugRemoveDevice(devInfo interface{}, devType device
return nil, nil return nil, nil
} }
func (m *mockHypervisor) getSandboxConsole(sandboxID string) (string, error) { func (m *mockHypervisor) getSandboxConsole(sandboxID string) (string, string, error) {
return "", nil return "", "", nil
} }
func (m *mockHypervisor) resizeMemory(memMB uint32, memorySectionSizeMB uint32, probe bool) (uint32, memoryDevice, error) { func (m *mockHypervisor) resizeMemory(memMB uint32, memorySectionSizeMB uint32, probe bool) (uint32, memoryDevice, error) {

View File

@ -66,9 +66,11 @@ func TestMockHypervisorGetSandboxConsole(t *testing.T) {
var m *mockHypervisor var m *mockHypervisor
expected := "" expected := ""
result, err := m.getSandboxConsole("testSandboxID") expectedProto := ""
proto, result, err := m.getSandboxConsole("testSandboxID")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, result, expected) assert.Equal(t, result, expected)
assert.Equal(t, proto, expectedProto)
} }
func TestMockHypervisorSaveSandbox(t *testing.T) { func TestMockHypervisorSaveSandbox(t *testing.T) {

View File

@ -380,7 +380,7 @@ func (q *qemu) createQmpSocket() ([]govmmQemu.QMPSocket, error) {
func (q *qemu) buildDevices(initrdPath string) ([]govmmQemu.Device, *govmmQemu.IOThread, error) { func (q *qemu) buildDevices(initrdPath string) ([]govmmQemu.Device, *govmmQemu.IOThread, error) {
var devices []govmmQemu.Device var devices []govmmQemu.Device
console, err := q.getSandboxConsole(q.id) _, console, err := q.getSandboxConsole(q.id)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -1764,11 +1764,16 @@ func (q *qemu) addDevice(devInfo interface{}, devType deviceType) error {
// getSandboxConsole builds the path of the console where we can read // getSandboxConsole builds the path of the console where we can read
// logs coming from the sandbox. // logs coming from the sandbox.
func (q *qemu) getSandboxConsole(id string) (string, error) { func (q *qemu) getSandboxConsole(id string) (string, string, error) {
span, _ := q.trace("getSandboxConsole") span, _ := q.trace("getSandboxConsole")
defer span.Finish() defer span.Finish()
return utils.BuildSocketPath(q.store.RunVMStoragePath(), id, consoleSocket) consoleURL, err := utils.BuildSocketPath(q.store.RunVMStoragePath(), id, consoleSocket)
if err != nil {
return consoleProtoUnix, "", err
}
return consoleProtoUnix, consoleURL, nil
} }
func (q *qemu) saveSandbox() error { func (q *qemu) saveSandbox() error {

View File

@ -314,9 +314,10 @@ func TestQemuGetSandboxConsole(t *testing.T) {
sandboxID := "testSandboxID" sandboxID := "testSandboxID"
expected := filepath.Join(q.store.RunVMStoragePath(), sandboxID, consoleSocket) expected := filepath.Join(q.store.RunVMStoragePath(), sandboxID, consoleSocket)
result, err := q.getSandboxConsole(sandboxID) proto, result, err := q.getSandboxConsole(sandboxID)
assert.NoError(err) assert.NoError(err)
assert.Equal(result, expected) assert.Equal(result, expected)
assert.Equal(proto, consoleProtoUnix)
} }
func TestQemuCapabilities(t *testing.T) { func TestQemuCapabilities(t *testing.T) {

View File

@ -6,6 +6,7 @@
package virtcontainers package virtcontainers
import ( import (
"bufio"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -189,6 +190,8 @@ type Sandbox struct {
cgroupMgr *vccgroups.Manager cgroupMgr *vccgroups.Manager
ctx context.Context ctx context.Context
cw *consoleWatcher
} }
// ID returns the sandbox identifier string. // ID returns the sandbox identifier string.
@ -929,6 +932,103 @@ func (s *Sandbox) ListRoutes() ([]*pbTypes.Route, error) {
return s.agent.listRoutes() return s.agent.listRoutes()
} }
const (
// unix socket type of console
consoleProtoUnix = "unix"
// pty type of console.
consoleProtoPty = "pty"
)
// console watcher is designed to monitor guest console output.
type consoleWatcher struct {
proto string
consoleURL string
conn net.Conn
ptyConsole *os.File
}
func newConsoleWatcher(s *Sandbox) (*consoleWatcher, error) {
var (
err error
cw consoleWatcher
)
cw.proto, cw.consoleURL, err = s.hypervisor.getSandboxConsole(s.id)
if err != nil {
return nil, err
}
return &cw, nil
}
// start the console watcher
func (cw *consoleWatcher) start(s *Sandbox) (err error) {
if cw.consoleWatched() {
return fmt.Errorf("console watcher has already watched for sandbox %s", s.id)
}
var scanner *bufio.Scanner
switch cw.proto {
case consoleProtoUnix:
cw.conn, err = net.Dial("unix", cw.consoleURL)
if err != nil {
return err
}
scanner = bufio.NewScanner(cw.conn)
case consoleProtoPty:
// read-only
cw.ptyConsole, err = os.Open(cw.consoleURL)
scanner = bufio.NewScanner(cw.ptyConsole)
default:
return fmt.Errorf("unknown console proto %s", cw.proto)
}
go func() {
for scanner.Scan() {
s.Logger().WithFields(logrus.Fields{
"console-protocol": cw.proto,
"console-url": cw.consoleURL,
"sandbox": s.id,
"vmconsole": scanner.Text(),
}).Debug("reading guest console")
}
if err := scanner.Err(); err != nil {
if err == io.EOF {
s.Logger().Info("console watcher quits")
} else {
s.Logger().WithError(err).WithFields(logrus.Fields{
"console-protocol": cw.proto,
"console-url": cw.consoleURL,
"sandbox": s.id,
}).Error("Failed to read guest console logs")
}
}
}()
return nil
}
// check if the console watcher has already watched the vm console.
func (cw *consoleWatcher) consoleWatched() bool {
return cw.conn != nil || cw.ptyConsole != nil
}
// stop the console watcher.
func (cw *consoleWatcher) stop() {
if cw.conn != nil {
cw.conn.Close()
cw.conn = nil
}
if cw.ptyConsole != nil {
cw.ptyConsole.Close()
cw.ptyConsole = nil
}
}
// startVM starts the VM. // startVM starts the VM.
func (s *Sandbox) startVM() (err error) { func (s *Sandbox) startVM() (err error) {
span, ctx := s.trace("startVM") span, ctx := s.trace("startVM")
@ -936,6 +1036,15 @@ func (s *Sandbox) startVM() (err error) {
s.Logger().Info("Starting VM") s.Logger().Info("Starting VM")
if s.config.HypervisorConfig.Debug {
// create console watcher
consoleWatcher, err := newConsoleWatcher(s)
if err != nil {
return err
}
s.cw = consoleWatcher
}
if err := s.network.Run(s.networkNS.NetNsPath, func() error { if err := s.network.Run(s.networkNS.NetNsPath, func() error {
if s.factory != nil { if s.factory != nil {
vm, err := s.factory.GetVM(ctx, VMConfig{ vm, err := s.factory.GetVM(ctx, VMConfig{
@ -980,6 +1089,14 @@ func (s *Sandbox) startVM() (err error) {
s.Logger().Info("VM started") s.Logger().Info("VM started")
if s.cw != nil {
s.Logger().Debug("console watcher starts")
if err := s.cw.start(s); err != nil {
s.cw.stop()
return err
}
}
// Once the hypervisor is done starting the sandbox, // Once the hypervisor is done starting the sandbox,
// we want to guarantee that it is manageable. // we want to guarantee that it is manageable.
// For that we need to ask the agent to start the // For that we need to ask the agent to start the
@ -1478,6 +1595,12 @@ func (s *Sandbox) Stop(force bool) error {
return err return err
} }
// shutdown console watcher if exists
if s.cw != nil {
s.Logger().Debug("stop the sandbox")
s.cw.stop()
}
if err := s.setSandboxState(types.StateStopped); err != nil { if err := s.setSandboxState(types.StateStopped); err != nil {
return err return err
} }

View File

@ -9,7 +9,6 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -303,26 +302,6 @@ const (
GiB = MiB << 10 GiB = MiB << 10
) )
// Binary to use to log program output
const LoggerBinaryName = "systemd-cat"
type ProgramLogger struct {
cmd *exec.Cmd
}
func NewProgramLogger(loggerLabel string) ProgramLogger {
return ProgramLogger{cmd: exec.Command(LoggerBinaryName, "-t", loggerLabel)}
}
func (p *ProgramLogger) StartLogger(output io.ReadCloser) error {
p.cmd.Stdin = output
return StartCmd(p.cmd)
}
func (p ProgramLogger) String() string {
return p.cmd.Path
}
func ConvertNetlinkFamily(netlinkFamily int32) pbTypes.IPFamily { func ConvertNetlinkFamily(netlinkFamily int32) pbTypes.IPFamily {
switch netlinkFamily { switch netlinkFamily {
case netlink.FAMILY_V6: case netlink.FAMILY_V6: