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

824 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +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 pass 

24 

25 

26@capacity.command("check") 

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

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

29@click.option( 

30 "--type", 

31 "-t", 

32 "capacity_type", 

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

34 default="both", 

35 help="Capacity type to check", 

36) 

37@click.option( 

38 "--enrich-historical", 

39 is_flag=True, 

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

41) 

42@pass_config 

43def check_capacity( 

44 config: Any, 

45 instance_type: Any, 

46 region: Any, 

47 capacity_type: Any, 

48 enrich_historical: Any, 

49) -> None: 

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

51 

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

53 """ 

54 formatter = get_output_formatter(config) 

55 checker = get_capacity_checker(config) 

56 

57 try: 

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

59 

60 if config.output_format == "table": 

61 print(format_capacity_table(estimates)) 

62 else: 

63 formatter.print(estimates) 

64 

65 if enrich_historical: 

66 _print_historical_enrichment(formatter, instance_type, region) 

67 

68 except Exception as e: 

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

70 sys.exit(1) 

71 

72 

73@capacity.command("recommend") 

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

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

76@click.option( 

77 "--fault-tolerance", 

78 "-f", 

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

80 default="medium", 

81 help="Fault tolerance level", 

82) 

83@pass_config 

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

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

86 formatter = get_output_formatter(config) 

87 checker = get_capacity_checker(config) 

88 

89 try: 

90 capacity_type, explanation = checker.recommend_capacity_type( 

91 instance_type, region, fault_tolerance 

92 ) 

93 

94 if config.output_format == "table": 

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

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

97 else: 

98 formatter.print( 

99 { 

100 "capacity_type": capacity_type, 

101 "explanation": explanation, 

102 } 

103 ) 

104 

105 except Exception as e: 

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

107 sys.exit(1) 

108 

109 

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

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

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

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

114@pass_config 

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

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

117 formatter = get_output_formatter(config) 

118 checker = get_capacity_checker(config) 

119 

120 try: 

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

122 

123 if not prices: 

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

125 return 

126 

127 formatter.print( 

128 prices, 

129 columns=[ 

130 "availability_zone", 

131 "current_price", 

132 "avg_price_7d", 

133 "min_price_7d", 

134 "max_price_7d", 

135 "price_stability", 

136 ], 

137 ) 

138 

139 except Exception as e: 

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

141 sys.exit(1) 

142 

143 

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

145@click.argument("instance_type") 

146@click.option( 

147 "--region", 

148 "-r", 

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

150) 

151@pass_config 

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

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

154 

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

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

157 reported the day it ships. 

158 

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

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

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

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

163 

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

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

166 recommend-region`. 

167 

168 Examples: 

169 gco capacity instance-info p5.48xlarge 

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

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

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

173 """ 

174 formatter = get_output_formatter(config) 

175 checker = get_capacity_checker(config) 

176 

177 try: 

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

179 if not info: 

180 target = region or config.default_region 

181 formatter.print_error( 

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

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

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

185 ) 

186 sys.exit(1) 

187 

188 if config.output_format != "table": 

189 formatter.print(info) 

190 return 

191 

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

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

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

195 if info.cores is not None: 

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

197 print() 

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

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

200 if info.processor_manufacturer: 

201 clock = ( 

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

203 ) 

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

205 

206 if info.gpu_devices: 

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

208 for device in info.gpu_devices: 

209 print( 

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

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

212 ) 

213 if info.neuron_devices: 

214 print( 

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

216 f"{info.neuron_memory_gib} GiB total" 

217 ) 

218 for device in info.neuron_devices: 

219 print( 

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

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

222 ) 

223 if info.inference_accelerators: 

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

225 

226 print("\n NETWORK") 

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

228 if info.efa_max_interfaces: 

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

230 print() 

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

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

233 if info.maximum_network_cards: 

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

235 

236 print("\n STORAGE") 

237 if info.instance_storage_total_gb: 

238 disks = ", ".join( 

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

240 for d in info.instance_storage_disks 

241 ) 

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

243 else: 

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

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

246 if info.ebs_maximum_iops: 

247 print( 

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

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

250 ) 

251 

252 print("\n PURCHASING") 

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

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

