From fc7fe2e6c02b28e3d6361a5e562ad7ec906ea674 Mon Sep 17 00:00:00 2001 From: AlexSSD7 Date: Fri, 1 Sep 2023 18:17:20 +0100 Subject: [PATCH] Start refactor how qemu-system commands are built --- qemucli/args.go | 81 +++++++++++++++++++++++++++++++++++++ qemucli/flag_arg.go | 45 +++++++++++++++++++++ qemucli/map_arg.go | 92 +++++++++++++++++++++++++++++++++++++++++++ qemucli/str_arg.go | 54 +++++++++++++++++++++++++ qemucli/uint_arg.go | 48 ++++++++++++++++++++++ qemucli/validation.go | 27 +++++++++++++ utils/int.go | 15 +++++++ vm/cfg.go | 65 ++++++++++++++++++++++++++++++ 8 files changed, 427 insertions(+) create mode 100644 qemucli/args.go create mode 100644 qemucli/flag_arg.go create mode 100644 qemucli/map_arg.go create mode 100644 qemucli/str_arg.go create mode 100644 qemucli/uint_arg.go create mode 100644 qemucli/validation.go create mode 100644 utils/int.go create mode 100644 vm/cfg.go diff --git a/qemucli/args.go b/qemucli/args.go new file mode 100644 index 0000000..4edaa2b --- /dev/null +++ b/qemucli/args.go @@ -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 +} diff --git a/qemucli/flag_arg.go b/qemucli/flag_arg.go new file mode 100644 index 0000000..af3e44d --- /dev/null +++ b/qemucli/flag_arg.go @@ -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 +} diff --git a/qemucli/map_arg.go b/qemucli/map_arg.go new file mode 100644 index 0000000..b48dcba --- /dev/null +++ b/qemucli/map_arg.go @@ -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 +} diff --git a/qemucli/str_arg.go b/qemucli/str_arg.go new file mode 100644 index 0000000..960aa4a --- /dev/null +++ b/qemucli/str_arg.go @@ -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 +} diff --git a/qemucli/uint_arg.go b/qemucli/uint_arg.go new file mode 100644 index 0000000..ed00537 --- /dev/null +++ b/qemucli/uint_arg.go @@ -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 +} diff --git a/qemucli/validation.go b/qemucli/validation.go new file mode 100644 index 0000000..e4db7d8 --- /dev/null +++ b/qemucli/validation.go @@ -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 +} diff --git a/utils/int.go b/utils/int.go new file mode 100644 index 0000000..883cafc --- /dev/null +++ b/utils/int.go @@ -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) +} diff --git a/vm/cfg.go b/vm/cfg.go new file mode 100644 index 0000000..e003788 --- /dev/null +++ b/vm/cfg.go @@ -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 +}