public inbox for kdevops@lists.linux.dev
 help / color / mirror / Atom feed
From: Luis Chamberlain <mcgrof@kernel.org>
To: Chuck Lever <cel@kernel.org>, Daniel Gomez <da.gomez@kruces.com>,
	kdevops@lists.linux.dev
Cc: Luis Chamberlain <mcgrof@kernel.org>
Subject: [PATCH v2 03/10] scripts: add Lambda Labs credentials management
Date: Wed, 27 Aug 2025 14:28:54 -0700	[thread overview]
Message-ID: <20250827212902.4021990-4-mcgrof@kernel.org> (raw)
In-Reply-To: <20250827212902.4021990-1-mcgrof@kernel.org>

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 <mcgrof@kernel.org>
---
 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 <api_key> [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


  parent reply	other threads:[~2025-08-27 21:29 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-08-27 21:28 [PATCH v2 00/10] terraform: add Lambda Labs cloud provider support with dynamic API-driven configuration Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 01/10] gitignore: add entries for Lambda Labs dynamic configuration Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 02/10] scripts: add Lambda Labs Python API library Luis Chamberlain
2025-08-28 18:59   ` Chuck Lever
2025-08-28 19:33     ` Luis Chamberlain
2025-08-28 20:00       ` Chuck Lever
2025-08-28 20:03         ` Luis Chamberlain
2025-08-28 20:13           ` Chuck Lever
2025-08-28 20:16             ` Luis Chamberlain
2025-08-29 11:24               ` Luis Chamberlain
2025-08-29 13:48                 ` Chuck Lever
2025-08-27 21:28 ` Luis Chamberlain [this message]
2025-08-27 21:28 ` [PATCH v2 04/10] scripts: add Lambda Labs SSH key management utilities Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 05/10] kconfig: add dynamic cloud provider configuration infrastructure Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 06/10] terraform/lambdalabs: add Kconfig structure for Lambda Labs Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 07/10] terraform/lambdalabs: add terraform provider implementation Luis Chamberlain
2025-08-27 21:28 ` [PATCH v2 08/10] ansible/terraform: integrate Lambda Labs into build system Luis Chamberlain
2025-08-27 21:29 ` [PATCH v2 09/10] scripts: add Lambda Labs testing and debugging utilities Luis Chamberlain
2025-08-27 21:29 ` [PATCH v2 10/10] terraform: enable Lambda Labs cloud provider in menus Luis Chamberlain

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250827212902.4021990-4-mcgrof@kernel.org \
    --to=mcgrof@kernel.org \
    --cc=cel@kernel.org \
    --cc=da.gomez@kruces.com \
    --cc=kdevops@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox