Minor bug fixes
This commit is contained in:
parent
42e2b884a1
commit
fe5979aa5b
|
@ -7,49 +7,51 @@ import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def htime(ts):
|
def htime(timestamp):
|
||||||
if ts:
|
if timestamp:
|
||||||
dt = datetime.datetime.fromtimestamp(ts)
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||||
return humanize.naturaltime(dt)
|
return humanize.naturaltime(dt)
|
||||||
|
|
||||||
|
|
||||||
def htime_dt(dt):
|
def htime_dt(datetime):
|
||||||
if dt:
|
if datetime:
|
||||||
try:
|
try:
|
||||||
return humanize.naturaltime(dt)
|
return humanize.naturaltime(datetime)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "Never"
|
return "Never"
|
||||||
|
return "Never"
|
||||||
|
|
||||||
|
|
||||||
def hdate(ts):
|
def hdate(timestamp):
|
||||||
if ts:
|
if timestamp:
|
||||||
dt = datetime.datetime.fromtimestamp(ts)
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||||
return humanize.naturaldate(dt)
|
return humanize.naturaldate(dt)
|
||||||
|
|
||||||
|
|
||||||
def hdate_dt(dt):
|
def hdate_dt(datetime):
|
||||||
if dt:
|
if datetime:
|
||||||
try:
|
try:
|
||||||
return humanize.naturaldate(dt)
|
return humanize.naturaldate(datetime)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "Never"
|
return "Never"
|
||||||
|
return "Never"
|
||||||
|
|
||||||
|
|
||||||
def fmt_timestamp(ts):
|
def fmt_timestamp(timestamp):
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
tz = ZoneInfo(current_app.config['APP_TZ'])
|
tz = ZoneInfo(current_app.config['APP_TZ'])
|
||||||
if ts:
|
if timestamp:
|
||||||
local_ts = datetime.datetime.fromtimestamp(ts, tz)
|
local_ts = datetime.datetime.fromtimestamp(timestamp, tz)
|
||||||
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
local_ts.tzname())
|
local_ts.tzname())
|
||||||
|
|
||||||
|
|
||||||
def fmt_datetime(dt):
|
def fmt_datetime(datetime):
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
tz = ZoneInfo(current_app.config['APP_TZ'])
|
tz = ZoneInfo(current_app.config['APP_TZ'])
|
||||||
if dt:
|
if datetime:
|
||||||
try:
|
try:
|
||||||
local_ts = dt.fromtimestamp(dt.timestamp(), tz)
|
local_ts = datetime.fromtimestamp(datetime.timestamp(), tz)
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
return "Never"
|
return "Never"
|
||||||
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
return "%s %s" % (local_ts.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
|
|
@ -20,6 +20,13 @@ def remote_ip() -> str:
|
||||||
return str(request.environ.get('REMOTE_ADDR'))
|
return str(request.environ.get('REMOTE_ADDR'))
|
||||||
|
|
||||||
|
|
||||||
|
def username() -> str:
|
||||||
|
userinfo = flask_session['userinfo']
|
||||||
|
if 'preferred_username' in userinfo:
|
||||||
|
return userinfo['preferred_username']
|
||||||
|
return userinfo['email']
|
||||||
|
|
||||||
|
|
||||||
def webMode() -> bool:
|
def webMode() -> bool:
|
||||||
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
|
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
|
||||||
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
|
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
|
||||||
|
|
|
@ -103,3 +103,10 @@ div.dt-container div.dt-scroll-body {
|
||||||
tr.pka-hide {
|
tr.pka-hide {
|
||||||
visibility: collapse;
|
visibility: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.disabled {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
span.expired {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
|
@ -16,9 +16,3 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro body_theme(theme) -%}
|
|
||||||
{% if theme == "dark" %}
|
|
||||||
bootstrap-dark
|
|
||||||
{% endif %}
|
|
||||||
{%- endmacro -%}
|
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!-- <body class="bootstrap {{ mc.body_theme(g.theme) }}"> -->
|
|
||||||
<body class="bootstrap bootstrap-dark">
|
<body class="bootstrap bootstrap-dark">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header>
|
<header>
|
||||||
|
|
|
@ -14,6 +14,18 @@
|
||||||
</h3>
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
<p></p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-3 float-left">
|
||||||
|
<strong>status</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col col-8 float-left">
|
||||||
|
{% if node.online %}
|
||||||
|
<span class="badge badge-pill badge-success">online</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-danger">offline</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-3 float-left">
|
<div class="col col-3 float-left">
|
||||||
<strong>registered</strong>
|
<strong>registered</strong>
|
||||||
|
@ -27,20 +39,23 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-3 float-left">
|
<div class="col col-3 float-left">
|
||||||
<strong>expiry</strong>
|
<strong>expire</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.expiry | fmt_datetime }}">
|
<span data-toggle="tooltip" data-placement="right" title="{{ node.expiry | fmt_datetime }}">
|
||||||
{{ node.expiry | htime_dt }}
|
{{ node.expiry | htime_dt }}
|
||||||
</span>
|
</span>
|
||||||
|
{% if node.expireDate and not node.expired %}
|
||||||
<a href="/node/{{ node.id }}/expire">
|
<a href="/node/{{ node.id }}/expire">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
|
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-3 float-left">
|
<div class="col col-3 float-left">
|
||||||
<strong>owner</strong>
|
<strong>owner</strong>
|
||||||
|
@ -73,7 +88,7 @@
|
||||||
<div class="col col-6 float-left">
|
<div class="col col-6 float-left">
|
||||||
{% if node.validTags %}
|
{% if node.validTags %}
|
||||||
{% for tag in node.validTags %}
|
{% for tag in node.validTags %}
|
||||||
<span class="badge badge-pill badge-info">
|
<span class="badge badge-pill badge-warning">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -89,7 +104,7 @@
|
||||||
<div class="col col-6 float-left">
|
<div class="col col-6 float-left">
|
||||||
{% if node.forced %}
|
{% if node.forced %}
|
||||||
{% for tag in node.forcedTags %}
|
{% for tag in node.forcedTags %}
|
||||||
<h3><span class="badge badge-pill badge-info">
|
<h3><span class="badge badge-pill badge-primary">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span></h3>
|
</span></h3>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
<th>user</th>
|
<th>user</th>
|
||||||
<th>registered</th>
|
<th>expire</th>
|
||||||
<th>last event</th>
|
<th>last event</th>
|
||||||
<th>online</th>
|
<th>online</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
|
@ -32,9 +32,11 @@
|
||||||
{{node.user.name}}
|
{{node.user.name}}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td data-order="{{ node.createdAt }}">
|
<td>
|
||||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
<span data-toggle="tooltip" data-placement="right"
|
||||||
{{node.createdAt | htime_dt }}
|
title="{{ node.expiry | fmt_datetime }}"
|
||||||
|
class="{% if node.expired %}expired{% endif %}">
|
||||||
|
{{node.expireDate | htime_dt | safe}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td data-order="{{ node.lastSeen | fmt_datetime }}">
|
<td data-order="{{ node.lastSeen | fmt_datetime }}">
|
||||||
|
@ -46,11 +48,15 @@
|
||||||
{{node.online | fancyBool | safe}}
|
{{node.online | fancyBool | safe}}
|
||||||
</td>
|
</td>
|
||||||
<td class="no-sort">
|
<td class="no-sort">
|
||||||
|
{% if node.expireDate and not node.expired %}
|
||||||
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
|
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
|
||||||
<a class="nodeco" href="/node/{{node.id}}/list-expire">
|
<a class="nodeco" href="/node/{{node.id}}/list-expire" disabled>
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-plug disabled"></i>
|
||||||
|
{% endif %}
|
||||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||||
<a class="nodeco" href="/node/{{node.id}}/delete">
|
<a class="nodeco" href="/node/{{node.id}}/delete">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
|
|
|
@ -5,6 +5,8 @@ from flask import render_template, Blueprint, request
|
||||||
from flask import redirect, session, url_for
|
from flask import redirect, session, url_for
|
||||||
from app import auth
|
from app import auth
|
||||||
|
|
||||||
|
from .lib import username
|
||||||
|
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
|
@ -134,25 +136,38 @@ def routeToggle(routeId: int):
|
||||||
if route:
|
if route:
|
||||||
route = route[0]
|
route = route[0]
|
||||||
if route.enabled:
|
if route.enabled:
|
||||||
|
action = 'disabled'
|
||||||
Route().disable(routeId)
|
Route().disable(routeId)
|
||||||
log.info(f"route {route.prefix}/{route.node.givenName} disabled")
|
|
||||||
else:
|
else:
|
||||||
Route().enable(routeId)
|
Route().enable(routeId)
|
||||||
log.info(f"route {route.prefix}/{route.node.givenName} enabled")
|
action = 'enabled'
|
||||||
|
log.info(
|
||||||
|
f"route '{route.prefix}' via '{route.node.givenName}'"
|
||||||
|
f"{action} by '{username()}'")
|
||||||
return redirect(url_for("main.routes"))
|
return redirect(url_for("main.routes"))
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def expireNode(nodeId: int):
|
def expireNode(nodeId: int):
|
||||||
|
"""
|
||||||
|
This expires a node from the node page.
|
||||||
|
The difference from above is that it returns to the /node/nodeId page
|
||||||
|
"""
|
||||||
Node().expire(nodeId)
|
Node().expire(nodeId)
|
||||||
|
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||||
return redirect(url_for("main.node", nodeId=nodeId))
|
return redirect(url_for("main.node", nodeId=nodeId))
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def expireNodeList(nodeId: int):
|
def expireNodeList(nodeId: int):
|
||||||
|
"""
|
||||||
|
This expires a node from the node list.
|
||||||
|
The difference from above is that it returns to the /nodes page
|
||||||
|
"""
|
||||||
Node().expire(nodeId)
|
Node().expire(nodeId)
|
||||||
|
log.info(f"node '{nodeId}' expired by '{username()}'")
|
||||||
return redirect(url_for("main.nodes"))
|
return redirect(url_for("main.nodes"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ ARG BUILD_DATE
|
||||||
ENV APP_VERSION=${APP_VERSION:-alpha0}
|
ENV APP_VERSION=${APP_VERSION:-alpha0}
|
||||||
ENV APP_SHA=${APP_SHA:-000000}
|
ENV APP_SHA=${APP_SHA:-000000}
|
||||||
# useful in case we want to run in debug mode
|
# useful in case we want to run in debug mode
|
||||||
ENV FLASK_APP /hsmon/wsgi.py
|
ENV FLASK_APP /hsman/wsgi.py
|
||||||
|
ENV FLASK_ENV production
|
||||||
|
|
||||||
RUN apk --update --no-cache add \
|
RUN apk --update --no-cache add \
|
||||||
bash \
|
bash \
|
||||||
|
@ -18,16 +19,18 @@ RUN apk --update --no-cache add \
|
||||||
chmod g+w /run && \
|
chmod g+w /run && \
|
||||||
pip install poetry gunicorn
|
pip install poetry gunicorn
|
||||||
|
|
||||||
COPY . /hsmon
|
COPY . /hsman
|
||||||
|
|
||||||
RUN cd hsmon && \
|
RUN cd hsman && \
|
||||||
poetry install && \
|
poetry install && \
|
||||||
poetry export | pip install -r /dev/stdin
|
poetry export | pip install -r /dev/stdin
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /hsman
|
||||||
|
|
||||||
HEALTHCHECK --interval=20s --timeout=3s CMD curl -I -s -o /dev/null localhost:5000/health || exit 1
|
HEALTHCHECK --interval=20s --timeout=3s CMD curl -I -s -o /dev/null localhost:5000/health || exit 1
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
# exectute start up script
|
# exectute start up script
|
||||||
ENTRYPOINT ["/hsmon/docker/entrypoint.sh"]
|
ENTRYPOINT ["/hsman/docker/entrypoint.sh"]
|
||||||
|
|
|
@ -5,11 +5,12 @@ if [ $# -gt 0 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo Starting ${APP_VERSION}
|
echo Starting ${APP_VERSION}
|
||||||
if [ -e /debug ]; then
|
|
||||||
echo "Running app in debug mode!"
|
if [ $FLASK_ENV == "development" ]; then
|
||||||
export PYTHONPATH=/app/site-packages
|
echo "Running app in debug mode"
|
||||||
/hsmon/site-packages/bin/flask run --host 0.0.0.0
|
export FLASK_DEBUG=1
|
||||||
|
flask run --host 0.0.0.0
|
||||||
else
|
else
|
||||||
echo "Running app in production mode!"
|
echo "Running app in production mode"
|
||||||
gunicorn --chdir hsmon wsgi:app -w 1 --threads 2 -b 0.0.0.0:5000
|
gunicorn wsgi:app --config gunicorn.conf.py
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -4,13 +4,15 @@ proc_name = "hsmon"
|
||||||
default_proc_name = "hsmon"
|
default_proc_name = "hsmon"
|
||||||
|
|
||||||
bind = "0.0.0.0:5000"
|
bind = "0.0.0.0:5000"
|
||||||
workers = 1
|
workers = 2
|
||||||
threads = 4
|
threads = 4
|
||||||
preload_app = True
|
preload_app = True
|
||||||
worker_class = "uvicorn.workers.UvicornWorker"
|
# worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
|
|
||||||
|
chdir = "/hsman"
|
||||||
|
|
||||||
# logconfig = "app/logging/production.ini"
|
# logconfig = "app/logging/production.ini"
|
||||||
logconfig = "/hsmon/app/logging/production.ini"
|
logconfig = "app/logging/production.ini"
|
||||||
# access_log_format = "%(h)s %(l)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s"
|
# access_log_format = "%(h)s %(l)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s"
|
||||||
# Log to stdout.
|
# Log to stdout.
|
||||||
accesslog = "-"
|
accesslog = "-"
|
||||||
|
|
|
@ -457,13 +457,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsapi-client"
|
name = "hsapi-client"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
description = "Headscale API client"
|
description = "Headscale API client"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.11"
|
python-versions = "<4.0,>=3.11"
|
||||||
files = [
|
files = [
|
||||||
{file = "hsapi_client-0.9.1-py3-none-any.whl", hash = "sha256:1b6531526ed476daaefa16eeb3a16e768372f4bd5d879da7bf4ed75a4bd36a5b"},
|
{file = "hsapi_client-0.9.2-py3-none-any.whl", hash = "sha256:bbda176e10b7ba0c1376eac6e8fcbd9027a41fab21585edad275cfbd21cce66d"},
|
||||||
{file = "hsapi_client-0.9.1.tar.gz", hash = "sha256:b047a21e642ae4ba108a17f6b68d71af8ba851270421261df92c97acdcfdc907"},
|
{file = "hsapi_client-0.9.2.tar.gz", hash = "sha256:902256e6240e47b3eecacde930f9d67e12a0d46d34d7dbcc86e95b2976013099"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -1186,4 +1186,4 @@ watchdog = ["watchdog (>=2.3)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.11,<4.0"
|
python-versions = ">=3.11,<4.0"
|
||||||
content-hash = "462d0b3a2b307ddc04cb74309f7a1a85679a3a3bf05155a8022d0047d14d2241"
|
content-hash = "b69a53708d986528f101f49ccb7e61fb1377d244511d9cd335b0ec200510b37b"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hsman"
|
name = "hsman"
|
||||||
version = "0.1.0"
|
version = "0.9.0"
|
||||||
description = "Flask Admin webui for Headscale"
|
description = "Flask Admin webui for Headscale"
|
||||||
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
||||||
license = "BSD"
|
license = "BSD"
|
||||||
|
@ -11,11 +11,11 @@ python = ">=3.11,<4.0"
|
||||||
Flask = "^3.0.3"
|
Flask = "^3.0.3"
|
||||||
Flask-pyoidc = "^3.14.3"
|
Flask-pyoidc = "^3.14.3"
|
||||||
gunicorn = "^22.0.0"
|
gunicorn = "^22.0.0"
|
||||||
hsapi-client = "^0.9.1"
|
|
||||||
flask-mobility = "^2.0.1"
|
flask-mobility = "^2.0.1"
|
||||||
humanize = "^4.9.0"
|
humanize = "^4.9.0"
|
||||||
flask-pydantic = "^0.12.0"
|
flask-pydantic = "^0.12.0"
|
||||||
uvicorn = "^0.30.1"
|
uvicorn = "^0.30.1"
|
||||||
|
hsapi-client = "^0.9.2"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
Loading…
Reference in New Issue