Compare commits

..

No commits in common. "master" and "v0.2.46" have entirely different histories.

20 changed files with 573 additions and 1468 deletions

View File

@ -8,10 +8,6 @@ REDOMOD = $(shell if [ -e go.sum ]; then echo go.sum exists; else GO111MODULE=
all: install
@echo build worked
virtigo list droplets
virtigo list droplets --on
virtigo droplet show --name check.lab.wit.org
virtigo droplet start --name check.lab.wit.org
build: goimports vet
GO111MODULE=off go build \
@ -92,7 +88,7 @@ redomod:
clean:
rm -f go.*
rm -f virtigo*
go-mod-clean purge
go-mod-clean --purge
# git clone the sources and all the golang dependancies into ~/go/src
# if you don't have go-clone, you can get it from http://go.wit.com/
@ -117,10 +113,3 @@ http-dumplibvirtxml:
protogen:
go-clone google.golang.org/protobuf
cd ~/go/src/google.golang.org/protobuf/cmd/protoc-gen-go && go install
gocui: install
virtigo --gui gocui --gui-verbose --gui-file ../../toolkits/gocui/gocui.so --admin
# virtigo --gui gocui --gui-verbose --gui-file ../../toolkits/gocui/gocui.so --admin >/tmp/forge.log 2>&1
log:
journalctl -f -xeu virtigod.service

93
argv.go
View File

@ -1,11 +1,6 @@
package main
import (
"fmt"
"os"
"go.wit.com/log"
)
import "go.wit.com/log"
/*
this parses the command line arguements
@ -16,58 +11,52 @@ import (
var argv args
type args struct {
List *ListCmd `arg:"subcommand:list" help:"list things"`
Droplet *DropletCmd `arg:"subcommand:droplet" help:"send events to a droplet"`
Config string `arg:"env:VIRTIGO_HOME" help:"defaults to ~/.config/virtigo/"`
Server string `arg:"env:VIRTIGO_SERVER" help:"what virtigo cluster to connect to"`
Localhost bool `arg:"--localhost" help:"use the local libvirt"`
Daemon bool `arg:"--daemon" help:"run as a daemon"`
Verbose bool `arg:"--verbose" help:"talk more"`
Port int `arg:"--port" default:"8080" help:"allow droplet events via http"`
Xml []string `arg:"--libvirt" help:"import qemu xml files: --libvirt /etc/libvirt/qemu/*.xml"`
Admin bool `arg:"--admin" help:"enter admin mode"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
Verbose bool `arg:"--verbose" help:"talk more"`
Config string `arg:"env:VIRTIGO_HOME" help:"defaults to ~/.config/virtigo/"`
Port int `arg:"--port" default:"8080" help:"allow droplet events via http"`
Server string `arg:"env:VIRTIGO_SERVER" help:"what virtigo cluster to connect to"`
Xml []string `arg:"--libvirt" help:"import qemu xml files: --libvirt /etc/libvirt/qemu/*.xml"`
}
type EmptyCmd struct {
}
// Daemon bool `arg:"--daemon" help:"run in daemon mode"`
// IgnoreCpu bool `arg:"--xml-ignore-cpu" default:"true" help:"ignore non-standard libvirt xml cpus"`
// IgnoreBr bool `arg:"--xml-ignore-net" default:"true" help:"ignore network bridge name changes"`
// IgnDisk bool `arg:"--xml-ignore-disk" default:"false" help:"ignore duplicate disk names"`
type testCmd string
type ListCmd struct {
Droplets *EmptyCmd `arg:"subcommand:droplets" help:"list droplets"`
Hypervisors *EmptyCmd `arg:"subcommand:hypervisors" help:"list hypervisors"`
On bool `arg:"--on" help:"only show things that are on"`
}
type DropletCmd struct {
Start *EmptyCmd `arg:"subcommand:start" help:"start droplet"`
Stop *EmptyCmd `arg:"subcommand:stop" help:"stop droplet"`
Show *EmptyCmd `arg:"subcommand:show" help:"show droplet"`
Console *EmptyCmd `arg:"subcommand:console" help:"open serial console"`
VNC *EmptyCmd `arg:"subcommand:vnc" help:"open VNC console"`
Spice *EmptyCmd `arg:"subcommand:spice" help:"open spiceconsole"`
Name string `arg:"--name" help:"what droplet to start"`
}
// Save bool `arg:"--save" default:"false" help:"save protobuf config after import"`
// Start string `arg:"--start" help:"start a droplet"`
// Uptime bool `arg:"--uptime" default:"true" help:"allow uptime checks for things like Kuma"`
// Hosts []string `arg:"--hosts" help:"hosts to connect to"`
func (a args) Description() string {
return `
virtigo: control your cluster
virtigo will help control your cluster
This maintains a master list of all your vm's (aka 'droplets')
in your homelab cloud. You can import libvirt xml files.
This app talks to your hypervisors via the virtigod daemon.
At this time, this _only_ supports qcow2 images. If you need
something else you'll have to add it in virtigolib.
This runs a http server so you can control your virtual machines.
For example to start a vm called 'www.wit.com' your cluster 'foo.bar.com':
curl http://virtigo.foo.com/start?www.wit.com
Use 'virtigoctl' to import xml files from libvirt and configure new
hypervisors in your cluster.
`
}
func (args) Version() string {
return ARGNAME + " " + VERSION + " Built on " + BUILDTIME
return "virtigo " + Version
}
var INFO *log.LogFlag
var POLL *log.LogFlag
var WARN *log.LogFlag
var SPEW *log.LogFlag
var EVENT *log.LogFlag
func init() {
@ -77,30 +66,6 @@ func init() {
INFO = log.NewFlag("INFO", false, full, short, "general virtigo")
POLL = log.NewFlag("POLL", false, full, short, "virtigo polling")
WARN = log.NewFlag("WARN", true, full, short, "bad things")
SPEW = log.NewFlag("SPEW", true, full, short, "dump everything")
EVENT = log.NewFlag("EVENT", true, full, short, "hypeprvisor/droplet events")
}
/*
handles shell autocomplete
*/
func (a args) DoAutoComplete(argv []string) {
switch argv[0] {
case "list":
fmt.Println("droplets hypervisors")
case "droplet":
fmt.Println("start stop")
case "devel":
fmt.Println("--force")
case "master":
fmt.Println("")
case "verify":
fmt.Println("user devel master")
default:
if argv[0] == ARGNAME {
// list the subcommands here
fmt.Println("--bash list droplet")
}
}
os.Exit(0)
}

View File

@ -10,7 +10,6 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/user"
"time"
@ -22,20 +21,16 @@ import (
)
// refresh the windows & tables the user has open
func (admin *adminT) refresh() error {
func (admin *adminT) refresh() {
if argv.Verbose {
log.Info("virtigo scan here")
}
if admin.url == nil {
log.Info("admin url == nil")
return fmt.Errorf("admin url == nil")
}
url := argv.Server
msg := []byte(`{"message": "Hello"}`)
// display the uptime
if data, err := postData(admin.url.String()+"/uptime", msg); err != nil {
if data, err := postData(url+"/uptime", msg); err != nil {
log.Info("/uptime Error:", err)
} else {
log.Info("Response:", string(data))
@ -43,144 +38,52 @@ func (admin *adminT) refresh() error {
}
// update the droplet list
if data, err := postData(admin.url.String()+"/DropletsPB", msg); err != nil {
if data, err := postData(url+"/DropletsPB", msg); err != nil {
log.Info("/DropletsPB Error:", err)
} else {
fmt.Println("DropletsPB Response len:", len(data))
admin.cluster.Droplets = new(virtpb.Droplets)
if err := admin.cluster.Droplets.Unmarshal(data); err != nil {
admin.droplets = new(virtpb.Droplets)
if err := admin.droplets.Unmarshal(data); err != nil {
fmt.Println("droplets marshal failed", err)
return err
return
}
fmt.Println("Droplet len=", admin.cluster.Droplets.Len())
fmt.Println("Droplet len=", admin.droplets.Len())
}
// update the hypervisor list
if data, err := postData(admin.url.String()+"/HypervisorsPB", msg); err != nil {
if data, err := postData(url+"/HypervisorsPB", msg); err != nil {
log.Info("Error:", err)
} else {
fmt.Println("HypervisorsPB Response len:", len(data))
admin.cluster.Hypervisors = new(virtpb.Hypervisors)
if err := admin.cluster.Hypervisors.Unmarshal(data); err != nil {
admin.hypervisors = new(virtpb.Hypervisors)
if err := admin.hypervisors.Unmarshal(data); err != nil {
fmt.Println("hypervisors marshal failed", err)
return err
return
}
fmt.Println("Hypervisors len=", admin.cluster.Hypervisors.Len())
fmt.Println("Hypervisors len=", admin.hypervisors.Len())
}
// update the events list
if data, err := postData(admin.url.String()+"/EventsPB", msg); err != nil {
if data, err := postData(url+"/EventsPB", msg); err != nil {
log.Info("Error:", err)
} else {
fmt.Println("EventsPB Response len:", len(data))
admin.cluster.Events = new(virtpb.Events)
if err := admin.cluster.Events.Unmarshal(data); err != nil {
admin.events = new(virtpb.Events)
if err := admin.events.Unmarshal(data); err != nil {
fmt.Println("events marshal failed", err)
return err
return
}
fmt.Println("Events len=", admin.cluster.Events.Len())
fmt.Println("Events len=", admin.events.Len())
}
return nil
}
var client *http.Client
func doLocalhostAdminGui() *adminT {
admin := new(adminT)
admin.uptime = me.gwin.Group.NewLabel("uptime")
grid := me.gwin.Group.RawGrid()
grid.NewButton("show hypervisors", func() {
if admin.cluster.Hypervisors == nil {
log.Info("hypervisors not initialized")
return
}
log.Info("Hypervisors len=", admin.cluster.Hypervisors.Len())
admin.hwin = newHypervisorsWindow()
admin.hwin.doStdHypervisors(admin.cluster.Hypervisors)
admin.hwin.win.Custom = func() {
log.Info("hiding table window")
}
})
grid.NewButton("droplets", func() {
if admin.cluster.Droplets == nil {
log.Info("droplets not initialized")
return
}
admin.dwin = newDropletsWindow(admin)
admin.dwin.win.Custom = func() {
log.Info("hiding droplet table window")
}
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current.State != virtpb.DropletState_ON {
continue
}
found.Append(vm)
}
admin.dwin.doActiveDroplets(found)
})
grid.NewButton("events", func() {
if admin.cluster.Events == nil {
log.Info("events are not initialized")
return
}
log.Info("Events len=", admin.cluster.Events.Len())
admin.ewin = newEventsWindow()
admin.ewin.doStdEvents(admin.cluster.Events)
admin.ewin.win.Custom = func() {
log.Info("hiding table window")
}
})
grid.NextRow()
grid.NewButton("refresh", func() {
admin.refresh()
})
return admin
}
func doAdminGui() {
// Initialize a persistent client with a custom Transport
client = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: false, // Ensure Keep-Alive is enabled
},
Timeout: 10 * time.Second, // Set a reasonable timeout
}
me.gwin = gadgets.NewGenericWindow("Virtigo: (run your cluster)", "")
me.gwin.Custom = func() {
log.Warn("Main window close")
os.Exit(0)
}
me.cmap = make(map[*virtpb.Cluster]*adminT)
for c := range me.clusters.IterAll() {
a := new(adminT)
me.cmap[c] = a
log.Info("found in the config file", c.URL[0])
a.makeClusterGroup(c)
}
// sit here forever refreshing the GUI
for {
// admin.refresh()
log.Info("todo: refresh() protobufs here")
time.Sleep(90 * time.Second)
}
}
func (admin *adminT) doAdminGui() {
me.myGui = gui.New()
me.myGui.InitEmbed(resources)
me.myGui.Default()
// Initialize a persistent client with a custom Transport
client = &http.Client{
Transport: &http.Transport{
@ -189,42 +92,41 @@ func (admin *adminT) doAdminGui() {
Timeout: 10 * time.Second, // Set a reasonable timeout
}
win := gadgets.NewGenericWindow("Virtigo: (run your cluster)", "localhost")
win := gadgets.NewGenericWindow("Virtigo: (run your cluster)", "virtigo stuff")
win.Custom = func() {
log.Warn("Main window close")
os.Exit(0)
}
me.gwin = win
admin.uptime = win.Group.NewLabel("uptime")
grid := win.Group.RawGrid()
grid.NewButton("show hypervisors", func() {
if admin.cluster.Hypervisors == nil {
if admin.hypervisors == nil {
log.Info("hypervisors not initialized")
return
}
log.Info("Hypervisors len=", admin.cluster.Hypervisors.Len())
log.Info("Hypervisors len=", admin.hypervisors.Len())
admin.hwin = newHypervisorsWindow()
admin.hwin.doStdHypervisors(admin.cluster.Hypervisors)
admin.hwin.doStdHypervisors(admin.hypervisors)
admin.hwin.win.Custom = func() {
log.Info("hiding table window")
}
})
grid.NewButton("droplets", func() {
if admin.cluster.Droplets == nil {
if admin.droplets == nil {
log.Info("droplets not initialized")
return
}
admin.dwin = newDropletsWindow(admin)
admin.dwin = newDropletsWindow()
admin.dwin.win.Custom = func() {
log.Info("hiding droplet table window")
}
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
all := admin.droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current.State != virtpb.DropletState_ON {
@ -236,13 +138,13 @@ func (admin *adminT) doAdminGui() {
})
grid.NewButton("events", func() {
if admin.cluster.Events == nil {
if admin.events == nil {
log.Info("events are not initialized")
return
}
log.Info("Events len=", admin.cluster.Events.Len())
log.Info("Events len=", admin.events.Len())
admin.ewin = newEventsWindow()
admin.ewin.doStdEvents(admin.cluster.Events)
admin.ewin.doStdEvents(admin.events)
admin.ewin.win.Custom = func() {
log.Info("hiding table window")
}
@ -255,18 +157,10 @@ func (admin *adminT) doAdminGui() {
})
grid.NewButton("test gui close", func() {
gui.StandardExit()
me.myGui.Close()
// okExit("admin close")
})
me.cmap = make(map[*virtpb.Cluster]*adminT)
for c := range me.clusters.IterAll() {
a := new(adminT)
me.cmap[c] = a
log.Info("found in the config file", c.URL[0])
a.makeClusterGroup(c)
}
// sit here forever refreshing the GUI
for {
admin.refresh()
@ -274,90 +168,6 @@ func (admin *adminT) doAdminGui() {
}
}
func (admin *adminT) makeClusterGroup(c *virtpb.Cluster) {
var err error
admin.url, err = url.Parse(c.URL[0])
if err != nil {
badExit(err)
}
if admin.cluster == nil {
admin.cluster = new(virtpb.Cluster)
admin.cluster.Name = c.Name
admin.cluster.Uuid = c.Uuid
}
name := c.GetName()
if name == "" {
name = admin.url.Hostname()
}
group := me.gwin.Bottom.NewGroup(name)
admin.uptime = group.NewLabel("uptime")
grid := group.RawGrid()
grid.NewButton("show hypervisors", func() {
if admin.cluster.Hypervisors == nil {
log.Info("hypervisors not initialized")
return
}
log.Info("Hypervisors len=", admin.cluster.Hypervisors.Len())
admin.hwin = newHypervisorsWindow()
admin.hwin.doStdHypervisors(admin.cluster.Hypervisors)
admin.hwin.win.Custom = func() {
log.Info("hiding table window")
}
})
grid.NewButton("droplets", func() {
if admin.cluster.Droplets == nil {
log.Info("droplets not initialized")
return
}
admin.dwin = newDropletsWindow(admin)
admin.dwin.win.Custom = func() {
log.Info("hiding droplet table window")
}
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current.State != virtpb.DropletState_ON {
continue
}
found.Append(vm)
}
admin.dwin.doActiveDroplets(found)
})
grid.NewButton("events", func() {
if admin.cluster.Events == nil {
log.Info("events are not initialized")
return
}
log.Info("Events len=", admin.cluster.Events.Len())
admin.ewin = newEventsWindow()
admin.ewin.doStdEvents(admin.cluster.Events)
admin.ewin.win.Custom = func() {
log.Info("hiding table window")
}
})
grid.NewButton("refresh", func() {
admin.refresh()
})
if err := admin.refresh(); err != nil {
return
}
grid.NewButton("save cluster.pb", func() {
admin.cluster.ConfigSave()
})
}
func postData(url string, data []byte) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
if err != nil {
@ -382,35 +192,21 @@ func postData(url string, data []byte) ([]byte, error) {
return body, nil
}
func (admin *adminT) postEvent(e *virtpb.Event) error {
var result *virtpb.Event
result = new(virtpb.Event)
/*
func main() {
url := "http://example.com/endpoint"
data := []byte(`{"message": "Hello"}`)
msg, err := e.Marshal()
if err != nil {
log.Info("postEvent() marshal() failed", err, e)
return err
}
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
url := admin.url.String() + "/event"
// update the droplet list
if data, err := postData(url, msg); err != nil {
log.Info("postEvent() /event Error:", err)
return err
} else {
if err := result.Unmarshal(data); err != nil {
log.Println("postEvent() result marshal failed", err, "len(dat) =", len(data))
log.Println("postEvent() data =", string(data))
return err
for range ticker.C {
response, err := postData(url, data)
if err != nil {
fmt.Println("Error:", err)
} else {
log.Println("postEvent() result marshal worked on len(dat) =", len(data))
log.Println("postEvent() result =", result.FormatTEXT())
fmt.Println("Response:", string(response))
}
}
if result.Error != "" {
return fmt.Errorf("%s", result.Error)
}
log.Printf("Event worked to %s uuid=%s\n", url, result.DropletUuid)
return nil
}
*/

View File

@ -1,154 +0,0 @@
// Copyright 2024 WIT.COM Inc Licensed GPL 3.0
package main
import (
"fmt"
"time"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/lib/virtigolib"
"go.wit.com/log"
)
func doDaemon() error {
// set defaults
me.unstable = time.Now() // initialize the grid as unstable
me.changed = false
me.hmap = make(map[*virtpb.Hypervisor]*HyperT)
// how long a droplet can be missing until it's declared dead
me.unstableTimeout = 17 * time.Second
me.missingDropletTimeout = time.Minute // not sure the difference between these values
// how often to poll the hypervisors
me.hyperPollDelay = 5 * time.Second
// how long the cluster must be stable before new droplets can be started
me.clusterStableDuration = 37 * time.Second
me.cluster = virtpb.InitCluster()
if err := me.cluster.ConfigLoad(); err != nil {
log.Info("config load error", err)
log.Info("")
log.Info("You have never run this before")
log.Info("init example cloud here")
log.Sleep(2)
return err
}
loop := me.cluster.DropletsAll() // get the list of droplets
for loop.Scan() {
d := loop.Next()
if d == nil {
fmt.Println("d == nil")
return fmt.Errorf("d == nil")
}
fmt.Println("Droplet UUID:", d.Uuid)
if d.Current == nil {
d.Current = new(virtpb.Current)
}
d.SetState(virtpb.DropletState_OFF)
log.Info("droplet", d.Hostname)
}
hmm := "pihole.wit.com"
d := me.cluster.FindDropletByName(hmm)
if d == nil {
log.Info("did not find found droplet", hmm)
} else {
log.Info("found droplet", d.Hostname, d)
}
var newEvents []*virtpb.Event
// sanity check the cluster & droplets
if _, _, err := ValidateDroplets(); err != nil {
log.Info("todo: add flag to ignore. for now, fix problems in the config file.")
return err
}
newe, err := ValidateDiskFilenames()
if err != nil {
log.Info(err)
return err
}
// this is a new droplet. add it to the cluster
for _, e := range newe {
newEvents = append(newEvents, e)
}
ValidateUniqueFilenames()
for _, filename := range argv.Xml {
domcfg, err := virtigolib.ReadXml(filename)
if err != nil {
// parsing the libvirt xml file failed
log.Info("error:", filename, err)
log.Info("readXml() error", filename)
log.Info("readXml() error", err)
log.Info("libvirt XML will have to be fixed by hand")
return err
}
// this is a new droplet. add it to the cluster
log.Info("Add XML Droplet here", domcfg.Name)
_, newe, err := virtigolib.AddDomainDroplet(me.cluster, domcfg)
if err != nil {
log.Info("addDomainDroplet() error", filename)
log.Info("addDomainDroplet() error", err)
log.Info("libvirt XML will have to be fixed by hand")
return err
}
for _, e := range newe {
newEvents = append(newEvents, e)
}
}
for i, e := range newEvents {
log.Info(i, "Event:", e.Droplet, e.FieldName, "orig:", e.OrigVal, "new:", e.NewVal)
me.changed = true
}
if me.changed {
if err := me.cluster.ConfigSave(); err != nil {
log.Info("configsave error", err)
return err
}
log.Info("XML changes saved in protobuf config")
return nil
}
if len(argv.Xml) != 0 {
log.Info("No XML changes found")
return fmt.Errorf("No XML changes found")
}
// initialize each hypervisor
for _, pbh := range me.cluster.H.Hypervisors {
// this is a new unknown droplet (not in the config file)
var h *HyperT
h = new(HyperT)
h.pb = pbh
h.lastDroplets = make(map[string]time.Time)
h.lastpoll = time.Now()
me.hmap[pbh] = h
me.hypers = append(me.hypers, h)
log.Log(EVENT, "config new hypervisors", h.pb.Hostname)
}
// start the watchdog polling for each hypervisor
for _, h := range me.hypers {
log.Info("starting polling on", h.pb.Hostname)
// start a watchdog on each hypervisor
go h.NewWatchdog()
}
var cloud *virtigolib.CloudManager
cloud = virtigolib.NewCloud()
found, _ := cloud.FindDropletByName("www.wit.com")
if found == nil {
log.Info("d == nil")
} else {
log.Info("d == ", found)
}
startHTTP()
return nil
}

View File

@ -1,361 +0,0 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"fmt"
"math/rand"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/log"
)
func doDroplet() (string, error) {
err := me.clusters.ConfigLoad()
if err != nil {
return "", err
}
msg := []byte(`{"message": "Hello"}`)
// Initialize a persistent client with a custom Transport
client = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: false, // Ensure Keep-Alive is enabled
},
Timeout: 10 * time.Second, // Set a reasonable timeout
}
me.cmap = make(map[*virtpb.Cluster]*adminT)
for c := range me.clusters.IterAll() {
var err error
admin := new(adminT)
if admin.cluster == nil {
admin.cluster = new(virtpb.Cluster)
}
me.cmap[c] = admin
log.Info("found in the config file", c.URL[0])
// a.makeClusterGroup(c)
admin.url, err = url.Parse(c.URL[0])
if err != nil {
return "", err
}
// update the droplet list
if data, err := postData(admin.url.String()+"/DropletsPB", msg); err != nil {
log.Info("/DropletsPB Error:", err)
continue
} else {
admin.cluster.Droplets = new(virtpb.Droplets)
if err := admin.cluster.Droplets.Unmarshal(data); err != nil {
log.Printf("DropletsPB Response len:%d\n", len(data))
log.Println("droplets marshal failed", err)
continue
}
}
log.Printf("Cluster Name: %s\n", c.Name)
log.Printf("Number of Droplets: %d\n", admin.cluster.Droplets.Len())
if argv.Droplet.Name == "" {
return "", fmt.Errorf("--name droplet name was empty")
}
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
for all.Scan() {
vm := all.Next()
if argv.Droplet.Name == vm.Hostname {
if argv.Droplet.Show != nil {
log.Info(vm.SprintHeader())
txt := vm.FormatTEXT()
log.Info(txt)
return "droplet status", nil
}
if argv.Droplet.Start != nil {
log.Info("should start droplet here")
log.Info(vm.SprintHeader())
e := new(virtpb.Event)
e.Etype = virtpb.EventType_POWERON
e.DropletUuid = vm.Uuid
if err := admin.postEvent(e); err != nil {
return "droplet start err", err
}
return "droplet start", nil
}
return "droplet found", fmt.Errorf("do what to the droplet?")
}
found.Append(vm)
}
log.Println("On Droplet count=", found.Len())
}
return "", fmt.Errorf("droplet %s not found", argv.Droplet.Name)
}
func doEvent(e *virtpb.Event) *virtpb.Event {
result := new(virtpb.Event)
if e.Etype == virtpb.EventType_POWERON {
log.Println("power on droplet on local cluster here", e.DropletUuid)
result.State = virtpb.Event_DONE
rs, err := Start(e.DropletUuid)
log.Println("Start() returned", rs)
log.Println("Start() returned err", err)
if err != nil {
result.Error = fmt.Sprintf("%v", err)
}
return result
}
if e.Etype == virtpb.EventType_EDIT {
log.Println("edit event", e.DropletUuid)
result.State = virtpb.Event_DONE
if e.Droplet != nil {
return updateDroplet(e.Droplet)
}
log.Println("unknown edit event")
result.State = virtpb.Event_FAIL
return result
}
if e.Etype == virtpb.EventType_ADD {
log.Println("START ADD droplet event", e.Droplet.FormatTEXT())
if e.Droplet == nil {
result.State = virtpb.Event_FAIL
return result
}
result.DropletName = e.Droplet.Hostname
result.Error = e.Droplet.FormatTEXT() // feedback to the other side for debugging
// attempt to create the new droplet
if err := createDroplet(e.Droplet, result); err != nil {
result.Error += fmt.Sprintf("createDroplet() err: %v", err)
result.State = virtpb.Event_FAIL
return result
}
log.Println("create droplet worked", e.Droplet.FormatTEXT())
result.State = virtpb.Event_DONE
return result
}
log.Println("unknown event", e)
result.Etype = e.Etype
result.State = virtpb.Event_FAIL
return result
}
func updateDroplet(newd *virtpb.Droplet) *virtpb.Event {
var changed bool = false
result := new(virtpb.Event)
if newd == nil {
result.Error = "updateDroplet() d == nil"
result.State = virtpb.Event_FAIL
return result
}
d := me.cluster.FindDropletByUuid(newd.Uuid)
if d == nil {
result.Error = "updateDroplet() could not find uuid"
result.State = virtpb.Event_FAIL
return result
}
log.Println("found droplet to update:", newd.Uuid, newd.Hostname, newd.Cpus, newd.Memory)
if d.Hostname != newd.Hostname && newd.Hostname != "" {
d.Hostname = newd.Hostname
changed = true
}
if d.Cpus != newd.Cpus && newd.Cpus > 0 {
d.Cpus = newd.Cpus
changed = true
}
// arbitrary check. don't make vm's with less than 64 MB of RAM
// big enough most things will load with some stdout
if d.Memory != newd.Memory && newd.Memory > (64*1024*1024) {
d.Memory = newd.Memory
changed = true
}
if changed {
if err := me.cluster.ConfigSave(); err != nil {
log.Info("configsave error", err)
result.Error = fmt.Sprintf("%v", err)
result.State = virtpb.Event_FAIL
return result
}
} else {
log.Println("nothing changed in", newd.Uuid, newd.Hostname)
}
result.State = virtpb.Event_DONE
return result
}
func createDroplet(newd *virtpb.Droplet, result *virtpb.Event) error {
if newd == nil {
return fmt.Errorf("droplet protobuf == nil")
}
if newd.Uuid == "" {
newd.Uuid = uuid.New().String()
}
d := me.cluster.FindDropletByUuid(newd.Uuid)
if d != nil {
return fmt.Errorf("droplet uuid already used")
}
log.Println("found droplet to update:", newd.Uuid, newd.Hostname, newd.Cpus, newd.Memory)
if newd.Hostname == "" {
return fmt.Errorf("Hostname can not be blank")
}
d = me.cluster.FindDropletByName(newd.Hostname)
if d != nil {
return fmt.Errorf("hostname already defined")
}
// by default, on locally imported domains, set the preferred hypervisor!
newd.LocalOnly = "yes on: " + "farm03"
newd.PreferredHypervisor = "farm03"
newd.StartState = virtpb.DropletState_OFF
newd.Current = new(virtpb.Current)
newd.Current.State = virtpb.DropletState_OFF
// create the network
if err := createNetwork(newd); err != nil {
return err
}
// create the disks
if err := createDisks(newd); err != nil {
return err
}
// append the protobuf and save it
me.cluster.AddDroplet(newd)
if err := me.cluster.ConfigSave(); err != nil {
log.Info("configsave error", err)
return fmt.Errorf("ConfigSave() error: %v", err)
}
return nil
}
func findDisks(d *virtpb.Droplet) error {
log.Info("need to do this")
return nil
}
func createDisks(d *virtpb.Droplet) error {
if d.Disks != nil {
return findDisks(d)
}
newdisk := new(virtpb.Disk)
newdisk.Filename = d.Hostname + ".qcow2"
newdisk.Filepath = "/home/nfs2"
d.Disks = append(d.Disks, newdisk)
basefile := "/home/nfs2/base2025.wit-5.qcow2"
newfile := filepath.Join(newdisk.Filepath, newdisk.Filename)
if !shell.Exists(newdisk.Filepath) {
return fmt.Errorf("disk image path missing: %s", newdisk.Filepath)
}
if !shell.Exists(basefile) {
return fmt.Errorf("basefile %s missing", basefile)
}
if shell.Exists(newfile) {
return fmt.Errorf("disk image already exists: %s", newfile)
}
cmd := []string{"dd", "bs=100M", "status=progress", "oflag=dsync", "if=" + basefile, "of=" + newfile}
result := shell.RunRealtime(cmd)
if result.Exit != 0 {
return fmt.Errorf("dd to %s failed %d\n%s\n%s", newfile, result.Exit, strings.Join(result.Stdout, "\n"), strings.Join(result.Stderr, "\n"))
}
return nil
}
func createNetwork(d *virtpb.Droplet) error {
if d.Networks != nil {
// network already done
return nil
}
if len(d.Networks) > 0 {
// network already done
return nil
}
n := new(virtpb.Network)
n.Mac = getNewMac()
n.Name = "worldbr"
d.Networks = append(d.Networks, n)
return nil
}
func getNewMac() string {
// mac address map to check for duplicates
var macs map[string]string
macs = make(map[string]string)
loop := me.cluster.DropletsAll() // get the list of droplets
for loop.Scan() {
d := loop.Next()
for _, n := range d.Networks {
// log.Println("network:", n.Mac, d.Uuid, d.Hostname)
if _, ok := macs[n.Mac]; ok {
// UUID already exists
log.Info("duplicate MAC", n.Mac, macs[n.Mac])
log.Info("duplicate MAC", n.Mac, d.Hostname)
return ""
}
macs[n.Mac] = d.Hostname
}
}
return generateMAC(macs)
}
func generateMAC(macs map[string]string) string {
prefix := []byte{0x22, 0x22, 0x22}
for {
// Generate last 3 bytes randomly
suffix := make([]byte, 3)
if _, err := rand.Read(suffix); err != nil {
log.Fatalf("Failed to generate random bytes: %v", err)
}
// Format full MAC address
mac := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
prefix[0], prefix[1], prefix[2],
suffix[0], suffix[1], suffix[2])
// Check if MAC is already used
if _, exists := macs[mac]; !exists {
log.Println("Using new MAC:", mac)
return mac
}
log.Println("MAC already defined:", mac)
}
}

133
doGui.go
View File

@ -11,6 +11,7 @@ import (
"strings"
"time"
"go.wit.com/gui"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/log"
@ -24,34 +25,43 @@ func debug() {
}
func doGui() {
mainWindow := gadgets.NewGenericWindow("Virtigo: (inventory your cluster)", "Local Cluster Settings")
me.myGui = gui.New()
me.myGui.InitEmbed(resources)
me.myGui.Default()
mainWindow := gadgets.RawBasicWindow("Virtigo: (inventory your cluster)")
mainWindow.Make()
mainWindow.Show()
mainWindow.Custom = func() {
log.Warn("Main window close")
os.Exit(0)
}
drawWindow(mainWindow)
// sits here forever
debug()
}
func drawWindow(win *gadgets.GenericWindow) {
grid := win.Group.RawGrid()
func drawWindow(win *gadgets.BasicWindow) {
box := win.Box()
var newHyperWin *stdHypervisorTableWin
grid.NewButton("show hypervisors", func() {
if newHyperWin != nil {
log.Info("redraw hypervisors")
newHyperWin.doNewStdHypervisors(me.cluster.H)
vbox := box.NewVerticalBox("BOX2")
group1 := vbox.NewGroup("Virtigo Settings")
grid := group1.NewGrid("buildOptions", 0, 0)
var hyperWin *GenericWindow
grid.NewButton("hypervisors", func() {
if hyperWin != nil {
hyperWin.Toggle()
return
}
log.Info("Hypervisors len=", me.cluster.H.Len())
newHyperWin = newHypervisorsWindow()
newHyperWin.doNewStdHypervisors(me.cluster.H)
newHyperWin.win.Custom = func() {
log.Info("hiding table window")
}
hyperWin = makeHypervisorsWindow(me.cluster.H)
})
var dropWin *gadgets.GenericWindow
var dropWin *GenericWindow
grid.NewButton("droplets", func() {
if dropWin != nil {
dropWin.Toggle()
@ -74,31 +84,22 @@ func drawWindow(win *gadgets.GenericWindow) {
}
})
var ewin *stdEventTableWin
grid.NewButton("events", func() {
if ewin != nil {
log.Info("update events here")
e := me.cluster.GetEventsPB()
log.Info("Events len=", e.Len())
ewin.doStdEvents(e)
var eventWin *GenericWindow
grid.NewButton("events)", func() {
log.Info("todo: make code for this")
if eventWin != nil {
eventWin.Toggle()
return
}
ewin = newEventsWindow()
ewin.win.Custom = func() {
log.Info("hiding table window")
}
e := me.cluster.GetEventsPB()
log.Info("Events len=", e.Len())
ewin.doStdEvents(e)
eventWin = makeEventsWindow(e)
})
grid.NextRow()
grid.NewButton("ConfigSave()", func() {
log.Info("todo: make code for this")
})
var testWin *gadgets.GenericWindow
var testWin *GenericWindow
grid.NewButton("create droplet", func() {
if testWin != nil {
testWin.Toggle()
@ -107,32 +108,27 @@ func drawWindow(win *gadgets.GenericWindow) {
d := me.cluster.GetDropletsPB()
testWin, _ = makeDropletsWindow(d)
})
grid.NewButton("uptime", func() {
updateUptimeGui("kuma uptime should update this")
})
grid.NextRow()
grid = win.Middle.RawGrid()
me.status = grid.NewLabel("cur status")
grid.NextRow()
me.lastuptime = grid.NewLabel("last uptime")
grid.NextRow()
grid.NewButton("uptime", func() {
updateUptimeGui("kuma uptime should update this")
})
}
func updateUptimeGui(uptime string) {
if me.status == nil {
// gui is not initialized
return
}
me.status.SetLabel(uptime)
datestamp := time.Now().Format("2006-01-02 15:04:03")
me.lastuptime.SetLabel("last uptime at " + datestamp)
}
func makeDropletsWindow(pb *virtpb.Droplets) (*gadgets.GenericWindow, *virtpb.DropletsTable) {
win := gadgets.NewGenericWindow("Droplets registered with Virtigo", "Buttons of things")
func makeDropletsWindow(pb *virtpb.Droplets) (*GenericWindow, *virtpb.DropletsTable) {
win := NewGenericWindow("Droplets registered with Virtigo", "Buttons of things")
t := pb.NewTable("testDroptable")
t.NewUuid()
@ -185,8 +181,55 @@ func makeDropletsWindow(pb *virtpb.Droplets) (*gadgets.GenericWindow, *virtpb.Dr
return win, t
}
func makeEventsWindow(pb *virtpb.Events) *gadgets.GenericWindow {
win := gadgets.NewGenericWindow("Cluster Events", "Buttons of things")
func makeHypervisorsWindow(pb *virtpb.Hypervisors) *GenericWindow {
win := NewGenericWindow("Hypervisors registered with Virtigo", "Buttons of things")
t := pb.NewTable("testHyper")
grid := win.Group.RawGrid()
grid.NewButton("List", func() {
log.Info("list...")
})
/*
grid.NewButton("Update", func() {
t.Update()
})
*/
tbox := win.Bottom.Box() // a vertical box (like a stack of books)
t.NewUuid()
t.SetParent(tbox)
t.AddHostname()
t.AddMemory()
t.AddCpus()
t.AddTimeFunc("last poll", func(h *virtpb.Hypervisor) time.Time {
hm := me.hmap[h]
tmp := hm.lastpoll
log.Info("poll age", h.Hostname, virtpb.FormatDuration(time.Since(tmp)))
return tmp
})
t.AddKillcount()
t.AddStringFunc("droplets", func(h *virtpb.Hypervisor) string {
var totalDroplets int
var totalUnknownDroplets int
// dur := time.Since(h.lastpoll)
// tmp := virtpb.FormatDuration(dur)
// fmt.Fprintln(w, h.pb.Hostname, "killcount =", h.killcount, "lastpoll:", tmp)
hm := me.hmap[h]
for name, _ := range hm.lastDroplets {
totalDroplets += 1
d := me.cluster.FindDropletByName(name)
if d == nil {
totalUnknownDroplets += 1
}
}
log.Printf("Total Droplets %d total libvirt only droplets = %d\n", totalDroplets, totalUnknownDroplets)
return fmt.Sprintf("%d", totalDroplets)
})
t.ShowTable()
return win
}
func makeEventsWindow(pb *virtpb.Events) *GenericWindow {
win := NewGenericWindow("Cluster Events", "Buttons of things")
grid := win.Group.RawGrid()
grid.NewButton("List", func() {
log.Info("list...")
@ -198,7 +241,7 @@ func makeEventsWindow(pb *virtpb.Events) *gadgets.GenericWindow {
t := pb.NewTable("test 2")
t.NewUuid()
t.SetParent(tbox)
t.AddDropletName()
t.AddDroplet()
t.AddHypervisor()
t.ShowTable()
return win

View File

@ -1,72 +0,0 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"net/http"
"net/url"
"time"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/log"
)
func doList() {
msg := []byte(`{"message": "Hello"}`)
// Initialize a persistent client with a custom Transport
client = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: false, // Ensure Keep-Alive is enabled
},
Timeout: 10 * time.Second, // Set a reasonable timeout
}
me.cmap = make(map[*virtpb.Cluster]*adminT)
for c := range me.clusters.IterAll() {
var err error
admin := new(adminT)
admin.cluster = new(virtpb.Cluster)
me.cmap[c] = admin
log.Info("found in the config file", c.URL[0])
// a.makeClusterGroup(c)
admin.url, err = url.Parse(c.URL[0])
if err != nil {
badExit(err)
}
// update the droplet list
if data, err := postData(admin.url.String()+"/DropletsPB", msg); err != nil {
log.Info("/DropletsPB Error:", err)
continue
} else {
admin.cluster.Droplets = new(virtpb.Droplets)
if err := admin.cluster.Droplets.Unmarshal(data); err != nil {
log.Printf("DropletsPB Response len:%d\n", len(data))
log.Println("droplets marshal failed", err)
continue
}
}
log.Printf("Cluster Name: %s\n", c.Name)
log.Printf("Number of Droplets: %d\n", admin.cluster.Droplets.Len())
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current == nil {
continue
}
if argv.List.On && (vm.Current.State == virtpb.DropletState_OFF) {
continue
}
found.Append(vm)
log.Info(vm.SprintHeader())
}
log.Println("On Droplet count=", found.Len())
}
}

21
exit.go
View File

@ -6,32 +6,19 @@ package main
import (
"os"
"go.wit.com/gui"
"go.wit.com/log"
)
func okExit(note string) {
if note != "" {
log.Info(ARGNAME, "exit:", note, "ok")
log.Info("virtigo exit:", note, "ok")
}
gui.StandardExit()
me.myGui.Close()
os.Exit(0)
}
func badExit(err error) {
log.Info(ARGNAME, "failed: ", err)
gui.StandardExit()
os.Exit(-1)
}
func exit(note string, err error) {
if note != "" {
log.Info(ARGNAME, "exit:", note, "ok")
}
gui.StandardExit()
if err == nil {
os.Exit(0)
}
log.Info(ARGNAME, "failed: ", err)
log.Info("virtigo failed: ", err)
me.myGui.Close()
os.Exit(-1)
}

38
http.go
View File

@ -21,8 +21,8 @@ func cleanURL(url string) string {
func okHandler(w http.ResponseWriter, r *http.Request) {
var route string
route = cleanURL(r.URL.Path)
// log.HttpMode(w)
// defer log.HttpMode(nil)
log.HttpMode(w)
defer log.HttpMode(nil)
msg, err := ioutil.ReadAll(r.Body) // Read the body as []byte
if err != nil {
@ -31,7 +31,7 @@ func okHandler(w http.ResponseWriter, r *http.Request) {
}
if route == "/uptime" {
ok, s := uptimeCheck()
fmt.Fprintln(w, s)
fmt.Fprint(w, s)
// log.Info(s)
updateUptimeGui(s)
if ok {
@ -70,24 +70,26 @@ func okHandler(w http.ResponseWriter, r *http.Request) {
return
}
if route == "/event" {
var e *virtpb.Event
e = new(virtpb.Event)
if err := e.Unmarshal(msg); err != nil {
log.Info("proto.Unmarshal() failed on wire message len", len(msg))
log.Info("error =", err)
if route == "/start" {
hostname := r.URL.Query().Get("hostname")
if hostname == "" {
log.Warn("start failed. hostname is blank", cleanURL(r.URL.Path))
return
}
log.Info("/event proto.Unmarshal() worked on msg len", len(msg), "hostname =", e.DropletUuid)
result := doEvent(e)
data, err := result.Marshal()
if err != nil {
log.Info("/event marshal failed", err, "len(data) =", len(data))
fmt.Fprintln(w, "/event failed", err)
return
log.Warn("hostname is", hostname)
// log.Warn("Handling URL:", tmp, "start droplet", start)
result, err := Start(hostname)
if err == nil {
log.Info(result)
log.Info(hostname, "started output ok")
log.Info(hostname, "need to parse the output here")
log.Info(hostname, "todo: switch to protobuf here")
} else {
log.Info(result)
log.Info(err)
log.Info(hostname, "start failed")
}
w.Write(data)
// fmt.Fprintln("droplet marshal failed", err)
return
}

218
main.go
View File

@ -4,34 +4,30 @@ package main
import (
"embed"
"net/url"
"fmt"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"go.wit.com/dev/alexflint/arg"
"go.wit.com/lib/gui/prep"
"go.wit.com/gui"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/lib/virtigolib"
"go.wit.com/log"
)
// sent via -ldflags
var VERSION string
var BUILDTIME string
var ARGNAME string = "virtigo"
var Version string
//go:embed resources/*
var resources embed.FS
func main() {
me = new(virtigoT)
prep.Bash(ARGNAME, argv.DoAutoComplete) // this line should be: prep.Bash(argv)
me.myGui = prep.Gui() // prepares the GUI package for go-args
me.pp = arg.MustParse(&argv)
var pp *arg.Parser
gui.InitArg()
pp = arg.MustParse(&argv)
if me.pp == nil {
me.pp.WriteHelp(os.Stdout)
if pp == nil {
pp.WriteHelp(os.Stdout)
os.Exit(0)
}
@ -41,64 +37,150 @@ func main() {
os.Setenv("VIRTIGO_HOME", fullpath)
}
me.clusters = virtpb.NewClusters()
if argv.List != nil {
err := me.clusters.ConfigLoad()
if err != nil {
badExit(err)
}
doList()
okExit("virtigo list")
}
if argv.Droplet != nil {
exit(doDroplet())
}
me.myGui.Start() // loads the GUI toolkit
if argv.Admin {
err := me.clusters.ConfigLoad()
if err != nil {
badExit(err)
}
doAdminGui()
okExit("admin close")
}
if argv.Server != "" {
log.Info("start admin interface")
admin := new(adminT)
var err error
admin.url, err = url.Parse(argv.Server)
if err != nil {
badExit(err)
}
err = me.clusters.ConfigLoad()
if err != nil {
clusters := virtpb.NewClusters()
c := new(virtpb.Cluster)
c.Uuid = uuid.New().String()
c.URL = append(c.URL, argv.Server)
clusters.Append(c)
virtpb.ConfigWriteTEXT(clusters, "cluster.text")
badExit(err)
}
admin.doAdminGui()
me.admin = new(adminT)
me.admin.doAdminGui()
okExit("admin close")
}
if argv.Daemon {
if err := doDaemon(); err != nil {
badExit(err)
}
okExit("")
// set defaults
me.unstable = time.Now() // initialize the grid as unstable
me.changed = false
me.hmap = make(map[*virtpb.Hypervisor]*HyperT)
// how long a droplet can be missing until it's declared dead
me.unstableTimeout = 17 * time.Second
me.missingDropletTimeout = time.Minute // not sure the difference between these values
// how often to poll the hypervisors
me.hyperPollDelay = 5 * time.Second
// how long the cluster must be stable before new droplets can be started
me.clusterStableDuration = 37 * time.Second
me.cluster = virtpb.InitCluster()
if err := me.cluster.ConfigLoad(); err != nil {
log.Info("config load error", err)
log.Info("")
log.Info("You have never run this before")
log.Info("init example cloud here")
log.Sleep(2)
os.Exit(-1)
}
doGui() // start making our forge GUI
startHTTP() // sit here forever
loop := me.cluster.DropletsAll() // get the list of droplets
for loop.Scan() {
d := loop.Next()
if d == nil {
fmt.Println("d == nil")
os.Exit(-1)
}
fmt.Println("Droplet UUID:", d.Uuid)
if d.Current == nil {
d.Current = new(virtpb.Current)
}
d.SetState(virtpb.DropletState_OFF)
log.Info("droplet", d.Hostname)
}
hmm := "pihole.wit.com"
d := me.cluster.FindDropletByName(hmm)
if d == nil {
log.Info("did not find found droplet", hmm)
} else {
log.Info("found droplet", d.Hostname, d)
}
var newEvents []*virtpb.Event
// sanity check the cluster & droplets
if _, _, err := ValidateDroplets(); err != nil {
log.Info("todo: add flag to ignore. for now, fix problems in the config file.")
os.Exit(0)
}
newe, err := ValidateDiskFilenames()
if err != nil {
log.Info(err)
os.Exit(-1)
}
// this is a new droplet. add it to the cluster
for _, e := range newe {
newEvents = append(newEvents, e)
}
ValidateUniqueFilenames()
for _, filename := range argv.Xml {
domcfg, err := virtigolib.ReadXml(filename)
if err != nil {
// parsing the libvirt xml file failed
log.Info("error:", filename, err)
log.Info("readXml() error", filename)
log.Info("readXml() error", err)
log.Info("libvirt XML will have to be fixed by hand")
os.Exit(-1)
}
// this is a new droplet. add it to the cluster
log.Info("Add XML Droplet here", domcfg.Name)
_, newe, err := virtigolib.AddDomainDroplet(me.cluster, domcfg)
if err != nil {
log.Info("addDomainDroplet() error", filename)
log.Info("addDomainDroplet() error", err)
log.Info("libvirt XML will have to be fixed by hand")
os.Exit(-1)
}
for _, e := range newe {
newEvents = append(newEvents, e)
}
}
for i, e := range newEvents {
log.Info(i, "Event:", e.Droplet, e.FieldName, "orig:", e.OrigVal, "new:", e.NewVal)
me.changed = true
}
if me.changed {
if err := me.cluster.ConfigSave(); err != nil {
log.Info("configsave error", err)
os.Exit(-1)
}
log.Info("XML changes saved in protobuf config")
os.Exit(0)
}
if len(argv.Xml) != 0 {
log.Info("No XML changes found")
os.Exit(0)
}
// initialize each hypervisor
for _, pbh := range me.cluster.H.Hypervisors {
// this is a new unknown droplet (not in the config file)
var h *HyperT
h = new(HyperT)
h.pb = pbh
h.lastDroplets = make(map[string]time.Time)
h.lastpoll = time.Now()
me.hmap[pbh] = h
me.hypers = append(me.hypers, h)
log.Log(EVENT, "config new hypervisors", h.pb.Hostname)
}
// start the watchdog polling for each hypervisor
for _, h := range me.hypers {
log.Info("starting polling on", h.pb.Hostname)
// start a watchdog on each hypervisor
go h.NewWatchdog()
}
var cloud *virtigolib.CloudManager
cloud = virtigolib.NewCloud()
found, _ := cloud.FindDropletByName("www.wit.com")
if found == nil {
log.Info("d == nil")
} else {
log.Info("d == ", found)
}
// sit here
go startHTTP()
doGui()
}

View File

@ -29,7 +29,7 @@ func isClusterStable() (string, error) {
// for now, because sometimes this should write to stdout and
// sometimes to http socket, it returns a string
func Start(id string) (string, error) {
func Start(name string) (string, error) {
var result string
if s, err := isClusterStable(); err != nil {
@ -38,9 +38,9 @@ func Start(id string) (string, error) {
}
// lookup the droplet by name
d := me.cluster.FindDropletByUuid(id)
d := me.cluster.FindDropletByName(name)
if d == nil {
result = "can't start unknown droplet: " + id
result = "can't start unknown droplet: " + name
return result, errors.New(result)
}
@ -51,10 +51,6 @@ func Start(id string) (string, error) {
return result, err
}
if d.Current == nil {
d.Current = new(virtpb.Current)
}
// is the droplet already on?
if d.Current.State == virtpb.DropletState_ON {
result = "EVENT start droplet " + d.Hostname + " is already ON"
@ -70,12 +66,12 @@ func Start(id string) (string, error) {
if ok {
return result + b, nil
}
return result + b, errors.New("start " + d.Hostname + " on hypervisor " + h.pb.Hostname)
return result + b, errors.New("start " + name + " on hypervisor " + h.pb.Hostname)
}
// skip hypervisors marked inactive
if h.pb.Active != true {
result += fmt.Sprintln("hypervisor is inactive:", d.Hostname, "for", h.pb.Hostname, h.pb.Active)
result += fmt.Sprintln("hypervisor is inactive:", name, "for", h.pb.Hostname, h.pb.Active)
continue
}
@ -86,10 +82,10 @@ func Start(id string) (string, error) {
if ok {
return result + b, nil
}
return result + b, errors.New("start " + d.Hostname + " on hypervisor " + h.pb.Hostname)
return result + b, errors.New("start " + name + " on hypervisor " + h.pb.Hostname)
}
result += fmt.Sprintln("hypervisor ready:", d.Hostname, "for", h.pb.Hostname, h.pb.Active)
result += fmt.Sprintln("hypervisor ready:", name, "for", h.pb.Hostname, h.pb.Active)
pool = append(pool, h)
}
@ -110,5 +106,5 @@ func Start(id string) (string, error) {
if ok {
return result + output, nil
}
return result + output, errors.New("start " + d.Hostname + " on hypervisor " + h.pb.Hostname)
return result + output, errors.New("start " + name + " on hypervisor " + h.pb.Hostname)
}

View File

@ -1,17 +1,13 @@
package main
import (
"net/url"
"time"
"go.wit.com/dev/alexflint/arg"
"go.wit.com/gui"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/gui/prep"
"go.wit.com/lib/protobuf/virtpb"
)
var me *virtigoT
var me virtigoT
// disable the GUI
func (b *virtigoT) Disable() {
@ -25,8 +21,8 @@ func (b *virtigoT) Enable() {
// this app's variables
type virtigoT struct {
pp *arg.Parser // go-arg parser
myGui *prep.GuiPrep // the gui toolkit handle
cluster *virtpb.Cluster // basic cluster settings
myGui *gui.Node // the gui toolkit handle
e *virtpb.Events // virt protobuf events
hmap map[*virtpb.Hypervisor]*HyperT // map to the local struct
names []string // ?
@ -40,21 +36,18 @@ type virtigoT struct {
missingDropletTimeout time.Duration // how long a droplet can be missing for
status *gui.Node // the cluster status
lastuptime *gui.Node // the last time uptime was checked by Kuma
clusters *virtpb.Clusters // clusters protobuf
cmap map[*virtpb.Cluster]*adminT // map to local GUI objects and the protobuf
gwin *gadgets.GenericWindow // main window
cluster *virtpb.OldCluster // basic cluster settings
// admin *adminT // the admin struct
admin *adminT // the admin struct
}
// cluster "admin" mode
type adminT struct {
cluster *virtpb.Cluster // the cluster protobuf
uptime *gui.Node // the uptime message
dwin *stdDropletTableWin // the droplet window
hwin *stdHypervisorTableWin // the hypervisor window
ewin *stdEventTableWin // the events window
url *url.URL // URL for the cloud
// admin mode
droplets *virtpb.Droplets // your droplets
hypervisors *virtpb.Hypervisors // yep
events *virtpb.Events // yep
uptime *gui.Node // the uptime message
dwin *stdDropletTableWin // the droplet window
hwin *stdHypervisorTableWin // the hypervisor window
ewin *stdEventTableWin // the events window
}
// the stuff that is needed for a hypervisor

View File

@ -15,6 +15,7 @@ package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
@ -162,6 +163,40 @@ func ValidateDiskFilenames() ([]*virtpb.Event, error) {
return alle, nil
}
func getNewMac() string {
// mac address map to check for duplicates
var macs map[string]string
macs = make(map[string]string)
loop := me.cluster.DropletsAll() // get the list of droplets
for loop.Scan() {
d := loop.Next()
for _, n := range d.Networks {
// log.Println("network:", n.Mac, d.Uuid, d.Hostname)
if _, ok := macs[n.Mac]; ok {
// UUID already exists
log.Info("duplicate MAC", n.Mac, macs[n.Mac])
log.Info("duplicate MAC", n.Mac, d.Hostname)
return ""
}
macs[n.Mac] = d.Hostname
}
}
var i int = 9
var mac string
for {
mac = fmt.Sprintf("22:22:22:22:22:%02d", i)
if _, ok := macs[mac]; ok {
log.Info("MAC already defined", mac, macs[mac])
i += 1
continue
}
log.Info("using new MAC:", mac)
return mac
}
}
// consistancy check. run on a regular basis
//
// runs on startup. dies if there are duplicates

View File

@ -1,33 +0,0 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"go.wit.com/lib/gadgets"
"go.wit.com/log"
)
func createWindow() *gadgets.GenericWindow {
createWindow := gadgets.NewGenericWindow("Create Droplet", "settings")
createWindow.Custom = func() {
log.Warn("create window close")
}
grid := createWindow.Group.RawGrid()
gadgets.NewBasicEntry(grid, "memory")
grid.NextRow()
grid.NewLabel("name")
grid.NewTextbox("something")
grid.NextRow()
grid.NewButton("Start", func() {
log.Info("make a box")
})
return createWindow
}

View File

@ -1,121 +0,0 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"fmt"
"strconv"
"go.wit.com/gui"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/log"
)
func (admin *adminT) createDropletWindow() *gadgets.GenericWindow {
d := new(virtpb.Droplet)
win := gadgets.NewGenericWindow("Create Droplet "+d.Hostname, "settings")
win.Custom = func() {
log.Warn("edit window close")
}
grid := win.Group.RawGrid()
var save *gui.Node
grid.NewLabel("name")
name := grid.NewTextbox("new2.wit.com")
d.Hostname = "new2.wit.com"
name.SetText(d.Hostname)
name.Custom = func() {
if d.Hostname == name.String() {
return
}
d.Hostname = name.String()
log.Info("changed droplet name to", d.Hostname)
save.Enable()
}
grid.NextRow()
mem := gadgets.NewBasicEntry(grid, "memory (GB)")
mem.SetText("16")
d.Memory = int64(16 * 1024 * 2024 * 1024)
grid.NextRow()
mem.Custom = func() {
newmem, err := strconv.Atoi(mem.String())
if err != nil {
log.Info("mem value error", mem.String(), err)
mem.SetText(fmt.Sprintf("%d", d.Memory/(1024*1024*1024)))
return
}
if newmem < 1 {
log.Info("mem can not be < 1")
mem.SetText(fmt.Sprintf("%d", d.Memory/(1024*1024*1024)))
return
}
d.Memory = int64(newmem * (1024 * 2024 * 1024))
log.Info("changed mem value. new val =", d.Memory)
save.Enable()
}
grid.NextRow() // each entry is on it's own row
cpus := gadgets.NewBasicEntry(grid, "cpus")
cpus.SetText("4")
d.Cpus = int64(4)
cpus.Custom = func() {
newcpu, err := strconv.Atoi(cpus.String())
if err != nil {
log.Info("cpus value error", cpus.String(), err)
cpus.SetText(fmt.Sprintf("%d", d.Cpus))
return
}
if newcpu < 1 {
log.Info("cpus can not be < 1")
cpus.SetText(fmt.Sprintf("%d", d.Cpus))
return
}
d.Cpus = int64(newcpu)
log.Info("changed cpus value. new val =", d.Cpus)
save.Enable()
}
grid.NextRow() // each entry is on it's own row
/*
save = grid.NewButton("postEvent() EDIT", func() {
log.Info("save droplet changes here")
e := new(virtpb.Event)
e.Etype = virtpb.EventType_EDIT
e.Droplet = d
if err := admin.postEvent(e); err != nil {
log.Info("event edit err", err)
} else {
log.Info("admin.postEvent() worked (?)")
}
})
*/
save = grid.NewButton("Create", func() {
log.Info("save droplet changes here")
e := new(virtpb.Event)
e.Etype = virtpb.EventType_ADD
e.Droplet = d
if err := admin.postEvent(e); err != nil {
log.Info("event edit err", err)
} else {
log.Info("admin.postEvent() worked (?)")
}
})
// save.Disable()
return win
}

View File

@ -1,125 +0,0 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"fmt"
"strconv"
"go.wit.com/gui"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/virtpb"
"go.wit.com/log"
)
func (admin *adminT) editDropletWindow(d *virtpb.Droplet) *gadgets.GenericWindow {
win := gadgets.NewGenericWindow("Edit Droplet "+d.Hostname, "settings")
win.Custom = func() {
log.Warn("edit window close")
}
grid := win.Group.RawGrid()
var save *gui.Node
grid.NewLabel("name")
name := grid.NewTextbox("something")
name.SetText(d.Hostname)
name.Custom = func() {
if d.Hostname == name.String() {
return
}
d.Hostname = name.String()
log.Info("changed droplet name to", d.Hostname)
save.Enable()
}
grid.NextRow()
mem := gadgets.NewBasicEntry(grid, "memory (GB)")
mem.SetText(fmt.Sprintf("%d", d.Memory/(1024*1024*1024)))
grid.NextRow()
mem.Custom = func() {
newmem, err := strconv.Atoi(mem.String())
if err != nil {
log.Info("mem value error", mem.String(), err)
mem.SetText(fmt.Sprintf("%d", d.Memory/(1024*1024*1024)))
return
}
if newmem < 1 {
log.Info("mem can not be < 1")
mem.SetText(fmt.Sprintf("%d", d.Memory/(1024*1024*1024)))
return
}
d.Memory = int64(newmem * (1024 * 2024 * 1024))
log.Info("changed mem value. new val =", d.Memory)
save.Enable()
}
cpus := gadgets.NewBasicEntry(grid, "cpus")
cpus.SetText(fmt.Sprintf("%d", d.Cpus))
grid.NextRow()
cpus.Custom = func() {
newcpu, err := strconv.Atoi(cpus.String())
if err != nil {
log.Info("cpus value error", cpus.String(), err)
cpus.SetText(fmt.Sprintf("%d", d.Cpus))
return
}
if newcpu < 1 {
log.Info("cpus can not be < 1")
cpus.SetText(fmt.Sprintf("%d", d.Cpus))
return
}
d.Cpus = int64(newcpu)
log.Info("changed cpus value. new val =", d.Cpus)
save.Enable()
}
grid.NewLabel("hypervisor")
hyper := grid.NewDropdown()
hyper.AddText("farm03")
hyper.AddText("farm04")
hyper.AddText("farm05")
if d.Current != nil {
hyper.SetText(d.Current.Hypervisor)
} else {
hyper.SetText("farm03")
}
grid.NextRow()
grid.NewButton("Start", func() {
log.Info("make a box")
})
save = grid.NewButton("save", func() {
log.Info("save droplet changes here")
e := new(virtpb.Event)
e.Etype = virtpb.EventType_EDIT
e.Droplet = d
/*
e.Droplet = new(virtpb.Droplet)
e.Droplet.Uuid = d.Uuid
e.Droplet.Cpus = 4
e.Droplet.Memory = 8 * (1024 * 1024 * 1024)
e.Droplet.Hostname = name.String()
*/
if err := admin.postEvent(e); err != nil {
log.Info("event edit err", err)
}
})
save.Disable()
grid.NewButton("dump", func() {
t := d.FormatTEXT()
log.Info(t)
})
return win
}

View File

@ -23,7 +23,6 @@ type stdDropletTableWin struct {
TB *virtpb.DropletsTable // the gui table buffer
update bool // if the window should be updated
Close func() // this function is called when the window is closed
admin *adminT
}
func (w *stdDropletTableWin) Toggle() {
@ -36,9 +35,8 @@ func (w *stdDropletTableWin) Toggle() {
w.win.Toggle()
}
func newDropletsWindow(admin *adminT) *stdDropletTableWin {
func newDropletsWindow() *stdDropletTableWin {
dwin := new(stdDropletTableWin)
dwin.admin = admin
dwin.win = gadgets.NewGenericWindow("virtigo current droplets", "Options")
dwin.win.Custom = func() {
log.Info("test delete window here")
@ -48,7 +46,7 @@ func newDropletsWindow(admin *adminT) *stdDropletTableWin {
grid.NewButton("Active", func() {
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
all := me.admin.droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current.State != virtpb.DropletState_ON {
@ -62,7 +60,7 @@ func newDropletsWindow(admin *adminT) *stdDropletTableWin {
grid.NewButton("Inactive", func() {
var found *virtpb.Droplets
found = virtpb.NewDroplets()
all := admin.cluster.Droplets.All()
all := me.admin.droplets.All()
for all.Scan() {
vm := all.Next()
if vm.Current.State == virtpb.DropletState_ON {
@ -73,16 +71,65 @@ func newDropletsWindow(admin *adminT) *stdDropletTableWin {
dwin.doInactiveDroplets(found)
})
grid.NewButton("Create", func() {
log.Info("create droplet here")
admin.createDropletWindow()
})
// make a box at the bottom of the window for the protobuf table
dwin.box = dwin.win.Bottom.Box().SetProgName("TBOX")
return dwin
}
/*
func makeWindownDropletsPB(pb *virtpb.Droplets) *stdDropletTableWin {
dwin := new(stdDropletTableWin)
dwin.win = gadgets.NewGenericWindow("virtigo current droplets", "")
dwin.win.Custom = func() {
log.Info("test delete window here")
}
// make a box at the bottom of the window for the protobuf table
dwin.box = dwin.win.Bottom.Box().SetProgName("TBOX")
dwin.doDropletsTable(pb)
return dwin
}
*/
/*
func (dwin *stdDropletTableWin) doDropletsTable(currentDroplets *virtpb.Droplets) {
dwin.Lock()
defer dwin.Unlock()
if dwin.TB != nil {
dwin.TB.Delete()
dwin.TB = nil
}
// display the protobuf
dwin.TB = addDropletsPB(dwin.box, currentDroplets)
f := func(e *virtpb.Droplet) {
log.Info("Triggered. do something here", e.Hostname)
// m.Enabled = true
}
dwin.TB.Custom(f)
}
func addDropletsPB(tbox *gui.Node, pb *virtpb.Droplets) *virtpb.DropletsTable {
t := pb.NewTable("DropletsPB")
t.NewUuid()
t.SetParent(tbox)
vp := t.AddButtonFunc("start", func(p *virtpb.Droplet) string {
return "poweron"
})
vp.Custom = func(d *virtpb.Droplet) {
log.Info("power on the droplet here:", d.Hostname)
}
t.AddHostname()
t.AddMemory()
t.AddCpus()
t.ShowTable()
return t
}
*/
// default window for active running droplets
func (dw *stdDropletTableWin) doInactiveDroplets(pb *virtpb.Droplets) {
dw.Lock()
@ -100,37 +147,32 @@ func (dw *stdDropletTableWin) doInactiveDroplets(pb *virtpb.Droplets) {
t.NewUuid()
t.SetParent(dw.box)
dropedit := t.AddButtonFunc("Edit", func(d *virtpb.Droplet) string {
return "edit"
})
dropedit.Custom = func(d *virtpb.Droplet) {
log.Info("edit droplet here", d.Hostname)
dw.admin.editDropletWindow(d)
}
// pick the columns
// t.AddHostname()
dropon := t.AddButtonFunc("Start", func(d *virtpb.Droplet) string {
return "poweron"
})
dropon.Custom = func(d *virtpb.Droplet) {
log.Info("start droplet here", d.Hostname)
log.Info("should start droplet here")
log.Info(d.SprintHeader())
e := new(virtpb.Event)
e.Etype = virtpb.EventType_POWERON
e.DropletUuid = d.Uuid
if err := dw.admin.postEvent(e); err != nil {
log.Info("droplet start err", err)
}
}
vp := t.AddButtonFunc("Verify Config", func(p *virtpb.Droplet) string {
/*
t.AddHostname()
t.AddStringFunc("location", func(d *virtpb.Droplet) string {
return d.Current.Hypervisor
})
*/
vp := t.AddButtonFunc("Configure Hostname", func(p *virtpb.Droplet) string {
return p.Hostname
})
vp.Custom = func(d *virtpb.Droplet) {
log.Info("open config window", d.Hostname)
}
/*
t.AddHostname()
t.AddStringFunc("location", func(d *virtpb.Droplet) string {
return d.Current.Hypervisor
})
*/
t.AddMemory()
t.AddCpus()
@ -178,15 +220,7 @@ func (dw *stdDropletTableWin) doActiveDroplets(pb *virtpb.Droplets) {
log.Info("data", string(data), "err =", err)
}
// t.AddHostname()
vp := t.AddButtonFunc("Hostname", func(p *virtpb.Droplet) string {
return p.Hostname
})
vp.Custom = func(d *virtpb.Droplet) {
log.Info("edit droplet here", d.Hostname)
dw.admin.editDropletWindow(d)
}
t.AddHostname()
t.AddStringFunc("location", func(d *virtpb.Droplet) string {
return d.Current.Hypervisor
})

View File

@ -61,7 +61,7 @@ func (dw *stdEventTableWin) doStdEvents(pb *virtpb.Events) {
t.SetParent(dw.box)
// pick the columns
t.AddDropletName()
t.AddDroplet()
t.AddDropletUuid()
t.AddHypervisor()

107
windowGeneric.go Normal file
View File

@ -0,0 +1,107 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// This model works for 99.9% of all windows
// This is the Default Standard Window Model
import (
"go.wit.com/lib/gadgets"
"go.wit.com/log"
"go.wit.com/gui"
)
type GenericWindow struct {
Win *gadgets.BasicWindow // the window widget itself
Shelf *gui.Node // the overall box: the shelf
Stack *gui.Node // the first box is a stack
Top *gui.Node // the first item in the stack is always a shelf like box
Group *gui.Node // the first item top box is always a group
Middle *gui.Node // the middle box (shelf style)
Bottom *gui.Node // the bottom box (stack style)
}
func (gw *GenericWindow) Hidden() bool {
if gw == nil {
return true
}
if gw.Win == nil {
return true
}
return gw.Win.Hidden()
}
func (gw *GenericWindow) Toggle() {
if gw.Hidden() {
gw.Show()
} else {
gw.Hide()
}
}
func (gw *GenericWindow) Show() {
if gw == nil {
return
}
if gw.Win == nil {
return
}
gw.Win.Show()
}
func (gw *GenericWindow) Hide() {
if gw == nil {
return
}
if gw.Win == nil {
return
}
gw.Win.Hide()
}
func (gw *GenericWindow) Disable() {
if gw == nil {
return
}
if gw.Shelf == nil {
return
}
gw.Shelf.Disable()
}
func (gw *GenericWindow) Enable() {
if gw == nil {
return
}
if gw.Shelf == nil {
return
}
gw.Shelf.Enable()
}
func NewGenericWindow(title string, grouptxt string) *GenericWindow {
gw := new(GenericWindow)
gw.Win = gadgets.RawBasicWindow(title)
gw.Win.Make()
gw.Win.Custom = func() {
log.Warn("Found Window close. setting hidden=true")
// sets the hidden flag to false so Toggle() works
gw.Win.Hide()
}
gw.Shelf = gw.Win.Box()
// gw.Shelf.Vertical().SetProgName("ShelfBox")
gw.Stack = gw.Shelf.NewVerticalBox("Stackbox")
gw.Top = gw.Stack.NewVerticalBox("Stackbox")
gw.Middle = gw.Stack.Box()
gw.Bottom = gw.Stack.Box()
gw.Group = gw.Top.NewGroup(grouptxt)
gw.Show()
return gw
}

View File

@ -4,7 +4,6 @@
package main
import (
"fmt"
"sync"
"time"
@ -104,60 +103,3 @@ func (dw *stdHypervisorTableWin) doStdHypervisors(pb *virtpb.Hypervisors) {
dw.TB.ShowTable()
}
// default table protobuf window
func (dw *stdHypervisorTableWin) doNewStdHypervisors(pb *virtpb.Hypervisors) {
dw.Lock()
defer dw.Unlock()
// erase the old table
if dw.TB != nil {
dw.TB.Delete()
dw.TB = nil
}
// init the table
dw.pb = pb
t := dw.pb.NewTable("HypervisorsPB Off")
t.NewUuid()
t.SetParent(dw.box)
// pick the columns
t.AddHostname()
t.AddMemory()
t.AddCpus()
t.AddKillcount()
t.AddTimeFunc("last poll", func(h *virtpb.Hypervisor) time.Time {
// hm := me.hmap[h]
// tmp := hm.lastpoll
// log.Info("poll age", h.Hostname, virtpb.FormatDuration(time.Since(tmp)))
return time.Now()
})
t.AddStringFunc("droplets", func(h *virtpb.Hypervisor) string {
var totalDroplets int
var totalUnknownDroplets int
// dur := time.Since(h.lastpoll)
// tmp := virtpb.FormatDuration(dur)
// fmt.Fprintln(w, h.pb.Hostname, "killcount =", h.killcount, "lastpoll:", tmp)
hm := me.hmap[h]
for name, _ := range hm.lastDroplets {
totalDroplets += 1
d := me.cluster.FindDropletByName(name)
if d == nil {
totalUnknownDroplets += 1
}
}
// log.Printf("Total Droplets %d total libvirt only droplets = %d\n", totalDroplets, totalUnknownDroplets)
return fmt.Sprintf("%d", totalDroplets)
})
// display the protobuf
dw.TB = t
f := func(e *virtpb.Hypervisor) {
log.Info("std HypervisorWindow() something here", e.Hostname)
// m.Enabled = true
}
dw.TB.Custom(f)
dw.TB.ShowTable()
}