Skip to content
View rapeazabe888's full-sized avatar

Block or report rapeazabe888

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Please don't include any personal information such as legal names or email addresses. Maximum 100 characters, markdown supported. This note will be visible to only you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
rapeazabe888/README.md

// SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors. // Licensed under the BSD-3-Clause License (the "License"). // You may not use this file except in compliance with the License. package qemu

import ( "context" "errors" "fmt" "io" "io/fs" "net" "os" "path/filepath" "reflect" "strconv" "strings" "time"

zip "api.zip"
"github.com/acorn-io/baaah/pkg/merr"
"github.com/mitchellh/mapstructure"
goprocess "github.com/shirou/gopsutil/v3/process"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"

machinev1alpha1 "kraftkit.sh/api/machine/v1alpha1"
"kraftkit.sh/config"
"kraftkit.sh/exec"
"kraftkit.sh/internal/logtail"
"kraftkit.sh/internal/retrytimeout"
"kraftkit.sh/machine/network/macaddr"
"kraftkit.sh/machine/qemu/qmp"
qmpapi "kraftkit.sh/machine/qemu/qmp/v7alpha2"
"kraftkit.sh/unikraft/export/v0/posixenviron"
"kraftkit.sh/unikraft/export/v0/ukargparse"
"kraftkit.sh/unikraft/export/v0/uknetdev"
"kraftkit.sh/unikraft/export/v0/vfscore"

)

// machineV1alpha1Service ... type machineV1alpha1Service struct { eopts []exec.ExecOption }

var ( ErrCouldNotAttachQMPClient = errors.New("could not attach QMP client") ErrCouldNotQueryMachineViaQMP = errors.New("could not query machine via QMP") )

// NewMachineV1alpha1Service implements kraftkit.sh/machine/platform.NewStrategyConstructor func NewMachineV1alpha1Service(ctx context.Context, opts ...any) (machinev1alpha1.MachineService, error) { service := machineV1alpha1Service{}

for _, opt := range opts {
	qopt, ok := opt.(MachineServiceV1alpha1Option)
	if !ok {
		panic("cannot apply non-MachineServiceV1alpha1Option type methods")
	}

	if err := qopt(&service); err != nil {
		return nil, err
	}
}

return &service, nil

}

// Create implements kraftkit.sh/api/machine/v1alpha1.MachineService.Create func (service *machineV1alpha1Service) Create(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { if machine.Status.KernelPath == "" { return machine, fmt.Errorf("empty kernel path") }

if _, err := os.Stat(machine.Status.KernelPath); err != nil && os.IsNotExist(err) {
	return machine, fmt.Errorf("supplied kernel path does not exist: %s", machine.Status.KernelPath)
}

var bin string

switch machine.Spec.Architecture {
case "x86_64", "amd64":
	bin = QemuSystemX86
case "arm":
	bin = QemuSystemArm
case "arm64":
	bin = QemuSystemAarch64
default:
	return nil, fmt.Errorf("unsupported architecture: %s", machine.Spec.Architecture)
}

if config.G[config.KraftKit](ctx).Qemu != "" {
	bin = config.G[config.KraftKit](ctx).Qemu
}

// Determine the version of QEMU so as to both determine whether it is a
// suitable version and to adjust the supplied command-line arguments.
qemuVersion, err := GetQemuVersionFromBin(ctx, bin)
if err != nil {
	return machine, err
}

if qemuVersion.LessThan(QemuVersion4_2_0) {
	return machine, fmt.Errorf("unsupported QEMU version: %s: please upgrade to a newer version", qemuVersion.String())
}

// Determine the QEMU machine type to use
qemuAccels, err := GetQemuMachineAccelFromBin(ctx, bin)
if err != nil {
	return machine, err
}

if machine.Spec.Emulation {
	emulation := false
	for _, accel := range qemuAccels {
		if accel == QemuMachineAccelTCG {
			emulation = true
			break
		}
	}

	if !emulation {
		return machine, fmt.Errorf("emulation requested but TCG is not available")
	}
} else {
	platform := false
	for _, accel := range qemuAccels {
		if accel == QemuMachineAccelKVM {
			platform = true
			break
		}
	}

	if !platform {
		return machine, fmt.Errorf("platform %s requested but it's not available", QemuMachineAccelKVM)
	}
}

