Coverage for cli / commands / capacity_cmd.py: 100.00%

825 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""Capacity checking commands.""" 

2 

3import sys 

4from typing import Any 

5 

6import click 

7from botocore.exceptions import ClientError 

8 

9from gco.bedrock import BEDROCK_FTU_REMEDIATION, is_bedrock_ftu_form_error 

10 

11from ..capacity import get_capacity_checker 

12from ..capacity.history import METRIC_FIELDS 

13from ..config import GCOConfig 

14from ..output import confirm, format_capacity_table, get_output_formatter 

15 

16pass_config = click.make_pass_decorator(GCOConfig, ensure=True) 

17 

18 

19@click.group() 

20@pass_config 

21def capacity(config: Any) -> None: 

22 """Check EC2 capacity availability.""" 

23 

24 

25@capacity.command("check") 

26@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

27@click.option("--region", "-r", required=True, help="AWS region") 

28@click.option( 

29 "--type", 

30 "-t", 

31 "capacity_type", 

32 type=click.Choice(["spot", "on-demand", "both"]), 

33 default="both", 

34 help="Capacity type to check", 

35) 

36@click.option( 

37 "--enrich-historical", 

38 is_flag=True, 

39 help="Append historical capacity context (requires historical.enabled)", 

40) 

41@pass_config 

42def check_capacity( 

43 config: Any, 

44 instance_type: Any, 

45 region: Any, 

46 capacity_type: Any, 

47 enrich_historical: Any, 

48) -> None: 

49 """Check capacity availability for an instance type. 

50 

51 Provides estimates based on spot price history and availability patterns. 

52 """ 

53 formatter = get_output_formatter(config) 

54 checker = get_capacity_checker(config) 

55 

56 try: 

57 estimates = checker.estimate_capacity(instance_type, region, capacity_type) 

58 

59 if config.output_format == "table": 

60 print(format_capacity_table(estimates)) 

61 else: 

62 formatter.print(estimates) 

63 

64 if enrich_historical: 

65 _print_historical_enrichment(formatter, instance_type, region) 

66 

67 except Exception as e: 

68 formatter.print_error(f"Failed to check capacity: {e}") 

69 sys.exit(1) 

70 

71 

72@capacity.command("recommend") 

73@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

74@click.option("--region", "-r", required=True, help="AWS region") 

75@click.option( 

76 "--fault-tolerance", 

77 "-f", 

78 type=click.Choice(["high", "medium", "low"]), 

79 default="medium", 

80 help="Fault tolerance level", 

81) 

82@pass_config 

83def recommend_capacity(config: Any, instance_type: Any, region: Any, fault_tolerance: Any) -> None: 

84 """Get capacity type recommendation for a workload.""" 

85 formatter = get_output_formatter(config) 

86 checker = get_capacity_checker(config) 

87 

88 try: 

89 capacity_type, explanation = checker.recommend_capacity_type( 

90 instance_type, region, fault_tolerance 

91 ) 

92 

93 if config.output_format == "table": 

94 formatter.print_info(f"Recommended: {capacity_type.upper()}") 

95 formatter.print_info(f"Reason: {explanation}") 

96 else: 

97 formatter.print( 

98 { 

99 "capacity_type": capacity_type, 

100 "explanation": explanation, 

101 } 

102 ) 

103 

104 except Exception as e: 

105 formatter.print_error(f"Failed to get recommendation: {e}") 

106 sys.exit(1) 

107 

108 

109@capacity.command("spot-prices") 

110@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

111@click.option("--region", "-r", required=True, help="AWS region") 

112@click.option("--days", "-d", default=7, help="Days of history") 

113@pass_config 

114def spot_prices(config: Any, instance_type: Any, region: Any, days: Any) -> None: 

115 """Get spot price history for an instance type.""" 

116 formatter = get_output_formatter(config) 

117 checker = get_capacity_checker(config) 

118 

119 try: 

120 prices = checker.get_spot_price_history(instance_type, region, days) 

121 

122 if not prices: 

123 formatter.print_warning(f"No spot price data for {instance_type} in {region}") 

124 return 

125 

126 formatter.print( 

127 prices, 

128 columns=[ 

129 "availability_zone", 

130 "current_price", 

131 "avg_price_7d", 

132 "min_price_7d", 

133 "max_price_7d", 

134 "price_stability", 

135 ], 

136 ) 

137 

138 except Exception as e: 

139 formatter.print_error(f"Failed to get spot prices: {e}") 

140 sys.exit(1) 

141 

142 

143@capacity.command("instance-info") 

144@click.argument("instance_type") 

145@click.option( 

146 "--region", 

147 "-r", 

148 help="Region to describe the type in (default: the configured default region)", 

149) 

150@pass_config 

151def instance_info(config: Any, instance_type: Any, region: Any) -> None: 

152 """Describe an instance type's compute characteristics. 

153 

154 Resolved live from EC2 DescribeInstanceTypes every time — there is no 

155 checked-in specification table, so a newly launched accelerator family is 

156 reported the day it ships. 

157 

158 Reports vCPUs/cores/threads, memory, every accelerator class (NVIDIA GPU, 

159 AWS Neuron, Inferentia, media, FPGA) with per-model breakdowns, EFA and 

160 network limits, local NVMe and EBS characteristics, placement-group support, 

161 purchase options (spot / capacity-block), and platform capabilities. 

162 

163 DescribeInstanceTypes is region-scoped: a type is only described where it is 

164 offered. If it is missing, try another region or `gco capacity 

165 recommend-region`. 

166 

167 Examples: 

168 gco capacity instance-info p5.48xlarge 

169 gco capacity instance-info trn2.48xlarge -r us-east-1 

170 gco -o json capacity instance-info p5.48xlarge 

171 gco -o json capacity instance-info g5.2xlarge | jq '.gpu_devices' 

172 """ 

173 formatter = get_output_formatter(config) 

174 checker = get_capacity_checker(config) 

175 

176 try: 

177 info = checker.get_instance_info(instance_type, region=region) 

178 if not info: 

179 target = region or config.default_region 

180 formatter.print_error( 

181 f"Could not describe instance type '{instance_type}' in {target}. " 

182 "It may not exist, or may not be offered in that region — try " 

183 "--region, or 'gco capacity recommend-region'." 

184 ) 

185 sys.exit(1) 

186 

187 if config.output_format != "table": 

188 formatter.print(info) 

189 return 

190 

191 print(f"\n {info.instance_type} ({info.region})") 

192 print(" " + "-" * 58) 

193 print(f" vCPUs: {info.vcpus}", end="") 

194 if info.cores is not None: 

195 print(f" ({info.cores} cores x {info.threads_per_core} threads)", end="") 

196 print() 

197 print(f" Memory: {info.memory_gib} GiB") 

198 print(f" Architecture: {', '.join(info.architectures) or info.architecture}") 

199 if info.processor_manufacturer: 

200 clock = ( 

201 f" @ {info.sustained_clock_speed_ghz} GHz" if info.sustained_clock_speed_ghz else "" 

202 ) 

203 print(f" Processor: {info.processor_manufacturer}{clock}") 

204 

205 if info.gpu_devices: 

206 print(f"\n GPUs: {info.gpu_count} total, {info.gpu_memory_gib} GiB total") 

207 for device in info.gpu_devices: 

208 print( 

209 f" {device.get('count')}x {device.get('manufacturer')} " 

210 f"{device.get('name')} @ {device.get('memory_gib')} GiB each" 

211 ) 

212 if info.neuron_devices: 

213 print( 

214 f"\n Neuron: {info.neuron_count} devices, " 

215 f"{info.neuron_memory_gib} GiB total" 

216 ) 

217 for device in info.neuron_devices: 

218 print( 

219 f" {device.get('count')}x {device.get('name')} " 

220 f"({device.get('core_count')} cores v{device.get('core_version')})" 

221 ) 

222 if info.inference_accelerators: 

223 print(f"\n Inferentia: {info.inference_accelerator_count} accelerators") 

224 

225 print("\n NETWORK") 

226 print(f" EFA: {'yes' if info.efa_supported else 'no'}", end="") 

227 if info.efa_max_interfaces: 

228 print(f" (max {info.efa_max_interfaces} interfaces)", end="") 

229 print() 

230 print(f" Performance: {info.network_performance}") 

231 print(f" Max ENIs: {info.maximum_network_interfaces}") 

232 if info.maximum_network_cards: 

233 print(f" Network cards: {info.maximum_network_cards}") 

234 

235 print("\n STORAGE") 

236 if info.instance_storage_total_gb: 

237 disks = ", ".join( 

238 f"{d.get('count')}x {d.get('size_gb')} GB {d.get('type')}" 

239 for d in info.instance_storage_disks 

240 ) 

241 print(f" Local: {info.instance_storage_total_gb} GB ({disks})") 

242 else: 

243 print(" Local: none (EBS only)") 

244 print(f" EBS optimized: {info.ebs_optimized_support}") 

245 if info.ebs_maximum_iops: 

246 print( 

247 f" EBS max: {info.ebs_maximum_iops} IOPS, " 

248 f"{info.ebs_maximum_throughput_mbps} MB/s" 

249 ) 

250 

251 print("\n PURCHASING") 

252 print(f" Usage classes: {', '.join(info.supported_usage_classes)}") 

253 print(f" Capacity blocks: {'yes' if info.capacity_block_supported else 'no'}") 

254 if info.supported_placement_strategies: 

255 print(f" Placement: {', '.join(info.supported_placement_strategies)}") 

256 print(f" Current gen: {'yes' if info.current_generation else 'no'}") 

257 print() 

258 

259 except Exception as e: 

260 formatter.print_error(f"Failed to get instance info: {e}") 

261 sys.exit(1) 

262 

263 

264@capacity.command("status") 

265@click.option("--region", "-r", help="Specific region to check") 

266@click.option("--all-regions", "-a", is_flag=True, default=True, help="Check all regions (default)") 

267@pass_config 

268def capacity_status(config: Any, region: Any, all_regions: Any) -> None: 

269 """Show comprehensive resource utilization across regions. 

