This commit is contained in:
JerryXiao 2021-07-01 11:37:54 +08:00
parent 2d4fb825a1
commit c07831944c
Signed by: Jerry
GPG key ID: 22618F758B5BE2E5
5 changed files with 102 additions and 3 deletions

View file

@ -1,11 +1,13 @@
import json import json
from pathlib import Path from pathlib import Path
import importlib.util import importlib.util
from base64 import b64decode
import sys import sys
from typing import Any from typing import Any
CONFIG_DIR = Path('/etc/pacroller') CONFIG_DIR = Path('/etc/pacroller')
CONFIG_FILE = 'config.json' CONFIG_FILE = 'config.json'
CONFIG_FILE_SMTP = 'smtp.json'
F_KNOWN_OUTPUT_OVERRIDE = 'known_output_override.py' F_KNOWN_OUTPUT_OVERRIDE = 'known_output_override.py'
LIB_DIR = Path('/var/lib/pacroller') LIB_DIR = Path('/var/lib/pacroller')
DB_FILE = 'db' DB_FILE = 'db'
@ -21,6 +23,11 @@ if (cfg := (CONFIG_DIR / CONFIG_FILE)).exists():
else: else:
_config = dict() _config = dict()
if (smtp_cfg := (CONFIG_DIR / CONFIG_FILE_SMTP)).exists():
_smtp_config: dict = json.loads(smtp_cfg.read_text())
else:
_smtp_config = dict()
def _import_module(fpath: Path) -> Any: def _import_module(fpath: Path) -> Any:
spec = importlib.util.spec_from_file_location(str(fpath).removesuffix('.py').replace('/', '.'), fpath) spec = importlib.util.spec_from_file_location(str(fpath).removesuffix('.py').replace('/', '.'), fpath)
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
@ -64,3 +71,23 @@ for i in NEEDRESTART_CMD:
SYSTEMD = bool(_config.get('systemd-check', True)) SYSTEMD = bool(_config.get('systemd-check', True))
PACMAN_SCC = bool(_config.get('clear_pkg_cache', False)) PACMAN_SCC = bool(_config.get('clear_pkg_cache', False))
SMTP_ENABLED = bool(_smtp_config.get('enabled', False))
SMTP_SSL = bool(_smtp_config.get('ssl', True))
SMTP_HOST = _smtp_config.get('host', "")
SMTP_PORT = int(_smtp_config.get('port', 0))
SMTP_FROM = _smtp_config.get('from', "")
SMTP_TO = _smtp_config.get('to', "")
SMTP_AUTH = dict(_smtp_config.get('auth', {}))
if SMTP_ENABLED:
assert SMTP_HOST
assert SMTP_FROM
assert SMTP_TO
assert 0 <= SMTP_PORT <= 65536
if SMTP_AUTH:
assert SMTP_AUTH['username']
if _smtp_auth_b64 := SMTP_AUTH.get('password_base64', ''):
SMTP_AUTH['password'] = b64decode(_smtp_auth_b64).decode('utf-8')
SMTP_AUTH.pop('password_base64')
assert SMTP_AUTH['password']
SMTP_AUTH = {k:v for k, v in SMTP_AUTH.items() if k in {'username', 'password'}}

46
src/pacroller/mailer.py Normal file
View file

@ -0,0 +1,46 @@
import smtplib
from email.message import EmailMessage
from typing import List
import logging
from platform import node
from pacroller.config import NETWORK_RETRY, SMTP_ENABLED, SMTP_SSL, SMTP_HOST, SMTP_PORT, SMTP_FROM, SMTP_TO, SMTP_AUTH
logger = logging.getLogger()
hostname = node() or "unknown-host"
class MailSender:
def __init__(self) -> None:
self.host = SMTP_HOST
self.port = SMTP_PORT
self.ssl = SMTP_SSL
self.auth = SMTP_AUTH
self.mailfrom = SMTP_FROM
self.mailto = SMTP_TO.split()
self.smtp_cls = smtplib.SMTP_SSL if self.ssl else smtplib.SMTP
def send_text_plain(self, text: str, subject: str = f"pacroller from {hostname}", mailto: List[str] = list()) -> None:
if not SMTP_ENABLED:
return
for _ in range(NETWORK_RETRY):
try:
server = self.smtp_cls(self.host, self.port)
if self.auth:
server.login(self.auth["username"], self.auth["password"])
mailto = mailto if mailto else self.mailto
msg = EmailMessage()
msg.set_content(f"from pacroller running on {hostname=}:\n\n{text}")
msg['Subject'] = subject
msg['From'] = self.mailfrom
msg['To'] = ', '.join(mailto)
server.send_message(msg)
server.quit()
except Exception:
logger.exception("error while smtp send_message")
else:
logger.debug(f"smtp sent {text=}")
break
else:
logger.error(f"unable to send email after {NETWORK_RETRY} attempts {text=}")
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(module)s - %(funcName)s - %(levelname)s - %(message)s')
MailSender().send_text_plain("This is a test mail\nIf you see this email, your smtp config is working.")

View file

@ -16,6 +16,7 @@ from pacroller.config import (CONFIG_DIR, CONFIG_FILE, LIB_DIR, DB_FILE, PACMAN_
TIMEOUT, UPGRADE_TIMEOUT, NETWORK_RETRY, CUSTOM_SYNC, SYNC_SH, TIMEOUT, UPGRADE_TIMEOUT, NETWORK_RETRY, CUSTOM_SYNC, SYNC_SH,
EXTRA_SAFE, SHELL, HOLD, NEEDRESTART, NEEDRESTART_CMD, SYSTEMD, EXTRA_SAFE, SHELL, HOLD, NEEDRESTART, NEEDRESTART_CMD, SYSTEMD,
PACMAN_PKG_DIR, PACMAN_SCC, PACMAN_DB_LCK, SAVE_STDOUT, LOG_DIR) PACMAN_PKG_DIR, PACMAN_SCC, PACMAN_DB_LCK, SAVE_STDOUT, LOG_DIR)
from pacroller.mailer import MailSender
logger = logging.getLogger() logger = logging.getLogger()
@ -279,6 +280,7 @@ def main() -> None:
logger.handlers[0].setLevel(logging.INFO) logger.handlers[0].setLevel(logging.INFO)
locale_set() locale_set()
interactive = args.interactive == "on" or not (args.interactive == 'off' or not isatty(0)) interactive = args.interactive == "on" or not (args.interactive == 'off' or not isatty(0))
send_mail = MailSender().send_text_plain if not interactive else lambda *_, **_: None
logger.debug(f"interactive questions {'enabled' if interactive else 'disabled'}") logger.debug(f"interactive questions {'enabled' if interactive else 'disabled'}")
if args.action == 'run': if args.action == 'run':
@ -286,26 +288,35 @@ def main() -> None:
logger.error('you need to be root') logger.error('you need to be root')
exit(1) exit(1)
if prev_err := has_previous_error(): if prev_err := has_previous_error():
logger.error(f'Cannot continue, a previous error {prev_err} is still present. Please resolve this issue and run reset.') _err = f'Cannot continue, a previous error {prev_err} is still present. Please resolve this issue and run reset.'
logger.error(_err)
send_mail(_err)
exit(2) exit(2)
if SYSTEMD: if SYSTEMD:
if _s := is_system_failed(): if _s := is_system_failed():
logger.error(f'systemd is in {_s} state, refused') _err = f'systemd is in {_s} state, refused'
logger.error(_err)
send_mail(_err)
exit(11) exit(11)
if Path(PACMAN_DB_LCK).exists(): if Path(PACMAN_DB_LCK).exists():
logger.error(f'Database is locked at {PACMAN_DB_LCK}') _err = f'Database is locked at {PACMAN_DB_LCK}'
logger.error(_err)
send_mail(_err)
exit(2) exit(2)
try: try:
report = do_system_upgrade(debug=args.debug, interactive=interactive) report = do_system_upgrade(debug=args.debug, interactive=interactive)
except NonFatal: except NonFatal:
send_mail(f"NonFatal Error:\n{traceback.format_exc()}")
raise raise
except Exception as e: except Exception as e:
write_db(None, e) write_db(None, e)
send_mail(f"Fatal Error:\n{traceback.format_exc()}")
raise raise
else: else:
exc = CheckFailed('manual inspection required') if report.failed else None exc = CheckFailed('manual inspection required') if report.failed else None
write_db(report, exc) write_db(report, exc)
if exc: if exc:
send_mail(f"{exc}\n\n{report.summary(verbose=args.verbose, show_package=False)}")
exit(2) exit(2)
if NEEDRESTART: if NEEDRESTART:
run_needrestart() run_needrestart()

15
src/pacroller/smtp.json Normal file
View file

@ -0,0 +1,15 @@
{
"enabled": false,
"ssl": true,
"host": "smtp.example.com",
"port": 465,
"from": "me@example.com",
"to": "you1@example.com you2@example.com",
"auth": {
"comment1": "## if you don't want any kind of authorization, delete the whole auth section",
"comment2": "## you can have either one of the two following password keys",
"username": "myname",
"password": "mypassword",
"password_base64": "bXlwYXNzd29yZA=="
}
}