├── main.go ├── pkg └── vctui │ ├── uiError.go │ ├── uiTypes.go │ ├── hack.go │ ├── uiSearch.go │ ├── uiPower.go │ ├── uiDeploy.go │ ├── internals.go │ ├── builder.go │ ├── uiNew.go │ └── ui.go ├── Dockerfile ├── go.mod ├── Makefile ├── cmd └── vctui.go ├── Readme.md └── go.sum /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/thebsdbox/vctui/cmd" 4 | 5 | // Version is populated from the Makefile and is tied to the release TAG 6 | var Version string 7 | 8 | // Build is the last GIT commit 9 | var Build string 10 | 11 | func main() { 12 | cmd.Release.Version = Version 13 | cmd.Release.Build = Build 14 | cmd.Execute() 15 | } 16 | -------------------------------------------------------------------------------- /pkg/vctui/uiError.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import "github.com/rivo/tview" 4 | 5 | func errorUI(err error) { 6 | uiBugFix() 7 | app := tview.NewApplication() 8 | button := tview.NewButton(err.Error()).SetSelectedFunc(func() { 9 | app.Stop() 10 | }) 11 | button.SetBorder(true).SetRect(2, 5, 78, 5) 12 | if err := app.SetRoot(button, false).Run(); err != nil { 13 | panic(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/vctui/uiTypes.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "github.com/vmware/govmomi/object" 5 | ) 6 | 7 | type reference struct { 8 | objectType string 9 | objectDetails string 10 | vm *object.VirtualMachine 11 | } 12 | 13 | var vmTypes = []string{"otherLinux64Guest", 14 | "ubuntu64Guest", 15 | "rhel7_64Guest", 16 | "vmkernel65Guest", 17 | "windows9_64Guest", 18 | "vmwarePhoton64Guest"} 19 | 20 | var deplopyTypes = []string{"reboot", 21 | "preseed", 22 | "kickstart", 23 | "pull", 24 | "push"} 25 | -------------------------------------------------------------------------------- /pkg/vctui/hack.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "github.com/micmonay/keybd_event" 5 | ) 6 | 7 | // uiBugFix - This is used to fix the issue with tcell dropping a keystroke between new tcell.screens being created 8 | // TODO (thebsdbox) remove this when tcell issue #194 is fixed 9 | func uiBugFix() { 10 | kb, err := keybd_event.NewKeyBonding() 11 | if err != nil { 12 | return 13 | } 14 | 15 | //set keys 16 | kb.SetKeys(keybd_event.VK_SPACE) 17 | 18 | //launch 19 | kb.Launching() 20 | //fmt.Printf("\033[2J") 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | 3 | # Build vctui 4 | FROM golang:1.14-alpine as dev 5 | # add make / gcc (CGO_ENABLED) 6 | RUN apk add --no-cache git ca-certificates make gcc linux-headers musl-dev 7 | COPY . /go/src/github.com/thebsdbox/vctui 8 | WORKDIR /go/src/github.com/thebsdbox/vctui 9 | ENV GO111MODULE=on 10 | RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \ 11 | --mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \ 12 | CGO_ENABLED=1 GOOS=linux make build 13 | 14 | FROM scratch 15 | COPY --from=dev /go/src/github.com/thebsdbox/vctui/vctui /bin/vctui 16 | ENTRYPOINT ["/bin/vctui"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thebsdbox/vctui 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a // indirect 7 | github.com/gdamore/tcell v1.3.0 8 | github.com/gorilla/mux v1.7.4 // indirect 9 | github.com/hooklift/iso9660 v1.0.0 // indirect 10 | github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 // indirect 11 | github.com/micmonay/keybd_event v1.0.0 12 | github.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4 // indirect 13 | github.com/plunder-app/plunder/pkg/apiserver v0.0.0-20200513203243-eccb418a5255 // indirect 14 | github.com/plunder-app/plunder/pkg/services v0.0.0-20200513203243-eccb418a5255 15 | github.com/plunder-app/plunder/pkg/utils v0.0.0-20200513203243-eccb418a5255 // indirect 16 | github.com/rivo/tview v0.0.0-20200507165325-823f280c5426 17 | github.com/sirupsen/logrus v1.6.0 18 | github.com/spf13/cobra v1.0.0 19 | github.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c // indirect 20 | github.com/thebsdbox/kuiz v0.0.0-20200327155153-b933733cc267 21 | github.com/vmware/govmomi v0.22.2 22 | github.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /pkg/vctui/uiSearch.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/rivo/tview" 7 | "github.com/vmware/govmomi/object" 8 | ) 9 | 10 | //SearchUI - this will provide a user interface for finding articles based upon a keyword entered in the UI 11 | func SearchUI(v []*object.VirtualMachine) (string, []*object.VirtualMachine) { 12 | 13 | uiBugFix() 14 | 15 | title := "Search" 16 | label := "Search string (RegEx)" 17 | for { 18 | app := tview.NewApplication() 19 | 20 | form := tview.NewForm(). 21 | AddInputField(label, "", 30, nil, nil). 22 | AddButton("Search", func() { app.Stop() }) 23 | 24 | form.SetBorder(true).SetTitle(title).SetTitleAlign(tview.AlignLeft) 25 | 26 | if err := app.SetRoot(form, true).SetFocus(form).Run(); err != nil { 27 | panic(err) 28 | } 29 | 30 | searchString := form.GetFormItemByLabel(label).(*tview.InputField).GetText() 31 | 32 | newVMs, err := searchVMS(searchString, v) 33 | if err == nil { 34 | return searchString, newVMs 35 | } 36 | title = err.Error() 37 | } 38 | } 39 | 40 | func searchVMS(searchString string, v []*object.VirtualMachine) ([]*object.VirtualMachine, error) { 41 | 42 | var newVMList []*object.VirtualMachine 43 | var err error 44 | for x := range v { 45 | matched, err := regexp.MatchString(searchString, v[x].Name()) 46 | if err != nil { 47 | break 48 | } 49 | // If the regex matches then add it to the new subset 50 | if matched == true { 51 | newVMList = append(newVMList, v[x]) 52 | } 53 | } 54 | if err == nil { 55 | return newVMList, nil 56 | } 57 | return nil, err 58 | } 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SHELL := /bin/sh 3 | 4 | # The name of the executable (default is current directory name) 5 | TARGET := vctui 6 | .DEFAULT_GOAL: $(TARGET) 7 | 8 | # These will be provided to the target 9 | VERSION := 0.1.0 10 | BUILD := `git rev-parse HEAD` 11 | 12 | # Operating System Default (LINUX) 13 | TARGETOS=linux 14 | 15 | # Use linker flags to provide version/build settings to the target 16 | #STATIC flags for (scratch and CGO_Enabled) 17 | LDFLAGS=-ldflags "-s -w -X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -linkmode external -extldflags '-static'" 18 | 19 | # go source files, ignore vendor directory 20 | SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*") 21 | 22 | REPO ?= thebsdbox 23 | DOCKERREPO ?= $(REPO) 24 | DOCKERTAG ?= $(VERSION) 25 | 26 | .PHONY: all build clean install uninstall fmt simplify check run 27 | 28 | all: check install 29 | 30 | $(TARGET): $(SRC) 31 | @go build $(LDFLAGS) -o $(TARGET) 32 | 33 | build: $(TARGET) 34 | @true 35 | 36 | clean: 37 | @rm -f $(TARGET) 38 | 39 | install: 40 | @echo Building and Installing project 41 | @go install $(LDFLAGS) 42 | 43 | uninstall: clean 44 | @rm -f $$(which ${TARGET}) 45 | 46 | fmt: 47 | @gofmt -l -w $(SRC) 48 | 49 | dockerx86: 50 | @docker buildx build --platform linux/amd64 --load -t $(DOCKERREPO)/$(TARGET):$(DOCKERTAG) -f Dockerfile . 51 | @echo New Multi Architecture Docker image created 52 | 53 | docker: 54 | @docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push -t $(DOCKERREPO)/$(TARGET):$(DOCKERTAG) -f Dockerfile . 55 | @echo New Multi Architecture Docker image created 56 | 57 | simplify: 58 | @gofmt -s -l -w $(SRC) 59 | 60 | check: 61 | @test -z $(shell gofmt -l main.go | tee /dev/stderr) || echo "[WARN] Fix formatting issues with 'make fmt'" 62 | @for d in $$(go list ./... | grep -v /vendor/); do golint $${d}; done 63 | @go tool vet ${SRC} 64 | 65 | run: install 66 | @$(TARGET) 67 | -------------------------------------------------------------------------------- /pkg/vctui/uiPower.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gdamore/tcell" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | const ( 11 | powerOn = iota 12 | powerOff 13 | guestPowerOff 14 | guestReboot 15 | suspend 16 | reset 17 | netPowerOn 18 | diskPowerOn 19 | ) 20 | 21 | var cancel bool 22 | var app *tview.Application 23 | 24 | // RadioButtons implements a simple primitive for radio button selections. 25 | type RadioButtons struct { 26 | *tview.Box 27 | options []string 28 | currentOption int 29 | } 30 | 31 | // NewRadioButtons returns a new radio button primitive. 32 | func NewRadioButtons(options []string) *RadioButtons { 33 | return &RadioButtons{ 34 | Box: tview.NewBox(), 35 | options: options, 36 | } 37 | } 38 | 39 | // Draw draws this primitive onto the screen. 40 | func (r *RadioButtons) Draw(screen tcell.Screen) { 41 | r.Box.Draw(screen) 42 | x, y, width, height := r.GetInnerRect() 43 | 44 | for index, option := range r.options { 45 | if index >= height { 46 | break 47 | } 48 | radioButton := "\u25ef" // Unchecked. 49 | if index == r.currentOption { 50 | radioButton = "\u25c9" // Checked. 51 | } 52 | line := fmt.Sprintf(`%s[white] %s`, radioButton, option) 53 | tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) 54 | } 55 | } 56 | 57 | // InputHandler returns the handler for this primitive. 58 | func (r *RadioButtons) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { 59 | return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { 60 | switch event.Key() { 61 | case tcell.KeyUp: 62 | r.currentOption-- 63 | if r.currentOption < 0 { 64 | r.currentOption = 0 65 | } 66 | case tcell.KeyDown: 67 | r.currentOption++ 68 | if r.currentOption >= len(r.options) { 69 | r.currentOption = len(r.options) - 1 70 | } 71 | case tcell.KeyEnter: 72 | app.Stop() 73 | case tcell.KeyCtrlC: 74 | app.Stop() 75 | cancel = true 76 | } 77 | 78 | }) 79 | } 80 | 81 | func powerui() int { 82 | 83 | uiBugFix() 84 | 85 | app = tview.NewApplication() 86 | cancel = false 87 | radioButtons := NewRadioButtons([]string{"Power On", "Power Off", "Guest Power Off (tools required)", "Guest Reboot (tools required)", "Suspend", "Reset", "PXE Boot (one-shot)", "Power On (Disk Boot)"}) 88 | 89 | radioButtons.SetBorder(true). 90 | SetTitle("Set the power state for this VM"). 91 | SetRect(20, 5, 40, 10) 92 | 93 | if err := app.SetRoot(radioButtons, false).Run(); err != nil { 94 | panic(err) 95 | } 96 | // Return a value outside of the constants 97 | if cancel == true { 98 | return -1 99 | } 100 | 101 | return radioButtons.currentOption 102 | } 103 | -------------------------------------------------------------------------------- /pkg/vctui/uiDeploy.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/plunder-app/plunder/pkg/services" 11 | "github.com/rivo/tview" 12 | ) 13 | 14 | func deployOnVM(address, hostname string) { 15 | uiBugFix() 16 | 17 | app := tview.NewApplication() 18 | 19 | form := tview.NewForm() 20 | form.AddInputField("Plunder Address", "http://localhost", 40, nil, nil). 21 | AddInputField("MAC Address", address, 18, nil, nil). 22 | AddInputField("Hostname", hostname, 40, nil, nil). 23 | AddInputField("IP Address", "", 18, nil, nil). 24 | AddDropDown("Deployment Type", deplopyTypes, 0, nil). 25 | AddButton("Save Settings", func() { app.Stop() }) 26 | 27 | form.SetBorder(true). 28 | SetTitle("Update deployment"). 29 | SetTitleAlign(tview.AlignCenter). 30 | SetRect(5, 1, 60, 23) 31 | 32 | if err := app.SetRoot(form, false).Run(); err != nil { 33 | panic(err) 34 | } 35 | var deployMac, deployHost, deployIP, deployType string 36 | 37 | plunderURL := form.GetFormItemByLabel("Plunder Address").(*tview.InputField).GetText() 38 | deployMac = form.GetFormItemByLabel("MAC Address").(*tview.InputField).GetText() 39 | deployHost = form.GetFormItemByLabel("Hostname").(*tview.InputField).GetText() 40 | deployIP = form.GetFormItemByLabel("IP Address").(*tview.InputField).GetText() 41 | _, deployType = form.GetFormItemByLabel("Deployment Type").(*tview.DropDown).GetCurrentOption() 42 | 43 | currentConfig, err := getConfig(plunderURL) 44 | if err != nil { 45 | errorUI(err) 46 | return 47 | } 48 | // Check for existing deployment 49 | var updatedExisting bool 50 | for i := range currentConfig.Configs { 51 | if currentConfig.Configs[i].MAC == deployMac { 52 | currentConfig.Configs[i].ConfigName = deployType 53 | currentConfig.Configs[i].ConfigHost.IPAddress = deployIP 54 | currentConfig.Configs[i].ConfigHost.ServerName = deployHost 55 | updatedExisting = true 56 | } 57 | } 58 | 59 | // If we've not updated the existing then it's a new entry 60 | if updatedExisting == false { 61 | newDeployment := services.DeploymentConfig{ 62 | MAC: deployMac, 63 | ConfigName: deployType, 64 | ConfigHost: services.HostConfig{ 65 | IPAddress: deployIP, 66 | ServerName: deployHost, 67 | }, 68 | } 69 | currentConfig.Configs = append(currentConfig.Configs, newDeployment) 70 | } 71 | 72 | // Update the deployment server 73 | err = postConfig(plunderURL, currentConfig) 74 | if err != nil { 75 | errorUI(err) 76 | return 77 | } 78 | } 79 | 80 | func getConfig(plunderURL string) (*services.DeploymentConfigurationFile, error) { 81 | u, err := url.Parse(plunderURL) 82 | if err != nil { 83 | return nil, err 84 | } 85 | u.Path = "/deployment" 86 | 87 | resp, err := http.Get(u.String()) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | var config services.DeploymentConfigurationFile 93 | defer resp.Body.Close() 94 | b, err := ioutil.ReadAll(resp.Body) 95 | if err != nil { 96 | return nil, err 97 | } 98 | err = json.Unmarshal(b, &config) 99 | if err != nil { 100 | return nil, err 101 | } 102 | return &config, nil 103 | } 104 | 105 | func postConfig(plunderURL string, config *services.DeploymentConfigurationFile) error { 106 | u, err := url.Parse(plunderURL) 107 | if err != nil { 108 | return err 109 | } 110 | u.Path = "/deployment" 111 | 112 | jsonValue, _ := json.Marshal(config) 113 | 114 | _, err = http.Post(u.String(), "application/json", bytes.NewBuffer(jsonValue)) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /cmd/vctui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strconv" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/thebsdbox/vctui/pkg/vctui" 12 | "github.com/vmware/govmomi" 13 | 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type vc struct { 18 | address string 19 | username string 20 | password string 21 | insecure bool 22 | } 23 | 24 | var vCenterDetails vc 25 | 26 | // Release - this struct contains the release information populated when building katbox 27 | var Release struct { 28 | Version string 29 | Build string 30 | } 31 | var logLevel int 32 | 33 | func init() { 34 | 35 | // VMware vCenter details 36 | vctuiCmd.Flags().StringVar(&vCenterDetails.address, "address", os.Getenv("VCURL"), "The Url/address of a VMware vCenter server") 37 | vctuiCmd.Flags().StringVar(&vCenterDetails.username, "user", os.Getenv("VCUSER"), "The Url/address of a VMware vCenter server") 38 | vctuiCmd.Flags().StringVar(&vCenterDetails.password, "pass", os.Getenv("VCPASS"), "The Url/address of a VMware vCenter server") 39 | vctuiCmd.Flags().BoolVar(&vCenterDetails.insecure, "insecure", false, "The Url/address of a VMware vCenter server") 40 | 41 | vctuiCmd.PersistentFlags().IntVar(&logLevel, "logLevel", 5, "Set the logging level [0=panic, 3=warning, 5=debug]") 42 | vctuiCmd.AddCommand(vctuiVersion) 43 | log.SetLevel(log.Level(logLevel)) 44 | } 45 | 46 | func parseCredentials(v *vc) (*url.URL, error) { 47 | 48 | // Check that an address was actually entered 49 | if v.address == "" { 50 | return nil, fmt.Errorf("No VMware vCenter URL/Address has been submitted") 51 | } 52 | 53 | // Check that the URL can be parsed 54 | u, err := url.Parse(v.address) 55 | if err != nil { 56 | return nil, fmt.Errorf("URL can't be parsed, ensure it is https://username:password/
/sdk") 57 | } 58 | 59 | // Check if a username was entered 60 | if v.username == "" { 61 | // if no username does one exist as part of the url 62 | if u.User.Username() == "" { 63 | return nil, fmt.Errorf("No VMware vCenter Username has been submitted") 64 | } 65 | } else { 66 | // A username was submitted update the url 67 | u.User = url.User(v.username) 68 | } 69 | 70 | if v.password == "" { 71 | _, set := u.User.Password() 72 | if set == false { 73 | return nil, fmt.Errorf("No VMware vCenter Password has been submitted") 74 | } 75 | } else { 76 | u.User = url.UserPassword(u.User.Username(), v.password) 77 | } 78 | return u, nil 79 | } 80 | 81 | var vctuiCmd = &cobra.Command{ 82 | Use: "vctui", 83 | Short: "VMware vCenter Text User Interface", 84 | Run: func(cmd *cobra.Command, args []string) { 85 | log.SetLevel(log.Level(logLevel)) 86 | 87 | u, err := parseCredentials(&vCenterDetails) 88 | if err != nil { 89 | log.Fatalf("%s", err.Error()) 90 | } 91 | 92 | ctx, cancel := context.WithCancel(context.Background()) 93 | defer cancel() 94 | 95 | c, err := govmomi.NewClient(ctx, u, true) 96 | if err != nil { 97 | log.Fatalf("%s", err.Error()) 98 | } 99 | 100 | defer c.Logout(ctx) 101 | 102 | vms, err := vctui.VMInventory(c, true) 103 | if err != nil { 104 | log.Fatalf("%s", err.Error()) 105 | } 106 | 107 | if len(vms) == 0 { 108 | log.Fatalf("No Virtual Machines to display") 109 | } 110 | 111 | // Escape code to clear screen (https://godoc.org/github.com/pborman/ansi) 112 | fmt.Printf("\033[2J") 113 | vctui.MainUI(vms, c) 114 | // Escape code to clear screen (tidy up any artifacts generated by the UI hacks) 115 | fmt.Printf("\033[2J") 116 | 117 | return 118 | }, 119 | } 120 | 121 | // Execute - starts the command parsing process 122 | func Execute() { 123 | if os.Getenv("VCLOG") != "" { 124 | i, err := strconv.ParseInt(os.Getenv("VCLOG"), 10, 8) 125 | if err != nil { 126 | log.Fatalf("Error parsing environment variable [VCLOG") 127 | } 128 | // We've only parsed to an 8bit integer, however i is still a int64 so needs casting 129 | logLevel = int(i) 130 | } else { 131 | // Default to logging anything Info and below 132 | logLevel = int(log.InfoLevel) 133 | } 134 | 135 | if err := vctuiCmd.Execute(); err != nil { 136 | fmt.Println(err) 137 | os.Exit(1) 138 | } 139 | } 140 | 141 | var vctuiVersion = &cobra.Command{ 142 | Use: "version", 143 | Short: "Version and Release information about the plunder tool", 144 | Run: func(cmd *cobra.Command, args []string) { 145 | fmt.Printf("Plunder Release Information\n") 146 | fmt.Printf("Version: %s\n", Release.Version) 147 | fmt.Printf("Build: %s\n", Release.Build) 148 | }, 149 | } 150 | -------------------------------------------------------------------------------- /pkg/vctui/internals.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/vmware/govmomi" 11 | "github.com/vmware/govmomi/find" 12 | "github.com/vmware/govmomi/object" 13 | "github.com/vmware/govmomi/view" 14 | "github.com/vmware/govmomi/vim25/mo" 15 | ) 16 | 17 | type vcInternal struct { 18 | findDataStore string 19 | findNetwork string 20 | findHost string 21 | datastore *object.Datastore 22 | dcFolders *object.DatacenterFolders 23 | hostSystem *object.HostSystem 24 | network object.NetworkReference 25 | resourcePool *object.ResourcePool 26 | } 27 | 28 | func (i *vcInternal) parseInternals(c *govmomi.Client) error { 29 | 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | // Create a new finder that will discover the defaults and are looked for Networks/Datastores 34 | f := find.NewFinder(c.Client, true) 35 | 36 | // Find one and only datacenter, not sure how VMware linked mode will work 37 | dc, err := f.DatacenterOrDefault(ctx, "") 38 | if err != nil { 39 | return fmt.Errorf("No Datacenter instance could be found inside of vCenter %v", err) 40 | } 41 | 42 | // Make future calls local to this datacenter 43 | f.SetDatacenter(dc) 44 | 45 | // Find Datastore/Network 46 | i.datastore, err = f.DatastoreOrDefault(ctx, i.findDataStore) 47 | if err != nil { 48 | return fmt.Errorf("%v", err) 49 | } 50 | 51 | i.dcFolders, err = dc.Folders(ctx) 52 | if err != nil { 53 | return fmt.Errorf("Error locating default datacenter folder") 54 | } 55 | 56 | // Set the host that the VM will be created on 57 | i.hostSystem, err = f.HostSystemOrDefault(ctx, i.findHost) 58 | if err != nil { 59 | return fmt.Errorf("%v", err) 60 | } 61 | 62 | // Find the resource pool attached to this host 63 | i.resourcePool, err = i.hostSystem.ResourcePool(ctx) 64 | if err != nil { 65 | return fmt.Errorf("Error locating default resource pool") 66 | } 67 | 68 | i.network, err = f.NetworkOrDefault(ctx, i.findNetwork) 69 | if err != nil { 70 | return fmt.Errorf("Network could not be found") 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func netInventory(c *govmomi.Client) (networks []mo.Network, err error) { 77 | // Create a view of Network types 78 | ctx, cancel := context.WithCancel(context.Background()) 79 | defer cancel() 80 | m := view.NewManager(c.Client) 81 | 82 | v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"Network"}, true) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | defer v.Destroy(ctx) 88 | 89 | err = v.Retrieve(ctx, []string{"Network"}, nil, &networks) 90 | return 91 | } 92 | 93 | func storageInventory(c *govmomi.Client) (dss []mo.Datastore, err error) { 94 | // Create a view of Datastore objects 95 | ctx, cancel := context.WithCancel(context.Background()) 96 | defer cancel() 97 | m := view.NewManager(c.Client) 98 | 99 | v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"Datastore"}, true) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | defer v.Destroy(ctx) 105 | 106 | err = v.Retrieve(ctx, []string{"Datastore"}, []string{"summary"}, &dss) 107 | return 108 | } 109 | func hostInventory(c *govmomi.Client) (hss []mo.HostSystem, err error) { 110 | // Create a view of Datastore objects 111 | ctx, cancel := context.WithCancel(context.Background()) 112 | defer cancel() 113 | m := view.NewManager(c.Client) 114 | 115 | v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"HostSystem"}, true) 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | 120 | defer v.Destroy(ctx) 121 | 122 | err = v.Retrieve(ctx, []string{"HostSystem"}, nil, &hss) 123 | return 124 | } 125 | 126 | //VMInventory will create an inventory 127 | func VMInventory(c *govmomi.Client, sortVMs bool) ([]*object.VirtualMachine, error) { 128 | 129 | ctx, cancel := context.WithCancel(context.Background()) 130 | defer cancel() 131 | 132 | // Create a new finder that will discover the defaults and are looked for Networks/Datastores 133 | f := find.NewFinder(c.Client, true) 134 | 135 | // Find one and only datacenter, not sure how VMware linked mode will work 136 | dc, err := f.DatacenterOrDefault(ctx, "") 137 | if err != nil { 138 | return nil, fmt.Errorf("No Datacenter instance could be found inside of vCenter %v", err) 139 | } 140 | 141 | // Make future calls local to this datacenter 142 | f.SetDatacenter(dc) 143 | 144 | vms, err := f.VirtualMachineList(ctx, "*") 145 | 146 | if sortVMs == true { 147 | // Sort function to sort by name 148 | sort.Slice(vms, func(i, j int) bool { 149 | switch strings.Compare(vms[i].Name(), vms[j].Name()) { 150 | case -1: 151 | return true 152 | case 1: 153 | return false 154 | } 155 | return vms[i].Name() > vms[j].Name() 156 | }) 157 | } 158 | 159 | return vms, nil 160 | } 161 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # VCTUI - VMware vCenter Text User Interface 2 | 3 | Basic holder `readme.me` 4 | 5 | Inspired by the time wasted continualy right clicking in the Web UI... 6 | 7 | ## To get 8 | 9 | `go get -u github.com/thebsdbox/vctui` 10 | 11 | ## To use 12 | 13 | Now has a docker image: 14 | 15 | Using `-e` to pass in the environment variables defined below. 16 | 17 | ``` 18 | docker run -it --rm -e VCUSER -e VCPASS thebsdbox/vctui:0.1.0 --address https://Username:password@URL/sdk 19 | ``` 20 | 21 | VMware vCenter credentials are required and can be specified in a number of ways: 22 | 23 | *Flags* 24 | 25 | `--address` - can either be https://Username:password@URL/sdk or omit the user/pass details 26 | 27 | `--user` - used to specify a username 28 | 29 | `--pass` - used to specify a password 30 | 31 | `--insecure` - used to ignore a bad certificate 32 | 33 | *Environment variables* 34 | 35 | `VCURL` - same as --address 36 | 37 | `VCUSER` / `VCPASS` - same as the credentials above 38 | 39 | Then just start the application and you should see something similar below: 40 | 41 | ``` 42 | VMware vCenter 43 | ├──VMs 44 | │ ├──server01 45 | │ │ └──Details 46 | │ │ ├──CPUs: 1 47 | │ │ ├──Memory: 1024 48 | │ │ ├──VMware Tools: guestToolsNotRunning 49 | │ │ ├──VM IP Address: 50 | │ │ └──MAC ADDRESS: 00:50:56:a3:64:a2 51 | │ ├──server02 52 | │ │ └──Details 53 | │ │ ├──CPUs: 1 54 | │ │ ├──Memory: 1024 55 | │ │ ├──VMware Tools: 56 | │ │ ├──VM IP Address: 57 | │ │ └──MAC ADDRESS: 00:50:56:a3:4c:da 58 | │ ├──server03 59 | │ ├──server04 60 | │ ├──server05 61 | │ └──server06 62 | │ └──Details 63 | │ ├──CPUs: 1 64 | │ ├──Memory: 1024 65 | │ ├──VMware Tools: guestToolsNotRunning 66 | ``` 67 | 68 | ## Additional functionality 69 | 70 | 71 | ### Create New Virtual Machine 72 | 73 | Pressing `ctrl+n` will open a new screen allowing you to create a new virtual machine, the datastore and network dropdowns will populate once a host has been chosen. 74 | 75 | ### Deleting a Virtual Machine 76 | 77 | Select the Virtual Machine and press `ctrl+d` WARNING, there will be no alert and the machine will be instantly deleted (VM has to be powered off) 78 | 79 | ### Search 80 | 81 | Pressing `ctrl+f` will allow a search option (regexp) allowing you to search for specific virtual machine names. 82 | 83 | ### Power Management 84 | 85 | Pressing `ctrl+p` will open a power management ui (press ctrl+c) to exit this menu without making any changes 86 | 87 | ``` 88 | ╔═══Set the power state for this VM════╗ 89 | ║◉ Power On ║ 90 | ║◯ Power Off ║ 91 | ║◯ Suspend ║ 92 | ║◯ Reset ║ 93 | ║◯ Reboot (guest tools required) ║ 94 | ╚══════════════════════════════════════╝ 95 | ``` 96 | 97 | ### Installation / Deployment 98 | 99 | This requires a `plunder` server up and running and accessible from where `vctui` is currently running. Navigate to the MAC address of a newly created virtual machine, and press `ctrl+i`. This will present a new menu for configuring the settings of a newly deployed OS through plunder. 100 | 101 | ``` 102 | ╔════════════════════Update deployment═════════════════════╗ 103 | ║ ║ 104 | ║ Plunder Address http://localhost ║ 105 | ║ ║ 106 | ║ MAC Address 00:50:56:9b:2a:b7 ║ 107 | ║ ║ 108 | ║ Hostname server09 ║ 109 | ║ ║ 110 | ║ IP Address 192.168.1.3 ║ 111 | ║ ║ 112 | ║ Deployment Type preseed ║ 113 | ║ ║ 114 | ║ Save Settings ║ 115 | ║ ║ 116 | ╚══════════════════════════════════════════════════════════╝ 117 | ``` 118 | 119 | ### Snapshot 120 | 121 | Pressing `ctrl+s` on one of the listed snapshots will revert the virtual machine to that snapshot, and it will be left in the powered off state 122 | 123 | ### Refreshing Virtual Machines 124 | 125 | Pressing `ctrl+r` will refresh the state of all virtual machines in the VMware vCenter inventory 126 | 127 | Feel free to use or get involved. 128 | 129 | @thebsdbox 130 | -------------------------------------------------------------------------------- /pkg/vctui/builder.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gdamore/tcell" 8 | "github.com/rivo/tview" 9 | "github.com/vmware/govmomi/object" 10 | "github.com/vmware/govmomi/vim25/mo" 11 | "github.com/vmware/govmomi/vim25/types" 12 | ) 13 | 14 | // This function will take the full article set and build a tree from any search parameters 15 | func buildTree(v []*object.VirtualMachine) *tview.TreeNode { 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | // reference is used to label the type of tree Node 20 | var r reference 21 | 22 | // Begin the UI Tree 23 | rootDir := "VMware vCenter" 24 | root := tview.NewTreeNode(rootDir). 25 | SetColor(tcell.ColorWhite).SetReference(r) 26 | 27 | // Add Github articles to the tree 28 | vmNode := tview.NewTreeNode("VMs").SetReference(r).SetSelectable(true) 29 | vmNode.SetColor(tcell.ColorYellow) 30 | 31 | // Add Github articles to the tree 32 | templateNode := tview.NewTreeNode("Templates").SetReference(r).SetSelectable(true) 33 | templateNode.SetColor(tcell.ColorYellow) 34 | 35 | for x := range v { 36 | // Set the reference to point back to the VM object 37 | r.vm = v[x] 38 | // Create the Virtual Machine child node 39 | vmChildNode := tview.NewTreeNode(v[x].Name()).SetSelectable(true).SetExpanded(false) 40 | 41 | // Retrieve the managed object (using the summary string) 42 | var o mo.VirtualMachine 43 | 44 | err := v[x].Properties(ctx, v[x].Reference(), []string{"summary", "snapshot"}, &o) 45 | if err != nil { 46 | break 47 | } 48 | 49 | powerstate, err := v[x].PowerState(ctx) 50 | if err != nil { 51 | vmChildNode.SetColor(tcell.ColorGray) 52 | } 53 | switch powerstate { 54 | case types.VirtualMachinePowerStatePoweredOff: 55 | vmChildNode.SetColor(tcell.ColorRed) 56 | 57 | case types.VirtualMachinePowerStatePoweredOn: 58 | vmChildNode.SetColor(tcell.ColorGreen) 59 | 60 | case types.VirtualMachinePowerStateSuspended: 61 | vmChildNode.SetColor(tcell.ColorGray) 62 | 63 | } 64 | 65 | // Build out the details of the virtual machine 66 | vmDetails := buildDetails(ctx, v[x], o) 67 | vmSnapshots := buildSnapshots(ctx, v[x], o) 68 | // Add as child nodes to the virtual machine node 69 | vmChildNode.AddChild(vmDetails) 70 | vmChildNode.AddChild(vmSnapshots) 71 | 72 | // Set the object type and pin the reference to the new node 73 | if o.Summary.Config.Template == true { 74 | r.objectType = "template" 75 | vmChildNode.SetReference(r) 76 | templateNode.AddChild(vmChildNode) 77 | } else { 78 | r.objectType = "Virtual Machines" 79 | vmChildNode.SetReference(r) 80 | vmNode.AddChild(vmChildNode) 81 | } 82 | } 83 | 84 | root.AddChild(vmNode) 85 | root.AddChild(templateNode) 86 | 87 | return root 88 | } 89 | 90 | func buildDetails(ctx context.Context, vm *object.VirtualMachine, vmo mo.VirtualMachine) *tview.TreeNode { 91 | 92 | // reference is used to label the type of tree Node 93 | var r reference 94 | r.vm = vm 95 | // Add Details subtree information 96 | r.objectType = "Details" 97 | vmDetails := tview.NewTreeNode("Details").SetReference(r).SetSelectable(true) 98 | 99 | r.objectType = "CPUs" 100 | vmDetail := tview.NewTreeNode(fmt.Sprintf("CPUs: %d", vmo.Summary.Config.NumCpu)).SetReference(r).SetSelectable(true) 101 | vmDetails.AddChild(vmDetail) 102 | 103 | r.objectType = "Memory" 104 | vmDetail = tview.NewTreeNode(fmt.Sprintf("Memory: %d", vmo.Summary.Config.MemorySizeMB)).SetReference(r).SetSelectable(true) 105 | vmDetails.AddChild(vmDetail) 106 | 107 | r.objectType = "VM Type" 108 | vmDetail = tview.NewTreeNode(fmt.Sprintf("Type: %s", vmo.Summary.Config.GuestFullName)).SetReference(r).SetSelectable(true) 109 | vmDetails.AddChild(vmDetail) 110 | 111 | r.objectType = "VM Tools" 112 | vmDetail = tview.NewTreeNode(fmt.Sprintf("VMware Tools: %s", vmo.Summary.Guest.ToolsStatus)).SetReference(r).SetSelectable(true) 113 | vmDetails.AddChild(vmDetail) 114 | 115 | r.objectType = "VM Address" 116 | vmDetail = tview.NewTreeNode(fmt.Sprintf("VM IP Address: %s", vmo.Summary.Guest.IpAddress)).SetReference(r).SetSelectable(true) 117 | vmDetails.AddChild(vmDetail) 118 | 119 | devices, _ := vm.Device(ctx) 120 | 121 | r.objectType = "MAC" 122 | r.objectDetails = devices.PrimaryMacAddress() 123 | 124 | vmDetail = tview.NewTreeNode(fmt.Sprintf("MAC ADDRESS: %s", r.objectDetails)).SetReference(r).SetSelectable(true) 125 | vmDetails.AddChild(vmDetail) 126 | 127 | return vmDetails 128 | } 129 | 130 | func buildSnapshots(ctx context.Context, vm *object.VirtualMachine, vmo mo.VirtualMachine) *tview.TreeNode { 131 | // Add Snapshots subtree information 132 | var r reference 133 | r.objectType = "snapshots" 134 | r.vm = vm 135 | vmSnapshots := tview.NewTreeNode("snapshots").SetReference(r).SetSelectable(true) 136 | r.objectType = "snapshot" 137 | if vmo.Snapshot != nil { 138 | if len(vmo.Snapshot.RootSnapshotList) != 0 { 139 | for i := range vmo.Snapshot.RootSnapshotList { 140 | vmSnapshot := tview.NewTreeNode(vmo.Snapshot.RootSnapshotList[i].Name).SetReference(r).SetSelectable(true) 141 | vmSnapshots.AddChild(vmSnapshot) 142 | } 143 | } 144 | } 145 | return vmSnapshots 146 | } 147 | -------------------------------------------------------------------------------- /pkg/vctui/uiNew.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | "github.com/vmware/govmomi" 10 | 11 | "github.com/rivo/tview" 12 | "github.com/vmware/govmomi/object" 13 | "github.com/vmware/govmomi/vim25/types" 14 | ) 15 | 16 | func newVM(c *govmomi.Client) *types.VirtualMachineConfigSpec { 17 | uiBugFix() 18 | 19 | ctx, cancel := context.WithCancel(context.Background()) 20 | defer cancel() 21 | 22 | app := tview.NewApplication() 23 | 24 | // Get inventories from VMware vCenter 25 | h, err := hostInventory(c) 26 | n, _ := netInventory(c) 27 | d, err := storageInventory(c) 28 | 29 | // Arrays to hold the resolved names of the objects 30 | var hosts, networks, datastores []string 31 | 32 | for i := range h { 33 | hosts = append(hosts, h[i].Summary.Config.Name) 34 | } 35 | 36 | // Function used to ensure that only numbers are entered into a field where expected 37 | var intCheck func(string, rune) bool 38 | intCheck = func(textToCheck string, lastChar rune) bool { 39 | _, err := strconv.Atoi(textToCheck) 40 | if err != nil { 41 | return false 42 | } 43 | return true 44 | } 45 | 46 | form := tview.NewForm() 47 | form.AddCheckbox("Boot new VM", false, nil). 48 | AddInputField("VM Name", "", 20, nil, nil). 49 | AddDropDown("VM Type", vmTypes, 0, nil). 50 | AddDropDown("Hosts", hosts, 0, func(option string, optionIndex int) { 51 | // Clear original arrays 52 | networks = nil 53 | datastores = nil 54 | 55 | // Build new Network array, this matches the references of all networks to references the host is mapped to 56 | for y := range h[optionIndex].Network { 57 | for i := range n { 58 | if h[optionIndex].Network[y].String() == n[i].Summary.GetNetworkSummary().Network.String() { 59 | networks = append(networks, n[i].Name) 60 | } 61 | } 62 | } 63 | // Update the Network dropdown with the new array 64 | if form.GetFormItemByLabel("Network") != nil { 65 | form.GetFormItemByLabel("Network").(*tview.DropDown).SetOptions(networks, nil) 66 | } 67 | // Build new datastore array, this matches the references of all datastore to references the host is mapped to 68 | for y := range h[optionIndex].Datastore { 69 | for i := range d { 70 | if h[optionIndex].Datastore[y].String() == d[i].Summary.Datastore.String() { 71 | datastores = append(datastores, d[i].Summary.Name) 72 | } 73 | } 74 | } 75 | 76 | //Update the datastore with the new array 77 | if form.GetFormItemByLabel("Datastore") != nil { 78 | form.GetFormItemByLabel("Datastore").(*tview.DropDown).SetOptions(datastores, nil) 79 | } 80 | }). 81 | AddDropDown("Network", networks, 0, nil). 82 | AddDropDown("Datastore", datastores, 0, nil). 83 | AddInputField("vCPUs", "", 2, intCheck, nil). 84 | AddInputField("Memory (MB)", "", 6, intCheck, nil). 85 | AddInputField("Disk Size (GB)", "", 6, intCheck, nil). 86 | AddButton("Save Settings", func() { app.Stop() }) 87 | 88 | form.SetBorder(true). 89 | SetTitle("New Virtual Machine"). 90 | SetTitleAlign(tview.AlignCenter). 91 | SetRect(5, 1, 60, 23) 92 | 93 | if err := app.SetRoot(form, false).Run(); err != nil { 94 | panic(err) 95 | } 96 | 97 | // New Virtual Machine configuration 98 | var vCPU, mem, diskSize int 99 | var name, guestType, host, network, datastore string 100 | 101 | // Parse the form values 102 | vCPU, _ = strconv.Atoi(form.GetFormItemByLabel("vCPUs").(*tview.InputField).GetText()) 103 | mem, _ = strconv.Atoi(form.GetFormItemByLabel("Memory (MB)").(*tview.InputField).GetText()) 104 | diskSize, _ = strconv.Atoi(form.GetFormItemByLabel("Disk Size (GB)").(*tview.InputField).GetText()) 105 | 106 | name = form.GetFormItemByLabel("VM Name").(*tview.InputField).GetText() 107 | _, guestType = form.GetFormItemByLabel("VM Type").(*tview.DropDown).GetCurrentOption() 108 | _, network = form.GetFormItemByLabel("Network").(*tview.DropDown).GetCurrentOption() 109 | _, datastore = form.GetFormItemByLabel("Datastore").(*tview.DropDown).GetCurrentOption() 110 | _, host = form.GetFormItemByLabel("Hosts").(*tview.DropDown).GetCurrentOption() 111 | 112 | spec := types.VirtualMachineConfigSpec{ 113 | Name: name, 114 | GuestId: guestType, 115 | Files: &types.VirtualMachineFileInfo{VmPathName: fmt.Sprintf("[%s]", datastore)}, 116 | NumCPUs: int32(vCPU), 117 | MemoryMB: int64(mem), 118 | } 119 | 120 | // TODO - ALL Code below needs moving to a seperate func() 121 | 122 | // Add SCSI controller to the new specification 123 | scsi, err := object.SCSIControllerTypes().CreateSCSIController("pvscsi") 124 | if err != nil { 125 | log.Fatalln("Error creating pvscsi controller as part of new VM") 126 | } 127 | 128 | spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ 129 | Operation: types.VirtualDeviceConfigSpecOperationAdd, 130 | Device: scsi, 131 | }) 132 | 133 | i := vcInternal{ 134 | findNetwork: network, 135 | findDataStore: datastore, 136 | findHost: host, 137 | } 138 | err = i.parseInternals(c) 139 | if err != nil { 140 | log.Fatalf("%v", err) 141 | } 142 | 143 | // Create the new Virtual Machine 144 | task, err := i.dcFolders.VmFolder.CreateVM(ctx, spec, i.resourcePool, i.hostSystem) 145 | 146 | if err != nil { 147 | log.Fatalln("Creating new VM failed, more detail can be found in vCenter tasks") 148 | } 149 | 150 | // Wait for the results of vCenter parsing 151 | info, err := task.WaitForResult(ctx, nil) 152 | if err != nil { 153 | log.Fatalf("Creating new VM failed\n%v", err) 154 | } 155 | 156 | // Retrieve the new VM 157 | vm := object.NewVirtualMachine(c.Client, info.Result.(types.ManagedObjectReference)) 158 | 159 | // Modify it's configuration 160 | backing, err := i.network.EthernetCardBackingInfo(ctx) 161 | if err != nil { 162 | log.Fatalf("Unable to determine vCenter network backend\n%v", err) 163 | } 164 | 165 | netdev, err := object.EthernetCardTypes().CreateEthernetCard("vmxnet3", backing) 166 | if err != nil { 167 | log.Fatalf("Unable to create vmxnet3 network interface\n%v", err) 168 | } 169 | 170 | var add []types.BaseVirtualDevice 171 | add = append(add, netdev) 172 | 173 | devices, err := vm.Device(ctx) 174 | if err != nil { 175 | log.Fatalf("Unable to read devices from VM configuration\n%v", err) 176 | } 177 | 178 | controller, err := devices.FindDiskController("scsi") 179 | if err != nil { 180 | log.Fatalf("Unable to find SCSI device from VM configuration\n%v", err) 181 | } 182 | 183 | disk := devices.CreateDisk(controller, i.datastore.Reference(), i.datastore.Path(fmt.Sprintf("%s/%s.vmdk", name, name))) 184 | disk.CapacityInKB = int64(diskSize * 1024 * 1024) 185 | add = append(add, disk) 186 | 187 | if vm.AddDevice(ctx, add...); err != nil { 188 | log.Fatalf("Unable to add new storage device to VM configuration\n%v", err) 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func newVMFromTemplate(template string) { 195 | uiBugFix() 196 | app := tview.NewApplication() 197 | 198 | form := tview.NewForm(). 199 | AddCheckbox("Update on starting katbox", false, nil). 200 | AddDropDown("Editor", vmTypes, 0, nil). 201 | AddInputField("(optional) Custom editor Path", "", 30, nil, nil). 202 | AddInputField("Git clone path", "", 30, nil, nil). 203 | AddCheckbox("Open URLs in Browser", false, nil). 204 | AddButton("Save Settings", func() { app.Stop() }) 205 | 206 | form.SetBorder(true).SetTitle(fmt.Sprintf("New Virtual Machine from template: %s", template)).SetTitleAlign(tview.AlignLeft) 207 | if err := app.SetRoot(form, true).Run(); err != nil { 208 | panic(err) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pkg/vctui/ui.go: -------------------------------------------------------------------------------- 1 | package vctui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/vmware/govmomi" 9 | 10 | "github.com/gdamore/tcell" 11 | "github.com/rivo/tview" 12 | "github.com/vmware/govmomi/object" 13 | "github.com/vmware/govmomi/vim25/types" 14 | ) 15 | 16 | // searchString is the filter that is applied to listed virtual machines 17 | var searchString string 18 | 19 | //MainUI starts up the katbox User Interface 20 | func MainUI(v []*object.VirtualMachine, c *govmomi.Client) error { 21 | // Check for a nil pointer 22 | if v == nil { 23 | return fmt.Errorf("No VMs") 24 | } 25 | 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | defer cancel() 28 | 29 | root := buildTree(v) 30 | 31 | tree := tview.NewTreeView(). 32 | SetRoot(root). 33 | SetCurrentNode(root) 34 | application := tview.NewApplication() 35 | 36 | // This handles what happens when (enter) is pressed on a node, typically it will just flip the expanded state 37 | tree.SetSelectedFunc(func(node *tview.TreeNode) { 38 | reference := node.GetReference() 39 | if reference == nil { 40 | return // Selecting the root node does nothing. 41 | } 42 | children := node.GetChildren() 43 | // If it has children then flip the expanded state, if it's the final child we will action it 44 | if len(children) != 0 { 45 | node.SetExpanded(!node.IsExpanded()) 46 | } else { 47 | // TODO - Open the action menu on the specific article 48 | } 49 | }) 50 | 51 | // This section handles all of the input from the end-user 52 | // 53 | // Ctrl+d = delete function 54 | // Ctrl+f = Find function 55 | // Ctrl+i = deploy/install function 56 | // Ctrl+p = Power function 57 | // Ctrl+r = Refresh function 58 | // Ctrl+s = Snapshot function 59 | 60 | // TODO - (thebsdbox) 61 | // Ctrl+n = new VM / new VM from template (use the reference to determine) 62 | 63 | tree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 64 | switch event.Key() { 65 | case tcell.KeyCtrlD: 66 | // Delete functionality 67 | r := tree.GetCurrentNode().GetReference().(reference) 68 | // Check that the node has a virtual machine associated with it 69 | if r.vm != nil { 70 | r.vm.Destroy(ctx) 71 | } 72 | case tcell.KeyCtrlF: 73 | // Search functionality 74 | 75 | var subset []*object.VirtualMachine 76 | // Stop the existing UI 77 | application.Suspend(func() { searchString, subset = SearchUI(v) }) 78 | uiBugFix() 79 | // Get new tree 80 | newRoot := buildTree(subset) 81 | if searchString == "" { 82 | root.SetText("VMware vCenter") 83 | } else { 84 | root.SetText(fmt.Sprintf("VMware vCenter (filter: %s)", searchString)) 85 | } 86 | root.ClearChildren() 87 | root.SetChildren(newRoot.GetChildren()) 88 | 89 | case tcell.KeyCtrlI: 90 | 91 | n := tree.GetCurrentNode() 92 | var address, hostname string 93 | 94 | n.Walk(func(node, parent *tview.TreeNode) bool { 95 | // Ensure we don't parse an object with no reference 96 | if node.GetReference() == nil { 97 | return false 98 | } 99 | r := node.GetReference().(reference) 100 | if r.objectType == "MAC" { 101 | address = r.objectDetails 102 | hostname = r.vm.Name() 103 | application.Suspend(func() { deployOnVM(address, hostname) }) 104 | return false 105 | } 106 | return true 107 | }) 108 | uiBugFix() 109 | 110 | case tcell.KeyCtrlN: 111 | // New Virtual Machine functionality 112 | r := tree.GetCurrentNode().GetReference().(reference) 113 | 114 | if r.objectType == "template" { 115 | application.Suspend(func() { newVMFromTemplate(tree.GetCurrentNode().GetText()) }) 116 | } else { 117 | application.Suspend(func() { newVM(c) }) 118 | } 119 | uiBugFix() 120 | case tcell.KeyCtrlP: 121 | 122 | // Power managment 123 | var action int 124 | //Stop existing UI 125 | application.Suspend(func() { action = powerui() }) 126 | uiBugFix() 127 | 128 | r := tree.GetCurrentNode().GetReference().(reference) 129 | 130 | if r.vm == nil { 131 | return nil 132 | } 133 | 134 | switch action { 135 | case powerOn: 136 | _, err := r.vm.PowerOn(ctx) 137 | if err != nil { 138 | errorUI(err) 139 | } 140 | 141 | case powerOff: 142 | _, err := r.vm.PowerOff(ctx) 143 | if err != nil { 144 | errorUI(err) 145 | } 146 | 147 | case guestPowerOff: 148 | err := r.vm.ShutdownGuest(ctx) 149 | if err != nil { 150 | errorUI(err) 151 | } 152 | case guestReboot: 153 | err := r.vm.RebootGuest(ctx) 154 | if err != nil { 155 | errorUI(err) 156 | } 157 | 158 | case suspend: 159 | _, err := r.vm.Suspend(ctx) 160 | if err != nil { 161 | errorUI(err) 162 | } 163 | 164 | case reset: 165 | _, err := r.vm.Reset(ctx) 166 | if err != nil { 167 | errorUI(err) 168 | } 169 | 170 | case netPowerOn: 171 | bootOrder := []string{"ethernet", "disk"} 172 | 173 | devices, err := r.vm.Device(ctx) 174 | if err != nil { 175 | errorUI(err) 176 | } 177 | 178 | bootOptions := types.VirtualMachineBootOptions{ 179 | BootOrder: devices.BootOrder(bootOrder), 180 | } 181 | 182 | err = r.vm.SetBootOptions(ctx, &bootOptions) 183 | if err != nil { 184 | errorUI(err) 185 | } 186 | _, err = r.vm.PowerOn(ctx) 187 | if err != nil { 188 | errorUI(err) 189 | } 190 | 191 | // Set the boot order back to disk after a three second timeout 192 | time.AfterFunc(3*time.Second, func() { 193 | bootOrder = []string{"disk", "ethernet"} 194 | 195 | bootOptions = types.VirtualMachineBootOptions{ 196 | BootOrder: devices.BootOrder(bootOrder), 197 | } 198 | 199 | err = r.vm.SetBootOptions(ctx, &bootOptions) 200 | if err != nil { 201 | errorUI(err) 202 | } 203 | }) 204 | 205 | case diskPowerOn: 206 | bootOrder := []string{"disk", "ethernet"} 207 | 208 | devices, err := r.vm.Device(ctx) 209 | if err != nil { 210 | errorUI(err) 211 | } 212 | 213 | bootOptions := types.VirtualMachineBootOptions{ 214 | BootOrder: devices.BootOrder(bootOrder), 215 | } 216 | 217 | err = r.vm.SetBootOptions(ctx, &bootOptions) 218 | if err != nil { 219 | errorUI(err) 220 | } 221 | _, err = r.vm.PowerOn(ctx) 222 | if err != nil { 223 | errorUI(err) 224 | } 225 | } 226 | case tcell.KeyCtrlR: 227 | // Refresh Virtual Machines 228 | v, err := VMInventory(c, true) 229 | if err != nil { 230 | // Throw Error UI 231 | application.Suspend(func() { errorUI(err) }) 232 | uiBugFix() 233 | } 234 | var newRoot *tview.TreeNode 235 | if searchString != "" { 236 | filteredVMs, err := searchVMS(searchString, v) 237 | if err != nil { 238 | // Throw Error UI 239 | application.Suspend(func() { errorUI(err) }) 240 | uiBugFix() 241 | } 242 | newRoot = buildTree(filteredVMs) 243 | } else { 244 | newRoot = buildTree(v) 245 | } 246 | root.ClearChildren() 247 | root.SetChildren(newRoot.GetChildren()) 248 | 249 | case tcell.KeyCtrlS: 250 | r := tree.GetCurrentNode().GetReference().(reference) 251 | if r.objectType == "snapshot" { 252 | snapshot := tree.GetCurrentNode().GetText() 253 | 254 | if r.vm != nil { 255 | _, err := r.vm.RevertToSnapshot(ctx, snapshot, true) 256 | if err != nil { 257 | // Throw Error UI 258 | application.Suspend(func() { errorUI(err) }) 259 | uiBugFix() 260 | } 261 | } 262 | } 263 | default: 264 | return event 265 | } 266 | return nil 267 | }) 268 | 269 | if err := application.SetRoot(tree, true).Run(); err != nil { 270 | panic(err) 271 | } 272 | 273 | fmt.Printf("More to come\n") 274 | 275 | return nil 276 | } 277 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a h1:+uvtaGSLJh0YpLLHCQ9F+UVGy4UOS542hsjj8wBjvH0= 11 | github.com/c4milo/gotoolkit v0.0.0-20190525173301-67483a18c17a/go.mod h1:txokOny9wavBtq2PWuHmj1P+eFwpCsj+gQeNNANChfU= 12 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 15 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 17 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 18 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 25 | github.com/digineo/go-dhclient v1.0.2/go.mod h1:DPvyqGEW8irJvp2lrnGfQWpjj6VidXX9STLBTfNing4= 26 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 27 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 28 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 29 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 30 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 31 | github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= 32 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 33 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 34 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 35 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 36 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 37 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 38 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 39 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 40 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 41 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 42 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 43 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 44 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 47 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 48 | github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= 49 | github.com/google/uuid v0.0.0-20170306145142-6a5e28554805/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 51 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 52 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 53 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 54 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 55 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 56 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 57 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 58 | github.com/hooklift/iso9660 v1.0.0 h1:GYN0ejrqTl1qtB+g+ics7xxWHp7J2B1zmr25O9EyG3c= 59 | github.com/hooklift/iso9660 v1.0.0/go.mod h1:sOC47ru8lB0DlU0EZ7BJ0KCP5rDqOvx0c/5K5ADm8H0= 60 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 61 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 62 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 63 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 64 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 65 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 66 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 67 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 68 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 69 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 70 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 71 | github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk= 72 | github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o= 73 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 74 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 75 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 76 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 77 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 78 | github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= 79 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 80 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 81 | github.com/mdlayher/raw v0.0.0-20191004140158-e1402808046b/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= 82 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= 83 | github.com/micmonay/keybd_event v1.0.0 h1:gq5S9QdUx/KHsWwT+bTgib4vCZlXuS+xplkEqwhu6Zo= 84 | github.com/micmonay/keybd_event v1.0.0/go.mod h1:QS2Kfz0PbPezFqMPEot+l/cK78/tHLZtZ7AbYUCRKsQ= 85 | github.com/micmonay/keybd_event v1.1.0 h1:fQzkqiG/Siwji1Ju9NDkIb8FSFFlPU76YbJntrXdtQw= 86 | github.com/micmonay/keybd_event v1.1.0/go.mod h1:QS2Kfz0PbPezFqMPEot+l/cK78/tHLZtZ7AbYUCRKsQ= 87 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 88 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 89 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 90 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 91 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 92 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 93 | github.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4 h1:XBPLmj1YuM8GlnueA/IMUKQDVVKP7E7NOjx1SlAd/g0= 94 | github.com/plunder-app/BOOTy v0.0.0-20200513203223-f43f6ea742c4/go.mod h1:ItncOB62Q244Txg1v6/S3XZq3KB+9uHcmmZhJF+5Z+A= 95 | github.com/plunder-app/plunder v0.4.5 h1:Q/hz5LDPFJeGzvB/6n992oS86LNEh2pFarXu3TSKDpM= 96 | github.com/plunder-app/plunder/pkg v0.0.0-20200513203243-eccb418a5255 h1:6ckJRrkzSjBTzISOY9sDVaJs+7ds2g5OqG3+w67G7UI= 97 | github.com/plunder-app/plunder/pkg/apiserver v0.0.0-20200513203243-eccb418a5255 h1:vdyTlM/EuXXWYUSu/9rvTCGE6SELeH5V2tCs8YUUOmE= 98 | github.com/plunder-app/plunder/pkg/apiserver v0.0.0-20200513203243-eccb418a5255/go.mod h1:AOpM7jJWaWxa9smjmjetM/Z4zjgsdfw1YnpLnNz5bn8= 99 | github.com/plunder-app/plunder/pkg/services v0.0.0-20200513203243-eccb418a5255 h1:GQC2yYCs3/FGebbLpH9s5qGDumqyk7vuda0yAwJGnog= 100 | github.com/plunder-app/plunder/pkg/services v0.0.0-20200513203243-eccb418a5255/go.mod h1:iBgJnYPhXMDczYk+SF5tCoVCOn5M8o4f28EBoAOn+tA= 101 | github.com/plunder-app/plunder/pkg/utils v0.0.0-20200513203243-eccb418a5255 h1:FlNkbRp2RQNlS0XQbOzDcgfOu/DIUfEyLnFEm3ArGWc= 102 | github.com/plunder-app/plunder/pkg/utils v0.0.0-20200513203243-eccb418a5255/go.mod h1:c8D83hu1Vz4Rz/f1rLHjhrOh3xRzga7r/o4+QyoeFTg= 103 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 104 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 105 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 106 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 107 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 108 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 109 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 110 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 111 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 112 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 113 | github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= 114 | github.com/rivo/tview v0.0.0-20200507165325-823f280c5426 h1:oUJaa48KPBmtgqppKbVqP4QHRa/cdONfQoXcNWRe7wE= 115 | github.com/rivo/tview v0.0.0-20200507165325-823f280c5426/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= 116 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 117 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 118 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 119 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 120 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 121 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 122 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 123 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 124 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 125 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 126 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 127 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 128 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 129 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 130 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 131 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 132 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 133 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 134 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 135 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 136 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 137 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 138 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 139 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 140 | github.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c h1:cYCrFUo78/407dxOlYw2g4pdm4Ly8RSPedsYB+z7h1s= 141 | github.com/thebsdbox/go-tftp v0.0.0-20190329154032-a7263f18c49c/go.mod h1:yXG6GIu/ptjkk0fd++y96R2cahlvxZr4LhMdf0j/L2Q= 142 | github.com/thebsdbox/kuiz v0.0.0-20200327155153-b933733cc267 h1:3CMuCB6n4KyRV6GI66cST+AUxn9N3Qt7lrg8TmWwxtc= 143 | github.com/thebsdbox/kuiz v0.0.0-20200327155153-b933733cc267/go.mod h1:RXRJ06S5O5Gf9AtA3ROLtf9hP4oGUm6P4YEilikZ79s= 144 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 145 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 146 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 147 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 148 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= 149 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 150 | github.com/vmware/govmomi v0.22.2 h1:hmLv4f+RMTTseqtJRijjOWzwELiaLMIoHv2D6H3bF4I= 151 | github.com/vmware/govmomi v0.22.2/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc= 152 | github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= 153 | github.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee h1:P2Wwq5QukiLY/I6+mc7NyLFX/atHAj6pGwiVu6fld98= 154 | github.com/whyrusleeping/go-tftp v0.0.0-20180830013254-3695fa5761ee/go.mod h1:ZemSN4DPuG1ppDttxnu45zl8BenKT9xSjMyapUd+Dd0= 155 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 156 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 157 | github.com/zcalusic/sysinfo v0.0.0-20200228145645-a159d7cc708b/go.mod h1:WGLNaWsjKQ2gXmAHh+MQztgu3FLFAnOFJjFzhpgShCY= 158 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 159 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 160 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 161 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 162 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 165 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 166 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 167 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 168 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 169 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 170 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 171 | golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 173 | golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= 175 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 176 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 177 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 185 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= 192 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c h1:kISX68E8gSkNYAFRFiDU8rl5RIn1sJYKYb/r2vMLDrU= 195 | golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 197 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 198 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 199 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 200 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 201 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 202 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 203 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 204 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 205 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 206 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 207 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 208 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 211 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 212 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 213 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 214 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 215 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 216 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 217 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 218 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 219 | --------------------------------------------------------------------------------