270 

271 Displays pending/running workloads, GPU/CPU utilization, queue depth, 

272 and active job counts for one or all GCO clusters. 

273 

274 Examples: 

275 gco capacity status 

276 gco capacity status --region us-east-1 

277 gco capacity status --all-regions 

278 """ 

279 from ..capacity import get_multi_region_capacity_checker 

280 

281 formatter = get_output_formatter(config) 

282 

283 try: 

284 checker = get_multi_region_capacity_checker(config) 

285 

286 if region: 

287 capacity = checker.get_region_capacity(region) 

288 formatter.print(capacity) 

289 else: 

290 capacities = checker.get_all_regions_capacity() 

291 

292 if not capacities: 

293 formatter.print_warning("No GCO stacks found") 

294 if config.output_format != "table": 

295 formatter.print(capacities) 

296 return 

297 

298 if config.output_format == "table": 

299 print("\n REGION QUEUE RUNNING GPU% CPU% SCORE") 

300 print(" " + "-" * 55) 

301 for c in sorted(capacities, key=lambda x: x.recommendation_score): 

302 print( 

303 f" {c.region:<15} {c.queue_depth:>5} {c.running_jobs:>7} " 

304 f"{c.gpu_utilization:>4.0f}% {c.cpu_utilization:>4.0f}% " 

305 f"{c.recommendation_score:>5.0f}" 

306 ) 

307 

308 print() 

309 best = min(capacities, key=lambda x: x.recommendation_score) 

310 formatter.print_info(f"Recommended region: {best.region} (lowest score = best)") 

311 else: 

312 formatter.print(capacities) 

313 

314 except Exception as e: 

315 formatter.print_error(f"Failed to get capacity status: {e}") 

316 sys.exit(1) 

317 

318 

319@capacity.command("recommend-region") 

320@click.option("--gpu", is_flag=True, help="Job requires GPUs") 

321@click.option("--min-gpus", default=0, help="Minimum GPUs required") 

322@click.option( 

323 "--instance-type", "-i", default=None, help="Specific instance type for workload-aware scoring" 

324) 

325@click.option("--gpu-count", default=0, help="Number of GPUs required") 

326@pass_config 

327def recommend_region( 

328 config: Any, gpu: Any, min_gpus: Any, instance_type: Any, gpu_count: Any 

329) -> None: 

330 """Recommend optimal region for job placement. 

331 

332 Analyzes capacity across all deployed EKS regions and recommends 

333 the best region. When --instance-type is provided, uses weighted 

334 multi-signal scoring that factors in spot placement scores, pricing, 

335 queue depth, GPU utilization, and running job counts. 

336 

337 Without --instance-type, uses a simpler composite score based on 

338 queue depth, GPU utilization, and running jobs. 

339 

340 Examples: 

341 gco capacity recommend-region 

342 gco capacity recommend-region --gpu 

343 gco capacity recommend-region -i g5.xlarge 

344 gco capacity recommend-region -i p4d.24xlarge --gpu-count 8 

345 """ 

346 from ..capacity import get_multi_region_capacity_checker 

347 

348 formatter = get_output_formatter(config) 

349 

350 try: 

351 checker = get_multi_region_capacity_checker(config) 

352 recommendation = checker.recommend_region_for_job( 

353 gpu_required=gpu, 

354 min_gpus=min_gpus, 

355 instance_type=instance_type, 

356 gpu_count=gpu_count, 

357 ) 

358 

359 if config.output_format == "table": 

360 formatter.print_success(f"Recommended region: {recommendation['region']}") 

361 formatter.print_info(f"Reason: {recommendation['reason']}") 

362 

363 if config.verbose: 

364 print("\nAll regions ranked:") 

365 for r in recommendation.get("all_regions", []): 

366 print( 

367 f" {r['region']}: score={r['score']:.4f}, " 

368 f"queue={r['queue_depth']}, gpu={r['gpu_utilization']:.0f}%" 

369 ) 

370 else: 

371 formatter.print(recommendation) 

372 

373 except Exception as e: 

374 formatter.print_error(f"Failed to get recommendation: {e}") 

375 sys.exit(1) 

376 

377 

378@capacity.command("ai-recommend") 

379@click.option("--workload", "-w", help="Description of your workload") 

380@click.option( 

381 "--instance-type", 

382 "-i", 

383 multiple=True, 

384 help="Instance types to consider (can specify multiple)", 

385) 

386@click.option("--region", "-r", multiple=True, help="Regions to consider (can specify multiple)") 

387@click.option("--gpu", is_flag=True, help="Workload requires GPUs") 

388@click.option("--min-gpus", default=0, help="Minimum GPUs required") 

389@click.option("--min-memory-gb", default=0, help="Minimum memory in GB") 

390@click.option( 

391 "--fault-tolerance", 

392 "-f", 

393 type=click.Choice(["high", "medium", "low"]), 

394 default="medium", 

395 help="Fault tolerance level", 

396) 

397@click.option("--max-cost", type=float, help="Maximum cost per hour in USD") 

398@click.option( 

399 "--model", 

400 "-m", 

401 default=None, 

402 help=( 

403 "Bedrock model ID to use " 

404 "(default: cdk.json context.bedrock.capacity_advisor_default_model_id)." 

405 ), 

406) 

407@click.option("--raw", is_flag=True, help="Show raw AI response") 

408@pass_config 

409def ai_recommend( 

410 config: Any, 

411 workload: Any, 

412 instance_type: Any, 

413 region: Any, 

414 gpu: Any, 

415 min_gpus: Any, 

416 min_memory_gb: Any, 

417 fault_tolerance: Any, 

418 max_cost: Any, 

419 model: Any, 

420 raw: Any, 

421) -> None: 

422 """Get AI-powered capacity recommendation using Amazon Bedrock. 

423 

424 This command gathers comprehensive capacity data including: 

425 - Spot placement scores, pricing, and 7-day per-AZ price trends across regions 

426 - On-demand availability and pricing 

427 - Capacity Reservations (ODCRs), Capacity Block offerings, and 26-week 

428 block-availability trends 

429 - Current cluster utilization (queue depth, GPU/CPU usage) 

430 - Running and pending job counts 

431 - The algorithmic multi-signal region ranking as advisory context 

432 

433 Without --instance-type, one representative type per current GPU 

434 generation is scanned (T4, L4, A10G, L40S, RTX PRO 4500/6000 Blackwell, 

435 A100, H100, H200, B200, B300). 

436 

437 The data is analyzed by an LLM to provide intelligent recommendations 

438 for where to place your workload. 

439 

440 ⚠️ DISCLAIMER: Recommendations are AI-generated and should be validated 

441 before making production decisions. Capacity availability and pricing 

442 can change rapidly. 

443 

444 REQUIREMENTS: 

445 - AWS credentials with bedrock:InvokeModel permission 

446 - The specified Bedrock model must be enabled in your account 

447 - Default model: cdk.json context.bedrock.capacity_advisor_default_model_id 

448 

449 Examples: 

450 gco capacity ai-recommend --workload "Training a large language model" 

451 

452 gco capacity ai-recommend -w "Inference workload" --gpu --min-gpus 4 

453 

454 gco capacity ai-recommend -i g5.xlarge -i g5.2xlarge -r us-east-1 -r us-west-2 