if machine.ObjectMeta.UID == "" {
	machine.ObjectMeta.UID = uuid.NewUUID()
}

machine.Status.State = machinev1alpha1.MachineStateUnknown

if len(machine.Status.StateDir) == 0 {
	machine.Status.StateDir = filepath.Join(config.G[config.KraftKit](ctx).RuntimeDir, string(machine.ObjectMeta.UID))
}

if err := os.MkdirAll(machine.Status.StateDir, fs.ModeSetgid|0o775); err != nil {
	return machine, err
}

// Set and create the log file for this machine
if len(machine.Status.LogFile) == 0 {
	machine.Status.LogFile = filepath.Join(machine.Status.StateDir, "machine.log")
}

if machine.Spec.Resources.Requests == nil {
	machine.Spec.Resources.Requests = make(corev1.ResourceList, 2)
}

if machine.Spec.Resources.Requests.Memory().Value() == 0 {
	quantity, err := resource.ParseQuantity("64Mi")
	if err != nil {
		machine.Status.State = machinev1alpha1.MachineStateFailed
		return machine, err
	}

	machine.Spec.Resources.Requests[corev1.ResourceMemory] = quantity
}

if machine.Spec.Resources.Requests.Cpu().Value() == 0 {
	quantity, err := resource.ParseQuantity("1")
	if err != nil {
		machine.Status.State = machinev1alpha1.MachineStateFailed
		return machine, err
	}

	machine.Spec.Resources.Requests[corev1.ResourceCPU] = quantity
}

qopts := []QemuOption{
	WithDaemonize(true),
	WithNoGraphic(true),
	WithPidFile(filepath.Join(machine.Status.StateDir, "machine.pid")),
	WithNoReboot(true),
	WithNoStart(true),
	WithName(string(machine.ObjectMeta.UID)),
	WithKernel(machine.Status.KernelPath),
	WithVGA(QemuVGANone),
	WithMemory(QemuMemory{
		// The value returned from Memory() is in bytes
		Size: uint64(machine.Spec.Resources.Requests.Memory().Value() / QemuMemoryScale),
		Unit: QemuMemoryUnitMB,
	}),
	// Create a QMP connection solely for manipulating the machine
	WithQMP(QemuHostCharDevUnix{
		SocketDir: machine.Status.StateDir,
		Name:      "qemu_control",
		NoWait:    true,
		Server:    true,
	}),
	// Create a QMP connection solely for listening to events
	WithQMP(QemuHostCharDevUnix{
		SocketDir: machine.Status.StateDir,
		Name:      "qemu_events",
		NoWait:    true,
		Server:    true,
	}),
	WithSerial(QemuHostCharDevFile{
		Monitor:  false,
		Filename: machine.Status.LogFile,
	}),
	WithMonitor(QemuHostCharDevUnix{
		SocketDir: machine.Status.StateDir,
		Name:      "qemu_mon",
		NoWait:    true,
		Server:    true,
	}),
	WithSMP(QemuSMP{
		CPUs:    uint64(machine.Spec.Resources.Requests.Cpu().Value()),
		Threads: 1,
		Sockets: 1,
	}),
	WithVGA(QemuVGANone),
	WithRTC(QemuRTC{
		Base: QemuRTCBaseUtc,
	}),
	WithDisplay(QemuDisplayNone{}),
	WithParallel(QemuHostCharDevNone{}),
}

// TODO: Parse Rootfs types
if len(machine.Status.InitrdPath) > 0 {
	qopts = append(qopts,
		WithInitRd(machine.Status.InitrdPath),
	)
}

kernelArgs, err := ukargparse.Parse(machine.Spec.KernelArgs...)
if err != nil {
	return machine, err
}

hostnetCounter := 0
// Start MAC addresses iteratively.  Each interface will have the last
// hexdecimal byte increase by 1 starting at 1, allowing for easy-to-spot
// interface IDs from the MAC address.  The return value below returns `:00`
// as the last byte.
startMac, err := macaddr.GenerateMacAddress(true)
if err != nil {
	return machine, err
}

