From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 23B0F21C179 for ; Sun, 31 Aug 2025 04:00:05 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=198.137.202.133 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756612810; cv=none; b=DpawVYCBOcRlPpqjuKskV6E9mgUmYSJEsOVwKQXJ//q5Nt6RO+dtJmmyrIAjL2FYllwwMVoYLaFmzZNL+kT2I7Nm/5KGuFhJNoX5BKrAOddGovwi8poMyZM5ZUdNHvv7Itfr4swMTVz31Mwjq5L/podNPbNCFUsNm8GQCpvxSJw= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756612810; c=relaxed/simple; bh=cQzSfyasw0brdilDI2cf1PDwEtqp3jZ7x3JHb2wkJuc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=EL2KATYENh/l7hE89rhGAZnBpZVtjt2I5WYwKojMGjA2m0GZ6uJ2dJt679d0fjsq/aS+AyNhtAWyYJhT4FR4y3jX7PHU+85tT9MB4Np21D42wdhkq7SXFwr97ND5b1z/o6tzgoJOsgCzBFDk2EUtB7G69Jcnq0tlUCNEIeNMQi4= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org; spf=none smtp.mailfrom=infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=CCZvvpmQ; arc=none smtp.client-ip=198.137.202.133 Authentication-Results: smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="CCZvvpmQ" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=bombadil.20210309; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:Cc: To:From:Reply-To:Content-ID:Content-Description; bh=sZ4rJ84/ikfb8ae/eUNXQckZcxJ2bS7LetllNWD8HrE=; b=CCZvvpmQFu8b/eUr/ARsqSHhoI 3JrfpbhR5AxUoQwa8HOR3VGQKjbAQeN3alkStYtV0b4TI3mnLzZ39QgqBgI/wjd/1lBlgBzeKNnxm iS6uaJCl56aIvO68wiGkJ7Spmypa2kdCgRqdaXEHSMeE/5OkPTWtrw1TdckjYIbA19sITHAuLuuyZ DjR2MssKBHmnv0tnZGwO9Z/JqLLtwIZm1t+m5TNV62xEWNj+uJuQcqmZKsnMEA9puBAo498iUnf2J SEGumyTnZjpLWJwN7tMzCt3blMUHtpdwTzX2UhuYQ801KVBOlNKnn1tWRGwkwru9SC/Fw/0UWC21s sf4k7rig==; Received: from mcgrof by bombadil.infradead.org with local (Exim 4.98.2 #2 (Red Hat Linux)) id 1usZEb-000000093rg-2nva; Sun, 31 Aug 2025 04:00:05 +0000 From: Luis Chamberlain To: Chuck Lever , Daniel Gomez , kdevops@lists.linux.dev Cc: Luis Chamberlain , Your Name Subject: [PATCH v3 03/10] scripts: add Lambda Labs testing and debugging utilities Date: Sat, 30 Aug 2025 20:59:57 -0700 Message-ID: <20250831040004.2159779-4-mcgrof@kernel.org> X-Mailer: git-send-email 2.49.0 In-Reply-To: <20250831040004.2159779-1-mcgrof@kernel.org> References: <20250831040004.2159779-1-mcgrof@kernel.org> Precedence: bulk X-Mailing-List: kdevops@lists.linux.dev List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sender: Luis Chamberlain Add comprehensive CLI tools for Lambda Labs development and testing: - lambda-cli: Full-featured CLI tool for Lambda Labs operations - Instance type listing and filtering - Region management with availability info - Pricing information and cost analysis - Smart instance/region selection algorithms - Availability checking for instance/region combinations - Kconfig generation for development workflow - cloud_list_all.sh: Multi-provider instance listing utility - docs/lambda-cli.1: Complete man page documentation The lambda-cli provides AWS-style command interface for: - Debugging API connectivity and authentication - Testing dynamic configuration generation - Manual instance and region selection - Development workflow automation - Cost analysis and optimization These tools enable efficient development, testing, and troubleshooting of Lambda Labs integration. Generated-by: Claude AI Signed-off-by: Your Name --- docs/lambda-cli.1 | 245 +++++++++++++++ scripts/cloud_list_all.sh | 152 +++++++++ scripts/lambda-cli | 639 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 1036 insertions(+) create mode 100644 docs/lambda-cli.1 create mode 100755 scripts/cloud_list_all.sh create mode 100755 scripts/lambda-cli diff --git a/docs/lambda-cli.1 b/docs/lambda-cli.1 new file mode 100644 index 0000000..dbb513e --- /dev/null +++ b/docs/lambda-cli.1 @@ -0,0 +1,245 @@ +.\" Manpage for lambda-cli +.\" Contact mcgrof@kernel.org to correct errors or typos. +.TH LAMBDA-CLI 1 "August 2025" "kdevops 5.0.2" "Lambda Labs CLI Manual" +.SH NAME +lambda-cli \- Lambda Labs cloud management CLI for kdevops +.SH SYNOPSIS +.B lambda-cli +[\fB\-h\fR] +[\fB\-\-output\fR \fIFORMAT\fR] +\fICOMMAND\fR +[\fIARGS\fR] +.SH DESCRIPTION +.B lambda-cli +is a structured command-line interface tool for managing Lambda Labs cloud +resources within the kdevops framework. It provides access to Lambda Labs +cloud provider functionality for dynamic configuration generation, resource +management, and cost optimization. + +The tool mimics AWS CLI patterns to provide a consistent and scalable +interface that can be extended for other cloud providers. +.SH OPTIONS +.TP +.BR \-h ", " \-\-help +Show help message and exit +.TP +.BR \-o " " \fIFORMAT\fR ", " \-\-output " " \fIFORMAT\fR +Output format. Valid options are: +.RS +.TP +.B json +Machine-readable JSON format (default for scripting) +.TP +.B text +Human-readable table format (default for interactive use) +.RE +.SH COMMANDS +.SS instance-types +Manage Lambda Labs instance types +.TP +.B instance-types list +[\fB\-\-available\-only\fR] +[\fB\-\-region\fR \fIREGION\fR] +.RS +List all instance types. Use \fB\-\-available\-only\fR to show only instances +with current capacity. Use \fB\-\-region\fR to filter by specific region. +.RE +.TP +.B instance-types get-cheapest +[\fB\-\-region\fR \fIREGION\fR] +[\fB\-\-min\-gpus\fR \fIN\fR] +.RS +Find the cheapest available instance. Optionally filter by region or +minimum number of GPUs required. +.RE +.SS regions +Manage Lambda Labs regions +.TP +.B regions list +[\fB\-\-with\-availability\fR] +.RS +List all Lambda Labs regions. Use \fB\-\-with\-availability\fR to include +the count of available instance types in each region. +.RE +.SS pricing +Get pricing information for Lambda Labs instances +.TP +.B pricing list +[\fB\-\-instance\-type\fR \fITYPE\fR] +.RS +List pricing for all instance types, or for a specific instance type. +Shows hourly, daily, and monthly costs. +.RE +.SS smart-select +Intelligent instance and region selection +.TP +.B smart-select +[\fB\-\-mode\fR \fIMODE\fR] +.RS +Automatically select optimal instance and region configuration. +.RE +.RS +.TP +\fIMODE\fR options: +.TP +.B cheapest +Select the globally cheapest available instance and best region (default) +.TP +.B closest +Select based on geographic proximity (not yet implemented) +.TP +.B balanced +Balance between cost and proximity +.RE +.SS generate-kconfig +Generate dynamic Kconfig files for Lambda Labs +.TP +.B generate-kconfig +[\fB\-\-output\-dir\fR \fIDIR\fR] +.RS +Generate Kconfig.compute.generated and Kconfig.location.generated files +based on current Lambda Labs API data. Default output directory is +terraform/lambdalabs/kconfigs. +.RE +.SH ENVIRONMENT +.TP +.B LAMBDALABS_API_KEY +Lambda Labs API key for authentication. If not set, the tool will attempt +to read credentials from ~/.lambdalabs/credentials. +.SH FILES +.TP +.I ~/.lambdalabs/credentials +Lambda Labs credentials file containing API key +.TP +.I terraform/lambdalabs/kconfigs/Kconfig.compute.generated +Dynamically generated Kconfig file for instance types +.TP +.I terraform/lambdalabs/kconfigs/Kconfig.location.generated +Dynamically generated Kconfig file for regions +.SH EXAMPLES +.SS Basic Usage +.TP +List all available instances: +.B lambda-cli instance-types list --available-only +.TP +Get pricing information: +.B lambda-cli pricing list +.TP +Find cheapest instance: +.B lambda-cli instance-types get-cheapest +.SS JSON Output +.TP +Get regions in JSON format: +.B lambda-cli --output json regions list +.TP +Smart selection with JSON output: +.B lambda-cli -o json smart-select --mode cheapest +.SS Filtering +.TP +List instances available in specific region: +.B lambda-cli instance-types list --region us-west-1 +.TP +Find cheapest instance with at least 2 GPUs: +.B lambda-cli instance-types get-cheapest --min-gpus 2 +.SS Kconfig Generation +.TP +Generate dynamic Kconfig files: +.B lambda-cli generate-kconfig +.TP +Generate to custom directory: +.B lambda-cli generate-kconfig --output-dir /tmp/kconfigs +.SH INTEGRATION WITH KDEVOPS +.SS Makefile Integration +The lambda-cli tool can be integrated into kdevops Makefiles: +.PP +.RS +.nf +LAMBDA_CLI := $(TOPDIR_PATH)/scripts/lambda-cli + +lambda-list-instances: + @$(LAMBDA_CLI) instance-types list --available-only + +lambda-smart-select: + @$(LAMBDA_CLI) smart-select --mode cheapest +.fi +.RE +.SS Kconfig Integration +Use lambda-cli in Kconfig shell commands: +.PP +.RS +.nf +config TERRAFORM_LAMBDALABS_REGION + string + default $(shell, scripts/lambda-cli smart-select \\ + --mode cheapest -o json | \\ + python3 -c "import sys, json; \\ + print(json.load(sys.stdin).get('region'))") +.fi +.RE +.SS Ansible Integration +Call lambda-cli from Ansible playbooks: +.PP +.RS +.nf +- name: Get cheapest Lambda Labs instance + command: scripts/lambda-cli instance-types \\ + get-cheapest --output json + register: cheapest_instance + delegate_to: localhost +.fi +.RE +.SH EXIT STATUS +.TP +.B 0 +Successful execution +.TP +.B 1 +General error (invalid arguments, API failure, etc.) +.SH DIAGNOSTICS +The lambda-cli tool provides detailed error messages when operations fail. +Common issues include: +.TP +.B "No API key found" +Set LAMBDALABS_API_KEY environment variable or configure ~/.lambdalabs/credentials +.TP +.B "No available instances matching criteria" +No instances have current capacity matching the specified filters +.TP +.B "API request failed" +Network error or invalid API key +.SH NOTES +.SS Caching +The underlying Lambda Labs API library may cache responses for performance. +Cache duration is typically 15 minutes for pricing data. +.SS Fallback Behavior +When API access fails, lambda-cli will attempt to use sensible defaults: +.RS +.IP \(bu 2 +Default instance type: gpu_1x_a10 +.IP \(bu 2 +Default region: us-west-1 +.IP \(bu 2 +Static Kconfig with minimal options +.RE +.SS Rate Limiting +Be aware of Lambda Labs API rate limits when using lambda-cli in automated +scripts. Consider adding delays between requests in tight loops. +.SH SEE ALSO +.BR opentofu (1), +.PP +Full documentation at: +.br +Lambda Labs documentation: +.SH BUGS +Report bugs to: +.SH AUTHOR +Written by the kdevops contributors. +.PP +Lambda-cli tool generated by Claude AI. +.SH COPYRIGHT +Copyright \(co 2025 Luis Chamberlain +.br +License: MIT +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. diff --git a/scripts/cloud_list_all.sh b/scripts/cloud_list_all.sh new file mode 100755 index 0000000..90bdd2d --- /dev/null +++ b/scripts/cloud_list_all.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +# List all cloud instances across supported providers +# Currently supports: Lambda Labs + +set -e + +PROVIDER="" + +# Detect which cloud provider is configured +if [ -f .config ]; then + if grep -q "CONFIG_TERRAFORM_LAMBDALABS=y" .config 2>/dev/null; then + PROVIDER="lambdalabs" + elif grep -q "CONFIG_TERRAFORM_AWS=y" .config 2>/dev/null; then + PROVIDER="aws" + elif grep -q "CONFIG_TERRAFORM_GCE=y" .config 2>/dev/null; then + PROVIDER="gce" + elif grep -q "CONFIG_TERRAFORM_AZURE=y" .config 2>/dev/null; then + PROVIDER="azure" + elif grep -q "CONFIG_TERRAFORM_OCI=y" .config 2>/dev/null; then + PROVIDER="oci" + fi +fi + +if [ -z "$PROVIDER" ]; then + echo "No cloud provider configured or .config file not found" + exit 1 +fi + +echo "Cloud Provider: $PROVIDER" +echo + +case "$PROVIDER" in + lambdalabs) + # Get API key from credentials file + API_KEY=$(python3 $(dirname "$0")/lambdalabs_credentials.py get 2>/dev/null) + if [ -z "$API_KEY" ]; then + echo "Error: Lambda Labs API key not found" + echo "Please configure it with: python3 scripts/lambdalabs_credentials.py set 'your-api-key'" + exit 1 + fi + + # Try to list instances using curl + echo "Fetching Lambda Labs instances..." + response=$(curl -s -H "Authorization: Bearer $API_KEY" \ + https://cloud.lambdalabs.com/api/v1/instances 2>&1) + + # Check if we got an error + if echo "$response" | grep -q '"error"'; then + echo "Error accessing Lambda Labs API:" + echo "$response" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if 'error' in data: + err = data['error'] + print(f\" {err.get('message', 'Unknown error')}\") + if 'suggestion' in err: + print(f\" Suggestion: {err['suggestion']}\") +except: + print(' Unable to parse error response') +" + exit 1 + fi + + # Parse and display instances + echo "$response" | python3 -c ' +import sys, json +from datetime import datetime + +def format_uptime(created_at): + try: + created = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + now = datetime.now(created.tzinfo) + delta = now - created + + days = delta.days + hours, remainder = divmod(delta.seconds, 3600) + minutes, _ = divmod(remainder, 60) + + if days > 0: + return f"{days}d {hours}h {minutes}m" + elif hours > 0: + return f"{hours}h {minutes}m" + else: + return f"{minutes}m" + except: + return "unknown" + +data = json.load(sys.stdin) +instances = data.get("data", []) + +if not instances: + print("No Lambda Labs instances currently running") +else: + print("Lambda Labs Instances:") + print("=" * 80) + headers = f"{'Name':<20} {'Type':<20} {'IP':<15} {'Region':<15} {'Status':<10}" + print(headers) + print("-" * 80) + + total_cost = 0 + for inst in instances: + name = inst.get("name", "unnamed") + inst_type = inst.get("instance_type", {}).get("name", "unknown") + ip = inst.get("ip", "pending") + region = inst.get("region", {}).get("name", "unknown") + status = inst.get("status", "unknown") + + # Highlight kdevops instances + if "cgpu" in name or "kdevops" in name.lower(): + name = f"→ {name}" + + row = f"{name:<20} {inst_type:<20} {ip:<15} {region:<15} {status:<10}" + print(row) + + price_cents = inst.get("instance_type", {}).get("price_cents_per_hour", 0) + total_cost += price_cents / 100 + + print("-" * 80) + print(f"Total instances: {len(instances)}") + if total_cost > 0: + print(f"Total hourly cost: ${total_cost:.2f}/hr") + print(f"Daily cost estimate: ${total_cost * 24:.2f}/day") +' + ;; + + aws) + echo "AWS cloud listing not yet implemented" + echo "You can use: aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,PublicIpAddress,State.Name,Tags[?Key==\`Name\`]|[0].Value]' --output table" + ;; + + gce) + echo "Google Cloud listing not yet implemented" + echo "You can use: gcloud compute instances list" + ;; + + azure) + echo "Azure cloud listing not yet implemented" + echo "You can use: az vm list --output table" + ;; + + oci) + echo "Oracle Cloud listing not yet implemented" + echo "You can use: oci compute instance list --compartment-id " + ;; + + *) + echo "Cloud provider '$PROVIDER' not supported for listing" + exit 1 + ;; +esac diff --git a/scripts/lambda-cli b/scripts/lambda-cli new file mode 100755 index 0000000..c4cf149 --- /dev/null +++ b/scripts/lambda-cli @@ -0,0 +1,639 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Lambda Labs CLI tool for kdevops + +A structured CLI tool that mimics AWS CLI patterns, providing access to +Lambda Labs cloud provider functionality for dynamic configuration generation +and resource management. +""" + +import argparse +import json +import sys +import os +from typing import Dict, List, Any, Optional, Tuple +from pathlib import Path + +# Import the existing Lambda Labs API functions +try: + from lambdalabs_api import ( + get_api_key, + get_instance_types_with_capacity, + get_regions, + get_instance_pricing, + generate_instance_types_kconfig, + generate_regions_kconfig, + generate_instance_type_mappings, + ) +except ImportError: + # Try to import from scripts directory if not in path + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from lambdalabs_api import ( + get_api_key, + get_instance_types_with_capacity, + get_regions, + get_instance_pricing, + generate_instance_types_kconfig, + generate_regions_kconfig, + generate_instance_type_mappings, + ) + + +class LambdaCLI: + """Lambda Labs CLI interface""" + + def __init__(self, output_format: str = "json"): + """ + Initialize the CLI with specified output format + + Args: + output_format: 'json' or 'text' for output formatting + """ + self.output_format = output_format + self.api_key = get_api_key() + + def output(self, data: Any, headers: Optional[List[str]] = None): + """ + Output data in the specified format + + Args: + data: Data to output (dict, list, or primitive) + headers: Column headers for text format (optional) + """ + if self.output_format == "json": + print(json.dumps(data, indent=2)) + else: + # Human-readable text format + if isinstance(data, list): + if data and isinstance(data[0], dict): + # Table format for list of dicts + if not headers: + headers = list(data[0].keys()) if data else [] + + if headers: + # Calculate column widths + widths = {h: len(h) for h in headers} + for item in data: + for h in headers: + val = str(item.get(h, "")) + widths[h] = max(widths[h], len(val)) + + # Print header + header_line = " | ".join(h.ljust(widths[h]) for h in headers) + print(header_line) + print("-" * len(header_line)) + + # Print rows + for item in data: + row = " | ".join( + str(item.get(h, "")).ljust(widths[h]) for h in headers + ) + print(row) + else: + # Simple list + for item in data: + print(item) + elif isinstance(data, dict): + # Key-value format + max_key_len = max(len(k) for k in data.keys()) if data else 0 + for key, value in data.items(): + print(f"{key.ljust(max_key_len)} : {value}") + else: + # Simple value + print(data) + + def list_instance_types( + self, available_only: bool = False, region: Optional[str] = None + ) -> List[Dict[str, Any]]: + """ + List instance types + + Args: + available_only: Only show available instances + region: Filter by specific region + + Returns: + List of instance type information + """ + if not self.api_key: + return [ + { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + ] + + instances, capacity_map = get_instance_types_with_capacity(self.api_key) + pricing = get_instance_pricing() + + result = [] + for name, info in instances.items(): + available_regions = capacity_map.get(name, []) + + # Apply filters + if available_only and not available_regions: + continue + + if region and region not in available_regions: + continue + + # Get price from pricing data + price_per_hour = pricing.get(name, 0.0) + + item = { + "name": name, + "price_per_hour": f"${price_per_hour:.2f}", + "specs": info.get("specs_overview", ""), + "available_regions": len(available_regions), + } + if region: + item["available_in_region"] = region in available_regions + result.append(item) + + # Sort by price + result.sort(key=lambda x: float(x["price_per_hour"].replace("$", ""))) + + return result + + def list_regions(self, with_availability: bool = False) -> List[Dict[str, Any]]: + """ + List regions + + Args: + with_availability: Include availability information + + Returns: + List of region information + """ + if not self.api_key: + return [ + { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + ] + + regions = get_regions(self.api_key) + + result = [] + for region in regions: + item = { + "name": region["name"], + "description": region.get("description", ""), + } + + if with_availability: + # Count available instance types in this region + _, capacity_map = get_instance_types_with_capacity(self.api_key) + available_count = sum( + 1 + for instance, regions_list in capacity_map.items() + if region["name"] in regions_list + ) + item["available_instances"] = available_count + + result.append(item) + + return result + + def get_cheapest_instance( + self, region: Optional[str] = None, min_gpus: int = 1 + ) -> Dict[str, Any]: + """ + Find the cheapest available instance + + Args: + region: Specific region to search in + min_gpus: Minimum number of GPUs required + + Returns: + Cheapest instance information + """ + if not self.api_key: + return { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + + instances, capacity_map = get_instance_types_with_capacity(self.api_key) + pricing = get_instance_pricing() + + # Find available instances with pricing + available = [] + for name, info in instances.items(): + available_regions = capacity_map.get(name, []) + if not available_regions: + continue + + if region and region not in available_regions: + continue + + # Filter by GPU count + if min_gpus > 1: + parts = name.split("_") + if len(parts) >= 2 and "x" in parts[1]: + gpu_count = int(parts[1].replace("x", "")) + if gpu_count < min_gpus: + continue + + price = pricing.get(name, float("inf")) + available.append( + { + "name": name, + "price": price, + "specs": info.get("specs_overview", ""), + "available_regions": available_regions, + } + ) + + if not available: + return {"error": "No available instances matching criteria"} + + # Sort by price and get cheapest + cheapest = min(available, key=lambda x: x["price"]) + + return { + "name": cheapest["name"], + "price_per_hour": f"${cheapest['price']:.2f}", + "specs": cheapest["specs"], + "available_regions": cheapest["available_regions"], + } + + def get_pricing(self, instance_type: Optional[str] = None) -> List[Dict[str, Any]]: + """ + Get pricing information + + Args: + instance_type: Specific instance type to get pricing for + + Returns: + Pricing information + """ + if not self.api_key: + return [ + { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + ] + + instances, _ = get_instance_types_with_capacity(self.api_key) + pricing = get_instance_pricing() + + result = [] + for name, info in instances.items(): + if instance_type and name != instance_type: + continue + + price = pricing.get(name, 0.0) + result.append( + { + "instance_type": name, + "price_per_hour": f"${price:.2f}", + "price_per_day": f"${price * 24:.2f}", + "price_per_month": f"${price * 24 * 30:.2f}", + "specs": info.get("specs_overview", ""), + } + ) + + # Sort by price + result.sort(key=lambda x: float(x["price_per_hour"].replace("$", ""))) + + return result + + def smart_select(self, mode: str = "cheapest") -> Dict[str, Any]: + """ + Smart selection of instance and region + + Args: + mode: Selection mode ('cheapest', 'closest', 'balanced') + + Returns: + Selected configuration + """ + if mode == "cheapest": + # Find cheapest instance globally + cheapest = self.get_cheapest_instance() + if "error" in cheapest: + return cheapest + + # Select closest region with this instance + available_regions = cheapest.get("available_regions", []) + if not available_regions: + return {"error": "No regions available for cheapest instance"} + + # For now, just pick the first available region + # In a full implementation, we'd determine closest based on user location + selected_region = available_regions[0] + + return { + "instance_type": cheapest["name"], + "region": selected_region, + "price_per_hour": cheapest["price_per_hour"], + "selection_mode": "cheapest_global", + } + + elif mode == "closest": + # This would require geolocation logic + # For now, return a placeholder + return { + "error": "Closest region selection not yet implemented", + "hint": "Use --mode cheapest for automatic selection", + } + + elif mode == "balanced": + # Balance between price and proximity + # This is a simplified implementation + cheapest = self.get_cheapest_instance() + if "error" in cheapest: + return cheapest + + return { + "instance_type": cheapest["name"], + "region": cheapest.get("available_regions", ["us-west-1"])[0], + "price_per_hour": cheapest["price_per_hour"], + "selection_mode": "balanced", + } + + else: + return {"error": f"Unknown selection mode: {mode}"} + + def check_availability(self, instance_type: str, region: str) -> Dict[str, Any]: + """ + Check if an instance type is available in a specific region. + + Args: + instance_type: Instance type to check + region: Region to check + + Returns: + Availability status + """ + if not self.api_key: + return { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + + instances, capacity_map = get_instance_types_with_capacity(self.api_key) + + if instance_type not in instances: + return { + "available": False, + "error": f"Instance type {instance_type} not found", + } + + available_regions = capacity_map.get(instance_type, []) + + if not available_regions: + return { + "available": False, + "error": f"Instance type {instance_type} has no available capacity in any region", + } + + if region not in available_regions: + return { + "available": False, + "error": f"Instance type {instance_type} not available in {region}", + "available_regions": available_regions, + } + + return { + "available": True, + "instance_type": instance_type, + "region": region, + "message": f"Instance {instance_type} is available in {region}", + } + + def generate_kconfig(self, output_dir: str = "terraform/lambdalabs/kconfigs"): + """ + Generate Kconfig files for Lambda Labs + + Args: + output_dir: Directory to write Kconfig files to + + Returns: + Status information + """ + if not self.api_key: + return { + "error": "No API key found. Please set LAMBDALABS_API_KEY or configure credentials." + } + + os.makedirs(output_dir, exist_ok=True) + + # Generate compute Kconfig + compute_kconfig = generate_instance_types_kconfig(self.api_key) + compute_path = os.path.join(output_dir, "Kconfig.compute.generated") + with open(compute_path, "w") as f: + f.write(compute_kconfig) + + # Generate location Kconfig + location_kconfig = generate_regions_kconfig(self.api_key) + location_path = os.path.join(output_dir, "Kconfig.location.generated") + with open(location_path, "w") as f: + f.write(location_kconfig) + + # Generate instance type mappings + mappings = generate_instance_type_mappings(self.api_key) + mappings_path = os.path.join(output_dir, "Kconfig.compute.mappings") + with open(mappings_path, "w") as f: + f.write(mappings) + + return { + "status": "success", + "files_generated": [compute_path, location_path, mappings_path], + "message": "Kconfig files generated successfully", + } + + +def main(): + """Main CLI entry point""" + parser = argparse.ArgumentParser( + prog="lambda-cli", + description="Lambda Labs CLI for kdevops", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # List all instance types + lambda-cli.py instance-types list + + # List available instances only + lambda-cli.py instance-types list --available-only + + # Get cheapest instance + lambda-cli.py instance-types get-cheapest + + # List regions with availability info + lambda-cli.py regions list --with-availability + + # Get pricing information + lambda-cli.py pricing list + + # Smart selection + lambda-cli.py smart-select --mode cheapest + + # Generate Kconfig files + lambda-cli.py generate-kconfig + + # JSON output + lambda-cli.py instance-types list --output json + """, + ) + + # Global options + parser.add_argument( + "--output", + "-o", + choices=["json", "text"], + default="text", + help="Output format (default: text)", + ) + + # Subparsers for different commands + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Instance types commands + instance_parser = subparsers.add_parser( + "instance-types", help="Manage instance types" + ) + instance_subparsers = instance_parser.add_subparsers(dest="subcommand") + + # instance-types list + list_instances = instance_subparsers.add_parser("list", help="List instance types") + list_instances.add_argument( + "--available-only", action="store_true", help="Show only available instances" + ) + list_instances.add_argument("--region", help="Filter by region") + + # instance-types get-cheapest + cheapest_parser = instance_subparsers.add_parser( + "get-cheapest", help="Find cheapest instance" + ) + cheapest_parser.add_argument("--region", help="Specific region") + cheapest_parser.add_argument( + "--min-gpus", type=int, default=1, help="Minimum number of GPUs" + ) + + # Regions commands + region_parser = subparsers.add_parser("regions", help="Manage regions") + region_subparsers = region_parser.add_subparsers(dest="subcommand") + + # regions list + list_regions = region_subparsers.add_parser("list", help="List regions") + list_regions.add_argument( + "--with-availability", + action="store_true", + help="Include availability information", + ) + + # Pricing commands + pricing_parser = subparsers.add_parser("pricing", help="Get pricing information") + pricing_subparsers = pricing_parser.add_subparsers(dest="subcommand") + + # pricing list + list_pricing = pricing_subparsers.add_parser("list", help="List pricing") + list_pricing.add_argument("--instance-type", help="Specific instance type") + + # Smart selection + smart_parser = subparsers.add_parser( + "smart-select", help="Smart instance/region selection" + ) + smart_parser.add_argument( + "--mode", + choices=["cheapest", "closest", "balanced"], + default="cheapest", + help="Selection mode", + ) + + # Check availability + check_parser = subparsers.add_parser( + "check-availability", help="Check instance availability" + ) + check_parser.add_argument("instance_type", help="Instance type to check") + check_parser.add_argument("region", help="Region to check") + + # Generate Kconfig + kconfig_parser = subparsers.add_parser( + "generate-kconfig", help="Generate Kconfig files" + ) + kconfig_parser.add_argument( + "--output-dir", + default="terraform/lambdalabs/kconfigs", + help="Output directory for Kconfig files", + ) + + # Parse arguments + args = parser.parse_args() + + # Initialize CLI + cli = LambdaCLI(output_format=args.output) + + # Handle commands + try: + if args.command == "instance-types": + if args.subcommand == "list": + result = cli.list_instance_types( + available_only=args.available_only, region=args.region + ) + headers = ["name", "price_per_hour", "specs", "available_regions"] + if args.region: + headers.append("available_in_region") + cli.output(result, headers=headers) + + elif args.subcommand == "get-cheapest": + result = cli.get_cheapest_instance( + region=args.region, min_gpus=args.min_gpus + ) + cli.output(result) + + else: + parser.error(f"Unknown subcommand: {args.subcommand}") + + elif args.command == "regions": + if args.subcommand == "list": + result = cli.list_regions(with_availability=args.with_availability) + headers = ["name", "description"] + if args.with_availability: + headers.append("available_instances") + cli.output(result, headers=headers) + + else: + parser.error(f"Unknown subcommand: {args.subcommand}") + + elif args.command == "pricing": + if args.subcommand == "list": + result = cli.get_pricing(instance_type=args.instance_type) + headers = [ + "instance_type", + "price_per_hour", + "price_per_day", + "price_per_month", + ] + cli.output(result, headers=headers) + + else: + parser.error(f"Unknown subcommand: {args.subcommand}") + + elif args.command == "smart-select": + result = cli.smart_select(mode=args.mode) + cli.output(result) + + elif args.command == "check-availability": + result = cli.check_availability(args.instance_type, args.region) + cli.output(result) + + elif args.command == "generate-kconfig": + result = cli.generate_kconfig(output_dir=args.output_dir) + cli.output(result) + + else: + parser.print_help() + sys.exit(1) + + except Exception as e: + if args.output == "json": + print(json.dumps({"error": str(e)}, indent=2)) + else: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() -- 2.50.1