-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
0 parents
commit f3b38fd
Showing
14 changed files
with
1,772 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
dist/ | ||
binaries/ | ||
BuildBinaries.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.