255 if info.supported_placement_strategies: 

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

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

258 print() 

259 

260 except Exception as e: 

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

262 sys.exit(1) 

263 

264 

265@capacity.command("status") 

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

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

268@pass_config 

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

270 """Show comprehensive resource utilization across regions. 

271 

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

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

274 

275 Examples: 

276 gco capacity status 

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

278 gco capacity status --all-regions 

279 """ 

280 from ..capacity import get_multi_region_capacity_checker 

281 

282 formatter = get_output_formatter(config) 

283 

284 try: 

285 checker = get_multi_region_capacity_checker(config) 

286 

287 if region: 

288 capacity = checker.get_region_capacity(region) 

289 formatter.print(capacity) 

290 else: 

291 capacities = checker.get_all_regions_capacity() 

292 

293 if not capacities: 

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

295 if config.output_format != "table": 

296 formatter.print(capacities) 

297 return 

298 

299 if config.output_format == "table": 

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

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

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

303 print( 

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

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

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

307 ) 

308 

309 print() 

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

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

312 else: 

313 formatter.print(capacities) 

314 

315 except Exception as e: 

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

317 sys.exit(1) 

318 

319 

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

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

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

323@click.option( 

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

325) 

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

327@pass_config 

328def recommend_region( 

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

330) -> None: 

331 """Recommend optimal region for job placement. 

332 

333 Analyzes capacity across all deployed EKS regions and recommends 

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

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

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

337 

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

339 queue depth, GPU utilization, and running jobs. 

340 

341 Examples: 

342 gco capacity recommend-region 

343 gco capacity recommend-region --gpu 

344 gco capacity recommend-region -i g5.xlarge 

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

346 """ 

347 from ..capacity import get_multi_region_capacity_checker 

348 

349 formatter = get_output_formatter(config) 

350 

351 try: 

352 checker = get_multi_region_capacity_checker(config) 

353 recommendation = checker.recommend_region_for_job( 

354 gpu_required=gpu, 

355 min_gpus=min_gpus, 

356 instance_type=instance_type, 

357 gpu_count=gpu_count, 

358 ) 

359 

360 if config.output_format == "table": 

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

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

363 

364 if config.verbose: 

365 print("\nAll regions ranked:") 

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

367 print( 

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

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

370 ) 

371 else: 

372 formatter.print(recommendation) 

373 

374 except Exception as e: 

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

376 sys.exit(1) 

377 

378 

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

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

381@click.option( 

382 "--instance-type", 

383 "-i", 

384 multiple=True, 

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

386) 

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

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

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

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

391@click.option( 

392 "--fault-tolerance", 

393 "-f", 

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

395 default="medium", 

396 help="Fault tolerance level", 

397) 

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

399@click.option( 

400 "--model", 

401 "-m", 

402 default=None, 

403 help=( 

404 "Bedrock model ID to use " 

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

406 ), 

407) 

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

409@pass_config 

410def ai_recommend( 

411 config: Any, 

412 workload: Any, 

413 instance_type: Any, 

414 region: Any, 

415 gpu: Any, 

416 min_gpus: Any, 

417 min_memory_gb: Any, 

418 fault_tolerance: Any, 

419 max_cost: Any, 

420 model: Any, 

421 raw: Any, 

422) -> None: 

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

424 

425 This command gathers comprehensive capacity data including: 

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

427 - On-demand availability and pricing 

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

429 block-availability trends 

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

431 - Running and pending job counts 

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

433 

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

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

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

437 

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

439 for where to place your workload. 

440 

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

442 before making production decisions. Capacity availability and pricing 

443 can change rapidly. 

444 

445 REQUIREMENTS: 

446 - AWS credentials with bedrock:InvokeModel permission 

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

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

449 

450 Examples: 

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

452 

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

454 

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

456 

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

458 """ 

459 from ..capacity import get_bedrock_capacity_advisor 

460 

461 formatter = get_output_formatter(config) 

462 

463 # Print disclaimer 

464 print() 

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

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

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

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

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

470 print(" ") 

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

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

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

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

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

476 print() 

477 

478 try: 

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

480 

481 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

482 

483 # Build requirements dict 

484 requirements = { 

485 "gpu_required": gpu, 

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

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

488 "fault_tolerance": fault_tolerance, 

489 "max_cost_per_hour": max_cost, 

490 } 

491 # Remove None values 

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

493 

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

495 

496 recommendation = advisor.get_recommendation( 

497 workload_description=workload, 

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

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

500 requirements=requirements if requirements else None, 

501 ) 

502 

503 # Display recommendation 

504 print() 

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

506 print(" 🤖 AI RECOMMENDATION") 

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

508 print() 

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

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

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

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

513 if recommendation.cost_estimate: 

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

515 print() 

516 print(" REASONING:") 

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

518 # Word wrap the reasoning 

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

520 for line in reasoning_lines: 

521 if line.strip(): 

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

523 print() 

524 

525 # Show alternatives 

526 if recommendation.alternative_options: 

527 print(" ALTERNATIVE OPTIONS:") 

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

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

530 print( 

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

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

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

534 ) 

535 if alt.get("reason"): 

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

537 print() 

538 

539 # Show warnings 

540 if recommendation.warnings: 

541 print(" ⚠️ WARNINGS:") 

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

543 for warning in recommendation.warnings: 

544 print(f"{warning}") 

545 print() 

546 

547 # Show raw response if requested 

548 if raw: 

549 print(" RAW AI RESPONSE:") 

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

551 print(recommendation.raw_response) 

552 print() 

553 

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

555 print() 

556 

557 except Exception as e: 

558 if is_bedrock_ftu_form_error(e): 

559 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

560 sys.exit(1) 

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

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

563 formatter.print_error(str(e)) 

564 sys.exit(1) 

565 

566 

567@capacity.command("reservations") 

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

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

570@pass_config 

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

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

573 

574 Shows all active capacity reservations with utilization details. 

575 

576 Examples: 

577 gco capacity reservations 

578 gco capacity reservations -i p5.48xlarge 

579 gco capacity reservations -r us-east-1 

580 """ 

581 formatter = get_output_formatter(config) 

582 checker = get_capacity_checker(config) 

583 

584 try: 

585 if region: 

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

587 result = { 

588 "regions_checked": [region], 

589 "total_reservations": len(reservations), 

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

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

592 "reservations": reservations, 

593 } 

594 else: 

595 result = checker.list_all_reservations(instance_type=instance_type) 

596 

597 if config.output_format != "table": 

598 formatter.print(result) 

599 return 

600 

601 reservations = result["reservations"] 

602 if not reservations: 

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

604 return 

605 

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

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

608 print( 

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

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

611 ) 

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

613 for r in reservations: 

614 print( 

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

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

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

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

619 ) 

620 

621 print() 

622 print( 

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

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

625 ) 

626 print() 

627 

628 except Exception as e: 

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

630 sys.exit(1) 

631 

632 

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

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

635@click.option( 

636 "--region", 

637 "-r", 

638 "regions", 

639 multiple=True, 

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

641) 

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

643@click.option( 

644 "--include-blocks/--no-blocks", 

645 default=True, 

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

647) 

648@click.option( 

649 "--block-duration", 

650 default=24, 

651 type=int, 

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

653) 

654@click.option( 

655 "--block-duration-days", 

656 default=None, 

657 type=int, 

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

659) 

660@click.option( 

661 "--earliest-start", 

662 default=None, 

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

664) 

665@click.option( 

666 "--latest-start", 

667 default=None, 

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

669) 

670@pass_config 

671def reservation_check( 

672 config: Any, 

673 instance_type: Any, 

674 regions: Any, 

675 count: Any, 

676 include_blocks: Any, 

677 block_duration: Any, 

678 block_duration_days: Any, 

679 earliest_start: Any, 

680 latest_start: Any, 

681) -> None: 

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

683 

684 Checks both existing ODCRs and purchasable Capacity Blocks for ML 

685 workloads. Capacity Blocks provide guaranteed GPU capacity for a 

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

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

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

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

690 

691 Examples: 

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

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

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

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

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