if len(machine.Spec.Networks) > 0 {
	// Iterate over each interface of each network interface associated with
	// this machine and attach it as a device.
	for _, network := range machine.Spec.Networks {
		for _, iface := range network.Interfaces {
			mac := iface.Spec.MacAddress
			if mac == "" {
				// Increase the MAC address value by 1 such that we are able to
				// identify interface IDs.
				startMac = macaddr.IncrementMacAddress(startMac)
				mac = startMac.String()
			}

			hostnetid := fmt.Sprintf("hostnet%d", hostnetCounter)
			hostnetCounter++

			qopts = append(qopts,
				// TODO(nderjung): The network device should be customizable based on
				// the network spec or machine spec.  Additional insight can be provided
				// by inspecting the KConfig options.  Potentially the MachineSpec is
				// updated to reflect different systems or provide access to the
				// KConfig values.
				WithDevice(QemuDeviceVirtioNetPci{
					Netdev: hostnetid,
					Mac:    mac,
				}),
				WithNetDevice(QemuNetDevTap{
					Id:         hostnetid,
					Ifname:     iface.Spec.IfName,
					Br:         network.IfName,
					Script:     "no", // Disable execution
					Downscript: "no", // Disable execution
				}),
			)

			kernelArgs = append(kernelArgs,
				uknetdev.NewParamIp().WithValue(uknetdev.NetdevIp{
					CIDR:     iface.Spec.CIDR,
					Gateway:  network.Gateway,
					DNS0:     iface.Spec.DNS0,
					DNS1:     iface.Spec.DNS1,
					Hostname: iface.Spec.Hostname,
					Domain:   iface.Spec.Domain,
				}),
			)
		}
	}
}

if len(machine.Spec.Ports) > 0 {
	mac := ""
	hostfwds := make([]string, 0, len(machine.Spec.Ports))
	for _, port := range machine.Spec.Ports {
		if port.MacAddress != "" {
			mac = port.MacAddress
		}
		hostfwds = append(hostfwds, fmt.Sprintf("%s::%d-:%d", port.Protocol, port.HostPort, port.MachinePort))
	}
	if mac == "" {
		startMac = macaddr.IncrementMacAddress(startMac)
		mac = startMac.String()
	}

	hostnetid := fmt.Sprintf("hostnet%d", hostnetCounter)

	qopts = append(qopts,
		WithDevice(QemuDeviceVirtioNetPci{
			Mac:    mac,
			Netdev: hostnetid,
		}),
		WithNetDevice(QemuNetDevUser{
			Id:      hostnetid,
			Hostfwd: hostfwds,
		}),
	)
}

var fstab []string

for i, vol := range machine.Spec.Volumes {
	switch vol.Spec.Driver {
	case "9pfs":
		hvirtioid := fmt.Sprintf("hvirtio%d", i+1)
		mounttag := fmt.Sprintf("fs%d", i+1)
		qopts = append(qopts,
			WithFsDevice(QemuFsDevLocal{
				SecurityModel: QemuFsDevLocalSecurityModelMappedXattr,
				Id:            hvirtioid,
				Path:          vol.Spec.Source,
			}),
			WithDevice(QemuDeviceVirtio9pPci{
				Fsdev:    hvirtioid,
				MountTag: mounttag,
			}),
		)

		fstab = append(fstab, vfscore.NewFstabEntry(
			mounttag,
			vol.Spec.Destination,
			vol.Spec.Driver,
			// TODO(nderjung): Options (such as ro/rw) are yet supported by
			// Unikraft:
			"",
			"",
			// By default, create the directory if it does not exist when mounting.
			"mkmp",
		).String())

	case "initrd":
		fstab = append(fstab, vfscore.NewFstabEntry(
			"initrd0",
			vol.Spec.Destination,
			"extract",
			"",
			"",
			"",
		).String())
	default:
		return machine, fmt.Errorf("unsupported QEMU volume driver: %v", vol.Spec.Driver)
	}
}

if len(fstab) > 0 {
	kernelArgs = append(kernelArgs,
		vfscore.ParamVfsFstab.WithValue(fstab),
	)
}

