runbooks 1.1.4__py3-none-any.whl → 1.1.6__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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +135 -91
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +17 -12
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +99 -79
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +315 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +461 -454
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- runbooks-1.1.4.dist-info/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -417,10 +417,15 @@ if __name__ == "__main__":
|
|
417
417
|
# REMOVED: Random performance simulation violates enterprise standards
|
418
418
|
# Use real performance metrics from actual AWS operations
|
419
419
|
# TODO: Replace with actual performance tracking from live operations
|
420
|
-
for i, (module, operation) in enumerate(
|
421
|
-
|
422
|
-
|
423
|
-
|
420
|
+
for i, (module, operation) in enumerate(
|
421
|
+
[
|
422
|
+
("inventory", "collect"),
|
423
|
+
("finops", "analyze"),
|
424
|
+
("security", "assess"),
|
425
|
+
("operate", "scan"),
|
426
|
+
("vpc", "analyze"),
|
427
|
+
]
|
428
|
+
):
|
424
429
|
# Use deterministic test data until real metrics are implemented
|
425
430
|
exec_time = 1.5 # Consistent performance target
|
426
431
|
success = True # Default success until real error tracking
|
@@ -75,7 +75,9 @@ class DynamoDBOperations(BaseOperation):
|
|
75
75
|
self.dry_run = dry_run or os.getenv("DRY_RUN", "false").lower() == "true"
|
76
76
|
|
77
77
|
# DynamoDB-specific environment variables from original file - NO hardcoded defaults
|
78
|
-
self.default_table_name = table_name or os.getenv(
|
78
|
+
self.default_table_name = table_name or os.getenv(
|
79
|
+
"TABLE_NAME", "employees"
|
80
|
+
) # Table name needs default for compatibility
|
79
81
|
self.max_batch_items = get_required_env_int("MAX_BATCH_ITEMS")
|
80
82
|
|
81
83
|
super().__init__(self.profile, self.region, self.dry_run)
|
@@ -195,9 +195,7 @@ class EC2Operations(BaseOperation):
|
|
195
195
|
return self.reboot_instances(context, kwargs.get("instance_ids", []))
|
196
196
|
elif operation_type == "get_ebs_volumes_with_low_usage":
|
197
197
|
return self.get_ebs_volumes_with_low_usage(
|
198
|
-
context,
|
199
|
-
kwargs.get("threshold_days", 10),
|
200
|
-
kwargs.get("usage_threshold", 10.0)
|
198
|
+
context, kwargs.get("threshold_days", 10), kwargs.get("usage_threshold", 10.0)
|
201
199
|
)
|
202
200
|
elif operation_type == "delete_volumes_by_id":
|
203
201
|
return self.delete_volumes_by_id(context, kwargs.get("volume_data", []))
|
@@ -703,63 +701,60 @@ class EC2Operations(BaseOperation):
|
|
703
701
|
) -> List[OperationResult]:
|
704
702
|
"""
|
705
703
|
Find EBS volumes with low usage based on CloudWatch VolumeUsage metric.
|
706
|
-
|
704
|
+
|
707
705
|
Migrated from unSkript notebook: AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
708
706
|
Function: aws_get_ebs_volume_for_low_usage()
|
709
|
-
|
707
|
+
|
710
708
|
Args:
|
711
709
|
context: Operation execution context
|
712
710
|
threshold_days: Number of days to analyze usage
|
713
711
|
usage_threshold: Usage percentage threshold (default: 10.0)
|
714
|
-
|
712
|
+
|
715
713
|
Returns:
|
716
714
|
List of OperationResults with low usage volumes found
|
717
715
|
"""
|
718
716
|
ec2_client = self.get_client("ec2", context.region)
|
719
717
|
cloudwatch_client = self.get_client("cloudwatch", context.region)
|
720
|
-
|
718
|
+
|
721
719
|
result = self.create_operation_result(context, "get_ebs_volumes_with_low_usage", "ec2:volume", "analysis")
|
722
|
-
|
720
|
+
|
723
721
|
try:
|
724
722
|
console.print(f"[blue]🔍 Analyzing EBS volume usage over {threshold_days} days...[/blue]")
|
725
|
-
|
723
|
+
|
726
724
|
# Get all volumes - migrated logic from unSkript notebook
|
727
725
|
volumes_response = self.execute_aws_call(ec2_client, "describe_volumes")
|
728
726
|
low_usage_volumes = []
|
729
|
-
|
727
|
+
|
730
728
|
now = datetime.utcnow()
|
731
729
|
days_ago = now - timedelta(days=threshold_days)
|
732
|
-
|
730
|
+
|
733
731
|
with Progress(
|
734
732
|
SpinnerColumn(),
|
735
733
|
TextColumn("[progress.description]{task.description}"),
|
736
734
|
transient=True,
|
737
735
|
) as progress:
|
738
|
-
task = progress.add_task(
|
739
|
-
|
736
|
+
task = progress.add_task(
|
737
|
+
f"Analyzing {len(volumes_response['Volumes'])} volumes...", total=len(volumes_response["Volumes"])
|
738
|
+
)
|
739
|
+
|
740
740
|
for volume in volumes_response["Volumes"]:
|
741
741
|
volume_id = volume["VolumeId"]
|
742
|
-
|
742
|
+
|
743
743
|
try:
|
744
744
|
# Get CloudWatch metrics for volume usage - exact logic from unSkript
|
745
745
|
cloudwatch_response = cloudwatch_client.get_metric_statistics(
|
746
|
-
Namespace=
|
747
|
-
MetricName=
|
748
|
-
Dimensions=[
|
749
|
-
{
|
750
|
-
'Name': 'VolumeId',
|
751
|
-
'Value': volume_id
|
752
|
-
}
|
753
|
-
],
|
746
|
+
Namespace="AWS/EBS",
|
747
|
+
MetricName="VolumeUsage",
|
748
|
+
Dimensions=[{"Name": "VolumeId", "Value": volume_id}],
|
754
749
|
StartTime=days_ago,
|
755
750
|
EndTime=now,
|
756
751
|
Period=3600,
|
757
|
-
Statistics=[
|
752
|
+
Statistics=["Average"],
|
758
753
|
)
|
759
|
-
|
754
|
+
|
760
755
|
# Analyze usage data - migrated from unSkript logic
|
761
|
-
for datapoint in cloudwatch_response.get(
|
762
|
-
if datapoint[
|
756
|
+
for datapoint in cloudwatch_response.get("Datapoints", []):
|
757
|
+
if datapoint["Average"] < usage_threshold:
|
763
758
|
ebs_volume = {
|
764
759
|
"volume_id": volume_id,
|
765
760
|
"region": context.region,
|
@@ -768,32 +763,36 @@ class EC2Operations(BaseOperation):
|
|
768
763
|
"volume_type": volume.get("VolumeType", "unknown"),
|
769
764
|
"encrypted": volume.get("Encrypted", False),
|
770
765
|
"create_time": str(volume["CreateTime"]),
|
771
|
-
"average_usage": datapoint[
|
772
|
-
"timestamp": str(datapoint[
|
766
|
+
"average_usage": datapoint["Average"],
|
767
|
+
"timestamp": str(datapoint["Timestamp"]),
|
773
768
|
}
|
774
769
|
low_usage_volumes.append(ebs_volume)
|
775
|
-
logger.debug(
|
770
|
+
logger.debug(
|
771
|
+
f"Low usage volume found: {volume_id} (avg usage: {datapoint['Average']:.2f}%)"
|
772
|
+
)
|
776
773
|
break
|
777
|
-
|
774
|
+
|
778
775
|
except ClientError as e:
|
779
776
|
# Handle individual volume metric errors gracefully
|
780
777
|
logger.warning(f"Could not get metrics for volume {volume_id}: {e}")
|
781
778
|
continue
|
782
|
-
|
779
|
+
|
783
780
|
progress.update(task, advance=1)
|
784
|
-
|
781
|
+
|
785
782
|
result.response_data = {
|
786
783
|
"low_usage_volumes": low_usage_volumes,
|
787
784
|
"count": len(low_usage_volumes),
|
788
785
|
"total_scanned": len(volumes_response["Volumes"]),
|
789
786
|
"threshold_days": threshold_days,
|
790
|
-
"usage_threshold": usage_threshold
|
787
|
+
"usage_threshold": usage_threshold,
|
791
788
|
}
|
792
789
|
result.mark_completed(OperationStatus.SUCCESS)
|
793
|
-
|
790
|
+
|
794
791
|
if low_usage_volumes:
|
795
|
-
console.print(
|
796
|
-
|
792
|
+
console.print(
|
793
|
+
f"[yellow]⚠️ Found {len(low_usage_volumes)} volumes with usage < {usage_threshold}%[/yellow]"
|
794
|
+
)
|
795
|
+
|
797
796
|
# Create Rich table for display
|
798
797
|
table = Table(title=f"Low Usage EBS Volumes (< {usage_threshold}%)")
|
799
798
|
table.add_column("Volume ID", style="cyan")
|
@@ -801,76 +800,80 @@ class EC2Operations(BaseOperation):
|
|
801
800
|
table.add_column("Type", style="green")
|
802
801
|
table.add_column("Usage %", justify="right", style="red")
|
803
802
|
table.add_column("State")
|
804
|
-
|
803
|
+
|
805
804
|
for vol in low_usage_volumes[:10]: # Show first 10
|
806
805
|
table.add_row(
|
807
806
|
vol["volume_id"],
|
808
807
|
str(vol["size"]),
|
809
808
|
vol["volume_type"],
|
810
809
|
f"{vol['average_usage']:.2f}%",
|
811
|
-
vol["state"]
|
810
|
+
vol["state"],
|
812
811
|
)
|
813
|
-
|
812
|
+
|
814
813
|
console.print(table)
|
815
|
-
|
814
|
+
|
816
815
|
if len(low_usage_volumes) > 10:
|
817
816
|
console.print(f"[dim]... and {len(low_usage_volumes) - 10} more volumes[/dim]")
|
818
|
-
|
817
|
+
|
819
818
|
# SNS notification
|
820
|
-
message =
|
819
|
+
message = (
|
820
|
+
f"Found {len(low_usage_volumes)} EBS volumes with usage < {usage_threshold}% in {context.region}"
|
821
|
+
)
|
821
822
|
self.send_sns_notification("Low Usage EBS Volumes Detected", message)
|
822
823
|
else:
|
823
824
|
console.print(f"[green]✅ No volumes found with usage < {usage_threshold}%[/green]")
|
824
|
-
|
825
|
+
|
825
826
|
except Exception as e:
|
826
827
|
error_msg = f"Failed to analyze EBS volume usage: {e}"
|
827
828
|
logger.error(error_msg)
|
828
829
|
result.mark_completed(OperationStatus.FAILED, error_msg)
|
829
|
-
|
830
|
+
|
830
831
|
return [result]
|
831
|
-
|
832
|
-
def delete_volumes_by_id(
|
832
|
+
|
833
|
+
def delete_volumes_by_id(
|
834
|
+
self, context: OperationContext, volume_data: List[Dict[str, str]]
|
835
|
+
) -> List[OperationResult]:
|
833
836
|
"""
|
834
837
|
Delete EBS volumes by ID with safety checks and confirmation.
|
835
|
-
|
838
|
+
|
836
839
|
Migrated from unSkript notebook: AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
837
840
|
Function: aws_delete_volume_by_id()
|
838
|
-
|
841
|
+
|
839
842
|
Args:
|
840
843
|
context: Operation execution context
|
841
844
|
volume_data: List of dicts with 'volume_id' and 'region' keys
|
842
|
-
|
845
|
+
|
843
846
|
Returns:
|
844
847
|
List of OperationResults for each volume deletion attempt
|
845
848
|
"""
|
846
849
|
results = []
|
847
|
-
|
850
|
+
|
848
851
|
for vol_data in volume_data:
|
849
852
|
volume_id = vol_data.get("volume_id")
|
850
853
|
region = vol_data.get("region", context.region)
|
851
|
-
|
854
|
+
|
852
855
|
ec2_client = self.get_client("ec2", region)
|
853
856
|
result = self.create_operation_result(context, "delete_volumes_by_id", "ec2:volume", volume_id)
|
854
|
-
|
857
|
+
|
855
858
|
try:
|
856
859
|
# Safety confirmation - enhanced from original
|
857
860
|
if not self.confirm_operation(context, volume_id, "delete EBS volume"):
|
858
861
|
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
859
862
|
results.append(result)
|
860
863
|
continue
|
861
|
-
|
864
|
+
|
862
865
|
if context.dry_run:
|
863
866
|
console.print(f"[yellow]🏃 DRY-RUN: Would delete volume {volume_id} in {region}[/yellow]")
|
864
867
|
result.mark_completed(OperationStatus.DRY_RUN)
|
865
868
|
else:
|
866
869
|
# Execute deletion - exact logic from unSkript
|
867
870
|
delete_response = self.execute_aws_call(ec2_client, "delete_volume", VolumeId=volume_id)
|
868
|
-
|
871
|
+
|
869
872
|
result.response_data = delete_response
|
870
873
|
result.mark_completed(OperationStatus.SUCCESS)
|
871
874
|
console.print(f"[green]✅ Successfully deleted volume {volume_id}[/green]")
|
872
875
|
logger.info(f"Deleted EBS volume: {volume_id} in {region}")
|
873
|
-
|
876
|
+
|
874
877
|
except ClientError as e:
|
875
878
|
error_msg = f"Failed to delete volume {volume_id}: {e}"
|
876
879
|
console.print(f"[red]❌ {error_msg}[/red]")
|
@@ -881,18 +884,22 @@ class EC2Operations(BaseOperation):
|
|
881
884
|
console.print(f"[red]❌ {error_msg}[/red]")
|
882
885
|
logger.error(error_msg)
|
883
886
|
result.mark_completed(OperationStatus.FAILED, error_msg)
|
884
|
-
|
887
|
+
|
885
888
|
results.append(result)
|
886
|
-
|
889
|
+
|
887
890
|
# Summary reporting
|
888
891
|
successful_deletions = [r.resource_id for r in results if r.success]
|
889
892
|
if successful_deletions:
|
890
|
-
message =
|
893
|
+
message = (
|
894
|
+
f"Successfully deleted {len(successful_deletions)} EBS volumes: {', '.join(successful_deletions[:5])}"
|
895
|
+
)
|
891
896
|
if len(successful_deletions) > 5:
|
892
897
|
message += f" and {len(successful_deletions) - 5} more"
|
893
898
|
self.send_sns_notification("EBS Volumes Deleted", message)
|
894
|
-
console.print(
|
895
|
-
|
899
|
+
console.print(
|
900
|
+
f"[green]🎯 Deletion Summary: {len(successful_deletions)}/{len(results)} volumes deleted successfully[/green]"
|
901
|
+
)
|
902
|
+
|
896
903
|
return results
|
897
904
|
|
898
905
|
def cleanup_unused_eips(self, context: OperationContext) -> List[OperationResult]:
|
@@ -1387,26 +1394,25 @@ def lambda_handler_run_instances(event, context):
|
|
1387
1394
|
logger.error(f"Lambda Handler Error: {e}")
|
1388
1395
|
return {"statusCode": 500, "body": {"error": str(e)}}
|
1389
1396
|
|
1390
|
-
|
1391
|
-
# CLI Support
|
1397
|
+
# CLI Support
|
1392
1398
|
def list_unattached_elastic_ips(self, context: OperationContext) -> List[OperationResult]:
|
1393
1399
|
"""
|
1394
1400
|
Find all unattached Elastic IPs across regions.
|
1395
|
-
|
1401
|
+
|
1396
1402
|
Extracted from: AWS_Release_Unattached_Elastic_IPs.ipynb
|
1397
|
-
|
1403
|
+
|
1398
1404
|
Args:
|
1399
1405
|
context: Operation execution context
|
1400
|
-
|
1406
|
+
|
1401
1407
|
Returns:
|
1402
1408
|
List of OperationResults with unattached Elastic IPs
|
1403
1409
|
"""
|
1404
1410
|
console.print("[bold cyan]Scanning for unattached Elastic IPs...[/bold cyan]")
|
1405
1411
|
results = []
|
1406
|
-
|
1412
|
+
|
1407
1413
|
# Get all regions to check
|
1408
1414
|
regions_to_check = [context.region] if context.region else self._get_all_regions()
|
1409
|
-
|
1415
|
+
|
1410
1416
|
for region in regions_to_check:
|
1411
1417
|
result = OperationResult(
|
1412
1418
|
operation_id=f"list_unattached_eips_{region}",
|
@@ -1414,65 +1420,61 @@ def lambda_handler_run_instances(event, context):
|
|
1414
1420
|
resource_id=f"region:{region}",
|
1415
1421
|
resource_type="elastic_ip",
|
1416
1422
|
)
|
1417
|
-
|
1423
|
+
|
1418
1424
|
try:
|
1419
1425
|
# Create EC2 client for specific region
|
1420
|
-
ec2_client = boto3.client(
|
1421
|
-
|
1426
|
+
ec2_client = boto3.client("ec2", region_name=region)
|
1427
|
+
|
1422
1428
|
# Get all Elastic IPs in region
|
1423
1429
|
response = ec2_client.describe_addresses()
|
1424
1430
|
unattached_eips = []
|
1425
|
-
|
1426
|
-
for eip in response.get(
|
1431
|
+
|
1432
|
+
for eip in response.get("Addresses", []):
|
1427
1433
|
# Check if EIP is not attached (no AssociationId)
|
1428
|
-
if
|
1434
|
+
if "AssociationId" not in eip:
|
1429
1435
|
eip_info = {
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1436
|
+
"public_ip": eip.get("PublicIp"),
|
1437
|
+
"allocation_id": eip.get("AllocationId"),
|
1438
|
+
"region": region,
|
1439
|
+
"domain": eip.get("Domain", "vpc"),
|
1440
|
+
"network_interface_id": eip.get("NetworkInterfaceId"),
|
1441
|
+
"private_ip": eip.get("PrivateIpAddress"),
|
1442
|
+
"tags": eip.get("Tags", []),
|
1437
1443
|
}
|
1438
1444
|
unattached_eips.append(eip_info)
|
1439
|
-
|
1445
|
+
|
1440
1446
|
if unattached_eips:
|
1441
1447
|
result.add_output("unattached_eips", unattached_eips)
|
1442
1448
|
result.add_output("count", len(unattached_eips))
|
1443
1449
|
result.add_output("monthly_cost", len(unattached_eips) * 3.60) # $3.60/month per EIP
|
1444
1450
|
result.mark_completed(
|
1445
|
-
OperationStatus.SUCCESS,
|
1446
|
-
f"Found {len(unattached_eips)} unattached Elastic IPs in {region}"
|
1451
|
+
OperationStatus.SUCCESS, f"Found {len(unattached_eips)} unattached Elastic IPs in {region}"
|
1447
1452
|
)
|
1448
1453
|
console.print(f"[yellow]Found {len(unattached_eips)} unattached EIPs in {region}[/yellow]")
|
1449
1454
|
else:
|
1450
|
-
result.mark_completed(
|
1451
|
-
|
1452
|
-
f"No unattached Elastic IPs found in {region}"
|
1453
|
-
)
|
1454
|
-
|
1455
|
+
result.mark_completed(OperationStatus.SUCCESS, f"No unattached Elastic IPs found in {region}")
|
1456
|
+
|
1455
1457
|
except ClientError as e:
|
1456
1458
|
error_msg = f"Failed to list Elastic IPs in {region}: {e}"
|
1457
1459
|
logger.error(error_msg)
|
1458
1460
|
result.mark_completed(OperationStatus.FAILED, error_msg)
|
1459
1461
|
console.print(f"[red]Error scanning {region}: {e}[/red]")
|
1460
|
-
|
1462
|
+
|
1461
1463
|
results.append(result)
|
1462
|
-
|
1464
|
+
|
1463
1465
|
return results
|
1464
1466
|
|
1465
1467
|
def release_elastic_ip(self, context: OperationContext, allocation_id: str, region: str) -> OperationResult:
|
1466
1468
|
"""
|
1467
1469
|
Release (delete) an unattached Elastic IP.
|
1468
|
-
|
1470
|
+
|
1469
1471
|
Extracted from: AWS_Release_Unattached_Elastic_IPs.ipynb
|
1470
|
-
|
1472
|
+
|
1471
1473
|
Args:
|
1472
1474
|
context: Operation execution context
|
1473
1475
|
allocation_id: Allocation ID of the Elastic IP
|
1474
1476
|
region: AWS region where the EIP exists
|
1475
|
-
|
1477
|
+
|
1476
1478
|
Returns:
|
1477
1479
|
OperationResult with release status
|
1478
1480
|
"""
|
@@ -1482,18 +1484,15 @@ def lambda_handler_run_instances(event, context):
|
|
1482
1484
|
resource_id=allocation_id,
|
1483
1485
|
resource_type="elastic_ip",
|
1484
1486
|
)
|
1485
|
-
|
1487
|
+
|
1486
1488
|
try:
|
1487
|
-
ec2_client = boto3.client(
|
1488
|
-
|
1489
|
+
ec2_client = boto3.client("ec2", region_name=region)
|
1490
|
+
|
1489
1491
|
if context.dry_run:
|
1490
1492
|
result.add_output("action", "DRY_RUN")
|
1491
1493
|
result.add_output("would_release", allocation_id)
|
1492
1494
|
result.add_output("monthly_savings", 3.60)
|
1493
|
-
result.mark_completed(
|
1494
|
-
OperationStatus.SUCCESS,
|
1495
|
-
f"DRY RUN: Would release Elastic IP {allocation_id}"
|
1496
|
-
)
|
1495
|
+
result.mark_completed(OperationStatus.SUCCESS, f"DRY RUN: Would release Elastic IP {allocation_id}")
|
1497
1496
|
console.print(f"[yellow]DRY RUN: Would release EIP {allocation_id}[/yellow]")
|
1498
1497
|
else:
|
1499
1498
|
# Actually release the Elastic IP
|
@@ -1501,27 +1500,24 @@ def lambda_handler_run_instances(event, context):
|
|
1501
1500
|
result.add_output("response", response)
|
1502
1501
|
result.add_output("released", True)
|
1503
1502
|
result.add_output("monthly_savings", 3.60)
|
1504
|
-
result.mark_completed(
|
1505
|
-
OperationStatus.SUCCESS,
|
1506
|
-
f"Successfully released Elastic IP {allocation_id}"
|
1507
|
-
)
|
1503
|
+
result.mark_completed(OperationStatus.SUCCESS, f"Successfully released Elastic IP {allocation_id}")
|
1508
1504
|
console.print(f"[green]✅ Released Elastic IP {allocation_id}[/green]")
|
1509
|
-
|
1505
|
+
|
1510
1506
|
except ClientError as e:
|
1511
1507
|
error_msg = f"Failed to release Elastic IP {allocation_id}: {e}"
|
1512
1508
|
logger.error(error_msg)
|
1513
1509
|
result.mark_completed(OperationStatus.FAILED, error_msg)
|
1514
1510
|
console.print(f"[red]❌ Failed to release {allocation_id}: {e}[/red]")
|
1515
|
-
|
1511
|
+
|
1516
1512
|
return result
|
1517
1513
|
|
1518
1514
|
def get_elastic_ip_cost_impact(self, context: OperationContext) -> OperationResult:
|
1519
1515
|
"""
|
1520
1516
|
Calculate cost impact of unattached Elastic IPs.
|
1521
|
-
|
1517
|
+
|
1522
1518
|
Args:
|
1523
1519
|
context: Operation execution context
|
1524
|
-
|
1520
|
+
|
1525
1521
|
Returns:
|
1526
1522
|
OperationResult with cost analysis
|
1527
1523
|
"""
|
@@ -1531,79 +1527,86 @@ def lambda_handler_run_instances(event, context):
|
|
1531
1527
|
resource_id=f"account:{context.account_id}",
|
1532
1528
|
resource_type="cost_analysis",
|
1533
1529
|
)
|
1534
|
-
|
1530
|
+
|
1535
1531
|
try:
|
1536
1532
|
# Get all unattached EIPs
|
1537
1533
|
eip_results = self.list_unattached_elastic_ips(context)
|
1538
|
-
|
1534
|
+
|
1539
1535
|
total_unattached = 0
|
1540
1536
|
total_monthly_cost = 0.0
|
1541
1537
|
regions_with_waste = []
|
1542
|
-
|
1538
|
+
|
1543
1539
|
for eip_result in eip_results:
|
1544
1540
|
if eip_result.status == OperationStatus.SUCCESS and eip_result.outputs:
|
1545
|
-
count = eip_result.outputs.get(
|
1541
|
+
count = eip_result.outputs.get("count", 0)
|
1546
1542
|
if count > 0:
|
1547
1543
|
total_unattached += count
|
1548
|
-
monthly_cost = eip_result.outputs.get(
|
1544
|
+
monthly_cost = eip_result.outputs.get("monthly_cost", 0)
|
1549
1545
|
total_monthly_cost += monthly_cost
|
1550
|
-
regions_with_waste.append(
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1546
|
+
regions_with_waste.append(
|
1547
|
+
{
|
1548
|
+
"region": eip_result.resource_id.split(":")[1],
|
1549
|
+
"count": count,
|
1550
|
+
"monthly_cost": monthly_cost,
|
1551
|
+
}
|
1552
|
+
)
|
1553
|
+
|
1556
1554
|
# Create cost analysis summary
|
1557
1555
|
cost_summary = {
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1556
|
+
"total_unattached_eips": total_unattached,
|
1557
|
+
"total_monthly_cost": total_monthly_cost,
|
1558
|
+
"total_annual_cost": total_monthly_cost * 12,
|
1559
|
+
"regions_affected": len(regions_with_waste),
|
1560
|
+
"regions_detail": regions_with_waste,
|
1561
|
+
"cost_per_eip_monthly": 3.60,
|
1562
|
+
"recommendation": "Release unattached Elastic IPs to save costs",
|
1565
1563
|
}
|
1566
|
-
|
1564
|
+
|
1567
1565
|
result.add_output("cost_analysis", cost_summary)
|
1568
1566
|
result.mark_completed(
|
1569
1567
|
OperationStatus.SUCCESS,
|
1570
|
-
f"Cost analysis complete: ${total_monthly_cost:.2f}/month waste from {total_unattached} unattached EIPs"
|
1568
|
+
f"Cost analysis complete: ${total_monthly_cost:.2f}/month waste from {total_unattached} unattached EIPs",
|
1571
1569
|
)
|
1572
|
-
|
1570
|
+
|
1573
1571
|
# Display cost impact table
|
1574
1572
|
if total_unattached > 0:
|
1575
1573
|
table = Table(title="Elastic IP Cost Impact Analysis")
|
1576
1574
|
table.add_column("Metric", style="cyan")
|
1577
1575
|
table.add_column("Value", style="yellow")
|
1578
|
-
|
1576
|
+
|
1579
1577
|
table.add_row("Unattached EIPs", str(total_unattached))
|
1580
1578
|
table.add_row("Monthly Cost", f"${total_monthly_cost:.2f}")
|
1581
1579
|
table.add_row("Annual Cost", f"${total_monthly_cost * 12:.2f}")
|
1582
1580
|
table.add_row("Regions Affected", str(len(regions_with_waste)))
|
1583
|
-
|
1581
|
+
|
1584
1582
|
console.print(table)
|
1585
1583
|
console.print(f"[bold red]💰 Potential savings: ${total_monthly_cost:.2f}/month[/bold red]")
|
1586
1584
|
else:
|
1587
1585
|
console.print("[green]✅ No unattached Elastic IPs found - no waste![/green]")
|
1588
|
-
|
1586
|
+
|
1589
1587
|
except Exception as e:
|
1590
1588
|
error_msg = f"Failed to analyze Elastic IP costs: {e}"
|
1591
1589
|
logger.error(error_msg)
|
1592
1590
|
result.mark_completed(OperationStatus.FAILED, error_msg)
|
1593
|
-
|
1591
|
+
|
1594
1592
|
return result
|
1595
1593
|
|
1596
1594
|
def _get_all_regions(self) -> List[str]:
|
1597
1595
|
"""Get all available AWS regions for EC2."""
|
1598
1596
|
try:
|
1599
|
-
ec2_client = boto3.client(
|
1597
|
+
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
1600
1598
|
response = ec2_client.describe_regions()
|
1601
|
-
return [region[
|
1599
|
+
return [region["RegionName"] for region in response["Regions"]]
|
1602
1600
|
except Exception:
|
1603
1601
|
# Fallback to common regions if API call fails
|
1604
1602
|
return [
|
1605
|
-
|
1606
|
-
|
1603
|
+
"us-east-1",
|
1604
|
+
"us-west-2",
|
1605
|
+
"eu-west-1",
|
1606
|
+
"ap-southeast-1",
|
1607
|
+
"us-west-1",
|
1608
|
+
"eu-central-1",
|
1609
|
+
"ap-southeast-2",
|
1607
1610
|
]
|
1608
1611
|
|
1609
1612
|
|
@@ -1663,3 +1666,8 @@ def main():
|
|
1663
1666
|
|
1664
1667
|
if __name__ == "__main__":
|
1665
1668
|
main()
|
1669
|
+
|
1670
|
+
|
1671
|
+
# Aliases for backward compatibility
|
1672
|
+
InstanceManager = EC2Operations
|
1673
|
+
SecurityGroupManager = EC2Operations
|