From 7f78815f404fd4971d8c0881175272e0dd588ca1 Mon Sep 17 00:00:00 2001 From: lavenderguitar Date: Mon, 28 Aug 2023 11:10:33 -0400 Subject: [PATCH] New go script for viewing and performing actions against AWS EC2 Instances. --- go/instances.go | 338 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 go/instances.go diff --git a/go/instances.go b/go/instances.go new file mode 100644 index 0000000..d26b8aa --- /dev/null +++ b/go/instances.go @@ -0,0 +1,338 @@ +// 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. +// +// Use: +// go run instances.go --profile default --region us-east-1 +// +// Build and Use: +// go build . +// ./instances --profile default --region 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) +} + +func displayInstanceDetails(table *tview.Table, instance *ec2.Instance) { + table.Clear() + + // Headers + headers := []string{ + "Field", "Value", + } + + 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, + } + + 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 + 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")) + } +} + +func startInstance(svc *ec2.EC2, instance *ec2.Instance) error { + input := &ec2.StartInstancesInput{ + InstanceIds: []*string{ + instance.InstanceId, + }, + } + _, err := svc.StartInstances(input) + return err +} + +func stopInstance(svc *ec2.EC2, instance *ec2.Instance) error { + input := &ec2.StopInstancesInput{ + InstanceIds: []*string{ + instance.InstanceId, + }, + } + _, err := svc.StopInstances(input) + return err +} + +func rebootInstance(svc *ec2.EC2, instance *ec2.Instance) error { + input := &ec2.RebootInstancesInput{ + InstanceIds: []*string{ + instance.InstanceId, + }, + } + _, err := svc.RebootInstances(input) + return err +} + +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) +} + +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() { + 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) + } + + 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) + + 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) + + list.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { + if index >= 0 && index < len(currentInstances) { + displayInstanceDetails(detailsTable, currentInstances[index]) + } + }) + + flex = tview.NewFlex(). + AddItem(list, 0, 1, true). + AddItem(detailsTable, 0, 2, false) + + 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) + } + }) + + app.SetRoot(flex, true). + SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyRune: + switch event.Rune() { + case 'h': + app.SetRoot(helpModal, true) + return nil + case 'q': + app.Stop() + case 'r': + fetchAndDisplayInstances("running", svc, list, detailsTable) + return nil + case 'a': + fetchAndDisplayInstances("any", svc, list, detailsTable) + return nil + case 's': + fetchAndDisplayInstances("stopped", svc, list, detailsTable) + return nil + case 't': + 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 + }) + + if err := app.Run(); err != nil { + panic(err) + } +}