avail-mon: small enh., use apprise for notif, add .service
Signed-off-by: Martin Matous <m@matous.dev>
This commit is contained in:
parent
85a34dc79a
commit
02e59ce7e2
3 changed files with 101 additions and 62 deletions
|
|
@ -8,9 +8,9 @@ Low-budget homemade handmade monitoring for homeserver.
|
||||||
|
|
||||||
Status: active use
|
Status: active use
|
||||||
|
|
||||||
Dependencies: python, dnspython, Matrix account
|
Dependencies: python3, apprise, cryptography, dnspython, requests, Matrix account
|
||||||
|
|
||||||
Usage: Run as a daemon, `/usr/local/bin/availability-monitor.py <interval-sec>`
|
Usage: Use provided availability-monitor.service as user unit.
|
||||||
|
|
||||||
## dnf-search-install.py
|
## dnf-search-install.py
|
||||||
Wrapper, marks already installed packages for `dnf search`.
|
Wrapper, marks already installed packages for `dnf search`.
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,77 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# nonstdlib requirements: dnspython
|
# nonstdlib deps: apprise, cryptography, dnspython, requests
|
||||||
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives import serialization, hashes
|
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
import dns.resolver
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import os
|
import os
|
||||||
import requests
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import ssl
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
MATRIX_ACCESS_TOKEN = os.environ['MATRIX_ACCESS_TOKEN']
|
import apprise
|
||||||
MATRIX_NOTIFICATION_ROOM = os.environ['MATRIX_NOTIFICATION_ROOM']
|
import dns.resolver
|
||||||
CHECK_INTERVAL = int(sys.argv[1])
|
import requests
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
|
||||||
|
MATRIX_USER = os.environ['MATRIX_USER']
|
||||||
|
MATRIX_PASSWORD = os.environ['MATRIX_PASSWORD']
|
||||||
|
MATRIX_ROOM = os.environ['MATRIX_ROOM']
|
||||||
|
|
||||||
|
CHECK_INTERVAL_SEC = int(sys.argv[1])
|
||||||
|
|
||||||
MONITORED_URLS = [
|
MONITORED_URLS = [
|
||||||
|
'https://git.matous.dev',
|
||||||
|
'https://nextcloud.matous.dev',
|
||||||
|
'https://mta-sts.matous.dev/.well-known/mta-sts.txt',
|
||||||
]
|
]
|
||||||
|
|
||||||
MONITORED_MAIL = [
|
MONITORED_MAIL = ['mx1.matous.dev']
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
HTTPS_PORT = 443
|
HTTPS_PORT = 443
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
SMTPS_PORT = 465
|
SMTPS_PORT = 465
|
||||||
|
|
||||||
def check_tlsa(url: str, port: int, dercert: bytes) -> Dict[str, bytes]:
|
|
||||||
cert = x509.load_der_x509_certificate(dercert)
|
class SmtpStatus(Enum):
|
||||||
|
OK = 250
|
||||||
|
|
||||||
|
|
||||||
|
def check_tlsa(url: str, port: int, cert: x509.Certificate) -> dict[str, bytes]:
|
||||||
pubkey = cert.public_key()
|
pubkey = cert.public_key()
|
||||||
pubkey = pubkey.public_bytes(
|
pubkey_bytes = pubkey.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||||
serialization.Encoding.DER,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
digest = hashes.Hash(hashes.SHA256())
|
digest = hashes.Hash(hashes.SHA256())
|
||||||
digest.update(pubkey)
|
digest.update(pubkey_bytes)
|
||||||
keyhash = digest.finalize()
|
keyhash = digest.finalize()
|
||||||
|
|
||||||
port = str(port)
|
|
||||||
tlsa = dns.resolver.resolve(f'_{port}._tcp.{url}', 'TLSA')
|
tlsa = dns.resolver.resolve(f'_{port}._tcp.{url}', 'TLSA')
|
||||||
|
|
||||||
return {'cert': tlsa[0].cert, 'dns': keyhash}
|
return {'local': tlsa[0].cert, 'dns': keyhash}
|
||||||
|
|
||||||
|
|
||||||
# check that websites are alive
|
# check that websites are alive
|
||||||
def web_check() -> List[str]:
|
def web_check() -> list[str]:
|
||||||
errors = []
|
errors = []
|
||||||
for url in MONITORED_URLS:
|
for url in MONITORED_URLS:
|
||||||
try:
|
try:
|
||||||
res = requests.head(url)
|
res = requests.head(url, timeout=10)
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
errors.append(f'{url} HEAD returned {res.status_code}: {res.reason}')
|
errors.append(f'{url} HEAD returned {res.status_code}: {res.reason}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f'{url} check failed: {e}')
|
errors.append(f'{url} check failed: {e}')
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def mail_check() -> List[str]:
|
|
||||||
|
def mail_check() -> list[str]:
|
||||||
errors = []
|
errors = []
|
||||||
for url in MONITORED_MAIL:
|
for url in MONITORED_MAIL:
|
||||||
# check that SMTP(S) is alive
|
# check that SMTP(S) is alive
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP_SSL(url, port=SMTPS_PORT) as smtp:
|
with smtplib.SMTP_SSL(url, port=SMTPS_PORT) as smtp:
|
||||||
res = smtp.noop()
|
res = smtp.noop()
|
||||||
if res != (250, b'2.0.0 I have sucessfully done nothing'):
|
if res != (SmtpStatus.OK, b'2.0.0 I have successfully done nothing'):
|
||||||
errors.append(f'{url}:{SMTPS_PORT} check returned {res}')
|
errors.append(f'{url}:{SMTPS_PORT} check returned {res}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f'{url} SMTPS check failed: {e}')
|
errors.append(f'{url} SMTPS check failed: {e}')
|
||||||
|
|
@ -75,7 +79,7 @@ def mail_check() -> List[str]:
|
||||||
with smtplib.SMTP(url, port=SMTP_PORT) as smtp:
|
with smtplib.SMTP(url, port=SMTP_PORT) as smtp:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
res = smtp.noop()
|
res = smtp.noop()
|
||||||
if res != (250, b'2.0.0 I have sucessfully done nothing'):
|
if res != (SmtpStatus.OK, b'2.0.0 I have successfully done nothing'):
|
||||||
errors.append(f'{url}:{SMTP_PORT} check returned {res}')
|
errors.append(f'{url}:{SMTP_PORT} check returned {res}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f'{url}:{SMTP_PORT} SMTP check failed: {e}')
|
errors.append(f'{url}:{SMTP_PORT} SMTP check failed: {e}')
|
||||||
|
|
@ -94,42 +98,33 @@ def mail_check() -> List[str]:
|
||||||
with smtplib.SMTP(url, port=SMTP_PORT) as smtp:
|
with smtplib.SMTP(url, port=SMTP_PORT) as smtp:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
dercert = smtp.sock.getpeercert(binary_form=True)
|
dercert = smtp.sock.getpeercert(binary_form=True)
|
||||||
tlsa_hash = check_tlsa(url, SMTP_PORT, dercert)
|
cert = x509.load_der_x509_certificate(dercert)
|
||||||
if tlsa_hash['cert'] != tlsa_hash['dns']:
|
if cert.not_valid_after_utc < datetime.now(UTC):
|
||||||
errors.append(
|
errors.append(f'{url} certificate expired {cert.not_valid_after}')
|
||||||
f'{url}:{SMTP_PORT} TLSA record \
|
tlsa_hash = check_tlsa(url, SMTP_PORT, cert)
|
||||||
{str(tlsa_hash["cert"])} != {str(tlsa_hash["dns"])}'
|
if tlsa_hash['local'] != tlsa_hash['dns']:
|
||||||
)
|
errors.append(f'{url}:{SMTP_PORT} TLSA record {tlsa_hash["local"]!s} != {tlsa_hash["dns"]!s}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f'{url}:{SMTP_PORT} TLSA check failed: {e}')
|
errors.append(f'{url}:{SMTP_PORT} TLSA check failed: {e}')
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def report_results(errors: List[str]) -> None:
|
|
||||||
|
def report_results(errors: list[str]) -> None:
|
||||||
if not errors:
|
if not errors:
|
||||||
errors = 'All systems nominal'
|
errors = ['All systems nominal']
|
||||||
|
errors: str = '\n\n'.join(errors)
|
||||||
print(errors)
|
print(errors)
|
||||||
|
|
||||||
txn_id_nonce = str(int(time.time()))
|
apobj = apprise.Apprise()
|
||||||
url = f'https://conduit.koesters.xyz/_matrix/client/r0/rooms/{MATRIX_NOTIFICATION_ROOM}/send/m.room.message/{txn_id_nonce}'
|
with apprise.LogCapture(level=apprise.logging.INFO) as output:
|
||||||
header_dict = {
|
apobj.add(f'matrixs://{MATRIX_USER}@nitro.chat/{MATRIX_ROOM}?pass={MATRIX_PASSWORD}')
|
||||||
'Accept': 'application/json',
|
apobj.add('glib://')
|
||||||
'Authorization' : f'Bearer {MATRIX_ACCESS_TOKEN}',
|
apobj.notify(title='Server status', body=errors)
|
||||||
'Content-Type': 'application/json'
|
print(output.getvalue())
|
||||||
}
|
|
||||||
body = f'"msgtype":"m.text", "body":"{errors}"'
|
|
||||||
body = '{' + body + '}'
|
|
||||||
try:
|
|
||||||
res = requests.put(url, data=body, headers=header_dict)
|
|
||||||
if res.status_code != 200:
|
|
||||||
print(res.json())
|
|
||||||
subprocess.run(['notify-send', f'Sending error report failed.\nPlease run {sys.argv[0]}\nError {res.json()}'])
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
subprocess.run(['notify-send', f'Sending error report failed.\nPlease run {sys.argv[0]}\nError {e}'])
|
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
prev_errors = []
|
prev_errors = None
|
||||||
print('Monitoring...')
|
print('Monitoring...')
|
||||||
while True:
|
while True:
|
||||||
errors += web_check()
|
errors += web_check()
|
||||||
|
|
@ -139,5 +134,4 @@ while True:
|
||||||
prev_errors = errors.copy()
|
prev_errors = errors.copy()
|
||||||
errors = []
|
errors = []
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
time.sleep(CHECK_INTERVAL)
|
time.sleep(CHECK_INTERVAL_SEC)
|
||||||
|
|
||||||
|
|
|
||||||
45
availability-monitor.service
Normal file
45
availability-monitor.service
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Server services monitoring
|
||||||
|
After=network-online.target graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=%h/.config/private-env/availability-monitor.env
|
||||||
|
ExecStart=/usr/local/bin/availability-monitor.py 3600
|
||||||
|
|
||||||
|
AmbientCapabilities=
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
InaccessiblePaths=/home /root
|
||||||
|
KeyringMode=private
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateIPC=true
|
||||||
|
PrivateMounts=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateUsers=true
|
||||||
|
ProcSubset=pid
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
# can't override rw for %t/bus if "true" (completely inaccessible /run)
|
||||||
|
# rw necessary for notif
|
||||||
|
ProtectHome=read-only
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectProc=noaccess
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=%t/bus
|
||||||
|
# AF_UNIX for dbus (notifications), net for checking (duh)
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
UMask=0277
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Add table
Add a link
Reference in a new issue