diff options
Diffstat (limited to 'hosts/hosts.py')
| -rwxr-xr-x | hosts/hosts.py | 135 |
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() |