697 """ 

698 formatter = get_output_formatter(config) 

699 checker = get_capacity_checker(config) 

700 

701 try: 

702 formatter.print_info( 

703 f"Checking reservations for {instance_type} " 

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

705 ) 

706 

707 result = checker.check_reservation_availability( 

708 instance_type=instance_type, 

709 regions=list(regions) or None, 

710 min_count=count, 

711 include_capacity_blocks=include_blocks, 

712 block_duration_hours=block_duration, 

713 block_duration_days=block_duration_days, 

714 earliest_start=earliest_start, 

715 latest_start=latest_start, 

716 ) 

717 

718 if config.output_format != "table": 

719 formatter.print(result) 

720 return 

721 

722 # ODCR section 

723 odcr = result["odcr"] 

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

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

726 if odcr["reservations"]: 

727 for r in odcr["reservations"]: 

728 print( 

729 f"{r['availability_zone']}: " 

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

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

732 ) 

733 print( 

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

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

736 ) 

737 else: 

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

739 

740 # Capacity Blocks section 

741 if include_blocks: 

742 block_section = result["capacity_blocks"] 

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

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

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

746 if block_section["offerings"]: 

747 for b in block_section["offerings"]: 

748 gpu_hr = b.get("price_per_gpu_hour") 

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

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

751 print( 

752 f"{b['availability_zone']}: " 

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

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

755 ) 

756 else: 

757 print(" No Capacity Block offerings available") 

758 

759 # Recommendation 

760 print() 

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

762 print() 

763 

764 except Exception as e: 

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

766 sys.exit(1) 

767 

768 

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

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

771 itype = result["instance_type"] 

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

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

774 else: 

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

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

777 

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

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

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

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

782 if days: 

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

784 else: 

785 span = "n/a" 

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

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

788 

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

790 print() 

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

792 print() 

793 return 

794 

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

796 if not offerings: 

797 print() 

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

799 print() 

800 return 

801 

802 print() 

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

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

805 for b in offerings: 

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

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

808 fee = b.get("upfront_fee_usd") 

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

810 gpu_hr = b.get("price_per_gpu_hour") 

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

812 print( 

813 f" {str(b.get('region') or ''):<13} {str(b.get('availability_zone') or ''):<17} " 

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

815 ) 

816 print() 

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

818 print() 

819 

820 

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

822@click.option( 

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

824) 

825@click.option( 

826 "--region", 

827 "-r", 

828 "regions", 

829 multiple=True, 

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

831) 

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

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

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

835@click.option( 

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

837) 

838@click.option( 

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

840) 

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

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

843@click.option( 

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

845) 

846@click.option( 

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

848) 

849@click.option( 

850 "--find-longest", 

851 is_flag=True, 

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

853) 

854@pass_config 

855def find_blocks( 

856 config: Any, 

857 instance_type: Any, 

858 regions: Any, 

859 count: Any, 

860 duration_days: Any, 

861 duration_hours: Any, 

862 min_duration_days: Any, 

863 max_duration_days: Any, 

864 min_duration_hours: Any, 

865 max_duration_hours: Any, 

866 earliest_start: Any, 

867 latest_start: Any, 

868 find_longest: Any, 

869) -> None: 

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

871 

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

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

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

875 

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

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

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

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

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

881 

882 Examples: 

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

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

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

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

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

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

889 """ 

890 formatter = get_output_formatter(config) 

891 checker = get_capacity_checker(config) 

892 

893 try: 

894 result = checker.find_capacity_blocks( 

895 instance_type, 

896 regions=list(regions) or None, 

897 instance_count=count, 

898 duration_hours=duration_hours, 

899 duration_days=duration_days, 

900 min_duration_hours=min_duration_hours, 

901 min_duration_days=min_duration_days, 

902 max_duration_hours=max_duration_hours, 

903 max_duration_days=max_duration_days, 

904 earliest_start=earliest_start, 

905 latest_start=latest_start, 

906 find_longest=find_longest, 

907 ) 

908 

909 if config.output_format != "table": 

910 formatter.print(result) 

911 return 

912 

913 _print_find_blocks_report(result) 

914 

915 except Exception as e: 

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

917 sys.exit(1) 

918 

919 

920@capacity.command("reserve") 

921@click.option( 

922 "--offering-id", 

923 "-o", 

924 required=True, 

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

926) 

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

928@click.option( 

929 "--dry-run", 

930 is_flag=True, 

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

932) 

