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>, Your Name <email@example.com>
Subject: [PATCH v3 04/10] scripts: add Lambda Labs credentials management
Date: Sat, 30 Aug 2025 20:59:58 -0700 [thread overview]
Message-ID: <20250831040004.2159779-5-mcgrof@kernel.org> (raw)
In-Reply-To: <20250831040004.2159779-1-mcgrof@kernel.org>
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 <email@example.com>
---
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 <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"[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
next prev parent reply other threads:[~2025-08-31 4:00 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-31 3:59 [PATCH v3 00/10] terraform: add Lambda Labs cloud provider support Luis Chamberlain
2025-08-31 3:59 ` [PATCH v3 01/10] gitignore: add entries for Lambda Labs dynamic configuration Luis Chamberlain
2025-08-31 3:59 ` [PATCH v3 02/10] scripts: add Lambda Labs Python API library Luis Chamberlain
2025-08-31 3:59 ` [PATCH v3 03/10] scripts: add Lambda Labs testing and debugging utilities Luis Chamberlain
2025-08-31 3:59 ` Luis Chamberlain [this message]
2025-08-31 3:59 ` [PATCH v3 05/10] scripts: add Lambda Labs SSH key management utilities Luis Chamberlain
2025-08-31 4:00 ` [PATCH v3 06/10] kconfig: add dynamic cloud provider configuration infrastructure Luis Chamberlain
2025-08-31 4:00 ` [PATCH v3 07/10] terraform/lambdalabs: add Kconfig structure for Lambda Labs Luis Chamberlain
2025-08-31 4:00 ` [PATCH v3 08/10] terraform/lambdalabs: add terraform provider implementation Luis Chamberlain
2025-08-31 4:00 ` [PATCH v3 09/10] ansible/terraform: integrate Lambda Labs into build system Luis Chamberlain
2025-08-31 4:00 ` [PATCH v3 10/10] kconfigs: enable Lambda Labs cloud provider in menus Luis Chamberlain
2025-09-01 1:10 ` [PATCH v3 00/10] terraform: add Lambda Labs cloud provider support 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=20250831040004.2159779-5-mcgrof@kernel.org \
--to=mcgrof@kernel.org \
--cc=cel@kernel.org \
--cc=da.gomez@kruces.com \
--cc=email@example.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