portal/storage: expose storage information via the portal

This commit is contained in:
Iain Learmonth 2022-11-01 10:17:31 +00:00
parent 1ee75fd37f
commit 293acba317
6 changed files with 121 additions and 7 deletions

View file

@ -1,9 +1,11 @@
import json
from datetime import datetime, timedelta, timezone
from typing import Optional
from flask import Blueprint, render_template, request, url_for, redirect
from flask.typing import ResponseReturnValue
from jinja2.utils import markupsafe
from markupsafe import Markup
from sqlalchemy import desc, or_, func
from app.alarms import alarms_for
@ -24,6 +26,7 @@ from app.portal.onion import bp as onion
from app.portal.pool import bp as pool
from app.portal.proxy import bp as proxy
from app.portal.smart_proxy import bp as smart_proxy
from app.portal.storage import bp as storage
from app.portal.webhook import bp as webhook
portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")
@ -38,6 +41,7 @@ portal.register_blueprint(onion, url_prefix="/onion")
portal.register_blueprint(pool, url_prefix="/pool")
portal.register_blueprint(proxy, url_prefix="/proxy")
portal.register_blueprint(smart_proxy, url_prefix="/smart")
portal.register_blueprint(storage, url_prefix="/state")
portal.register_blueprint(webhook, url_prefix="/webhook")
@ -91,6 +95,13 @@ def describe_brn(s: str) -> ResponseReturnValue:
return s
@portal.app_template_filter("pretty_json")
def pretty_json(input: Optional[str]):
if not input:
return "None"
return json.dumps(json.loads(input), indent=2)
def total_origins_blocked() -> int:
count = 0
for o in Origin.query.filter(Origin.destroyed.is_(None)).all():

View file

@ -4,11 +4,12 @@ from typing import Optional
from flask import render_template, flash, Response, Blueprint, current_app
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from sqlalchemy import exc, desc
from wtforms import SubmitField, BooleanField
from app.extensions import db
from app.models.automation import Automation, AutomationLogs
from app.models.tfstate import TerraformState
from app.portal.util import view_lifecycle, response_404
bp = Blueprint("automation", __name__)
@ -27,16 +28,17 @@ class EditAutomationForm(FlaskForm): # type: ignore
@bp.route("/list")
def automation_list() -> ResponseReturnValue:
automations = Automation.query.filter(
Automation.destroyed.is_(None)).order_by(Automation.description).all()
automations = list(filter(
lambda a: a.short_name not in current_app.config.get('HIDDEN_AUTOMATIONS', []),
automations
Automation.query.filter(
Automation.destroyed.is_(None)).order_by(Automation.description).all()
))
states = {tfs.key: tfs for tfs in TerraformState.query.all()}
return render_template("list.html.j2",
title="Automation Jobs",
item="automation",
items=automations,
states=states,
**_SECTION_TEMPLATE_VARS)
@ -58,7 +60,8 @@ def automation_edit(automation_id: int) -> ResponseReturnValue:
flash("Saved changes to bridge configuration.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the bridge configuration.", "danger")
logs = AutomationLogs.query.filter(AutomationLogs.automation_id == automation.id).order_by(AutomationLogs.added).all()
logs = AutomationLogs.query.filter(AutomationLogs.automation_id == automation.id).order_by(
desc(AutomationLogs.added)).limit(5).all()
return render_template("automation.html.j2",
automation=automation,
logs=logs,

69
app/portal/storage.py Normal file
View file

@ -0,0 +1,69 @@
from datetime import datetime
from typing import Optional
from flask import render_template, flash, Response, Blueprint, current_app
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import SubmitField, BooleanField
from app.extensions import db
from app.models.automation import Automation, AutomationLogs
from app.models.tfstate import TerraformState
from app.portal.util import view_lifecycle, response_404
bp = Blueprint("storage", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "automation",
"help_url": "https://bypass.censorship.guide/user/automation.html"
}
class EditStorageForm(FlaskForm): # type: ignore
force_unlock = BooleanField('Force Unlock')
submit = SubmitField('Save Changes')
@bp.route('/edit/<storage_key>', methods=['GET', 'POST'])
def storage_edit(storage_key: str) -> ResponseReturnValue:
storage: Optional[TerraformState] = TerraformState.query.filter(TerraformState.key == storage_key).first()
if storage is None:
return Response(render_template("error.html.j2",
header="404 Storage Key Not Found",
message="The requested storage could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
form = EditStorageForm()
if form.validate_on_submit():
if form.force_unlock.data:
storage.lock = None
storage.updated = datetime.utcnow()
try:
db.session.commit()
flash("Storage has been force unlocked.", "success")
except exc.SQLAlchemyError:
flash("An error occurred unlocking the storage.", "danger")
return render_template("storage.html.j2",
storage=storage,
form=form,
**_SECTION_TEMPLATE_VARS)
@bp.route("/kick/<automation_id>", methods=['GET', 'POST'])
def automation_kick(automation_id: int) -> ResponseReturnValue:
automation = Automation.query.filter(
Automation.id == automation_id,
Automation.destroyed.is_(None)).first()
if automation is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
header="Kick automation timer?",
message=automation.description,
section="automation",
success_view="portal.automation.automation_list",
success_message="This automation job will next run within 1 minute.",
resource=automation,
action="kick"
)

View file

@ -14,7 +14,7 @@
{% if section == "alarm" %}
{{ alarms_table(items) }}
{% elif item == "automation" %}
{{ automations_table(items) }}
{{ automations_table(items, states) }}
{% elif item == "bridge configuration" %}
{{ bridgeconfs_table(items) }}
{% elif item == "bridge" %}

View file

@ -0,0 +1,21 @@
{% 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">State Storage</h1>
<h2 class="h3">{{ storage.key }}</h2>
<div style="border: 1px solid #666;" class="p-3">
{{ render_form(form) }}
</div>
<h3>Storage</h3>
<h4>Current Lock</h4>
<pre>{{ storage.lock | pretty_json }}</pre>
<h4>State Dump</h4>
<pre>{{ storage.state | pretty_json }}</pre>
{% endblock %}

View file

@ -114,7 +114,7 @@
{{ instances_table("eotk", instances) }}
{% endmacro %}
{% macro automations_table(automations) %}
{% macro automations_table(automations, states) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
@ -122,6 +122,7 @@
<th scope="col">Description</th>
<th scope="col">Status</th>
<th scope="col">Enabled</th>
<th scope="col">Storage</th>
<th scope="col">Last Run</th>
<th scope="col">Next Run</th>
<th scope="col">Actions</th>
@ -141,6 +142,15 @@
{% endif %}
</td>
<td>{% if automation.enabled %}✅{% else %}❌{% endif %}</td>
<td>
{% if automation.short_name in states %}
<a href="#" title="{{ states[automation.short_name].lock or 'Unlocked' }}" class="text-decoration-none">
{% if states[automation.short_name].lock %}🔒{% else %}🔓{% endif %}
</a>
{% else %}
<span title="No Storage">✨</span>
{% endif %}
</td>
<td>{{ automation.last_run | format_datetime }}</td>
<td>{{ automation.next_run | format_datetime }}</td>
<td>