933@pass_config 

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

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

936 

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

938 and their IDs, then purchase with this command. 

939 

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

941 Use --dry-run to validate first. 

942 

943 Examples: 

944 # First, find offerings: 

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

946 

947 # Validate without purchasing: 

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

949 

950 # Purchase: 

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

952 """ 

953 formatter = get_output_formatter(config) 

954 checker = get_capacity_checker(config) 

955 

956 try: 

957 if dry_run: 

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

959 else: 

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

961 

962 result = checker.purchase_capacity_block( 

963 offering_id=offering_id, 

964 region=region, 

965 dry_run=dry_run, 

966 ) 

967 

968 if config.output_format != "table": 

969 formatter.print(result) 

970 return 

971 

972 if result["success"]: 

973 if dry_run: 

974 print() 

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

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

977 print() 

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

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

980 print() 

981 else: 

982 print() 

983 print(" ✓ Capacity Block purchased successfully") 

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

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

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

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

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

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

990 print() 

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

992 print( 

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

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

995 ) 

996 print() 

997 else: 

998 formatter.print_error( 

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

1000 ) 

1001 sys.exit(1) 

1002 

1003 except Exception as e: 

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

1005 sys.exit(1) 

1006 

1007 

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

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

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

1011 req = result.get("requested_instance_type") 

1012 if req and req != itype: 

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

1014 else: 

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

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

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

1018 

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

1020 print() 

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

1022 print() 

1023 return 

1024 

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

1026 if not reservations: 

1027 print() 

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

1029 print() 

1030 return 

1031 

1032 print() 

1033 print( 

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

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

1036 ) 

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

1038 for r in reservations: 

1039 gpu_hr = r.get("price_per_gpu_hour") 

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

1041 print( 

1042 f" {str(r.get('instance_type') or ''):<18} {str(r.get('region') or ''):<13} " 

1043 f"{str(r.get('availability_zone') or ''):<17} " 

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

1045 f"{gpu_hr_str:>10}" 

1046 ) 

1047 print() 

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

1049 print() 

1050 

1051 

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

1053@click.option( 

1054 "--instance-type", 

1055 "-i", 

1056 default=None, 

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

1058) 

1059@click.option( 

1060 "--region", 

1061 "-r", 

1062 "regions", 

1063 multiple=True, 

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

1065) 

1066@click.option( 

1067 "--count", 

1068 "-c", 

1069 default=1, 

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

1071) 

1072@click.option( 

1073 "--state", 

1074 default="active", 

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

1076) 

1077@click.option( 

1078 "--pricing/--no-pricing", 

1079 default=True, 

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

1081) 

1082@pass_config 

1083def find_reservations( 

1084 config: Any, 

1085 instance_type: Any, 

1086 regions: Any, 

1087 count: Any, 

1088 state: Any, 

1089 pricing: Any, 

1090) -> None: 

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

1092 

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

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

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

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

1097 

1098 Examples: 

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

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

1101 gco capacity find-reservations --no-pricing 

1102 """ 

1103 formatter = get_output_formatter(config) 

1104 checker = get_capacity_checker(config) 

1105 

1106 try: 

1107 result = checker.find_capacity_reservations( 

1108 instance_type=instance_type, 

1109 regions=list(regions) or None, 

1110 min_count=count, 

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

1112 include_pricing=pricing, 

1113 ) 

1114 

1115 if config.output_format != "table": 

1116 formatter.print(result) 

1117 return 

1118 

1119 _print_find_reservations_report(result) 

1120 

1121 except Exception as e: 

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

1123 sys.exit(1) 

1124 

1125 

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

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

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

1129@click.option( 

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

1131) 

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

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

1134@click.option( 

1135 "--tenancy", 

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

1137 default="default", 

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

1139) 

1140@click.option( 

1141 "--match-criteria", 

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

1143 default="open", 

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

1145) 

1146@click.option( 

1147 "--end-date", 

1148 default=None, 

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

1150) 

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

1152@click.option( 

1153 "--dry-run", 

1154 is_flag=True, 

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

1156) 

1157@pass_config 

