runbooks 1.1.3__py3-none-any.whl → 1.1.5__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.
Files changed (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@
3
3
  DORA Metrics Engine for HITL System Optimization
4
4
 
5
5
  Issue #93: HITL System & DORA Metrics Optimization
6
- Priority: High (Sprint 1 Improvements)
6
+ Priority: High (Phase 1 Improvements)
7
7
  Scope: Optimize Human-in-the-Loop system and enhance DORA metrics collection
8
8
  """
9
9
 
@@ -1228,7 +1228,7 @@ async def simulate_dora_metrics_collection(duration_minutes: int = 5) -> Dict:
1228
1228
  deployments = [
1229
1229
  ("deploy-001", "production", "vpc-wrapper", "v1.2.0", "abc123", "manager"),
1230
1230
  ("deploy-002", "staging", "finops-dashboard", "v2.1.0", "def456", "architect"),
1231
- ("deploy-003", "production", "organizations-api", "v1.0.1", "ghi789", "manager"),
1231
+ ("deploy-003", "production", "organizations-api", "latest version", "ghi789", "manager"),
1232
1232
  ]
1233
1233
 
1234
1234
  for dep_id, env, service, version, commit, approver in deployments:
@@ -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
- ("inventory", "collect"), ("finops", "analyze"), ("security", "assess"),
422
- ("operate", "scan"), ("vpc", "analyze")
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("TABLE_NAME", "employees") # Table name needs default for compatibility
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(f"Analyzing {len(volumes_response['Volumes'])} volumes...", total=len(volumes_response['Volumes']))
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='AWS/EBS',
747
- MetricName='VolumeUsage',
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=['Average']
752
+ Statistics=["Average"],
758
753
  )
759
-
754
+
760
755
  # Analyze usage data - migrated from unSkript logic
761
- for datapoint in cloudwatch_response.get('Datapoints', []):
762
- if datapoint['Average'] < usage_threshold:
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['Average'],
772
- "timestamp": str(datapoint['Timestamp'])
766
+ "average_usage": datapoint["Average"],
767
+ "timestamp": str(datapoint["Timestamp"]),
773
768
  }
774
769
  low_usage_volumes.append(ebs_volume)
775
- logger.debug(f"Low usage volume found: {volume_id} (avg usage: {datapoint['Average']:.2f}%)")
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(f"[yellow]⚠️ Found {len(low_usage_volumes)} volumes with usage < {usage_threshold}%[/yellow]")
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 = f"Found {len(low_usage_volumes)} EBS volumes with usage < {usage_threshold}% in {context.region}"
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(self, context: OperationContext, volume_data: List[Dict[str, str]]) -> List[OperationResult]:
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 = f"Successfully deleted {len(successful_deletions)} EBS volumes: {', '.join(successful_deletions[:5])}"
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(f"[green]🎯 Deletion Summary: {len(successful_deletions)}/{len(results)} volumes deleted successfully[/green]")
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('ec2', region_name=region)
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('Addresses', []):
1431
+
1432
+ for eip in response.get("Addresses", []):
1427
1433
  # Check if EIP is not attached (no AssociationId)
1428
- if 'AssociationId' not in eip:
1434
+ if "AssociationId" not in eip:
1429
1435
  eip_info = {
1430
- 'public_ip': eip.get('PublicIp'),
1431
- 'allocation_id': eip.get('AllocationId'),
1432
- 'region': region,
1433
- 'domain': eip.get('Domain', 'vpc'),
1434
- 'network_interface_id': eip.get('NetworkInterfaceId'),
1435
- 'private_ip': eip.get('PrivateIpAddress'),
1436
- 'tags': eip.get('Tags', [])
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
- OperationStatus.SUCCESS,
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('ec2', region_name=region)
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('count', 0)
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('monthly_cost', 0)
1544
+ monthly_cost = eip_result.outputs.get("monthly_cost", 0)
1549
1545
  total_monthly_cost += monthly_cost
1550
- regions_with_waste.append({
1551
- 'region': eip_result.resource_id.split(':')[1],
1552
- 'count': count,
1553
- 'monthly_cost': monthly_cost
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
- 'total_unattached_eips': total_unattached,
1559
- 'total_monthly_cost': total_monthly_cost,
1560
- 'total_annual_cost': total_monthly_cost * 12,
1561
- 'regions_affected': len(regions_with_waste),
1562
- 'regions_detail': regions_with_waste,
1563
- 'cost_per_eip_monthly': 3.60,
1564
- 'recommendation': 'Release unattached Elastic IPs to save costs'
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('ec2', region_name='us-east-1')
1597
+ ec2_client = boto3.client("ec2", region_name="us-east-1")
1600
1598
  response = ec2_client.describe_regions()
1601
- return [region['RegionName'] for region in response['Regions']]
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
- 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1',
1606
- 'us-west-1', 'eu-central-1', 'ap-southeast-2'
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