From c0d13c4389649f83c569346ea0c3311297f4a803 Mon Sep 17 00:00:00 2001 From: "Matthew C. Morgan" Date: Fri, 14 Mar 2025 16:17:19 -0400 Subject: [PATCH] wip --- .../gfl-resource-actions/Makefile | 2 +- .../gfl-resource-actions/aws_utils.py | 101 ++++++++++++++++++ .../{discovery => }/discovery.py | 6 +- .../{logging_utils => }/logging_utils.py | 2 +- .../python-tools/gfl-resource-actions/main.py | 10 +- .../gfl-resource-actions/managers/__init__.py | 8 +- .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 557 bytes .../managers/__pycache__/base.cpython-311.pyc | Bin 0 -> 2917 bytes .../managers/__pycache__/ec2.cpython-311.pyc | Bin 0 -> 5216 bytes .../managers/__pycache__/eks.cpython-311.pyc | Bin 0 -> 9565 bytes .../managers/__pycache__/emr.cpython-311.pyc | Bin 0 -> 5382 bytes .../managers/__pycache__/rds.cpython-311.pyc | Bin 0 -> 3893 bytes .../gfl-resource-actions/managers/base.py | 6 +- .../gfl-resource-actions/managers/ec2.py | 6 +- .../gfl-resource-actions/managers/eks.py | 6 +- .../gfl-resource-actions/managers/emr.py | 6 +- .../gfl-resource-actions/managers/rds.py | 6 +- 17 files changed, 130 insertions(+), 29 deletions(-) rename local-app/python-tools/gfl-resource-actions/{discovery => }/discovery.py (97%) rename local-app/python-tools/gfl-resource-actions/{logging_utils => }/logging_utils.py (95%) create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/__init__.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/base.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/ec2.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/eks.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/emr.cpython-311.pyc create mode 100644 local-app/python-tools/gfl-resource-actions/managers/__pycache__/rds.cpython-311.pyc diff --git a/local-app/python-tools/gfl-resource-actions/Makefile b/local-app/python-tools/gfl-resource-actions/Makefile index 05025e45..9eb927df 100644 --- a/local-app/python-tools/gfl-resource-actions/Makefile +++ b/local-app/python-tools/gfl-resource-actions/Makefile @@ -31,7 +31,7 @@ setup: install-dev # Install development dependencies install-dev: - pip install boto3 + pip install boto3 pytest pytest-mock # Basic code check - no external tools needed code-check: diff --git a/local-app/python-tools/gfl-resource-actions/aws_utils.py b/local-app/python-tools/gfl-resource-actions/aws_utils.py index e69de29b..f1b6d899 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_utils.py +++ b/local-app/python-tools/gfl-resource-actions/aws_utils.py @@ -0,0 +1,101 @@ +""" +AWS utility functions for resource management. +""" +import boto3 +import config +from logging_utils import setup_logging + +logger = setup_logging() + +def create_boto3_client(service, credentials, region): + """ + Create a boto3 client for a specific service using the provided credentials. + + Args: + service (str): AWS service name (e.g., 'ec2', 'rds') + credentials (dict): AWS credentials dict with access key, secret key, and token + region (str): AWS region name + + Returns: + boto3.client: Configured boto3 client + """ + return boto3.client( + service, + aws_access_key_id=credentials['AccessKeyId'], + aws_secret_access_key=credentials['SecretAccessKey'], + aws_session_token=credentials['SessionToken'], + region_name=region + ) + +def get_available_regions(): + """ + Get a list of all available AWS regions. + + Returns: + list: List of region names + """ + try: + ec2_client = boto3.client('ec2') + response = ec2_client.describe_regions() + return [region['RegionName'] for region in response['Regions']] + except Exception as e: + logger.error(f"Error getting available regions: {e}") + # Return a default list of common regions as fallback + return ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2'] + +def assume_role(account_id): + """ + Assume the OrganizationAccountAccessRole in the specified account. + + Args: + account_id (str): AWS account ID to assume role in + + Returns: + dict: Credentials dictionary or None if failed + """ + try: + role_arn = f"arn:aws:iam::{account_id}:role/{config.ASSUME_ROLE_NAME}" + sts_client = boto3.client('sts') + + response = sts_client.assume_role( + RoleArn=role_arn, + RoleSessionName="ResourceManagementSession" + ) + + return response['Credentials'] + except Exception as e: + logger.error(f"Error assuming role in account {account_id}: {e}") + return None + +def get_organization_accounts(exclude_accounts=None): + """ + Get a list of accounts in the AWS organization. + + Args: + exclude_accounts (list): List of account IDs to exclude + + Returns: + list: List of account dicts with id and name + """ + if exclude_accounts is None: + exclude_accounts = [] + + try: + org_client = boto3.client('organizations') + accounts = [] + + # Get all accounts in the organization + paginator = org_client.get_paginator('list_accounts') + for page in paginator.paginate(): + for account in page['Accounts']: + # Skip suspended accounts and accounts in exclude list + if account['Status'] == 'ACTIVE' and account['Id'] not in exclude_accounts: + accounts.append({ + 'id': account['Id'], + 'name': account['Name'] + }) + + return accounts + except Exception as e: + logger.error(f"Error getting organization accounts: {e}") + return [] diff --git a/local-app/python-tools/gfl-resource-actions/discovery/discovery.py b/local-app/python-tools/gfl-resource-actions/discovery.py similarity index 97% rename from local-app/python-tools/gfl-resource-actions/discovery/discovery.py rename to local-app/python-tools/gfl-resource-actions/discovery.py index d4966145..6925a2a4 100644 --- a/local-app/python-tools/gfl-resource-actions/discovery/discovery.py +++ b/local-app/python-tools/gfl-resource-actions/discovery.py @@ -1,9 +1,9 @@ """ Resource discovery functions for AWS resources. """ -from . import config -from .aws_utils import create_boto3_client -from .logging_utils import setup_logging +import config +from aws_utils import create_boto3_client +from logging_utils import setup_logging logger = setup_logging() 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.py similarity index 95% rename from local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py rename to local-app/python-tools/gfl-resource-actions/logging_utils.py index 07e0c927..180b7c08 100644 --- a/local-app/python-tools/gfl-resource-actions/logging_utils/logging_utils.py +++ b/local-app/python-tools/gfl-resource-actions/logging_utils.py @@ -3,7 +3,7 @@ """ import logging import sys -from . import config +import config def setup_logging(): """Configure logging for the application.""" diff --git a/local-app/python-tools/gfl-resource-actions/main.py b/local-app/python-tools/gfl-resource-actions/main.py index c2604ba4..f7bb649e 100644 --- a/local-app/python-tools/gfl-resource-actions/main.py +++ b/local-app/python-tools/gfl-resource-actions/main.py @@ -4,11 +4,11 @@ """ 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 +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() diff --git a/local-app/python-tools/gfl-resource-actions/managers/__init__.py b/local-app/python-tools/gfl-resource-actions/managers/__init__.py index f34abf8a..72dde61f 100644 --- a/local-app/python-tools/gfl-resource-actions/managers/__init__.py +++ b/local-app/python-tools/gfl-resource-actions/managers/__init__.py @@ -2,7 +2,7 @@ Resource managers for different AWS resource types. """ -from .ec2 import EC2Manager -from .rds import RDSManager -from .eks import EKSManager -from .emr import EMRManager +from managers.ec2 import EC2Manager +from managers.rds import RDSManager +from managers.eks import EKSManager +from managers.emr import EMRManager diff --git a/local-app/python-tools/gfl-resource-actions/managers/__pycache__/__init__.cpython-311.pyc b/local-app/python-tools/gfl-resource-actions/managers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..507fb42776de803e7e51de108d7b6f5ed05e8893 GIT binary patch literal 557 zcma)2%}N6?5KeZtKNgFxu%MS7+Qp&>B7$g7QV;Dx*h>hT?7B4BER$5J@6fZ4AU=%X z!GrYb$y=efp3JsgXgxV3lkfW`naRv+v)Mq_rqj>l-JahCDb3k{#kmD9h@lWM%p8HG z6FRuSu5hIndMoUSimZm!6|RVytcP`rt3%YPJ)brPT&poe+?ON2ZyKC*uYJ@Nq5i;8^t9E$Q z1=ptCl7MOOb|pU2Q28TCfU1?=(87AOd<+;hG%y^1+E!%nq+q0T8gU&+1qnSlJKl=w zah@wMflv_@ZJOsnJ~0m}Ya6A64w6{3%l6weGO5aJn$4&JLeeZXgmm)BvVI{I8w-93 p2lgA-ZcVJ4yBOoS<6v)&3Rg$?=mQ;YudiKdL;q3M8K7xv`~vqlrUU>0 literal 0 HcmV?d00001 diff --git a/local-app/python-tools/gfl-resource-actions/managers/__pycache__/base.cpython-311.pyc b/local-app/python-tools/gfl-resource-actions/managers/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..006f7d383a3f5d119b28e0068eb4e4c7b250a252 GIT binary patch literal 2917 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 literal 0 HcmV?d00001 diff --git a/local-app/python-tools/gfl-resource-actions/managers/__pycache__/eks.cpython-311.pyc b/local-app/python-tools/gfl-resource-actions/managers/__pycache__/eks.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14fb9f005f2c2ecaacb8b9ae2c8696e81c77a169 GIT binary patch literal 9565 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 QqK#S~4k#wk0`~9DQP!rnxJM63L}! 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