diff --git a/go.mod b/go.mod index 8dca580..a9969c1 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.staur.ca/stobbsm/clustvirt go 1.22.1 -require libvirt.org/go/libvirt v1.10001.0 // indirect +require libvirt.org/go/libvirt v1.10001.0 diff --git a/lib/guest/domains.go b/lib/guest/domains.go index b624e22..b5e6db0 100644 --- a/lib/guest/domains.go +++ b/lib/guest/domains.go @@ -6,6 +6,7 @@ import ( "log" "time" + "git.staur.ca/stobbsm/clustvirt/util" "libvirt.org/go/libvirt" ) @@ -120,11 +121,11 @@ func GetGuest(name string, conn *libvirt.Connect) (*VM, error) { return g, err } g.BlockIOParameters.Weight = blkiop.Weight - g.BlockIOParameters.DeviceWeight = setNotSet(blkiop.DeviceWeight, blkiop.DeviceWeightSet) - g.BlockIOParameters.DeviceReadIops = setNotSet(blkiop.DeviceReadIops, blkiop.DeviceReadIopsSet) - g.BlockIOParameters.DeviceWriteIops = setNotSet(blkiop.DeviceWriteIops, blkiop.DeviceWriteIopsSet) - g.BlockIOParameters.DeviceReadBps = setNotSet(blkiop.DeviceReadBps, blkiop.DeviceReadBpsSet) - g.BlockIOParameters.DeviceWriteBps = setNotSet(blkiop.DeviceWriteBps, blkiop.DeviceWriteBpsSet) + g.BlockIOParameters.DeviceWeight = util.SetNotSet(blkiop.DeviceWeight, blkiop.DeviceWeightSet) + g.BlockIOParameters.DeviceReadIops = util.SetNotSet(blkiop.DeviceReadIops, blkiop.DeviceReadIopsSet) + g.BlockIOParameters.DeviceWriteIops = util.SetNotSet(blkiop.DeviceWriteIops, blkiop.DeviceWriteIopsSet) + g.BlockIOParameters.DeviceReadBps = util.SetNotSet(blkiop.DeviceReadBps, blkiop.DeviceReadBpsSet) + g.BlockIOParameters.DeviceWriteBps = util.SetNotSet(blkiop.DeviceWriteBps, blkiop.DeviceWriteBpsSet) // Set as much guest info as possible info, err := g.dom.GetGuestInfo( @@ -141,13 +142,13 @@ func GetGuest(name string, conn *libvirt.Connect) (*VM, error) { if len(info.Disks) > 0 { g.Disks = make([]DiskInfo, len(info.Disks)) for i, n := range info.Disks { - g.Disks[i].Name = setNotSet(n.Name, n.NameSet) - g.Disks[i].Alias = setNotSet(n.Alias, n.AliasSet) + g.Disks[i].Name = util.SetNotSet(n.Name, n.NameSet) + g.Disks[i].Alias = util.SetNotSet(n.Alias, n.AliasSet) g.Disks[i].Partition = n.Partition - g.Disks[i].GuestAlias = setNotSet(n.GuestAlias, n.GuestAliasSet) + g.Disks[i].GuestAlias = util.SetNotSet(n.GuestAlias, n.GuestAliasSet) g.Disks[i].DiskDependency = make([]string, len(n.Dependencies)) for j, k := range n.Dependencies { - g.Disks[i].DiskDependency[j] = setNotSet(k.Name, k.NameSet) + g.Disks[i].DiskDependency[j] = util.SetNotSet(k.Name, k.NameSet) } } } @@ -155,23 +156,23 @@ func GetGuest(name string, conn *libvirt.Connect) (*VM, error) { if len(info.Users) > 0 { g.Users = make([]UserInfo, len(info.Users)) for i, n := range info.Users { - g.Users[i].Name = setNotSet(n.Name, n.NameSet) - g.Users[i].Domain = setNotSet(n.Domain, n.DomainSet) + g.Users[i].Name = util.SetNotSet(n.Name, n.NameSet) + g.Users[i].Domain = util.SetNotSet(n.Domain, n.DomainSet) g.Users[i].LoginTime = time.Unix(int64(n.LoginTime), 0) } } - g.OS.ID = setNotSet(info.OS.ID, info.OS.IDSet) - g.OS.Name = setNotSet(info.OS.Name, info.OS.NameSet) - g.OS.Machine = setNotSet(info.OS.Machine, info.OS.MachineSet) - g.OS.Variant = setNotSet(info.OS.Variant, info.OS.VariantSet) - g.OS.Version = setNotSet(info.OS.Version, info.OS.VersionSet) - g.OS.VariantID = setNotSet(info.OS.VariantID, info.OS.VariantIDSet) - g.OS.VersionID = setNotSet(info.OS.VersionID, info.OS.VersionIDSet) - g.OS.PrettyName = setNotSet(info.OS.PrettyName, info.OS.PrettyNameSet) - g.OS.KernelRelease = setNotSet(info.OS.KernelRelease, info.OS.KernelReleaseSet) - g.OS.KernelVersion = setNotSet(info.OS.KernelVersion, info.OS.KernelVersionSet) - g.TimeZone.Name = setNotSet(info.TimeZone.Name, info.TimeZone.NameSet) + g.OS.ID = util.SetNotSet(info.OS.ID, info.OS.IDSet) + g.OS.Name = util.SetNotSet(info.OS.Name, info.OS.NameSet) + g.OS.Machine = util.SetNotSet(info.OS.Machine, info.OS.MachineSet) + g.OS.Variant = util.SetNotSet(info.OS.Variant, info.OS.VariantSet) + g.OS.Version = util.SetNotSet(info.OS.Version, info.OS.VersionSet) + g.OS.VariantID = util.SetNotSet(info.OS.VariantID, info.OS.VariantIDSet) + g.OS.VersionID = util.SetNotSet(info.OS.VersionID, info.OS.VersionIDSet) + g.OS.PrettyName = util.SetNotSet(info.OS.PrettyName, info.OS.PrettyNameSet) + g.OS.KernelRelease = util.SetNotSet(info.OS.KernelRelease, info.OS.KernelReleaseSet) + g.OS.KernelVersion = util.SetNotSet(info.OS.KernelVersion, info.OS.KernelVersionSet) + g.TimeZone.Name = util.SetNotSet(info.TimeZone.Name, info.TimeZone.NameSet) g.TimeZone.Offset = info.TimeZone.Offset // Set the hostname from the guest if it is set, otherwise use the VM name @@ -183,13 +184,13 @@ func GetGuest(name string, conn *libvirt.Connect) (*VM, error) { if len(info.Interfaces) > 0 { g.NetIFace = make([]NetIFaceInfo, len(info.Interfaces)) for i, n := range info.Interfaces { - g.NetIFace[i].HWAddr = setNotSet(n.Hwaddr, n.HwaddrSet) - g.NetIFace[i].Name = setNotSet(n.Name, n.NameSet) + g.NetIFace[i].HWAddr = util.SetNotSet(n.Hwaddr, n.HwaddrSet) + g.NetIFace[i].Name = util.SetNotSet(n.Name, n.NameSet) if len(n.Addrs) > 0 { g.NetIFace[i].IP = make([]IPInfo, len(n.Addrs)) for j, a := range n.Addrs { - g.NetIFace[i].IP[j].Addr = setNotSet(a.Addr, a.AddrSet) - g.NetIFace[i].IP[j].Type = setNotSet(a.Type, a.TypeSet) + g.NetIFace[i].IP[j].Addr = util.SetNotSet(a.Addr, a.AddrSet) + g.NetIFace[i].IP[j].Type = util.SetNotSet(a.Type, a.TypeSet) g.NetIFace[i].IP[j].Prefix = a.Prefix } } @@ -210,14 +211,6 @@ func GetGuest(name string, conn *libvirt.Connect) (*VM, error) { return g, nil } -// helper function to set a string value to "NotSet" if it wasn't actually set -func setNotSet(v string, s bool) string { - if s { - return v - } - return "Notset" -} - // Close closes an open connection func (g *VM) Close() error { log.Println("Closing VM", g.Name) diff --git a/lib/host/host.go b/lib/host/host.go index 101234e..0960474 100644 --- a/lib/host/host.go +++ b/lib/host/host.go @@ -1,10 +1,16 @@ +// Package host provides utilities and data structures in relation to a libvirt host. +// This includes getting a list of virtual machines running on a host, launching +// a new virtual machine on a host, triggering a virtual machine migration to another +// host, getting hardware and resource usage from a host, and eventually more. +// Most of this is data at the moment, ensuring data can be gathered efficiently without +// slowing down the host from it's main job of running virtual machines. package host import ( - "fmt" "log" "git.staur.ca/stobbsm/clustvirt/lib/guest" + "git.staur.ca/stobbsm/clustvirt/util" "libvirt.org/go/libvirt" ) @@ -12,17 +18,51 @@ import ( // If a connection is closed prematurely, will re-open the connection and // try the attempted method again type Host struct { - HostName string + HostName string + SystemHomeName string + FreeMemory uint64 + LibVersion uint32 + HostInfo NodeInfo + HostSEVInfo SEVInfo + AvailableCPUTypes []string + uri *URI conn *libvirt.Connect close chan struct{} closeErr chan error } +// NodeInfo represents the basic HW info for a host node +type NodeInfo struct { + // livirt.NodeInfo section + Model string + Memory uint64 + Cpus uint + MHz uint + Nodes uint32 + Sockets uint32 + Cores uint32 + Threads uint32 +} + +// SEVInfo provides information about AMD SEV support +type SEVInfo struct{ + // livirt.NodeSEVParameters section + SEVEnabled bool + PDH string + CertChain string + CBitPos uint + ReducedPhysBits uint + MaxGuests uint + MaxEsGuests uint + CPU0ID string +} + // ConnectHost creates a host connection wrapper that can be used regularly -func ConnectHost(host string) (*Host, error) { +func ConnectHost(uri *URI, host string) (*Host, error) { h := &Host{ HostName: host, + uri: uri, } if err := h.connect(); err != nil { @@ -32,6 +72,52 @@ func ConnectHost(host string) (*Host, error) { h.close = make(chan struct{}) h.closeErr = make(chan error) + var err error + h.AvailableCPUTypes, err = h.conn.GetCPUModelNames("x86_64", 0) + if err != nil { + log.Println("Error getting cpu model names", err) + } + + // Extract libvirt.NodeInfo and libvirt.NodeSEVParameters + ni, err := h.conn.GetNodeInfo() + if err != nil { + return nil, err + } + h.HostInfo.Model = ni.Model + h.HostInfo.Memory = ni.Memory + h.HostInfo.Cpus = ni.Cpus + h.HostInfo.MHz = ni.MHz + h.HostInfo.Nodes = ni.Nodes + h.HostInfo.Sockets = ni.Sockets + h.HostInfo.Cores = ni.Cores + h.HostInfo.Threads = ni.Threads + + // Assume SEV is enabled, until we know otherwise + h.HostSEVInfo.SEVEnabled = true + ns, err := h.conn.GetSEVInfo(0) + if err != nil { + lverr, ok := err.(libvirt.Error) + if !ok { + return nil, err + } + switch lverr.Code { + case 84: + log.Println("SEV functions not supported") + h.HostSEVInfo.SEVEnabled = false + default: + log.Println("Error encountered", lverr.Error()) + } + } + if h.HostSEVInfo.SEVEnabled { + h.HostSEVInfo.PDH = util.SetNotSet(ns.PDH, ns.PDHSet) + h.HostSEVInfo.CertChain = util.SetNotSet(ns.CertChain, ns.CertChainSet) + h.HostSEVInfo.CBitPos = ns.CBitPos + h.HostSEVInfo.ReducedPhysBits = ns.ReducedPhysBits + h.HostSEVInfo.MaxGuests = ns.MaxGuests + h.HostSEVInfo.MaxEsGuests = ns.MaxEsGuests + h.HostSEVInfo.CPU0ID = util.SetNotSet(ns.CPU0ID, ns.CPU0IDSet) + } + go func() { defer close(h.closeErr) <-h.close @@ -44,9 +130,7 @@ func ConnectHost(host string) (*Host, error) { // connect creates a host connection func (h *Host) connect() error { var err error - h.conn, err = libvirt.NewConnect( - fmt.Sprintf("qemu+ssh://%s/system", h.HostName), - ) + h.conn, err = libvirt.NewConnect(h.uri.ConnectionString(h.HostName)) return err } diff --git a/lib/host/uri.go b/lib/host/uri.go new file mode 100644 index 0000000..1abdc65 --- /dev/null +++ b/lib/host/uri.go @@ -0,0 +1,82 @@ +package host + +import ( + "log" + "strings" +) + +// URI is a string type, accessed via the pre-defined variables, and represent +// the URI pattern used to connect to a host. +// Example: +// Driver[+Transport]://[:PORT]/[?Options&in=uri&format] +type URI struct { + Driver string + Transport string + Path string + Options []string +} + +// CustomURI create and return a custom URI method, following RFC2396, +// keeping in mind that the hostname will be inserted between the transport and path +func CustomURI(driver, transport, path string, options ...string) *URI { + return &URI{ + Driver: driver, + Transport: transport, + Path: path, + Options: options, + } +} + +// URIs available to build connections to a libvirt host +var ( + // URI for connecting to a remote QEMU system over SSH + URI_QEMU_SSH_SYSTEM = &URI{Driver: "qemu", Transport: "ssh", Path: "system"} + // URI for connecting to a remote QEMU system over TLS + // Builds the URI qemu:///system + URI_QEMU_TLS_SYSTEM = &URI{Driver: "qemu", Transport: "", Path: "system"} + // URI for connecting to a remote QEMU session over SSH + URI_QEMU_SSH_SESSION = &URI{Driver: "qemu", Transport: "ssh", Path: "session"} + // URI for connecting to a local QEMU system over a UNIX socket + URI_QEMU_UNIX_SYSTEM = &URI{Driver: "qemu", Transport: "unix", Path: "system"} + // URI for connecting to a remote QEMU system over unsecured TCP + URI_QEMU_TCP_SYSTEM = &URI{Driver: "qemu", Transport: "tcp", Path: "system"} + + // URI for connecting to a remote XEN system with SSH + URI_XEN_SSH_SYSTEM = &URI{Driver: "xen", Transport: "ssh", Path: "system"} + // URI for connecting to a remote XEN system over TLS + URI_XEN_TLS_SYSTEM = &URI{Driver: "xen", Transport: "", Path: "system"} +) + +// ConnectionString takes a host name to interpolate into a URI and returns the string +func (u *URI) ConnectionString(h string) string { + var sb strings.Builder + optlen := len(u.Options) + sb.WriteString(u.Driver) + if u.Transport != "" { + sb.WriteRune('+') + sb.WriteString(u.Transport) + } + sb.WriteString("://") + if h != "" { + sb.WriteString(h) + } + sb.WriteRune('/') + sb.WriteString(u.Path) + + if optlen > 0 { + sb.WriteRune('?') + for i, o := range u.Options { + sb.WriteString(o) + if optlen != i+1 { + sb.WriteRune('&') + } + } + } + log.Printf("Connection URI: %s", sb.String()) + return sb.String() +} + +// AddOpt adds more options to the option list +func (u *URI) AddOpt(opt string) { + u.Options = append(u.Options, opt) +} diff --git a/main.go b/main.go index 55086d4..f2a2772 100644 --- a/main.go +++ b/main.go @@ -9,11 +9,12 @@ import ( func main() { log.Println("Starting clustvirt, the libvirt cluster manager") - venus, err := host.ConnectHost("venus.staur.ca") + venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca") if err != nil { log.Fatal(err) } defer venus.Close() + log.Println(venus) lm, err := venus.GetGuestByName("logan-minecraft") if err != nil { log.Fatal(err) diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..11fdec7 --- /dev/null +++ b/util/util.go @@ -0,0 +1,9 @@ +package util + +// helper function to set a string value to "NotSet" if it wasn't actually set +func SetNotSet(v string, s bool) string { + if s { + return v + } + return "Notset" +}