var environ []string
for k, v := range machine.Spec.Env {
	environ = append(environ, fmt.Sprintf("%s=%s", k, v))
}

if len(environ) > 0 {
	kernelArgs = append(kernelArgs,
		posixenviron.ParamEnvVars.WithValue(environ),
	)
}

// TODO(nderjung): This is standard "Unikraft" positional argument syntax
// (kernel args and application arguments separated with "--").  The resulting
// string should be standardized through a central function.
args := kernelArgs.Strings()
if len(args) > 0 {
	args = append(args, "--")
}

// We do not need to append the kernel path since it is already provided
// by default by QEMU. QEMU sets arg[0] to the absolute path of the kernel
// image. If people want to change this they need to modify this script
// 'qemu-binfmt-conf.sh' when installing QEMU.
// args = append(args, filepath.Base(machine.Status.KernelPath))

args = append(args, machine.Spec.ApplicationArgs...)
qopts = append(qopts, WithAppend(args...))

switch machine.Spec.Architecture {
case "x86_64", "amd64":
	qopts = append(qopts,
		WithDevice(QemuDevicePvpanic{}),
	)
	if machine.Spec.Emulation {
		onFeatures := QemuCPUFeatures{QemuCPUFeaturePdpe1gb}

		onFeatures = append(onFeatures, QemuCPUFeatureRdrand)
		if qemuVersion.GreaterThanEqual(QemuVersion8_0_0) {
			onFeatures = append(onFeatures, QemuCPUFeatureRdseed)
		}

		qopts = append(qopts,
			WithMachine(QemuMachine{
				Type: QemuMachineTypePC,
			}),
			WithCPU(QemuCPU{
				CPU: QemuCPUX86Qemu64,
				On:  onFeatures,
				Off: QemuCPUFeatures{QemuCPUFeatureVmx, QemuCPUFeatureSvm},
			}),
		)
	} else {
		qopts = append(qopts,
			WithEnableKVM(true),
			WithMachine(QemuMachine{
				Type:         QemuMachineTypePC,
				Accelerators: []QemuMachineAccelerator{QemuMachineAccelKVM},
			}),
			WithCPU(QemuCPU{
				CPU: QemuCPUX86Host,
				On:  QemuCPUFeatures{QemuCPUFeatureX2apic},
				Off: QemuCPUFeatures{QemuCPUFeaturePmu},
			}),
		)
	}
	if qemuVersion.LessThan(QemuVersion8_0_0) {
		qopts = append(qopts,
			WithDevice(QemuDeviceSga{}),
		)
	}
case "arm", "arm64":
	qopts = append(qopts,
		WithMachine(QemuMachine{
			Type: QemuMachineTypeVirt,
		}),
		WithCPU(QemuCPU{
			CPU: QemuCPUArmMax,
		}),
	)

default:
	return nil, fmt.Errorf("unsupported architecture: %s", machine.Spec.Architecture)
}

// Create a log file just for the QEMU process which can be used to debug
// issues when starting the VMM.
qemuLogFile := filepath.Join(machine.Status.StateDir, "qemu.log")
fi, err := os.Create(qemuLogFile)
if err != nil {
	return machine, err
}

defer fi.Close()

service.eopts = append(service.eopts,
	exec.WithStdout(fi),
)

qcfg, err := NewQemuConfig(qopts...)
if err != nil {
	machine.Status.State = machinev1alpha1.MachineStateFailed
	return machine, fmt.Errorf("could not generate QEMU config: %v", err)
}

machine.Status.PlatformConfig = *qcfg

e, err := exec.NewExecutable(bin, *qcfg)
if err != nil {
	machine.Status.State = machinev1alpha1.MachineStateFailed
	return machine, fmt.Errorf("could not prepare QEMU executable: %v", err)
}

process, err := exec.NewProcessFromExecutable(e, service.eopts...)
if err != nil {
	machine.Status.State = machinev1alpha1.MachineStateFailed
	return machine, fmt.Errorf("could not prepare QEMU process: %v", err)
}

machine.CreationTimestamp = metav1.Now()

