mirror of
https://github.com/isjerryxiao/pacroller.git
synced 2024-11-25 17:20:41 +08:00
Merge pull request #9 from hertzyang/master
add telegram message notification support
This commit is contained in:
commit
a321dd5284
5 changed files with 104 additions and 43 deletions
13
README.md
13
README.md
|
@ -20,8 +20,8 @@ status [-v --verbose] [-m --max <number>]
|
||||||
print details of a previously successful upgrade
|
print details of a previously successful upgrade
|
||||||
reset
|
reset
|
||||||
reset the current failure status
|
reset the current failure status
|
||||||
test-smtp
|
test-mail
|
||||||
send an test email to the configured address
|
send test mails to all configured notification destinations
|
||||||
```
|
```
|
||||||
There is also a systemd timer for scheduled automatic upgrades.
|
There is also a systemd timer for scheduled automatic upgrades.
|
||||||
|
|
||||||
|
@ -47,8 +47,13 @@ Pacroller wipes /var/cache/pacman/pkg after a successful upgrade if the option "
|
||||||
### save pacman output
|
### save pacman output
|
||||||
Every time an upgrade is performed, the pacman output is stored into /var/log/pacroller. This can be configured via the "save_stdout" keyword.
|
Every time an upgrade is performed, the pacman output is stored into /var/log/pacroller. This can be configured via the "save_stdout" keyword.
|
||||||
|
|
||||||
## Smtp
|
## Notification
|
||||||
Configure `/etc/pacroller/smtp.json` to receive an email notification when an upgrade fails. Note that pacroller will not send any email if stdin is a tty (can be overridden by the `--interactive` switch).
|
When configuring your notification system, please note that pacroller will not send any notification if stdin is a tty (can be overridden by the `--interactive` switch).
|
||||||
|
Notification will be sent through all configured methods when it requires manual inspection. Currently, two notification methods are supported: SMTP and telegram
|
||||||
|
### SMTP
|
||||||
|
Configure `/etc/pacroller/smtp.json` to receive email notifications.
|
||||||
|
### Telegram
|
||||||
|
Configure `/etc/pacroller/telegram.json` to receive telegram notifications.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- Your favourite package may not be supported, however it's easy to add another set of rules.
|
- Your favourite package may not be supported, however it's easy to add another set of rules.
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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'
|
CONFIG_FILE_SMTP = 'smtp.json'
|
||||||
|
CONFIG_FILE_TG = 'telegram.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'
|
||||||
|
@ -34,6 +35,16 @@ if (smtp_cfg := (CONFIG_DIR / CONFIG_FILE_SMTP)).exists():
|
||||||
else:
|
else:
|
||||||
_smtp_config = dict()
|
_smtp_config = dict()
|
||||||
|
|
||||||
|
if (tg_cfg := (CONFIG_DIR / CONFIG_FILE_TG)).exists():
|
||||||
|
try:
|
||||||
|
_tg_cfg_text = tg_cfg.read_text()
|
||||||
|
except PermissionError:
|
||||||
|
_tg_config = dict()
|
||||||
|
else:
|
||||||
|
_tg_config: dict = json.loads(_tg_cfg_text)
|
||||||
|
else:
|
||||||
|
_tg_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)
|
||||||
|
@ -98,3 +109,8 @@ if SMTP_ENABLED:
|
||||||
SMTP_AUTH.pop('password_base64')
|
SMTP_AUTH.pop('password_base64')
|
||||||
assert SMTP_AUTH['password']
|
assert SMTP_AUTH['password']
|
||||||
SMTP_AUTH = {k:v for k, v in SMTP_AUTH.items() if k in {'username', 'password'}}
|
SMTP_AUTH = {k:v for k, v in SMTP_AUTH.items() if k in {'username', 'password'}}
|
||||||
|
|
||||||
|
TG_ENABLED = bool(_tg_config.get('enabled', False))
|
||||||
|
TG_BOT_TOKEN = _tg_config.get('bot_token', "")
|
||||||
|
TG_API_HOST = _tg_config.get('api_host', 'api.telegram.org')
|
||||||
|
TG_RECIPIENT = _tg_config.get('recipient', "")
|
||||||
|
|
|
@ -1,40 +1,32 @@
|
||||||
|
import json
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from typing import List
|
from typing import List
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.request, urllib.parse
|
||||||
from platform import node
|
from platform import node
|
||||||
from pacroller.config import NETWORK_RETRY, SMTP_ENABLED, SMTP_SSL, SMTP_HOST, SMTP_PORT, SMTP_FROM, SMTP_TO, SMTP_AUTH
|
from pacroller.config import NETWORK_RETRY, SMTP_ENABLED, SMTP_SSL, SMTP_HOST, SMTP_PORT, SMTP_FROM, SMTP_TO, SMTP_AUTH, TG_ENABLED, TG_BOT_TOKEN, TG_API_HOST, TG_RECIPIENT
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
hostname = node() or "unknown-host"
|
hostname = node() or "unknown-host"
|
||||||
|
|
||||||
class MailSender:
|
|
||||||
def __init__(self) -> None:
|
def send_email(text: str, subject: str, mailto: List[str]) -> bool:
|
||||||
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 on {hostname}", mailto: List[str] = list()) -> bool:
|
|
||||||
if not SMTP_ENABLED:
|
|
||||||
return None
|
|
||||||
for _ in range(NETWORK_RETRY):
|
for _ in range(NETWORK_RETRY):
|
||||||
try:
|
try:
|
||||||
server = self.smtp_cls(self.host, self.port)
|
smtp_cls = smtplib.SMTP_SSL if SMTP_SSL else smtplib.SMTP
|
||||||
if self.auth:
|
server = smtp_cls(SMTP_HOST, SMTP_PORT)
|
||||||
server.login(self.auth["username"], self.auth["password"])
|
if SMTP_AUTH:
|
||||||
mailto = mailto if mailto else self.mailto
|
server.login(SMTP_AUTH["username"], SMTP_AUTH["password"])
|
||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
msg.set_content(f"from pacroller running on {hostname=}:\n\n{text}")
|
msg.set_content(f"from pacroller running on {hostname=}:\n\n{text}")
|
||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg['From'] = self.mailfrom
|
msg['From'] = SMTP_FROM
|
||||||
msg['To'] = ', '.join(mailto)
|
msg['To'] = ', '.join(mailto)
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
server.quit()
|
server.quit()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error while smtp send_message")
|
logger.exception("error while send_email")
|
||||||
else:
|
else:
|
||||||
logger.debug(f"smtp sent {text=}")
|
logger.debug(f"smtp sent {text=}")
|
||||||
break
|
break
|
||||||
|
@ -43,6 +35,48 @@ class MailSender:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def send_tg_message(text: str, subject: str, mailto: List[str]) -> bool:
|
||||||
|
for _ in range(NETWORK_RETRY):
|
||||||
|
all_succeeded = True
|
||||||
|
try:
|
||||||
|
for recipient in mailto:
|
||||||
|
url = f'https://{TG_API_HOST}/bot{TG_BOT_TOKEN}/sendMessage'
|
||||||
|
headers = {'User-Agent': 'Mozilla/5.0 (compatible; Pacroller/0.1; +https://github.com/isjerryxiao/pacroller)'}
|
||||||
|
data = urllib.parse.urlencode({"chat_id": recipient, "text": f"<b>{subject}</b>\n\n<code>{text}</code>", "parse_mode": "HTML"})
|
||||||
|
req = urllib.request.Request(url, data=data.encode(), headers=headers)
|
||||||
|
resp = urllib.request.urlopen(req).read().decode('utf-8')
|
||||||
|
content = json.loads(resp)
|
||||||
|
if not content["ok"]:
|
||||||
|
all_succeeded = False
|
||||||
|
logger.error(f"unable to send telegram message to {recipient}: %s" % content['description'])
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error while send_tg_message")
|
||||||
|
else:
|
||||||
|
logger.debug(f"telegram message sent {text=}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.error(f"unable to send telegram message after {NETWORK_RETRY} attempts {text=}")
|
||||||
|
return False
|
||||||
|
return all_succeeded
|
||||||
|
|
||||||
|
|
||||||
|
class MailSender:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
def send_text_plain(elsf, text: str, subject: str = f"pacroller on {hostname}") -> bool:
|
||||||
|
if_failed = False
|
||||||
|
if SMTP_ENABLED:
|
||||||
|
if not send_email(text, subject, SMTP_TO.split()):
|
||||||
|
if_failed = True
|
||||||
|
if TG_ENABLED:
|
||||||
|
if not send_tg_message(text, subject, TG_RECIPIENT.split()):
|
||||||
|
if_failed = True
|
||||||
|
if not SMTP_ENABLED and not TG_ENABLED:
|
||||||
|
return None
|
||||||
|
return not if_failed
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(module)s - %(funcName)s - %(levelname)s - %(message)s')
|
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.")
|
MailSender().send_text_plain("This is a test mail\nIf you see this mail, your notification config is working.")
|
||||||
|
|
|
@ -288,8 +288,8 @@ def main() -> None:
|
||||||
logger.debug(f'needrestart {p.stdout=}')
|
logger.debug(f'needrestart {p.stdout=}')
|
||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser(description='Unattended Upgrades for Arch Linux')
|
parser = argparse.ArgumentParser(description='Unattended Upgrades for Arch Linux')
|
||||||
parser.add_argument('action', choices=['run', 'status', 'reset', 'fail-reset', 'reset-failed', 'test-smtp'],
|
parser.add_argument('action', choices=['run', 'status', 'reset', 'fail-reset', 'reset-failed', 'test-mail'],
|
||||||
help="what to do", metavar="run / status / reset / test-smtp")
|
help="what to do", metavar="run / status / reset / test-mail")
|
||||||
parser.add_argument('-d', '--debug', action='store_true', help='enable debug mode')
|
parser.add_argument('-d', '--debug', action='store_true', help='enable debug mode')
|
||||||
parser.add_argument('-v', '--verbose', action='store_true', help='show verbose report')
|
parser.add_argument('-v', '--verbose', action='store_true', help='show verbose report')
|
||||||
parser.add_argument('-m', '--max', type=int, default=1, help='Number of upgrades to show')
|
parser.add_argument('-m', '--max', type=int, default=1, help='Number of upgrades to show')
|
||||||
|
@ -361,12 +361,12 @@ def main() -> None:
|
||||||
if PACMAN_SCC:
|
if PACMAN_SCC:
|
||||||
clear_pkg_cache()
|
clear_pkg_cache()
|
||||||
|
|
||||||
elif args.action == 'test-smtp':
|
elif args.action == 'test-mail':
|
||||||
logger.info('sending test mail...')
|
logger.info('sending test mail...')
|
||||||
if _smtp_result := MailSender().send_text_plain("This is a test mail\nIf you see this email, your smtp config is working."):
|
if _notification_result := MailSender().send_text_plain("This is a test mail\nIf you see this mail, your notification config is working."):
|
||||||
logger.info("success")
|
logger.info("success")
|
||||||
elif _smtp_result is None:
|
elif _notification_result is None:
|
||||||
logger.warning("smtp is disabled")
|
logger.warning("no notification method is enabled")
|
||||||
else:
|
else:
|
||||||
logger.error("fail")
|
logger.error("fail")
|
||||||
|
|
||||||
|
|
6
src/pacroller/telegram.json
Normal file
6
src/pacroller/telegram.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"recipient": "",
|
||||||
|
"api_host": "api.telegram.org",
|
||||||
|
"bot_token": ""
|
||||||
|
}
|
Loading…
Reference in a new issue