455 

456 gco capacity ai-recommend --fault-tolerance high --max-cost 5.00 

457 """ 

458 from ..capacity import get_bedrock_capacity_advisor 

459 

460 formatter = get_output_formatter(config) 

461 

462 # Print disclaimer 

463 print() 

464 print(" " + "=" * 70) 

465 print(" ⚠️ AI-POWERED RECOMMENDATION DISCLAIMER") 

466 print(" " + "-" * 70) 

467 print(" This recommendation is generated by an AI model and should be") 

468 print(" validated before making production decisions.") 

469 print(" ") 

470 print(" • Capacity availability can change rapidly") 

471 print(" • Spot instances may be interrupted at any time") 

472 print(" • Pricing data may not reflect real-time prices") 

473 print(" • AI recommendations are not guaranteed to be optimal") 

474 print(" " + "=" * 70) 

475 print() 

476 

477 try: 

478 formatter.print_info("Gathering capacity data across regions...") 

479 

480 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

481 

482 # Build requirements dict 

483 requirements = { 

484 "gpu_required": gpu, 

485 "min_gpus": min_gpus if min_gpus > 0 else None, 

486 "min_memory_gb": min_memory_gb if min_memory_gb > 0 else None, 

487 "fault_tolerance": fault_tolerance, 

488 "max_cost_per_hour": max_cost, 

489 } 

490 # Remove None values 

491 requirements = {k: v for k, v in requirements.items() if v is not None} 

492 

493 formatter.print_info(f"Analyzing with {advisor.model_id}...") 

494 

495 recommendation = advisor.get_recommendation( 

496 workload_description=workload, 

497 instance_types=list(instance_type) if instance_type else None, 

498 regions=list(region) if region else None, 

499 requirements=requirements if requirements else None, 

500 ) 

501 

502 # Display recommendation 

503 print() 

504 print(" " + "=" * 70) 

505 print(" 🤖 AI RECOMMENDATION") 

506 print(" " + "=" * 70) 

507 print() 

508 print(f" Region: {recommendation.recommended_region}") 

509 print(f" Instance Type: {recommendation.recommended_instance_type}") 

510 print(f" Capacity Type: {recommendation.recommended_capacity_type.upper()}") 

511 print(f" Confidence: {recommendation.confidence.upper()}") 

512 if recommendation.cost_estimate: 

513 print(f" Est. Cost: {recommendation.cost_estimate}") 

514 print() 

515 print(" REASONING:") 

516 print(" " + "-" * 68) 

517 # Word wrap the reasoning 

518 reasoning_lines = recommendation.reasoning.split(". ") 

519 for line in reasoning_lines: 

520 if line.strip(): 

521 print(f" {line.strip()}.") 

522 print() 

523 

524 # Show alternatives 

525 if recommendation.alternative_options: 

526 print(" ALTERNATIVE OPTIONS:") 

527 print(" " + "-" * 68) 

528 for i, alt in enumerate(recommendation.alternative_options[:3], 1): 

529 print( 

530 f" {i}. {alt.get('region', 'N/A')} / " 

531 f"{alt.get('instance_type', 'N/A')} / " 

532 f"{alt.get('capacity_type', 'N/A').upper()}" 

533 ) 

534 if alt.get("reason"): 

535 print(f" {alt['reason']}") 

536 print() 

537 

538 # Show warnings 

539 if recommendation.warnings: 

540 print(" ⚠️ WARNINGS:") 

541 print(" " + "-" * 68) 

542 for warning in recommendation.warnings: 

543 print(f" • {warning}") 

544 print() 

545 

546 # Show raw response if requested 

547 if raw: 

548 print(" RAW AI RESPONSE:") 

549 print(" " + "-" * 68) 

550 print(recommendation.raw_response) 

551 print() 

552 

553 print(" " + "=" * 70) 

554 print() 

555 

556 except Exception as e: 

557 if is_bedrock_ftu_form_error(e): 

558 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

559 sys.exit(1) 

560 # Advisor errors are already fully worded (and may carry their own 

561 # remediation); re-prefixing here used to print the same phrase twice. 

562 formatter.print_error(str(e)) 

563 sys.exit(1) 

564 

565 

566@capacity.command("reservations") 

567@click.option("--instance-type", "-i", help="Filter by instance type") 

568@click.option("--region", "-r", help="Specific region (default: all deployed regions)") 

569@pass_config 

570def list_reservations(config: Any, instance_type: Any, region: Any) -> None: 

571 """List On-Demand Capacity Reservations (ODCRs) across regions. 

572 

573 Shows all active capacity reservations with utilization details. 

574 

575 Examples: 

576 gco capacity reservations 

577 gco capacity reservations -i p5.48xlarge 

578 gco capacity reservations -r us-east-1 

579 """ 

580 formatter = get_output_formatter(config) 

581 checker = get_capacity_checker(config) 

582 

583 try: 

584 if region: 

585 reservations = checker.list_capacity_reservations(region, instance_type=instance_type) 

586 result = { 

587 "regions_checked": [region], 

588 "total_reservations": len(reservations), 

589 "total_reserved_instances": sum(r["total_instances"] for r in reservations), 

590 "total_available_instances": sum(r["available_instances"] for r in reservations), 

591 "reservations": reservations, 

592 } 

593 else: 

594 result = checker.list_all_reservations(instance_type=instance_type) 

595 

596 if config.output_format != "table": 

597 formatter.print(result) 

598 return 

599 

600 reservations = result["reservations"] 

601 if not reservations: 

602 formatter.print_info("No active capacity reservations found") 

603 return 

604 

605 print(f"\n Capacity Reservations ({len(reservations)} found)") 

606 print(" " + "-" * 90) 

607 print( 

608 f" {'INSTANCE TYPE':<18} {'REGION':<15} {'AZ':<18} " 

609 f"{'TOTAL':>5} {'AVAIL':>5} {'USED%':>6} {'MATCH CRITERIA'}" 

610 ) 

611 print(" " + "-" * 90) 

612 for r in reservations: 

613 print( 

614 f" {r['instance_type']:<18} {r['region']:<15} " 

615 f"{r['availability_zone']:<18} {r['total_instances']:>5} " 

616 f"{r['available_instances']:>5} {r['utilization_pct']:>5.1f}% " 

617 f"{r.get('instance_match_criteria', 'open')}" 

618 ) 

619 

620 print() 

621 print( 

622 f" Total: {result['total_reserved_instances']} reserved, " 

623 f"{result['total_available_instances']} available" 

624 ) 

625 print() 

626 

627 except Exception as e: 

628 formatter.print_error(f"Failed to list reservations: {e}") 

629 sys.exit(1) 

630 

631 

632@capacity.command("reservation-check") 

633@click.option("--instance-type", "-i", required=True, help="Instance type to check") 

634@click.option( 

635 "--region", 

636 "-r", 

637 "regions", 

638 multiple=True, 

639 help="Region(s) to check; repeatable (default: all deployed regions)", 

640) 

641@click.option("--count", "-c", default=1, help="Minimum instances needed") 

642@click.option( 

643 "--include-blocks/--no-blocks", 

644 default=True, 

645 help="Include Capacity Block offerings (default: yes)", 

646) 

647@click.option( 

648 "--block-duration", 

649 default=24, 

650 type=int, 

651 help="Capacity Block duration in hours (default: 24)", 

652) 

653@click.option( 

654 "--block-duration-days", 

655 default=None, 

656 type=int, 

657 help="Capacity Block duration in days (overrides --block-duration)", 

658) 

659@click.option( 

660 "--earliest-start", 

661 default=None, 

662 help="Earliest block start date (YYYY-MM-DD or ISO datetime)", 

663) 

664@click.option( 

665 "--latest-start", 

666 default=None, 

667 help="Latest block start date (YYYY-MM-DD or ISO datetime)", 

668) 

669@pass_config 

670def reservation_check( 

671 config: Any, 

672 instance_type: Any, 

673 regions: Any, 

674 count: Any, 

675 include_blocks: Any, 

676 block_duration: Any, 

677 block_duration_days: Any, 

678 earliest_start: Any, 

679 latest_start: Any, 

680) -> None: 

681 """Check reservation availability and Capacity Block offerings. 

682 

683 Checks both existing ODCRs and purchasable Capacity Blocks for ML 

684 workloads. Capacity Blocks provide guaranteed GPU capacity for a 

685 fixed duration at a known price. Pass --region more than once to check 

686 several regions in parallel, and use --earliest-start/--latest-start to 

687 bound when the block may begin. For a full duration-range sweep across 

688 many regions, use 'gco capacity find-blocks'. 

689 

690 Examples: 

691 gco capacity reservation-check -i p5.48xlarge 

692 gco capacity reservation-check -i p4d.24xlarge -c 2 --block-duration 48 

693 gco capacity reservation-check -i g5.48xlarge -r us-east-1 --no-blocks 

694 gco capacity reservation-check -i p5.48xlarge -r us-east-1 -r us-west-2 \\ 

695 --block-duration-days 14 --earliest-start 2026-07-01 

696 """ 

697 formatter = get_output_formatter(config) 

698 checker = get_capacity_checker(config) 

699 

700 try: 

701 formatter.print_info( 

702 f"Checking reservations for {instance_type} " 

703 f"(min {count} instance{'s' if count > 1 else ''})..." 

704 ) 

705 

706 result = checker.check_reservation_availability( 

707 instance_type=instance_type, 

708 regions=list(regions) or None, 

709 min_count=count, 

710 include_capacity_blocks=include_blocks, 

711 block_duration_hours=block_duration, 

712 block_duration_days=block_duration_days, 

713 earliest_start=earliest_start, 

714 latest_start=latest_start, 

715 ) 

716 

717 if config.output_format != "table": 

718 formatter.print(result) 

719 return 

720 

721 # ODCR section 

722 odcr = result["odcr"] 

723 print(f"\n On-Demand Capacity Reservations for {instance_type}") 

724 print(" " + "-" * 60) 

725 if odcr["reservations"]: 

726 for r in odcr["reservations"]: 

727 print( 

728 f" ✓ {r['availability_zone']}: " 

729 f"{r['available_instances']}/{r['total_instances']} available " 

730 f"({r['reservation_id']})" 

731 ) 

732 print( 

733 f"\n Total: {odcr['total_available_instances']} available " 

734 f"of {odcr['total_reserved_instances']} reserved" 

735 ) 

736 else: 

737 print(" No active ODCRs found for this instance type") 

738 

739 # Capacity Blocks section 

740 if include_blocks: 

741 block_section = result["capacity_blocks"] 

742 duration = block_section.get("duration_hours", block_duration) 

743 print(f"\n Capacity Block Offerings ({duration}h)") 

744 print(" " + "-" * 60) 

745 if block_section["offerings"]: 

746 for b in block_section["offerings"]: 

747 gpu_hr = b.get("price_per_gpu_hour") 

748 gpu_hr_str = f" (${gpu_hr}/GPU-hr)" if gpu_hr is not None else "" 

749 start = (b.get("start_date") or "")[:16] 

750 print( 

751 f" ✓ {b['availability_zone']}: " 

752 f"{b['instance_count']}x {b['duration_hours']}h " 

753 f"starting {start} — ${b['upfront_fee']}{gpu_hr_str}" 

754 ) 

755 else: 

756 print(" No Capacity Block offerings available") 

757 

758 # Recommendation 

759 print() 

760 print(f" 💡 {result['recommendation']}") 

761 print() 

762 

763 except Exception as e: 

764 formatter.print_error(f"Failed to check reservations: {e}") 

765 sys.exit(1) 

766 

767 

768def _print_find_blocks_report(result: dict[str, Any]) -> None: 

769 """Render a consolidated find-blocks report as a readable table block.""" 

770 itype = result["instance_type"] 

771 if result.get("requested_instance_type") and result["requested_instance_type"] != itype: 

772 print(f"\n Capacity Block search for {result['requested_instance_type']} -> {itype}") 

773 else: 

774 print(f"\n Capacity Block search for {itype}") 

775 print(" " + "-" * 72) 

776 

777 window = result.get("date_window", {}) 

778 earliest = (window.get("earliest_start") or "any")[:16] 

779 latest = (window.get("latest_start") or "any")[:16] 

780 days = result.get("durations_probed_days") or [] 

781 if days: 

782 span = f"{min(days):g}-{max(days):g}d" if len(days) > 1 else f"{days[0]:g}d" 

783 else: 

784 span = "n/a" 

785 print(f" Regions: {', '.join(result.get('regions_checked', []))}") 

786 print(f" Durations probed: {span} Start window: {earliest} .. {latest}") 

787 

788 if not result.get("valid_instance_type", True): 

789 print() 

790 print(f" ⚠ {result.get('recommendation', 'Invalid instance type.')}") 

791 print() 

792 return 

793 

794 offerings = result.get("offerings", []) 

795 if not offerings: 

796 print() 

797 print(f" {result.get('recommendation', 'No offerings found.')}") 

798 print() 

799 return 

800 

801 print() 

802 print(f" {'REGION':<13} {'AZ':<17} {'START':<17} {'DUR':>6} {'UPFRONT':>11} {'$/GPU-hr':>10}") 

803 print(" " + "-" * 72) 

804 for b in offerings: 

805 start = (b.get("start_date") or "")[:16] 

806 dur = f"{b.get('duration_days') or '?'}d" 

807 fee = b.get("upfront_fee_usd") 

808 fee_str = f"${fee:,.0f}" if isinstance(fee, int | float) else "?" 

809 gpu_hr = b.get("price_per_gpu_hour") 

810 gpu_hr_str = f"${gpu_hr:,.2f}" if isinstance(gpu_hr, int | float) else "-" 

811 print( 

812 f" {b.get('region') or ''!s:<13} {b.get('availability_zone') or ''!s:<17} " 

813 f"{start:<17} {dur:>6} {fee_str:>11} {gpu_hr_str:>10}" 

814 ) 

815 print() 

816 print(f" 💡 {result['recommendation']}") 

817 print() 

818 

819 

820@capacity.command("find-blocks") 

821@click.option( 

822 "--instance-type", "-i", required=True, help="GPU instance type or alias (e.g. p6-b200)" 

823) 

824@click.option( 

825 "--region", 

826 "-r", 

827 "regions", 

828 multiple=True, 

829 help="Region(s) to search; repeatable (default: all deployed regions)", 

830) 

831@click.option("--count", "-c", default=1, help="Instances per block") 

832@click.option("--duration-days", default=None, type=int, help="Single target duration in days") 

833@click.option("--duration-hours", default=None, type=int, help="Single target duration in hours") 

834@click.option( 

835 "--min-duration-days", default=None, type=int, help="Minimum duration (days) for a range search" 

836) 

837@click.option( 

838 "--max-duration-days", default=None, type=int, help="Maximum duration (days) for a range search" 

839) 

840@click.option("--min-duration-hours", default=None, type=int, help="Minimum duration (hours)") 

841@click.option("--max-duration-hours", default=None, type=int, help="Maximum duration (hours)") 

842@click.option( 

843 "--earliest-start", default=None, help="Earliest block start (YYYY-MM-DD or ISO datetime)" 

844) 

845@click.option( 

846 "--latest-start", default=None, help="Latest block start (YYYY-MM-DD or ISO datetime)" 

847) 

848@click.option( 

849 "--find-longest", 

850 is_flag=True, 

851 help="Sweep the duration ladder and surface the longest available block", 

852) 

853@pass_config 

854def find_blocks( 

855 config: Any, 

856 instance_type: Any, 

857 regions: Any, 

858 count: Any, 

859 duration_days: Any, 

860 duration_hours: Any, 

861 min_duration_days: Any, 

862 max_duration_days: Any, 

863 min_duration_hours: Any, 

864 max_duration_hours: Any, 

865 earliest_start: Any, 

866 latest_start: Any, 

867 find_longest: Any, 

868) -> None: 

869 """Find Capacity Blocks across regions, durations, and a start-date window. 

870 

871 One command sweeps every requested region and every valid Capacity Block 

872 duration in the range, in parallel, then returns a single consolidated, 

873 de-duplicated, ranked report with per-hour and per-GPU-hour pricing. 

874 

875 AWS allows Capacity Block durations in 1-day increments up to 14 days, then 

876 7-day increments up to 182 days; a duration range is expanded to those 

877 discrete values automatically. Friendly names are normalized (p6-b200 -> 

878 p6-b200.48xlarge, p6-b300 -> p6-b300.48xlarge); the Grace-Blackwell GB200/ 

879 GB300 UltraServer families (P6e-GB200/P6e-GB300) are flagged as not standalone. 

880 

881 Examples: 

882 gco capacity find-blocks -i p6-b200.48xlarge \\ 

883 -r us-east-1 -r us-east-2 -r us-west-2 -r eu-west-1 \\ 

884 --min-duration-days 1 --max-duration-days 63 \\ 

885 --earliest-start 2026-07-01 --latest-start 2026-07-10 

886 gco capacity find-blocks -i p5.48xlarge -r us-east-1 --duration-days 14 

887 gco capacity find-blocks -i p5.48xlarge -r us-east-1 --find-longest 

