automation: establish an automation framework

This commit is contained in:
Iain Learmonth 2022-05-08 17:20:04 +01:00
parent 1b53bf451c
commit 8abe5d60fa
31 changed files with 586 additions and 274 deletions

View file

@ -8,6 +8,7 @@ from app.models.alarms import Alarm
from app import Origin, Proxy
from app.models.base import Group, MirrorList
from app.portal.forms import LifecycleForm, NewMirrorListForm
from app.portal.automation import bp as automation
from app.portal.bridgeconf import bp as bridgeconf
from app.portal.bridge import bp as bridge
from app.portal.group import bp as group
@ -17,7 +18,7 @@ from app.portal.proxy import bp as proxy
from app.portal.util import response_404, view_lifecycle
portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")
portal.register_blueprint(automation, url_prefix="/automation")
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
portal.register_blueprint(bridge, url_prefix="/bridge")
portal.register_blueprint(group, url_prefix="/group")

70
app/portal/automation.py Normal file
View file

@ -0,0 +1,70 @@
from datetime import datetime
from flask import render_template, flash, Response, Blueprint
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import SubmitField, BooleanField
from wtforms.validators import DataRequired
from app.extensions import db
from app.models.automation import Automation
from app.portal.util import view_lifecycle, response_404
bp = Blueprint("automation", __name__)
class EditAutomationForm(FlaskForm):
enabled = BooleanField('Enabled')
submit = SubmitField('Save Changes')
@bp.route("/list")
def automation_list():
automations = Automation.query.filter(
Automation.destroyed == None).order_by(Automation.description).all()
return render_template("list.html.j2",
section="automation",
title="Automation Jobs",
item="automation",
items=automations)
@bp.route('/edit/<automation_id>', methods=['GET', 'POST'])
def automation_edit(automation_id):
automation = Automation.query.filter(Automation.id == automation_id).first()
if automation is None:
return Response(render_template("error.html.j2",
section="automation",
header="404 Automation Job Not Found",
message="The requested automation job could not be found."),
status=404)
form = EditAutomationForm(enabled=automation.enabled)
if form.validate_on_submit():
automation.enabled = form.enabled.data
automation.updated = datetime.utcnow()
try:
db.session.commit()
flash("Saved changes to bridge configuration.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the bridge configuration.", "danger")
return render_template("automation.html.j2",
section="automation",
automation=automation, form=form)
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
def automation_kick(automation_id: int):
automation = Automation.query.filter(
Automation.id == automation_id,
Automation.destroyed == None).first()
if automation is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
header=f"Kick automation timer?",
message=automation.description,
success_view="portal.automation.automation_list",
success_message="This automation job will next run within 1 minute.",
section="automation",
resource=automation,
action="kick"
)

View file

@ -0,0 +1,16 @@
{% extends "base.html.j2" %}
{% from 'bootstrap5/form.html' import render_form %}
{% from "tables.html.j2" import automation_logs_table %}
{% block content %}
<h1 class="h2 mt-3">Automation Job</h1>
<h2 class="h3">{{ automation.description }} ({{ automation.short_name }})</h2>
<div style="border: 1px solid #666;" class="p-3">
{{ render_form(form) }}
</div>
<h3>Logs</h3>
{{ automation_logs_table(automation.logs) }}
{% endblock %}

View file

@ -128,6 +128,12 @@
<span>Monitoring</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link{% if section == "automation" %} active{% endif %}"
href="{{ url_for("portal.automation.automation_list") }}">
Automation
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if section == "alarm" %} active{% endif %}"
href="{{ url_for("portal.view_alarms") }}">

View file

@ -1,5 +1,5 @@
{% extends "base.html.j2" %}
{% from "tables.html.j2" import bridgeconfs_table, bridges_table,
{% from "tables.html.j2" import automations_table, bridgeconfs_table, bridges_table,
groups_table, mirrorlists_table, origins_table, origin_onion_table,
onions_table, proxies_table %}
@ -8,7 +8,9 @@
{% if new_link %}
<a href="{{ new_link }}" class="btn btn-success">Create new {{ item }}</a>
{% endif %}
{% if item == "bridge configuration" %}
{% if item == "automation" %}
{{ automations_table(items) }}
{% elif item == "bridge configuration" %}
{{ bridgeconfs_table(items) }}
{% elif item == "bridge" %}
{{ bridges_table(items) }}

View file

@ -26,6 +26,67 @@
<div class="alert alert-danger">Not implemented yet.</div>
{% endmacro %}
{% macro automations_table(automations) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Description</th>
<th scope="col">Status</th>
<th scope="col">Enabled</th>
<th scope="col">Last Run</th>
<th scope="col">Next Run</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for automation in automations %}
<tr>
<td title="{{ automation.short_name }}">{{ automation.description }}</td>
<td title="{{ automation.state.name }}">
{% if automation.state.name == "IDLE" %}
🕰️
{% elif automation.state.name == "RUNNING" %}
🏃
{% else %}
💥
{% endif %}
</td>
<td>{% if automation.enabled %}✅{% else %}❌{% endif %}</td>
<td>{{ automation.last_run | format_datetime }}</td>
<td>{{ automation.next_run | format_datetime }}</td>
<td>
<a href="{{ url_for("portal.automation.automation_edit", automation_id=automation.id) }}" class="btn btn-primary btn-sm">View/Edit</a>
<a href="{{ url_for("portal.automation.automation_kick", automation_id=automation.id) }}" class="btn btn-success btn-sm">Kick Timer</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}
{% macro automation_logs_table(logs) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Log Created</th>
<th scope="col">Log</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.added | format_datetime }}</td>
<td>{{ log.logs }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}
{% macro groups_table(groups) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
@ -84,6 +145,8 @@
<td>
<a href="{{ url_for("portal.origin.origin_edit", origin_id=origin.id) }}"
class="btn btn-primary btn-sm">View/Edit</a>
<a href="{{ url_for("portal.origin.origin_destroy", origin_id=origin.id) }}"
class="btn btn-danger btn-sm">Destroy</a>
</td>
</tr>
{% endif %}

View file

@ -20,11 +20,19 @@ def view_lifecycle(*,
resource: AbstractResource,
action: str):
form = LifecycleForm()
if action == "destroy":
form.submit.render_kw = {"class": "btn btn-danger"}
elif action == "deprecate":
form.submit.render_kw = {"class": "btn btn-warning"}
elif action == "kick":
form.submit.render_kw = {"class": "btn btn-success"}
if form.validate_on_submit():
if action == "destroy":
resource.destroy()
elif action == "deprecate":
resource.deprecate(reason="manual")
elif action == "kick":
resource.kick()
else:
flash("Unknown action")
return redirect(url_for("portal.portal_home"))