From b351816c9883d0155fdc2cf2f3aa990de372072b Mon Sep 17 00:00:00 2001 From: "Matthew C. Morgan" Date: Fri, 14 Mar 2025 15:59:28 -0400 Subject: [PATCH] modules and makefile --- .../gfl-resource-actions/Makefile | 92 ++ .../gfl-resource-actions/__init__.py | 6 + .../aws_resource_management.cpython-311.pyc | Bin 0 -> 1512 bytes .../__pycache__/main.cpython-311.pyc | Bin 0 -> 7064 bytes .../aws_resource_management.py | 51 ++ .../gfl-resource-actions/aws_utils.py | 0 .../gfl-resource-actions/config.py | 27 + .../discovery/discovery.py | 133 +++ .../gfl-resource-actions/gliffy.py | 822 ------------------ .../logging_utils/logging_utils.py | 18 + .../python-tools/gfl-resource-actions/main.py | 104 +++ .../gfl-resource-actions/managers/__init__.py | 8 + .../gfl-resource-actions/managers/base.py | 46 + .../gfl-resource-actions/managers/ec2.py | 91 ++ .../gfl-resource-actions/managers/eks.py | 195 +++++ .../gfl-resource-actions/managers/emr.py | 82 ++ .../gfl-resource-actions/managers/rds.py | 60 ++ 17 files changed, 913 insertions(+), 822 deletions(-) create mode 100644 local-app/python-tools/gfl-resource-actions/Makefile create mode 100644 local-app/python-tools/gfl-resource-actions/__init__.py create mode 100644 local-app/python-tools/gfl-resource-actions/__pycache__/aws_resource_management.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/__pycache__/main.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/aws_resource_management.py create mode 100644 local-app/python-tools/gfl-resource-actions/aws_utils.py create mode 100644 local-app/python-tools/gfl-resource-actions/config.py create mode 100644 local-app/python-tools/gfl-resource-actions/discovery/discovery.py delete mode 100644 local-app/python-tools/gfl-resource-actions/gliffy.py create mode 100644 local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py create mode 100644 local-app/python-tools/gfl-resource-actions/main.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__init__.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/base.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/ec2.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/eks.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/emr.py create mode 100644 local-app/python-tools/gfl-resource-actions/managers/rds.py diff --git a/local-app/python-tools/gfl-resource-actions/Makefile b/local-app/python-tools/gfl-resource-actions/Makefile new file mode 100644 index 00000000..05025e45 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/Makefile @@ -0,0 +1,92 @@ +# AWS Resource Management +# Makefile for development, testing, and execution + +.PHONY: help setup install-dev code-check run-dry-run run-stop run-start \ + run-stop-ec2 run-start-ec2 clean all + +# Variables +SCRIPT_DIR = $(shell pwd) +MODULE_PATH = $(SCRIPT_DIR)/aws_resource_management.py +LOG_FILE = aws_resource_management.log + +# Default target +help: + @echo "AWS Resource Management Tool Makefile" + @echo "" + @echo "Available targets:" + @echo " help - Show this help message" + @echo " setup - Set up the development environment" + @echo " install-dev - Install development dependencies" + @echo " code-check - Basic code checks (uses 'python -m py_compile')" + @echo " run-dry-run - Run the tool in dry-run mode (no changes)" + @echo " run-stop - Run the tool to stop all resources" + @echo " run-start - Run the tool to start all resources" + @echo " run-stop-ec2 - Run the tool to stop EC2 instances only" + @echo " run-start-ec2 - Run the tool to start EC2 instances only" + @echo " clean - Remove temporary and generated files" + @echo " all - Run code-check" + +# Setup the development environment +setup: install-dev + +# Install development dependencies +install-dev: + pip install boto3 + +# Basic code check - no external tools needed +code-check: + @echo "Checking Python files for syntax errors..." + @if [ -f $(MODULE_PATH) ]; then \ + python -m py_compile $(MODULE_PATH) && echo "No syntax errors found in main script."; \ + else \ + echo "Warning: Main script $(MODULE_PATH) not found."; \ + fi + @if [ -d aws_resource_management ]; then \ + find aws_resource_management -name "*.py" -type f -exec python -m py_compile {} \; && \ + echo "No syntax errors found in module files."; \ + else \ + echo "Note: aws_resource_management directory not found. Skipping module checks."; \ + fi + +# Run basic test verification +test: + @echo "Basic verification:" + @if [ -f $(MODULE_PATH) ]; then \ + echo "Main script exists."; \ + else \ + echo "Main script $(MODULE_PATH) not found."; \ + fi + @echo "For full testing, install pytest: pip install pytest pytest-mock" + +# Run the tool in dry-run mode +run-dry-run: + python $(MODULE_PATH) --dry-run + +# Run the tool to stop all resources +run-stop: + python $(MODULE_PATH) --action stop + +# Run the tool to start all resources +run-start: + python $(MODULE_PATH) --action start + +# Run the tool to stop EC2 instances only +run-stop-ec2: + python $(MODULE_PATH) --action stop --exclude-resources rds eks emr + +# Run the tool to start EC2 instances only +run-start-ec2: + python $(MODULE_PATH) --action start --exclude-resources rds eks emr + +# Clean up temporary files +clean: + rm -f $(LOG_FILE) + find . -name "__pycache__" -type d -exec rm -rf {} + + find . -name "*.pyc" -delete + find . -name "*.pyo" -delete + find . -name "*.pyd" -delete + find . -name ".pytest_cache" -type d -exec rm -rf {} + + @echo "Cleaned up temporary files." + +# Run all development tasks +all: clean setup code-check test run-dry-run diff --git a/local-app/python-tools/gfl-resource-actions/__init__.py b/local-app/python-tools/gfl-resource-actions/__init__.py new file mode 100644 index 00000000..37c99dfc --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/__init__.py @@ -0,0 +1,6 @@ +""" +AWS Resource Management package. +Provides tools for managing AWS resources across multiple accounts. +""" + +__version__ = "1.0.0" diff --git a/local-app/python-tools/gfl-resource-actions/__pycache__/aws_resource_management.cpython-311.pyc b/local-app/python-tools/gfl-resource-actions/__pycache__/aws_resource_management.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..975de38cf517d3d3571f37a4e603489af75c3c63 GIT binary patch literal 1512 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 literal 0 HcmV?d00001 diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management.py new file mode 100644 index 00000000..5afe76c3 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Runner script for AWS Resource Management. +Acts as a wrapper for the main.py script. +""" + +import sys +import os +import tempfile +import subprocess + +if __name__ == "__main__": + # Get the current script directory + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # Path to main.py + main_path = os.path.join(script_dir, "main.py") + + # Check if the main script exists + if not os.path.exists(main_path): + print(f"Error: Main script not found at {main_path}") + sys.exit(1) + + # Create a temporary version of main.py with absolute imports + with open(main_path, "r") as f: + content = f.read() + + # Replace relative imports with absolute imports + content = content.replace("from . import config", "import config") + content = content.replace("from .logging_utils", "from logging_utils") + content = content.replace("from .aws_utils", "from aws_utils") + content = content.replace("from .discovery", "from discovery") + content = content.replace("from .managers", "from managers") + + # Write to temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=".py", delete=False) + temp_file.write(content.encode('utf-8')) + temp_file.close() + + try: + # Execute the temporary file with the current script directory in PYTHONPATH + cmd = [sys.executable, temp_file.name] + sys.argv[1:] + env = os.environ.copy() + env['PYTHONPATH'] = script_dir + os.pathsep + env.get('PYTHONPATH', '') + + # Run the command and exit with its return code + result = subprocess.run(cmd, env=env) + sys.exit(result.returncode) + finally: + # Clean up the temporary file + os.unlink(temp_file.name) diff --git a/local-app/python-tools/gfl-resource-actions/aws_utils.py b/local-app/python-tools/gfl-resource-actions/aws_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/local-app/python-tools/gfl-resource-actions/config.py b/local-app/python-tools/gfl-resource-actions/config.py new file mode 100644 index 00000000..060abc19 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/config.py @@ -0,0 +1,27 @@ +""" +Configuration settings for AWS Resource Management. +""" + +# Role name to assume in member accounts +ASSUME_ROLE_NAME = "OrganizationAccountAccessRole" + +# Default node group sizes for EKS when starting +DEFAULT_NG_MIN_SIZE = 1 +DEFAULT_NG_DESIRED_SIZE = 1 + +# Tag keys for storing node group sizes +TAG_KEY_ORIGINAL_MIN_SIZE = "managed-by:resource-management:original-min-size" +TAG_KEY_ORIGINAL_DESIRED_SIZE = "managed-by:resource-management:original-desired-size" + +# Exclusion tag - resources with this tag will be excluded from management +EXCLUSION_TAG = "gfl_exclude" + +# Service-specific tags that indicate managed EC2 instances +EKS_TAG = "eks:cluster-name" +EMR_TAG = "aws:elasticmapreduce:job-flow-id" + +# Tag for tracking stop action +STOP_TAG = "gfl_stop" + +# Log file name +LOG_FILE = "aws_resource_management.log" diff --git a/local-app/python-tools/gfl-resource-actions/discovery/discovery.py b/local-app/python-tools/gfl-resource-actions/discovery/discovery.py new file mode 100644 index 00000000..d4966145 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/discovery/discovery.py @@ -0,0 +1,133 @@ +""" +Resource discovery functions for AWS resources. +""" +from . import config +from .aws_utils import create_boto3_client +from .logging_utils import setup_logging + +logger = setup_logging() + +def get_ec2_instances(credentials, region): + """Get EC2 instances in a region.""" + try: + ec2_client = create_boto3_client('ec2', credentials, region) + instances = [] + + response = ec2_client.describe_instances() + for reservation in response.get('Reservations', []): + for instance in reservation.get('Instances', []): + # Capture tags for exclusion check + tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])} + + instances.append({ + "id": instance['InstanceId'], + "state": instance['State']['Name'], + "region": region, + "tags": tags + }) + + # Handle pagination + while 'NextToken' in response: + response = ec2_client.describe_instances(NextToken=response['NextToken']) + for reservation in response.get('Reservations', []): + for instance in reservation.get('Instances', []): + # Capture tags for exclusion check + tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])} + + instances.append({ + "id": instance['InstanceId'], + "state": instance['State']['Name'], + "region": region, + "tags": tags + }) + + return instances + except Exception as e: + logger.error(f"Error getting EC2 instances in region {region}: {e}") + return [] + +def get_rds_instances(credentials, region): + """Get RDS instances in a region.""" + try: + rds_client = create_boto3_client('rds', credentials, region) + instances = [] + + response = rds_client.describe_db_instances() + for instance in response.get('DBInstances', []): + # Capture tags for exclusion check + tags = {} + try: + tag_list = rds_client.list_tags_for_resource( + ResourceName=instance['DBInstanceArn'] + ).get('TagList', []) + tags = {tag['Key']: tag['Value'] for tag in tag_list} + except Exception as tag_e: + logger.warning(f"Error getting tags for RDS instance {instance['DBInstanceIdentifier']}: {tag_e}") + + instances.append({ + "id": instance['DBInstanceIdentifier'], + "status": instance['DBInstanceStatus'], + "region": region, + "tags": tags + }) + + # Handle pagination + while 'Marker' in response: + response = rds_client.describe_db_instances(Marker=response['Marker']) + for instance in response.get('DBInstances', []): + # Capture tags for exclusion check + tags = {} + try: + tag_list = rds_client.list_tags_for_resource( + ResourceName=instance['DBInstanceArn'] + ).get('TagList', []) + tags = {tag['Key']: tag['Value'] for tag in tag_list} + except Exception as tag_e: + logger.warning(f"Error getting tags for RDS instance {instance['DBInstanceIdentifier']}: {tag_e}") + + instances.append({ + "id": instance['DBInstanceIdentifier'], + "status": instance['DBInstanceStatus'], + "region": region, + "tags": tags + }) + + return instances + except Exception as e: + logger.error(f"Error getting RDS instances in region {region}: {e}") + return [] + +# Similarly implement get_eks_clusters() and get_emr_clusters()... + +def get_account_resources(credentials, regions, exclude_resources=None): + """Get all resources from an account across specified regions.""" + if exclude_resources is None: + exclude_resources = [] + + resources = { + "ec2_instances": [], + "rds_instances": [], + "eks_clusters": [], + "emr_clusters": [] + } + + for region in regions: + # Get EC2 instances + if "ec2" not in exclude_resources: + resources["ec2_instances"].extend(get_ec2_instances(credentials, region)) + + # Get RDS instances + if "rds" not in exclude_resources: + resources["rds_instances"].extend(get_rds_instances(credentials, region)) + + # Get EKS clusters + if "eks" not in exclude_resources: + # Implementation would go here + pass + + # Get EMR clusters + if "emr" not in exclude_resources: + # Implementation would go here + pass + + return resources diff --git a/local-app/python-tools/gfl-resource-actions/gliffy.py b/local-app/python-tools/gfl-resource-actions/gliffy.py deleted file mode 100644 index 191c55b5..00000000 --- a/local-app/python-tools/gfl-resource-actions/gliffy.py +++ /dev/null @@ -1,822 +0,0 @@ -#!/usr/bin/env python3 -import boto3 -import logging -import argparse -import sys -import time -from botocore.exceptions import ClientError - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("aws_resource_management.log"), - logging.StreamHandler(sys.stdout) - ] -) -logger = logging.getLogger("AWS-Resource-Management") - -# Constants -ASSUME_ROLE_NAME = "OrganizationAccountAccessRole" # Typically available in member accounts -REGIONS = [] # Empty list means all regions will be discovered -DEFAULT_NG_MIN_SIZE = 1 # Default minimum size when starting EKS node groups -DEFAULT_NG_DESIRED_SIZE = 1 # Default desired capacity when starting EKS node groups -# Tag keys for storing node group sizes -TAG_KEY_ORIGINAL_MIN_SIZE = "managed-by:resource-management:original-min-size" -TAG_KEY_ORIGINAL_DESIRED_SIZE = "managed-by:resource-management:original-desired-size" -# Exclusion tag - resources with this tag will be excluded from management -EXCLUSION_TAG = "gfl_exclude" -# Tag for tracking stop action -STOP_TAG = "gfl_stop" -# Service-specific tags that indicate managed EC2 instances -EKS_TAG = "eks:cluster-name" -EMR_TAG = "aws:elasticmapreduce:job-flow-id" - -def parse_arguments(): - """Parse command-line arguments""" - parser = argparse.ArgumentParser(description="AWS Organization Resource Management Tool") - parser.add_argument("--action", choices=["stop", "start"], default="stop", - help="Action to perform: stop or start resources (default: stop)") - parser.add_argument("--dry-run", action="store_true", help="List resources without modifying them") - parser.add_argument("--regions", type=str, help="Comma-separated list of regions to scan") - parser.add_argument("--exclude-accounts", type=str, help="Comma-separated list of account IDs to exclude") - parser.add_argument("--exclude-resources", choices=["ec2", "rds", "eks", "emr"], nargs="+", - help="Resource types to exclude from management") - return parser.parse_args() - -def get_available_regions(): - """Get all available AWS regions""" - ec2_client = boto3.client('ec2') - regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']] - return regions - -def assume_role(account_id, role_name): - """Assume role in the specified account""" - try: - sts_client = boto3.client('sts') - assumed_role = sts_client.assume_role( - RoleArn=f"arn:aws:iam::{account_id}:role/{role_name}", - RoleSessionName="ResourceManagementSession" - ) - credentials = assumed_role['Credentials'] - return credentials - except Exception as e: - logger.error(f"Failed to assume role in account {account_id}: {e}") - return None - -def get_account_resources(credentials, regions, exclude_resources): - """Get resources from an account across specified regions""" - resources = { - "ec2_instances": [], - "rds_instances": [], - "eks_clusters": [], - "emr_clusters": [] - } - - for region in regions: - session_kwargs = { - "aws_access_key_id": credentials['AccessKeyId'], - "aws_secret_access_key": credentials['SecretAccessKey'], - "aws_session_token": credentials['SessionToken'], - "region_name": region - } - - # Get EC2 instances - if "ec2" not in exclude_resources: - try: - ec2_client = boto3.client('ec2', **session_kwargs) - response = ec2_client.describe_instances() - for reservation in response.get('Reservations', []): - for instance in reservation.get('Instances', []): - # Capture tags for exclusion check - tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])} - - resources["ec2_instances"].append({ - "id": instance['InstanceId'], - "state": instance['State']['Name'], - "region": region, - "tags": tags - }) - - # Handle pagination - while 'NextToken' in response: - response = ec2_client.describe_instances(NextToken=response['NextToken']) - for reservation in response.get('Reservations', []): - for instance in reservation.get('Instances', []): - # Capture tags for exclusion check - tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])} - - resources["ec2_instances"].append({ - "id": instance['InstanceId'], - "state": instance['State']['Name'], - "region": region, - "tags": tags - }) - except Exception as e: - logger.error(f"Error getting EC2 instances in region {region}: {e}") - - # Get RDS instances - if "rds" not in exclude_resources: - try: - rds_client = boto3.client('rds', **session_kwargs) - response = rds_client.describe_db_instances() - for instance in response.get('DBInstances', []): - # Capture tags for exclusion check - tags = {} - try: - tag_list = rds_client.list_tags_for_resource( - ResourceName=instance['DBInstanceArn'] - ).get('TagList', []) - tags = {tag['Key']: tag['Value'] for tag in tag_list} - except Exception as tag_e: - logger.warning(f"Error getting tags for RDS instance {instance['DBInstanceIdentifier']}: {tag_e}") - - resources["rds_instances"].append({ - "id": instance['DBInstanceIdentifier'], - "status": instance['DBInstanceStatus'], - "region": region, - "tags": tags - }) - - # Handle pagination - while 'Marker' in response: - response = rds_client.describe_db_instances(Marker=response['Marker']) - for instance in response.get('DBInstances', []): - # Capture tags for exclusion check - tags = {} - try: - tag_list = rds_client.list_tags_for_resource( - ResourceName=instance['DBInstanceArn'] - ).get('TagList', []) - tags = {tag['Key']: tag['Value'] for tag in tag_list} - except Exception as tag_e: - logger.warning(f"Error getting tags for RDS instance {instance['DBInstanceIdentifier']}: {tag_e}") - - resources["rds_instances"].append({ - "id": instance['DBInstanceIdentifier'], - "status": instance['DBInstanceStatus'], - "region": region, - "tags": tags - }) - except Exception as e: - logger.error(f"Error getting RDS instances in region {region}: {e}") - - # Get EKS clusters - if "eks" not in exclude_resources: - try: - eks_client = boto3.client('eks', **session_kwargs) - response = eks_client.list_clusters() - clusters = response.get('clusters', []) - - # Handle pagination - while 'nextToken' in response: - response = eks_client.list_clusters(nextToken=response['nextToken']) - clusters.extend(response.get('clusters', [])) - - for cluster_name in clusters: - try: - cluster_info = eks_client.describe_cluster(name=cluster_name) - - # Capture tags for exclusion check - cluster_tags = cluster_info['cluster'].get('tags', {}) - - resources["eks_clusters"].append({ - "name": cluster_name, - "status": cluster_info['cluster']['status'], - "region": region, - "tags": cluster_tags - }) - except Exception as e: - logger.error(f"Error getting details for EKS cluster {cluster_name} in region {region}: {e}") - except Exception as e: - logger.error(f"Error getting EKS clusters in region {region}: {e}") - - # Get EMR clusters - if "emr" not in exclude_resources: - try: - emr_client = boto3.client('emr', **session_kwargs) - # For stopping: get active clusters - active_states = ['STARTING', 'BOOTSTRAPPING', 'RUNNING', 'WAITING'] - response = emr_client.list_clusters(ClusterStates=active_states) - clusters = response.get('Clusters', []) - - # Handle pagination for active clusters - while 'Marker' in response: - response = emr_client.list_clusters( - ClusterStates=active_states, - Marker=response['Marker'] - ) - clusters.extend(response.get('Clusters', [])) - - # For starting: also get stopped clusters - response = emr_client.list_clusters(ClusterStates=['TERMINATED_WITH_ERRORS', 'STOPPED']) - stopped_clusters = response.get('Clusters', []) - - # Handle pagination for stopped clusters - while 'Marker' in response: - response = emr_client.list_clusters( - ClusterStates=['TERMINATED_WITH_ERRORS', 'STOPPED'], - Marker=response['Marker'] - ) - stopped_clusters.extend(response.get('Clusters', [])) - - # Add all clusters to the resources list - for cluster in clusters + stopped_clusters: - resources["emr_clusters"].append({ - "id": cluster['Id'], - "name": cluster['Name'], - "status": cluster['Status']['State'], - "region": region - }) - except Exception as e: - logger.error(f"Error getting EMR clusters in region {region}: {e}") - - return resources - -def shutdown_ec2_instances(credentials, instances, account_id): - """Gracefully stop EC2 instances""" - for instance in instances: - # Skip instances with the exclusion tag - if EXCLUSION_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with exclusion tag {EXCLUSION_TAG}") - continue - - # Skip instances that are part of EKS clusters - if EKS_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with tag {EKS_TAG}={instance['tags'][EKS_TAG]} (EKS managed node)") - continue - - # Skip instances that are part of EMR clusters - if EMR_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with tag {EMR_TAG}={instance['tags'][EMR_TAG]} (EMR managed node)") - continue - - if instance["state"] == "running": - try: - region = instance["region"] - ec2_client = boto3.client( - 'ec2', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - # Create ISO 8601 timestamp - timestamp = datetime.datetime.now().isoformat() - - # Tag the instance before stopping - logger.info(f"Tagging EC2 instance {instance['id']} with {STOP_TAG}={timestamp}") - ec2_client.create_tags( - Resources=[instance['id']], - Tags=[ - { - 'Key': STOP_TAG, - 'Value': timestamp - } - ] - ) - - # Stop the instance - logger.info(f"Stopping EC2 instance {instance['id']} in region {region}") - ec2_client.stop_instances(InstanceIds=[instance['id']]) - - # Log with detailed information - logger.info(f"ACTION: instance={instance['id']}, region={region}, account_id={account_id}, action=stop, timestamp={timestamp}") - - except Exception as e: - logger.error(f"Failed to stop EC2 instance {instance['id']}: {e}") - -def start_ec2_instances(credentials, instances, account_id): - """Start stopped EC2 instances""" - for instance in instances: - # Skip instances with the exclusion tag - if EXCLUSION_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with exclusion tag {EXCLUSION_TAG}") - continue - - # Skip instances that are part of EKS clusters - if EKS_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with tag {EKS_TAG}={instance['tags'][EKS_TAG]} (EKS managed node)") - continue - - # Skip instances that are part of EMR clusters - if EMR_TAG in instance.get("tags", {}): - logger.info(f"Skipping EC2 instance {instance['id']} with tag {EMR_TAG}={instance['tags'][EMR_TAG]} (EMR managed node)") - continue - - if instance["state"] == "stopped": - try: - region = instance["region"] - ec2_client = boto3.client( - 'ec2', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - timestamp = datetime.datetime.now().isoformat() - - logger.info(f"Starting EC2 instance {instance['id']} in region {region}") - ec2_client.start_instances(InstanceIds=[instance['id']]) - - # Log with detailed information - logger.info(f"ACTION: instance={instance['id']}, region={region}, account_id={account_id}, action=start, timestamp={timestamp}") - - except Exception as e: - logger.error(f"Failed to start EC2 instance {instance['id']}: {e}") - -def shutdown_rds_instances(credentials, instances): - """Stop RDS instances""" - for instance in instances: - # Skip instances with the exclusion tag - if EXCLUSION_TAG in instance.get("tags", {}): - logger.info(f"Skipping RDS instance {instance['id']} with exclusion tag {EXCLUSION_TAG}") - continue - - if instance["status"] == "available": - try: - region = instance["region"] - rds_client = boto3.client( - 'rds', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - logger.info(f"Stopping RDS instance {instance['id']} in region {region}") - rds_client.stop_db_instance(DBInstanceIdentifier=instance['id']) - except Exception as e: - logger.error(f"Failed to stop RDS instance {instance['id']}: {e}") - -def start_rds_instances(credentials, instances): - """Start stopped RDS instances""" - for instance in instances: - # Skip instances with the exclusion tag - if EXCLUSION_TAG in instance.get("tags", {}): - logger.info(f"Skipping RDS instance {instance['id']} with exclusion tag {EXCLUSION_TAG}") - continue - - if instance["status"] == "stopped": - try: - region = instance["region"] - rds_client = boto3.client( - 'rds', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - logger.info(f"Starting RDS instance {instance['id']} in region {region}") - rds_client.start_db_instance(DBInstanceIdentifier=instance['id']) - except Exception as e: - logger.error(f"Failed to start RDS instance {instance['id']}: {e}") - -def shutdown_eks_clusters(credentials, clusters): - """ - Shutdown EKS clusters gracefully - Note: This doesn't actually delete clusters, just scales down node groups - """ - for cluster in clusters: - # Skip clusters with the exclusion tag - if EXCLUSION_TAG in cluster.get("tags", {}): - logger.info(f"Skipping EKS cluster {cluster['name']} with exclusion tag {EXCLUSION_TAG}") - continue - - if cluster["status"] == "ACTIVE": - try: - region = cluster["region"] - eks_client = boto3.client( - 'eks', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - autoscaling_client = boto3.client( - 'autoscaling', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - # Get nodegroups - nodegroups = eks_client.list_nodegroups(clusterName=cluster['name']).get('nodegroups', []) - - logger.info(f"Scaling down EKS cluster {cluster['name']} in region {region}") - - # Scale down each nodegroup - for nodegroup in nodegroups: - ng_info = eks_client.describe_nodegroup( - clusterName=cluster['name'], - nodegroupName=nodegroup - ) - - if 'autoScalingGroups' in ng_info['nodegroup']: - for asg in ng_info['nodegroup']['autoScalingGroups']: - asg_name = asg['name'] - - # Get current ASG configuration - asg_info = autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[asg_name] - ) - - if len(asg_info['AutoScalingGroups']) > 0: - current_asg = asg_info['AutoScalingGroups'][0] - - # Save the current min size and desired capacity as tags - current_min_size = current_asg['MinSize'] - current_desired_size = current_asg['DesiredCapacity'] - - # Only save if greater than 0 - if current_min_size > 0 or current_desired_size > 0: - logger.info(f"Saving original ASG {asg_name} sizes: min={current_min_size}, desired={current_desired_size}") - autoscaling_client.create_or_update_tags( - Tags=[ - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_MIN_SIZE, - 'Value': str(current_min_size), - 'PropagateAtLaunch': False - }, - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_DESIRED_SIZE, - 'Value': str(current_desired_size), - 'PropagateAtLaunch': False - } - ] - ) - - # Now scale down the ASG - logger.info(f"Scaling down ASG {asg_name} for nodegroup {nodegroup}") - autoscaling_client.update_auto_scaling_group( - AutoScalingGroupName=asg_name, - MinSize=0, - DesiredCapacity=0 - ) - except Exception as e: - logger.error(f"Failed to scale down EKS cluster {cluster['name']}: {e}") - -def shutdown_eks_clusters(credentials, clusters): - """ - Shutdown EKS clusters gracefully - Note: This doesn't actually delete clusters, just scales down node groups - """ - for cluster in clusters: - if cluster["status"] == "ACTIVE": - try: - region = cluster["region"] - eks_client = boto3.client( - 'eks', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - autoscaling_client = boto3.client( - 'autoscaling', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - # Get nodegroups - nodegroups = eks_client.list_nodegroups(clusterName=cluster['name']).get('nodegroups', []) - - logger.info(f"Scaling down EKS cluster {cluster['name']} in region {region}") - - # Scale down each nodegroup - for nodegroup in nodegroups: - ng_info = eks_client.describe_nodegroup( - clusterName=cluster['name'], - nodegroupName=nodegroup - ) - - if 'autoScalingGroups' in ng_info['nodegroup']: - for asg in ng_info['nodegroup']['autoScalingGroups']: - asg_name = asg['name'] - - # Get current ASG configuration - asg_info = autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[asg_name] - ) - - if len(asg_info['AutoScalingGroups']) > 0: - current_asg = asg_info['AutoScalingGroups'][0] - - # Save the current min size and desired capacity as tags - current_min_size = current_asg['MinSize'] - current_desired_size = current_asg['DesiredCapacity'] - - # Only save if greater than 0 - if current_min_size > 0 or current_desired_size > 0: - logger.info(f"Saving original ASG {asg_name} sizes: min={current_min_size}, desired={current_desired_size}") - autoscaling_client.create_or_update_tags( - Tags=[ - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_MIN_SIZE, - 'Value': str(current_min_size), - 'PropagateAtLaunch': False - }, - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_DESIRED_SIZE, - 'Value': str(current_desired_size), - 'PropagateAtLaunch': False - } - ] - ) - - # Now scale down the ASG - logger.info(f"Scaling down ASG {asg_name} for nodegroup {nodegroup}") - autoscaling_client.update_auto_scaling_group( - AutoScalingGroupName=asg_name, - MinSize=0, - DesiredCapacity=0 - ) - except Exception as e: - logger.error(f"Failed to scale down EKS cluster {cluster['name']}: {e}") - -def start_eks_clusters(credentials, clusters): - """ - Start EKS clusters by scaling up node groups - Uses saved tags to restore original capacity if available - """ - for cluster in clusters: - if EXCLUSION_TAG in cluster.get("tags", {}): - logger.info(f"Skipping EKS cluster {cluster['name']} with exclusion tag {EXCLUSION_TAG}") - continue - - if cluster["status"] == "ACTIVE": - try: - region = cluster["region"] - eks_client = boto3.client( - 'eks', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - autoscaling_client = boto3.client( - 'autoscaling', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - # Get nodegroups - nodegroups = eks_client.list_nodegroups(clusterName=cluster['name']).get('nodegroups', []) - - logger.info(f"Scaling up EKS cluster {cluster['name']} in region {region}") - - # Scale up each nodegroup - for nodegroup in nodegroups: - ng_info = eks_client.describe_nodegroup( - clusterName=cluster['name'], - nodegroupName=nodegroup - ) - - if 'autoScalingGroups' in ng_info['nodegroup']: - for asg in ng_info['nodegroup']['autoScalingGroups']: - asg_name = asg['name'] - - # Check current capacity - asg_info = autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[asg_name] - ) - - if len(asg_info['AutoScalingGroups']) > 0: - current_asg = asg_info['AutoScalingGroups'][0] - - # Only scale up if currently at zero - if current_asg['MinSize'] == 0 and current_asg['DesiredCapacity'] == 0: - # Check if we have saved sizes in tags - min_size = DEFAULT_NG_MIN_SIZE - desired_size = DEFAULT_NG_DESIRED_SIZE - - # Look for original size tags - for tag in current_asg.get('Tags', []): - if tag['Key'] == TAG_KEY_ORIGINAL_MIN_SIZE: - try: - min_size = int(tag['Value']) - except (ValueError, TypeError): - logger.warning(f"Invalid tag value for {TAG_KEY_ORIGINAL_MIN_SIZE} on ASG {asg_name}: {tag['Value']}") - - if tag['Key'] == TAG_KEY_ORIGINAL_DESIRED_SIZE: - try: - desired_size = int(tag['Value']) - except (ValueError, TypeError): - logger.warning(f"Invalid tag value for {TAG_KEY_ORIGINAL_DESIRED_SIZE} on ASG {asg_name}: {tag['Value']}") - - logger.info(f"Scaling up ASG {asg_name} for nodegroup {nodegroup} to minimum={min_size}, desired={desired_size}") - autoscaling_client.update_auto_scaling_group( - AutoScalingGroupName=asg_name, - MinSize=min_size, - DesiredCapacity=desired_size - ) - - # Clean up the tags after restoring - try: - autoscaling_client.delete_tags( - Tags=[ - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_MIN_SIZE - }, - { - 'ResourceId': asg_name, - 'ResourceType': 'auto-scaling-group', - 'Key': TAG_KEY_ORIGINAL_DESIRED_SIZE - } - ] - ) - except Exception as tag_e: - logger.warning(f"Failed to clean up size tags on ASG {asg_name}: {tag_e}") - else: - logger.info(f"ASG {asg_name} already has capacity. Skipping scale up.") - except Exception as e: - logger.error(f"Failed to scale up EKS cluster {cluster['name']}: {e}") - -def shutdown_emr_clusters(credentials, clusters): - """ - Stop EMR clusters gracefully - Note: This preserves the cluster configuration and data - """ - for cluster in clusters: - # Only RUNNING and WAITING clusters can be stopped - if cluster["status"] in ["RUNNING", "WAITING"]: - try: - region = cluster["region"] - emr_client = boto3.client( - 'emr', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - logger.info(f"Stopping EMR cluster {cluster['name']} ({cluster['id']}) in region {region}") - emr_client.stop_cluster(ClusterId=cluster['id']) - except Exception as e: - logger.error(f"Failed to stop EMR cluster {cluster['id']}: {e}") - # For STARTING and BOOTSTRAPPING clusters, we need to terminate them as they can't be stopped - elif cluster["status"] in ["STARTING", "BOOTSTRAPPING"]: - try: - region = cluster["region"] - emr_client = boto3.client( - 'emr', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - logger.info(f"Terminating EMR cluster {cluster['name']} ({cluster['id']}) in region {region} (cannot stop a cluster in {cluster['status']} state)") - emr_client.terminate_job_flows(JobFlowIds=[cluster['id']]) - except Exception as e: - logger.error(f"Failed to terminate EMR cluster {cluster['id']}: {e}") - -def start_emr_clusters(credentials, clusters): - """ - Start stopped EMR clusters - """ - for cluster in clusters: - if cluster["status"] == "STOPPED": - try: - region = cluster["region"] - emr_client = boto3.client( - 'emr', - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) - - logger.info(f"Starting EMR cluster {cluster['name']} ({cluster['id']}) in region {region}") - emr_client.start_cluster(ClusterId=cluster['id']) - except Exception as e: - logger.error(f"Failed to start EMR cluster {cluster['id']}: {e}") - -def main(): - args = parse_arguments() - - # Process arguments - action = args.action - dry_run = args.dry_run - regions = args.regions.split(',') if args.regions else get_available_regions() - exclude_accounts = args.exclude_accounts.split(',') if args.exclude_accounts else [] - exclude_resources = args.exclude_resources or [] - - logger.info(f"Starting AWS organization resource {'discovery' if dry_run else action} {'(DRY RUN)' if dry_run else ''}") - - # Get organization accounts - try: - org_client = boto3.client('organizations') - accounts = [] - - paginator = org_client.get_paginator('list_accounts') - for page in paginator.paginate(): - for account in page['Accounts']: - if account['Status'] == 'ACTIVE' and account['Id'] not in exclude_accounts: - accounts.append({ - 'id': account['Id'], - 'name': account['Name'] - }) - except Exception as e: - logger.error(f"Failed to list organization accounts: {e}") - logger.info("Falling back to using current account only") - - # Fallback to current account if can't access organization - sts_client = boto3.client('sts') - identity = sts_client.get_caller_identity() - accounts = [{ - 'id': identity['Account'], - 'name': 'Current Account' - }] - - # Process each account -for account in accounts: - logger.info(f"Processing account: {account['name']} ({account['id']})") - - # Assume role in the account - credentials = assume_role(account['id'], ASSUME_ROLE_NAME) - if not credentials: - logger.warning(f"Skipping account {account['id']} due to role assumption failure") - continue - - # Get resources - resources = get_account_resources(credentials, regions, exclude_resources) - - # Count service-managed EC2 instances - eks_managed = sum(1 for i in resources['ec2_instances'] if EKS_TAG in i.get('tags', {})) - emr_managed = sum(1 for i in resources['ec2_instances'] if EMR_TAG in i.get('tags', {})) - - # Log discovered resources with service-managed counts - - - # Log discovered resources - total_ec2 = len(resources['ec2_instances']) - total_rds = len(resources['rds_instances']) - total_eks = len(resources['eks_clusters']) - total_emr = len(resources['emr_instances']) - - # Count excluded resources - excluded_ec2 = sum(1 for i in resources['ec2_instances'] if EXCLUSION_TAG in i.get('tags', {})) - excluded_rds = sum(1 for i in resources['rds_instances'] if EXCLUSION_TAG in i.get('tags', {})) - excluded_eks = sum(1 for i in resources['eks_clusters'] if EXCLUSION_TAG in i.get('tags', {})) - - logger.info(f"Account {account['id']} - Found {total_ec2} EC2 instances ({excluded_ec2} explicitly excluded, {eks_managed} EKS-managed, {emr_managed} EMR-managed)") - logger.info(f"Account {account['id']} - Found {total_rds} RDS instances ({excluded_rds} excluded)") - logger.info(f"Account {account['id']} - Found {total_eks} EKS clusters ({excluded_eks} excluded)") - logger.info(f"Account {account['id']} - Found {total_emr} EMR clusters") - - if not dry_run: - # Perform actions based on selected mode - if action == "stop": - # Shutdown resources - if "ec2" not in exclude_resources: - shutdown_ec2_instances(credentials, resources['ec2_instances'], account['id']) - - if "rds" not in exclude_resources: - shutdown_rds_instances(credentials, resources['rds_instances']) - - if "eks" not in exclude_resources: - shutdown_eks_clusters(credentials, resources['eks_clusters']) - - if "emr" not in exclude_resources: - shutdown_emr_clusters(credentials, resources['emr_clusters']) - else: # action == "start" - # Start resources - if "ec2" not in exclude_resources: - start_ec2_instances(credentials, resources['ec2_instances'], account['id']) - - if "rds" not in exclude_resources: - start_rds_instances(credentials, resources['rds_instances']) - - if "eks" not in exclude_resources: - start_eks_clusters(credentials, resources['eks_clusters']) - - if "emr" not in exclude_resources: - start_emr_clusters(credentials, resources['emr_clusters']) - - logger.info(f"Resource {'discovery' if dry_run else action} completed") - -if __name__ == "__main__": - main() diff --git a/local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py b/local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py new file mode 100644 index 00000000..07e0c927 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py @@ -0,0 +1,18 @@ +""" +Logging utilities for AWS Resource Management. +""" +import logging +import sys +from . import config + +def setup_logging(): + """Configure logging for the application.""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(config.LOG_FILE), + logging.StreamHandler(sys.stdout) + ] + ) + return logging.getLogger("AWS-Resource-Management") diff --git a/local-app/python-tools/gfl-resource-actions/main.py b/local-app/python-tools/gfl-resource-actions/main.py new file mode 100644 index 00000000..c2604ba4 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/main.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Main entry point for AWS Resource Management tool. +""" + +import argparse +from . import config +from .logging_utils import setup_logging +from .aws_utils import get_available_regions, assume_role, get_organization_accounts +from .discovery import get_account_resources +from .managers import EC2Manager, RDSManager, EKSManager, EMRManager + +logger = setup_logging() + +def parse_arguments(): + """Parse command-line arguments""" + parser = argparse.ArgumentParser(description="AWS Organization Resource Management Tool") + parser.add_argument("--action", choices=["stop", "start"], default="stop", + help="Action to perform: stop or start resources (default: stop)") + parser.add_argument("--dry-run", action="store_true", help="List resources without modifying them") + parser.add_argument("--regions", type=str, help="Comma-separated list of regions to scan") + parser.add_argument("--exclude-accounts", type=str, help="Comma-separated list of account IDs to exclude") + parser.add_argument("--exclude-resources", choices=["ec2", "rds", "eks", "emr"], nargs="+", + help="Resource types to exclude from management") + return parser.parse_args() + +def main(): + """Main execution function.""" + args = parse_arguments() + + # Process arguments + action = args.action + dry_run = args.dry_run + regions = args.regions.split(',') if args.regions else get_available_regions() + exclude_accounts = args.exclude_accounts.split(',') if args.exclude_accounts else [] + exclude_resources = args.exclude_resources or [] + + logger.info(f"Starting AWS organization resource {action} {'(DRY RUN)' if dry_run else ''}") + + # Get organization accounts + accounts = get_organization_accounts(exclude_accounts) + + # Process each account + for account in accounts: + logger.info(f"Processing account: {account['name']} ({account['id']})") + + # Assume role in the account + credentials = assume_role(account['id']) + if not credentials: + logger.warning(f"Skipping account {account['id']} due to role assumption failure") + continue + + # Get resources + resources = get_account_resources(credentials, regions, exclude_resources) + + # Count service-managed EC2 instances + eks_managed = sum(1 for i in resources['ec2_instances'] if config.EKS_TAG in i.get('tags', {})) + emr_managed = sum(1 for i in resources['ec2_instances'] if config.EMR_TAG in i.get('tags', {})) + + # Log discovered resources with service-managed counts + total_ec2 = len(resources['ec2_instances']) + excluded_ec2 = sum(1 for i in resources['ec2_instances'] if config.EXCLUSION_TAG in i.get('tags', {})) + + logger.info(f"Account {account['id']} - Found {total_ec2} EC2 instances ({excluded_ec2} explicitly excluded, {eks_managed} EKS-managed, {emr_managed} EMR-managed)") + logger.info(f"Account {account['id']} - Found {len(resources['rds_instances'])} RDS instances") + logger.info(f"Account {account['id']} - Found {len(resources['eks_clusters'])} EKS clusters") + logger.info(f"Account {account['id']} - Found {len(resources['emr_clusters'])} EMR clusters") + + # Initialize resource managers + ec2_manager = EC2Manager(credentials, account['id']) + rds_manager = RDSManager(credentials, account['id']) + eks_manager = EKSManager(credentials, account['id']) + emr_manager = EMRManager(credentials, account['id']) + + # Perform actions based on selected mode + if action == "stop": + if "ec2" not in exclude_resources: + ec2_manager.stop(resources['ec2_instances'], dry_run) + + if "rds" not in exclude_resources: + rds_manager.stop(resources['rds_instances'], dry_run) + + if "eks" not in exclude_resources: + eks_manager.stop(resources['eks_clusters'], dry_run) + + if "emr" not in exclude_resources: + emr_manager.stop(resources['emr_clusters'], dry_run) + else: # action == "start" + if "ec2" not in exclude_resources: + ec2_manager.start(resources['ec2_instances'], dry_run) + + if "rds" not in exclude_resources: + rds_manager.start(resources['rds_instances'], dry_run) + + if "eks" not in exclude_resources: + eks_manager.start(resources['eks_clusters'], dry_run) + + if "emr" not in exclude_resources: + emr_manager.start(resources['emr_clusters'], dry_run) + + logger.info(f"Resource {action} completed {'(DRY RUN)' if dry_run else ''}") + +if __name__ == "__main__": + main() diff --git a/local-app/python-tools/gfl-resource-actions/managers/__init__.py b/local-app/python-tools/gfl-resource-actions/managers/__init__.py new file mode 100644 index 00000000..f34abf8a --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/__init__.py @@ -0,0 +1,8 @@ +""" +Resource managers for different AWS resource types. +""" + +from .ec2 import EC2Manager +from .rds import RDSManager +from .eks import EKSManager +from .emr import EMRManager diff --git a/local-app/python-tools/gfl-resource-actions/managers/base.py b/local-app/python-tools/gfl-resource-actions/managers/base.py new file mode 100644 index 00000000..67453fa2 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/base.py @@ -0,0 +1,46 @@ +""" +Base resource manager class with common functionality for AWS resources. +""" +import datetime +from .. import config +from ..aws_utils import create_boto3_client +from ..logging_utils import setup_logging + +logger = setup_logging() + +class ResourceManager: + """Base class for AWS resource managers.""" + + def __init__(self, credentials, account_id): + """ + Initialize resource manager. + + Args: + credentials: AWS credentials dictionary + account_id: AWS account ID + """ + self.credentials = credentials + self.account_id = account_id + + def create_client(self, service, region): + """Create a boto3 client for the resource service.""" + return create_boto3_client(service, self.credentials, region) + + def get_timestamp(self): + """Get ISO 8601 timestamp.""" + return datetime.datetime.now().isoformat() + + def log_action(self, resource_id, region, action, resource_name=None, dry_run=False): + """Log resource action.""" + action_prefix = "[DRY RUN] " if dry_run else "" + resource_info = f"name={resource_name}, " if resource_name else "" + timestamp = self.get_timestamp() + + logger.info(f"{action_prefix}ACTION: resource={resource_id}, {resource_info}region={region}, " + f"account_id={self.account_id}, action={action}, timestamp={timestamp}") + + return timestamp + + def should_exclude(self, resource): + """Check if resource should be excluded based on tags.""" + return config.EXCLUSION_TAG in resource.get("tags", {}) diff --git a/local-app/python-tools/gfl-resource-actions/managers/ec2.py b/local-app/python-tools/gfl-resource-actions/managers/ec2.py new file mode 100644 index 00000000..6eead1a1 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/ec2.py @@ -0,0 +1,91 @@ +""" +EC2 resource manager class. +""" +from . import base +from .. import config +from ..logging_utils import setup_logging + +logger = setup_logging() + +class EC2Manager(base.ResourceManager): + """Manager for EC2 instances.""" + + def should_exclude(self, instance): + """Check if EC2 instance should be excluded based on tags.""" + tags = instance.get("tags", {}) + + # Check for explicit exclusion tag + if config.EXCLUSION_TAG in tags: + logger.info(f"Skipping EC2 instance {instance['id']} with exclusion tag {config.EXCLUSION_TAG}") + return True + + # Skip instances that are part of EKS clusters + if config.EKS_TAG in tags: + logger.info(f"Skipping EC2 instance {instance['id']} with tag {config.EKS_TAG}={tags[config.EKS_TAG]} (EKS managed node)") + return True + + # Skip instances that are part of EMR clusters + if config.EMR_TAG in tags: + logger.info(f"Skipping EC2 instance {instance['id']} with tag {config.EMR_TAG}={tags[config.EMR_TAG]} (EMR managed node)") + return True + + return False + + def stop(self, instances, dry_run=False): + """Stop running EC2 instances.""" + for instance in instances: + if self.should_exclude(instance): + continue + + if instance["state"] == "running": + try: + region = instance["region"] + ec2_client = self.create_client('ec2', region) + + # Create ISO 8601 timestamp + timestamp = self.get_timestamp() + + # Tag the instance before stopping + logger.info(f"{'[DRY RUN] Would tag' if dry_run else 'Tagging'} EC2 instance {instance['id']} with {config.STOP_TAG}={timestamp}") + if not dry_run: + ec2_client.create_tags( + Resources=[instance['id']], + Tags=[ + { + 'Key': config.STOP_TAG, + 'Value': timestamp + } + ] + ) + + # Stop the instance + logger.info(f"{'[DRY RUN] Would stop' if dry_run else 'Stopping'} EC2 instance {instance['id']} in region {region}") + if not dry_run: + ec2_client.stop_instances(InstanceIds=[instance['id']]) + + # Log with detailed information + self.log_action(instance['id'], region, "stop", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate stopping' if dry_run else 'stop'} EC2 instance {instance['id']}: {e}") + + def start(self, instances, dry_run=False): + """Start stopped EC2 instances.""" + for instance in instances: + if self.should_exclude(instance): + continue + + if instance["state"] == "stopped": + try: + region = instance["region"] + ec2_client = self.create_client('ec2', region) + + logger.info(f"{'[DRY RUN] Would start' if dry_run else 'Starting'} EC2 instance {instance['id']} in region {region}") + if not dry_run: + ec2_client.start_instances(InstanceIds=[instance['id']]) + + # Log with detailed information + self.log_action(instance['id'], region, "start", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate starting' if dry_run else 'start'} EC2 instance {instance['id']}: {e}") diff --git a/local-app/python-tools/gfl-resource-actions/managers/eks.py b/local-app/python-tools/gfl-resource-actions/managers/eks.py new file mode 100644 index 00000000..b48fae57 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/eks.py @@ -0,0 +1,195 @@ +""" +EKS resource manager class. +""" +from . import base +from .. import config +from ..logging_utils import setup_logging + +logger = setup_logging() + +class EKSManager(base.ResourceManager): + """Manager for EKS clusters.""" + + def stop(self, clusters, dry_run=False): + """ + Stop EKS clusters by scaling down node groups to zero. + This doesn't actually delete clusters, just scales down node groups. + """ + for cluster in clusters: + # Skip clusters with the exclusion tag + if self.should_exclude(cluster): + logger.info(f"Skipping EKS cluster {cluster['name']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + if cluster["status"] == "ACTIVE": + try: + region = cluster["region"] + eks_client = self.create_client('eks', region) + autoscaling_client = self.create_client('autoscaling', region) + + # Get nodegroups + nodegroups = eks_client.list_nodegroups(clusterName=cluster['name']).get('nodegroups', []) + + logger.info(f"{'[DRY RUN] Would scale down' if dry_run else 'Scaling down'} EKS cluster {cluster['name']} in region {region}") + + timestamp = self.get_timestamp() + + # Scale down each nodegroup + for nodegroup in nodegroups: + ng_info = eks_client.describe_nodegroup( + clusterName=cluster['name'], + nodegroupName=nodegroup + ) + + if 'autoScalingGroups' in ng_info['nodegroup']: + for asg in ng_info['nodegroup']['autoScalingGroups']: + asg_name = asg['name'] + + # Get current ASG configuration + asg_info = autoscaling_client.describe_auto_scaling_groups( + AutoScalingGroupNames=[asg_name] + ) + + if len(asg_info['AutoScalingGroups']) > 0: + current_asg = asg_info['AutoScalingGroups'][0] + + # Save the current min size and desired capacity as tags + current_min_size = current_asg['MinSize'] + current_desired_size = current_asg['DesiredCapacity'] + + # Only save if greater than 0 + if current_min_size > 0 or current_desired_size > 0: + logger.info(f"{'[DRY RUN] Would save' if dry_run else 'Saving'} original ASG {asg_name} sizes: min={current_min_size}, desired={current_desired_size}") + if not dry_run: + autoscaling_client.create_or_update_tags( + Tags=[ + { + 'ResourceId': asg_name, + 'ResourceType': 'auto-scaling-group', + 'Key': config.TAG_KEY_ORIGINAL_MIN_SIZE, + 'Value': str(current_min_size), + 'PropagateAtLaunch': False + }, + { + 'ResourceId': asg_name, + 'ResourceType': 'auto-scaling-group', + 'Key': config.TAG_KEY_ORIGINAL_DESIRED_SIZE, + 'Value': str(current_desired_size), + 'PropagateAtLaunch': False + } + ] + ) + + # Now scale down the ASG + logger.info(f"{'[DRY RUN] Would scale down' if dry_run else 'Scaling down'} ASG {asg_name} for nodegroup {nodegroup}") + if not dry_run: + autoscaling_client.update_auto_scaling_group( + AutoScalingGroupName=asg_name, + MinSize=0, + DesiredCapacity=0 + ) + + # Log with detailed information + self.log_action(cluster['name'], region, "scale_down", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate scaling down' if dry_run else 'scale down'} EKS cluster {cluster['name']}: {e}") + + def start(self, clusters, dry_run=False): + """ + Start EKS clusters by scaling up node groups. + Uses saved tags to restore original capacity if available. + """ + for cluster in clusters: + # Skip clusters with the exclusion tag + if self.should_exclude(cluster): + logger.info(f"Skipping EKS cluster {cluster['name']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + if cluster["status"] == "ACTIVE": + try: + region = cluster["region"] + eks_client = self.create_client('eks', region) + autoscaling_client = self.create_client('autoscaling', region) + + # Get nodegroups + nodegroups = eks_client.list_nodegroups(clusterName=cluster['name']).get('nodegroups', []) + + logger.info(f"{'[DRY RUN] Would scale up' if dry_run else 'Scaling up'} EKS cluster {cluster['name']} in region {region}") + + timestamp = self.get_timestamp() + + # Scale up each nodegroup + for nodegroup in nodegroups: + ng_info = eks_client.describe_nodegroup( + clusterName=cluster['name'], + nodegroupName=nodegroup + ) + + if 'autoScalingGroups' in ng_info['nodegroup']: + for asg in ng_info['nodegroup']['autoScalingGroups']: + asg_name = asg['name'] + + # Check current capacity + asg_info = autoscaling_client.describe_auto_scaling_groups( + AutoScalingGroupNames=[asg_name] + ) + + if len(asg_info['AutoScalingGroups']) > 0: + current_asg = asg_info['AutoScalingGroups'][0] + + # Only scale up if currently at zero + if current_asg['MinSize'] == 0 and current_asg['DesiredCapacity'] == 0: + # Check if we have saved sizes in tags + min_size = config.DEFAULT_NG_MIN_SIZE + desired_size = config.DEFAULT_NG_DESIRED_SIZE + + # Look for original size tags + for tag in current_asg.get('Tags', []): + if tag['Key'] == config.TAG_KEY_ORIGINAL_MIN_SIZE: + try: + min_size = int(tag['Value']) + except (ValueError, TypeError): + logger.warning(f"Invalid tag value for {config.TAG_KEY_ORIGINAL_MIN_SIZE} on ASG {asg_name}: {tag['Value']}") + + if tag['Key'] == config.TAG_KEY_ORIGINAL_DESIRED_SIZE: + try: + desired_size = int(tag['Value']) + except (ValueError, TypeError): + logger.warning(f"Invalid tag value for {config.TAG_KEY_ORIGINAL_DESIRED_SIZE} on ASG {asg_name}: {tag['Value']}") + + logger.info(f"{'[DRY RUN] Would scale up' if dry_run else 'Scaling up'} ASG {asg_name} for nodegroup {nodegroup} to minimum={min_size}, desired={desired_size}") + if not dry_run: + autoscaling_client.update_auto_scaling_group( + AutoScalingGroupName=asg_name, + MinSize=min_size, + DesiredCapacity=desired_size + ) + + # Clean up the tags after restoring + if not dry_run: + try: + autoscaling_client.delete_tags( + Tags=[ + { + 'ResourceId': asg_name, + 'ResourceType': 'auto-scaling-group', + 'Key': config.TAG_KEY_ORIGINAL_MIN_SIZE + }, + { + 'ResourceId': asg_name, + 'ResourceType': 'auto-scaling-group', + 'Key': config.TAG_KEY_ORIGINAL_DESIRED_SIZE + } + ] + ) + except Exception as tag_e: + logger.warning(f"Failed to clean up size tags on ASG {asg_name}: {tag_e}") + else: + logger.info(f"ASG {asg_name} already has capacity. Skipping scale up.") + + # Log with detailed information + self.log_action(cluster['name'], region, "scale_up", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate scaling up' if dry_run else 'scale up'} EKS cluster {cluster['name']}: {e}") diff --git a/local-app/python-tools/gfl-resource-actions/managers/emr.py b/local-app/python-tools/gfl-resource-actions/managers/emr.py new file mode 100644 index 00000000..dd3596c7 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/emr.py @@ -0,0 +1,82 @@ +""" +EMR resource manager class. +""" +from . import base +from .. import config +from ..logging_utils import setup_logging + +logger = setup_logging() + +class EMRManager(base.ResourceManager): + """Manager for EMR clusters.""" + + def stop(self, clusters, dry_run=False): + """ + Stop EMR clusters gracefully. + This preserves the cluster configuration and data. + """ + for cluster in clusters: + # Skip clusters with the exclusion tag + if self.should_exclude(cluster): + logger.info(f"Skipping EMR cluster {cluster['id']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + # Only RUNNING and WAITING clusters can be stopped + if cluster["status"] in ["RUNNING", "WAITING"]: + try: + region = cluster["region"] + emr_client = self.create_client('emr', region) + + timestamp = self.get_timestamp() + + logger.info(f"{'[DRY RUN] Would stop' if dry_run else 'Stopping'} EMR cluster {cluster['name']} ({cluster['id']}) in region {region}") + if not dry_run: + emr_client.stop_cluster(ClusterId=cluster['id']) + + # Log with detailed information + self.log_action(cluster['id'], region, "stop", resource_name=cluster['name'], dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate stopping' if dry_run else 'stop'} EMR cluster {cluster['id']}: {e}") + # For STARTING and BOOTSTRAPPING clusters, we need to terminate them as they can't be stopped + elif cluster["status"] in ["STARTING", "BOOTSTRAPPING"]: + try: + region = cluster["region"] + emr_client = self.create_client('emr', region) + + timestamp = self.get_timestamp() + + logger.info(f"{'[DRY RUN] Would terminate' if dry_run else 'Terminating'} EMR cluster {cluster['name']} ({cluster['id']}) in region {region} (cannot stop a cluster in {cluster['status']} state)") + if not dry_run: + emr_client.terminate_job_flows(JobFlowIds=[cluster['id']]) + + # Log with detailed information + self.log_action(cluster['id'], region, "terminate", resource_name=cluster['name'], dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate terminating' if dry_run else 'terminate'} EMR cluster {cluster['id']}: {e}") + + def start(self, clusters, dry_run=False): + """Start stopped EMR clusters.""" + for cluster in clusters: + # Skip clusters with the exclusion tag + if self.should_exclude(cluster): + logger.info(f"Skipping EMR cluster {cluster['id']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + if cluster["status"] == "STOPPED": + try: + region = cluster["region"] + emr_client = self.create_client('emr', region) + + timestamp = self.get_timestamp() + + logger.info(f"{'[DRY RUN] Would start' if dry_run else 'Starting'} EMR cluster {cluster['name']} ({cluster['id']}) in region {region}") + if not dry_run: + emr_client.start_cluster(ClusterId=cluster['id']) + + # Log with detailed information + self.log_action(cluster['id'], region, "start", resource_name=cluster['name'], dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate starting' if dry_run else 'start'} EMR cluster {cluster['id']}: {e}") diff --git a/local-app/python-tools/gfl-resource-actions/managers/rds.py b/local-app/python-tools/gfl-resource-actions/managers/rds.py new file mode 100644 index 00000000..8f63d772 --- /dev/null +++ b/local-app/python-tools/gfl-resource-actions/managers/rds.py @@ -0,0 +1,60 @@ +""" +RDS resource manager class. +""" +from . import base +from .. import config +from ..logging_utils import setup_logging + +logger = setup_logging() + +class RDSManager(base.ResourceManager): + """Manager for RDS instances.""" + + def stop(self, instances, dry_run=False): + """Stop available RDS instances.""" + for instance in instances: + # Skip instances with the exclusion tag + if self.should_exclude(instance): + logger.info(f"Skipping RDS instance {instance['id']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + if instance["status"] == "available": + try: + region = instance["region"] + rds_client = self.create_client('rds', region) + + # Create ISO 8601 timestamp + timestamp = self.get_timestamp() + + logger.info(f"{'[DRY RUN] Would stop' if dry_run else 'Stopping'} RDS instance {instance['id']} in region {region}") + if not dry_run: + rds_client.stop_db_instance(DBInstanceIdentifier=instance['id']) + + # Log with detailed information + self.log_action(instance['id'], region, "stop", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate stopping' if dry_run else 'stop'} RDS instance {instance['id']}: {e}") + + def start(self, instances, dry_run=False): + """Start stopped RDS instances.""" + for instance in instances: + # Skip instances with the exclusion tag + if self.should_exclude(instance): + logger.info(f"Skipping RDS instance {instance['id']} with exclusion tag {config.EXCLUSION_TAG}") + continue + + if instance["status"] == "stopped": + try: + region = instance["region"] + rds_client = self.create_client('rds', region) + + logger.info(f"{'[DRY RUN] Would start' if dry_run else 'Starting'} RDS instance {instance['id']} in region {region}") + if not dry_run: + rds_client.start_db_instance(DBInstanceIdentifier=instance['id']) + + # Log with detailed information + self.log_action(instance['id'], region, "start", dry_run=dry_run) + + except Exception as e: + logger.error(f"Failed to {'simulate starting' if dry_run else 'start'} RDS instance {instance['id']}: {e}")