add usage and improve doc; lazy config loading;
This commit is contained in:
Daniel Langbein 2021-11-23 19:18:38 +01:00
parent 9eafc7bb77
commit 19d4188205
8 changed files with 103 additions and 45 deletions

View File

@ -53,9 +53,26 @@ sudo python3 -m pip install --upgrade --index-url https://test.pypi.org/simple/
## 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
@ -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.
You can pipe into `de-p1st-execNotify` but **not** from `de-p1st-execNotify`
as the output gets modified.
**Example:**
`de-p1st-execNotify ls /non/existent/file` will mail you the exit code, stdout and stderr of `ls /non/existent/file`

View File

@ -3,10 +3,10 @@
[metadata]
name = de.p1st.exec_notify
version = 0.1.4
version = 0.1.5
author = Daniel Langbein
author_email = daniel@systemli.org
description = Send mail with process output
description = mail notification if command fails
long_description = file: README.md
long_description_content_type = text/markdown
url = https://codeberg.org/privacy1st/execNotify

View File

@ -8,6 +8,15 @@ from de.p1st.exec_notify.lib import exec, config, mail, util
def main():
"""
Usage:
de-p1st-execNotify <command>
with command = <executable> [<argument> ...]
:return: exit code of executed command
"""
if len(argv) <= 1:
print('No command given to execute!', file=sys.stderr)
exit(1)
@ -15,7 +24,7 @@ def main():
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.

View File

@ -1,60 +1,80 @@
import sys
import traceback
from sys import stderr
from pathlib import Path, PosixPath
import configparser
from typing import Tuple
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:
return config['mail']['host'], config['mail']['port']
return LazyConfig.get('mail', 'host'), LazyConfig.get('mail', 'port')
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)
def getPassword():
def getPassword() -> str:
try:
return config['mail']['password']
return LazyConfig.get('mail', 'password')
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)
def getFrom():
def getFrom() -> str:
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:
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)
def getTo():
def getTo() -> str:
try:
return config['mail']['to']
return LazyConfig.get('mail', 'to')
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)
def getMailDir() -> Path:
try:
return Path(config['file']['maildir'])
return Path(LazyConfig.get('file', 'maildir'))
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)
#
# Helper Methods
#
def _getCfgFile() -> Path:
return _getCfgDir().joinpath('cfg.ini')
def _getCfgDir() -> Path:
if util.isInDevelopment():
return util.getProjectBase().joinpath('etc','execNotify')
return util.getProjectBase().joinpath('etc', 'execNotify')
else:
return PosixPath('/etc/execNotify/')
config: configparser.ConfigParser = configparser.ConfigParser()
config.read(_getCfgFile())

View File

@ -1,12 +1,12 @@
from typing import List
import sys
from sys import stdin
import subprocess
def execute(command: List[str]):
"""
Run the given command in a subprocess and pass stdin (if given) to that process.
Wait for command to complete.
Runs the given command in a subprocess and passes stdin (if given) to that process.
Waits for command to complete.
:param command: A command to executed as list of words, e.g. ['echo', 'Hello world!']
:return: (exit_code, stdout, stderr)
@ -14,7 +14,7 @@ def execute(command: List[str]):
completed: subprocess.CompletedProcess = subprocess.run(
command,
stdin=sys.stdin,
stdin=stdin,
capture_output=True,
check=False,
text=True

View File

@ -1,4 +1,4 @@
import sys
from sys import stderr
import datetime
import smtplib, ssl
import socket
@ -10,9 +10,9 @@ from de.p1st.exec_notify.lib import config, util
def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = True):
mailDir = config.getMailDir()
if informAboutLocalMail and _localMailExists():
mailDir = config.getMailDir()
SUBJECT=f'{SUBJECT} | UNREAD LOCAL MAIL'
BODY=f'[!] Note [!]\n' \
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:
sendMail(SUBJECT=SUBJECT, BODY=BODY)
except Exception as e:
print(f'execNotify>> Could not send mail: {e}', file=sys.stderr)
print(f'execNotify>> Writing to file instead ...', file=sys.stderr)
print(f'execNotify>> Could not send mail: {e}', file=stderr)
print(f'execNotify>> Writing to file instead ...', file=stderr)
# Instead, try to save the mail so that the user can read
# 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):
"""
:throws Exception: If mail could not be sent
:raises Exception: If mail could not be sent
"""
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.
"""
mailDir = config.getMailDir()
if not mailDir.exists():
return False
else:
# mailDir has at least one child (file/folder)
return len(list(mailDir.iterdir())) > 0

View File

@ -6,7 +6,7 @@ def isInDevelopment() -> bool:
"""
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
@ -18,13 +18,19 @@ def getProjectBase() -> Path:
def readFirstLine(file: Path) -> str:
"""
:param file: Path to file
:return: first line of file
:returns: first line of file
"""
with open(file, "r") as f:
return f.readline()
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:
return prefix
result = ''.join([prefix + line for line in s.splitlines(keepends=True)])

View File

@ -7,10 +7,16 @@ from de.p1st.exec_notify.lib import exec, config, mail
def main():
"""
echo <body> | ./notify
echo <body> | ./notify <subject>
./notify <subject> <body>
./notify <subject> <body_word_1> <body_word_2> ... <body_word_n>
Send an email with some content and optionally specify an email subject.
Usage:
echo <body> | de-p1st-notify
echo <body> | de-p1st-notify <subject>
de-p1st-notify <body>
de-p1st-notify <subject> <body>
"""
BODY = None
@ -18,7 +24,7 @@ def main():
hostname = socket.gethostname()
if len(argv) > 3:
print('execNotify>> Expected at most two arguments!')
print('execNotify>> Expected at most two arguments!', file=stderr)
exit(1)
if len(argv) == 2 or len(argv) == 3:
subj = argv[1]
@ -31,6 +37,7 @@ def main():
BODY = stdin.read()
SUBJECT = f'{hostname} | {subj}'
print(SUBJECT)
print(BODY)
mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)