runbooks 0.7.9__py3-none-any.whl → 0.9.0__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 +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_integration.py +539 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +171 -0
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +339 -451
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +29 -1880
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/main.py +740 -134
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +7 -5
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +164 -33
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +931 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +36 -49
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +265 -236
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +62 -33
- runbooks/vpc/rich_formatters.py +22 -8
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/METADATA +136 -54
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/RECORD +94 -55
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/top_level.txt +0 -0
runbooks/operate/base.py
CHANGED
@@ -6,30 +6,35 @@ ensuring consistent patterns, safety features, and enterprise-grade reliability
|
|
6
6
|
across all service-specific operations.
|
7
7
|
"""
|
8
8
|
|
9
|
+
import asyncio
|
10
|
+
import time
|
9
11
|
from abc import ABC, abstractmethod
|
10
12
|
from dataclasses import dataclass, field
|
11
13
|
from datetime import datetime
|
12
14
|
from enum import Enum
|
13
|
-
from typing import Any, Dict, List, Optional, Union
|
14
|
-
import time
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
15
16
|
|
16
17
|
import boto3
|
17
18
|
from botocore.exceptions import ClientError, NoCredentialsError
|
18
19
|
from loguru import logger
|
19
20
|
from rich.console import Console
|
20
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
21
21
|
from rich.panel import Panel
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
22
23
|
from rich.table import Table
|
23
24
|
|
25
|
+
from runbooks.common.cross_module_integration import DataFlowType, EnterpriseCrossModuleIntegrator
|
26
|
+
from runbooks.common.mcp_integration import EnterpriseMCPIntegrator, MCPOperationType
|
27
|
+
from runbooks.common.profile_utils import create_operational_session, get_profile_for_operation
|
28
|
+
from runbooks.common.rich_utils import print_error, print_info, print_success, print_warning
|
24
29
|
from runbooks.inventory.models.account import AWSAccount
|
25
30
|
from runbooks.inventory.utils.aws_helpers import aws_api_retry, get_boto3_session
|
26
31
|
|
27
32
|
# Enterprise 4-Profile Architecture - Proven FinOps Patterns
|
28
33
|
ENTERPRISE_PROFILES = {
|
29
34
|
"BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185",
|
30
|
-
"MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185",
|
35
|
+
"MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185",
|
31
36
|
"CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030",
|
32
|
-
"SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520"
|
37
|
+
"SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520",
|
33
38
|
}
|
34
39
|
|
35
40
|
# Rich console instance for consistent formatting
|
@@ -147,16 +152,23 @@ class BaseOperation(ABC):
|
|
147
152
|
console.print(f"[blue]Using enterprise profile: {profile} -> {self.profile}[/blue]")
|
148
153
|
else:
|
149
154
|
self.profile = profile
|
150
|
-
|
155
|
+
|
151
156
|
self.region = region or "us-east-1"
|
152
157
|
self.dry_run = dry_run
|
153
158
|
self._session = None
|
154
159
|
self._clients = {}
|
155
|
-
|
160
|
+
|
156
161
|
# Performance benchmarking
|
157
162
|
self._operation_start_time = None
|
158
163
|
self._performance_target = 2.0 # <2s target for operate operations
|
159
164
|
|
165
|
+
# Phase 4: MCP Integration Framework
|
166
|
+
self.mcp_integrator = EnterpriseMCPIntegrator(self.profile)
|
167
|
+
self.cross_module_integrator = EnterpriseCrossModuleIntegrator(self.profile)
|
168
|
+
self.enable_mcp_validation = True
|
169
|
+
|
170
|
+
print_info(f"BaseOperation initialized with MCP integration for {self.service_name or 'unknown'} service")
|
171
|
+
|
160
172
|
@property
|
161
173
|
def session(self) -> boto3.Session:
|
162
174
|
"""Get or create AWS session."""
|
@@ -222,24 +234,28 @@ class BaseOperation(ABC):
|
|
222
234
|
True if operation is confirmed
|
223
235
|
"""
|
224
236
|
if context.dry_run:
|
225
|
-
console.print(
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
237
|
+
console.print(
|
238
|
+
Panel(
|
239
|
+
f"[yellow]Would perform {operation_type} on {resource_id}[/yellow]",
|
240
|
+
title="🏃 DRY-RUN MODE",
|
241
|
+
border_style="yellow",
|
242
|
+
)
|
243
|
+
)
|
230
244
|
return True
|
231
245
|
|
232
246
|
if context.force or not self.requires_confirmation:
|
233
247
|
return True
|
234
248
|
|
235
249
|
# Rich CLI confirmation display
|
236
|
-
console.print(
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
250
|
+
console.print(
|
251
|
+
Panel(
|
252
|
+
f"[red]⚠️ Destructive operation: {operation_type}[/red]\n"
|
253
|
+
f"[white]Resource: {resource_id}[/white]\n"
|
254
|
+
f"[white]Account: {context.account.account_id}[/white]",
|
255
|
+
title="🚨 CONFIRMATION REQUIRED",
|
256
|
+
border_style="red",
|
257
|
+
)
|
258
|
+
)
|
243
259
|
return True # Simplified for this implementation
|
244
260
|
|
245
261
|
@aws_api_retry
|
@@ -343,81 +359,233 @@ class BaseOperation(ABC):
|
|
343
359
|
# In a real implementation, this would query a database or log store
|
344
360
|
console.print(f"[blue]📊 Operation history requested for {resource_id or 'all resources'}[/blue]")
|
345
361
|
return []
|
346
|
-
|
362
|
+
|
347
363
|
def start_performance_benchmark(self) -> None:
|
348
364
|
"""Start performance timing for operation benchmarking."""
|
349
365
|
self._operation_start_time = time.time()
|
350
|
-
|
366
|
+
|
351
367
|
def end_performance_benchmark(self, operation_name: str) -> float:
|
352
368
|
"""
|
353
369
|
End performance timing and display results.
|
354
|
-
|
370
|
+
|
355
371
|
Args:
|
356
372
|
operation_name: Name of the operation for reporting
|
357
|
-
|
373
|
+
|
358
374
|
Returns:
|
359
375
|
Elapsed time in seconds
|
360
376
|
"""
|
361
377
|
if self._operation_start_time is None:
|
362
378
|
return 0.0
|
363
|
-
|
379
|
+
|
364
380
|
elapsed_time = time.time() - self._operation_start_time
|
365
|
-
|
381
|
+
|
366
382
|
# Performance validation against target
|
367
383
|
if elapsed_time <= self._performance_target:
|
368
|
-
console.print(
|
384
|
+
console.print(
|
385
|
+
f"[green]⚡ {operation_name} completed in {elapsed_time:.2f}s (target: {self._performance_target}s) ✅[/green]"
|
386
|
+
)
|
369
387
|
else:
|
370
|
-
console.print(
|
371
|
-
|
388
|
+
console.print(
|
389
|
+
f"[yellow]⚠️ {operation_name} completed in {elapsed_time:.2f}s (exceeded target: {self._performance_target}s)[/yellow]"
|
390
|
+
)
|
391
|
+
|
372
392
|
self._operation_start_time = None
|
373
393
|
return elapsed_time
|
374
|
-
|
394
|
+
|
375
395
|
def display_operation_summary(self, results: List[OperationResult]) -> None:
|
376
396
|
"""
|
377
397
|
Display operation summary using Rich table formatting.
|
378
|
-
|
398
|
+
|
379
399
|
Args:
|
380
400
|
results: List of operation results to summarize
|
381
401
|
"""
|
382
402
|
if not results:
|
383
403
|
console.print("[yellow]No operations to display[/yellow]")
|
384
404
|
return
|
385
|
-
|
405
|
+
|
386
406
|
table = Table(title="🔧 Operation Summary")
|
387
407
|
table.add_column("Operation", style="cyan")
|
388
408
|
table.add_column("Resource", style="magenta")
|
389
409
|
table.add_column("Status", style="green")
|
390
410
|
table.add_column("Duration", style="blue")
|
391
|
-
|
411
|
+
|
392
412
|
success_count = 0
|
393
413
|
total_count = len(results)
|
394
|
-
|
414
|
+
|
395
415
|
for result in results:
|
396
416
|
status_icon = "✅" if result.success else "❌"
|
397
417
|
status_text = f"{status_icon} {result.status.value}"
|
398
|
-
|
418
|
+
|
399
419
|
duration = "N/A"
|
400
420
|
if result.completed_at and result.started_at:
|
401
421
|
elapsed = (result.completed_at - result.started_at).total_seconds()
|
402
422
|
duration = f"{elapsed:.2f}s"
|
403
|
-
|
423
|
+
|
404
424
|
if result.success:
|
405
425
|
success_count += 1
|
406
|
-
|
407
|
-
table.add_row(
|
408
|
-
|
409
|
-
result.resource_id,
|
410
|
-
status_text,
|
411
|
-
duration
|
412
|
-
)
|
413
|
-
|
426
|
+
|
427
|
+
table.add_row(result.operation_type, result.resource_id, status_text, duration)
|
428
|
+
|
414
429
|
console.print(table)
|
415
|
-
|
430
|
+
|
416
431
|
# Success rate summary
|
417
432
|
success_rate = (success_count / total_count) * 100 if total_count > 0 else 0
|
418
433
|
if success_rate >= 95:
|
419
|
-
console.print(
|
434
|
+
console.print(
|
435
|
+
f"[green]🎯 Success Rate: {success_rate:.1f}% ({success_count}/{total_count}) - Excellent![/green]"
|
436
|
+
)
|
420
437
|
elif success_rate >= 90:
|
421
|
-
console.print(
|
438
|
+
console.print(
|
439
|
+
f"[yellow]📊 Success Rate: {success_rate:.1f}% ({success_count}/{total_count}) - Good[/yellow]"
|
440
|
+
)
|
422
441
|
else:
|
423
|
-
console.print(
|
442
|
+
console.print(
|
443
|
+
f"[red]⚠️ Success Rate: {success_rate:.1f}% ({success_count}/{total_count}) - Needs Attention[/red]"
|
444
|
+
)
|
445
|
+
|
446
|
+
# Phase 4: MCP Integration Methods
|
447
|
+
async def validate_operation_with_mcp(self, operation_data: Dict[str, Any]) -> Dict[str, Any]:
|
448
|
+
"""
|
449
|
+
Validate operation results using MCP integration.
|
450
|
+
|
451
|
+
Args:
|
452
|
+
operation_data: Operation results to validate
|
453
|
+
|
454
|
+
Returns:
|
455
|
+
Dictionary containing validation results
|
456
|
+
"""
|
457
|
+
try:
|
458
|
+
if not self.enable_mcp_validation:
|
459
|
+
return {"validation_skipped": True, "reason": "MCP validation disabled"}
|
460
|
+
|
461
|
+
print_info("Validating operation results with MCP integration")
|
462
|
+
|
463
|
+
validation_result = await self.mcp_integrator.validate_operate_operations(operation_data)
|
464
|
+
|
465
|
+
if validation_result.success:
|
466
|
+
print_success(f"Operation MCP validation passed: {validation_result.accuracy_score}% accuracy")
|
467
|
+
else:
|
468
|
+
print_warning("Operation MCP validation encountered issues")
|
469
|
+
|
470
|
+
return validation_result.to_dict()
|
471
|
+
|
472
|
+
except Exception as e:
|
473
|
+
print_error(f"MCP validation failed: {str(e)[:50]}...")
|
474
|
+
return {"validation_error": str(e), "validation_failed": True}
|
475
|
+
|
476
|
+
async def prepare_data_for_finops_analysis(self, operation_results: List[OperationResult]) -> Dict[str, Any]:
|
477
|
+
"""
|
478
|
+
Prepare operation results for FinOps cost analysis integration.
|
479
|
+
|
480
|
+
Args:
|
481
|
+
operation_results: List of operation results
|
482
|
+
|
483
|
+
Returns:
|
484
|
+
Dictionary formatted for FinOps module consumption
|
485
|
+
"""
|
486
|
+
try:
|
487
|
+
print_info("Preparing operation data for FinOps analysis")
|
488
|
+
|
489
|
+
# Convert operation results to data flow format
|
490
|
+
operation_data = {
|
491
|
+
"operations": [
|
492
|
+
{
|
493
|
+
"id": result.operation_id,
|
494
|
+
"type": result.operation_type,
|
495
|
+
"resource_id": result.resource_id,
|
496
|
+
"resource_type": result.resource_type,
|
497
|
+
"account_id": result.account_id,
|
498
|
+
"region": result.region,
|
499
|
+
"success": result.success,
|
500
|
+
"started_at": result.started_at.isoformat(),
|
501
|
+
"completed_at": result.completed_at.isoformat() if result.completed_at else None,
|
502
|
+
"metadata": result.metadata,
|
503
|
+
}
|
504
|
+
for result in operation_results
|
505
|
+
]
|
506
|
+
}
|
507
|
+
|
508
|
+
data_flow_result = await self.cross_module_integrator.execute_data_flow(
|
509
|
+
flow_type=DataFlowType.OPERATE_TO_FINOPS, source_data=operation_data
|
510
|
+
)
|
511
|
+
|
512
|
+
if data_flow_result.success:
|
513
|
+
print_success("Operate → FinOps data flow completed successfully")
|
514
|
+
return data_flow_result.transformed_data
|
515
|
+
else:
|
516
|
+
print_error(f"Data flow failed: {', '.join(data_flow_result.error_details)}")
|
517
|
+
return {}
|
518
|
+
|
519
|
+
except Exception as e:
|
520
|
+
print_error(f"Failed to prepare data for FinOps analysis: {str(e)}")
|
521
|
+
return {}
|
522
|
+
|
523
|
+
def execute_operation_with_validation(
|
524
|
+
self, context: OperationContext, operation_func: Callable, *args, **kwargs
|
525
|
+
) -> List[OperationResult]:
|
526
|
+
"""
|
527
|
+
Execute operation with automatic MCP validation and performance tracking.
|
528
|
+
|
529
|
+
Args:
|
530
|
+
context: Operation context
|
531
|
+
operation_func: Operation function to execute
|
532
|
+
*args: Arguments for operation function
|
533
|
+
**kwargs: Keyword arguments for operation function
|
534
|
+
|
535
|
+
Returns:
|
536
|
+
List of operation results with MCP validation
|
537
|
+
"""
|
538
|
+
self.start_performance_benchmark()
|
539
|
+
|
540
|
+
try:
|
541
|
+
# Execute the operation
|
542
|
+
results = operation_func(context, *args, **kwargs)
|
543
|
+
|
544
|
+
# Add MCP validation asynchronously if enabled
|
545
|
+
if self.enable_mcp_validation and results:
|
546
|
+
try:
|
547
|
+
operation_data = {
|
548
|
+
"operations": [result.__dict__ for result in results],
|
549
|
+
"context": context.__dict__,
|
550
|
+
}
|
551
|
+
|
552
|
+
validation_result = asyncio.run(self.validate_operation_with_mcp(operation_data))
|
553
|
+
|
554
|
+
# Add validation results to operation metadata
|
555
|
+
for result in results:
|
556
|
+
result.metadata["mcp_validation"] = validation_result
|
557
|
+
|
558
|
+
except Exception as e:
|
559
|
+
print_warning(f"MCP validation failed: {str(e)[:50]}... - operation completed without validation")
|
560
|
+
|
561
|
+
# End performance benchmark
|
562
|
+
elapsed_time = self.end_performance_benchmark(f"{context.operation_type} operation")
|
563
|
+
|
564
|
+
# Add performance metrics to results
|
565
|
+
for result in results:
|
566
|
+
result.metadata["performance_seconds"] = elapsed_time
|
567
|
+
result.metadata["performance_target_met"] = elapsed_time <= self._performance_target
|
568
|
+
|
569
|
+
return results
|
570
|
+
|
571
|
+
except Exception as e:
|
572
|
+
self.end_performance_benchmark(f"{context.operation_type} operation (failed)")
|
573
|
+
raise e
|
574
|
+
|
575
|
+
def get_mcp_integration_status(self) -> Dict[str, Any]:
|
576
|
+
"""
|
577
|
+
Get current MCP integration status and configuration.
|
578
|
+
|
579
|
+
Returns:
|
580
|
+
Dictionary containing MCP integration details
|
581
|
+
"""
|
582
|
+
return {
|
583
|
+
"service_name": self.service_name,
|
584
|
+
"mcp_validation_enabled": self.enable_mcp_validation,
|
585
|
+
"mcp_integrator_initialized": self.mcp_integrator is not None,
|
586
|
+
"cross_module_integrator_initialized": self.cross_module_integrator is not None,
|
587
|
+
"supported_operations": list(self.supported_operations),
|
588
|
+
"performance_target_seconds": self._performance_target,
|
589
|
+
"profile": self.profile,
|
590
|
+
"region": self.region,
|
591
|
+
}
|
@@ -200,11 +200,13 @@ class EC2Operations(BaseOperation):
|
|
200
200
|
|
201
201
|
try:
|
202
202
|
if context.dry_run:
|
203
|
-
console.print(
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
203
|
+
console.print(
|
204
|
+
Panel(
|
205
|
+
f"[yellow]Would start instance {instance_id}[/yellow]",
|
206
|
+
title="🏃 DRY-RUN MODE",
|
207
|
+
border_style="yellow",
|
208
|
+
)
|
209
|
+
)
|
208
210
|
result.mark_completed(OperationStatus.DRY_RUN)
|
209
211
|
else:
|
210
212
|
response = self.execute_aws_call(ec2_client, "start_instances", InstanceIds=[instance_id])
|
@@ -31,7 +31,6 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
|
31
31
|
import boto3
|
32
32
|
from botocore.exceptions import BotoCoreError, ClientError
|
33
33
|
|
34
|
-
from runbooks.operate.base import BaseOperation, OperationResult
|
35
34
|
from runbooks.common.rich_utils import (
|
36
35
|
console,
|
37
36
|
create_panel,
|
@@ -43,6 +42,7 @@ from runbooks.common.rich_utils import (
|
|
43
42
|
print_success,
|
44
43
|
print_warning,
|
45
44
|
)
|
45
|
+
from runbooks.operate.base import BaseOperation, OperationResult
|
46
46
|
|
47
47
|
logger = logging.getLogger(__name__)
|
48
48
|
|
@@ -29,7 +29,6 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
29
29
|
import boto3
|
30
30
|
from botocore.exceptions import BotoCoreError, ClientError
|
31
31
|
|
32
|
-
from runbooks.operate.base import BaseOperation, OperationResult
|
33
32
|
from runbooks.common.rich_utils import (
|
34
33
|
console,
|
35
34
|
create_panel,
|
@@ -39,6 +38,7 @@ from runbooks.common.rich_utils import (
|
|
39
38
|
print_status,
|
40
39
|
print_success,
|
41
40
|
)
|
41
|
+
from runbooks.operate.base import BaseOperation, OperationResult
|
42
42
|
|
43
43
|
logger = logging.getLogger(__name__)
|
44
44
|
|