// Start and also wait for the process to be released, this ensures the
// program is actively being executed.
if err := process.StartAndWait(ctx); err != nil {
	machine.Status.State = machinev1alpha1.MachineStateFailed

	// Propagate the contents of the QEMU log file as an error
	if errLog, err2 := os.ReadFile(qemuLogFile); err2 == nil {
		err = errors.Join(fmt.Errorf("%s", strings.TrimSpace(string(errLog))), err)
	}

	return machine, fmt.Errorf("could not start and wait for QEMU process: %v", err)
}

machine.Status.State = machinev1alpha1.MachineStateCreated

return machine, nil

}

// Update implements kraftkit.sh/api/machine/v1alpha1.MachineService func (service *machineV1alpha1Service) Update(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { panic("not implemented: kraftkit.sh/machine/qemu.machineV1alpha1Service.Update") }

// getQEMUConfigFromPlatformConfig converts the provided platformConfig // interface into meaningful QemuConfig. func getQEMUConfigFromPlatformConfig(platformConfig interface{}) (*QemuConfig, error) { qcfgptr, ok := platformConfig.(*QemuConfig) if ok { return qcfgptr, nil }

// If we cannot directly cast it to the structure, attempt to decode a
// mapstructure version of the same configuration.
var qcfg QemuConfig
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
	Result: &qcfg,
	DecodeHook: mapstructure.ComposeDecodeHookFunc(
		// Directly embbed a less-erroring version of StringToIPHookFunc[0] which
		// does not return an error when parsing an IP that returns nil.
		// [0]: https://github.com/mitchellh/mapstructure/blob/bf980b35cac4dfd34e05254ee5aba086504c3f96/decode_hooks.go#L141-L163
		func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
			if f.Kind() != reflect.String {
				return data, nil
			}
			if t != reflect.TypeOf(net.IP{}) {
				return data, nil
			}

			// Instead of parsing it, just return an empty net.IP structure, since
			// we do not yet set IP addresses with Firecracker's configuration.
			return net.IP{}, nil
		},
	),
})
if err != nil {
	return nil, err
}

if err := decoder.Decode(platformConfig); err != nil {
	return nil, err
}

return &qcfg, nil

}

func qmpClientHandshake(conn *net.Conn) (*qmpapi.QEMUMachineProtocolClient, error) { qmpClient := qmpapi.NewQEMUMachineProtocolClient(*conn)

greeting, err := qmpClient.Greeting()
if err != nil {
	return nil, err
}

_, err = qmpClient.Capabilities(qmpapi.CapabilitiesRequest{
	Arguments: qmpapi.CapabilitiesRequestArguments{
		Enable: greeting.Qmp.Capabilities,
	},
})
if err != nil {
	return nil, err
}

return qmpClient, nil

}

func (service *machineV1alpha1Service) QMPClient(ctx context.Context, machine *machinev1alpha1.Machine) (*qmpapi.QEMUMachineProtocolClient, error) { qcfg, err := getQEMUConfigFromPlatformConfig(machine.Status.PlatformConfig) if err != nil { return nil, err }

// Always use index 0 for manipulating the machine
conn, err := qcfg.QMP[0].Connection()
if err != nil {
	return nil, err
}

return qmpClientHandshake(&conn)

}

func processFromPidFile(pidFile string) (*goprocess.Process, error) { pidData, err := os.ReadFile(pidFile) if err != nil { return nil, fmt.Errorf("could not read pid file: %v", err) }

pid, err := strconv.ParseUint(strings.TrimSpace(string(pidData)), 10, 64)
if err != nil {
	return nil, fmt.Errorf("could not convert pid string \"%s\" to uint64: %v", pidData, err)
}

process, err := goprocess.NewProcess(int32(pid))
if err != nil {
	return nil, fmt.Errorf("could not look up process %d: %v", pid, err)
}

return process, nil

}

// Watch implements kraftkit.sh/api/machine/v1alpha1.MachineService func (service *machineV1alpha1Service) Watch(ctx context.Context, machine *machinev1alpha1.Machine) (chan *machinev1alpha1.Machine, chan error, error) { events := make(chan *machinev1alpha1.Machine) errs := make(chan error)