888 """ 

889 formatter = get_output_formatter(config) 

890 checker = get_capacity_checker(config) 

891 

892 try: 

893 result = checker.find_capacity_blocks( 

894 instance_type, 

895 regions=list(regions) or None, 

896 instance_count=count, 

897 duration_hours=duration_hours, 

898 duration_days=duration_days, 

899 min_duration_hours=min_duration_hours, 

900 min_duration_days=min_duration_days, 

901 max_duration_hours=max_duration_hours, 

902 max_duration_days=max_duration_days, 

903 earliest_start=earliest_start, 

904 latest_start=latest_start, 

905 find_longest=find_longest, 

906 ) 

907 

908 if config.output_format != "table": 

909 formatter.print(result) 

910 return 

911 

912 _print_find_blocks_report(result) 

913 

914 except Exception as e: 

915 formatter.print_error(f"Failed to find capacity blocks: {e}") 

916 sys.exit(1) 

917 

918 

919@capacity.command("reserve") 

920@click.option( 

921 "--offering-id", 

922 "-o", 

923 required=True, 

924 help="Capacity Block offering ID (cb-xxx) from reservation-check", 

925) 

926@click.option("--region", "-r", required=True, help="AWS region where the offering exists") 

927@click.option( 

928 "--dry-run", 

929 is_flag=True, 

930 help="Validate the offering without purchasing (no cost incurred)", 

931) 

932@pass_config 

933def reserve_capacity(config: Any, offering_id: Any, region: Any, dry_run: Any) -> None: 

934 """Purchase a Capacity Block offering by its ID. 

935 

936 Use 'gco capacity reservation-check' first to find available offerings 

937 and their IDs, then purchase with this command. 

938 

939 ⚠️ WARNING: This command purchases capacity and incurs charges. 

940 Use --dry-run to validate first. 

941 

942 Examples: 

943 # First, find offerings: 

944 gco capacity reservation-check -i p4d.24xlarge -r us-east-1 

945 

946 # Validate without purchasing: 

947 gco capacity reserve -o cb-0123456789abcdef0 -r us-east-1 --dry-run 

948 

949 # Purchase: 

950 gco capacity reserve -o cb-0123456789abcdef0 -r us-east-1 

951 """ 

952 formatter = get_output_formatter(config) 

953 checker = get_capacity_checker(config) 

954 

955 try: 

956 if dry_run: 

957 formatter.print_info(f"Dry run: validating offering {offering_id} in {region}...") 

958 else: 

959 formatter.print_info(f"Purchasing Capacity Block {offering_id} in {region}...") 

960 

961 result = checker.purchase_capacity_block( 

962 offering_id=offering_id, 

963 region=region, 

964 dry_run=dry_run, 

965 ) 

966 

967 if config.output_format != "table": 

968 formatter.print(result) 

969 return 

970 

971 if result["success"]: 

972 if dry_run: 

973 print() 

974 print(f" ✓ Dry run passed — offering {offering_id} is valid and purchasable") 

975 print(f" Region: {region}") 

976 print() 

977 print(" To purchase, run without --dry-run:") 

978 print(f" gco capacity reserve -o {offering_id} -r {region}") 

979 print() 

980 else: 

981 print() 

982 print(" ✓ Capacity Block purchased successfully") 

983 print(f" Reservation ID: {result['reservation_id']}") 

984 print(f" Instance Type: {result['instance_type']}") 

985 print(f" AZ: {result['availability_zone']}") 

986 print(f" Instances: {result['total_instances']}") 

987 print(f" Start: {result.get('start_date', 'N/A')}") 

988 print(f" End: {result.get('end_date', 'N/A')}") 

989 print() 

990 print(" To create a NodePool for this Capacity Block:") 

991 print( 

992 f" gco nodepools create-capacity-block -n my-pool -r {region} " 

993 f"-c {result['reservation_id']} -i {result['instance_type']}" 

994 ) 

995 print() 

996 else: 

997 formatter.print_error( 

998 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

999 ) 

1000 sys.exit(1) 

1001 

1002 except Exception as e: 

1003 formatter.print_error(f"Failed to reserve capacity: {e}") 

1004 sys.exit(1) 

1005 

1006 

1007def _print_find_reservations_report(result: dict[str, Any]) -> None: 

1008 """Render a consolidated find-reservations report as a readable table block.""" 

1009 itype = result.get("instance_type") or "any instance type" 

1010 req = result.get("requested_instance_type") 

1011 if req and req != itype: 

1012 print(f"\n ODCR search for {req} -> {itype}") 

1013 else: 

1014 print(f"\n ODCR search for {itype}") 

1015 print(" " + "-" * 78) 

1016 print(f" Regions: {', '.join(result.get('regions_checked', []))}") 

1017 

1018 if req and not result.get("valid_instance_type", True): 

1019 print() 

1020 print(f" ⚠ {result.get('recommendation', 'Invalid instance type.')}") 

1021 print() 

1022 return 

1023 

1024 reservations = result.get("reservations", []) 

1025 if not reservations: 

1026 print() 

1027 print(f" {result.get('recommendation', 'No reservations found.')}") 

1028 print() 

1029 return 

1030 

1031 print() 

1032 print( 

1033 f" {'INSTANCE TYPE':<18} {'REGION':<13} {'AZ':<17} " 

1034 f"{'AVAIL':>6} {'TOTAL':>6} {'$/GPU-hr':>10}" 

1035 ) 

1036 print(" " + "-" * 78) 

1037 for r in reservations: 

1038 gpu_hr = r.get("price_per_gpu_hour") 

1039 gpu_hr_str = f"${gpu_hr:,.2f}" if isinstance(gpu_hr, int | float) else "-" 

1040 print( 

1041 f" {r.get('instance_type') or ''!s:<18} {r.get('region') or ''!s:<13} " 

1042 f"{r.get('availability_zone') or ''!s:<17} " 

1043 f"{r.get('available_instances', 0):>6} {r.get('total_instances', 0):>6} " 

1044 f"{gpu_hr_str:>10}" 

1045 ) 

1046 print() 

1047 print(f" 💡 {result['recommendation']}") 

1048 print() 

1049 

1050 

1051@capacity.command("find-reservations") 

1052@click.option( 

1053 "--instance-type", 

1054 "-i", 

1055 default=None, 

1056 help="Instance type or alias to filter by (e.g. p6-b200); omit for all types", 

1057) 

1058@click.option( 

1059 "--region", 

1060 "-r", 

1061 "regions", 

1062 multiple=True, 

1063 help="Region(s) to search; repeatable (default: all deployed regions)", 

1064) 

1065@click.option( 

1066 "--count", 

1067 "-c", 

1068 default=1, 

1069 help="Minimum available instances to consider the search satisfied", 

1070) 

1071@click.option( 

1072 "--state", 

1073 default="active", 

1074 help="Reservation state filter (default: active; use 'all' for any state)", 

1075) 

1076@click.option( 

1077 "--pricing/--no-pricing", 

1078 default=True, 

1079 help="Enrich reservations with On-Demand pricing (default: yes)", 

1080) 

1081@pass_config 

1082def find_reservations( 

1083 config: Any, 

1084 instance_type: Any, 

1085 regions: Any, 

1086 count: Any, 

1087 state: Any, 

1088 pricing: Any, 

1089) -> None: 

1090 """Find existing ODCRs across regions in one parallel, ranked report. 

1091 

1092 The ODCR counterpart to 'gco capacity find-blocks': it searches every 

1093 requested region in parallel, normalizes friendly instance-type aliases 

1094 (p6-b200 -> p6-b200.48xlarge), enriches each reservation with On-Demand 

1095 pricing, and ranks them most-available-first (then cheapest per-GPU-hour). 

1096 

1097 Examples: 

1098 gco capacity find-reservations -i p5.48xlarge 

1099 gco capacity find-reservations -i p6-b200 -r us-east-1 -r us-west-2 

1100 gco capacity find-reservations --no-pricing 

1101 """ 

1102 formatter = get_output_formatter(config) 

1103 checker = get_capacity_checker(config) 

1104 

1105 try: 

1106 result = checker.find_capacity_reservations( 

1107 instance_type=instance_type, 

1108 regions=list(regions) or None, 

1109 min_count=count, 

1110 state=None if str(state).lower() == "all" else state, 

1111 include_pricing=pricing, 

1112 ) 

1113 

1114 if config.output_format != "table": 

1115 formatter.print(result) 

1116 return 

1117 

1118 _print_find_reservations_report(result) 

1119 

1120 except Exception as e: 

1121 formatter.print_error(f"Failed to find reservations: {e}") 

1122 sys.exit(1) 

1123 

1124 

1125@capacity.command("create-reservation") 

1126@click.option("--instance-type", "-i", required=True, help="EC2 instance type or alias") 

1127@click.option("--region", "-r", required=True, help="AWS region") 

1128@click.option( 

1129 "--availability-zone", "-z", required=True, help="Target Availability Zone (e.g. us-east-1a)" 

1130) 

1131@click.option("--count", "-c", default=1, help="Number of instances to reserve") 

1132@click.option("--platform", default="Linux/UNIX", help="Instance platform/OS (default: Linux/UNIX)") 

1133@click.option( 

1134 "--tenancy", 

1135 type=click.Choice(["default", "dedicated"]), 

1136 default="default", 

1137 help="Reservation tenancy (default: default)", 

1138) 

1139@click.option( 

1140 "--match-criteria", 

1141 type=click.Choice(["open", "targeted"]), 

1142 default="open", 

1143 help="Instance match criteria (default: open)", 

1144) 

1145@click.option( 

1146 "--end-date", 

1147 default=None, 

1148 help="Optional end date (YYYY-MM-DD or ISO datetime); omit for an unlimited reservation", 

1149) 

1150@click.option("--ebs-optimized", is_flag=True, help="Reserve EBS-optimized capacity") 

1151@click.option( 

1152 "--dry-run", 

1153 is_flag=True, 

1154 help="Validate the request without creating (no cost incurred)", 

1155) 

1156@pass_config 

1157def create_reservation( 

1158 config: Any, 

1159 instance_type: Any, 

1160 region: Any, 

1161 availability_zone: Any, 

1162 count: Any, 

1163 platform: Any, 

1164 tenancy: Any, 

1165 match_criteria: Any, 

1166 end_date: Any, 

1167 ebs_optimized: Any, 

1168 dry_run: Any, 

1169) -> None: 

1170 """Create a new On-Demand Capacity Reservation (ODCR). 

1171 

1172 The ODCR counterpart to 'gco capacity reserve'. Reserves On-Demand capacity 

1173 for an instance type in a specific AZ. 

1174 

1175 ⚠️ WARNING: creating a reservation incurs On-Demand charges for the reserved 

1176 capacity whether or not it is used, until the reservation is cancelled. 

1177 Use --dry-run to validate first. 

1178 

1179 Examples: 

1180 gco capacity create-reservation -i p5.48xlarge -r us-east-1 -z us-east-1a -c 2 --dry-run 

1181 gco capacity create-reservation -i p6-b200 -r us-east-1 -z us-east-1a -c 1 

1182 gco capacity create-reservation -i p4d.24xlarge -r us-west-2 -z us-west-2b \\ 

1183 --end-date 2026-08-01 

1184 """ 

1185 formatter = get_output_formatter(config) 

1186 checker = get_capacity_checker(config) 

1187 

1188 try: 

1189 if dry_run: 

1190 formatter.print_info( 

1191 f"Dry run: validating reservation for {count}x {instance_type} " 

1192 f"in {availability_zone}..." 

1193 ) 

1194 else: 

1195 formatter.print_info( 

1196 f"Creating reservation for {count}x {instance_type} in {availability_zone}..." 

1197 ) 

1198 

1199 result = checker.create_capacity_reservation( 

1200 instance_type=instance_type, 

1201 region=region, 

1202 availability_zone=availability_zone, 

1203 instance_count=count, 

1204 instance_platform=platform, 

1205 tenancy=tenancy, 

1206 instance_match_criteria=match_criteria, 

1207 end_date=end_date, 

1208 ebs_optimized=ebs_optimized, 

1209 dry_run=dry_run, 

1210 ) 

1211 

1212 if config.output_format != "table": 

1213 formatter.print(result) 

1214 return 

1215 

1216 if result["success"]: 

1217 if dry_run: 

1218 print() 

1219 print(" ✓ Dry run passed — reservation parameters are valid") 

1220 print(f" Instance Type: {result.get('instance_type')}") 

1221 print(f" AZ: {result.get('availability_zone')}") 

1222 print(f" Instances: {result.get('instance_count')}") 

1223 print() 

1224 print(" To create, run without --dry-run:") 

1225 print( 

1226 f" gco capacity create-reservation -i {result.get('instance_type')} " 

1227 f"-r {region} -z {availability_zone} -c {count}" 

1228 ) 

1229 print() 

1230 else: 

1231 print() 

1232 print(" ✓ Capacity Reservation created successfully") 

1233 print(f" Reservation ID: {result['reservation_id']}") 

1234 print(f" Instance Type: {result['instance_type']}") 

1235 print(f" AZ: {result['availability_zone']}") 

1236 print(f" Instances: {result['total_instances']}") 

1237 print(f" State: {result.get('state', 'N/A')}") 

1238 print(f" End: {result.get('end_date') or 'unlimited'}") 

1239 print() 

1240 print(" To create a NodePool for this reservation:") 

1241 print( 

1242 f" gco nodepools create-odcr -n my-pool -r {region} " 

1243 f"-c {result['reservation_id']} -i {result['instance_type']}" 

1244 ) 

1245 print() 

1246 else: 

1247 formatter.print_error( 

1248 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

1249 ) 

1250 sys.exit(1) 

1251 

1252 except Exception as e: 

1253 formatter.print_error(f"Failed to create reservation: {e}") 

1254 sys.exit(1) 

1255 

1256 

1257@capacity.command("cancel-reservation") 

1258@click.option( 

1259 "--reservation-id", "-o", required=True, help="Capacity Reservation ID (cr-xxx) to cancel" 

1260) 

1261@click.option("--region", "-r", required=True, help="AWS region where the reservation exists") 

1262@click.option( 

1263 "--dry-run", is_flag=True, help="Validate the cancellation without cancelling (no change)" 

1264) 

1265@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") 

1266@pass_config 

1267def cancel_reservation( 

1268 config: Any, reservation_id: Any, region: Any, dry_run: Any, yes: Any 

1269) -> None: 

1270 """Cancel an On-Demand Capacity Reservation, releasing its capacity. 

1271 

1272 Stops On-Demand charges for the reserved capacity. Only ODCRs can be 

1273 cancelled; a Capacity Block runs for its fixed term. Instances already 

1274 running against the reservation are not terminated — they revert to normal 

1275 On-Demand billing. 

1276 

1277 Examples: 

1278 gco capacity cancel-reservation -o cr-0123456789abcdef0 -r us-east-1 --dry-run 

1279 gco capacity cancel-reservation -o cr-0123456789abcdef0 -r us-east-1 -y 

1280 """ 

1281 formatter = get_output_formatter(config) 

1282 checker = get_capacity_checker(config) 

1283 

1284 if not dry_run and not yes: 

1285 confirm(f"Cancel capacity reservation '{reservation_id}' in {region}?", abort=True) 

1286 

1287 try: 

1288 if dry_run: 

1289 formatter.print_info(f"Dry run: validating cancellation of {reservation_id}...") 

1290 else: 

1291 formatter.print_info(f"Cancelling capacity reservation {reservation_id}...") 

1292 

1293 result = checker.cancel_capacity_reservation( 

1294 reservation_id=reservation_id, 

1295 region=region, 

1296 dry_run=dry_run, 

1297 ) 

1298 

1299 if config.output_format != "table": 

1300 formatter.print(result) 

1301 return 

1302 

1303 if result["success"]: 

1304 print() 

1305 if dry_run: 

1306 print(f" ✓ Dry run passed — {reservation_id} can be cancelled") 

1307 else: 

1308 print(f" ✓ {result.get('message', 'Reservation cancelled.')}") 

1309 print() 

1310 else: 

1311 formatter.print_error( 

1312 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

1313 ) 

1314 sys.exit(1) 

1315 

1316 except Exception as e: 

1317 formatter.print_error(f"Failed to cancel reservation: {e}") 

1318 sys.exit(1) 

1319 

1320 

1321_HISTORY_DISABLED_HINT = ( 

1322 "The historical capacity surface is not enabled. It is an optional add-on to " 

1323 "the global stack: set historical.enabled to true in cdk.json and run " 

1324 "'gco stacks deploy gco-global'. See lambda/capacity-poller/README.md." 

1325) 

1326 

1327 

1328def _history_disabled(exc: Exception) -> bool: 

1329 """True if exc is a 'table does not exist' error (feature not deployed).""" 

1330 return ( 

1331 isinstance(exc, ClientError) 

1332 and exc.response.get("Error", {}).get("Code") == "ResourceNotFoundException" 

1333 ) 

1334 

1335 

1336def _print_historical_enrichment(formatter: Any, instance_type: str, region: str) -> None: 

1337 """Append a historical capacity summary to ``gco capacity check`` output.""" 

1338 from ..capacity.history import get_capacity_history_store 

1339 

1340 try: 

1341 stats = get_capacity_history_store().get_statistics(instance_type, region) 

1342 except Exception as e: # supplementary to check; never fail the command 

1343 if _history_disabled(e): 

1344 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1345 else: 

1346 formatter.print_warning(f"Historical enrichment unavailable: {e}") 

1347 return 

1348 if stats["sample_count"] == 0: 

1349 formatter.print_warning( 

1350 f"No historical samples for {instance_type} in {region} yet " 

1351 "(the poller records one about every 15 minutes)." 

1352 ) 

1353 return 

1354 formatter.print_info(f"Historical context (last 7 days, {stats['sample_count']} samples):") 

1355 spot_stats = stats["metrics"].get("spot_score") 

1356 if spot_stats: 

1357 print( 

1358 f" spot_score p25/p50/p75: " 

1359 f"{spot_stats['p25']}/{spot_stats['p50']}/{spot_stats['p75']} " 

1360 f"(min {spot_stats['min']}, max {spot_stats['max']})" 

1361 ) 

1362 price_stats = stats["metrics"].get("spot_price") 

1363 if price_stats: 

1364 print( 

1365 f" spot_price p25/p50/p75: " 

1366 f"{price_stats['p25']}/{price_stats['p50']}/{price_stats['p75']}" 

1367 ) 

1368 

1369 

1370def _format_patterns_grid(patterns: dict[str, Any]) -> str: 

1371 """Render a day-of-week x hour heatmap of average scores.""" 

1372 from ..capacity.history import DAY_NAMES 

1373 

1374 grid = patterns.get("patterns", {}) 

1375 metric = patterns.get("metric", "spot_score") 

1376 lines = [f"Average {metric} by day-of-week and hour (UTC)"] 

1377 header = "Day".ljust(10) + "".join(f"{hour:>5}" for hour in range(24)) 

1378 lines.append(header) 

1379 lines.append("-" * len(header)) 

1380 for day in DAY_NAMES: 

1381 hours = grid.get(day, {}) 

1382 row = day[:9].ljust(10) 

1383 for hour in range(24): 

1384 cell = hours.get(hour) or hours.get(str(hour)) 

1385 row += f"{cell['avg']:>5.1f}" if cell else f"{'.':>5}" 

1386 lines.append(row) 

1387 best = patterns.get("best_windows", [])[:3] 

1388 if best: 

1389 lines.append("") 

1390 lines.append("Best windows:") 

1391 for window in best: 

1392 lines.append( 

1393 f"- {window['day']} {window['hour']:02d}:00 UTC " 

1394 f"avg={window['avg']} (n={window['count']})" 

1395 ) 

1396 return "\n".join(lines) 

1397 

1398 

1399@capacity.group("history") 

1400def history() -> None: 

1401 """Query the historical capacity surface (requires historical.enabled).""" 

1402 

1403 

1404@history.command("show") 

1405@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1406@click.option("--region", "-r", required=True, help="AWS region") 

1407@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1408@pass_config 

1409def history_show(config: Any, instance_type: Any, region: Any, hours: Any) -> None: 

1410 """Show the capacity time-series for an instance type in a region.""" 

1411 from ..capacity.history import get_capacity_history_store 

1412 

1413 formatter = get_output_formatter(config) 

1414 try: 

1415 trend = get_capacity_history_store().get_trend(instance_type, region, hours) 

1416 if not trend: 

1417 formatter.print_warning( 

1418 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1419 ) 

1420 return 

1421 # Columns derive from the store's exported metric registry so a new 

1422 # metric (e.g. another SPS target capacity) appears here without a 

1423 # hand-maintained literal drifting out of date. 

1424 formatter.print(trend, columns=["timestamp", *METRIC_FIELDS]) 

1425 except Exception as e: 

1426 if _history_disabled(e): 

1427 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1428 return 

1429 formatter.print_error(f"Failed to load capacity history: {e}") 

1430 sys.exit(1) 

1431 

1432 

1433@history.command("stats") 

1434@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1435@click.option("--region", "-r", required=True, help="AWS region") 

1436@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1437@pass_config 

1438def history_stats(config: Any, instance_type: Any, region: Any, hours: Any) -> None: 

1439 """Show a statistical summary (p25/p50/p75/min/max/stddev) per metric.""" 

1440 from ..capacity.history import get_capacity_history_store 

1441 

1442 formatter = get_output_formatter(config) 

1443 try: 

1444 stats = get_capacity_history_store().get_statistics(instance_type, region, hours) 

1445 if stats["sample_count"] == 0: 

1446 formatter.print_warning( 

1447 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1448 ) 

1449 return 

1450 if config.output_format == "table": 

1451 rows = [{"metric": name, **values} for name, values in stats["metrics"].items()] 

1452 print( 

1453 formatter.format( 

1454 rows, 

1455 columns=[ 

1456 "metric", 

1457 "count", 

1458 "min", 

1459 "p25", 

1460 "p50", 

1461 "p75", 

1462 "max", 

1463 "mean", 

1464 "stddev", 

1465 ], 

1466 ) 

1467 ) 

1468 else: 

1469 formatter.print(stats) 

1470 except Exception as e: 

1471 if _history_disabled(e): 

1472 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1473 return 

1474 formatter.print_error(f"Failed to compute capacity statistics: {e}") 

1475 sys.exit(1) 

1476 

1477 

1478@history.command("patterns") 

1479@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1480@click.option("--region", "-r", required=True, help="AWS region") 

1481@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1482@click.option( 

1483 "--metric", 

1484 "-m", 

1485 # Accepted values derive from the store's exported metric registry, so the 

1486 # multi-capacity SPS fields (and any future metric) are selectable without 

1487 # updating a literal here. 

1488 type=click.Choice(METRIC_FIELDS), 

1489 default="spot_score", 

1490 show_default=True, 

1491 help="Metric to aggregate into the day/hour grid", 

1492) 

1493@pass_config 

1494def history_patterns(config: Any, instance_type: Any, region: Any, hours: Any, metric: Any) -> None: 

1495 """Show a day/hour heatmap grid of a metric's averages (default spot_score).""" 

1496 from ..capacity.history import get_capacity_history_store 

1497 

1498 formatter = get_output_formatter(config) 

1499 try: 

1500 patterns = get_capacity_history_store().get_temporal_patterns( 

1501 instance_type, region, hours, metric=metric 

1502 ) 

1503 if not patterns["patterns"]: 

1504 formatter.print_warning( 

1505 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1506 ) 

1507 return 

1508 if config.output_format == "table": 

1509 print(_format_patterns_grid(patterns)) 

1510 else: 

1511 formatter.print(patterns) 

1512 except Exception as e: 

1513 if _history_disabled(e): 

1514 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1515 return 

1516 formatter.print_error(f"Failed to compute capacity patterns: {e}") 

1517 sys.exit(1) 

1518 

1519 

1520def _prediction_to_dict(prediction: Any) -> dict[str, Any]: 

1521 """Serialize a CapacityPredictionResult for non-table output.""" 

1522 return { 

1523 "instance_type": prediction.instance_type, 

1524 "region": prediction.region, 

1525 "confidence": prediction.confidence, 

1526 "best_windows": prediction.best_windows, 

1527 "avoid_windows": prediction.avoid_windows, 

1528 "reasoning": prediction.reasoning, 

1529 } 

1530 

1531 

1532def _print_prediction(prediction: Any, raw: bool) -> None: 

1533 """Render a single capacity-window prediction as a table block.""" 

1534 print() 

1535 print( 

1536 f" Best time to acquire {prediction.instance_type} in {prediction.region} " 

1537 f"(confidence: {prediction.confidence.upper()})" 

1538 ) 

1539 print(" " + "-" * 68) 

1540 if prediction.best_windows: 

1541 for window in prediction.best_windows[:5]: 

1542 print( 

1543 f" + {window.get('day', '?')} {window.get('hour_range', '?')}: " 

1544 f"{window.get('why', '')}" 

1545 ) 

1546 else: 

1547 print(" (no clear best window identified)") 

1548 if prediction.avoid_windows: 

1549 print() 

1550 print(" Windows to avoid:") 

1551 for window in prediction.avoid_windows[:5]: 

1552 print( 

1553 f" - {window.get('day', '?')} {window.get('hour_range', '?')}: " 

1554 f"{window.get('why', '')}" 

1555 ) 

1556 if prediction.reasoning: 

1557 print() 

1558 print(" Reasoning:") 

1559 for line in prediction.reasoning.split(". "): 

1560 if line.strip(): 

1561 print(f" {line.strip()}") 

1562 if raw: 

1563 print() 

1564 print(prediction.raw_response) 

1565 

1566 

1567@capacity.command("predict") 

1568@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1569@click.option("--region", "-r", help="AWS region (omit when using --all-regions)") 

1570@click.option( 

1571 "--all-regions", 

1572 "-a", 

1573 is_flag=True, 

1574 help="Predict across every region that has historical data for the instance type", 

1575) 

1576@click.option( 

1577 "--hours", "-H", default=168, help="Hours of history to analyze (default 168 = 7 days)" 

1578) 

1579@click.option( 

1580 "--model", 

1581 "-m", 

1582 default=None, 

1583 help=( 

1584 "Bedrock model ID to use " 

1585 "(default: cdk.json context.bedrock.capacity_advisor_default_model_id)." 

1586 ), 

1587) 

1588@click.option("--raw", is_flag=True, help="Show the raw AI response") 

1589@pass_config 

1590def predict_capacity( 

1591 config: Any, 

1592 instance_type: Any, 

1593 region: Any, 

1594 all_regions: Any, 

1595 hours: Any, 

1596 model: Any, 

1597 raw: Any, 

1598) -> None: 

1599 """Predict the best time to acquire capacity from historical patterns (Bedrock). 

1600 

1601 Combines the historical capacity surface (an optional add-on to the global 

1602 stack) with Amazon Bedrock to recommend the day/hour windows with the best 

1603 spot availability and pricing. Requires historical.enabled and collected 

1604 samples. Pass --all-regions to run the prediction for every region that has 

1605 data for the instance type instead of a single --region. 

1606 """ 

1607 from ..capacity import get_bedrock_capacity_advisor 

1608 

1609 formatter = get_output_formatter(config) 

1610 if all_regions and region: 

1611 formatter.print_error("Pass either --region or --all-regions, not both.") 

1612 sys.exit(1) 

1613 if not all_regions and not region: 

1614 formatter.print_error("Provide --region <region> or --all-regions.") 

1615 sys.exit(1) 

1616 

1617 try: 

1618 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

1619 if all_regions: 

1620 predictions = advisor.predict_capacity_windows_all_regions( 

1621 instance_type, hours_back=hours 

1622 ) 

1623 else: 

1624 predictions = [advisor.predict_capacity_window(instance_type, region, hours_back=hours)] 

1625 except ValueError as e: 

1626 formatter.print_warning(str(e)) 

1627 return 

1628 except Exception as e: 

1629 if _history_disabled(e): 

1630 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1631 return 

1632 if is_bedrock_ftu_form_error(e): 

1633 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

1634 sys.exit(1) 

1635 formatter.print_error(f"Failed to predict capacity window: {e}") 

1636 sys.exit(1) 

1637 

1638 if not predictions: 

1639 formatter.print_warning( 

1640 f"No usable historical samples for {instance_type} in any region yet." 

1641 ) 

1642 return 

1643 

1644 if config.output_format != "table": 

1645 payload = [_prediction_to_dict(p) for p in predictions] 

1646 formatter.print(payload if all_regions else payload[0]) 

1647 return 

1648 

1649 if all_regions: 

1650 formatter.print_info( 

1651 f"Predicted acquisition windows for {instance_type} across " 

1652 f"{len(predictions)} region(s) with data:" 

1653 ) 

1654 for prediction in predictions: 

1655 _print_prediction(prediction, raw) 

1656 

1657 

1658@capacity.group("traffic-dial") 

1659@pass_config 

1660def traffic_dial(config: Any) -> None: 

1661 """Inspect and control Global Accelerator traffic dials. 

1662 

1663 Each workload region's endpoint group has a TrafficDialPercentage: the 

1664 share of traffic Global Accelerator sends that region relative to optimal 

1665 routing. `show` reads the current dials plus the scheduled controller's 

1666 last decisions; `set` applies a manual dial and records an override the 

1667 controller honors; `clear` removes the override so the controller (when 

1668 enabled in enforce mode) resumes managing the region. 

1669 """ 

1670 

1671 

1672@traffic_dial.command("show") 

1673@pass_config 

1674def traffic_dial_show(config: Any) -> None: 

1675 """Show per-region traffic dials, endpoint health, and overrides.""" 

1676 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1677 

1678 formatter = get_output_formatter(config) 

1679 manager = get_traffic_dial_manager(config) 

1680 try: 

1681 statuses = manager.get_status() 

1682 state = manager.read_controller_state() 

1683 except TrafficDialError as e: 

1684 formatter.print_error(str(e)) 

1685 sys.exit(1) 

1686 except Exception as e: 

1687 formatter.print_error(f"Failed to read traffic-dial status: {e}") 

1688 sys.exit(1) 

1689 

1690 if config.output_format == "table" and state: 

1691 formatter.print_info( 

1692 f"Controller: {state.get('mode', 'unknown')} mode, " 

1693 f"last run {state.get('timestamp', 'unknown')}" 

1694 ) 

1695 formatter.print( 

1696 statuses, 

1697 columns=[ 

1698 "region", 

1699 "traffic_dial", 

1700 "endpoint_health", 

1701 "healthy_percent", 

1702 "override", 

1703 "controller_reason", 

1704 ], 

1705 ) 

1706 for status in statuses: 

1707 if status.endpoint_health == "no endpoints": 

1708 # An empty endpoint group gets no global traffic at any dial. The 

1709 # region's last convergence never published its Gateway ALB. 

1710 formatter.print_warning( 

1711 f"{status.region} has no endpoint in its Global Accelerator endpoint group, " 

1712 "so global requests never reach it. The ALB is registered by the last step " 

1713 "of the Region's add-on convergence: check it with 'gco stacks addons status " 

1714 f"-r {status.region}' and re-run it with 'gco stacks addons install -r " 

1715 f"{status.region}'. See 'Global Accelerator Stops Routing to a Region' in " 

1716 "docs/RUNBOOKS.md." 

1717 ) 

1718 

1719 

1720@traffic_dial.command("set") 

1721@click.argument("region") 

1722@click.argument("percentage", type=click.IntRange(0, 100)) 

1723@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt") 

1724@pass_config 

1725def traffic_dial_set(config: Any, region: Any, percentage: Any, yes: Any) -> None: 

1726 """Manually dial REGION to PERCENTAGE and pin it against the controller. 

1727 

1728 Applies UpdateEndpointGroup immediately and records an override parameter 

1729 so the scheduled controller leaves the region alone until 

1730 `gco capacity traffic-dial clear` removes it. Dial changes converge to the 

1731 accelerator's edge locations over a few minutes. 

1732 """ 

1733 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1734 

1735 formatter = get_output_formatter(config) 

1736 if not yes and not confirm(f"Dial {region} to {percentage}% of its optimally routed traffic?"): 

1737 formatter.print_info("Aborted; no changes made.") 

1738 return 

1739 

1740 manager = get_traffic_dial_manager(config) 

1741 try: 

1742 status = manager.set_dial(region, percentage) 

1743 except TrafficDialError as e: 

1744 formatter.print_error(str(e)) 

1745 sys.exit(1) 

1746 except Exception as e: 

1747 formatter.print_error(f"Failed to set traffic dial: {e}") 

1748 sys.exit(1) 

1749 

1750 for warning in status.warnings: 

1751 formatter.print_warning(warning) 

1752 formatter.print_success( 

1753 f"Dialed {region} to {percentage}% and recorded the override " 

1754 "(clear it with `gco capacity traffic-dial clear`)." 

1755 ) 

1756 if config.output_format != "table": 

1757 formatter.print(status) 

1758 

1759 

1760@traffic_dial.command("clear") 

1761@click.argument("region") 

1762@pass_config 

1763def traffic_dial_clear(config: Any, region: Any) -> None: 

1764 """Clear REGION's manual override so the controller resumes managing it. 

1765 

1766 The dial itself is left unchanged: with the controller disabled or in 

1767 monitor mode it keeps the last manual value; in enforce mode the 

1768 controller re-converges it from the region's health signal on its next 

1769 cycle. 

1770 """ 

1771 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1772 

1773 formatter = get_output_formatter(config) 

1774 manager = get_traffic_dial_manager(config) 

1775 try: 

1776 existed = manager.clear_override(region) 

1777 except TrafficDialError as e: 

1778 formatter.print_error(str(e)) 

1779 sys.exit(1) 

1780 except Exception as e: 

1781 formatter.print_error(f"Failed to clear traffic-dial override: {e}") 

1782 sys.exit(1) 

1783 

1784 if existed: 

1785 formatter.print_success(f"Cleared the {region} traffic-dial override.") 

1786 else: 

1787 formatter.print_info(f"No traffic-dial override was set for {region}.")