runbooks 0.2.5__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +7 -7
- runbooks/security_baseline/report_template_jp.html +7 -7
- runbooks/security_baseline/report_template_kr.html +12 -12
- runbooks/security_baseline/report_template_vn.html +7 -7
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.5.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,88 @@
|
|
1
|
+
"""
|
2
|
+
AWS FinOps Dashboard - Cost and Resource Monitoring Tool
|
3
|
+
|
4
|
+
This module provides terminal-based AWS cost monitoring with features including:
|
5
|
+
- Multi-account cost summaries
|
6
|
+
- Service-level cost breakdown
|
7
|
+
- Budget monitoring
|
8
|
+
- EC2 resource status
|
9
|
+
- Cost trend analysis
|
10
|
+
- Audit reporting for optimization opportunities
|
11
|
+
|
12
|
+
Integrated as a submodule of CloudOps Runbooks for enterprise FinOps automation.
|
13
|
+
"""
|
14
|
+
|
15
|
+
__version__ = "0.7.0"
|
16
|
+
|
17
|
+
# Core components
|
18
|
+
# AWS client utilities
|
19
|
+
from runbooks.finops.aws_client import (
|
20
|
+
ec2_summary,
|
21
|
+
get_accessible_regions,
|
22
|
+
get_account_id,
|
23
|
+
get_aws_profiles,
|
24
|
+
get_budgets,
|
25
|
+
get_stopped_instances,
|
26
|
+
get_untagged_resources,
|
27
|
+
get_unused_eips,
|
28
|
+
get_unused_volumes,
|
29
|
+
)
|
30
|
+
|
31
|
+
# Data processors
|
32
|
+
from runbooks.finops.cost_processor import get_cost_data, get_trend
|
33
|
+
from runbooks.finops.dashboard_runner import run_dashboard
|
34
|
+
from runbooks.finops.helpers import (
|
35
|
+
export_audit_report_to_csv,
|
36
|
+
export_audit_report_to_json,
|
37
|
+
export_audit_report_to_pdf,
|
38
|
+
export_cost_dashboard_to_pdf,
|
39
|
+
export_to_csv,
|
40
|
+
export_to_json,
|
41
|
+
export_trend_data_to_json,
|
42
|
+
load_config_file,
|
43
|
+
)
|
44
|
+
from runbooks.finops.profile_processor import process_combined_profiles, process_single_profile
|
45
|
+
|
46
|
+
# Type definitions
|
47
|
+
from runbooks.finops.types import BudgetInfo, CostData, EC2Summary, ProfileData, RegionName
|
48
|
+
|
49
|
+
# Visualization and export
|
50
|
+
from runbooks.finops.visualisations import create_trend_bars
|
51
|
+
|
52
|
+
__all__ = [
|
53
|
+
# Core functionality
|
54
|
+
"run_dashboard",
|
55
|
+
# Processors
|
56
|
+
"get_cost_data",
|
57
|
+
"get_trend",
|
58
|
+
"process_single_profile",
|
59
|
+
"process_combined_profiles",
|
60
|
+
# AWS utilities
|
61
|
+
"get_aws_profiles",
|
62
|
+
"get_account_id",
|
63
|
+
"get_accessible_regions",
|
64
|
+
"ec2_summary",
|
65
|
+
"get_stopped_instances",
|
66
|
+
"get_unused_volumes",
|
67
|
+
"get_unused_eips",
|
68
|
+
"get_untagged_resources",
|
69
|
+
"get_budgets",
|
70
|
+
# Visualization and export
|
71
|
+
"create_trend_bars",
|
72
|
+
"export_to_csv",
|
73
|
+
"export_to_json",
|
74
|
+
"export_audit_report_to_pdf",
|
75
|
+
"export_cost_dashboard_to_pdf",
|
76
|
+
"export_audit_report_to_csv",
|
77
|
+
"export_audit_report_to_json",
|
78
|
+
"export_trend_data_to_json",
|
79
|
+
"load_config_file",
|
80
|
+
# Types
|
81
|
+
"ProfileData",
|
82
|
+
"CostData",
|
83
|
+
"BudgetInfo",
|
84
|
+
"EC2Summary",
|
85
|
+
"RegionName",
|
86
|
+
# Metadata
|
87
|
+
"__version__",
|
88
|
+
]
|
@@ -0,0 +1,245 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from typing import Dict, List, Optional
|
3
|
+
|
4
|
+
import boto3
|
5
|
+
from boto3.session import Session
|
6
|
+
from botocore.exceptions import ClientError
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from runbooks.finops.types import BudgetInfo, EC2Summary, RegionName
|
10
|
+
|
11
|
+
console = Console()
|
12
|
+
|
13
|
+
|
14
|
+
def get_aws_profiles() -> List[str]:
|
15
|
+
"""Get all configured AWS profiles from the AWS CLI configuration."""
|
16
|
+
try:
|
17
|
+
session = boto3.Session()
|
18
|
+
return session.available_profiles
|
19
|
+
except Exception as e:
|
20
|
+
console.print(f"[bold red]Error getting AWS profiles: {str(e)}[/]")
|
21
|
+
return []
|
22
|
+
|
23
|
+
|
24
|
+
def get_account_id(session: Session) -> Optional[str]:
|
25
|
+
"""Get the AWS account ID for a session."""
|
26
|
+
try:
|
27
|
+
account_id = session.client("sts").get_caller_identity().get("Account")
|
28
|
+
return str(account_id) if account_id is not None else None
|
29
|
+
except Exception as e:
|
30
|
+
console.log(f"[yellow]Warning: Could not get account ID: {str(e)}[/]")
|
31
|
+
return None
|
32
|
+
|
33
|
+
|
34
|
+
def get_all_regions(session: Session) -> List[RegionName]:
|
35
|
+
"""
|
36
|
+
Get all available AWS regions.
|
37
|
+
Using us-east-1 as a default region to get the list of all regions.
|
38
|
+
|
39
|
+
If the call fails, it will return a hardcoded list of common regions.
|
40
|
+
"""
|
41
|
+
try:
|
42
|
+
ec2_client = session.client("ec2", region_name="us-east-1")
|
43
|
+
regions = [region["RegionName"] for region in ec2_client.describe_regions()["Regions"]]
|
44
|
+
return regions
|
45
|
+
except Exception as e:
|
46
|
+
console.log(f"[yellow]Warning: Could not get all regions: {str(e)}[/]")
|
47
|
+
return [
|
48
|
+
"us-east-1",
|
49
|
+
"us-east-2",
|
50
|
+
"us-west-1",
|
51
|
+
"us-west-2",
|
52
|
+
"ap-southeast-1",
|
53
|
+
"ap-south-1",
|
54
|
+
"eu-west-1",
|
55
|
+
"eu-west-2",
|
56
|
+
"eu-central-1",
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
def get_accessible_regions(session: Session) -> List[RegionName]:
|
61
|
+
"""Get regions that are accessible with the current credentials."""
|
62
|
+
all_regions = get_all_regions(session)
|
63
|
+
accessible_regions = []
|
64
|
+
|
65
|
+
for region in all_regions:
|
66
|
+
try:
|
67
|
+
ec2_client = session.client("ec2", region_name=region)
|
68
|
+
ec2_client.describe_instances(MaxResults=5)
|
69
|
+
accessible_regions.append(region)
|
70
|
+
except Exception:
|
71
|
+
console.log(f"[yellow]Region {region} is not accessible with the current credentials[/]")
|
72
|
+
|
73
|
+
if not accessible_regions:
|
74
|
+
console.log("[yellow]No accessible regions found. Using default regions.[/]")
|
75
|
+
return ["us-east-1", "us-east-2", "us-west-1", "us-west-2"]
|
76
|
+
|
77
|
+
return accessible_regions
|
78
|
+
|
79
|
+
|
80
|
+
def ec2_summary(session: Session, regions: Optional[List[RegionName]] = None) -> EC2Summary:
|
81
|
+
"""Get EC2 instance summary across specified regions or all regions."""
|
82
|
+
if regions is None:
|
83
|
+
regions = [
|
84
|
+
"us-east-1",
|
85
|
+
"us-east-2",
|
86
|
+
"us-west-1",
|
87
|
+
"us-west-2",
|
88
|
+
"ap-southeast-1",
|
89
|
+
"ap-south-1",
|
90
|
+
"eu-central-1",
|
91
|
+
"eu-west-1",
|
92
|
+
"eu-west-2",
|
93
|
+
]
|
94
|
+
|
95
|
+
instance_summary: EC2Summary = defaultdict(int)
|
96
|
+
|
97
|
+
for region in regions:
|
98
|
+
try:
|
99
|
+
ec2_regional = session.client("ec2", region_name=region)
|
100
|
+
instances = ec2_regional.describe_instances()
|
101
|
+
for reservation in instances["Reservations"]:
|
102
|
+
for instance in reservation["Instances"]:
|
103
|
+
state = instance["State"]["Name"]
|
104
|
+
instance_summary[state] += 1
|
105
|
+
except Exception as e:
|
106
|
+
console.log(f"[yellow]Warning: Could not access EC2 in region {region}: {str(e)}[/]")
|
107
|
+
|
108
|
+
if "running" not in instance_summary:
|
109
|
+
instance_summary["running"] = 0
|
110
|
+
if "stopped" not in instance_summary:
|
111
|
+
instance_summary["stopped"] = 0
|
112
|
+
|
113
|
+
return instance_summary
|
114
|
+
|
115
|
+
|
116
|
+
def get_stopped_instances(session: Session, regions: List[RegionName]) -> Dict[RegionName, List[str]]:
|
117
|
+
"""Get stopped EC2 instances per region."""
|
118
|
+
stopped = {}
|
119
|
+
for region in regions:
|
120
|
+
try:
|
121
|
+
ec2 = session.client("ec2", region_name=region)
|
122
|
+
response = ec2.describe_instances(Filters=[{"Name": "instance-state-name", "Values": ["stopped"]}])
|
123
|
+
ids = [inst["InstanceId"] for res in response["Reservations"] for inst in res["Instances"]]
|
124
|
+
if ids:
|
125
|
+
stopped[region] = ids
|
126
|
+
except Exception as e:
|
127
|
+
console.log(f"[yellow]Warning: Could not fetch stopped instances in {region}: {str(e)}[/]")
|
128
|
+
return stopped
|
129
|
+
|
130
|
+
|
131
|
+
def get_unused_volumes(session: Session, regions: List[RegionName]) -> Dict[RegionName, List[str]]:
|
132
|
+
"""Get unattached EBS volumes per region."""
|
133
|
+
unused = {}
|
134
|
+
for region in regions:
|
135
|
+
try:
|
136
|
+
ec2 = session.client("ec2", region_name=region)
|
137
|
+
response = ec2.describe_volumes(Filters=[{"Name": "status", "Values": ["available"]}])
|
138
|
+
vols = [vol["VolumeId"] for vol in response["Volumes"]]
|
139
|
+
if vols:
|
140
|
+
unused[region] = vols
|
141
|
+
except Exception as e:
|
142
|
+
console.log(f"[yellow]Warning: Could not fetch unused volumes in {region}: {str(e)}[/]")
|
143
|
+
return unused
|
144
|
+
|
145
|
+
|
146
|
+
def get_unused_eips(session: Session, regions: List[RegionName]) -> Dict[RegionName, List[str]]:
|
147
|
+
"""Get unused Elastic IPs per region."""
|
148
|
+
eips = {}
|
149
|
+
for region in regions:
|
150
|
+
try:
|
151
|
+
ec2 = session.client("ec2", region_name=region)
|
152
|
+
response = ec2.describe_addresses()
|
153
|
+
free = [addr["PublicIp"] for addr in response["Addresses"] if not addr.get("AssociationId")]
|
154
|
+
if free:
|
155
|
+
eips[region] = free
|
156
|
+
except Exception as e:
|
157
|
+
console.log(f"[yellow]Warning: Could not fetch EIPs in {region}: {str(e)}[/]")
|
158
|
+
return eips
|
159
|
+
|
160
|
+
|
161
|
+
def get_untagged_resources(session: Session, regions: List[str]) -> Dict[str, Dict[str, List[str]]]:
|
162
|
+
result: Dict[str, Dict[str, List[str]]] = {
|
163
|
+
"EC2": {},
|
164
|
+
"RDS": {},
|
165
|
+
"Lambda": {},
|
166
|
+
"ELBv2": {},
|
167
|
+
}
|
168
|
+
|
169
|
+
for region in regions:
|
170
|
+
# EC2
|
171
|
+
try:
|
172
|
+
ec2 = session.client("ec2", region_name=region)
|
173
|
+
response = ec2.describe_instances()
|
174
|
+
for reservation in response["Reservations"]:
|
175
|
+
for instance in reservation["Instances"]:
|
176
|
+
if not instance.get("Tags"):
|
177
|
+
result["EC2"].setdefault(region, []).append(instance["InstanceId"])
|
178
|
+
except Exception as e:
|
179
|
+
console.log(f"[yellow]Warning: Could not fetch EC2 instances in {region}: {str(e)}[/]")
|
180
|
+
|
181
|
+
# RDS
|
182
|
+
try:
|
183
|
+
rds = session.client("rds", region_name=region)
|
184
|
+
response = rds.describe_db_instances()
|
185
|
+
for db_instance in response["DBInstances"]:
|
186
|
+
arn = db_instance["DBInstanceArn"]
|
187
|
+
tags = rds.list_tags_for_resource(ResourceName=arn).get("TagList", [])
|
188
|
+
if not tags:
|
189
|
+
result["RDS"].setdefault(region, []).append(db_instance["DBInstanceIdentifier"])
|
190
|
+
except Exception as e:
|
191
|
+
console.log(f"[yellow]Warning: Could not fetch RDS instances in {region}: {str(e)}[/]")
|
192
|
+
|
193
|
+
# Lambda
|
194
|
+
try:
|
195
|
+
lambda_client = session.client("lambda", region_name=region)
|
196
|
+
response = lambda_client.list_functions()
|
197
|
+
for function in response["Functions"]:
|
198
|
+
arn = function["FunctionArn"]
|
199
|
+
tags = lambda_client.list_tags(Resource=arn).get("Tags", {})
|
200
|
+
if not tags:
|
201
|
+
result["Lambda"].setdefault(region, []).append(function["FunctionName"])
|
202
|
+
except Exception as e:
|
203
|
+
console.log(f"[yellow]Warning: Could not fetch Lambda functions in {region}: {str(e)}[/]")
|
204
|
+
|
205
|
+
# ELBv2
|
206
|
+
try:
|
207
|
+
elbv2 = session.client("elbv2", region_name=region)
|
208
|
+
lbs = elbv2.describe_load_balancers().get("LoadBalancers", [])
|
209
|
+
|
210
|
+
if lbs:
|
211
|
+
arn_to_name = {lb["LoadBalancerArn"]: lb["LoadBalancerName"] for lb in lbs}
|
212
|
+
arns = list(arn_to_name.keys())
|
213
|
+
|
214
|
+
tags_response = elbv2.describe_tags(ResourceArns=arns)
|
215
|
+
for desc in tags_response["TagDescriptions"]:
|
216
|
+
arn = desc["ResourceArn"]
|
217
|
+
if not desc.get("Tags"):
|
218
|
+
lb_name = arn_to_name.get(arn, arn)
|
219
|
+
result["ELBv2"].setdefault(region, []).append(lb_name)
|
220
|
+
except Exception as e:
|
221
|
+
console.log(f"[yellow]Warning: Could not fetch ELBv2 load balancers in {region}: {str(e)}[/]")
|
222
|
+
|
223
|
+
return result
|
224
|
+
|
225
|
+
|
226
|
+
def get_budgets(session: Session) -> List[BudgetInfo]:
|
227
|
+
account_id = get_account_id(session)
|
228
|
+
budgets = session.client("budgets", region_name="us-east-1")
|
229
|
+
|
230
|
+
budgets_data: List[BudgetInfo] = []
|
231
|
+
try:
|
232
|
+
response = budgets.describe_budgets(AccountId=account_id)
|
233
|
+
for budget in response["Budgets"]:
|
234
|
+
budgets_data.append(
|
235
|
+
{
|
236
|
+
"name": budget["BudgetName"],
|
237
|
+
"limit": float(budget["BudgetLimit"]["Amount"]),
|
238
|
+
"actual": float(budget["CalculatedSpend"]["ActualSpend"]["Amount"]),
|
239
|
+
"forecast": float(budget["CalculatedSpend"].get("ForecastedSpend", {}).get("Amount", 0.0)) or None,
|
240
|
+
}
|
241
|
+
)
|
242
|
+
except Exception as e:
|
243
|
+
pass
|
244
|
+
|
245
|
+
return budgets_data
|
runbooks/finops/cli.py
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
import argparse
|
2
|
+
import sys
|
3
|
+
from typing import Any, Dict, Optional
|
4
|
+
|
5
|
+
import requests
|
6
|
+
from packaging import version
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from runbooks.finops.helpers import load_config_file
|
10
|
+
|
11
|
+
console = Console()
|
12
|
+
|
13
|
+
__version__ = "0.7.0"
|
14
|
+
|
15
|
+
|
16
|
+
def welcome_banner() -> None:
|
17
|
+
banner = rf"""
|
18
|
+
[bold red]
|
19
|
+
/$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$$ /$$ /$$$$$$
|
20
|
+
/$$__ $$| $$ /$ | $$ /$$__ $$ | $$_____/|__/ /$$__ $$
|
21
|
+
| $$ \ $$| $$ /$$$| $$| $$ \__/ | $$ /$$ /$$$$$$$ | $$ \ $$ /$$$$$$ /$$$$$$$
|
22
|
+
| $$$$$$$$| $$/$$ $$ $$| $$$$$$ | $$$$$ | $$| $$__ $$| $$ | $$ /$$__ $$ /$$_____/
|
23
|
+
| $$__ $$| $$$$_ $$$$ \____ $$ | $$__/ | $$| $$ \ $$| $$ | $$| $$ \ $$| $$$$$$
|
24
|
+
| $$ | $$| $$$/ \ $$$ /$$ \ $$ | $$ | $$| $$ | $$| $$ | $$| $$ | $$ \____ $$
|
25
|
+
| $$ | $$| $$/ \ $$| $$$$$$/ | $$ | $$| $$ | $$| $$$$$$/| $$$$$$$/ /$$$$$$$/
|
26
|
+
|__/ |__/|__/ \__/ \______/ |__/ |__/|__/ |__/ \______/ | $$____/ |_______/
|
27
|
+
| $$
|
28
|
+
| $$
|
29
|
+
|__/
|
30
|
+
[/]
|
31
|
+
[bold bright_blue]AWS FinOps Dashboard CLI (v{__version__})[/]
|
32
|
+
"""
|
33
|
+
console.print(banner)
|
34
|
+
|
35
|
+
|
36
|
+
def check_latest_version() -> None:
|
37
|
+
"""Check for the latest version of the AWS FinOps Dashboard (CLI)."""
|
38
|
+
try:
|
39
|
+
response = requests.get("https://pypi.org/pypi/aws-finops-dashboard/json", timeout=3)
|
40
|
+
latest = response.json()["info"]["version"]
|
41
|
+
if version.parse(latest) > version.parse(__version__):
|
42
|
+
console.print(f"[bold red]A new version of AWS FinOps Dashboard is available: {latest}[/]")
|
43
|
+
console.print(
|
44
|
+
"[bold bright_yellow]Please update using:\npipx upgrade aws-finops-dashboard\nor\npip install --upgrade aws-finops-dashboard\n[/]"
|
45
|
+
)
|
46
|
+
except Exception:
|
47
|
+
pass
|
48
|
+
|
49
|
+
|
50
|
+
def main() -> int:
|
51
|
+
"""Command-line interface entry point."""
|
52
|
+
welcome_banner()
|
53
|
+
check_latest_version()
|
54
|
+
from runbooks.finops.dashboard_runner import run_dashboard
|
55
|
+
|
56
|
+
# Create the parser instance to be accessible for get_default
|
57
|
+
parser = argparse.ArgumentParser(description="AWS FinOps Dashboard CLI")
|
58
|
+
|
59
|
+
parser.add_argument(
|
60
|
+
"--config-file",
|
61
|
+
"-C",
|
62
|
+
help="Path to a TOML, YAML, or JSON configuration file.",
|
63
|
+
type=str,
|
64
|
+
)
|
65
|
+
parser.add_argument(
|
66
|
+
"--profiles",
|
67
|
+
"-p",
|
68
|
+
nargs="+",
|
69
|
+
help="Specific AWS profiles to use (space-separated)",
|
70
|
+
type=str,
|
71
|
+
)
|
72
|
+
parser.add_argument(
|
73
|
+
"--regions",
|
74
|
+
"-r",
|
75
|
+
nargs="+",
|
76
|
+
help="AWS regions to check for EC2 instances (space-separated)",
|
77
|
+
type=str,
|
78
|
+
)
|
79
|
+
parser.add_argument("--all", "-a", action="store_true", help="Use all available AWS profiles")
|
80
|
+
parser.add_argument(
|
81
|
+
"--combine",
|
82
|
+
"-c",
|
83
|
+
action="store_true",
|
84
|
+
help="Combine profiles from the same AWS account",
|
85
|
+
)
|
86
|
+
parser.add_argument(
|
87
|
+
"--report-name",
|
88
|
+
"-n",
|
89
|
+
help="Specify the base name for the report file (without extension)",
|
90
|
+
default=None,
|
91
|
+
type=str,
|
92
|
+
)
|
93
|
+
parser.add_argument(
|
94
|
+
"--report-type",
|
95
|
+
"-y",
|
96
|
+
nargs="+",
|
97
|
+
choices=["csv", "json", "pdf"],
|
98
|
+
help="Specify one or more report types: csv and/or json and/or pdf (space-separated)",
|
99
|
+
type=str,
|
100
|
+
default=["csv"],
|
101
|
+
)
|
102
|
+
parser.add_argument(
|
103
|
+
"--dir",
|
104
|
+
"-d",
|
105
|
+
help="Directory to save the report files (default: current directory)",
|
106
|
+
type=str,
|
107
|
+
)
|
108
|
+
parser.add_argument(
|
109
|
+
"--time-range",
|
110
|
+
"-t",
|
111
|
+
help="Time range for cost data in days (default: current month). Examples: 7, 30, 90",
|
112
|
+
type=int,
|
113
|
+
)
|
114
|
+
parser.add_argument(
|
115
|
+
"--tag",
|
116
|
+
"-g",
|
117
|
+
nargs="+",
|
118
|
+
help="Cost allocation tag to filter resources, e.g., --tag Team=DevOps",
|
119
|
+
type=str,
|
120
|
+
)
|
121
|
+
parser.add_argument(
|
122
|
+
"--trend",
|
123
|
+
action="store_true",
|
124
|
+
help="Display a trend report as bars for the past 6 months time range",
|
125
|
+
)
|
126
|
+
parser.add_argument(
|
127
|
+
"--audit",
|
128
|
+
action="store_true",
|
129
|
+
help="Display an audit report with cost anomalies, stopped EC2 instances, unused EBS columes, budget alerts, and more",
|
130
|
+
)
|
131
|
+
|
132
|
+
args = parser.parse_args()
|
133
|
+
|
134
|
+
config_data: Optional[Dict[str, Any]] = None
|
135
|
+
if args.config_file:
|
136
|
+
config_data = load_config_file(args.config_file)
|
137
|
+
if config_data is None:
|
138
|
+
return 1 # Exit if config file loading failed
|
139
|
+
|
140
|
+
# Override args with config_data if present and arg is not set via CLI
|
141
|
+
if config_data:
|
142
|
+
for key, value in config_data.items():
|
143
|
+
if hasattr(args, key) and getattr(args, key) == parser.get_default(key):
|
144
|
+
setattr(args, key, value)
|
145
|
+
|
146
|
+
result = run_dashboard(args)
|
147
|
+
return 0 if result == 0 else 1
|
148
|
+
|
149
|
+
|
150
|
+
if __name__ == "__main__":
|
151
|
+
sys.exit(main())
|