diff --git a/README.md b/README.md index 768167e..2a24ff5 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,28 @@ Overall goals: ``` + +### Open Source Projects + +The following projects are being used as part of ClustVirt. +Only those being used directly are listed, not the dependencies. + +#### Go + +| Library | Use | License | +| --- | --- | --- | +| [xdg](https://github.com/adrg/xdg) | XDG paths | MIT | +| [spf13/viper](https://github.com/spf13/viper) | Configuration | MIT | +| [spf13](https://github.com/spf13/cobra) | Command Pattern CLI | Apache 2.0 | +| [go-chart/v2](https://github.com/wcharczuk/go-chart) | Chart generation | MIT | +| [libvirt](https://libvirt.org/go/libvirt) | Communications with Libvirtd | MIT | +| [libvirtxml](https://libvirt.org/go/libvirtxml) | Libvirt XML parsing and generation | MIT | +| [templ](https://github.com/a-h/templ) | HTML Templating Engine | MIT | +| [chi/v5](https://github.com/go-chi/chi) | HTTP Routing | MIT | + +#### Node NPM + +| Library | Use | License | +| --- | --- | ------- | +| [tailwindcss](https://tailwindcss.com) | CSS UI library | MIT | +| [tailwindcss/forms](https://tailwindcss.com) | CSS UI library for forms | MIT | diff --git a/cluster/api.go b/cluster/api.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/api.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/auth.go b/cluster/auth.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/auth.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/builder.go b/cluster/builder.go new file mode 100644 index 0000000..d319d48 --- /dev/null +++ b/cluster/builder.go @@ -0,0 +1,28 @@ +package cluster + +import "time" + +// ClusterBuilder is used to build a Cluster object, which can then be used +type ClusterBuilder struct { + cluster *Cluster +} + +// New starts the builder pattern for the Cluster. +// Sets the default interval time to 30 seconds. +func New() *ClusterBuilder { + return &ClusterBuilder{ + cluster: &Cluster{ + interval: time.Duration(time.Second * 30), + }, + } +} + +// SetInterval sets the check interval of the Cluster being built. +func (c *ClusterBuilder) SetInterval(i time.Duration) *ClusterBuilder { + c.cluster.interval = i + return c +} +// Build retuns the built cluster +func (c *ClusterBuilder) Build() *Cluster { + return c.cluster +} diff --git a/cluster/cluster.go b/cluster/cluster.go new file mode 100644 index 0000000..5119438 --- /dev/null +++ b/cluster/cluster.go @@ -0,0 +1,27 @@ +// Package cluster is the unique portion of this application that implements +// basic cluster controls overtop of Libvirtd hosts. The controller is agnostic +// about where it is running, and doesn't need to be running on a host that +// has Libvirtd installed on it. +// +// The cluster can be configured through the use of TOML configuration file, +// or with CLI flags. This is done via the itegration of the greate go libraries +// spf13/viper and spf13/cobra. +package cluster + +import ( + "time" + + "git.staur.ca/stobbsm/clustvirt/lib/host" + "git.staur.ca/stobbsm/clustvirt/lib/network" +) + +// Cluster is the data structure and controller for cluster access. +// Using it's methods, you can access hosts, virtual machines, health +// data, and more. +// Cluster implements a time.Ticker that will be used to check the connection +// status of hosts, and reconnect if a connection was blocked or interrupted. +type Cluster struct { + interval time.Duration + hosts map[string]*host.Host + networks map[string]*network.Network +} diff --git a/cluster/config.go b/cluster/config.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/config.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/daemon.go b/cluster/daemon.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/daemon.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/format/format.go b/cluster/format/format.go new file mode 100644 index 0000000..7324612 --- /dev/null +++ b/cluster/format/format.go @@ -0,0 +1 @@ +package format diff --git a/cluster/format/json.go b/cluster/format/json.go new file mode 100644 index 0000000..7324612 --- /dev/null +++ b/cluster/format/json.go @@ -0,0 +1 @@ +package format diff --git a/cluster/format/xml.go b/cluster/format/xml.go new file mode 100644 index 0000000..7324612 --- /dev/null +++ b/cluster/format/xml.go @@ -0,0 +1 @@ +package format diff --git a/cluster/host.go b/cluster/host.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/host.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/network.go b/cluster/network.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/network.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/stats.go b/cluster/stats.go new file mode 100644 index 0000000..20ce13f --- /dev/null +++ b/cluster/stats.go @@ -0,0 +1,259 @@ +package cluster + +// ClusterStats is used to gather stats for the entire cluster +type ClusterStats struct { + CPU struct { + Sockets uint32 + Cores uint32 + Threads uint32 + Allocated uint32 + } + Memory struct { + Total uint64 + Free uint64 + Buffers uint64 + Cached uint64 + Allocated uint64 + } + Storage struct { + Total uint64 + Used uint64 + Free uint64 + Active uint32 + Inactive uint32 + Pools uint32 + + Volumes struct { + Total uint32 + Active uint32 + Inactive uint32 + } + } + VM struct { + Count uint32 + Started uint32 + Stopped uint32 + } + Host struct { + Count uint32 + Available uint32 + } + Network struct { + Count uint32 + Active uint32 + Inactive uint32 + } + + old *ClusterStats + c *Cluster +} + +// ClusterStats is used to gather stats for the entire cluster +type StatDiff struct { + CPU struct { + Sockets int + Cores int + Threads int + Allocated int + } + Memory struct { + Total int + Free int + Buffers int + Cached int + Allocated int + } + Storage struct { + Total int + Used int + Free int + Active int + Inactive int + Pools int + + Volumes struct { + Total int + Active int + Inactive int + } + } + VM struct { + Count int + Started int + Stopped int + } + Host struct { + Count int + Available int + } + Network struct { + Count int + Active int + Inactive int + } +} + + +// Init is given a cluster, which it then uses to load the initial statistics +// Does not close connections, but uses the host connections available to the +// cluster to add statistics together. +func Init(c *Cluster) *ClusterStats { + cs := &ClusterStats{} + + return cs +} + +// Update triggers the stats collector to refresh it's statistics +func (cs *ClusterStats) Update() { + cs.old = cs.copy() + + cs.reset() + // Start looping through each host in the cluster, adding to the total + for _, h := range cs.c.hosts { + cs.Host.Count++ + cs.Host.Available++ + + cs.CPU.Sockets += h.HostInfo.Sockets + cs.CPU.Cores += h.HostInfo.Cores + cs.CPU.Threads += h.HostInfo.Threads + + cs.Memory.Total += h.NodeMemory.Total + cs.Memory.Free += h.NodeMemory.Free + cs.Memory.Buffers += h.NodeMemory.Buffers + cs.Memory.Cached += h.NodeMemory.Cached + + + + // Storage Pool counting + cs.Storage.Pools += uint32(len(h.StoragePoolList)) + countedSharedPools := map[string]struct{}{} + // Loop through available storage pools + for _, sp := range h.StoragePoolList { + if _, ok := countedSharedPools[sp.Name]; ok { + // Already counted this shared pool, move on + continue + } + if sp.HAEnabled == true { + countedSharedPools[sp.Name] = struct{}{} + } + if sp.Active == false { + cs.Storage.Inactive++ + continue + } + cs.Storage.Active++ + cs.Storage.Total += sp.Capacity + cs.Storage.Used += sp.Allocation + cs.Storage.Free += sp.Capacity - sp.Allocation + // Volumes in the pool + cs.Storage.Volumes.Total += uint32(len(sp.Volumes)) + for range sp.Volumes { + cs.Storage.Volumes.Active++ + } + } + + // VM Count + cs.VM.Count += uint32(len(h.VMList)) + for _, vm := range h.VMList { + cs.CPU.Allocated += uint32(vm.VCPUs) + cs.Memory.Allocated += uint64(vm.Memory) + if vm.Active { + cs.VM.Started++ + continue + } + cs.VM.Stopped++ + } + + // Network count + cs.Network.Count += uint32(len(h.NetworkList)) + for _, ni := range h.NetworkList { + if ni.Active { + cs.Network.Active++ + continue + } + cs.Network.Inactive++ + } + } +} + +// Diff returns a map of all the field and how they changed +func (cs *ClusterStats) Diff() StatDiff { + return StatDiff{ + CPU: struct{Sockets int; Cores int; Threads int; Allocated int}{ + Sockets: int(cs.old.CPU.Sockets - cs.CPU.Sockets), + Cores: int(cs.old.CPU.Cores - cs.CPU.Cores), + Threads: int(cs.old.CPU.Threads - cs.CPU.Threads), + Allocated: int(cs.old.CPU.Allocated - cs.CPU.Allocated), + }, + Memory: struct{Total int; Free int; Buffers int; Cached int; Allocated int}{ + Total: int(cs.old.Memory.Total - cs.Memory.Total), + Free: int(cs.old.Memory.Free - cs.Memory.Free), + Buffers: int(cs.old.Memory.Buffers - cs.Memory.Buffers), + Cached: int(cs.old.Memory.Cached - cs.Memory.Cached), + Allocated: int(cs.old.Memory.Allocated - cs.Memory.Allocated), + }, + Storage: struct{Total int; Used int; Free int; Active int; Inactive int; Pools int; Volumes struct{Total int; Active int; Inactive int}}{ + Total: int(cs.old.Storage.Total - cs.Storage.Total), + Used: int(cs.old.Storage.Used - cs.Storage.Used), + Free: int(cs.old.Storage.Free - cs.Storage.Free), + Active: int(cs.old.Storage.Active - cs.Storage.Active), + Inactive: int(cs.old.Storage.Inactive - cs.Storage.Inactive), + Pools: int(cs.old.Storage.Pools - cs.Storage.Pools), + Volumes: struct{Total int; Active int; Inactive int}{ + Total: int(cs.old.Storage.Volumes.Total - cs.Storage.Volumes.Total), + Active: int(cs.old.Storage.Volumes.Active - cs.Storage.Volumes.Active), + Inactive: int(cs.old.Storage.Volumes.Inactive - cs.Storage.Volumes.Inactive), + }, + }, + VM: struct{Count int; Started int; Stopped int}{ + Count: int(cs.old.VM.Count - cs.VM.Count), + Started: int(cs.old.VM.Started - cs.VM.Started), + Stopped: int(cs.old.VM.Stopped - cs.VM.Stopped), + }, + Host: struct{Count int; Available int}{ + Count: int(cs.old.Host.Count - cs.Host.Count), + Available: int(cs.old.Host.Available - cs.Host.Available), + }, + Network: struct{Count int; Active int; Inactive int}{ + Count: int(cs.old.Network.Count - cs.Network.Count), + Active: int(cs.old.Network.Active - cs.Network.Active), + Inactive: int(cs.old.Network.Inactive - cs.Network.Inactive), + }, + } +} + +// copy the clusterstats into a new clusterstatus object for comparison purposes +func (cs *ClusterStats) copy() *ClusterStats { + ncs := *cs + return &ncs +} + +// reset all values to zero value +func (cs *ClusterStats) reset() { + cs.CPU.Sockets=0 + cs.CPU.Cores=0 + cs.CPU.Threads=0 + cs.CPU.Allocated=0 + + cs.Memory.Total=0 + cs.Memory.Free=0 + cs.Memory.Buffers=0 + cs.Memory.Cached=0 + + cs.Storage.Total=0 + cs.Storage.Used=0 + cs.Storage.Free=0 + cs.Storage.Active=0 + cs.Storage.Inactive=0 + cs.Storage.Pools=0 + + cs.VM.Count=0 + cs.VM.Started=0 + cs.VM.Stopped=0 + + cs.Host.Count=0 + cs.Host.Available=0 + + cs.Network.Count=0 + cs.Network.Active=0 + cs.Network.Inactive=0 +} diff --git a/cluster/storagepool.go b/cluster/storagepool.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/storagepool.go @@ -0,0 +1 @@ +package cluster diff --git a/cluster/vm.go b/cluster/vm.go new file mode 100644 index 0000000..916b1b5 --- /dev/null +++ b/cluster/vm.go @@ -0,0 +1 @@ +package cluster diff --git a/cmd/clustvirt.go b/cmd/clustvirt.go new file mode 100644 index 0000000..8ba1012 --- /dev/null +++ b/cmd/clustvirt.go @@ -0,0 +1,29 @@ +// Package cmd contains the main application file for ClustVirt. +// This command is used for everything, implementing the command pattern +// with the help of spf13/cobra. +// After building, the executable's first argument must be one of the valid +// commands, such as `daemon`, `api`, `query`, `host`, etc. This pattern allows +// for a simple way to access functionality, reducing the need to use the +// REST api directly. +package cmd + +// Global Arguments: +// All commands can use the following arguments: +// --output none|json|xml: Set the output type, none being just plain log, +// json outputing as a JSON document, and XML outputing as an XML document + +// Commands planned: +// config: actions associated with the configuration of clustvirt +// set: set a configuration value +// get: get the current value of a configuration options +// list: list available configuration options +// daemon: actions associated with running clustvirt in daemon mode +// start: start the daemon +// stop: safely stop the daemon +// status: query the status of the daemon +// api: query the REST api. There must a clustvirt daemon running for this to function +// host: actions associated with a specific host in the cluster +// vm: actions asociated with a specifc vm in the cluster +// storagepool: actions associated with storage pools +// auth: actions associated with authentication +// network: actions to define networks diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4495bd9 --- /dev/null +++ b/config/config.go @@ -0,0 +1,9 @@ +// Package config used spf13/viper to handle configuration. Sane defaults are set +// as default values, and a single host is set to `qemu:///system`, which is only +// used if it is found. Not having a local instance will not panic or crash anything. +// +// Viper uses configuration set via defaults -> config file -> env variables -> +// cli flags -> calls to Set (while running), so clustvirt uses the same method. +package config + + diff --git a/go.mod b/go.mod index bce00a0..787bf20 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,32 @@ require ( ) require ( + github.com/adrg/xdg v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/wcharczuk/go-chart/v2 v2.1.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/image v0.11.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + libvirt.org/go/libvirtxml v1.10001.0 // indirect ) diff --git a/go.sum b/go.sum index 76ffb87..a3a48f4 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,68 @@ github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo= github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -27,9 +77,12 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -38,10 +91,20 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= libvirt.org/go/libvirt v1.10001.0 h1:lEVDNE7xfzmZXiDEGIS8NvJSuaz11OjRXw+ufbQEtPY= libvirt.org/go/libvirt v1.10001.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= +libvirt.org/go/libvirtxml v1.10001.0 h1:r9WBs24r3mxIG3/hAMRRwDMy4ZaPHmhHjw72o/ceXic= +libvirt.org/go/libvirtxml v1.10001.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng= diff --git a/lib/host/lib.go b/lib/host/lib.go index c9143b2..4ef75f0 100644 --- a/lib/host/lib.go +++ b/lib/host/lib.go @@ -16,6 +16,7 @@ import ( "git.staur.ca/stobbsm/clustvirt/lib/storagevol" "git.staur.ca/stobbsm/clustvirt/util" "libvirt.org/go/libvirt" + "libvirt.org/go/libvirtxml" ) // Host holds information and acts as a connection handle for a Host @@ -80,6 +81,9 @@ type VMInfo struct { ID uint UUID []byte XML string + Active bool + VCPUs uint + Memory uint // States are the current states active on the host States []guest.VMState } @@ -105,6 +109,7 @@ type StoragePoolInfo struct { Capacity uint64 Allocation uint64 Available uint64 + IsNet bool // HAEnabled indicates if the storage pool has High Availability HAEnabled bool // Volumes defined in the storage pool @@ -134,6 +139,7 @@ type NetworkInfo struct { Name string UUID []byte XML string + Active bool // NetIf is the network interface this connection is applied to NetIf NetIfInfo } @@ -232,13 +238,13 @@ func (h *Host) getInfo() { infoFuncs := []func(){ h.getDevicesInfo, -// h.getDomainInfo, + // h.getDomainInfo, h.getIfaceInfo, h.getNetsInfo, h.getNodeInfo, -// h.getSEVInfo, -// h.getSecretsInfo, -// h.getStoragePools, + // h.getSEVInfo, + // h.getSecretsInfo, + // h.getStoragePools, } for _, f := range infoFuncs { @@ -287,6 +293,19 @@ func (h *Host) getStoragePools() { h.StoragePoolList[i].Allocation = spInfo.Allocation h.StoragePoolList[i].Available = spInfo.Available + spoolXML := &libvirtxml.StoragePool{} + if err = spoolXML.Unmarshal(h.StoragePoolList[i].XML); err != nil { + log.Println(err) + } + h.StoragePoolList[i].Type = spoolXML.Type + for _, t := range storagepool.NetTypes { + if h.StoragePoolList[i].Type == t { + h.StoragePoolList[i].IsNet = true + h.StoragePoolList[i].HAEnabled = true + continue + } + } + svols, err := s.ListAllStorageVolumes(0) if err != nil { log.Println(err) @@ -463,6 +482,15 @@ func (h *Host) getDomainInfo() { if h.VMList[i].XML, err = d.GetXMLDesc(0); err != nil { log.Println(err) } + vmXML := &libvirtxml.Domain{} + if err = vmXML.Unmarshal(h.VMList[i].XML); err != nil { + log.Println(err) + } + h.VMList[i].VCPUs = vmXML.VCPU.Value + h.VMList[i].Memory = vmXML.CurrentMemory.Value + if h.VMList[i].Active, err = d.IsActive(); err != nil { + log.Println(err) + } d.Free() } @@ -517,6 +545,9 @@ func (h *Host) getNetsInfo() { if h.NetworkList[i].XML, err = net.GetXMLDesc(0); err != nil { log.Println(err) } + if h.NetworkList[i].Active, err = net.IsActive(); err != nil { + log.Println(err) + } net.Free() } diff --git a/lib/network/lib.go b/lib/network/lib.go index 1ae2e9d..eab7d2f 100644 --- a/lib/network/lib.go +++ b/lib/network/lib.go @@ -1 +1,6 @@ +// Package network defines varialbes and methods to control libvirtd +// networks. Networks Can be defined cluster wide, and mapped to +// host interfaces this way. package network + +type Network struct{} diff --git a/lib/storagepool/lib.go b/lib/storagepool/lib.go index d9e6996..0ab9992 100644 --- a/lib/storagepool/lib.go +++ b/lib/storagepool/lib.go @@ -9,3 +9,38 @@ var StoragePoolStateMap = map[libvirt.StoragePoolState]string{ libvirt.STORAGE_POOL_DEGRADED: "degraded", libvirt.STORAGE_POOL_INACCESSIBLE: "inaccessible", } + +var Types = []string{ + "dir", + "fs", + "netfs", + "disk", + "iscsi", + "logical", + "scsi", + "mpath", + "rbd", + "sheepdog", + "gluster", + "zfs", + "iscsi-direct", +} + +var LocalTypes = []string{ + "dir", + "fs", + "disk", + "logical", + "scsi", + "zfs", +} + +var NetTypes = []string{ + "netfs", + "iscsi", + "mpath", + "rbd", + "sheepdog", + "gluster", + "iscsi-direct", +} diff --git a/main.go b/main.go index de742bc..407866c 100644 --- a/main.go +++ b/main.go @@ -23,8 +23,8 @@ func main() { // Start webserver and serve homepage defaultNavBar := []components.NavItem{ - {Name: "Home", Href: "/"}, - {Name: "Hosts", Href: "/hostinfo"}, + {Name: "Cluster", Href: "/" }, + {Name: "Configuration", Href: "/config" }, {Name: "About", Href: "/about"}, } fs := http.StripPrefix("/static/", http.FileServer(http.Dir("public"))) @@ -51,6 +51,15 @@ func main() { defer rhost.Close() log.Println("Rendering HostInfo", view.HostInfo(rhost).Render(context.Background(), w)) }) + r.Get("/host/{hostname}/stats", func(w http.ResponseWriter, r *http.Request) { + rhost, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, chi.URLParam(r, "hostname")) + if err != nil { + http.Error(w, fmt.Sprintf("error while getting host: %s", err), http.StatusInternalServerError) + return + } + defer rhost.Close() + log.Println("Rendering stats", view.SysInfo(rhost).Render(context.Background(), w)) + }) }) log.Println(http.ListenAndServe(":3000", r)) diff --git a/public/css/style.css b/public/css/style.css index a1a91a3..d2b721d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -822,10 +822,6 @@ span>a:visited { position: static; } -.absolute { - position: absolute; -} - .bottom-0 { bottom: 0px; } @@ -852,10 +848,31 @@ span>a:visited { margin-right: auto; } +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + .block { display: block; } +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + .flex { display: flex; } @@ -869,16 +886,9 @@ span>a:visited { height: 16rem; } -.size-full { - width: 100%; - height: 100%; -} - -.size-max { - width: -moz-max-content; - width: max-content; - height: -moz-max-content; - height: max-content; +.size-40 { + width: 10rem; + height: 10rem; } .h-28 { @@ -893,37 +903,20 @@ span>a:visited { height: 100%; } -.h-lvh { - height: 100lvh; -} - -.h-svh { - height: 100svh; -} - -.h-dvh { - height: 100dvh; -} - -.h-max { - height: -moz-max-content; - height: max-content; -} - -.h-48 { - height: 12rem; -} - .h-screen { height: 100vh; } -.min-h-80 { - min-height: 20rem; +.h-2 { + height: 0.5rem; } -.w-1\/4 { - width: 25%; +.h-4 { + height: 1rem; +} + +.w-1\/6 { + width: 16.666667%; } .w-3\/4 { @@ -934,8 +927,13 @@ span>a:visited { width: 100%; } -.w-1\/6 { - width: 16.666667%; +.w-16 { + width: 4rem; +} + +.w-fit { + width: -moz-fit-content; + width: fit-content; } .flex-auto { @@ -990,6 +988,10 @@ span>a:visited { align-items: center; } +.justify-start { + justify-content: flex-start; +} + .justify-end { justify-content: flex-end; } @@ -9032,14 +9034,6 @@ span>a:visited { border-radius: 9999px; } -.rounded-none { - border-radius: 0px; -} - -.border-2 { - border-width: 2px; -} - .border { border-width: 1px; } @@ -9048,19 +9042,10 @@ span>a:visited { border-style: solid; } -.border-dashed { - border-style: dashed; -} - .border-dotted { border-style: dotted; } -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - .border-uiblue-100 { --tw-border-opacity: 1; border-color: rgb(221 231 248 / var(--tw-border-opacity)); @@ -138666,14 +138651,14 @@ span>a:visited { stroke: rgb(51 51 30 / 0.95); } -.p-2 { - padding: 0.5rem; -} - .p-1 { padding: 0.25rem; } +.p-2 { + padding: 0.5rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -138684,6 +138669,11 @@ span>a:visited { padding-right: 1rem; } +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -220262,6 +220252,16 @@ span>a:visited { --tw-ring-offset-color: rgb(51 51 30 / 0.95); } +.hover\:border-uipurple-400:hover { + --tw-border-opacity: 1; + border-color: rgb(227 116 229 / var(--tw-border-opacity)); +} + +.hover\:bg-uipurple-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(176 65 178 / var(--tw-bg-opacity)); +} + .hover\:text-uiblue-200:hover { --tw-text-opacity: 1; color: rgb(187 207 241 / var(--tw-text-opacity)); @@ -220290,6 +220290,11 @@ span>a:visited { order: 3; } + .md\:size-80 { + width: 20rem; + height: 20rem; + } + .md\:h-16 { height: 4rem; } diff --git a/public/images/bars.svg b/public/images/bars.svg new file mode 100644 index 0000000..faff448 --- /dev/null +++ b/public/images/bars.svg @@ -0,0 +1,52 @@ + diff --git a/public/images/grid.svg b/public/images/grid.svg new file mode 100644 index 0000000..140cb89 --- /dev/null +++ b/public/images/grid.svg @@ -0,0 +1,56 @@ + diff --git a/view/hostinfo.templ b/view/hostinfo.templ index d1670e6..9d9e052 100644 --- a/view/hostinfo.templ +++ b/view/hostinfo.templ @@ -1,36 +1,102 @@ package view import ( + "fmt" "git.staur.ca/stobbsm/clustvirt/lib/host" - bf "github.com/labstack/gommon/bytes" "git.staur.ca/stobbsm/clustvirt/view/components" "git.staur.ca/stobbsm/clustvirt/view/layouts" ) +script memchart(used uint64, free uint64, buf uint64, cache uint64) { + ctx = document.getElementById('memchart'); + + new Chart(ctx, { + type: 'pie', + data: { + labels: [ + 'Used', + 'Free', + 'Cached', + 'Buffered' + ], + datasets: [{ + label: 'Memory Usage', + data: [ + used, + free, + cache, + buf, + ], + backgroundColor: [ + 'rgb(255,0,0)', + 'rgb(0,255,0)', + 'rgb(128,128,0)', + 'rgb(0,0,255)' + ], + hoverOffset: 4 + }], + } + }); +} + // MemChart get's the memory pi chart from the host templ MemChart(h *host.Host) {