aboutsummaryrefslogtreecommitdiffstats
path: root/hosts/hosts.py
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/hosts.py')
-rwxr-xr-xhosts/hosts.py135
1 files changed, 135 insertions, 0 deletions
diff --git a/hosts/hosts.py b/hosts/hosts.py
new file mode 100755
index 0000000..424a674
--- /dev/null
+++ b/hosts/hosts.py
@@ -0,0 +1,135 @@
+#!/usr/bin/python3
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+"""The LAN map as JSON: one entry per host note in the vault, each carrying
+the ~/.ssh/config aliases that point at it, plus one entry per alias that
+points at no note, so every alias the rofi menu listed is still here.
+
+ hosts.py print the list
+ hosts.py --check run the self-check
+"""
+
+import json
+import os
+import sys
+from pathlib import Path
+
+VAULT = os.environ.get(
+ "HOSTS_DIR", "~/Documents/Obsidian/my KB/Knowledge base/Local Network/services")
+SSH_CONFIG = "~/.ssh/config"
+
+
+def frontmatter(text):
+ """The `key: value` lines between the first two `---` lines."""
+ # ponytail: flat scalars only, no yaml lib; a list value stays its raw
+ # string, which is fine for the fields read here.
+ lines = text.splitlines()
+ if not lines or lines[0].strip() != "---":
+ return {}
+ out = {}
+ for line in lines[1:]:
+ if line.strip() == "---":
+ break
+ key, sep, value = line.partition(":")
+ if sep and key.strip() and not key.startswith(" "):
+ out[key.strip()] = value.strip().strip("\"'")
+ return out
+
+
+def ssh_hosts(text):
+ """Aliases in an ssh config: {alias, hostname, user, port}."""
+ # ponytail: plain "Keyword value" lines only, like blackpearl-sshmenu.sh;
+ # Include and Match are not followed, wildcard Host patterns are skipped.
+ hosts, cur = [], None
+ for line in text.splitlines():
+ parts = line.split(None, 1)
+ if len(parts) < 2 or parts[0].startswith("#"):
+ continue
+ key, value = parts[0].lower(), parts[1].strip()
+ if key == "host":
+ name = value.split()[0]
+ cur = None if any(c in name for c in "*?") else {
+ "alias": name, "hostname": name, "user": "", "port": ""}
+ if cur:
+ hosts.append(cur)
+ elif cur and key in ("hostname", "user", "port"):
+ cur[key] = value
+ return hosts
+
+
+def merge(notes, aliases):
+ """notes: [(title, frontmatter)]. Each alias joins the first note whose ip
+ or hostname is its HostName, or whose short hostname is the alias."""
+ out = []
+ for title, fm in notes:
+ out.append({
+ "name": title,
+ "ip": fm.get("ip", ""),
+ "port": fm.get("port", ""),
+ "url": fm.get("url", ""),
+ "hostname": fm.get("hostname", ""),
+ "node": fm.get("node", ""),
+ "ct_id": fm.get("ct_id", ""),
+ "aliases": [],
+ "user": "",
+ })
+ for a in aliases:
+ target = a["hostname"]
+ host = next((h for h in out if target and target in (h["ip"], h["hostname"])
+ or a["alias"] == h["hostname"].split(".")[0]), None)
+ if host:
+ host["aliases"].append(a["alias"])
+ host["user"] = host["user"] or a["user"]
+ else:
+ out.append({
+ "name": a["alias"], "ip": target, "port": a["port"], "url": "",
+ "hostname": target, "node": "", "ct_id": "",
+ "aliases": [a["alias"]], "user": a["user"],
+ })
+ return out
+
+
+def main():
+ notes = [(p.stem, frontmatter(p.read_text(errors="replace")))
+ for p in sorted(Path(VAULT).expanduser().glob("*.md"))]
+ cfg = Path(SSH_CONFIG).expanduser()
+ aliases = ssh_hosts(cfg.read_text(errors="replace")) if cfg.exists() else []
+ json.dump(merge(notes, aliases), sys.stdout)
+
+
+def check():
+ fm = frontmatter("---\ntags: [a, b]\nip: 10.0.0.7\nport: 5000\n"
+ "node: HP server\n---\nbody: not this\n")
+ assert fm == {"tags": "[a, b]", "ip": "10.0.0.7", "port": "5000",
+ "node": "HP server"}, fm
+ assert frontmatter("no frontmatter\n") == {}
+
+ cfg = ssh_hosts("Host *\n User x\nHost box\n HostName 10.0.0.7\n"
+ " User root\nHost web\n HostName web.example.org\n"
+ "Host lone\n Port 2222\n")
+ assert [h["alias"] for h in cfg] == ["box", "web", "lone"], cfg
+ assert cfg[0]["user"] == "root" and cfg[2]["port"] == "2222"
+
+ hosts = merge([("Box", fm), ("Web", {"hostname": "web.example.org"}),
+ ("Media", {"ip": "10.0.0.9", "hostname": "media.example.org"})],
+ cfg + ssh_hosts("Host media\n HostName 10.0.0.99\n"))
+ by = {h["name"]: h for h in hosts}
+ assert by["Box"]["aliases"] == ["box"] and by["Box"]["user"] == "root"
+ assert by["Web"]["aliases"] == ["web"]
+ assert by["Media"]["aliases"] == ["media"], "short hostname match"
+ assert by["lone"]["port"] == "2222" and by["lone"]["node"] == ""
+ assert len(hosts) == 4
+ print("ok")
+
+
+if __name__ == "__main__":
+ check() if "--check" in sys.argv else main()