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 04/10] scripts: add Lambda Labs SSH key management utilities
Date: Wed, 27 Aug 2025 14:28:55 -0700 [thread overview]
Message-ID: <20250827212902.4021990-5-mcgrof@kernel.org> (raw)
In-Reply-To: <20250827212902.4021990-1-mcgrof@kernel.org>
Add utilities for managing SSH keys with Lambda Labs cloud instances.
This includes automatic key upload, verification, and SSH config
management with per-directory isolation to support multiple kdevops
instances.
Key features:
- Upload and manage SSH keys via API
- Per-directory SSH key isolation using directory checksums
- SSH config file management and updates
- Key verification and listing utilities
- Automatic key creation workflow support
The per-directory isolation ensures each kdevops workspace uses unique
SSH keys, preventing conflicts when running multiple instances.
Generated-by: Claude AI
Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
---
scripts/lambdalabs_ssh_key_name.py | 135 +++++++++
scripts/lambdalabs_ssh_keys.py | 357 ++++++++++++++++++++++++
scripts/ssh_config_file_name.py | 79 ++++++
scripts/test_ssh_keys.py | 97 +++++++
scripts/update_ssh_config_lambdalabs.py | 145 ++++++++++
scripts/upload_ssh_key_to_lambdalabs.py | 176 ++++++++++++
6 files changed, 989 insertions(+)
create mode 100755 scripts/lambdalabs_ssh_key_name.py
create mode 100755 scripts/lambdalabs_ssh_keys.py
create mode 100755 scripts/ssh_config_file_name.py
create mode 100644 scripts/test_ssh_keys.py
create mode 100755 scripts/update_ssh_config_lambdalabs.py
create mode 100755 scripts/upload_ssh_key_to_lambdalabs.py
diff --git a/scripts/lambdalabs_ssh_key_name.py b/scripts/lambdalabs_ssh_key_name.py
new file mode 100755
index 0000000..131ac3a
--- /dev/null
+++ b/scripts/lambdalabs_ssh_key_name.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+"""
+Generate a unique SSH key name for Lambda Labs based on the current directory.
+This ensures each kdevops instance uses its own SSH key for security.
+"""
+
+import hashlib
+import os
+import sys
+
+
+def get_directory_hash(path: str, length: int = 8) -> str:
+ """
+ Generate a short hash of the directory path.
+
+ Args:
+ path: Directory path to hash
+ length: Number of hex characters to use (default 8)
+
+ Returns:
+ Hex string of specified length
+ """
+ # Get the absolute path to ensure consistency
+ abs_path = os.path.abspath(path)
+
+ # Create SHA256 hash of the path
+ hash_obj = hashlib.sha256(abs_path.encode("utf-8"))
+
+ # Return first N characters of the hex digest
+ return hash_obj.hexdigest()[:length]
+
+
+def get_project_name(path: str) -> str:
+ """
+ Extract a meaningful project name from the path.
+
+ Args:
+ path: Directory path
+
+ Returns:
+ Project name derived from directory
+ """
+ abs_path = os.path.abspath(path)
+
+ # Get the last two directory components for context
+ # e.g., /home/user/projects/kdevops -> projects-kdevops
+ parts = abs_path.rstrip("/").split("/")
+
+ if len(parts) >= 2:
+ # Use last two directories
+ project_parts = parts[-2:]
+ # Filter out generic names
+ filtered = [
+ p
+ for p in project_parts
+ if p not in ["data", "home", "root", "usr", "var", "tmp"]
+ ]
+ if filtered:
+ return "-".join(filtered)
+
+ # Fallback to just the last directory
+ return parts[-1] if parts else "kdevops"
+
+
+def generate_ssh_key_name(prefix: str = "kdevops", include_project: bool = True) -> str:
+ """
+ Generate a unique SSH key name for the current directory.
+
+ Args:
+ prefix: Prefix for the key name (default "kdevops")
+ include_project: Include project name in the key (default True)
+
+ Returns:
+ Unique SSH key name like "kdevops-lambda-kdevops-a1b2c3d4"
+ """
+ cwd = os.getcwd()
+ dir_hash = get_directory_hash(cwd)
+
+ parts = [prefix]
+
+ if include_project:
+ project = get_project_name(cwd)
+ # Limit project name length and sanitize
+ project = project.replace("_", "-").replace(".", "-")[:20]
+ parts.append(project)
+
+ parts.append(dir_hash)
+
+ # Create the key name
+ key_name = "-".join(parts)
+
+ # Ensure it's a valid name (alphanumeric and hyphens only)
+ key_name = "".join(c if c.isalnum() or c == "-" else "-" for c in key_name)
+
+ # Remove multiple consecutive hyphens
+ while "--" in key_name:
+ key_name = key_name.replace("--", "-")
+
+ # Trim to reasonable length (Lambda Labs might have limits)
+ if len(key_name) > 50:
+ # Keep prefix, partial project, and full hash
+ key_name = f"{prefix}-{dir_hash}"
+
+ return key_name.strip("-")
+
+
+def main():
+ """Main entry point."""
+ if len(sys.argv) > 1:
+ if sys.argv[1] == "--help" or sys.argv[1] == "-h":
+ print("Usage: lambdalabs_ssh_key_name.py [--simple]")
+ print()
+ print("Generate a unique SSH key name based on current directory.")
+ print()
+ print("Options:")
+ print(" --simple Generate simple name without project context")
+ print(" --help Show this help message")
+ print()
+ print("Examples:")
+ print(" Default: kdevops-lambda-kdevops-a1b2c3d4")
+ print(" Simple: kdevops-a1b2c3d4")
+ sys.exit(0)
+ elif sys.argv[1] == "--simple":
+ print(generate_ssh_key_name(include_project=False))
+ else:
+ print(f"Unknown option: {sys.argv[1]}", file=sys.stderr)
+ sys.exit(1)
+ else:
+ print(generate_ssh_key_name())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/lambdalabs_ssh_keys.py b/scripts/lambdalabs_ssh_keys.py
new file mode 100755
index 0000000..d4caede
--- /dev/null
+++ b/scripts/lambdalabs_ssh_keys.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+"""
+Lambda Labs SSH Key Management via API.
+Provides functions to list, add, and delete SSH keys through the Lambda Labs API.
+"""
+
+import json
+import os
+import sys
+import urllib.request
+import urllib.error
+from typing import Dict, List, Optional, Tuple
+
+# Import our credentials module
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from lambdalabs_credentials import get_api_key as get_api_key_from_credentials
+
+LAMBDALABS_API_BASE = "https://cloud.lambdalabs.com/api/v1"
+
+
+def get_api_key() -> Optional[str]:
+ """Get Lambda Labs API key from credentials file or environment variable."""
+ return get_api_key_from_credentials()
+
+
+def make_api_request(
+ endpoint: str, api_key: str, method: str = "GET", data: Optional[Dict] = None
+) -> Optional[Dict]:
+ """Make a request to Lambda Labs API."""
+ url = f"{LAMBDALABS_API_BASE}{endpoint}"
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ "User-Agent": "kdevops/1.0",
+ }
+
+ try:
+ req_data = None
+ if data and method in ["POST", "PUT", "PATCH"]:
+ req_data = json.dumps(data).encode("utf-8")
+
+ req = urllib.request.Request(url, headers=headers, data=req_data, method=method)
+ with urllib.request.urlopen(req) as response:
+ return json.loads(response.read().decode())
+ except urllib.error.HTTPError as e:
+ print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
+ if e.code == 404:
+ print(f"Endpoint not found: {endpoint}", file=sys.stderr)
+ try:
+ error_body = e.read().decode()
+ print(f"Error details: {error_body}", file=sys.stderr)
+ except:
+ pass
+ return None
+ except Exception as e:
+ print(f"Error making API request: {e}", file=sys.stderr)
+ return None
+
+
+def list_ssh_keys(api_key: str) -> Optional[List[Dict]]:
+ """
+ List all SSH keys associated with the Lambda Labs account.
+
+ Returns:
+ List of SSH key dictionaries with 'name', 'id', and 'public_key' fields
+ """
+ response = make_api_request("/ssh-keys", api_key)
+ if response:
+ # The API returns {"data": [{name, id, public_key}, ...]}
+ if "data" in response:
+ return response["data"]
+ # Fallback for other response formats
+ elif isinstance(response, list):
+ return response
+ return None
+
+
+def add_ssh_key(api_key: str, name: str, public_key: str) -> bool:
+ """
+ Add a new SSH key to the Lambda Labs account.
+
+ Args:
+ api_key: Lambda Labs API key
+ name: Name for the SSH key
+ public_key: The public key content
+
+ Returns:
+ True if successful, False otherwise
+ """
+ # Based on the API response structure, the endpoint is /ssh-keys
+ # and the format is likely {"name": name, "public_key": public_key}
+ endpoint = "/ssh-keys"
+ data = {"name": name, "public_key": public_key.strip()}
+
+ print(f"Adding SSH key '{name}' via POST {endpoint}", file=sys.stderr)
+ response = make_api_request(endpoint, api_key, method="POST", data=data)
+ if response:
+ print(f"Successfully added SSH key '{name}'", file=sys.stderr)
+ return True
+
+ # Try alternative format if the first one fails
+ data = {"name": name, "key": public_key.strip()}
+ print(f"Trying alternative format with 'key' field", file=sys.stderr)
+ response = make_api_request(endpoint, api_key, method="POST", data=data)
+ if response:
+ print(f"Successfully added SSH key '{name}'", file=sys.stderr)
+ return True
+
+ return False
+
+
+def delete_ssh_key(api_key: str, key_name_or_id: str) -> bool:
+ """
+ Delete an SSH key from the Lambda Labs account.
+
+ Args:
+ api_key: Lambda Labs API key
+ key_name_or_id: Name or ID of the SSH key to delete
+
+ Returns:
+ True if successful, False otherwise
+ """
+ # Check if input looks like an ID (32 character hex string)
+ is_id = len(key_name_or_id) == 32 and all(
+ c in "0123456789abcdef" for c in key_name_or_id.lower()
+ )
+
+ if not is_id:
+ # If we have a name, we need to find the ID
+ keys = list_ssh_keys(api_key)
+ if keys:
+ for key in keys:
+ if key.get("name") == key_name_or_id:
+ key_id = key.get("id")
+ if key_id:
+ print(
+ f"Found ID {key_id} for key '{key_name_or_id}'",
+ file=sys.stderr,
+ )
+ key_name_or_id = key_id
+ break
+ else:
+ print(f"SSH key '{key_name_or_id}' not found", file=sys.stderr)
+ return False
+
+ # Delete using the ID
+ endpoint = f"/ssh-keys/{key_name_or_id}"
+ print(f"Deleting SSH key via DELETE {endpoint}", file=sys.stderr)
+ response = make_api_request(endpoint, api_key, method="DELETE")
+ if response is not None:
+ print(f"Successfully deleted SSH key", file=sys.stderr)
+ return True
+
+ return False
+
+
+def read_public_key_file(filepath: str) -> Optional[str]:
+ """Read SSH public key from file."""
+ expanded_path = os.path.expanduser(filepath)
+ if not os.path.exists(expanded_path):
+ print(f"SSH public key file not found: {expanded_path}", file=sys.stderr)
+ return None
+
+ try:
+ with open(expanded_path, "r") as f:
+ return f.read().strip()
+ except Exception as e:
+ print(f"Error reading SSH public key: {e}", file=sys.stderr)
+ return None
+
+
+def check_ssh_key_exists(api_key: str, key_name: str) -> bool:
+ """
+ Check if an SSH key with the given name exists.
+
+ Args:
+ api_key: Lambda Labs API key
+ key_name: Name of the SSH key to check
+
+ Returns:
+ True if key exists, False otherwise
+ """
+ keys = list_ssh_keys(api_key)
+ if not keys:
+ return False
+
+ for key in keys:
+ # Try different possible field names
+ if key.get("name") == key_name or key.get("key_name") == key_name:
+ return True
+
+ return False
+
+
+def validate_ssh_setup(
+ api_key: str, expected_key_name: str = "kdevops-lambdalabs"
+) -> Tuple[bool, str]:
+ """
+ Validate that SSH keys are properly configured for Lambda Labs.
+
+ Args:
+ api_key: Lambda Labs API key
+ expected_key_name: The SSH key name we expect to use
+
+ Returns:
+ Tuple of (success, message)
+ """
+ # First, try to list SSH keys
+ keys = list_ssh_keys(api_key)
+
+ if keys is None:
+ # API doesn't support SSH key management
+ return (
+ False,
+ "Lambda Labs API does not appear to support SSH key management.\n"
+ "You must manually add your SSH key through the Lambda Labs web console:\n"
+ "1. Go to https://cloud.lambdalabs.com/ssh-keys\n"
+ "2. Click 'Add SSH key'\n"
+ f"3. Name it '{expected_key_name}'\n"
+ "4. Paste your public key from ~/.ssh/kdevops_terraform.pub",
+ )
+
+ if not keys:
+ # No keys found
+ return (
+ False,
+ "No SSH keys found in your Lambda Labs account.\n"
+ "Please add an SSH key through the web console or API before proceeding.",
+ )
+
+ # Check if expected key exists
+ key_names = []
+ for key in keys:
+ name = key.get("name") or key.get("key_name")
+ if name:
+ key_names.append(name)
+ if name == expected_key_name:
+ return (True, f"SSH key '{expected_key_name}' found and ready to use.")
+
+ # Key not found but other keys exist
+ key_list = "\n - ".join(key_names)
+ return (
+ False,
+ f"SSH key '{expected_key_name}' not found in your Lambda Labs account.\n"
+ f"Available SSH keys:\n - {key_list}\n"
+ f"Either:\n"
+ f"1. Add a key named '{expected_key_name}' through the web console\n"
+ f"2. Or update terraform/lambdalabs/kconfigs/Kconfig.identity to use one of the existing keys",
+ )
+
+
+def main():
+ """Main entry point for SSH key management."""
+ if len(sys.argv) < 2:
+ print("Usage: lambdalabs_ssh_keys.py <command> [args...]")
+ print("Commands:")
+ print(" list - List all SSH keys")
+ print(" check <name> - Check if a specific key exists")
+ print(" add <name> <public_key_file> - Add a new SSH key")
+ print(" delete <name> - Delete an SSH key")
+ print(" validate [key_name] - Validate SSH setup for kdevops")
+ sys.exit(1)
+
+ command = sys.argv[1]
+ api_key = get_api_key()
+
+ if not api_key:
+ print("Error: Lambda Labs API key not found", file=sys.stderr)
+ print("Please configure your API key:", file=sys.stderr)
+ print(
+ " python3 scripts/lambdalabs_credentials.py set 'your-api-key'", file=sys.stderr
+ )
+ sys.exit(1)
+
+ if command == "list":
+ keys = list_ssh_keys(api_key)
+ if keys is None:
+ print("Failed to list SSH keys - API may not support this feature")
+ sys.exit(1)
+ elif not keys:
+ print("No SSH keys found")
+ else:
+ print("SSH Keys:")
+ for key in keys:
+ if isinstance(key, dict):
+ name = key.get("name") or key.get("key_name") or "Unknown"
+ key_id = key.get("id", "")
+ fingerprint = key.get("fingerprint", "")
+ print(f" - Name: {name}")
+ if key_id and key_id != name:
+ print(f" ID: {key_id}")
+ if fingerprint:
+ print(f" Fingerprint: {fingerprint}")
+ # Show all fields for debugging
+ for k, v in key.items():
+ if k not in ["name", "id", "fingerprint", "key_name"]:
+ print(f" {k}: {v}")
+ else:
+ # Key is just a string (name)
+ print(f" - {key}")
+
+ elif command == "check":
+ if len(sys.argv) < 3:
+ print("Usage: lambdalabs_ssh_keys.py check <key_name>")
+ sys.exit(1)
+ key_name = sys.argv[2]
+ if check_ssh_key_exists(api_key, key_name):
+ print(f"SSH key '{key_name}' exists")
+ else:
+ print(f"SSH key '{key_name}' not found")
+ sys.exit(1)
+
+ elif command == "add":
+ if len(sys.argv) < 4:
+ print("Usage: lambdalabs_ssh_keys.py add <name> <public_key_file>")
+ sys.exit(1)
+ name = sys.argv[2]
+ key_file = sys.argv[3]
+
+ public_key = read_public_key_file(key_file)
+ if not public_key:
+ sys.exit(1)
+
+ if add_ssh_key(api_key, name, public_key):
+ print(f"Successfully added SSH key '{name}'")
+ else:
+ print(f"Failed to add SSH key '{name}'")
+ sys.exit(1)
+
+ elif command == "delete":
+ if len(sys.argv) < 3:
+ print("Usage: lambdalabs_ssh_keys.py delete <key_name>")
+ sys.exit(1)
+ key_name = sys.argv[2]
+
+ if delete_ssh_key(api_key, key_name):
+ print(f"Successfully deleted SSH key '{key_name}'")
+ else:
+ print(f"Failed to delete SSH key '{key_name}'")
+ sys.exit(1)
+
+ elif command == "validate":
+ key_name = sys.argv[2] if len(sys.argv) > 2 else "kdevops-lambdalabs"
+ success, message = validate_ssh_setup(api_key, key_name)
+ print(message)
+ if not success:
+ sys.exit(1)
+
+ else:
+ print(f"Unknown command: {command}", file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/ssh_config_file_name.py b/scripts/ssh_config_file_name.py
new file mode 100755
index 0000000..9363548
--- /dev/null
+++ b/scripts/ssh_config_file_name.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+"""
+Generate a unique SSH config file name based on the current directory.
+This ensures each kdevops instance uses its own SSH config file.
+"""
+
+import hashlib
+import os
+import sys
+
+
+def get_directory_hash(path: str, length: int = 8) -> str:
+ """
+ Generate a short hash of the directory path.
+
+ Args:
+ path: Directory path to hash
+ length: Number of hex characters to use (default 8)
+
+ Returns:
+ Hex string of specified length
+ """
+ # Get the absolute path to ensure consistency
+ abs_path = os.path.abspath(path)
+
+ # Create SHA256 hash of the path
+ hash_obj = hashlib.sha256(abs_path.encode("utf-8"))
+
+ # Return first N characters of the hex digest
+ return hash_obj.hexdigest()[:length]
+
+
+def generate_ssh_config_filename(base_path: str = "~/.ssh/config_kdevops") -> str:
+ """
+ Generate a unique SSH config filename for the current directory.
+
+ Args:
+ base_path: Base path for the SSH config file (default ~/.ssh/config_kdevops)
+
+ Returns:
+ Unique SSH config filename like "~/.ssh/config_kdevops_a1b2c3d4"
+ """
+ cwd = os.getcwd()
+ dir_hash = get_directory_hash(cwd)
+
+ # Create the unique filename
+ config_file = f"{base_path}_{dir_hash}"
+
+ return config_file
+
+
+def main():
+ """Main entry point."""
+ if len(sys.argv) > 1:
+ if sys.argv[1] == "--help" or sys.argv[1] == "-h":
+ print("Usage: ssh_config_file_name.py [base_path]")
+ print()
+ print("Generate a unique SSH config filename based on current directory.")
+ print()
+ print("Options:")
+ print(
+ " base_path Base path for SSH config (default: ~/.ssh/config_kdevops)"
+ )
+ print()
+ print("Examples:")
+ print(" Default: ~/.ssh/config_kdevops_a1b2c3d4")
+ print(" Custom: /tmp/ssh_config_a1b2c3d4")
+ sys.exit(0)
+ else:
+ # Use provided base path
+ print(generate_ssh_config_filename(sys.argv[1]))
+ else:
+ print(generate_ssh_config_filename())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_ssh_keys.py b/scripts/test_ssh_keys.py
new file mode 100644
index 0000000..608268a
--- /dev/null
+++ b/scripts/test_ssh_keys.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+
+import json
+import os
+import sys
+import urllib.request
+import urllib.error
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from lambdalabs_credentials import get_api_key
+
+
+def list_ssh_keys():
+ api_key = get_api_key()
+ if not api_key:
+ print("No API key found")
+ return None
+
+ url = "https://cloud.lambdalabs.com/api/v1/ssh-keys"
+ headers = {"Authorization": f"Bearer {api_key}", "User-Agent": "kdevops/1.0"}
+
+ print(f"Attempting to list SSH keys...")
+ print(f"URL: {url}")
+ print(f"API Key prefix: {api_key[:15]}...")
+
+ try:
+ req = urllib.request.Request(url, headers=headers)
+ with urllib.request.urlopen(req) as response:
+ data = json.loads(response.read().decode())
+ return data
+ except urllib.error.HTTPError as e:
+ print(f"HTTP Error {e.code}: {e.reason}")
+ try:
+ error_body = json.loads(e.read().decode())
+ print(f"Error details: {json.dumps(error_body, indent=2)}")
+ except:
+ pass
+ return None
+ except Exception as e:
+ print(f"Error: {e}")
+ return None
+
+
+def delete_ssh_key(key_name):
+ api_key = get_api_key()
+ if not api_key:
+ print("No API key found")
+ return False
+
+ url = f"https://cloud.lambdalabs.com/api/v1/ssh-keys/{key_name}"
+ headers = {"Authorization": f"Bearer {api_key}", "User-Agent": "kdevops/1.0"}
+
+ print(f"Attempting to delete SSH key: {key_name}")
+
+ try:
+ req = urllib.request.Request(url, headers=headers, method="DELETE")
+ with urllib.request.urlopen(req) as response:
+ print(f"Successfully deleted key: {key_name}")
+ return True
+ except urllib.error.HTTPError as e:
+ print(f"HTTP Error {e.code}: {e.reason}")
+ try:
+ error_body = json.loads(e.read().decode())
+ print(f"Error details: {json.dumps(error_body, indent=2)}")
+ except:
+ pass
+ return False
+ except Exception as e:
+ print(f"Error: {e}")
+ return False
+
+
+if __name__ == "__main__":
+ # First try to list keys
+ keys_data = list_ssh_keys()
+
+ if keys_data:
+ print("\nSSH Keys found:")
+ print(json.dumps(keys_data, indent=2))
+
+ # Extract key names
+ if "data" in keys_data:
+ keys = keys_data["data"]
+ else:
+ keys = keys_data if isinstance(keys_data, list) else []
+
+ if keys:
+ print("\nAttempting to delete all keys...")
+ for key in keys:
+ if isinstance(key, dict):
+ key_name = key.get("name") or key.get("id")
+ if key_name:
+ delete_ssh_key(key_name)
+ elif isinstance(key, str):
+ delete_ssh_key(key)
+ else:
+ print("\nCould not retrieve SSH keys")
diff --git a/scripts/update_ssh_config_lambdalabs.py b/scripts/update_ssh_config_lambdalabs.py
new file mode 100755
index 0000000..66a626f
--- /dev/null
+++ b/scripts/update_ssh_config_lambdalabs.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+"""
+Update SSH config for Lambda Labs instances.
+Based on the existing SSH config update scripts.
+"""
+
+import sys
+import os
+import re
+import argparse
+
+
+def update_ssh_config(
+ action, hostname, ip_address, username, ssh_config_path, ssh_key_path, tag
+):
+ """Update or remove SSH config entries for Lambda Labs instances."""
+
+ # Normalize paths
+ ssh_config_path = (
+ os.path.expanduser(ssh_config_path) if ssh_config_path else "~/.ssh/config"
+ )
+ # For ssh_key_path, only expand and use if not None and if action is update
+ if action == "update" and ssh_key_path:
+ ssh_key_path = os.path.expanduser(ssh_key_path)
+
+ # Ensure SSH config directory exists
+ ssh_config_dir = os.path.dirname(ssh_config_path)
+ if not os.path.exists(ssh_config_dir):
+ os.makedirs(ssh_config_dir, mode=0o700)
+
+ # Read existing SSH config
+ if os.path.exists(ssh_config_path):
+ with open(ssh_config_path, "r") as f:
+ config_lines = f.readlines()
+ else:
+ config_lines = []
+
+ # Find and remove existing entry for this host
+ new_lines = []
+ skip_block = False
+ for line in config_lines:
+ if line.strip().startswith(f"Host {hostname}"):
+ skip_block = True
+ elif skip_block and line.strip().startswith("Host "):
+ skip_block = False
+
+ if not skip_block:
+ new_lines.append(line)
+
+ # Add new entry if action is update
+ if action == "update":
+ if not ssh_key_path:
+ print(f"Error: SSH key path is required for update action")
+ return
+
+ # Add Lambda Labs tag comment if not present
+ tag_comment = f"# {tag} instances\n"
+ if tag_comment not in new_lines:
+ new_lines.append(f"\n{tag_comment}")
+
+ # Add host configuration
+ host_config = f"""
+Host {hostname}
+ HostName {ip_address}
+ User {username}
+ IdentityFile {ssh_key_path}
+ StrictHostKeyChecking no
+ UserKnownHostsFile /dev/null
+ LogLevel ERROR
+"""
+ new_lines.append(host_config)
+
+ # Write updated config
+ with open(ssh_config_path, "w") as f:
+ f.writelines(new_lines)
+
+ if action == "update":
+ print(f"Updated SSH config for {hostname} ({ip_address})")
+ else:
+ print(f"Removed SSH config for {hostname}")
+
+
+def main():
+ """Main entry point."""
+ parser = argparse.ArgumentParser(
+ description="Update SSH config for Lambda Labs instances"
+ )
+ parser.add_argument(
+ "action", choices=["update", "remove"], help="Action to perform"
+ )
+ parser.add_argument("hostname", help="Hostname for the SSH config entry")
+ parser.add_argument(
+ "ip_address", nargs="?", help="IP address of the instance (required for update)"
+ )
+ parser.add_argument(
+ "username", nargs="?", help="SSH username (required for update)"
+ )
+ parser.add_argument(
+ "ssh_config_path",
+ nargs="?",
+ default="~/.ssh/config",
+ help="Path to SSH config file",
+ )
+ parser.add_argument(
+ "ssh_key_path",
+ nargs="?",
+ default=None,
+ help="Path to SSH private key",
+ )
+ parser.add_argument(
+ "tag", nargs="?", default="Lambda Labs", help="Tag for grouping instances"
+ )
+
+ args = parser.parse_args()
+
+ if args.action == "update":
+ if not args.ip_address or not args.username:
+ print("Error: IP address and username are required for update action")
+ sys.exit(1)
+ update_ssh_config(
+ args.action,
+ args.hostname,
+ args.ip_address,
+ args.username,
+ args.ssh_config_path,
+ args.ssh_key_path,
+ args.tag,
+ )
+ else:
+ # For remove action, we don't need all parameters
+ update_ssh_config(
+ args.action,
+ args.hostname,
+ None,
+ None,
+ args.ssh_config_path or "~/.ssh/config",
+ None,
+ args.tag,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/upload_ssh_key_to_lambdalabs.py b/scripts/upload_ssh_key_to_lambdalabs.py
new file mode 100755
index 0000000..06a5f03
--- /dev/null
+++ b/scripts/upload_ssh_key_to_lambdalabs.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+"""
+Upload SSH key to Lambda Labs via API.
+This script helps upload your local SSH public key to Lambda Labs.
+"""
+
+import json
+import os
+import sys
+import urllib.request
+import urllib.error
+import urllib.parse
+import lambdalabs_credentials
+
+LAMBDALABS_API_BASE = "https://cloud.lambdalabs.com/api/v1"
+
+
+def get_api_key():
+ """Get Lambda Labs API key from credentials file."""
+ # Get API key from credentials
+ api_key = lambdalabs_credentials.get_api_key()
+ if not api_key:
+ print(
+ "Error: Lambda Labs API key not found in credentials file", file=sys.stderr
+ )
+ print(
+ "Please configure it with: python3 scripts/lambdalabs_credentials.py set 'your-api-key'",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ return api_key
+
+
+def make_api_request(endpoint, api_key, method="GET", data=None):
+ """Make a request to Lambda Labs API."""
+ url = f"{LAMBDALABS_API_BASE}{endpoint}"
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
+
+ try:
+ if data:
+ data = json.dumps(data).encode("utf-8")
+
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
+ with urllib.request.urlopen(req) as response:
+ return json.loads(response.read().decode())
+ except urllib.error.HTTPError as e:
+ error_body = e.read().decode() if e.read() else "No error body"
+ print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
+ print(f"Error details: {error_body}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"Error making API request: {e}", file=sys.stderr)
+ return None
+
+
+def list_ssh_keys(api_key):
+ """List existing SSH keys."""
+ response = make_api_request("/ssh-keys", api_key)
+ if response and "data" in response:
+ return response["data"]
+ return []
+
+
+def create_ssh_key(api_key, name, public_key):
+ """Create a new SSH key."""
+ data = {"name": name, "public_key": public_key.strip()}
+ response = make_api_request("/ssh-keys", api_key, method="POST", data=data)
+ return response
+
+
+def delete_ssh_key(api_key, key_id):
+ """Delete an SSH key by ID."""
+ response = make_api_request(f"/ssh-keys/{key_id}", api_key, method="DELETE")
+ return response
+
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: python3 upload_ssh_key_to_lambdalabs.py <command> [args]")
+ print("Commands:")
+ print(" list - List all SSH keys")
+ print(" upload <name> <public_key_file> - Upload a new SSH key")
+ print(" delete <key_id> - Delete an SSH key by ID")
+ print(" check <name> - Check if key with name exists")
+ sys.exit(1)
+
+ command = sys.argv[1]
+ api_key = get_api_key()
+
+ if command == "list":
+ keys = list_ssh_keys(api_key)
+ if keys:
+ print("Existing SSH keys:")
+ for key in keys:
+ print(f" - ID: {key.get('id')}, Name: {key.get('name')}")
+ else:
+ print("No SSH keys found or unable to retrieve keys")
+
+ elif command == "upload":
+ if len(sys.argv) < 4:
+ print(
+ "Error: upload requires <name> and <public_key_file> arguments",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ name = sys.argv[2]
+ key_file = sys.argv[3]
+
+ # Check if key already exists
+ existing_keys = list_ssh_keys(api_key)
+ for key in existing_keys:
+ if key.get("name") == name:
+ print(
+ f"SSH key with name '{name}' already exists (ID: {key.get('id')})"
+ )
+ print("Use 'delete' command first if you want to replace it")
+ sys.exit(1)
+
+ # Read the public key
+ try:
+ with open(os.path.expanduser(key_file), "r") as f:
+ public_key = f.read().strip()
+ except FileNotFoundError:
+ print(f"Error: File {key_file} not found", file=sys.stderr)
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error reading file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Upload the key
+ result = create_ssh_key(api_key, name, public_key)
+ if result:
+ print(f"Successfully uploaded SSH key '{name}'")
+ if "data" in result:
+ print(f"Key ID: {result['data'].get('id')}")
+ else:
+ print("Failed to upload SSH key")
+ sys.exit(1)
+
+ elif command == "delete":
+ if len(sys.argv) < 3:
+ print("Error: delete requires <key_id> argument", file=sys.stderr)
+ sys.exit(1)
+
+ key_id = sys.argv[2]
+ result = delete_ssh_key(api_key, key_id)
+ if result is not None:
+ print(f"Successfully deleted SSH key with ID: {key_id}")
+ else:
+ print("Failed to delete SSH key")
+ sys.exit(1)
+
+ elif command == "check":
+ if len(sys.argv) < 3:
+ print("Error: check requires <name> argument", file=sys.stderr)
+ sys.exit(1)
+
+ name = sys.argv[2]
+ existing_keys = list_ssh_keys(api_key)
+ for key in existing_keys:
+ if key.get("name") == name:
+ print(f"SSH key '{name}' exists (ID: {key.get('id')})")
+ sys.exit(0)
+ print(f"SSH key '{name}' does not exist")
+ sys.exit(1)
+
+ 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-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 ` [PATCH v2 03/10] scripts: add Lambda Labs credentials management Luis Chamberlain
2025-08-27 21:28 ` Luis Chamberlain [this message]
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-5-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