mirror of
https://github.com/isjerryxiao/pacroller.git
synced 2024-11-22 15:50:43 +08:00
add smtp
This commit is contained in:
parent
2d4fb825a1
commit
c07831944c
5 changed files with 102 additions and 3 deletions
|
@ -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
46
src/pacroller/mailer.py
Normal 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.")
|
|
@ -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
15
src/pacroller/smtp.json
Normal 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=="
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue