fix: streamable-http (hyphen, not underscore) in FastMCP transport
This commit is contained in:
@@ -333,4 +333,4 @@ def ping_service(service: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run(transport="streamable_http")
|
mcp.run(transport="streamable-http")
|
||||||
|
|||||||
208
ssh/gen-config.py
Normal file
208
ssh/gen-config.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate ~/.ssh/config.d/homelab from inventory.yaml.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 ssh/gen-config.py # print to stdout
|
||||||
|
python3 ssh/gen-config.py --install # write to ~/.ssh/config.d/homelab
|
||||||
|
|
||||||
|
The generated config provides short hostname aliases for every host in
|
||||||
|
the homelab inventory. LAN IPs are preferred (they work directly on-LAN
|
||||||
|
and are routed via Netbird 192.168.8.0/24 off-LAN); mesh FQDNs are
|
||||||
|
available as <name>-mesh fallbacks for roaming workstations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
print("PyYAML required (pip install pyyaml)", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
INVENTORY_PATH = Path(os.environ.get(
|
||||||
|
"HOMELAB_CONTEXT_DIR", "/opt/homelab-context"
|
||||||
|
)) / "inventory.yaml"
|
||||||
|
|
||||||
|
SSH_CONFIG_DIR = Path.home() / ".ssh" / "config.d"
|
||||||
|
SSH_CONFIG_FILE = SSH_CONFIG_DIR / "homelab"
|
||||||
|
SSH_MAIN_CONFIG = Path.home() / ".ssh" / "config"
|
||||||
|
INCLUDE_LINE = f"Include ~/.ssh/config.d/homelab"
|
||||||
|
|
||||||
|
# Hosts to skip in the generated config
|
||||||
|
SKIP_HOSTS = {"ludo-mini", "authentik"} # offline / not enrolled
|
||||||
|
|
||||||
|
|
||||||
|
def inventory() -> dict:
|
||||||
|
if not INVENTORY_PATH.exists():
|
||||||
|
print(f"ERROR: no inventory at {INVENTORY_PATH}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return yaml.safe_load(INVENTORY_PATH.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
def gen_config() -> str:
|
||||||
|
inv = inventory()
|
||||||
|
hosts = inv.get("hosts", {})
|
||||||
|
mesh_globals = inv.get("mesh", {})
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append("# Homelab SSH config — generated from inventory.yaml")
|
||||||
|
lines.append(f"# Source: {INVENTORY_PATH}")
|
||||||
|
lines.append("# Do not edit by hand. Run: homelab ssh-config")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# --- Common defaults (before any Host block) ---
|
||||||
|
lines.append("# --- Defaults ---")
|
||||||
|
lines.append("Host *")
|
||||||
|
lines.append(" IdentityFile ~/.ssh/id_ed25519")
|
||||||
|
lines.append(" IdentitiesOnly yes")
|
||||||
|
lines.append(" ServerAliveInterval 30")
|
||||||
|
lines.append(" StrictHostKeyChecking accept-new")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
def add_entry(
|
||||||
|
tag: str,
|
||||||
|
hostname: str,
|
||||||
|
user: str = "root",
|
||||||
|
port: int = 22,
|
||||||
|
extra_lines: list[str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
lines.append(f"Host {tag}")
|
||||||
|
lines.append(f" HostName {hostname}")
|
||||||
|
lines.append(f" User {user}")
|
||||||
|
if port != 22:
|
||||||
|
lines.append(f" Port {port}")
|
||||||
|
if extra_lines:
|
||||||
|
lines.extend(f" {el}" for el in extra_lines)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Sort hosts by kind for a logical output order
|
||||||
|
def sort_key(item):
|
||||||
|
name, h = item
|
||||||
|
kind = h.get("kind", "")
|
||||||
|
order = {
|
||||||
|
"workstation": 0,
|
||||||
|
"proxmox-host": 1,
|
||||||
|
"lxc": 2,
|
||||||
|
"vm": 3,
|
||||||
|
"external": 4,
|
||||||
|
}
|
||||||
|
return (order.get(kind, 9), name)
|
||||||
|
|
||||||
|
sorted_hosts = sorted(
|
||||||
|
[(n, h) for n, h in hosts.items() if n not in SKIP_HOSTS],
|
||||||
|
key=sort_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, h in sorted_hosts:
|
||||||
|
kind = h.get("kind", "")
|
||||||
|
ssh_config = h.get("ssh", {}) or {}
|
||||||
|
user = ssh_config.get("user", "root")
|
||||||
|
port = ssh_config.get("port", 22)
|
||||||
|
lan_ip = h.get("lan_ip")
|
||||||
|
mesh = h.get("mesh", {}) or {}
|
||||||
|
nb = mesh.get("netbird", {}) or {}
|
||||||
|
ts = mesh.get("tailscale", {}) or {}
|
||||||
|
nb_fqdn = nb.get("fqdn") or nb.get("ip") or ""
|
||||||
|
nb_port = ssh_config.get("netbird_port")
|
||||||
|
|
||||||
|
# Determine primary address
|
||||||
|
if nb_port and not lan_ip:
|
||||||
|
# Hosts with netbird SSH but no LAN IP: mesh-only
|
||||||
|
primary = nb_fqdn
|
||||||
|
elif lan_ip:
|
||||||
|
primary = lan_ip
|
||||||
|
elif nb_fqdn:
|
||||||
|
primary = nb_fqdn
|
||||||
|
elif ts.get("fqdn"):
|
||||||
|
primary = ts["fqdn"]
|
||||||
|
else:
|
||||||
|
continue # no address found
|
||||||
|
|
||||||
|
extra = []
|
||||||
|
if kind == "proxmox-host":
|
||||||
|
extra = [
|
||||||
|
"ControlMaster auto",
|
||||||
|
"ControlPath ~/.ssh/cm/%C",
|
||||||
|
"ControlPersist 2h",
|
||||||
|
]
|
||||||
|
|
||||||
|
add_entry(name, primary, user, port, extra_lines=extra)
|
||||||
|
|
||||||
|
# Mesh fallback for workstations and hubris
|
||||||
|
if nb_fqdn and (kind == "workstation" or nb_port):
|
||||||
|
mesh_tag = f"{name}-mesh"
|
||||||
|
mesh_port = nb_port or port
|
||||||
|
mesh_extra = []
|
||||||
|
if kind == "proxmox-host":
|
||||||
|
mesh_extra = [
|
||||||
|
"ControlMaster auto",
|
||||||
|
"ControlPath ~/.ssh/cm/%C",
|
||||||
|
"ControlPersist 2h",
|
||||||
|
]
|
||||||
|
add_entry(mesh_tag, nb_fqdn, user, mesh_port, extra_lines=mesh_extra)
|
||||||
|
|
||||||
|
# --- *.hubris.network ControlMaster ---
|
||||||
|
lines.append("# --- Mesh ControlMaster (speeds up repeated mesh ops) ---")
|
||||||
|
lines.append("Host *.netbird.selfhosted")
|
||||||
|
lines.append(" ControlMaster auto")
|
||||||
|
lines.append(" ControlPath ~/.ssh/cm/%C")
|
||||||
|
lines.append(" ControlPersist 2h")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_include() -> bool:
|
||||||
|
"""Add 'Include ~/.ssh/config.d/homelab' to the main SSH config if missing."""
|
||||||
|
if not SSH_MAIN_CONFIG.exists():
|
||||||
|
SSH_MAIN_CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(SSH_MAIN_CONFIG, "w") as f:
|
||||||
|
f.write(f"{INCLUDE_LINE}\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
content = SSH_MAIN_CONFIG.read_text()
|
||||||
|
for line in content.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("Include") and "homelab" in stripped:
|
||||||
|
return False # already present
|
||||||
|
|
||||||
|
# Prepend to existing config
|
||||||
|
updated = f"{INCLUDE_LINE}\n\n{content}"
|
||||||
|
SSH_MAIN_CONFIG.write_text(updated)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def install() -> None:
|
||||||
|
"""Write config to ~/.ssh/config.d/homelab and ensure Include is set."""
|
||||||
|
SSH_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
config = gen_config()
|
||||||
|
SSH_CONFIG_FILE.write_text(config)
|
||||||
|
SSH_CONFIG_FILE.chmod(0o644)
|
||||||
|
changed = ensure_include()
|
||||||
|
print(f"Wrote {SSH_CONFIG_FILE} ({len(config.splitlines())} lines)")
|
||||||
|
if changed:
|
||||||
|
print(f"Added '{INCLUDE_LINE}' to {SSH_MAIN_CONFIG}")
|
||||||
|
print("Done. Run: ssh <hostname> (e.g. ssh gitea)")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
|
p.add_argument("--install", "-i", action="store_true",
|
||||||
|
help=f"write to {SSH_CONFIG_FILE} and wire Include into main config")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
if args.install:
|
||||||
|
install()
|
||||||
|
else:
|
||||||
|
print(gen_config())
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user