Compare commits
2 Commits
da47558566
...
2b38fc33e4
Author | SHA1 | Date |
---|---|---|
lavenderguitar | 2b38fc33e4 | 1 year ago |
lavenderguitar | 7f78815f40 | 1 year ago |
1 changed files with 358 additions and 0 deletions
@ -0,0 +1,358 @@ |
|||
// This script runs a Terminal User Interface displaying a list of
|
|||
// EC2 Instances located in an AWS account. A valid set of AWS
|
|||
// credentials must be stored in ~/.aws/credentials before usage.
|
|||
// After running, press 'h' to view the help menu.
|
|||
//
|
|||
// Dependencies:
|
|||
// go mod init aws-helpers
|
|||
// go get .
|
|||
//
|
|||
// Use:
|
|||
// go run instances.go --profile default --region us-east-1
|
|||
//
|
|||
// Build:
|
|||
// go build instances.go
|
|||
// ./instances -p default -r us-east-1
|
|||
|
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"flag" |
|||
"fmt" |
|||
"log" |
|||
"os" |
|||
"strings" |
|||
|
|||
"github.com/aws/aws-sdk-go/aws" |
|||
"github.com/aws/aws-sdk-go/aws/session" |
|||
"github.com/aws/aws-sdk-go/service/ec2" |
|||
"github.com/rivo/tview" |
|||
"github.com/gdamore/tcell/v2" |
|||
) |
|||
|
|||
var ( |
|||
profile string |
|||
region string |
|||
currentInstances []*ec2.Instance |
|||
app = tview.NewApplication() |
|||
flex *tview.Flex |
|||
) |
|||
|
|||
// Helper function to get the Name tag for an EC2 instance.
|
|||
func getInstanceName(instance *ec2.Instance) string { |
|||
for _, tag := range instance.Tags { |
|||
if aws.StringValue(tag.Key) == "Name" && tag.Value != nil && *tag.Value != "" { |
|||
return *tag.Value |
|||
} |
|||
} |
|||
return *instance.InstanceId // default to instance ID if no Name tag found
|
|||
} |
|||
|
|||
// Helper function to filter the displayed instances by filtered state.
|
|||
func fetchAndDisplayInstances(state string, svc *ec2.EC2, list *tview.List, detailsTable *tview.Table) { |
|||
var filterName string |
|||
switch state { |
|||
case "running": |
|||
filterName = "running" |
|||
case "stopped": |
|||
filterName = "stopped" |
|||
case "terminated": |
|||
filterName = "terminated" |
|||
default: |
|||
filterName = "any" |
|||
} |
|||
|
|||
params := &ec2.DescribeInstancesInput{} |
|||
if filterName != "any" { |
|||
params.Filters = []*ec2.Filter{ |
|||
{ |
|||
Name: aws.String("instance-state-name"), |
|||
Values: []*string{aws.String(filterName)}, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
resp, err := svc.DescribeInstances(params) |
|||
if err != nil { |
|||
log.Fatalf("Failed to retrieve EC2 instances: %s", err) |
|||
} |
|||
|
|||
list.Clear() |
|||
currentInstances = nil |
|||
for _, res := range resp.Reservations { |
|||
for _, instance := range res.Instances { |
|||
currentInstances = append(currentInstances, instance) |
|||
list.AddItem(getInstanceName(instance), "", 0, nil) |
|||
} |
|||
} |
|||
|
|||
if len(currentInstances) > 0 { |
|||
displayInstanceDetails(detailsTable, currentInstances[0]) |
|||
} |
|||
} |
|||
|
|||
// Helper function to display the instance details of the first instance in the list. Function executes when application first loads.
|
|||
func initialDisplay(svc *ec2.EC2, list *tview.List, detailsTable *tview.Table) { |
|||
fetchAndDisplayInstances("running", svc, list, detailsTable) |
|||
} |
|||
|
|||
// Helper function to display the instance details.
|
|||
func displayInstanceDetails(table *tview.Table, instance *ec2.Instance) { |
|||
table.Clear() |
|||
|
|||
// Headers
|
|||
headers := []string{ |
|||
"Field", "Value", |
|||
} |
|||
|
|||
// Assign headers to table.
|
|||
for col, header := range headers { |
|||
cell := tview.NewTableCell(header). |
|||
SetTextColor(tview.Styles.SecondaryTextColor). |
|||
SetAlign(tview.AlignCenter). |
|||
SetAttributes(tcell.AttrBold) |
|||
table.SetCell(0, col, cell) |
|||
} |
|||
|
|||
// Ordered list of fields
|
|||
fields := []string{ |
|||
"State", |
|||
"Instance ID", |
|||
"Type", |
|||
"AMI ID", |
|||
"Architecture", |
|||
"Public DNS Name", |
|||
"Public IP Address", |
|||
"PrivateDnsName", |
|||
"PrivateIpAddress", |
|||
"Pem Key Name", |
|||
"VPC ID", |
|||
"Subnet ID", |
|||
} |
|||
|
|||
// Mapping of fields to their pointers
|
|||
fieldMap := map[string]**string{ |
|||
"State": &instance.State.Name, |
|||
"Instance ID": &instance.InstanceId, |
|||
"Type": &instance.InstanceType, |
|||
"AMI ID": &instance.ImageId, |
|||
"Architecture": &instance.Architecture, |
|||
"Public DNS Name": &instance.PublicDnsName, |
|||
"Public IP Address": &instance.PublicIpAddress, |
|||
"PrivateDnsName": &instance.PrivateDnsName, |
|||
"PrivateIpAddress": &instance.PrivateIpAddress, |
|||
"Pem Key Name": &instance.KeyName, |
|||
"VPC ID": &instance.VpcId, |
|||
"Subnet ID": &instance.SubnetId, |
|||
} |
|||
|
|||
// Add instance details to table.
|
|||
row := 1 |
|||
for _, field := range fields { |
|||
ptr := fieldMap[field] |
|||
value := "Unknown" |
|||
if *ptr != nil { |
|||
value = **ptr |
|||
} |
|||
table.SetCell(row, 0, tview.NewTableCell(field)) |
|||
table.SetCell(row, 1, tview.NewTableCell(value)) |
|||
row++ |
|||
} |
|||
|
|||
// Special cases that return non-string values.
|
|||
table.SetCell(row, 0, tview.NewTableCell("Iam Instance Profile Arn")) |
|||
if instance.IamInstanceProfile != nil && instance.IamInstanceProfile.Arn != nil { |
|||
table.SetCell(row, 1, tview.NewTableCell(*instance.IamInstanceProfile.Arn)) |
|||
} else { |
|||
table.SetCell(row, 1, tview.NewTableCell("No ARN")) |
|||
} |
|||
row++ |
|||
|
|||
table.SetCell(row, 0, tview.NewTableCell("Security Groups")) |
|||
if instance.SecurityGroups != nil { |
|||
var groupNames []string |
|||
for _, group := range instance.SecurityGroups { |
|||
if group != nil && group.GroupName != nil { |
|||
groupNames = append(groupNames, *group.GroupName) |
|||
} |
|||
} |
|||
table.SetCell(row, 1, tview.NewTableCell(strings.Join(groupNames, ", "))) |
|||
} else { |
|||
table.SetCell(row, 1, tview.NewTableCell("No Security Groups")) |
|||
} |
|||
} |
|||
|
|||
// Helper function to start an instance.
|
|||
func startInstance(svc *ec2.EC2, instance *ec2.Instance) error { |
|||
input := &ec2.StartInstancesInput{ |
|||
InstanceIds: []*string{ |
|||
instance.InstanceId, |
|||
}, |
|||
} |
|||
_, err := svc.StartInstances(input) |
|||
return err |
|||
} |
|||
|
|||
// Helper function to stop an instance.
|
|||
func stopInstance(svc *ec2.EC2, instance *ec2.Instance) error { |
|||
input := &ec2.StopInstancesInput{ |
|||
InstanceIds: []*string{ |
|||
instance.InstanceId, |
|||
}, |
|||
} |
|||
_, err := svc.StopInstances(input) |
|||
return err |
|||
} |
|||
|
|||
// Helper function to reboot an instance.
|
|||
func rebootInstance(svc *ec2.EC2, instance *ec2.Instance) error { |
|||
input := &ec2.RebootInstancesInput{ |
|||
InstanceIds: []*string{ |
|||
instance.InstanceId, |
|||
}, |
|||
} |
|||
_, err := svc.RebootInstances(input) |
|||
return err |
|||
} |
|||
|
|||
// Helper function to create a confirm prompt before action is taken.
|
|||
func confirmModal(title, text string, confirmFunc func()) { |
|||
modal := tview.NewModal(). |
|||
SetText(text). |
|||
AddButtons([]string{"Yes", "No"}). |
|||
SetDoneFunc(func(buttonIndex int, buttonLabel string) { |
|||
if buttonLabel == "Yes" { |
|||
confirmFunc() |
|||
} |
|||
app.SetRoot(flex, true) |
|||
}) |
|||
fmt.Println("Attempting to confirm start instance") |
|||
app.SetRoot(modal, true) |
|||
} |
|||
|
|||
// Helper function to display any AWS operation errors to the user.
|
|||
func handleEC2OperationResult(err error, successMsg string, list *tview.List, detailsTable *tview.Table) { |
|||
if err != nil { |
|||
// Show error modal
|
|||
errorModal := tview.NewModal(). |
|||
SetText("Error: " + err.Error()). |
|||
AddButtons([]string{"Close"}). |
|||
SetDoneFunc(func(buttonIndex int, buttonLabel string) { |
|||
app.SetRoot(flex, true) |
|||
}) |
|||
app.SetRoot(errorModal, true) |
|||
} else { |
|||
// Refresh the current instance details
|
|||
displayInstanceDetails(detailsTable, currentInstances[list.GetCurrentItem()]) |
|||
} |
|||
} |
|||
|
|||
func main() { |
|||
// Require profile and region from the user.
|
|||
flag.StringVar(&profile, "profile", "", "AWS Profile") |
|||
flag.StringVar(&profile, "p", "", "AWS Profile (shorthand)") |
|||
flag.StringVar(®ion, "region", "", "AWS Region") |
|||
flag.StringVar(®ion, "r", "", "AWS Region (shorthand)") |
|||
flag.Parse() |
|||
|
|||
if profile == "" || region == "" { |
|||
fmt.Println("Both --profile (-p) and --region (-r) are required.") |
|||
os.Exit(1) |
|||
} |
|||
|
|||
// Establish AWS connection.
|
|||
sess, err := session.NewSessionWithOptions(session.Options{ |
|||
Profile: profile, |
|||
Config: aws.Config{ |
|||
Region: aws.String(region), |
|||
}, |
|||
}) |
|||
if err != nil { |
|||
log.Fatalf("Failed to initialize AWS session: %s", err) |
|||
} |
|||
|
|||
svc := ec2.New(sess) |
|||
|
|||
// Create the application.
|
|||
app = tview.NewApplication() |
|||
list := tview.NewList().ShowSecondaryText(false) |
|||
detailsTable := tview.NewTable().SetBorders(true) |
|||
list.SetBorder(true).SetTitle("Instances") |
|||
detailsTable.SetBorder(true).SetTitle("Instance Details") |
|||
|
|||
initialDisplay(svc, list, detailsTable) |
|||
|
|||
// Changed function to keep index and currentInstances slice in sync during user navigation.
|
|||
list.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { |
|||
if index >= 0 && index < len(currentInstances) { |
|||
displayInstanceDetails(detailsTable, currentInstances[index]) |
|||
} |
|||
}) |
|||
|
|||
// Global view and add instance information.
|
|||
flex = tview.NewFlex(). |
|||
AddItem(list, 0, 1, true). |
|||
AddItem(detailsTable, 0, 2, false) |
|||
|
|||
// Create the help modal.
|
|||
helpModal := tview.NewModal(). |
|||
SetText("Keyboard Commands:\n\nh - Show this help menu\nq - Quit the program\nr - Display running instances\na - Display all instances\ns - Display stopped instances\nt - Display terminated instances\n'S'(Shift+S) - Start instance.\n'X'(Shift+X) - Stop instance.\n'R'(Shift+r) - Reboot instance."). |
|||
AddButtons([]string{"Close"}). |
|||
SetDoneFunc(func(buttonIndex int, buttonLabel string) { |
|||
if buttonLabel == "Close" { |
|||
app.SetRoot(flex, true) |
|||
} |
|||
}) |
|||
|
|||
// Assign keybindings.
|
|||
app.SetRoot(flex, true). |
|||
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { |
|||
switch event.Key() { |
|||
case tcell.KeyRune: |
|||
switch event.Rune() { |
|||
case 'h': // Display help modal.
|
|||
app.SetRoot(helpModal, true) |
|||
return nil |
|||
case 'q': // Quit the application.
|
|||
app.Stop() |
|||
case 'r': // Display running instances
|
|||
fetchAndDisplayInstances("running", svc, list, detailsTable) |
|||
return nil |
|||
case 'a': // Display any(all) instances.
|
|||
fetchAndDisplayInstances("any", svc, list, detailsTable) |
|||
return nil |
|||
case 's': // Display terminated instances.
|
|||
fetchAndDisplayInstances("stopped", svc, list, detailsTable) |
|||
return nil |
|||
case 't': // Display terminated instances.
|
|||
fetchAndDisplayInstances("terminated", svc, list, detailsTable) |
|||
return nil |
|||
case 'S': // Start Instance.
|
|||
confirmModal("Start Instance", "Are you sure you want to start the instance?", func() { |
|||
fmt.Println("Executing start instance command") |
|||
err := startInstance(svc, currentInstances[list.GetCurrentItem()]) |
|||
handleEC2OperationResult(err, "Successfully started the instance.", list, detailsTable) |
|||
}) |
|||
return nil |
|||
case 'X': // Stop Instance.
|
|||
confirmModal("Stop Instance", "Are you sure you want to stop the instance?", func() { |
|||
err := stopInstance(svc, currentInstances[list.GetCurrentItem()]) |
|||
handleEC2OperationResult(err, "Successfully stopped the instance.", list, detailsTable) |
|||
}) |
|||
return nil |
|||
case 'R': // Reboot Instance.
|
|||
confirmModal("Reboot Instance", "Are you sure you want to reboot the instance?", func() { |
|||
err := rebootInstance(svc, currentInstances[list.GetCurrentItem()]) |
|||
handleEC2OperationResult(err, "Successfully rebooted the instance.", list, detailsTable) |
|||
}) |
|||
} |
|||
} |
|||
return event |
|||
}) |
|||
|
|||
// Exit the application on panic.
|
|||
if err := app.Run(); err != nil { |
|||
panic(err) |
|||
} |
|||
} |
Loading…
Reference in new issue