mirror of
https://codeberg.org/privacy1st/exec-notify
synced 2024-12-22 23:16:04 +01:00
v0.1.5
add usage and improve doc; lazy config loading;
This commit is contained in:
parent
9eafc7bb77
commit
19d4188205
24
README.md
24
README.md
@ -53,9 +53,26 @@ sudo python3 -m pip install --upgrade --index-url https://test.pypi.org/simple/
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Create configuration file `/etc/execNotify/cfg.ini`.
|
Create configuration file `/etc/execNotify/cfg.ini`:
|
||||||
|
|
||||||
For the required fields, see [./etc/execNotify/cfg.ini.example](etc/execNotify/cfg.ini.example)
|
```shell
|
||||||
|
install -Dm0600 /etc/execNotify/cfg.ini << 'EOF'
|
||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
[mail]
|
||||||
|
host = smtp.example.com
|
||||||
|
port = 465
|
||||||
|
password = examplePassword
|
||||||
|
|
||||||
|
from = noreply@example.com
|
||||||
|
to = me@example.com
|
||||||
|
|
||||||
|
[file]
|
||||||
|
maildir = /home/exampleUser/mail/
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
See also: [./etc/execNotify/cfg.ini.example](etc/execNotify/cfg.ini.example)
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -64,9 +81,6 @@ For the required fields, see [./etc/execNotify/cfg.ini.example](etc/execNotify/c
|
|||||||
|
|
||||||
Add `de-p1st-execNotify` in front of your command to execute.
|
Add `de-p1st-execNotify` in front of your command to execute.
|
||||||
|
|
||||||
You can pipe into `de-p1st-execNotify` but **not** from `de-p1st-execNotify`
|
|
||||||
as the output gets modified.
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
`de-p1st-execNotify ls /non/existent/file` will mail you the exit code, stdout and stderr of `ls /non/existent/file`
|
`de-p1st-execNotify ls /non/existent/file` will mail you the exit code, stdout and stderr of `ls /non/existent/file`
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
name = de.p1st.exec_notify
|
name = de.p1st.exec_notify
|
||||||
version = 0.1.4
|
version = 0.1.5
|
||||||
author = Daniel Langbein
|
author = Daniel Langbein
|
||||||
author_email = daniel@systemli.org
|
author_email = daniel@systemli.org
|
||||||
description = Send mail with process output
|
description = mail notification if command fails
|
||||||
long_description = file: README.md
|
long_description = file: README.md
|
||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
url = https://codeberg.org/privacy1st/execNotify
|
url = https://codeberg.org/privacy1st/execNotify
|
||||||
|
@ -8,6 +8,15 @@ from de.p1st.exec_notify.lib import exec, config, mail, util
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
de-p1st-execNotify <command>
|
||||||
|
|
||||||
|
with command = <executable> [<argument> ...]
|
||||||
|
|
||||||
|
:return: exit code of executed command
|
||||||
|
"""
|
||||||
|
|
||||||
if len(argv) <= 1:
|
if len(argv) <= 1:
|
||||||
print('No command given to execute!', file=sys.stderr)
|
print('No command given to execute!', file=sys.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
@ -15,7 +24,7 @@ def main():
|
|||||||
exit(executeCommand(argv[1:]))
|
exit(executeCommand(argv[1:]))
|
||||||
|
|
||||||
|
|
||||||
def executeCommand(command: List) -> int:
|
def executeCommand(command: List[str]) -> int:
|
||||||
"""
|
"""
|
||||||
Executes the given command and sends an email on failure.
|
Executes the given command and sends an email on failure.
|
||||||
|
|
||||||
|
@ -1,60 +1,80 @@
|
|||||||
import sys
|
import traceback
|
||||||
|
from sys import stderr
|
||||||
from pathlib import Path, PosixPath
|
from pathlib import Path, PosixPath
|
||||||
import configparser
|
import configparser
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from de.p1st.exec_notify.lib import util
|
from de.p1st.exec_notify.lib import util
|
||||||
|
|
||||||
|
|
||||||
def getHostAndPort():
|
class LazyConfig:
|
||||||
|
# (static) class variable
|
||||||
|
_config: configparser.ConfigParser = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get(section: str, key: str):
|
||||||
|
if LazyConfig._config is None:
|
||||||
|
LazyConfig._config = configparser.ConfigParser()
|
||||||
|
LazyConfig._config.read(_getCfgFile())
|
||||||
|
return LazyConfig._config[section][key]
|
||||||
|
|
||||||
|
|
||||||
|
def getHostAndPort() -> Tuple[str, str]:
|
||||||
try:
|
try:
|
||||||
return config['mail']['host'], config['mail']['port']
|
return LazyConfig.get('mail', 'host'), LazyConfig.get('mail', 'port')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not read value [mail][host] from {_getCfgFile()}', file=sys.stderr)
|
print(f'execNotify>> Could not read value [mail][host] from {_getCfgFile()}', file=stderr)
|
||||||
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def getPassword():
|
def getPassword() -> str:
|
||||||
try:
|
try:
|
||||||
return config['mail']['password']
|
return LazyConfig.get('mail', 'password')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not read value [mail][password] from {_getCfgFile()}', file=sys.stderr)
|
print(f'execNotify>> Could not read value [mail][password] from {_getCfgFile()}', file=stderr)
|
||||||
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def getFrom():
|
def getFrom() -> str:
|
||||||
try:
|
try:
|
||||||
return config['mail']['from'] # used for mail login as well
|
return LazyConfig.get('mail', 'from') # used for mail login as well
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not read value [mail][from] from {_getCfgFile()}', file=sys.stderr)
|
print(f'execNotify>> Could not read value [mail][from] from {_getCfgFile()}', file=stderr)
|
||||||
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def getTo():
|
def getTo() -> str:
|
||||||
try:
|
try:
|
||||||
return config['mail']['to']
|
return LazyConfig.get('mail', 'to')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not read value [mail][to] from {_getCfgFile()}', file=sys.stderr)
|
print(f'execNotify>> Could not read value [mail][to] from {_getCfgFile()}', file=stderr)
|
||||||
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def getMailDir() -> Path:
|
def getMailDir() -> Path:
|
||||||
try:
|
try:
|
||||||
return Path(config['file']['maildir'])
|
return Path(LazyConfig.get('file', 'maildir'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not read value [file][maildir] from {_getCfgFile()}', file=sys.stderr)
|
print(f'execNotify>> Could not read value [file][maildir] from {_getCfgFile()}', file=stderr)
|
||||||
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Helper Methods
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
def _getCfgFile() -> Path:
|
def _getCfgFile() -> Path:
|
||||||
return _getCfgDir().joinpath('cfg.ini')
|
return _getCfgDir().joinpath('cfg.ini')
|
||||||
|
|
||||||
|
|
||||||
def _getCfgDir() -> Path:
|
def _getCfgDir() -> Path:
|
||||||
if util.isInDevelopment():
|
if util.isInDevelopment():
|
||||||
return util.getProjectBase().joinpath('etc','execNotify')
|
return util.getProjectBase().joinpath('etc', 'execNotify')
|
||||||
else:
|
else:
|
||||||
return PosixPath('/etc/execNotify/')
|
return PosixPath('/etc/execNotify/')
|
||||||
|
|
||||||
|
|
||||||
config: configparser.ConfigParser = configparser.ConfigParser()
|
|
||||||
config.read(_getCfgFile())
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
import sys
|
from sys import stdin
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
def execute(command: List[str]):
|
def execute(command: List[str]):
|
||||||
"""
|
"""
|
||||||
Run the given command in a subprocess and pass stdin (if given) to that process.
|
Runs the given command in a subprocess and passes stdin (if given) to that process.
|
||||||
Wait for command to complete.
|
Waits for command to complete.
|
||||||
|
|
||||||
:param command: A command to executed as list of words, e.g. ['echo', 'Hello world!']
|
:param command: A command to executed as list of words, e.g. ['echo', 'Hello world!']
|
||||||
:return: (exit_code, stdout, stderr)
|
:return: (exit_code, stdout, stderr)
|
||||||
@ -14,7 +14,7 @@ def execute(command: List[str]):
|
|||||||
|
|
||||||
completed: subprocess.CompletedProcess = subprocess.run(
|
completed: subprocess.CompletedProcess = subprocess.run(
|
||||||
command,
|
command,
|
||||||
stdin=sys.stdin,
|
stdin=stdin,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False,
|
check=False,
|
||||||
text=True
|
text=True
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import sys
|
from sys import stderr
|
||||||
import datetime
|
import datetime
|
||||||
import smtplib, ssl
|
import smtplib, ssl
|
||||||
import socket
|
import socket
|
||||||
@ -10,9 +10,9 @@ from de.p1st.exec_notify.lib import config, util
|
|||||||
|
|
||||||
|
|
||||||
def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = True):
|
def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = True):
|
||||||
mailDir = config.getMailDir()
|
|
||||||
|
|
||||||
if informAboutLocalMail and _localMailExists():
|
if informAboutLocalMail and _localMailExists():
|
||||||
|
mailDir = config.getMailDir()
|
||||||
|
|
||||||
SUBJECT=f'{SUBJECT} | UNREAD LOCAL MAIL'
|
SUBJECT=f'{SUBJECT} | UNREAD LOCAL MAIL'
|
||||||
BODY=f'[!] Note [!]\n' \
|
BODY=f'[!] Note [!]\n' \
|
||||||
f'There is local mail inside {mailDir} that was not delivered previously! ' \
|
f'There is local mail inside {mailDir} that was not delivered previously! ' \
|
||||||
@ -22,8 +22,8 @@ def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool =
|
|||||||
try:
|
try:
|
||||||
sendMail(SUBJECT=SUBJECT, BODY=BODY)
|
sendMail(SUBJECT=SUBJECT, BODY=BODY)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'execNotify>> Could not send mail: {e}', file=sys.stderr)
|
print(f'execNotify>> Could not send mail: {e}', file=stderr)
|
||||||
print(f'execNotify>> Writing to file instead ...', file=sys.stderr)
|
print(f'execNotify>> Writing to file instead ...', file=stderr)
|
||||||
|
|
||||||
# Instead, try to save the mail so that the user can read
|
# Instead, try to save the mail so that the user can read
|
||||||
# it later when connected to this computer
|
# it later when connected to this computer
|
||||||
@ -32,7 +32,7 @@ def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool =
|
|||||||
|
|
||||||
def sendMail(SUBJECT: str, BODY: str):
|
def sendMail(SUBJECT: str, BODY: str):
|
||||||
"""
|
"""
|
||||||
:throws Exception: If mail could not be sent
|
:raises Exception: If mail could not be sent
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FROM = config.getFrom()
|
FROM = config.getFrom()
|
||||||
@ -92,8 +92,10 @@ def _localMailExists():
|
|||||||
"""
|
"""
|
||||||
:return: True if local mail exists in maildir folder. Once the mail is read the user shall delete (or move) it.
|
:return: True if local mail exists in maildir folder. Once the mail is read the user shall delete (or move) it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mailDir = config.getMailDir()
|
mailDir = config.getMailDir()
|
||||||
if not mailDir.exists():
|
if not mailDir.exists():
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
# mailDir has at least one child (file/folder)
|
||||||
return len(list(mailDir.iterdir())) > 0
|
return len(list(mailDir.iterdir())) > 0
|
||||||
|
@ -6,7 +6,7 @@ def isInDevelopment() -> bool:
|
|||||||
"""
|
"""
|
||||||
Helpful for local development where this python module is not installed.
|
Helpful for local development where this python module is not installed.
|
||||||
|
|
||||||
:return: True if environment variable DE_P1ST_EXEC_NOTIFY is set.
|
:returns: True if environment variable DE_P1ST_EXEC_NOTIFY is set.
|
||||||
"""
|
"""
|
||||||
return 'DE_P1ST_EXEC_NOTIFY' in os.environ
|
return 'DE_P1ST_EXEC_NOTIFY' in os.environ
|
||||||
|
|
||||||
@ -18,13 +18,19 @@ def getProjectBase() -> Path:
|
|||||||
def readFirstLine(file: Path) -> str:
|
def readFirstLine(file: Path) -> str:
|
||||||
"""
|
"""
|
||||||
:param file: Path to file
|
:param file: Path to file
|
||||||
:return: first line of file
|
:returns: first line of file
|
||||||
"""
|
"""
|
||||||
with open(file, "r") as f:
|
with open(file, "r") as f:
|
||||||
return f.readline()
|
return f.readline()
|
||||||
|
|
||||||
|
|
||||||
def appendLinePrefix(prefix: str, s: str) -> str:
|
def appendLinePrefix(prefix: str, s: str) -> str:
|
||||||
|
"""
|
||||||
|
Append `prefix` to the left of each line in `s`
|
||||||
|
|
||||||
|
:returns: modified `s`
|
||||||
|
"""
|
||||||
|
|
||||||
if s is None or len(s) == 0:
|
if s is None or len(s) == 0:
|
||||||
return prefix
|
return prefix
|
||||||
result = ''.join([prefix + line for line in s.splitlines(keepends=True)])
|
result = ''.join([prefix + line for line in s.splitlines(keepends=True)])
|
||||||
|
@ -7,10 +7,16 @@ from de.p1st.exec_notify.lib import exec, config, mail
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
echo <body> | ./notify
|
Send an email with some content and optionally specify an email subject.
|
||||||
echo <body> | ./notify <subject>
|
|
||||||
./notify <subject> <body>
|
Usage:
|
||||||
./notify <subject> <body_word_1> <body_word_2> ... <body_word_n>
|
echo <body> | de-p1st-notify
|
||||||
|
|
||||||
|
echo <body> | de-p1st-notify <subject>
|
||||||
|
|
||||||
|
de-p1st-notify <body>
|
||||||
|
|
||||||
|
de-p1st-notify <subject> <body>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
BODY = None
|
BODY = None
|
||||||
@ -18,7 +24,7 @@ def main():
|
|||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
if len(argv) > 3:
|
if len(argv) > 3:
|
||||||
print('execNotify>> Expected at most two arguments!')
|
print('execNotify>> Expected at most two arguments!', file=stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
if len(argv) == 2 or len(argv) == 3:
|
if len(argv) == 2 or len(argv) == 3:
|
||||||
subj = argv[1]
|
subj = argv[1]
|
||||||
@ -31,6 +37,7 @@ def main():
|
|||||||
BODY = stdin.read()
|
BODY = stdin.read()
|
||||||
|
|
||||||
SUBJECT = f'{hostname} | {subj}'
|
SUBJECT = f'{hostname} | {subj}'
|
||||||
|
print(SUBJECT)
|
||||||
print(BODY)
|
print(BODY)
|
||||||
|
|
||||||
mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
|
mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
|
||||||
|
Loading…
Reference in New Issue
Block a user