Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First release
Browse files Browse the repository at this point in the history
Golgautier committed Dec 15, 2022
0 parents commit f3b38fd
Showing 14 changed files with 1,772 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: goreleaser

on:
push:
tags:
- "*"

permissions:
contents: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19.4
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: ${{ env.GITHUB_REF_NAME }}
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

dist/
binaries/
BuildBinaries.sh
31 changes: 31 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
builds:
- binary: nvgm
goos:
- darwin
- linux
goarch:
- amd64
- arm64
env:
- CGO_ENABLED=0

release:
prerelease: auto

universal_binaries:
- replace: true
name_template: nvgm

brews:
-
name: nvgm
homepage: "https://github.com/Golgautier/nutanix_vg_manager"
tap:
owner: Golgautier
name: homebrew-tap
commit_author:
name: Gautier
email: [email protected]

checksum:
name_template: 'checksums.txt'
83 changes: 83 additions & 0 deletions functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"crypto/tls"
"flag"
"fmt"
"net/http"
"os"
"syscall"

"golang.org/x/term"
)

func ProgUsage() bool {
fmt.Println("Usage : nvgm [--server <PC Name>] [--user <Username>] [--help] [--usage] [--secure-mode]")
os.Exit(1)
return true
}

// This function get PE connection info from parameters or from STDIN
func GetPrismInfo() {

// Define all parameters
PC := flag.String("server", "", "Prism Element IP of FQDN")
User := flag.String("user", "", "Prism Element User")
help := flag.Bool("help", false, "Request usage")
usage := flag.Bool("usage", false, "Request usage")
secure := flag.Bool("secure-mode", false, "Request usage")
flag.Parse()

// Affect or request server value
if *PC == string("") {
fmt.Printf("Please enter Prism Central IP or FQDN : ")
fmt.Scanln(&MyPrism.PC)
} else {
MyPrism.PC = *PC
}

if *help || *usage {
ProgUsage()
}

// Affect or request user value
if *User == string("") {
fmt.Printf("Please enter Prism User : ")
fmt.Scanln(&MyPrism.User)
} else {
MyPrism.User = *User
}

// Request password
fmt.Printf("Please enter Prism password for " + MyPrism.User + " : ")
tmp, _ := term.ReadPassword(int(syscall.Stdin))
fmt.Println("")

MyPrism.Password = string(tmp)

// Define API call mode
MyPrism.Mode = "password"

// Deactivate SSL Check
if *secure {
ActivateSSLCheck(true)
} else {
ActivateSSLCheck(false)
}

}

// =========== ActivateSSLCheck ===========
func ActivateSSLCheck(value bool) {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: !value}

}

// =========== CheckErr ===========
// This function is will handle errors
func CheckErr(context string, err error) {
if err != nil {
fmt.Println("ERROR", context, " : ", err.Error())
os.Exit(2)
}
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module nvgm

go 1.19

require (
github.com/gizak/termui/v3 v3.1.0
golang.org/x/term v0.2.0
)

require (
github.com/mattn/go-runewidth v0.0.2 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
golang.org/x/sys v0.2.0 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
72 changes: 72 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"fmt"
"log"
ntnx "nvgm/ntnx_api_call"

ui "github.com/gizak/termui/v3"
)

// Global variable
var MyPrism ntnx.Ntnx_endpoint

func main() {
// Var declaration
var MyUI UI
var action string

// Get Prism info
GetPrismInfo()

// Test API connection
fmt.Print("Test connection to PC...")
MyPrism.CallAPIJSON("PC", "GET", "/api/nutanix/v3/users/me", "", nil)
fmt.Println("Ok")

fmt.Println("Please wait during VG List collection...")

GetVGList()

fmt.Println("Done")

// Initialize TermUI engine
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()

// Create initial UI
MyUI.Create()
MyUI.Resize()
MyUI.Render()

// 2 types of events : time & keyboards inputs
//tick := time.NewTicker(1 * time.Second)
uiEvents := ui.PollEvents()

for action != "quit" {
select {
//case <-tick.C:
// MyUI.UpdateListTitle()

case e := <-uiEvents:
switch MyUI.Mode {
case "view":
action = MyUI.HandleKeyViewMode(e.ID)
case "help":
action = MyUI.HandleKeyHelpMode(e.ID)
}

// If MyUI.Ask action have been executed, we need to 'flush' the chan of uiEvents
// because entry will be seen here and will cause problems.
// Note : we set MyUI.uglyfix at 'true' when function ask is called
if MyUI.UglyFix == true {
<-uiEvents
MyUI.UglyFix = false
}

}
MyUI.Render()
}
}
145 changes: 145 additions & 0 deletions ntnx_api_call/ntnx_api_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package ntnx_api_call

import (
"bytes"
"crypto/tls"
"encoding/json"
"io"
"log"
"net/http"
"strings"
"time"
)

// Emoji symbols from http://www.unicode.org/emoji/charts/emoji-list.html
var symbols = map[string]string{
"FAIL": "\U0000274C",
"INFO": "\U0001F449",
"OK": "\U00002705",
"WAIT": "\U0001F55B",
"NEUTRAL": "\U00002796",
}

type Ntnx_endpoint struct {
PC string
PE string
Mode string
User string
Password string
Cert string
Chain string
Key string
}

const const_max_retry int = 3

// =========== CheckErr ===========
// This function is will handle errors
func CheckErr(context string, err error) {
if err != nil {
log.Fatal(symbols["FAIL"], context, err.Error())
}
}

// =========== WaitForTask ===========
// Wait for end of task and return stats

func (e Ntnx_endpoint) WaitForTask(task string) (bool, string, string) {
url := "/api/nutanix/v3/tasks/" + task

type TmpStruct struct {
ProgressMessage string `json:"progress_message"`
PercentageComplete int64 `json:"percentage_complete"`
Status string `json:"status"`
ErrorCode string `json:"error_code"`
ErrorDetail string `json:"error_detail"`
}

var ReturnValue TmpStruct

for ReturnValue.PercentageComplete < 100 {
e.CallAPIJSON("PC", "GET", url, "", &ReturnValue)
time.Sleep(time.Duration(10) * time.Second)
}

if ReturnValue.Status == "SUCCEEDED" {
return true, ReturnValue.Status, ReturnValue.ErrorDetail
} else {
return false, ReturnValue.Status, ReturnValue.ErrorDetail
}

}

// =========== CallAPIJSON ===========
// Do a call API and unmarshall the result
func (e Ntnx_endpoint) CallAPIJSON(target string, method string, url string, payload string, retour interface{}) {

var long_url, ReqMethod string
var jsonStr []byte
var resp *http.Response
client := &http.Client{}

if strings.ToUpper(target) == "PE" {
long_url = "https://" + e.PE + ":9440" + url
} else {
long_url = "https://" + e.PC + ":9440" + url
}

if strings.ToUpper(method) == "POST" {
jsonStr = []byte(payload)
ReqMethod = http.MethodPost
} else if strings.ToUpper(method) == "GET" {
jsonStr = nil
ReqMethod = http.MethodGet
} else if strings.ToUpper(method) == "PATCH" {
jsonStr = []byte(payload)
ReqMethod = http.MethodPatch
} else if strings.ToUpper(method) == "DELETE" {
jsonStr = []byte(payload)
ReqMethod = http.MethodDelete
} else {
log.Fatalln("HTTP method", method, "not handled")
}

// Create new request
req, err := http.NewRequest(ReqMethod, long_url, bytes.NewBuffer(jsonStr))
CheckErr("Unable to prepare API Call", err)

// Define default headers
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")

if e.Mode == "password" {
// Authentication
req.SetBasicAuth(e.User, e.Password)

} else if e.Mode == "cert" {

_, err := tls.X509KeyPair([]byte(e.Cert), []byte(e.Key))
CheckErr("Unable to load certs", err)

} else {
log.Fatalln("FAIL", "Mode "+e.Mode+" unknown for Nutanix API call")
}

// Launch request
resp, err = client.Do(req)

// If an error occurs, we retry
for retry := 1; retry < const_max_retry && err != nil; retry++ {
resp, err = client.Do(req)
}
CheckErr("API Call failed", err)

if int(resp.StatusCode) > 299 {
log.Fatal(symbols["FAIL"], " API Call failed : ", resp.Status)
}

defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
CheckErr("Unable to read API answer body", err)

// Transform json answer to map
err = json.Unmarshal(bodyBytes, &retour)
CheckErr("Unable to get json answer from API Call.", err)
}
64 changes: 64 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Nutanix VG Manager (NVGM)

## Summary ##

nvgm is a tool to manage VG from Nutanix clusters. It allows to list and display and filter them, display all details of the VG, change their description or delete VG, one by one or in bulk mode.

This tool is NOT an official tool of Nutanix.

## Demo


## Install

### Brew
With brew, just do
```
brew install golgautier/tap/nvgm
```

### Compile your own binary ###
If you prefer to create your own binary (for security reason). Download code from this folder, install golang on your computer, and launch command

`go build -o <binary name> .`


## Launch ##

Interactive :
just launch `nvgm` and answer to all questions.

With parameters :
Launch `nvgm` with several parameters :
- `-help` or `-usage`: Display usage message
- `-secure-mode`: Secure mode, to force https with valid certificate
- `-server <Server name>` : Specify Prism Central name or address
- `-user <User name>` : Specify user to use (must have admin rights)

Password will be requested interactively

Example :
```
./nvgm -server pc.ntnx.fr -user admin
```

## Usage ##

When app is launched, it will get VG list from PC. It can take few seconds regarding VG numbers

Then, VG are displayed in an array. You can use the following keys :
- `h` : Display help popup (then `Escape` to leave it)
- `d` : Switch display mode : Name (UUID) or UUID (Name)
- `o` : Order list ascending or descending
- `f` : filter VG List (Warning : it is case sentsitive)
- You can use simple expression, checked on all VG details
- You can specify field to filter (`UUID`, `Container`, `Name`, `Size`, `Mounted`, `Description`) by adding field name then `:` and the filter value
- `Tab` do auto-completion, for the field name
- You can specify multiple filter values with `|` (OR) or `&` (AND)
- `Space` : Select the highlighted VG
- `Ctrl + A` : Select all displayed VG
- `Ctrl + Space` : Clear Selection
- `u` : Update description of selected VG (or the highlighted one)
- `Ctrl + d` : Delete selected VG (or the highlighted one)
- `q` : Quit

690 changes: 690 additions & 0 deletions ui_functions.go

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions ui_handlekeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

// This function handle every keyboards input in "view" mode
func (MyUI *UI) HandleKeyHelpMode(input string) string {
switch input {
// Quit
case "q", "Q", "<C-c>", "<Escape>":
MyUI.DisplayPopup("help", false)
}
return "continue"
}

// This function handle every keyboards input in "view" mode
func (MyUI *UI) HandleKeyViewMode(input string) string {
// Initalize returnValue
returnValue := "continue"

// Handle keyboard inputs
switch input {

// Quit
case "q", "<C-c>":
return "quit"

// Move in the list
case "<Down>", "<MouseWheelDown>":
if MyUI.List.Rows != nil {
MyUI.List.ScrollDown()
}
case "<Up>", "<MouseWheelUp>":
MyUI.List.ScrollUp()
case "<PageDown>":
if MyUI.List.Rows != nil {
MyUI.List.ScrollPageDown()
}
case "<PageUp>":
MyUI.List.ScrollPageUp()
case "<Home>":
MyUI.List.ScrollTop()
case "<End>":
if MyUI.List.Rows != nil {
MyUI.List.ScrollBottom()
}

// Refresh VG List
case "<C-r>", "<C-R>":
MyUI.Log("VG list refresh requested", "yellow", "clear")
GetVGList()
MyUI.UpdateList()
MyUI.Log("List updated", "yellow", "clear")

// Select items
case "s", "<Space>":
MyUI.Select()

// Select All
case "<C-a>":
MyUI.SelectAll()

// Clear selection
case "<C-<Space>>":
MyUI.ClearSelection()

// Change display mode
case "d", "D":
MyUI.ChangeDisplayMode()

// Display help
case "h", "H", "<F1>":
MyUI.DisplayPopup("help", true)

// Create filter
case "f", "F", "/":
MyUI.EnterFilter()
MyUI.UpdateContentFilterZone(MyUI.Filter)
MyUI.UpdateList()

// Change order
case "o", "O":
MyUI.OrderList()

// Ask for desc udpate
case "U", "u":
if MyUI.SelectedItems < 1 {
MyUI.Select()
}
MyUI.UpdateVGDescription()
MyUI.ClearSelection()

// Delete filter
case "<C-f>", "<Escape>":
MyUI.Filter = ""
MyUI.UpdateContentFilterZone("")
MyUI.UpdateList()

// Delete VG
case "<C-d>":
if MyUI.SelectedItems < 1 {
MyUI.Select()
}
if MyUI.RequestDeleteVG() {
GetVGList()
MyUI.ClearSelection()
MyUI.UpdateList()
MyUI.Log("List updated", "yellow", "clear")
}

// In case of resize of the terminal
case "<Resize>":
MyUI.Resize()
MyUI.UpdateList()
}

MyUI.UpdateDetail()
MyUI.Render()

// Return value
return returnValue
}
160 changes: 160 additions & 0 deletions ui_render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

import (
"regexp"

ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)

// Const for UI positioning
const (
ConstTitleHeight int = 3
ConstDetailHeight int = 11
ConstInteractHeight int = 3
ConstFooterHeight int = 3
ConstFilterZoneShift int = 43
ConstMaxANDFiltering int = 10
ConstZoneActive string = "(fg:black,bg:green)"
ConstZonePassive string = "(fg:black,bg:white)"
ConstZoneError string = "(fg:white,bg:red)"
ConstSelectionHighlight string = "(fg:green,bg:clear)"
ConstFilterHighlight string = "(fg:black,bg:green)"
)

// define Filtering struct
type filter_struct struct {
UUID [ConstMaxANDFiltering]*regexp.Regexp
Name [ConstMaxANDFiltering]*regexp.Regexp
Cluster [ConstMaxANDFiltering]*regexp.Regexp
Container [ConstMaxANDFiltering]*regexp.Regexp
Mounted [ConstMaxANDFiltering]*regexp.Regexp
Description [ConstMaxANDFiltering]*regexp.Regexp
Size [ConstMaxANDFiltering]*regexp.Regexp
}

// Define Struct for UI design & content
type UI struct {
Title *widgets.Paragraph
List *widgets.List
Detail *widgets.Paragraph
Interact *widgets.Paragraph
Footer *widgets.Paragraph
FilterZone *widgets.Paragraph
Debug *widgets.Paragraph
Popup *widgets.Paragraph
SelectedItems int
DisplayOrder string // uuid | name
Mode string // view | help
TermWidth int
TermHeight int
Filter string
AdvFilter filter_struct
Order string // asc | desc
TotalVG int
UglyFix bool
}

// Create/initialize UI
func (MyUI *UI) Create() {
// Set default mode and order
MyUI.Mode = "view"
MyUI.Order = "asc"
MyUI.SelectedItems = 0

// Use a string that will match to nothing as intialization
tmp, _ := regexp.Compile("###")

// Intialize the filters
for i := 0; i < ConstMaxANDFiltering; i++ {
MyUI.AdvFilter.Container[i] = tmp
MyUI.AdvFilter.UUID[i] = tmp
MyUI.AdvFilter.Name[i] = tmp
MyUI.AdvFilter.Cluster[i] = tmp
MyUI.AdvFilter.Mounted[i] = tmp
MyUI.AdvFilter.Description[i] = tmp
MyUI.AdvFilter.Size[i] = tmp
}

// Get terminal size
MyUI.TermWidth, MyUI.TermHeight = ui.TerminalDimensions()

MyUI.Title = widgets.NewParagraph()
MyUI.Title.Border = false
MyUI.Title.Text = "Nutanix VG Manager"
MyUI.Title.TextStyle.Fg = ui.ColorCyan
MyUI.Title.PaddingLeft = (MyUI.TermWidth - 3 - len(MyUI.Title.Text)) / 2
MyUI.Title.SetRect(0, 0, MyUI.TermWidth, ConstTitleHeight)

MyUI.List = widgets.NewList()
MyUI.List.Title = "VG List - Name (UUID) - Sorted : " + MyUI.Order
MyUI.List.SelectedRowStyle.Bg = ui.ColorCyan
MyUI.List.SelectedRowStyle.Fg = ui.ColorBlack
MyUI.List.WrapText = false
MyUI.List.PaddingLeft = 1
MyUI.List.PaddingRight = 1
MyUI.List.Rows = []string{}
MyUI.List.SetRect(0, ConstTitleHeight, MyUI.TermWidth, MyUI.TermHeight-ConstDetailHeight-ConstInteractHeight-ConstFooterHeight+2)
MyUI.DisplayOrder = "name"

MyUI.Detail = widgets.NewParagraph()
MyUI.Detail.Title = "Details"
MyUI.Detail.WrapText = false
MyUI.Detail.PaddingLeft = 1
MyUI.Detail.PaddingTop = 1
MyUI.Detail.SetRect(0, MyUI.TermHeight-ConstDetailHeight-ConstInteractHeight-ConstFooterHeight+2, MyUI.TermWidth, MyUI.TermHeight-ConstInteractHeight-ConstFooterHeight+2)

MyUI.Interact = widgets.NewParagraph()
MyUI.Interact.Title = "Logs/Actions"
MyUI.Interact.WrapText = false
MyUI.Interact.Border = true
MyUI.Interact.PaddingLeft = 1
MyUI.Interact.PaddingTop = 0
MyUI.Interact.SetRect(0, MyUI.TermHeight-ConstInteractHeight-ConstFooterHeight+2, MyUI.TermWidth, MyUI.TermHeight-ConstFooterHeight+2)

MyUI.Footer = widgets.NewParagraph()
MyUI.Footer.Border = false
MyUI.Footer.Text = "h : Help / Q : Quit"
MyUI.Footer.SetRect(0, MyUI.TermHeight-ConstFooterHeight+1, MyUI.TermWidth, MyUI.TermHeight+1)
MyUI.Footer.WrapText = true

MyUI.FilterZone = widgets.NewParagraph()
MyUI.FilterZone.Border = false
MyUI.FilterZone.Text = "Filter : [ ]" + ConstZonePassive
MyUI.FilterZone.SetRect(MyUI.TermWidth-ConstFilterZoneShift, ConstTitleHeight, MyUI.TermWidth-2, ConstTitleHeight+1)

MyUI.Debug = widgets.NewParagraph()
MyUI.Debug.TextStyle.Bg = ui.ColorRed
MyUI.Debug.Border = false
MyUI.Debug.SetRect(MyUI.TermWidth-50, 0, MyUI.TermWidth, 3)

MyUI.Popup = widgets.NewParagraph()
MyUI.Popup.Border = true
MyUI.Popup.PaddingLeft = 1
MyUI.Popup.PaddingTop = 1
MyUI.Popup.SetRect(0, 0, 0, 0) // We do not display it right now

// Fill list and detail
MyUI.UpdateList()
MyUI.UpdateDetail()
}

// Function to resize UI elements after terminal size change
func (MyUI *UI) Resize() {

MyUI.TermWidth, MyUI.TermHeight = ui.TerminalDimensions()
MyUI.Title.PaddingLeft = (MyUI.TermWidth - 3 - len(MyUI.Title.Text)) / 2
MyUI.Title.SetRect(0, 0, MyUI.TermWidth, ConstTitleHeight)
MyUI.List.SetRect(0, ConstTitleHeight, MyUI.TermWidth, MyUI.TermHeight-ConstDetailHeight-ConstInteractHeight-ConstFooterHeight+2)
MyUI.Detail.SetRect(0, MyUI.TermHeight-ConstDetailHeight-ConstInteractHeight-ConstFooterHeight+2, MyUI.TermWidth, MyUI.TermHeight-ConstInteractHeight-ConstFooterHeight+2)
MyUI.Interact.SetRect(0, MyUI.TermHeight-ConstInteractHeight-ConstFooterHeight+2, MyUI.TermWidth, MyUI.TermHeight-ConstFooterHeight+2)
MyUI.Footer.SetRect(0, MyUI.TermHeight-ConstFooterHeight+1, MyUI.TermWidth, MyUI.TermHeight+1)
MyUI.FilterZone.SetRect(MyUI.TermWidth-ConstFilterZoneShift, ConstTitleHeight, MyUI.TermWidth-2, ConstTitleHeight+1)
MyUI.Debug.SetRect(MyUI.TermWidth-50, 0, MyUI.TermWidth, 3)
MyUI.Popup.SetRect(0, 0, 0, 0) // We do not display it right now
}

// Render UI
func (MyUI UI) Render() {
ui.Render(MyUI.Title, MyUI.List, MyUI.Detail, MyUI.Footer, MyUI.Interact, MyUI.Debug, MyUI.FilterZone, MyUI.Popup)
}
344 changes: 344 additions & 0 deletions vg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
package main

import (
"fmt"
"strconv"
"strings"
"time"
)

// Declare VG struct
type VG struct {
Cluster string
UUID string
Name string
Container string
Description string
Size string
Attached string
Attached_vm []string
Attached_iscsi []string
}

// Create global GlobalVGList
var GlobalVGList []VG

// function to get list (without details, we want quick operation)
func GetVGList() {
var StoContainerList = make(map[string]string) // UUID => Storage Container name
var VM_List = make(map[string]string) // UUID => VM Name
var iSCSI_List = make(map[string]string) // UUID => iSCSI

// Reinit VG_List
GlobalVGList = nil

// =========================== Get Storage Container List ===========================
var tmp1 struct {
Data []struct {
ContainerExtID string `json:"containerExtId"`
Name string `json:"name"`
} `json:"data"`
}

MyPrism.CallAPIJSON("PC", "GET", "/api/storage/v4.0.a2/config/storage-containers", "", &tmp1)

// Parse all SC
for tmp := range tmp1.Data {
StoContainerList[tmp1.Data[tmp].ContainerExtID] = tmp1.Data[tmp].Name
}

// =========================== Get external iSCSI List ===========================
var tmpiscsi struct {
DataItemDiscriminator string `json:"$dataItemDiscriminator"`
Data []struct {
ExtID string `json:"extId"`
IscsiInitiatorName string `json:"iscsiInitiatorName"`
} `json:"data"`
}

// We do request until we get EMPTY_LIST
for page := 0; tmpiscsi.DataItemDiscriminator != "EMPTY_LIST"; page++ {

MyPrism.CallAPIJSON("PC", "GET", fmt.Sprintf("/api/storage/v4.0.a2/config/iscsi-clients?$limit=100&$page=%d", page), "", &tmpiscsi)
if tmpiscsi.DataItemDiscriminator != "EMPTY_LIST" {
// If list is not empty, we store everything in an array
for _, tmp := range tmpiscsi.Data {
iSCSI_List[tmp.ExtID] = tmp.IscsiInitiatorName
}
}
}

// =========================== Get VM list ===========================
var tmpvm struct {
GroupResults []struct {
EntityResults []struct {
Data []struct {
DataType string `json:"data_type"`
Name string `json:"name"`
Values []struct {
Time int64 `json:"time"`
Values []string `json:"values"`
} `json:"values"`
} `json:"data"`
EntityID string `json:"entity_id"`
} `json:"entity_results"`
} `json:"group_results"`
}

payloadvm := `{
"entity_type":"mh_vm",
"group_member_attributes":[
{
"attribute":"vm_name"
}
]
}`

MyPrism.CallAPIJSON("PC", "POST", "/api/nutanix/v3/groups", payloadvm, &tmpvm)

for _, tmp := range tmpvm.GroupResults[0].EntityResults {

vmname := ""

for _, tmp2 := range tmp.Data {
switch tmp2.Name {
case "vm_name":
vmname = tmp2.Values[0].Values[0]
}

VM_List[tmp.EntityID] = vmname

}
}

// =========================== Get VG list ===========================
var VGList struct {
GroupResults []struct {
EntityResults []struct {
Data []struct {
DataType string `json:"data_type"`
Name string `json:"name"`
Values []struct {
Time int64 `json:"time"`
Values []string `json:"values"`
} `json:"values"`
} `json:"data"`
EntityID string `json:"entity_id"`
} `json:"entity_results"`
} `json:"group_results"`
}

// We do the API call to get VG list
payload := `{
"entity_type": "volume_group_config",
"group_member_attributes": [
{
"attribute": "name"
},
{
"attribute": "controller_user_bytes"
},
{
"attribute": "client_uuids"
},
{
"attribute": "cluster_name"
},
{
"attribute": "capacity_bytes"
},
{
"attribute": "vm_uuids"
},
{
"attribute": "annotation"
},
{
"attribute": "container_uuids"
}
]
}`
MyPrism.CallAPIJSON("PC", "POST", "/api/nutanix/v3/groups", payload, &VGList)

// Parse all VG an put them in a file
for _, tmp := range VGList.GroupResults[0].EntityResults {

// Create vg
var tmpelt VG
var sto_used, sto_capacity float64

for _, tmp2 := range tmp.Data {

// Fill struct elements regarding "name" field
switch tmp2.Name {
case "name":
if len(tmp2.Values) > 0 {
tmpelt.Name = tmp2.Values[0].Values[0]
}
case "controller_user_bytes":
if len(tmp2.Values) > 0 {

conv, _ := strconv.Atoi(tmp2.Values[0].Values[0])
sto_used = float64(conv) / (1024 * 1024 * 1024)
}
case "client_uuids":
if len(tmp2.Values) > 0 {

tmpelt.Attached_iscsi = tmp2.Values[0].Values
}
case "cluster_name":
if len(tmp2.Values) > 0 {

tmpelt.Cluster = tmp2.Values[0].Values[0]
}
case "capacity_bytes":
if len(tmp2.Values) > 0 {

conv, _ := strconv.Atoi(tmp2.Values[0].Values[0])
sto_capacity = float64(conv) / (1024 * 1024 * 1024)
}
case "vm_uuids":
if len(tmp2.Values) > 0 {

tmpelt.Attached_vm = tmp2.Values[0].Values
}
case "annotation":
if len(tmp2.Values) > 0 {

tmpelt.Description = tmp2.Values[0].Values[0]
}
case "container_uuids":
if len(tmp2.Values) > 0 {

tmpelt.Container = StoContainerList[tmp2.Values[0].Values[0]]
}
}

}
tmpelt.Size = fmt.Sprintf("%0.2f (%0.2f used)", sto_capacity, sto_used)

if len(tmpelt.Attached_vm) > 0 || len(tmpelt.Attached_iscsi) > 0 {
var tmp []string
var j_vm string = ""
var j_iscsi string = ""

if len(tmpelt.Attached_vm) > 0 {
tmp = []string{}
for _, vmuuid := range tmpelt.Attached_vm {
tmp = append(tmp, VM_List[vmuuid])
}
j_vm = strings.Join(tmp, ",")
} else {
j_vm = "-"
}

if len(tmpelt.Attached_iscsi) > 0 {
tmp = []string{}
for _, iscsi_uuid := range tmpelt.Attached_iscsi {
tmp = append(tmp, iSCSI_List[iscsi_uuid])
}
j_iscsi = strings.Join(tmp, ",")
} else {
j_iscsi = "-"
}

tmpelt.Attached = fmt.Sprintf("True (VM : %s / iSCSI : %s)", j_vm, j_iscsi)
} else {
tmpelt.Attached = "False"
}
tmpelt.UUID = tmp.EntityID

GlobalVGList = append(GlobalVGList, tmpelt)
}

}

// DeleteVG UUID
func DeleteVG(uuid string) bool {
var vg VG
var IndexOfVG int

// We start by finding this UUID in VG_List
for IndexOfVG, vg = range GlobalVGList {
if vg.UUID == uuid {
break // for loop
}
}

var answer struct {
Data struct {
ExtID string `json:"extId"`
} `json:"data"`
}

// We start by detaching iscsi connection if exists
for _, tmp := range vg.Attached_iscsi {
MyPrism.CallAPIJSON("PC", "POST", "/api/storage/v4.0.a2/config/volume-groups/"+vg.UUID+"/$actions/detach-iscsi-client/"+tmp, "", &answer)

tmp2 := strings.Split(answer.Data.ExtID, ":")
state := WaitForTask(tmp2[1])

if !state {
return false
}
}

// We continue by detaching vm connection if exists
for _, tmp := range vg.Attached_vm {
MyPrism.CallAPIJSON("PC", "POST", "/api/storage/v4.0.a2/config/volume-groups/"+vg.UUID+"/$actions/detach-vm/"+tmp, "", &answer)

tmp2 := strings.Split(answer.Data.ExtID, ":")
state := WaitForTask(tmp2[1])

if !state {
return false
}
}

// Now we can delete the VG
MyPrism.CallAPIJSON("PC", "DELETE", "/api/storage/v4.0.a2/config/volume-groups/"+vg.UUID, "", &answer)

tmp2 := strings.Split(answer.Data.ExtID, ":")
state := WaitForTask(tmp2[1])

// Now we can delete VG from list
SmartDelete(IndexOfVG)

if !state {
// Deletion failed
return false
} else {
// Deletion OK
return true
}
}

// Function WaitForTask
func WaitForTask(taskID string) bool {

var answer struct {
Status string `json:"status"`
PercentageComplete int `json:"percentage_complete"`
}

MyPrism.CallAPIJSON("PC", "GET", "/api/nutanix/v3/tasks/"+taskID, "", &answer)

// We wait for end of task
for answer.PercentageComplete < 100 {
time.Sleep(time.Duration(3) * time.Second)
MyPrism.CallAPIJSON("PC", "GET", "/api/nutanix/v3/tasks/"+taskID, "", &answer)
}

if answer.Status == "SUCCEEDED" {
return true
} else {
return false
}
}

// SmartDelete
// Delete single entry from VGList without reload all VG
func SmartDelete(index int) {
GlobalVGList = append(GlobalVGList[:index], GlobalVGList[index+1:]...)

}
Binary file added video/demo.mkv
Binary file not shown.

0 comments on commit f3b38fd

Please sign in to comment.