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


  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