aboutsummaryrefslogtreecommitdiff
path: root/src/palhm/__init__.py
diff options
context:
space:
mode:
authorDavid Timber <dxdt@dev.snart.me>2022-05-15 21:31:54 +0800
committerDavid Timber <dxdt@dev.snart.me>2022-05-15 21:31:54 +0800
commitf0e3d3ef8f9dad895f7b6bc1768630f0ef17912e (patch)
tree8fed439b2aab75f091c41e5778cbc72ac2c1f20a /src/palhm/__init__.py
parent7d2345ecae07e4b792c60f7d36fa78bb2dc69706 (diff)
Add boot-report subcmd ...
- Fix palhm-dnssec-check.sh - Make "tasks" config optional so that PALHM can be used only for boot-report
Diffstat (limited to 'src/palhm/__init__.py')
-rw-r--r--src/palhm/__init__.py129
1 files changed, 128 insertions, 1 deletions
diff --git a/src/palhm/__init__.py b/src/palhm/__init__.py
index c227a3e..e34e58a 100644
--- a/src/palhm/__init__.py
+++ b/src/palhm/__init__.py
@@ -1,3 +1,8 @@
+import platform
+import sys
+import time
+
+import yaml
from .exceptions import InvalidConfigError
import io
import json
@@ -64,6 +69,10 @@ class GlobalContext:
self.l = logging.getLogger("palhm")
self.l.setLevel(self.vl)
+ self.boot_report = (
+ BootReport(jobj["boot-report"]) if "boot-report" in jobj
+ else None)
+
if self.nb_workers == 0:
self.nb_workers = DEFAULT.NB_WORKERS.value
elif self.nb_workers < 0:
@@ -104,6 +113,120 @@ class GlobalContext:
("task_map:\n" + "\n".join([ (i[0] + ":\n" + str(i[1])).replace("\n", "\n\t") for i in self.task_map.items() ])).replace("\n", "\n\t")
]).replace("\t", " ")
+class BootReport:
+ def _hostname () -> str:
+ return platform.node()
+
+ def _do_format (x: str) -> str:
+ return x.format(
+ hostname = BootReport._hostname()
+ )
+
+ def _default_subject () -> str:
+ return "Boot Report from {hostname}"
+
+ def _fmt_yaml_comment_header (x: str) -> str:
+ ret = list[str]()
+
+ for i in x.splitlines():
+ ret.append("# " + i)
+
+ return "\n".join(ret)
+
+ def _default_header () -> str:
+ return (
+ "This is a boot report from {hostname}.\n" +
+ "More details as follows.")
+
+ def __init__ (self, jobj: dict):
+ mua = jobj["mua"]
+ if mua == "mailx": self._mua_f = self._do_send_mailx
+ elif mua == "stdout": self._mua_f = self._do_send_stdout
+ else: raise InvalidConfigError("Unsupported MUA", mua)
+
+ self.recipients = jobj["mail-to"]
+ self.subject = jobj.get("subject", BootReport._default_subject())
+ self.header = jobj.get("header", BootReport._default_header())
+ self.uptime_since = jobj.get("uptime-since", True)
+ self.uptime = jobj.get("uptime", True)
+ self.bootid = jobj.get("boot-id", True)
+ self.systemd_analyze = jobj.get("systemd-analyze", True)
+
+ def get_subject (self) -> str:
+ return BootReport._do_format(self.subject)
+
+ def compose_body (self, ctx: GlobalContext):
+ body = {}
+ root_doc = { "boot-report": body }
+
+ yield BootReport._fmt_yaml_comment_header(
+ BootReport._do_format(self.header)) + "\n"
+
+ body["hostname"] = BootReport._hostname()
+ body["tz"] = list(time.tzname) + [time.timezone]
+
+ if self.uptime_since:
+ p = subprocess.run(
+ [ "/bin/uptime", "--since" ],
+ stdin = subprocess.DEVNULL,
+ capture_output = True)
+ if p.returncode != 0:
+ raise ChildProcessError("uptime-since", p.returncode)
+ body["uptime-since"] = p.stdout.decode().strip()
+
+ if self.uptime:
+ p = subprocess.run(
+ [ "/bin/uptime", "-p" ],
+ stdin = subprocess.DEVNULL,
+ capture_output = True)
+ if p.returncode != 0:
+ raise ChildProcessError("uptime", p.returncode)
+ body["uptime"] = p.stdout.decode().strip()
+
+ if self.bootid:
+ with open("/proc/sys/kernel/random/boot_id") as f:
+ body["bood-id"] = f.readline(36)
+
+ if self.systemd_analyze:
+ p = subprocess.run(
+ [ "/bin/systemd-analyze" ],
+ stdin = subprocess.DEVNULL,
+ capture_output = True)
+ if p.returncode != 0:
+ raise ChildProcessError("systemd-analyze", p.returncode)
+ body["systemd-analyze"] = p.stdout.decode().strip()
+
+ yield yaml.dump(root_doc)
+
+ def do_send (self, ctx: GlobalContext) -> int:
+ return self._mua_f(ctx)
+
+ def _do_send_mailx (self, ctx: GlobalContext) -> int:
+ argv = [ "/bin/mailx", "-s", self.get_subject() ] + self.recipients
+
+ with subprocess.Popen(
+ argv,
+ stdin = subprocess.PIPE,
+ stdout = subprocess.DEVNULL,
+ stderr = subprocess.PIPE) as p:
+ for d in self.compose_body(ctx):
+ p.stdin.write(d.encode())
+ p.stdin.close()
+
+ return p.wait()
+
+ def _do_send_stdout (self, ctx: GlobalContext) -> int:
+ sys.stdout.write(self.get_subject() + "\n")
+
+ for r in self.recipients:
+ sys.stdout.write(r + "\n")
+ sys.stdout.write("\n")
+
+ for d in self.compose_body(ctx):
+ sys.stdout.write(d)
+
+ return 0
+
class Runnable (ABC):
@abstractmethod
def run (self, ctx: GlobalContext):
@@ -710,6 +833,10 @@ def merge_conf (a: dict, b: dict) -> dict:
c = chk_dup_id("tasks", a, b)
if c:
raise KeyError("Dup tasks", c)
+ # "boot-report" conflict
+ if "boot-report" in a and "boot-report" in b:
+ raise InvalidConfigError(
+ "'boot-report' already defined in the previous config")
ret = a | b
ret["execs"] = a.get("execs", []) + b.get("execs", [])
@@ -764,7 +891,7 @@ def setup_conf (jobj: dict) -> GlobalContext:
for i in jobj.get("execs", iter(())):
ret.exec_map[i["id"]] = Exec(i)
- for i in jobj["tasks"]:
+ for i in jobj.get("tasks", iter(())):
ret.task_map[i["id"]] = TaskClassMap[i["type"]](ret, i)
return ret