1158def create_reservation( 

1159 config: Any, 

1160 instance_type: Any, 

1161 region: Any, 

1162 availability_zone: Any, 

1163 count: Any, 

1164 platform: Any, 

1165 tenancy: Any, 

1166 match_criteria: Any, 

1167 end_date: Any, 

1168 ebs_optimized: Any, 

1169 dry_run: Any, 

1170) -> None: 

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

1172 

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

1174 for an instance type in a specific AZ. 

1175 

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

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

1178 Use --dry-run to validate first. 

1179 

1180 Examples: 

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

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

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

1184 --end-date 2026-08-01 

1185 """ 

1186 formatter = get_output_formatter(config) 

1187 checker = get_capacity_checker(config) 

1188 

1189 try: 

1190 if dry_run: 

1191 formatter.print_info( 

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

1193 f"in {availability_zone}..." 

1194 ) 

1195 else: 

1196 formatter.print_info( 

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

1198 ) 

1199 

1200 result = checker.create_capacity_reservation( 

1201 instance_type=instance_type, 

1202 region=region, 

1203 availability_zone=availability_zone, 

1204 instance_count=count, 

1205 instance_platform=platform, 

1206 tenancy=tenancy, 

1207 instance_match_criteria=match_criteria, 

1208 end_date=end_date, 

1209 ebs_optimized=ebs_optimized, 

1210 dry_run=dry_run, 

1211 ) 

1212 

1213 if config.output_format != "table": 

1214 formatter.print(result) 

1215 return 

1216 

1217 if result["success"]: 

1218 if dry_run: 

1219 print() 

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

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

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

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

1224 print() 

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

1226 print( 

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

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

1229 ) 

1230 print() 

1231 else: 

1232 print() 

1233 print(" ✓ Capacity Reservation created successfully") 

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

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

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

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

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

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

1240 print() 

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

1242 print( 

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

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

1245 ) 

1246 print() 

1247 else: 

1248 formatter.print_error( 

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

1250 ) 

1251 sys.exit(1) 

1252 

1253 except Exception as e: 

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

1255 sys.exit(1) 

1256 

1257 

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

1259@click.option( 

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

1261) 

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

1263@click.option( 

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

1265) 

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

1267@pass_config 

1268def cancel_reservation( 

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

1270) -> None: 

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

1272 

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

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

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

1276 On-Demand billing. 

1277 

1278 Examples: 

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

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

1281 """ 

1282 formatter = get_output_formatter(config) 

1283 checker = get_capacity_checker(config) 

1284 

1285 if not dry_run and not yes: 

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

1287 

1288 try: 

1289 if dry_run: 

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

1291 else: 

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

1293 

1294 result = checker.cancel_capacity_reservation( 

1295 reservation_id=reservation_id, 

1296 region=region, 

1297 dry_run=dry_run, 

1298 ) 

1299 

1300 if config.output_format != "table": 

1301 formatter.print(result) 

1302 return 

1303 

1304 if result["success"]: 

1305 print() 

1306 if dry_run: 

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

1308 else: 

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

1310 print() 

1311 else: 

1312 formatter.print_error( 

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

1314 ) 

1315 sys.exit(1) 

1316 

1317 except Exception as e: 

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

1319 sys.exit(1) 

1320 

1321 

1322_HISTORY_DISABLED_HINT = ( 

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

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

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

1326) 

1327 

1328 

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

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

1331 return ( 

1332 isinstance(exc, ClientError) 

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

1334 ) 

1335 

1336 

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

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

1339 from ..capacity.history import get_capacity_history_store 

1340 

1341 try: 

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

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

1344 if _history_disabled(e): 

1345 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1346 else: 

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

1348 return 

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

1350 formatter.print_warning( 

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

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

1353 ) 

1354 return 

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

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

1357 if spot_stats: 

1358 print( 

1359 f" spot_score p25/p50/p75: " 

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

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

1362 ) 

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

1364 if price_stats: 

1365 print( 

1366 f" spot_price p25/p50/p75: " 

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

1368 ) 

1369 

1370 

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

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

1373 from ..capacity.history import DAY_NAMES 

1374 

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

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

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

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

1379 lines.append(header) 

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

1381 for day in DAY_NAMES: 

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

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

1384 for hour in range(24): 

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

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

1387 lines.append(row) 

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

1389 if best: 

1390 lines.append("") 

1391 lines.append("Best windows:") 

1392 for window in best: 

1393 lines.append( 

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

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

1396 ) 

1397 return "\n".join(lines) 

1398 

1399 

1400@capacity.group("history") 

1401def history() -> None: 

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

1403 

1404 

1405@history.command("show") 

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

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

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

1409@pass_config 

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

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

1412 from ..capacity.history import get_capacity_history_store 

1413 

1414 formatter = get_output_formatter(config) 

1415 try: 

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

1417 if not trend: 

1418 formatter.print_warning( 

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

1420 ) 

1421 return 

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

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

1424 # hand-maintained literal drifting out of date. 

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

1426 except Exception as e: 

1427 if _history_disabled(e): 

1428 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1429 return 

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

1431 sys.exit(1) 

1432 

1433 

1434@history.command("stats") 

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

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

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

1438@pass_config 

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

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

1441 from ..capacity.history import get_capacity_history_store 

1442 

1443 formatter = get_output_formatter(config) 

1444 try: 

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

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

1447 formatter.print_warning( 

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

1449 ) 

1450 return 

1451 if config.output_format == "table": 

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

1453 print( 

1454 formatter.format( 

1455 rows, 

1456 columns=[ 

1457 "metric", 

1458 "count", 

1459 "min", 

1460 "p25", 

1461 "p50", 

1462 "p75", 

1463 "max", 

1464 "mean", 

1465 "stddev", 

1466 ], 

1467 ) 

1468 ) 

1469 else: 

1470 formatter.print(stats) 

1471 except Exception as e: 

1472 if _history_disabled(e): 

1473 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1474 return 

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

1476 sys.exit(1) 

1477 

1478 

1479@history.command("patterns") 

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

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

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

1483@click.option( 

1484 "--metric", 

1485 "-m", 

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

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

1488 # updating a literal here. 

1489 type=click.Choice(METRIC_FIELDS), 

1490 default="spot_score", 

1491 show_default=True, 

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

1493) 

1494@pass_config 

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

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

1497 from ..capacity.history import get_capacity_history_store 

1498 

1499 formatter = get_output_formatter(config) 

1500 try: 

1501 patterns = get_capacity_history_store().get_temporal_patterns( 

1502 instance_type, region, hours, metric=metric 

1503 ) 

1504 if not patterns["patterns"]: 

1505 formatter.print_warning( 

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

1507 ) 

1508 return 

1509 if config.output_format == "table": 

1510 print(_format_patterns_grid(patterns)) 

1511 else: 

1512 formatter.print(patterns) 

1513 except Exception as e: 

1514 if _history_disabled(e): 

1515 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1516 return 

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

1518 sys.exit(1) 

1519 

1520 

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

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

1523 return { 

1524 "instance_type": prediction.instance_type, 

1525 "region": prediction.region, 

1526 "confidence": prediction.confidence, 

1527 "best_windows": prediction.best_windows, 

1528 "avoid_windows": prediction.avoid_windows, 

1529 "reasoning": prediction.reasoning, 

1530 } 

1531 

1532 

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

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

1535 print() 

1536 print( 

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

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

1539 ) 

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

1541 if prediction.best_windows: 

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

1543 print( 

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

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

1546 ) 

1547 else: 

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

1549 if prediction.avoid_windows: 

1550 print() 

1551 print(" Windows to avoid:") 

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

1553 print( 

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

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

1556 ) 

1557 if prediction.reasoning: 

1558 print() 

1559 print(" Reasoning:") 

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

1561 if line.strip(): 

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

1563 if raw: 

1564 print() 

1565 print(prediction.raw_response) 

1566 

1567 

1568@capacity.command("predict") 

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

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

1571@click.option( 

1572 "--all-regions", 

1573 "-a", 

1574 is_flag=True, 

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

1576) 

1577@click.option( 

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

1579) 

1580@click.option( 

1581 "--model", 

1582 "-m", 

1583 default=None, 

1584 help=( 

1585 "Bedrock model ID to use " 

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

1587 ), 

1588) 

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

1590@pass_config 

1591def predict_capacity( 

1592 config: Any, 

1593 instance_type: Any, 

1594 region: Any, 

1595 all_regions: Any, 

1596 hours: Any, 

1597 model: Any, 

1598 raw: Any, 

1599) -> None: 

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

1601 

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

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

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

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

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

1607 """ 

1608 from ..capacity import get_bedrock_capacity_advisor 

1609 

1610 formatter = get_output_formatter(config) 

1611 if all_regions and region: 

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

1613 sys.exit(1) 

1614 if not all_regions and not region: 

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

1616 sys.exit(1) 

1617 

1618 try: 

1619 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

1620 if all_regions: 

1621 predictions = advisor.predict_capacity_windows_all_regions( 

1622 instance_type, hours_back=hours 

1623 ) 

1624 else: 

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

1626 except ValueError as e: 

1627 formatter.print_warning(str(e)) 

1628 return 

1629 except Exception as e: 

1630 if _history_disabled(e): 

1631 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1632 return 

1633 if is_bedrock_ftu_form_error(e): 

1634 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

1635 sys.exit(1) 

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

1637 sys.exit(1) 

1638 

1639 if not predictions: 

1640 formatter.print_warning( 

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

1642 ) 

1643 return 

1644 

1645 if config.output_format != "table": 

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

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

1648 return 

1649 

1650 if all_regions: 

1651 formatter.print_info( 

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

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

1654 ) 

1655 for prediction in predictions: 

1656 _print_prediction(prediction, raw) 

1657 

1658 

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

1660@pass_config 

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

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

1663 

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

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

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

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

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

1669 enabled in enforce mode) resumes managing the region. 

1670 """ 

1671 pass 

1672 

1673 

1674@traffic_dial.command("show") 

1675@pass_config 

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

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

1678 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1679 

1680 formatter = get_output_formatter(config) 

1681 manager = get_traffic_dial_manager(config) 

1682 try: 

1683 statuses = manager.get_status() 

1684 state = manager.read_controller_state() 

1685 except TrafficDialError as e: 

1686 formatter.print_error(str(e)) 

1687 sys.exit(1) 

1688 except Exception as e: 

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

1690 sys.exit(1) 

1691 

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

1693 formatter.print_info( 

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

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

1696 ) 

1697 formatter.print( 

1698 statuses, 

1699 columns=[ 

1700 "region", 

1701 "traffic_dial", 

1702 "endpoint_health", 

1703 "healthy_percent", 

1704 "override", 

1705 "controller_reason", 

1706 ], 

1707 ) 

1708 

1709 

1710@traffic_dial.command("set") 

1711@click.argument("region") 

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

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

1714@pass_config 

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

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

1717 

1718 Applies UpdateEndpointGroup immediately and records an override parameter 

1719 so the scheduled controller leaves the region alone until 

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

1721 accelerator's edge locations over a few minutes. 

1722 """ 

1723 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1724 

1725 formatter = get_output_formatter(config) 

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

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

1728 return 

1729 

1730 manager = get_traffic_dial_manager(config) 

1731 try: 

1732 status = manager.set_dial(region, percentage) 

1733 except TrafficDialError as e: 

1734 formatter.print_error(str(e)) 

1735 sys.exit(1) 

1736 except Exception as e: 

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

1738 sys.exit(1) 

1739 

1740 for warning in status.warnings: 

1741 formatter.print_warning(warning) 

1742 formatter.print_success( 

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

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

1745 ) 

1746 if config.output_format != "table": 

1747 formatter.print(status) 

1748 

1749 

1750@traffic_dial.command("clear") 

1751@click.argument("region") 

1752@pass_config 

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

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

1755 

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

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

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

1759 cycle. 

1760 """ 

1761 from ..capacity.traffic_dial import TrafficDialError, get_traffic_dial_manager 

1762 

1763 formatter = get_output_formatter(config) 

1764 manager = get_traffic_dial_manager(config) 

1765 try: 

1766 existed = manager.clear_override(region) 

1767 except TrafficDialError as e: 

1768 formatter.print_error(str(e)) 

1769 sys.exit(1) 

1770 except Exception as e: 

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

1772 sys.exit(1) 

1773 

1774 if existed: 

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

1776 else: 

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