qcfg, ok := machine.Status.PlatformConfig.(QemuConfig)
if !ok {
	return nil, nil, fmt.Errorf("cannot cast QEMU platform configuration from machine status")
}

// Always use index 1 for monitoring events
conn, err := qcfg.QMP[1].Connection()
if err != nil {
	return nil, nil, err
}

// Perform the handshake
_, err = qmpClientHandshake(&conn)
if err != nil {
	return nil, nil, err
}

monitor, err := qmp.NewQMPEventMonitor(conn,
	qmpapi.EventTypes(),
	nil,
)
if err != nil {
	return nil, nil, err
}

// firstCall is used to initialize the channel with the current state of the
// machine, so that it can be immediately acted upon.
firstCall := true

go func() {
accept:
	for {
		// First check if the context has been cancelled
		select {
		case <-ctx.Done():
			break accept
		default:
		}

		// Check the current state
		machine, err := service.Get(ctx, machine)
		if err != nil {
			errs <- err
			continue
		}

		// Initialize with the current state
		if firstCall {
			events <- machine
			firstCall = false
		}

		// Listen for changes in state
		event, err := monitor.Accept()
		if err != nil {
			errs <- err
			continue
		}

		// Send the event through the channel
		switch event.Event {
		case qmpapi.EVENT_STOP, qmpapi.EVENT_SUSPEND, qmpapi.EVENT_POWERDOWN:
			machine.Status.State = machinev1alpha1.MachineStatePaused
			events <- machine

		case qmpapi.EVENT_RESUME:
			machine.Status.State = machinev1alpha1.MachineStateRunning
			events <- machine

		case qmpapi.EVENT_RESET, qmpapi.EVENT_WAKEUP:
			machine.Status.State = machinev1alpha1.MachineStateRestarting
			events <- machine

		case qmpapi.EVENT_SHUTDOWN:
			machine.Status.State = machinev1alpha1.MachineStateExited
			events <- machine

			if !qcfg.NoShutdown {
				break accept
			}
		case qmpapi.EVENT_GUEST_PANICKED:
			machine.Status.State = machinev1alpha1.MachineStateErrored
			events <- machine

			if !qcfg.NoShutdown {
				break accept
			}
		default:
			errs <- fmt.Errorf("unsupported event: %s", event.Event)
		}
	}
}()

return events, errs, nil

}

// Start implements kraftkit.sh/api/machine/v1alpha1.MachineService func (service *machineV1alpha1Service) Start(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { qmpClient, err := service.QMPClient(ctx, machine) if err != nil && strings.HasSuffix(err.Error(), "connect: no such file or directory") { machine, err = service.Create(ctx, machine) if err != nil { return machine, err } qmpClient, err = service.QMPClient(ctx, machine) } if err != nil { return machine, fmt.Errorf("could not start qemu instance: %v", err) }

defer qmpClient.Close()
_, err = qmpClient.Cont(qmpapi.ContRequest{})
if err != nil {
	return machine, err
}

qcfg, ok := machine.Status.PlatformConfig.(QemuConfig)
if !ok {
	return machine, fmt.Errorf("cannot cast QEMU platform configuration from machine status")
}

process, err := processFromPidFile(qcfg.PidFile)
if err != nil {
	return machine, err
}

machine.Status.Pid = process.Pid
machine.Status.State = machinev1alpha1.MachineStateRunning
machine.Status.StartedAt = time.Now()

return machine, nil

}

// Pause implements kraftkit.sh/api/machine/v1alpha1.MachineService func (service *machineV1alpha1Service) Pause(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { qmpClient, err := service.QMPClient(ctx, machine) if err != nil { return machine, fmt.Errorf("could not pause qemu instance: %v", err) }

defer qmpClient.Close()

_, err = qmpClient.Stop(qmpapi.StopRequest{})
if err != nil {
	return machine, err
}

machine.Status.State = machinev1alpha1.MachineStatePaused

return machine, nil

}

// Logs implements kraftkit.sh/api/machine/v1alpha1.MachineService func (service *machineV1alpha1Service) Logs(ctx context.Context, machine *machinev1alpha1.Machine) (chan string, chan error, error) { out, errOut, err := logtail.NewLogTail(ctx, machine.Status.LogFile) if err != nil { return nil, nil, err }

