portal/storage: expose storage information via the portal
This commit is contained in:
parent
1ee75fd37f
commit
293acba317
6 changed files with 121 additions and 7 deletions
|
@ -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():
|
||||
|
|
|
@ -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
69
app/portal/storage.py
Normal 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"
|
||||
)
|
|
@ -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" %}
|
||||
|
|
21
app/portal/templates/storage.html.j2
Normal file
21
app/portal/templates/storage.html.j2
Normal 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 %}
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue