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 23A4E21C160 for ; Sun, 31 Aug 2025 04:00:06 +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=OI3ZbPJxNe/jI7hNlnHXUHtfmEprI5oPy1Fyn5uX7K/FeMjBr+Wi56ga0GcRPDrEFwrxKJEfHHK2w345nG1IjDxo/Zi9L28NNxbWaNZFkaQrmCumJ0ZdH9TVtL733AluQvALaVcP6gmj62gyyFJTdtJfohvWEDKrO/JgrWkvjtI= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756612810; c=relaxed/simple; bh=lVZ+JtSRbL1MkGpCra3rvz87eLbGHlKZEIaQvK0Ph60=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Ivbaz5onx/pe9SHZeyvYQw5LQuuCsbGXA0cgkvx/F3QpGI0BGNmNB+Iju/AmnJUu/14ieR+2mcw2twO1uVxL+8fXC/JRVVaYfhbi23WwnauvMkyrknlxJwJZ7E3CGf7H8W3Pg72vat2nBbfOBmGdPT1zbtcm6TvKUVG1kaZaWPo= 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=RsNYgB+D; 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="RsNYgB+D" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=bombadil.20210309; h=Sender:Content-Transfer-Encoding: MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From: Reply-To:Content-Type:Content-ID:Content-Description; bh=Cv1VbJTvNYqSxVphm9Boe1JkIyWCTj7SrLZHLZ5mIiM=; b=RsNYgB+DgSeveZOUVRFZBnsWfU xIn10Rz4UNMEtEF+XCapW1kSu7p7AWxArPMNWOhfIlE1YUgaH7mfd4uOoPFCmbbcaQaNFxlQwSnle oOmLnsFrTJPGOoFH1AydGtBuBA+CNPWO7CJiXPahrnnjxdDngW3qdjXRY3RWNnSLq6colKzCXwpNQ viZpLhl9F38/IJXJkxjV9iFqHwhiil9td0TYbXL5jgWAiB+DHCVow5/0RESl511F9DRcZWdmmRDAO nTNkX8WaDTNnOCUg5E3+Vqg3Vhr8MDvLBK4Fum3AQqzffc0td/TAvATO5cIAMJuziCzEyUF0F/xG1 RyOCNU4A==; Received: from mcgrof by bombadil.infradead.org with local (Exim 4.98.2 #2 (Red Hat Linux)) id 1usZEb-000000093rj-2wYS; 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 04/10] scripts: add Lambda Labs credentials management Date: Sat, 30 Aug 2025 20:59:58 -0700 Message-ID: <20250831040004.2159779-5-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-Transfer-Encoding: 8bit Sender: Luis Chamberlain Add secure credential management for Lambda Labs API access: - Support for environment variable (LAMBDALABS_API_KEY) - Local credential file storage (~/.config/lambda-labs/credentials) - API key validation and testing - Secure file permissions (0600) - Cross-platform compatibility Provides standardized credential handling following cloud provider best practices for API key management. Generated-by: Claude AI Signed-off-by: Your Name --- scripts/lambdalabs_credentials.py | 242 ++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100755 scripts/lambdalabs_credentials.py diff --git a/scripts/lambdalabs_credentials.py b/scripts/lambdalabs_credentials.py new file mode 100755 index 0000000..0079491 --- /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"[OK] 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("[ERROR] 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("[ERROR] 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"[OK] 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"[ERROR] 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"[ERROR] API test failed: HTTP {e.code}") + sys.exit(1) + except Exception as e: + print(f"[ERROR] 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() -- 2.50.1