qcfg, ok := machine.Status.PlatformConfig.(QemuConfig)
if !ok {
	return nil, nil, fmt.Errorf("cannot cast QEMU platform configuration from machine status")
}

// Wait and trim the preamble from the logs before returning
for {
	select {
	case line := <-out:
		if !qcfg.ShowSGABiosPreamble && machine.Spec.Architecture == "x86_64" {
			if strings.Contains(line, "Booting from ") {
				qcfg.ShowSGABiosPreamble = true
				machine.Status.PlatformConfig = qcfg
				return out, errOut, nil
			}
			continue
		}
		return out, errOut, nil

	case err := <-errOut:
		if err != io.EOF {
			return nil, nil, fmt.Errorf("reading log: %w", err)
		}

	case <-ctx.Done():
		return out, errOut, nil
	}
}

}

// Get implements kraftkit.sh/api/machine/v1alpha1/MachineService.Get func (service *machineV1alpha1Service) Get(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { state := machinev1alpha1.MachineStateUnknown savedState := machine.Status.State

qcfg, ok := machine.Status.PlatformConfig.(QemuConfig)
if !ok {
	return machine, fmt.Errorf("cannot read QEMU platform configuration from machine status")
}

// Set the cpu and memory resources
// TODO(craciunouc): This is a temporary solution until we have proper
// un/marshalling of the resources (and all structures).
machine.Spec.Resources.Requests[corev1.ResourceCPU] = resource.MustParse("1")

// Backwards compatibility with older runs
memory := "0Mi"
if qcfg.Memory.String() != "" {
	memory = strings.SplitN(qcfg.Memory.String(), "=", 2)[1]
}

machine.Spec.Resources.Requests[corev1.ResourceMemory] = resource.MustParse(memory)

// Check if the process is alive, which ultimately indicates to us whether we
// able to speak to the exposed QMP socket
activeProcess := false
if process, err := processFromPidFile(qcfg.PidFile); err == nil {
	activeProcess, err = process.IsRunning()
	if err != nil {
		state = machinev1alpha1.MachineStateExited
	}
}

exitedAt := machine.Status.ExitedAt
exitCode := machine.Status.ExitCode

defer func() {
	if exitCode >= 0 && machine.Status.ExitedAt.IsZero() {
		exitedAt = time.Now()
	}

	// Update the machine config with the latest values if they are different from
	// what we have on record
	if machine.Status.ExitedAt != exitedAt || machine.Status.ExitCode != exitCode {
		machine.Status.ExitedAt = exitedAt
		machine.Status.ExitCode = exitCode
	}

	// Set the start time to now if it was not previously set
	if machine.Status.StartedAt.IsZero() && state == machinev1alpha1.MachineStateRunning {
		machine.Status.StartedAt = time.Now()
	}

	// Finally, save the state if it is different from the what we have on
	// record
	if state != machinev1alpha1.MachineStateUnknown && state != savedState {
		machine.Status.State = state
	}
}()

if !activeProcess {
	state = machinev1alpha1.MachineStateExited
	if savedState == machinev1alpha1.MachineStateRunning {
		exitCode = 1
	}
	return machine, nil
}

qmpClient, err := service.QMPClient(ctx, machine)
if err != nil && errors.Is(err, os.ErrNotExist) {
	state = machinev1alpha1.MachineStateExited
	exitCode = 1
	return machine, nil
} else if err != nil {
	return machine, errors.Join(ErrCouldNotAttachQMPClient, err)
}

defer qmpClient.Close()

// Grab the actual state of the machine by querying QMP
status, err := qmpClient.QueryStatus(qmpapi.QueryStatusRequest{})
if err != nil {
	// We cannot amend the status at this point, even if the process is
	// alive, since it is not an indicator of the state of the VM, only of the
	// VMM.  So we return what we already know via LookupMachineConfig.
	return machine, errors.Join(ErrCouldNotQueryMachineViaQMP, err)
}

