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
next prev 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