-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from Protozoan1369/develop
Initial release
- Loading branch information
Showing
4 changed files
with
387 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,14 @@ | ||
# LSN_TUI | ||
Terminal User Interface for Limestonenetworks because their web interface is slow and clunky. | ||
|
||
``` | ||
go run main.go -username '[email protected]' -password '<super secret password>' | ||
``` | ||
|
||
## TODO | ||
* Search feature | ||
* Sort by ID | ||
* Refresh feature | ||
* Stop server | ||
* Start server | ||
* Other things from the [API](https://api.dallas-idc.com/v1/documentation) |
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,20 @@ | ||
module github.com/quiknod-labs/lsn_tui | ||
|
||
go 1.22.3 | ||
|
||
require ( | ||
github.com/go-resty/resty/v2 v2.15.3 | ||
github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 | ||
) | ||
|
||
require ( | ||
github.com/gdamore/encoding v1.0.0 // indirect | ||
github.com/gdamore/tcell/v2 v2.7.1 // indirect | ||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | ||
github.com/mattn/go-runewidth v0.0.15 // indirect | ||
github.com/rivo/uniseg v0.4.7 // indirect | ||
golang.org/x/net v0.27.0 // indirect | ||
golang.org/x/sys v0.22.0 // indirect | ||
golang.org/x/term v0.22.0 // indirect | ||
golang.org/x/text v0.16.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,59 @@ | ||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= | ||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | ||
github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= | ||
github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= | ||
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= | ||
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= | ||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | ||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= | ||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||
github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 h1:oa+fljZiaJUVyiT7WgIM3OhirtwBm0LJA97LvWUlBu8= | ||
github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= | ||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= | ||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= | ||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= | ||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= | ||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= | ||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= | ||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
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,294 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"os" | ||
|
||
"github.com/gdamore/tcell/v2" | ||
"github.com/go-resty/resty/v2" | ||
"github.com/rivo/tview" | ||
) | ||
|
||
type RebootResponse struct { | ||
Status bool `json:"status"` | ||
Message string `json:"message"` | ||
} | ||
|
||
type IPSubnet struct { | ||
Block string `json:"block"` | ||
NetworkType string `json:"network_type"` | ||
} | ||
|
||
type PackageItem struct { | ||
Category string `json:"category"` | ||
Option string `json:"option"` | ||
} | ||
|
||
type Package struct { | ||
ClientID int `json:"client_id"` | ||
Core string `json:"core"` | ||
Hostname string `json:"hostname"` | ||
Items []PackageItem `json:"items"` | ||
Name string `json:"name"` | ||
Status string `json:"status"` | ||
} | ||
|
||
type Server struct { | ||
ServerID string `json:"server_id"` | ||
Facility string `json:"facility"` | ||
IPSubnets []IPSubnet `json:"ip_subnets"` | ||
ManagementIP string `json:"management_ip"` | ||
Package Package `json:"package"` | ||
} | ||
|
||
type Credentials struct { | ||
Username string | ||
Password string | ||
} | ||
|
||
var ( | ||
app *tview.Application | ||
pages *tview.Pages | ||
serverTable *tview.Table | ||
credentials Credentials | ||
servers []Server | ||
apiUrl = "https://api.dallas-idc.com/v1/server" | ||
) | ||
|
||
func main() { | ||
username := flag.String("username", "", "API username") | ||
password := flag.String("password", "", "API password") | ||
flag.Parse() | ||
|
||
if *username == "" || *password == "" { | ||
fmt.Println("Please provide both username and password") | ||
flag.PrintDefaults() | ||
os.Exit(1) | ||
} | ||
|
||
credentials = Credentials{ | ||
Username: *username, | ||
Password: *password, | ||
} | ||
|
||
app = tview.NewApplication() | ||
pages = tview.NewPages() | ||
|
||
if err := fetchServers(); err != nil { | ||
log.Fatalf("Error fetching servers: %v", err) | ||
} | ||
|
||
showServerList() | ||
|
||
if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil { | ||
log.Fatalf("Error running application: %v", err) | ||
} | ||
} | ||
|
||
func showServerList() { | ||
// Create a flex layout | ||
flex := tview.NewFlex().SetDirection(tview.FlexRow) | ||
|
||
// Create a new flex for the title and total count | ||
topFlex := tview.NewFlex().SetDirection(tview.FlexColumn) | ||
|
||
// Add title to the left | ||
title := tview.NewTextView().SetText("Server List").SetTextColor(tcell.ColorWhite) | ||
topFlex.AddItem(title, 0, 1, false) | ||
|
||
// Add total count to the right | ||
totalCount := tview.NewTextView().SetText(fmt.Sprintf("Total Servers: %d", len(servers))).SetTextColor(tcell.ColorGreen).SetTextAlign(tview.AlignRight) | ||
topFlex.AddItem(totalCount, 0, 1, false) | ||
|
||
// Add the top flex to the main flex | ||
flex.AddItem(topFlex, 1, 0, false) | ||
|
||
// Create and populate the server table | ||
serverTable = tview.NewTable().SetSelectable(true, false) | ||
|
||
// Add headers | ||
serverTable.SetCell(0, 0, tview.NewTableCell("ID").SetTextColor(tcell.ColorYellow).SetSelectable(false)) | ||
serverTable.SetCell(0, 1, tview.NewTableCell("name").SetTextColor(tcell.ColorYellow).SetSelectable(false)) | ||
serverTable.SetCell(0, 2, tview.NewTableCell("Public IP").SetTextColor(tcell.ColorYellow).SetSelectable(false)) | ||
|
||
// Add server data | ||
for i, server := range servers { | ||
publicIP := getPublicIP(server.IPSubnets) | ||
serverTable.SetCell(i+1, 0, tview.NewTableCell(server.ServerID)) | ||
serverTable.SetCell(i+1, 1, tview.NewTableCell(server.Package.Name)) | ||
serverTable.SetCell(i+1, 2, tview.NewTableCell(publicIP)) | ||
} | ||
|
||
serverTable.Select(1, 0).SetFixed(1, 0).SetDoneFunc(func(key tcell.Key) { | ||
if key == tcell.KeyEscape { | ||
app.Stop() | ||
} | ||
}).SetSelectedFunc(func(row int, column int) { | ||
if row > 0 && row <= len(servers) { | ||
showServerMenu(servers[row-1]) | ||
} | ||
}) | ||
|
||
// Add the server table to the main flex | ||
flex.AddItem(serverTable, 0, 1, true) | ||
|
||
// Add the flex layout to the pages | ||
pages.AddPage("serverList", flex, true, true) | ||
} | ||
|
||
func showServerMenu(server Server) { | ||
menu := tview.NewList().ShowSecondaryText(false) | ||
menu.SetBorder(true).SetTitle(fmt.Sprintf("Server: %s", server.ServerID)) | ||
|
||
menu.AddItem("View Details", "", 'd', func() { | ||
showServerDetails(server) | ||
}) | ||
menu.AddItem("Restart Server", "", 'r', func() { | ||
showConfirmationDialog(server, func() { | ||
statusCode, rebootResp, err := rebootServer(server.ServerID) | ||
if err != nil { | ||
showMessage(fmt.Sprintf("Error restarting server %s: %v", server.ServerID, err)) | ||
} else { | ||
message := fmt.Sprintf("Restart command for server %s\nStatus Code: %d\nSuccess: %t\nMessage: %s", | ||
server.ServerID, statusCode, rebootResp.Status, rebootResp.Message) | ||
showMessage(message) | ||
} | ||
}) | ||
}) | ||
menu.AddItem("Power Off Server", "", 'o', func() { | ||
powerOffServer(server.ServerID) | ||
}) | ||
menu.AddItem("Power On Server", "", 'n', func() { | ||
powerOnServer(server.ServerID) | ||
}) | ||
menu.AddItem("Back to Server List", "", 'b', func() { | ||
pages.SwitchToPage("serverList") | ||
}) | ||
|
||
pages.AddPage("serverMenu", menu, true, true) | ||
pages.SwitchToPage("serverMenu") | ||
} | ||
|
||
func showServerDetails(server Server) { | ||
details := tview.NewTextView().SetDynamicColors(true).SetRegions(true) | ||
details.SetBorder(true).SetTitle(fmt.Sprintf("Details: %s", server.ServerID)) | ||
|
||
fmt.Fprintf(details, "[yellow]Server ID:[white] %s\n", server.ServerID) | ||
fmt.Fprintf(details, "[yellow]Hostname:[white] %s\n", server.Package.Hostname) | ||
fmt.Fprintf(details, "[yellow]Facility:[white] %s\n", server.Facility) | ||
fmt.Fprintf(details, "[yellow]Management IP:[white] %s\n", server.ManagementIP) | ||
fmt.Fprintf(details, "[yellow]Status:[white] %s\n", server.Package.Status) | ||
fmt.Fprintf(details, "[yellow]CPU:[white] %s\n", server.Package.Core) | ||
fmt.Fprintf(details, "[yellow]RAM:[white] %s\n", getItemOption(server.Package.Items, "RAM")) | ||
fmt.Fprintf(details, "[yellow]Storage:[white] %s\n", getItemOption(server.Package.Items, "Hard Drive")) | ||
fmt.Fprintf(details, "[yellow]OS:[white] %s\n", getItemOption(server.Package.Items, "Operating System")) | ||
|
||
details.SetDoneFunc(func(key tcell.Key) { | ||
if key == tcell.KeyEscape { | ||
pages.SwitchToPage("serverMenu") | ||
} | ||
}) | ||
|
||
pages.AddPage("serverDetails", details, true, true) | ||
pages.SwitchToPage("serverDetails") | ||
} | ||
|
||
func fetchServers() error { | ||
client := resty.New() | ||
resp, err := client.R(). | ||
SetBasicAuth(credentials.Username, credentials.Password). | ||
Get(apiUrl) | ||
if err != nil { | ||
return fmt.Errorf("error making request: %v", err) | ||
} | ||
|
||
if resp.StatusCode() != 200 { | ||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode()) | ||
} | ||
|
||
err = json.Unmarshal(resp.Body(), &servers) | ||
if err != nil { | ||
return fmt.Errorf("error unmarshaling response: %v", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func getItemOption(items []PackageItem, category string) string { | ||
for _, item := range items { | ||
if item.Category == category { | ||
return item.Option | ||
} | ||
} | ||
return "N/A" | ||
} | ||
|
||
func getPublicIP(subnets []IPSubnet) string { | ||
for _, subnet := range subnets { | ||
if subnet.NetworkType == "public" { | ||
return subnet.Block | ||
} | ||
} | ||
return "N/A" | ||
} | ||
|
||
func rebootServer(serverID string) (int, RebootResponse, error) { | ||
client := resty.New() | ||
resp, err := client.R(). | ||
SetBasicAuth(credentials.Username, credentials.Password). | ||
Get(fmt.Sprintf("%s/%s/restart", apiUrl, serverID)) | ||
if err != nil { | ||
return 0, RebootResponse{}, fmt.Errorf("error making request: %v", err) | ||
} | ||
|
||
statusCode := resp.StatusCode() | ||
|
||
var rebootResp RebootResponse | ||
err = json.Unmarshal(resp.Body(), &rebootResp) | ||
if err != nil { | ||
return statusCode, RebootResponse{}, fmt.Errorf("error unmarshaling response: %v", err) | ||
} | ||
|
||
return statusCode, rebootResp, nil | ||
} | ||
|
||
func powerOffServer(serverID string) { | ||
// Implement power off API call here | ||
showMessage(fmt.Sprintf("Powering off server %s", serverID)) | ||
} | ||
|
||
func powerOnServer(serverID string) { | ||
// Implement power on API call here | ||
showMessage(fmt.Sprintf("Powering on server %s", serverID)) | ||
} | ||
|
||
func showMessage(message string) { | ||
modal := tview.NewModal(). | ||
SetText(message). | ||
AddButtons([]string{"OK"}). | ||
SetDoneFunc(func(buttonIndex int, buttonLabel string) { | ||
pages.SwitchToPage("serverList") | ||
}) | ||
|
||
pages.AddPage("message", modal, true, true) | ||
pages.SwitchToPage("message") | ||
} | ||
|
||
func showConfirmationDialog(server Server, onConfirm func()) { | ||
modal := tview.NewModal(). | ||
SetText(fmt.Sprintf("Are you sure you want to restart server %s?", server.ServerID)). | ||
AddButtons([]string{"Yes", "No"}). | ||
SetDoneFunc(func(buttonIndex int, buttonLabel string) { | ||
if buttonLabel == "Yes" { | ||
onConfirm() | ||
} else { | ||
pages.SwitchToPage("serverMenu") | ||
} | ||
}) | ||
|
||
pages.AddPage("confirmationDialog", modal, true, true) | ||
pages.SwitchToPage("confirmationDialog") | ||
} |