// Map the QMP status to supported machine states
switch status.Return.Status {
case qmpapi.RUN_STATE_GUEST_PANICKED:
	state = machinev1alpha1.MachineStateErrored
	exitCode = 1

case qmpapi.RUN_STATE_INTERNAL_ERROR, qmpapi.RUN_STATE_IO_ERROR:
	state = machinev1alpha1.MachineStateFailed
	exitCode = 1

case qmpapi.RUN_STATE_PAUSED:
	state = machinev1alpha1.MachineStatePaused
	exitCode = -1

case qmpapi.RUN_STATE_RUNNING:
	state = machinev1alpha1.MachineStateRunning
	exitCode = -1

case qmpapi.RUN_STATE_SHUTDOWN:
	state = machinev1alpha1.MachineStateExited
	exitCode = 0

case qmpapi.RUN_STATE_SUSPENDED:
	state = machinev1alpha1.MachineStateSuspended
	exitCode = -1

default:
	// qmpapi.RUN_STATE_SAVE_VM,
	// qmpapi.RUN_STATE_PRELAUNCH,
	// qmpapi.RUN_STATE_RESTORE_VM,
	// qmpapi.RUN_STATE_WATCHDOG,
	state = machinev1alpha1.MachineStateUnknown
	exitCode = -1
}

return machine, nil

}

// List implements kraftkit.sh/api/machine/v1alpha1.MachineService.List func (service *machineV1alpha1Service) List(ctx context.Context, machines *machinev1alpha1.MachineList) (*machinev1alpha1.MachineList, error) { cached := machines.Items machines.Items = []zip.Object[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus]{}

// Iterate through each machine and grab the latest status
for _, machine := range cached {
	machine, err := service.Get(ctx, &machine)
	// It's fine to list machines that are not running, so we ignore the error
	// as long as it is not related to the QMP client.
	if err != nil && !errors.Is(err, ErrCouldNotAttachQMPClient) && !errors.Is(err, ErrCouldNotQueryMachineViaQMP) {
		machines.Items = cached
		return machines, err
	}

	machines.Items = append(machines.Items, *machine)
}

return machines, nil

}

// Stop implements kraftkit.sh/api/machine/v1alpha1.MachineService.Stop func (service *machineV1alpha1Service) Stop(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { qmpClient, err := service.QMPClient(ctx, machine) if err != nil { if strings.HasSuffix(err.Error(), "connect: no such file or directory") { machine.Status.State = machinev1alpha1.MachineStateExited machine.Status.ExitedAt = time.Now() return machine, nil }

	return machine, fmt.Errorf("could not stop qemu instance: %v", err)
}

defer qmpClient.Close()
_, err = qmpClient.Quit(qmpapi.QuitRequest{})
if err != nil {
	return machine, err
}

qcfg, ok := machine.Status.PlatformConfig.(QemuConfig)
if !ok {
	return machine, fmt.Errorf("cannot read QEMU platform configuration from machine status")
}

machine.Status.State = machinev1alpha1.MachineStateExited

if err := retrytimeout.RetryTimeout(5*time.Second, func() error {
	if _, err := os.ReadFile(qcfg.PidFile); !os.IsNotExist(err) {
		return fmt.Errorf("process still active")
	}

	return nil
}); err != nil {
	return machine, err
}

return machine, nil

}

// Delete implements kraftkit.sh/api/machine/v1alpha1.MachineService.Delete func (service *machineV1alpha1Service) Delete(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { qcfg, ok := machine.Status.PlatformConfig.(QemuConfig) if !ok { return machine, fmt.Errorf("cannot read QEMU platform configuration from machine status") }

var errs merr.Errors

err := os.RemoveAll(machine.Status.StateDir)
if err != nil {
	errs = append(errs, fmt.Errorf("error deleting QEMU's state directory %s: %w", machine.Status.StateDir, err))
}

// Do not throw errors (likely these resources do not exist at this point)
// when trying to remove ephemeral files that are controlled by the QEMU
// process.
_ = os.Remove(qcfg.QMP[0].Resource())
_ = os.Remove(qcfg.QMP[1].Resource())

errs = append(errs, os.RemoveAll(machine.Status.LogFile))
errs = append(errs, os.RemoveAll(machine.Status.StateDir))

return nil, errs.Err()

}

@rapeazabe888's activity is private