aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--.vscode/launch.json10
-rw-r--r--src/conf/palhm-boot-report.service14
-rw-r--r--src/conf/py-sample/boot-report.jsonc7
-rwxr-xr-xsrc/palhm-dnssec-check.sh9
-rwxr-xr-xsrc/palhm.py32
-rw-r--r--src/palhm/__init__.py129
6 files changed, 188 insertions, 13 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 2d8e2c8..5ed76e2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -25,6 +25,16 @@
"justMyCode": true
},
{
+ "name": "palhm boot-report",
+ "type": "python",
+ "request": "launch",
+ "cwd": "${workspaceFolder}",
+ "program": "src/palhm.py",
+ "args": [ "-f", "src/conf/py-debug/palhm.jsonc", "boot-report" ],
+ "console": "integratedTerminal",
+ "justMyCode": true
+ },
+ {
"name": "palhm run default",
"type": "python",
"request": "launch",
diff --git a/src/conf/palhm-boot-report.service b/src/conf/palhm-boot-report.service
new file mode 100644
index 0000000..288aabd
--- /dev/null
+++ b/src/conf/palhm-boot-report.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=PALHM send boot report mail
+After=postfix.service sendmail.service exim.service
+
+[Service]
+Type=oneshot
+ExecStart=/var/lib/PALHM/src/palhm.py -q boot-report
+Nice=10
+ProtectSystem=strict
+ReadOnlyPaths=/
+PrivateDevices=true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/conf/py-sample/boot-report.jsonc b/src/conf/py-sample/boot-report.jsonc
new file mode 100644
index 0000000..dd9d606
--- /dev/null
+++ b/src/conf/py-sample/boot-report.jsonc
@@ -0,0 +1,7 @@
+{
+ "boot-report": {
+ // "mua": "stdout",
+ "mua": "mailx",
+ "mail-to": [ "root" ]
+ }
+}
diff --git a/src/palhm-dnssec-check.sh b/src/palhm-dnssec-check.sh
index f5ee466..4601d8e 100755
--- a/src/palhm-dnssec-check.sh
+++ b/src/palhm-dnssec-check.sh
@@ -1,7 +1,4 @@
#!/bin/bash
-set -e
-. "$( dirname -- "${BASH_SOURCE[0]}" )"/common.sh
-
do_query () {
# dig returns 0 upon successful reception and parse of the response message.
# All the other exit codes other than 0 will cause the script to terminate
@@ -11,9 +8,9 @@ do_query () {
# record will also return nothing with the status code zero.
dig +short +dnssec ANY "$TARGET" > "$tmpf"
if [ ! -s "$tmpf" ]; then
- palhm_die \
- "The nameserver returned no RR!
-DNSSEC verification probably failed."
+ echo "The nameserver returned no RR!
+DNSSEC verification probably failed." >&2
+ exit 1
fi
}
diff --git a/src/palhm.py b/src/palhm.py
index 474f0a0..f008736 100755
--- a/src/palhm.py
+++ b/src/palhm.py
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
from getopt import getopt
import palhm
+from palhm.exceptions import InvalidConfigError
class ProgConf:
@@ -113,6 +114,23 @@ class ModsCmd (Cmd):
"Usage: " + sys.argv[0] + " mods" + '''
Prints the available modules to stdout.''')
+class BootReportCmd (Cmd):
+ def __init__ (self, *args, **kwargs):
+ pass
+
+ def do_cmd (self):
+ ProgConf.alloc_ctx()
+
+ if ProgConf.ctx.boot_report is None:
+ raise InvalidConfigError("'boot-report' not configured")
+
+ return ProgConf.ctx.boot_report.do_send(ProgConf.ctx)
+
+ def print_help ():
+ print(
+"Usage: " + sys.argv[0] + " boot-report" + '''
+Send mail of boot report to recipients configured.''')
+
class HelpCmd (Cmd):
def __init__ (self, optlist, args):
self.optlist = optlist
@@ -138,11 +156,12 @@ Options:
-f FILE Load config from FILE instead of the hard-coded default
Config: ''' + ProgConf.conf + '''
Commands:
- run run a task
- config load config and print the contents
- help [CMD] print this message and exit normally if [CMD] is not specified.
- Print usage of [CMD] otherwise
- mods list available modules''')
+ run run a task
+ config load config and print the contents
+ help [CMD] print this message and exit normally if [CMD] is not specified.
+ Print usage of [CMD] otherwise
+ mods list available modules
+ boot-report mail boot report''')
return 0
@@ -150,7 +169,8 @@ CmdMap = {
"config": ConfigCmd,
"run": RunCmd,
"help": HelpCmd,
- "mods": ModsCmd
+ "mods": ModsCmd,
+ "boot-report": BootReportCmd
}
optlist, args = getopt(sys.argv[1:], "qvf:")
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