Start refactor how qemu-system commands are built
This commit is contained in:
parent
153d1b5e1f
commit
fc7fe2e6c0
8 changed files with 427 additions and 0 deletions
81
qemucli/args.go
Normal file
81
qemucli/args.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArgAcceptedValue string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ArgAcceptedValueUint ArgAcceptedValue = "uint"
|
||||||
|
ArgAcceptedValueString ArgAcceptedValue = "string"
|
||||||
|
ArgAcceptedValueMap ArgAcceptedValue = "map"
|
||||||
|
ArgAcceptedValueNone ArgAcceptedValue = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
var safeArgs = map[string]ArgAcceptedValue{
|
||||||
|
"accel": ArgAcceptedValueString,
|
||||||
|
"boot": ArgAcceptedValueString,
|
||||||
|
"m": ArgAcceptedValueUint,
|
||||||
|
"smp": ArgAcceptedValueUint,
|
||||||
|
"device": ArgAcceptedValueMap,
|
||||||
|
"netdev": ArgAcceptedValueMap,
|
||||||
|
"serial": ArgAcceptedValueString,
|
||||||
|
"cdrom": ArgAcceptedValueString,
|
||||||
|
"machine": ArgAcceptedValueString,
|
||||||
|
"cpu": ArgAcceptedValueString,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Arg interface {
|
||||||
|
StringKey() string
|
||||||
|
StringValue() string
|
||||||
|
ValueType() ArgAcceptedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeArgs(args []Arg) ([]string, error) {
|
||||||
|
var cmdArgs []string
|
||||||
|
|
||||||
|
for i, arg := range args {
|
||||||
|
flag, value, err := EncodeArg(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "encode flag #%v", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs = append(cmdArgs, flag)
|
||||||
|
if value != nil {
|
||||||
|
cmdArgs = append(cmdArgs, *value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeArg(a Arg) (string, *string, error) {
|
||||||
|
// We're making copies because we don't want to trust
|
||||||
|
// that Arg always returns the same value.
|
||||||
|
argKey := a.StringKey()
|
||||||
|
argValueType := a.ValueType()
|
||||||
|
|
||||||
|
err := validateArgKey(argKey, argValueType)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrap(err, "validate arg key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if argValueType == ArgAcceptedValueNone {
|
||||||
|
if a.StringValue() != "" {
|
||||||
|
return "", nil, fmt.Errorf("arg returned a value while declaring no value (type %v)", reflect.TypeOf(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return argKey, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
argValueStr := a.StringValue()
|
||||||
|
if argValueStr == "" {
|
||||||
|
return "", nil, fmt.Errorf("empty string value while declaring non-empty value (type %v)", reflect.TypeOf(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "-" + argKey, &argValueStr, nil
|
||||||
|
}
|
||||||
45
qemucli/flag_arg.go
Normal file
45
qemucli/flag_arg.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlagArg struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNewFlagArg(key string) *FlagArg {
|
||||||
|
a, err := NewFlagArg(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlagArg(key string) (*FlagArg, error) {
|
||||||
|
a := &FlagArg{
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preflight arg key/type check.
|
||||||
|
err := validateArgKey(a.key, a.ValueType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate arg key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FlagArg) StringKey() string {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FlagArg) StringValue() string {
|
||||||
|
// Boolean flags have no value.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FlagArg) ValueType() ArgAcceptedValue {
|
||||||
|
return ArgAcceptedValueNone
|
||||||
|
}
|
||||||
92
qemucli/map_arg.go
Normal file
92
qemucli/map_arg.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MapArg struct {
|
||||||
|
key string
|
||||||
|
values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNewMapArg(key string, values map[string]string) *MapArg {
|
||||||
|
a, err := NewMapArg(key, values)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMapArg(key string, values map[string]string) (*MapArg, error) {
|
||||||
|
a := &MapArg{
|
||||||
|
key: key,
|
||||||
|
values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preflight arg key/type check.
|
||||||
|
err := validateArgKey(key, a.ValueType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate arg key")
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
// The reason why we're making copies here and creating
|
||||||
|
// a whole other copy of the entire map is because maps
|
||||||
|
// are pointers, and we do not want to reference anything
|
||||||
|
// that will not be able to validate except at this stage
|
||||||
|
// of MapArg creation.
|
||||||
|
k := k
|
||||||
|
v := v
|
||||||
|
|
||||||
|
if len(k) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty map key not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v) == 1 {
|
||||||
|
// Values *can* be empty, though. We do not allow them for consistency.
|
||||||
|
return nil, fmt.Errorf("empty map value for key '%v' is not allowed", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateArgStrValue(k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "validate map key '%v'", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateArgStrValue(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "validate map value '%v'", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.values[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MapArg) StringKey() string {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MapArg) StringValue() string {
|
||||||
|
sb := new(strings.Builder)
|
||||||
|
for k, v := range a.values {
|
||||||
|
// We're not validating anything here because
|
||||||
|
// we expect that the keys/values were validated
|
||||||
|
// at the creation of the MapArg.
|
||||||
|
|
||||||
|
sb.WriteString(k)
|
||||||
|
if len(v) > 0 {
|
||||||
|
sb.WriteString("=" + v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MapArg) ValueType() ArgAcceptedValue {
|
||||||
|
return ArgAcceptedValueMap
|
||||||
|
}
|
||||||
54
qemucli/str_arg.go
Normal file
54
qemucli/str_arg.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StringArg struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNewStringArg(key string, value string) *StringArg {
|
||||||
|
a, err := NewStringArg(key, value)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStringArg(key string, value string) (*StringArg, error) {
|
||||||
|
a := &StringArg{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preflight arg key/type check.
|
||||||
|
err := validateArgKey(a.key, a.ValueType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate arg key")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateArgStrValue(a.value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate str value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *StringArg) StringKey() string {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *StringArg) StringValue() string {
|
||||||
|
// We're not validating anything here because
|
||||||
|
// we expect that the string value was validated
|
||||||
|
// at the creation of the StringArg.
|
||||||
|
return a.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *StringArg) ValueType() ArgAcceptedValue {
|
||||||
|
return ArgAcceptedValueString
|
||||||
|
}
|
||||||
48
qemucli/uint_arg.go
Normal file
48
qemucli/uint_arg.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexSSD7/linsk/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UintArg struct {
|
||||||
|
key string
|
||||||
|
value uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustNewUintArg[T constraints.Integer](key string, value T) *UintArg {
|
||||||
|
a, err := NewUintArg(key, uint64(value))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUintArg(key string, value uint64) (*UintArg, error) {
|
||||||
|
a := &UintArg{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preflight arg key/type check.
|
||||||
|
err := validateArgKey(key, a.ValueType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate arg key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UintArg) StringKey() string {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UintArg) StringValue() string {
|
||||||
|
return utils.UintToStr(a.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UintArg) ValueType() ArgAcceptedValue {
|
||||||
|
return ArgAcceptedValueUint
|
||||||
|
}
|
||||||
27
qemucli/validation.go
Normal file
27
qemucli/validation.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package qemucli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateArgKey(key string, t ArgAcceptedValue) error {
|
||||||
|
allowedValue, ok := safeArgs[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown safe arg '%v'", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if want, have := allowedValue, t; want != have {
|
||||||
|
return fmt.Errorf("bad arg value type: want '%v', have '%v'", allowedValue, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateArgStrValue(s string) error {
|
||||||
|
if strings.Contains(s, ",") {
|
||||||
|
return fmt.Errorf("commas are not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
utils/int.go
Normal file
15
utils/int.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IntToStr[T constraints.Signed](v T) string {
|
||||||
|
return strconv.FormatInt(int64(v), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UintToStr[T constraints.Unsigned](v T) string {
|
||||||
|
return strconv.FormatUint(uint64(v), 10)
|
||||||
|
}
|
||||||
65
vm/cfg.go
Normal file
65
vm/cfg.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexSSD7/linsk/qemucli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configureBaseVMCmd(logger *slog.Logger, cfg VMConfig) (string, []qemucli.Arg, error) {
|
||||||
|
baseCmd := "qemu-system"
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
baseCmd += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []qemucli.Arg{
|
||||||
|
qemucli.MustNewStringArg("serial", "stdio"),
|
||||||
|
qemucli.MustNewUintArg("m", cfg.MemoryAlloc),
|
||||||
|
qemucli.MustNewUintArg("m", runtime.NumCPU()),
|
||||||
|
}
|
||||||
|
|
||||||
|
var accel string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// TODO: To document: For Windows, we need to install QEMU using an installer and add it to PATH.
|
||||||
|
// Then, we should enable Windows Hypervisor Platform in "Turn Windows features on or off".
|
||||||
|
// IMPORTANT: We should also install libusbK drivers for USB devices we want to pass through.
|
||||||
|
// This can be easily done with a program called Zadiag by Akeo.
|
||||||
|
accel = "whpx,kernel-irqchip=off"
|
||||||
|
case "darwin":
|
||||||
|
accel = "hvf"
|
||||||
|
default:
|
||||||
|
accel = "kvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
baseCmd += "-x86_64"
|
||||||
|
case "arm64":
|
||||||
|
if cfg.BIOSPath == "" {
|
||||||
|
logger.Warn("BIOS image path is not specified while attempting to run an aarch64 (arm64) VM. The VM will not boot.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// "highmem=off" is required for M1.
|
||||||
|
args = append(args,
|
||||||
|
qemucli.MustNewMapArg("machine", map[string]string{"type": "virt", "highmem": "off"}),
|
||||||
|
qemucli.MustNewStringArg("cpu", "host"),
|
||||||
|
)
|
||||||
|
|
||||||
|
baseCmd += "-aarch64"
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("arch '%v' is not supported", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, qemucli.MustNewStringArg("accel", accel))
|
||||||
|
|
||||||
|
if cfg.BIOSPath != "" {
|
||||||
|
args = append(args, qemucli.MustNewStringArg("bios", filepath.Clean(cfg.BIOSPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseCmd, args, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue