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/__init__.py +6 -0
- dron/__main__.py +5 -0
- dron/api.py +106 -0
- dron/cli.py +382 -0
- dron/common.py +174 -0
- dron/conftest.py +18 -0
- dron/dron.py +510 -0
- dron/launchd.py +415 -0
- dron/launchd_wrapper.py +90 -0
- dron/monitor.py +157 -0
- dron/notify/common.py +53 -0
- dron/notify/email.py +43 -0
- dron/notify/ntfy_common.py +24 -0
- dron/notify/ntfy_desktop.py +15 -0
- dron/notify/ntfy_telegram.py +12 -0
- dron/notify/telegram.py +42 -0
- dron/py.typed +0 -0
- dron/systemd.py +542 -0
- dron/tests/test_dron.py +119 -0
- dron-0.1.20241008.dist-info/LICENSE.txt +21 -0
- dron-0.1.20241008.dist-info/METADATA +47 -0
- dron-0.1.20241008.dist-info/RECORD +25 -0
- dron-0.1.20241008.dist-info/WHEEL +5 -0
- dron-0.1.20241008.dist-info/entry_points.txt +2 -0
- dron-0.1.20241008.dist-info/top_level.txt +1 -0
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()
|
dron/notify/telegram.py
ADDED
@@ -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
|