dron 0.1.20241008__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dron/notify/common.py ADDED
@@ -0,0 +1,53 @@
1
+ import argparse
2
+ import platform
3
+ import shlex
4
+ import sys
5
+ from subprocess import PIPE, STDOUT, Popen, check_output
6
+ from typing import Iterator
7
+
8
+ IS_SYSTEMD = platform.system() != 'Darwin' # if not systemd it's launchd
9
+
10
+
11
+ def get_parser() -> argparse.ArgumentParser:
12
+ p = argparse.ArgumentParser()
13
+ p.add_argument('--job', required=True)
14
+ p.add_argument('--stdin', action='store_true')
15
+ return p
16
+
17
+
18
+ def get_stdin() -> Iterator[bytes]:
19
+ yield from sys.stdin.buffer
20
+
21
+
22
+ def get_last_systemd_log(job: str) -> Iterator[bytes]:
23
+ # output unit status
24
+ cmd = ['systemctl', '--user', 'status', '--no-pager', job, '-o', 'cat']
25
+ yield b'$ ' + ' '.join(map(shlex.quote, cmd)).encode('utf8') + b'\n\n'
26
+ with Popen(cmd, stdout=PIPE, stderr=STDOUT) as po:
27
+ out = po.stdout
28
+ assert out is not None
29
+ yield from out
30
+ rc = po.poll()
31
+ assert rc in {
32
+ 0,
33
+ 3, # 3 means failure due to job exit code
34
+ }, rc
35
+
36
+ # for logs, we used to use --lines 1000000 in systemctl status
37
+ # however, from around 2024 it stated consuming too much time
38
+ # (as if it actually retrieved 1000000 lines and only then tooks the ones relevant to the unit??)
39
+
40
+ cmd = ['systemctl', '--user', 'show', job, '-p', 'InvocationID', '--value']
41
+ invocation_id = check_output(cmd, text=True)
42
+ invocation_id = invocation_id.strip() # for some reason dumps multiple lines?
43
+ assert len(invocation_id) > 0 # just in case, todo maybe make defensive?
44
+
45
+ yield b'\n'
46
+ cmd = ['journalctl', '--no-pager', f'_SYSTEMD_INVOCATION_ID={invocation_id}']
47
+ yield b'$ ' + ' '.join(map(shlex.quote, cmd)).encode('utf8') + b'\n\n'
48
+ with Popen(cmd, stdout=PIPE, stderr=STDOUT) as po:
49
+ out = po.stdout
50
+ assert out is not None
51
+ yield from out
52
+ rc = po.poll()
53
+ assert rc == 0
dron/notify/email.py ADDED
@@ -0,0 +1,43 @@
1
+ import socket
2
+ from subprocess import PIPE, Popen
3
+ from typing import Iterator
4
+
5
+ from .common import get_last_systemd_log, get_parser, get_stdin
6
+
7
+
8
+ def send_payload(payload: Iterator[bytes]) -> None:
9
+ with Popen(['sendmail', '-t'], stdin=PIPE) as po:
10
+ stdin = po.stdin
11
+ assert stdin is not None
12
+ for line in payload:
13
+ stdin.write(line)
14
+ stdin.flush()
15
+ rc = po.poll()
16
+ assert rc == 0, rc
17
+
18
+
19
+ def send_email(*, to: str, job: str, stdin: bool) -> None:
20
+ def payload() -> Iterator[bytes]:
21
+ hostname = socket.gethostname()
22
+ yield f'''
23
+ To: {to}
24
+ From: dron <root@{hostname}>
25
+ Subject: {job}
26
+ Content-Transfer-Encoding: 8bit
27
+ Content-Type: text/plain; charset=UTF-8
28
+ '''.lstrip().encode('utf8')
29
+ last_log = get_stdin() if stdin else get_last_systemd_log(job)
30
+ yield from last_log
31
+
32
+ send_payload(payload())
33
+
34
+
35
+ def main() -> None:
36
+ p = get_parser()
37
+ p.add_argument('--to', required=True)
38
+ args = p.parse_args()
39
+ send_email(to=args.to, job=args.job, stdin=args.stdin)
40
+
41
+
42
+ if __name__ == '__main__':
43
+ main()
@@ -0,0 +1,24 @@
1
+ """
2
+ uses https://github.com/dschep/ntfy
3
+ """
4
+
5
+ import logging
6
+ import socket
7
+ import subprocess
8
+ import sys
9
+ from typing import NoReturn
10
+
11
+
12
+ def run_ntfy(*, job: str, backend: str) -> NoReturn:
13
+ # TODO not sure what to do with --stdin arg here?
14
+ # could probably use last N lines of log or something
15
+ # TODO get last logs here?
16
+ title = f'dron[{socket.gethostname()}]: {job} failed'
17
+ body = title
18
+ try:
19
+ subprocess.check_call(['ntfy', '-b', backend, '-t', title, 'send', body])
20
+ except Exception as e:
21
+ logging.exception(e)
22
+ # TODO fallback on email?
23
+ sys.exit(1)
24
+ sys.exit(0)
@@ -0,0 +1,15 @@
1
+ from .common import IS_SYSTEMD, get_parser
2
+ from .ntfy_common import run_ntfy
3
+
4
+ BACKEND = 'linux' if IS_SYSTEMD else 'darwin'
5
+
6
+
7
+ def main() -> None:
8
+ p = get_parser()
9
+ args = p.parse_args()
10
+
11
+ run_ntfy(job=args.job, backend=BACKEND)
12
+
13
+
14
+ if __name__ == '__main__':
15
+ main()
@@ -0,0 +1,12 @@
1
+ from .common import get_parser
2
+ from .ntfy_common import run_ntfy
3
+
4
+
5
+ def main() -> None:
6
+ p = get_parser()
7
+ args = p.parse_args()
8
+ run_ntfy(job=args.job, backend='telegram')
9
+
10
+
11
+ if __name__ == '__main__':
12
+ main()
@@ -0,0 +1,42 @@
1
+ """
2
+ uses telegram-send for Telegram notifications
3
+ make sure to run "telegram-send --configure" beforehand!
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import socket
9
+ import sys
10
+
11
+ from .common import get_last_systemd_log, get_parser, get_stdin
12
+
13
+
14
+ def send(*, message: str) -> None:
15
+ import telegram_send # type: ignore[import-untyped]
16
+
17
+ asyncio.run(telegram_send.send(messages=[message]))
18
+
19
+
20
+ def main() -> None:
21
+ p = get_parser()
22
+ args = p.parse_args()
23
+
24
+ job: str = args.job
25
+ stdin: bool = args.stdin
26
+
27
+ body = f'dron[{socket.gethostname()}]: {job} failed'
28
+
29
+ last_log = get_stdin() if stdin else get_last_systemd_log(job)
30
+ body += '\n' + '\n'.join(l.decode('utf8') for l in last_log)
31
+
32
+ try:
33
+ send(message=body)
34
+ except Exception as e:
35
+ logging.exception(e)
36
+ # TODO fallback on email?
37
+ sys.exit(1)
38
+ sys.exit(0)
39
+
40
+
41
+ if __name__ == '__main__':
42
+ main()
dron/py.typed ADDED
File without changes