Skip to content

Commit

Permalink
Merge pull request #4 from Protozoan1369/develop
Browse files Browse the repository at this point in the history
Initial release
  • Loading branch information
Protozoan1369 authored Oct 2, 2024
2 parents 833cc30 + 49b57d2 commit af37ddb
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
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)
20 changes: 20 additions & 0 deletions go.mod
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
)
59 changes: 59 additions & 0 deletions go.sum
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=
294 changes: 294 additions & 0 deletions main.go
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")
}

0 comments on commit af37ddb

Please sign in to comment.