aboutsummaryrefslogtreecommitdiffstats
path: root/hosts/hosts.py
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/hosts.py')
-rwxr-xr-xhosts/hosts.py88
1 files changed, 83 insertions, 5 deletions
diff --git a/hosts/hosts.py b/hosts/hosts.py
index 424a674..0d1b664 100755
--- a/hosts/hosts.py
+++ b/hosts/hosts.py
@@ -15,13 +15,23 @@ 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 --probe print {name: reachable} for the same list
hosts.py --check run the self-check
+
+Each entry carries a `group`: the Proxmox node it runs on, its own name if
+other notes name it as their node, "Devices" for anything else on a private
+address, "Remote" for the rest.
"""
+import ipaddress
import json
import os
+import socket
+import subprocess
import sys
+from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
+from urllib.parse import urlsplit
VAULT = os.environ.get(
"HOSTS_DIR", "~/Documents/Obsidian/my KB/Knowledge base/Local Network/services")
@@ -95,15 +105,69 @@ def merge(notes, aliases):
"hostname": target, "node": "", "ct_id": "",
"aliases": [a["alias"]], "user": a["user"],
})
- return out
-
-
-def main():
+ return group(out)
+
+
+def private(ip):
+ try:
+ return ipaddress.ip_address(ip).is_private
+ except ValueError:
+ return False
+
+
+def group(hosts):
+ """Set each host's group and sort: nodes in name order, each with its own
+ card first, then Devices, then Remote."""
+ nodes = sorted({h["node"] for h in hosts if h["node"]})
+ for h in hosts:
+ h["group"] = (h["node"] or (h["name"] if h["name"] in nodes else "")
+ or ("Devices" if private(h["ip"]) else "Remote"))
+ order = nodes + ["Devices", "Remote"]
+ # sorted() is stable, so within a group the note order holds.
+ return sorted(hosts, key=lambda h: (order.index(h["group"]),
+ h["name"] != h["group"]))
+
+
+def target(h):
+ """What to probe: (host, tcp port), port None meaning ICMP. A service is
+ up when its port answers, not merely when its CT does."""
+ url = urlsplit(h["url"]) if h["url"] else None
+ port = h["port"] or (url and (url.port or {"http": 80, "https": 443}.get(url.scheme)))
+ return h["ip"] or h["hostname"], int(port) if port else None
+
+
+def reachable(h):
+ host, port = target(h)
+ if not host:
+ return False
+ if port:
+ try:
+ socket.create_connection((host, port), timeout=1).close()
+ return True
+ except OSError:
+ return False
+ return subprocess.run(["ping", "-c1", "-W1", host], stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL).returncode == 0
+
+
+def probe(hosts):
+ # ponytail: one thread per host, ~1s for the whole list; a pool cap
+ # if the list ever reaches the hundreds.
+ with ThreadPoolExecutor(max_workers=max(1, len(hosts))) as pool:
+ return dict(zip((h["name"] for h in hosts), pool.map(reachable, hosts)))
+
+
+def load():
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)
+ return merge(notes, aliases)
+
+
+def main():
+ hosts = load()
+ json.dump(probe(hosts) if "--probe" in sys.argv else hosts, sys.stdout)
def check():
@@ -128,6 +192,20 @@ def check():
assert by["Media"]["aliases"] == ["media"], "short hostname match"
assert by["lone"]["port"] == "2222" and by["lone"]["node"] == ""
assert len(hosts) == 4
+ assert by["lone"]["group"] == "Remote" and by["Box"]["group"] == "HP server"
+
+ g = group([{"name": "dev", "node": "", "ip": "10.0.0.3"},
+ {"name": "svc", "node": "N", "ip": "10.0.0.4"},
+ {"name": "far", "node": "", "ip": "example.org"},
+ {"name": "N", "node": "", "ip": "10.0.0.2"}])
+ assert [(h["name"], h["group"]) for h in g] == [
+ ("N", "N"), ("svc", "N"), ("dev", "Devices"), ("far", "Remote")], g
+
+ t = lambda **kw: target({"ip": "", "hostname": "", "port": "", "url": "", **kw})
+ assert t(ip="10.0.0.7", port="5000") == ("10.0.0.7", 5000)
+ assert t(ip="10.0.0.7", url="https://x.example.org") == ("10.0.0.7", 443)
+ assert t(ip="10.0.0.7", url="http://x.example.org:81") == ("10.0.0.7", 81)
+ assert t(hostname="example.org") == ("example.org", None)
print("ok")