diff --git a/client/configuration.go b/client/configuration.go index 133845c..3b645f8 100644 --- a/client/configuration.go +++ b/client/configuration.go @@ -11,6 +11,7 @@ import ( "github.com/0xef53/kvmrun/client/flag_types" "github.com/0xef53/kvmrun/kvmrun" + "github.com/0xef53/kvmrun/kvmrun/backend/block" pb_machines "github.com/0xef53/kvmrun/api/services/machines/v2" pb_types "github.com/0xef53/kvmrun/api/types/v2" @@ -145,6 +146,223 @@ func MachineInspect(ctx context.Context, vmname string, c *cli.Command, grpcClie return nil } +func MachineInfo(ctx context.Context, vmname string, c *cli.Command, grpcClient *grpc_interfaces.Kvmrun) error { + req := pb_machines.GetRequest{ + Name: vmname, + } + + resp, err := grpcClient.Machines().Get(ctx, &req) + if err != nil { + return err + } + + appendLine := func(s string, a ...interface{}) string { + switch len(a) { + case 0: + s += "\n" + case 1: + s += fmt.Sprintf("%*s : \n", 20, a[0]) + case 2: + var format string + switch a[1].(type) { + case int, int32, int64, uint, uint32, uint64: + format = "%*s : %d\n" + case string: + format = "%*s : %s\n" + default: + format = "%*s : %q\n" + } + s += fmt.Sprintf(format, 20, a[0], a[1]) + } + return s + } + + bootDevice := func(opts *pb_types.MachineOpts) string { + var bootdev string = "default" + var bootidx uint32 = ^uint32(0) + + for _, d := range opts.Cdrom { + if d.Bootindex > 0 && d.Bootindex < bootidx { + bootdev = d.Name + bootidx = d.Bootindex + } + } + + for _, d := range opts.Storage { + if d.Bootindex > 0 && d.Bootindex < bootidx { + bootdev = d.Path + bootidx = d.Bootindex + } + } + + return bootdev + } + + printBrief := func(m *pb_types.Machine) { + var opts *pb_types.MachineOpts + + if m.Runtime != nil { + opts = m.Runtime + } else { + opts = m.Config + } + + var s string + + // Header + if m.Runtime != nil { + s += fmt.Sprintf("* %s (state: %s, pid: %d", m.Name, m.State, m.PID) + } else { + s += fmt.Sprintf("* %s (state: %s", m.Name, m.State) + } + + if opts.VsockDevice != nil { + s += fmt.Sprintf(", cid: %d)", opts.VsockDevice.ContextID) + } else { + s += ")" + } + + s = appendLine(s) + + // Machine type + var machineType string + + if m.Runtime != nil { + machineType = opts.MachineType + } else { + machineType = "default" + } + + s = appendLine(s, "Machine type", machineType) + s = appendLine(s) + + // Processor + if opts.CPU != nil { + s = appendLine(s, "Processor") + s = appendLine(s, "Model", opts.CPU.Model) + s = appendLine(s, "Actual", opts.CPU.Actual) + s = appendLine(s, "Total", opts.CPU.Total) + + if c.Bool("verbose") { + s = appendLine(s, "Sockets", opts.CPU.Sockets) + } + + s = appendLine(s) + } + + // Memory + if opts.Memory != nil { + s = appendLine(s, "Memory") + s = appendLine(s, "Actual", fmt.Sprintf("%d MiB", opts.Memory.Actual)) + s = appendLine(s, "Total", fmt.Sprintf("%d MiB", opts.Memory.Total)) + s = appendLine(s) + } + + // Firmware + if opts.Firmware != nil { + s = appendLine(s, "Firmware") + s = appendLine(s, "Image", opts.Firmware.Image) + s = appendLine(s, "Flash", opts.Firmware.Flash) + s = appendLine(s) + } + + s = appendLine(s, "Boot device", bootDevice(opts)) + + if opts.CloudInitDrive != nil { + s = appendLine(s, "CloudInit", opts.CloudInitDrive.Path) + } + + s = appendLine(s) + + // Cdrom + if count := len(opts.Cdrom); count > 0 { + s = appendLine(s, "Cdroms", count) + + for _, d := range opts.Cdrom { + s = appendLine(s, d.Name, fmt.Sprintf("%s, %s", d.Driver, d.Media)) + } + + s = appendLine(s) + } + + // Storage + if count := len(opts.Storage); count > 0 { + s = appendLine(s, "Storage", count) + + for _, d := range opts.Storage { + // ignore all errors -- it's OK in this case + size, _ := block.GetSize64(d.Path) + + s = appendLine(s, filepath.Base(d.Path), fmt.Sprintf("%.2f GiB, %s", float64(size/(1<<30)), d.Driver)) + + if c.Bool("verbose") { + s = appendLine(s, "Path", d.Path) + s = appendLine(s, "IopsRd", d.IopsRd) + s = appendLine(s, "IopsWr", d.IopsWr) + s = appendLine(s) + } + } + + s = appendLine(s) + } + + // Network + if count := len(opts.Network); count > 0 { + s = appendLine(s, "Network", count) + + for _, nc := range opts.Network { + s = appendLine(s, nc.Ifname, fmt.Sprintf("%s, %s (queue = %d)", nc.HwAddr, nc.Driver, nc.Queues)) + + if c.Bool("verbose") { + s = appendLine(s, "Ifup", nc.Ifup) + s = appendLine(s, "Ifdown", nc.Ifdown) + s = appendLine(s) + } + } + + s = appendLine(s) + } + + // Input devices + if count := len(opts.Inputs); count > 0 { + s = appendLine(s, "Input devices", count) + + for _, d := range opts.Inputs { + s = appendLine(s, "", d.Type) + } + + s = appendLine(s) + } + + // Host PCI devices + if count := len(opts.HostPCI); count > 0 { + s = appendLine(s, "Host devices", count) + + for _, d := range opts.HostPCI { + s = appendLine(s, "", d.PCIAddr) + } + + s = appendLine(s) + } + + // External kernel + if opts.Kernel != nil { + s = appendLine(s, "External kernel") + s = appendLine(s, "Image", opts.Kernel.Image) + s = appendLine(s, "Cmdline", opts.Kernel.Cmdline) + s = appendLine(s, "Initrd", opts.Kernel.Initrd) + s = appendLine(s, "Modules", opts.Kernel.Modiso) + s = appendLine(s) + } + + fmt.Printf("%s", s) + } + + printBrief(resp.Machine) + + return nil +} + func MachineListEvents(ctx context.Context, vmname string, c *cli.Command, grpcClient *grpc_interfaces.Kvmrun) error { req := pb_machines.GetEventsRequest{ Name: vmname, diff --git a/client/ifupdown/ifupdown.go b/client/ifupdown/ifupdown.go index 3c10f24..7537aca 100644 --- a/client/ifupdown/ifupdown.go +++ b/client/ifupdown/ifupdown.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/0xef53/kvmrun/kvmrun" @@ -27,11 +28,11 @@ func InterfaceUp(ctx context.Context, ifname string, secondStage bool) error { var vmname string if cwd, err := os.Getwd(); err == nil { - if err := kvmrun.ValidateMachineName(cwd); err != nil { + if err := kvmrun.ValidateMachineName(filepath.Base(cwd)); err != nil { return err } - vmname = cwd + vmname = filepath.Base(cwd) } else { return fmt.Errorf("cannot determine machine name: %w", err) } @@ -67,11 +68,11 @@ func InterfaceDown(ctx context.Context, ifname string) error { var vmname string if cwd, err := os.Getwd(); err == nil { - if err := kvmrun.ValidateMachineName(cwd); err != nil { + if err := kvmrun.ValidateMachineName(filepath.Base(cwd)); err != nil { return err } - vmname = cwd + vmname = filepath.Base(cwd) } else { return fmt.Errorf("cannot determine machine name: %w", err) } diff --git a/cmd/vmm/commands/configuration.go b/cmd/vmm/commands/configuration.go index c882636..1f84a44 100644 --- a/cmd/vmm/commands/configuration.go +++ b/cmd/vmm/commands/configuration.go @@ -41,9 +41,23 @@ var CommandRemoveConf = &cli.Command{ }, } +var CommandInfo = &cli.Command{ + Name: "info", + Usage: "print a virtual machine details in human-readable format", + ArgsUsage: "VMNAME", + HideHelp: true, + Category: "Configuration", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "enable verbose output"}, + }, + Action: func(ctx context.Context, c *cli.Command) error { + return grpc_client.CommandGRPC(ctx, c, client.MachineInfo) + }, +} + var CommandInspect = &cli.Command{ Name: "inspect", - Usage: "print a virtual machine details", + Usage: "print low-level information about a virtual machine in JSON", ArgsUsage: "VMNAME", HideHelp: true, Category: "Configuration", diff --git a/cmd/vmm/main.go b/cmd/vmm/main.go index e04131b..b110220 100644 --- a/cmd/vmm/main.go +++ b/cmd/vmm/main.go @@ -55,6 +55,7 @@ func main() { commands.CommandCreateConf, commands.CommandRemoveConf, commands.CommandPrintList, + commands.CommandInfo, commands.CommandInspect, commands.MemoryCommands, commands.CPUCommands, diff --git a/cmd/vnetctl/commands.go b/cmd/vnetctl/commands.go index ffaf1c0..9aecb7c 100644 --- a/cmd/vnetctl/commands.go +++ b/cmd/vnetctl/commands.go @@ -37,7 +37,7 @@ var CommandCreateConf = &cli.Command{ var CommandUpdateConf = &cli.Command{ Name: "update-conf", - Usage: "create a new network configuration", + Usage: "update an existing network configuration", ArgsUsage: "VMNAME IFNAME", HideHelp: true, Category: "Configuration", diff --git a/contrib/qemu.wrapper.debian b/contrib/qemu.wrapper.debian index 3629124..4058eef 100644 --- a/contrib/qemu.wrapper.debian +++ b/contrib/qemu.wrapper.debian @@ -30,10 +30,6 @@ if [[ "$QEMU_ROOTDIR" != "/" ]] ; then mount --bind "/sys/fs/cgroup" "${QEMU_ROOTDIR}/sys/fs/cgroup" fi - if mountpoint -q --nofollow "/sys/fs/cgroup/net_cls" ; then - mount --bind "/sys/fs/cgroup/net_cls" "${QEMU_ROOTDIR}/sys/fs/cgroup/net_cls" - fi - # kvmrun dirs install -d "${QEMU_ROOTDIR}/etc/kvmrun" install -d "${QEMU_ROOTDIR}/usr/lib/kvmrun" @@ -54,8 +50,12 @@ if [[ "$QEMU_ROOTDIR" != "/" ]] ; then /usr/lib/kvmrun/delegate-cgroup-v1-controller "$SYSTEMD_UNITNAME" "net_cls" fi + if mountpoint -q --nofollow "/sys/fs/cgroup/net_cls" ; then + mount --bind --make-shared "/sys/fs/cgroup/net_cls" "${QEMU_ROOTDIR}/sys/fs/cgroup/net_cls" + fi + # run chrooted QEMU binary - exec chroot "$QEMU_ROOTDIR" sh -c "cd $PWD && exec /usr/bin/qemu-system-x86_64 ${ARGS}" + exec chroot "$QEMU_ROOTDIR" /bin/sh -c "cd $PWD && exec /usr/bin/qemu-system-x86_64 ${ARGS}" fi UNSHARED=1 exec unshare -m "$0" ${ARGS} diff --git a/internal/hostnet/vrouter.go b/internal/hostnet/vrouter.go index 8014da6..d1ab5cd 100644 --- a/internal/hostnet/vrouter.go +++ b/internal/hostnet/vrouter.go @@ -224,11 +224,11 @@ func RouterConfigureAddrs(linkname string, addrs []string, gateway4, gateway6 st for _, addr := range addrs { if err := routerAddRoute(link, addr, "main"); err != nil { - fmt.Printf("DEBUG ConfigureRouterAddrs(): addRoute err (type = %T): %+v\n", err, err) + //fmt.Printf("DEBUG ConfigureRouterAddrs(): addRoute err (type = %T): %+v\n", err, err) return err } if err := routerAddRule(link, addr, "main"); err != nil { - fmt.Printf("DEBUG ConfigureRouterAddrs(): addRule err (type = %T): %+v\n", err, err) + //fmt.Printf("DEBUG ConfigureRouterAddrs(): addRule err (type = %T): %+v\n", err, err) return err } } diff --git a/internal/utils/network.go b/internal/utils/network.go index f55e732..167786a 100644 --- a/internal/utils/network.go +++ b/internal/utils/network.go @@ -2,9 +2,11 @@ package utils import ( "bufio" + "errors" "fmt" "net" "os" + "path/filepath" "strconv" "strings" @@ -24,34 +26,70 @@ func ParseIPNet(s string) (*net.IPNet, error) { } func GetRouteTableIndex(table string) (int, error) { - fd, err := os.Open("/etc/iproute2/rt_tables") - if err != nil { - return -1, err - } - defer fd.Close() + var errTableNotFound = errors.New("table not found") + + findIn := func(fname string) (int, error) { + fd, err := os.Open(fname) + if err != nil { + return -1, err + } + defer fd.Close() + + scanner := bufio.NewScanner(fd) - scanner := bufio.NewScanner(fd) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "#") { + continue + } + + ff := strings.Fields(line) + + if len(ff) == 2 && strings.ToLower(ff[1]) == table { + if v, err := strconv.Atoi(ff[0]); err == nil { + return v, nil + } else { + return -1, err + } + } + } - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, "#") { - continue + if err := scanner.Err(); err != nil { + return -1, err } - ff := strings.Fields(line) + return -1, errTableNotFound + } + + possiblePlaces := []string{ + "/etc/iproute2/rt_tables", + "/usr/share/iproute2/rt_tables", + } - if len(ff) == 2 && strings.ToLower(ff[1]) == table { - if v, err := strconv.Atoi(ff[0]); err == nil { - return v, nil - } else { - return -1, err + // Also look in /etc/iproute2/rt_tables.d/* + if ff, err := os.ReadDir("/etc/iproute2/rt_tables.d"); err == nil { + for _, f := range ff { + if f.Type().IsRegular() { + possiblePlaces = append(possiblePlaces, filepath.Join("/etc/iproute2/rt_tables.d", f.Name())) } } + } else { + if !os.IsNotExist(err) { + return -1, err + } } - if err := scanner.Err(); err != nil { - return -1, err + for _, p := range possiblePlaces { + idx, err := findIn(p) + if err != nil { + if os.IsNotExist(err) || err == errTableNotFound { + continue + } + return -1, err + } + + return idx, nil } - return -1, fmt.Errorf("table not found: %s", table) + return -1, fmt.Errorf("%w: %s", errTableNotFound, table) } diff --git a/kvmrun/instance_properties.go b/kvmrun/instance_properties.go index c8e58cc..04cf4e9 100644 --- a/kvmrun/instance_properties.go +++ b/kvmrun/instance_properties.go @@ -15,7 +15,7 @@ func ValidateMachineName(name string) error { return nil } - return fmt.Errorf("invalid machine name: only [0-9A-Za-z_] are allowed, min length is 3 and max length is 16") + return fmt.Errorf("invalid machine name '%s': only [0-9A-Za-z_] are allowed, min length is 3 and max length is 16", name) } type InstanceProperties struct { diff --git a/kvmrun/version.go b/kvmrun/version.go index b77e32f..d92ed36 100644 --- a/kvmrun/version.go +++ b/kvmrun/version.go @@ -4,4 +4,4 @@ import ( "github.com/0xef53/kvmrun/internal/version" ) -var Version = version.Version{Major: 2, Minor: 0, Micro: 1} +var Version = version.Version{Major: 2, Minor: 0, Micro: 2} diff --git a/server/cloudinit/cloudinit.go b/server/cloudinit/cloudinit.go index 6e1e871..062d07e 100644 --- a/server/cloudinit/cloudinit.go +++ b/server/cloudinit/cloudinit.go @@ -160,7 +160,7 @@ func buildEthernetsConfig(vmdir string) (map[string]cloudinit.EthernetConfig, er ID string `json:"id"` Name string `json:"ifname"` Scheme string `json:"scheme"` - Addrs []string `json:"ips"` + Addrs []string `json:"addrs"` Gateway4 string `json:"gateway4"` Gateway6 string `json:"gateway6"` diff --git a/server/network/configuration.go b/server/network/configuration.go index 77620ce..e8b5c00 100644 --- a/server/network/configuration.go +++ b/server/network/configuration.go @@ -2,7 +2,6 @@ package network import ( "context" - "encoding/json" "errors" "fmt" "io/fs" @@ -34,16 +33,6 @@ func (s *Server) CreateConf(ctx context.Context, vmname, ifname string, opts Net } } - /* - TODO: - убрать - if b, err := json.MarshalIndent(opts, "", " "); err == nil { - fmt.Printf("DEBUG: CreateConf: vmname = %s, ifname = %s, opts = %s\n", vmname, ifname, string(b)) - } else { - fmt.Printf("DEBUG: CreateConf: error = %s\n", err.Error()) - } - */ - err := s.TaskRunFunc(ctx, server.BlockAnyOperations(vmname, ifname+"/hostnet"), true, nil, func(l *log.Entry) error { schemes, err := GetNetworkSchemes(vmname) if err != nil { @@ -58,12 +47,6 @@ func (s *Server) CreateConf(ctx context.Context, vmname, ifname string, opts Net schemes = append(schemes, opts.Properties()) - if b, err := json.MarshalIndent(schemes, "", " "); err == nil { - fmt.Printf("DEBUG: CreateConf: vmname = %s, ifname = %s, schemes = %s\n", vmname, ifname, string(b)) - } else { - fmt.Printf("DEBUG: CreateConf: schemes error = %s\n", err.Error()) - } - if err := WriteNetworkSchemes(vmname, schemes...); err != nil { return err } @@ -120,12 +103,6 @@ func (s *Server) UpdateConf(ctx context.Context, vmname, ifname string, apply bo case SchemeUpdate_GATEWAY4, SchemeUpdate_GATEWAY6: scheme.Set(p.String(), update.Value) } - - if b, err := json.MarshalIndent(schemes, "", " "); err == nil { - fmt.Printf("DEBUG: UpdateConf: vmname = %s, ifname = %s, schemes = %s\n", vmname, ifname, string(b)) - } else { - fmt.Printf("DEBUG: UpdateConf: schemes error = %s\n", err.Error()) - } } if err := WriteNetworkSchemes(vmname, schemes...); err != nil { diff --git a/server/network/hostnet_configure.go b/server/network/hostnet_configure.go index 0e7e3f6..33c7e5a 100644 --- a/server/network/hostnet_configure.go +++ b/server/network/hostnet_configure.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + cg "github.com/0xef53/kvmrun/internal/cgroups" "github.com/0xef53/kvmrun/internal/hostnet" "github.com/0xef53/kvmrun/kvmrun" "github.com/0xef53/kvmrun/server" @@ -105,6 +106,18 @@ func (s *Server) ConfigureHostNetwork(ctx context.Context, vmname, ifname string return nil } + if cgroups, err := cg.GetProcessGroups(int(routerAttrs.ProcessID)); err == nil { + if g, ok := cgroups["net_cls"]; ok { + fname := filepath.Join(kvmrun.CHROOTDIR, vmname, "run/cgroups.net_cls.path") + + if err := os.WriteFile(fname, []byte(g.Path()), 0644); err != nil { + log.WithField("ifname", ifname).Warnf("Non-fatal error: %s", err) + } + } + } else { + log.WithField("ifname", ifname).Warnf("Non-fatal error: %s", err) + } + return err } case Scheme_BRIDGE: diff --git a/server/network/hostnet_deconfigure.go b/server/network/hostnet_deconfigure.go index 44f10a1..2325619 100644 --- a/server/network/hostnet_deconfigure.go +++ b/server/network/hostnet_deconfigure.go @@ -3,6 +3,8 @@ package network import ( "context" "fmt" + "os" + "path/filepath" "strings" "github.com/0xef53/kvmrun/internal/hostnet" @@ -60,6 +62,20 @@ func (s *Server) DeconfigureHostNetwork(ctx context.Context, vmname, ifname stri return err } + // Remove net_cls controller for this virt.machine + if b, err := os.ReadFile(filepath.Join(kvmrun.CHROOTDIR, vmname, "run/cgroups.net_cls.path")); err == nil { + dirname := string(b) + + // Just a fast check + if strings.HasSuffix(dirname, fmt.Sprintf("kvmrun@%s.service", vmname)) { + if err := os.RemoveAll(dirname); err == nil { + log.WithField("ifname", ifname).Infof("Removed: %s", dirname) + } else { + log.WithField("ifname", ifname).Warnf("Non-fatal error: %s", err) + } + } + } + return hostnet.RouterDeconfigure(ifname, attrs.BindInterface) case Scheme_BRIDGE: attrs, err := scheme.ExtractAttrs_Bridge() diff --git a/server/network/scheme_properties.go b/server/network/scheme_properties.go index 37ad680..7c072e9 100644 --- a/server/network/scheme_properties.go +++ b/server/network/scheme_properties.go @@ -120,7 +120,7 @@ func (p *SchemeProperties) ValueAs(key string, target interface{}) error { return p.valueAs(key, target) } -func (p *SchemeProperties) valueAs(key string, target interface{}) error { +func (p *SchemeProperties) valueAs(key string, target interface{}) (err error) { if target == nil { return fmt.Errorf("target must be a non-nil pointer") } @@ -162,12 +162,17 @@ func (p *SchemeProperties) valueAs(key string, target interface{}) error { valueRV := reflect.ValueOf(value) - // Is value from attrs can be assigned to the target ? - if !valueRV.Type().AssignableTo(targetElem.Type()) { - return fmt.Errorf("type mismatch: value type = %s, target type = %s", valueRV.Type(), targetElem.Type()) + // Is value from attrs can be converted to the target ? + if !valueRV.Type().ConvertibleTo(targetElem.Type()) { + return fmt.Errorf("type mismatch: key = %s, value type = %s, target type = %s", key, valueRV.Type(), targetElem.Type()) } - targetElem.Set(valueRV) + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("type mismatch: key = %s, %v", key, r) + } + }() + targetElem.Set(valueRV.Convert(targetElem.Type())) return nil } @@ -353,6 +358,10 @@ func GetNetworkSchemes(vmname string, ifnames ...string) ([]*SchemeProperties, e return nil, err } } else { + if os.IsNotExist(err) { + // no one found, no problem + return nil, nil + } return nil, err }