diff --git a/app/cli/automate.py b/app/cli/automate.py index 21e0b00..6ab0905 100644 --- a/app/cli/automate.py +++ b/app/cli/automate.py @@ -6,6 +6,7 @@ from traceback import TracebackException from app import app from app.extensions import db +from app.models.activity import Activity from app.models.automation import Automation, AutomationState, AutomationLogs from app.terraform import BaseAutomation from app.terraform.block_bridge_github import BlockBridgeGitHubAutomation @@ -102,6 +103,11 @@ def run_job(job: BaseAutomation, *, force: bool = False, ignore_schedule: bool = log.updated = datetime.datetime.utcnow() log.logs = json.dumps(logs) db.session.add(log) + activity = Activity( + activity_type="automation", + text=f"FLASH! Automation Failure: {automation.short_name}. See logs for details." + ) + activity.notify() # Notify before commit because the failure occurred even if we can't commit. automation.last_run = datetime.datetime.utcnow() db.session.commit() diff --git a/app/models/__init__.py b/app/models/__init__.py index 784b65e..a448384 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -38,6 +38,13 @@ class AbstractResource(db.Model): deprecation_reason = db.Column(db.String(), nullable=True) destroyed = db.Column(db.DateTime(), nullable=True) + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.added is None: + self.added = datetime.utcnow() + if self.updated is None: + self.updated = datetime.utcnow() + def deprecate(self, *, reason: str): self.deprecated = datetime.utcnow() self.deprecation_reason = reason diff --git a/app/models/activity.py b/app/models/activity.py new file mode 100644 index 0000000..9a4b87b --- /dev/null +++ b/app/models/activity.py @@ -0,0 +1,46 @@ +import datetime + +import requests + +from app.models import AbstractConfiguration +from app.extensions import db + + +class Activity(db.Model): + id = db.Column(db.Integer(), primary_key=True) + group_id = db.Column(db.Integer(), nullable=True) + activity_type = db.Column(db.String(20), nullable=False) + text = db.Column(db.Text(), nullable=False) + added = db.Column(db.DateTime(), nullable=False) + + def __init__(self, **kwargs): + if type(kwargs["activity_type"]) != str or len(kwargs["activity_type"]) > 20 or kwargs["activity_type"] == "": + raise TypeError("expected string for activity type between 1 and 20 characters") + if type(kwargs["text"]) != str: + raise TypeError("expected string for text") + if "added" not in kwargs: + kwargs["added"] = datetime.datetime.utcnow() + super().__init__(**kwargs) + + def notify(self) -> int: + count = 0 + hooks = Webhook.query.filter( + Webhook.destroyed == None + ) + for hook in hooks: + hook.send(self.text) + count += 1 + return count + + +class Webhook(AbstractConfiguration): + format = db.Column(db.String(20)) + url = db.Column(db.String(255)) + + def send(self, text: str): + if self.format == "telegram": + data = {"text": text} + else: + # Matrix as default + data = {"body": text} + r = requests.post(self.url, json=data) diff --git a/app/portal/__init__.py b/app/portal/__init__.py index ac2987a..432219b 100644 --- a/app/portal/__init__.py +++ b/app/portal/__init__.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta, timezone from flask import Blueprint, render_template, request from sqlalchemy import desc, or_ +from app.models.activity import Activity from app.models.alarms import Alarm from app.models.mirrors import Origin, Proxy from app.models.base import Group @@ -16,6 +17,7 @@ from app.portal.list import bp as list_ from app.portal.origin import bp as origin from app.portal.onion import bp as onion from app.portal.proxy import bp as proxy +from app.portal.webhook import bp as webhook portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static") portal.register_blueprint(automation, url_prefix="/automation") @@ -27,6 +29,7 @@ portal.register_blueprint(list_, url_prefix="/list") portal.register_blueprint(origin, url_prefix="/origin") portal.register_blueprint(onion, url_prefix="/onion") portal.register_blueprint(proxy, url_prefix="/proxy") +portal.register_blueprint(webhook, url_prefix="/webhook") @portal.app_template_filter("mirror_expiry") @@ -57,8 +60,9 @@ def portal_home(): s: len(Alarm.query.filter(Alarm.alarm_state == s.upper(), Alarm.last_updated > (now - timedelta(days=1))).all()) for s in ["critical", "warning", "ok", "unknown"] } + activity = Activity.query.filter(Activity.added > (now - timedelta(days=2))).order_by(desc(Activity.added)).all() return render_template("home.html.j2", section="home", groups=groups, last24=last24, last72=last72, - lastweek=lastweek, proxies=proxies, **alarms) + lastweek=lastweek, proxies=proxies, **alarms, activity=activity) @portal.route("/search") diff --git a/app/portal/templates/base.html.j2 b/app/portal/templates/base.html.j2 index c763d4f..76c2065 100644 --- a/app/portal/templates/base.html.j2 +++ b/app/portal/templates/base.html.j2 @@ -114,6 +114,12 @@ {{ icon("file-earmark-excel") }} Block Lists +
{{ a.text }} | +{{ a.added | format_datetime }} | +