runbooks 1.1.4__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 (228) 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/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +138 -35
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +11 -0
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +63 -74
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +201 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/cloud_foundations_integration.py +144 -149
  113. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  114. runbooks/inventory/collectors/aws_networking.py +109 -99
  115. runbooks/inventory/collectors/base.py +4 -0
  116. runbooks/inventory/core/collector.py +495 -313
  117. runbooks/inventory/drift_detection_cli.py +69 -96
  118. runbooks/inventory/inventory_mcp_cli.py +48 -46
  119. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  120. runbooks/inventory/mcp_inventory_validator.py +549 -465
  121. runbooks/inventory/mcp_vpc_validator.py +359 -442
  122. runbooks/inventory/organizations_discovery.py +55 -51
  123. runbooks/inventory/rich_inventory_display.py +33 -32
  124. runbooks/inventory/unified_validation_engine.py +278 -251
  125. runbooks/inventory/vpc_analyzer.py +732 -695
  126. runbooks/inventory/vpc_architecture_validator.py +293 -348
  127. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  128. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  129. runbooks/main.py +49 -34
  130. runbooks/main_final.py +91 -60
  131. runbooks/main_minimal.py +22 -10
  132. runbooks/main_optimized.py +131 -100
  133. runbooks/main_ultra_minimal.py +7 -2
  134. runbooks/mcp/__init__.py +36 -0
  135. runbooks/mcp/integration.py +679 -0
  136. runbooks/monitoring/performance_monitor.py +9 -4
  137. runbooks/operate/dynamodb_operations.py +3 -1
  138. runbooks/operate/ec2_operations.py +145 -137
  139. runbooks/operate/iam_operations.py +146 -152
  140. runbooks/operate/networking_cost_heatmap.py +29 -8
  141. runbooks/operate/rds_operations.py +223 -254
  142. runbooks/operate/s3_operations.py +107 -118
  143. runbooks/operate/vpc_operations.py +646 -616
  144. runbooks/remediation/base.py +1 -1
  145. runbooks/remediation/commons.py +10 -7
  146. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  147. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  148. runbooks/remediation/multi_account.py +24 -21
  149. runbooks/remediation/rds_snapshot_list.py +86 -60
  150. runbooks/remediation/remediation_cli.py +92 -146
  151. runbooks/remediation/universal_account_discovery.py +83 -79
  152. runbooks/remediation/workspaces_list.py +46 -41
  153. runbooks/security/__init__.py +19 -0
  154. runbooks/security/assessment_runner.py +1150 -0
  155. runbooks/security/baseline_checker.py +812 -0
  156. runbooks/security/cloudops_automation_security_validator.py +509 -535
  157. runbooks/security/compliance_automation_engine.py +17 -17
  158. runbooks/security/config/__init__.py +2 -2
  159. runbooks/security/config/compliance_config.py +50 -50
  160. runbooks/security/config_template_generator.py +63 -76
  161. runbooks/security/enterprise_security_framework.py +1 -1
  162. runbooks/security/executive_security_dashboard.py +519 -508
  163. runbooks/security/multi_account_security_controls.py +959 -1210
  164. runbooks/security/real_time_security_monitor.py +422 -444
  165. runbooks/security/security_baseline_tester.py +1 -1
  166. runbooks/security/security_cli.py +143 -112
  167. runbooks/security/test_2way_validation.py +439 -0
  168. runbooks/security/two_way_validation_framework.py +852 -0
  169. runbooks/sre/production_monitoring_framework.py +167 -177
  170. runbooks/tdd/__init__.py +15 -0
  171. runbooks/tdd/cli.py +1071 -0
  172. runbooks/utils/__init__.py +14 -17
  173. runbooks/utils/logger.py +7 -2
  174. runbooks/utils/version_validator.py +50 -47
  175. runbooks/validation/__init__.py +6 -6
  176. runbooks/validation/cli.py +9 -3
  177. runbooks/validation/comprehensive_2way_validator.py +745 -704
  178. runbooks/validation/mcp_validator.py +906 -228
  179. runbooks/validation/terraform_citations_validator.py +104 -115
  180. runbooks/validation/terraform_drift_detector.py +447 -451
  181. runbooks/vpc/README.md +617 -0
  182. runbooks/vpc/__init__.py +8 -1
  183. runbooks/vpc/analyzer.py +577 -0
  184. runbooks/vpc/cleanup_wrapper.py +476 -413
  185. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  186. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  187. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  188. runbooks/vpc/config.py +92 -97
  189. runbooks/vpc/cost_engine.py +411 -148
  190. runbooks/vpc/cost_explorer_integration.py +553 -0
  191. runbooks/vpc/cross_account_session.py +101 -106
  192. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  193. runbooks/vpc/eni_gate_validator.py +961 -0
  194. runbooks/vpc/heatmap_engine.py +185 -160
  195. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  196. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  197. runbooks/vpc/networking_wrapper.py +15 -8
  198. runbooks/vpc/pdca_remediation_planner.py +528 -0
  199. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  200. runbooks/vpc/runbooks_adapter.py +1167 -241
  201. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  202. runbooks/vpc/test_data_loader.py +358 -0
  203. runbooks/vpc/tests/conftest.py +314 -4
  204. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  205. runbooks/vpc/tests/test_cost_engine.py +0 -2
  206. runbooks/vpc/topology_generator.py +326 -0
  207. runbooks/vpc/unified_scenarios.py +1297 -1124
  208. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  209. runbooks-1.1.5.dist-info/METADATA +328 -0
  210. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
  211. runbooks/finops/README.md +0 -414
  212. runbooks/finops/accuracy_cross_validator.py +0 -647
  213. runbooks/finops/business_cases.py +0 -950
  214. runbooks/finops/dashboard_router.py +0 -922
  215. runbooks/finops/ebs_optimizer.py +0 -973
  216. runbooks/finops/embedded_mcp_validator.py +0 -1629
  217. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  218. runbooks/finops/finops_dashboard.py +0 -584
  219. runbooks/finops/finops_scenarios.py +0 -1218
  220. runbooks/finops/legacy_migration.py +0 -730
  221. runbooks/finops/multi_dashboard.py +0 -1519
  222. runbooks/finops/single_dashboard.py +0 -1113
  223. runbooks/finops/unlimited_scenarios.py +0 -393
  224. runbooks-1.1.4.dist-info/METADATA +0 -800
  225. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  226. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  227. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  228. {runbooks-1.1.4.dist-info → runbooks-1.1.5.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
- ("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