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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.