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 A652A2EBDC2 for ; Wed, 27 Aug 2025 21:29:04 +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=1756330146; cv=none; b=Dxl7anW2y5i5E3UrnEVS6Jg2NauttGLt5gHm7Q9b9PfmQ5i13ZK0XLp7bJMZGCuRLqCHx1nqEzn+7rNpiLql4IQZ9c0QAmvxhn3AGqgXbhaTYUhwhJRf0Rux5P5TD3LeZC+YOdop4xj9PQVjgaR3NJxUKZxaJtGddPL/avtr2hE= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756330146; c=relaxed/simple; bh=ZnFSbXoNI0htAqVdVwjW3YEQXIh5hWTLJHpxIG6l6fA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=W1kUEqpzlJGXKEyAJM41M4+hl5UZFbUfxFS1ezENQ4NHzMj/GmnQQ72f21KAdu4XDUSuqhdLyGvEZFxsyoC+DBZ/rbN4AsYFY4homEXNv/LR/qWnCA1kzqFxB14Zqvt5uYEUr2IfqkOk2BoYo96gTGPQFAr/49CZ846+kU0p1yw= 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=svFOyxMO; 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="svFOyxMO" 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=ucnX4vb2JFBR3urvFMczNL2I1hZkPMCd2OnlG2fsEJA=; b=svFOyxMOfPfze1iT+fUT8HDW8Y PWDzJQc39luxVRkTaJyOjAzddFc4a/iCgT+yzlZfq/2fsAt/1YdHx606np30kBrgWoKMGL5OWEmP0 UoVh83bttPrLVsbeF9mfd2Axoj8uy+umRbgDC/dsiYuM9BQ3ctkavjXiMJHTiZ9nH+mudfSVfG0mf pI7EoXMRjNCiTUNs0oKGTpvc3YRuy3ZjtOmoC3Qo3p++Lv5Jwb6lLflm27uU8HG5aVARDrmeha7Dm rXOcNaLvBuYx0iIIMO+PbW4sgRjNPfDWfklVtZ1O7P2IXncqcCiu6FAdCnMZ6nP96vPa/zlrs43O3 pDTF8+KQ==; Received: from mcgrof by bombadil.infradead.org with local (Exim 4.98.2 #2 (Red Hat Linux)) id 1urNhY-0000000GsJP-1KQw; Wed, 27 Aug 2025 21:29:04 +0000 From: Luis Chamberlain To: Chuck Lever , Daniel Gomez , kdevops@lists.linux.dev Cc: Luis Chamberlain Subject: [PATCH v2 03/10] scripts: add Lambda Labs credentials management Date: Wed, 27 Aug 2025 14:28:54 -0700 Message-ID: <20250827212902.4021990-4-mcgrof@kernel.org> X-Mailer: git-send-email 2.49.0 In-Reply-To: <20250827212902.4021990-1-mcgrof@kernel.org> References: <20250827212902.4021990-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 secure credentials management for Lambda Labs API keys following the AWS-style approach with ~/.lambdalabs/credentials file. This avoids environment variable complexity and provides a secure, persistent way to store API credentials. Features: - File-based credential storage (~/.lambdalabs/credentials) - Profile support (default profile for now) - Secure file permissions (600) - Commands to set, get, check, and clear credentials - Validation and testing utilities This provides the authentication foundation needed by the API library but doesn't enable any user-facing features yet. Generated-by: Claude AI Signed-off-by: Luis Chamberlain --- scripts/lambdalabs_credentials.py | 242 +++++++++++++++++++++++++ scripts/test_lambdalabs_credentials.py | 50 +++++ 2 files changed, 292 insertions(+) create mode 100755 scripts/lambdalabs_credentials.py create mode 100755 scripts/test_lambdalabs_credentials.py diff --git a/scripts/lambdalabs_credentials.py b/scripts/lambdalabs_credentials.py new file mode 100755 index 0000000..86fb45e --- /dev/null +++ b/scripts/lambdalabs_credentials.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: copyleft-next-0.3.1 + +""" +Lambda Labs credentials management. +Reads API keys from credentials file (~/.lambdalabs/credentials). +""" + +import os +import configparser +from pathlib import Path +from typing import Optional + + +def get_credentials_file_path() -> Path: + """Get the default Lambda Labs credentials file path.""" + return Path.home() / ".lambdalabs" / "credentials" + + +def read_credentials_file( + path: Optional[Path] = None, profile: str = "default" +) -> Optional[str]: + """ + Read Lambda Labs API key from credentials file. + + Args: + path: Path to credentials file (defaults to ~/.lambdalabs/credentials) + profile: Profile name to use (defaults to "default") + + Returns: + API key if found, None otherwise + """ + if path is None: + path = get_credentials_file_path() + + if not path.exists(): + return None + + try: + config = configparser.ConfigParser() + config.read(path) + + if profile in config: + # Try different possible key names + for key_name in ["lambdalabs_api_key", "api_key"]: + if key_name in config[profile]: + return config[profile][key_name].strip() + + # Also check if it's in DEFAULT section + if "DEFAULT" in config: + for key_name in ["lambdalabs_api_key", "api_key"]: + if key_name in config["DEFAULT"]: + return config["DEFAULT"][key_name].strip() + + except Exception: + # Silently fail if file can't be parsed + pass + + return None + + +def get_api_key(profile: str = "default") -> Optional[str]: + """ + Get Lambda Labs API key from credentials file. + + Args: + profile: Profile name to use from credentials file + + Returns: + API key if found, None otherwise + """ + # Try default credentials file + api_key = read_credentials_file(profile=profile) + if api_key: + return api_key + + # Try custom credentials file path from environment + custom_path = os.environ.get("LAMBDALABS_CREDENTIALS_FILE") + if custom_path: + api_key = read_credentials_file(Path(custom_path), profile=profile) + if api_key: + return api_key + + return None + + +def create_credentials_file( + api_key: str, path: Optional[Path] = None, profile: str = "default" +) -> bool: + """ + Create or update Lambda Labs credentials file. + + Args: + api_key: The API key to save + path: Path to credentials file (defaults to ~/.lambdalabs/credentials) + profile: Profile name to use (defaults to "default") + + Returns: + True if successful, False otherwise + """ + if path is None: + path = get_credentials_file_path() + + try: + # Create directory if it doesn't exist + path.parent.mkdir(parents=True, exist_ok=True) + + # Read existing config or create new one + config = configparser.ConfigParser() + if path.exists(): + config.read(path) + + # Add or update the profile + if profile not in config: + config[profile] = {} + + config[profile]["lambdalabs_api_key"] = api_key + + # Write the config file with restricted permissions + with open(path, "w") as f: + config.write(f) + + # Set restrictive permissions (owner read/write only) + path.chmod(0o600) + + return True + + except Exception as e: + print(f"Error creating credentials file: {e}") + return False + + +def main(): + """Command-line utility for managing Lambda Labs credentials.""" + import sys + + if len(sys.argv) < 2: + print("Usage:") + print(" lambdalabs_credentials.py get [profile] - Get API key") + print(" lambdalabs_credentials.py set [profile] - Set API key") + print( + " lambdalabs_credentials.py check [profile] - Check if API key is configured" + ) + print(" lambdalabs_credentials.py test [profile] - Test API key validity") + print( + " lambdalabs_credentials.py path - Show credentials file path" + ) + sys.exit(1) + + command = sys.argv[1] + + if command == "get": + profile = sys.argv[2] if len(sys.argv) > 2 else "default" + api_key = get_api_key(profile) + if api_key: + print(api_key) + sys.exit(0) + else: + print("No API key found", file=sys.stderr) + sys.exit(1) + + elif command == "set": + if len(sys.argv) < 3: + print("Error: API key required", file=sys.stderr) + sys.exit(1) + api_key = sys.argv[2] + profile = sys.argv[3] if len(sys.argv) > 3 else "default" + + if create_credentials_file(api_key, profile=profile): + print( + f"API key saved to {get_credentials_file_path()} (profile: {profile})" + ) + sys.exit(0) + else: + print("Failed to save API key", file=sys.stderr) + sys.exit(1) + + elif command == "check": + profile = sys.argv[2] if len(sys.argv) > 2 else "default" + api_key = get_api_key(profile) + if api_key: + print(f"✓ API key configured (profile: {profile})") + # Show sources checked + if read_credentials_file(profile=profile): + print(f" Source: {get_credentials_file_path()}") + elif os.environ.get("LAMBDALABS_CREDENTIALS_FILE"): + print(f" Source: {os.environ.get('LAMBDALABS_CREDENTIALS_FILE')}") + sys.exit(0) + else: + print("✗ No API key found") + print(f" Checked: {get_credentials_file_path()}") + if os.environ.get("LAMBDALABS_CREDENTIALS_FILE"): + print(f" Checked: {os.environ.get('LAMBDALABS_CREDENTIALS_FILE')}") + sys.exit(1) + + elif command == "test": + profile = sys.argv[2] if len(sys.argv) > 2 else "default" + api_key = get_api_key(profile) + if not api_key: + print("✗ No API key found") + sys.exit(1) + + # Test the API key + import urllib.request + import urllib.error + import json + + print(f"Testing API key (profile: {profile})...") + headers = {"Authorization": f"Bearer {api_key}", "User-Agent": "kdevops/1.0"} + + try: + req = urllib.request.Request( + "https://cloud.lambdalabs.com/api/v1/instances", headers=headers + ) + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + print(f"✓ API key is VALID") + print(f" Current instances: {len(data.get('data', []))}") + sys.exit(0) + except urllib.error.HTTPError as e: + if e.code == 403: + print(f"✗ API key is INVALID (HTTP 403 Forbidden)") + print(" The key exists but Lambda Labs rejected it.") + print(" Please get a new API key from https://cloud.lambdalabs.com") + else: + print(f"✗ API test failed: HTTP {e.code}") + sys.exit(1) + except Exception as e: + print(f"✗ API test failed: {e}") + sys.exit(1) + + elif command == "path": + print(get_credentials_file_path()) + sys.exit(0) + + else: + print(f"Unknown command: {command}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_lambdalabs_credentials.py b/scripts/test_lambdalabs_credentials.py new file mode 100755 index 0000000..3991be2 --- /dev/null +++ b/scripts/test_lambdalabs_credentials.py @@ -0,0 +1,50 @@ +#!/bin/bash +# SPDX-License-Identifier: copyleft-next-0.3.1 + +# Setup Lambda Labs environment for kdevops +# This scriptcan be used to test the Lambda Labs credentials are properly +# configured + +echo "Lambda Labs Environment Setup" +echo "==============================" + +# Get API key from credentials file +API_KEY=$(python3 $(dirname "$0")/lambdalabs_credentials.py get 2>/dev/null) + +if [ -z "$API_KEY" ]; then + echo "❌ Lambda Labs API key not found in credentials file" + echo " Please configure it with: python3 scripts/lambdalabs_credentials.py set 'your-api-key'" + exit 1 +else + echo "✓ Lambda Labs API key loaded from credentials file" + echo " Key starts with: ${API_KEY:0:10}..." + echo " Key length: ${#API_KEY} characters" +fi + +# Test API key validity +echo "" +echo "Testing API key validity..." +response=$(curl -s -H "Authorization: Bearer $API_KEY" https://cloud.lambdalabs.com/api/v1/instance-types 2>&1) + +if echo "$response" | grep -q '"data"'; then + echo "✓ API key is valid and working" +else + echo "❌ API key appears to be invalid" + echo " Response: $(echo "$response" | head -3)" + exit 1 +fi + +# Show current configuration +echo "" +echo "Current Configuration:" +echo "----------------------" +if [ -f terraform/lambdalabs/terraform.tfvars ]; then + grep -E "^(lambdalabs_region|lambdalabs_instance_type|lambdalabs_ssh_key_name)" terraform/lambdalabs/terraform.tfvars | sed 's/^/ /' +fi + +echo "" +echo "Environment ready! You can now run:" +echo " make bringup" +echo "" +echo "Lambda Labs API key is stored in: ~/.lambdalabs/credentials" +echo "To update it, run: python3 scripts/lambdalabs_credentials.py set 'new-api-key'" -- 2.50.1