v8.1.5: Add UDP backend status checks and refine cluster HA handling

Introduced backend status monitoring for UDP listeners and enhanced HA cluster checks. Updated several templates and JavaScript files to reflect these changes, improving service visibility and coordination. Minor code refactoring and removed unused functions for cleaner implementation.
pull/418/head
Aidaho 2025-01-04 10:49:28 +03:00
parent f3c7cf97f2
commit e53a7445c7
16 changed files with 213 additions and 36 deletions

View File

@ -16,7 +16,7 @@ from app.views.service.lets_encrypt_views import LetsEncryptsView, LetsEncryptVi
from app.views.service.haproxy_lists_views import HaproxyListView
from app.views.ha.views import HAView, HAVIPView, HAVIPsView
from app.views.user.views import UserView, UserGroupView, UserRoles
from app.views.udp.views import UDPListener, UDPListeners, UDPListenerActionView
from app.views.udp.views import UDPListener, UDPListeners, UDPListenerActionView, UDPListenerBackendStatusView
from app.views.channel.views import ChannelView, ChannelsView
from app.views.tools.views import CheckerView
from app.views.tools.port_scanner_views import PortScannerView, PortScannerPortsView
@ -59,6 +59,7 @@ register_api(HAVIPView, 'ha_vip', '/ha/<service>/<int:cluster_id>/vip', 'vip_id'
bp.add_url_rule('/ha/<service>/<int:cluster_id>/vips', view_func=HAVIPsView.as_view('ha_vips'), methods=['GET'])
register_api(UDPListener, 'udp_listener', '/<service>/listener', 'listener_id')
bp.add_url_rule('/<service>/listener/<int:listener_id>/<any(start, stop, reload, restart):action>', view_func=UDPListenerActionView.as_view('listener_action'), methods=['GET'])
bp.add_url_rule('/<service>/listener/<int:listener_id>/<backend_ip>', view_func=UDPListenerBackendStatusView.as_view('UDPListenerBackendStatusView'), methods=['GET'])
bp.add_url_rule('/<service>/listeners', view_func=UDPListeners.as_view('listeners'), methods=['GET'])
bp.add_url_rule('/service/<service>/<int:server_id>/install', view_func=InstallGetStatus.as_view('install_status'), methods=['GET'])
bp.add_url_rule('/service/<service>/<server_id>/install', view_func=InstallGetStatus.as_view('install_status_ip'), methods=['GET'])

View File

@ -705,7 +705,7 @@ def update_db_v_8_1_4():
def update_ver():
try:
Version.update(version='8.1.4').execute()
Version.update(version='8.1.5').execute()
except Exception:
print('Cannot update version')

View File

@ -3,43 +3,31 @@ from app.modules.db.common import out_error
def select_keep_alive():
query = Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.haproxy_active == 1)
try:
query_res = query.execute()
return Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.haproxy_active == 1).execute()
except Exception as e:
out_error(e)
else:
return query_res
def select_nginx_keep_alive():
query = Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.nginx_active == 1)
try:
query_res = query.execute()
return Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.nginx_active == 1).execute()
except Exception as e:
out_error(e)
else:
return query_res
def select_apache_keep_alive():
query = Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.apache_active == 1)
try:
query_res = query.execute()
return Server.select(Server.ip, Server.group_id, Server.server_id).where(Server.apache_active == 1).execute()
except Exception as e:
out_error(e)
else:
return query_res
def select_keepalived_keep_alive():
query = Server.select(Server.ip, Server.port, Server.group_id, Server.server_id).where(Server.keepalived_active == 1)
try:
query_res = query.execute()
return Server.select(Server.ip, Server.port, Server.group_id, Server.server_id).where(Server.keepalived_active == 1).execute()
except Exception as e:
out_error(e)
else:
return query_res
def select_update_keep_alive_restart(server_id: int, service: str) -> int:
@ -55,8 +43,7 @@ def select_update_keep_alive_restart(server_id: int, service: str) -> int:
def update_keep_alive_restart(server_id: int, service: str, restarted: int) -> None:
query = KeepaliveRestart.insert(server_id=server_id, service=service, restarted=restarted).on_conflict('replace')
try:
query.execute()
KeepaliveRestart.insert(server_id=server_id, service=service, restarted=restarted).on_conflict('replace').execute()
except Exception as e:
out_error(e)

View File

@ -23,13 +23,6 @@ def get_user() -> UserName:
print(str(e))
def select_user_status() -> int:
try:
return UserName.get().Status
except Exception:
return 0
def get_roxy_tools():
try:
query_res = RoxyTool.select().where(RoxyTool.is_roxy == 1).execute()

View File

@ -103,7 +103,7 @@ def action_service(action: str, service: str) -> str:
if not re.match(r'^[a-zA-Z0-9\.\-]+$', service):
return f"Invalid service name: {service}. Only alphanumeric characters, dots, and hyphens are allowed."
cmd = f"sudo systemctl {actions[action]} {service}"
if not roxy_sql.select_user_status():
if not roxy_sql.get_user().Status:
return 'warning: The service is disabled because you are not subscribed. Read <a href="https://roxy-wi.org/pricing" ' \
'title="Roxy-WI pricing" target="_blank">here</a> about subscriptions'
if is_in_docker:

View File

@ -150,3 +150,26 @@ def get_masters(service):
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get free servers')
return jsonify([model_to_dict(free) for free in free_servers])
@bp.route('/<service>/<int:cluster_id>/status')
@check_services
@get_user_params()
def check_cluster_status(service: str, cluster_id: int):
try:
router_id = ha_sql.get_router_id(cluster_id, default_router=1)
slaves = ha_sql.select_cluster_slaves(cluster_id, router_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get slaves')
status = 'ok'
statuses = []
cmd = f'systemctl is-active keepalived.service'
for slave in slaves:
status = server_mod.ssh_command(slave[2], cmd)
statuses.append(status.replace('\n', '').replace('\r', ''))
if 'inactive' in statuses and 'active' in statuses:
status = 'warning'
elif 'inactive' in statuses and 'active' not in statuses:
status = 'error'
return jsonify({'status': status})

View File

@ -1,7 +1,7 @@
from flask_jwt_extended import jwt_required
from app.routes.udp import bp
from app.views.udp.views import UDPListener
from app.views.udp.views import UDPListener, UDPListenerBackendStatusView
@bp.before_request
@ -13,3 +13,4 @@ def before_request():
bp.add_url_rule('/<service>/listener', view_func=UDPListener.as_view('udp_listener', False), methods=['GET'], defaults={'listener_id': None})
bp.add_url_rule('/<service>/listener/<int:listener_id>', view_func=UDPListener.as_view('udp_listener_id', False), methods=['GET'])
bp.add_url_rule('/<service>/listener/<int:listener_id>/<backend_ip>', view_func=UDPListenerBackendStatusView.as_view('udp_listener_backend_ip'), methods=['GET'])

View File

@ -151,6 +151,14 @@ $( function() {
let id = $(this).attr('id').split('-');
updateReceiver(id[1], 'pd')
});
$("#checker_mm_table input").change(function () {
let id = $(this).attr('id').split('-');
updateReceiver(id[2], 'mm')
});
$("#checker_pd_tablechecker_mm_table select").on('selectmenuchange', function () {
let id = $(this).attr('id').split('-');
updateReceiver(id[1], 'mm')
});
});
function loadChannel() {
$.ajax({

View File

@ -864,3 +864,53 @@ function getHaCluster(cluster_id, new_cluster=false) {
}
});
}
function checkHaClusterStatus(cluster_id) {
if (sessionStorage.getItem('check-ha-cluster-'+cluster_id) == 0) {
return false;
}
NProgress.configure({showSpinner: false});
let listener_div = $('#cluster-' + cluster_id);
$.ajax({
url: "/ha/cluster/" + cluster_id + "/status",
contentType: "application/json; charset=utf-8",
statusCode: {
404: function (xhr) {
$('#cluster-' + cluster_id).remove();
},
403: function (xhr) {
sessionStorage.setItem('check-ha-cluster-'+cluster_id, 0);
},
500: function (xhr) {
sessionStorage.setItem('ccheck-ha-cluster-'+cluster_id, 0);
}
},
success: function (data) {
try {
if (data.indexOf('logout') != '-1') {
sessionStorage.setItem('check-ha-cluster-'+cluster_id, 0);
}
} catch (e) {}
if (data.status === 'ok') {
listener_div.addClass('div-server-head-up');
listener_div.attr('title', 'All services are UP');
listener_div.removeClass('div-server-head-down');
listener_div.removeClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-dis');
} else if (data.status === 'failed' || data.status === 'error') {
listener_div.removeClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-up');
listener_div.removeClass('div-server-head-dis');
listener_div.addClass('div-server-head-down');
listener_div.attr('title', 'All services are DOWN');
} else if (data.status === 'warning') {
listener_div.addClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-up');
listener_div.removeClass('div-server-head-down');
listener_div.removeClass('div-server-head-dis');
listener_div.attr('title', 'Not all services are UP');
}
}
});
NProgress.configure({showSpinner: true});
}

View File

@ -518,6 +518,7 @@ function check_service_status(id, ip, service) {
} else {
if (data.status === 'failed') {
server_div.removeClass('div-server-head-unknown');
server_div.removeClass('div-server-head-dis');
server_div.removeClass('div-server-head-up');
server_div.addClass('div-server-head-down');
} else {
@ -525,10 +526,12 @@ function check_service_status(id, ip, service) {
server_div.addClass('div-server-head-up');
server_div.removeClass('div-server-head-down');
server_div.removeClass('div-server-head-unknown');
server_div.removeClass('div-server-head-dis');
$('#uptime-word-'+id).text(translate_div.attr('data-uptime'));
} else {
server_div.removeClass('div-server-head-up');
server_div.removeClass('div-server-head-unknown');
server_div.removeClass('div-server-head-dis');
server_div.addClass('div-server-head-down');
$('#uptime-word-'+id).text(translate_div.attr('data-downtime'));
}

View File

@ -549,8 +549,10 @@ function checkStatus(listener_id) {
listener_div.attr('title', 'All services are UP');
listener_div.removeClass('div-server-head-down');
listener_div.removeClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-dis');
} else if (data.status === 'failed' || data.status === 'error') {
listener_div.removeClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-dis');
listener_div.removeClass('div-server-head-up');
listener_div.addClass('div-server-head-down');
listener_div.attr('title', 'All services are DOWN');
@ -558,6 +560,7 @@ function checkStatus(listener_id) {
listener_div.addClass('div-server-head-unknown');
listener_div.removeClass('div-server-head-up');
listener_div.removeClass('div-server-head-down');
listener_div.removeClass('div-server-head-dis');
listener_div.attr('title', 'Not all services are UP');
}
$(`#listener-name-${listener_id}`).text(data.name.replaceAll("'", ""));
@ -572,3 +575,34 @@ function checkStatus(listener_id) {
});
NProgress.configure({showSpinner: true});
}
function checkUdpBackendStatus(listener_id, backend_ip) {
$.ajax({
url: `/udp/listener/${listener_id}/${backend_ip}`,
type: "GET",
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.status === 'failed') {
toastr.error(data.error);
return false;
} else {
let server_div = $('#backend_server_status_' + backend_ip.replaceAll('.', ''));
if (data.data === 'yes') {
server_div.removeClass('serverNone');
server_div.removeClass('serverDown');
server_div.addClass('serverUp');
server_div.attr('title', 'Server is reachable');
} else if (data.data === 'no') {
server_div.removeClass('serverNone');
server_div.removeClass('serverUp');
server_div.addClass('serverDown');
server_div.attr('title', 'Server is unreachable');
} else {
server_div.removeClass('serverDown');
server_div.removeClass('serverUp');
server_div.addClass('serverNone');
server_div.attr('title', 'Cannot get server status');
}
}
}
});
}

View File

@ -160,9 +160,9 @@
<caption><i class="fas fa-power-off caption-icon"></i><h3>Mattermost {{lang.words.channels|title()}}</h3></caption>
<tr class="overviewHead" style="width: 50%;">
<td class="padding10 first-collumn" style="width: 25%;">
{{lang.words.key|title()}}
Webhook
</td>
<td style="width: 20%;">Webhook</td>
<td style="width: 20%;">{{lang.words.channel|title()}} {{lang.words.name}}</td>
{% if user_params['role']|int() == 1 %}
<td style="width: 25%;">{{lang.words.group|title()}}</td>
{% endif %}

View File

@ -1,7 +1,7 @@
{% import 'languages/'+lang|default('en')+'.html' as lang %}
{% from 'include/input_macros.html' import input, checkbox, copy_to_clipboard %}
{% for cluster in clusters %}
<div id="cluster-{{cluster.id}}" class="div-server-hapwi">
<div id="cluster-{{cluster.id}}" class="div-server-hapwi div-server-head-dis">
<div class="server-name">
<a href="/ha/cluster/{{cluster.id}}" title="{{lang.words.open|title()}} {{lang.words.cluster|replace("'", "")}}">
<span id="cluster-name-{{cluster.id}}">{{cluster.name|replace("'", "")}}</span>
@ -69,4 +69,5 @@
</div>
{{ input('router_id-'+ cluster.id|string(), type='hidden') }}
</div>
<script>checkHaClusterStatus({{ cluster.id }})</script>
{% endfor %}

View File

@ -97,6 +97,7 @@
setInterval(showBytes, 60000, server_ip);
{%- elif service == 'keepalived' %}
keepalivedBecameMaster(server_ip)
setInterval(keepalivedBecameMaster, 60000, server_ip)
{% endif %}
}
showMetrics();
@ -181,7 +182,7 @@
{% set is_checker_enabled = s.4.0.8 %}
{% set is_metrics_enabled = s.4.0.9 %}
{% endif %}
<div id="div-server-{{s.0}}" class="div-server-hapwi div-server-head-unknown">
<div id="div-server-{{s.0}}" class="div-server-hapwi div-server-head-dis">
<div class="server-name">
<input type="hidden" id="server-name-{{s.0}}" value="{{s.1}}" />
<input type="hidden" id="service" value="{{service}}" />

View File

@ -1,6 +1,6 @@
{% import 'languages/'+lang|default('en')+'.html' as lang %}
{% from 'include/input_macros.html' import input, checkbox, copy_to_clipboard %}
<div id="listener-{{listener.id}}" class="div-server-hapwi">
<div id="listener-{{listener.id}}" class="div-server-hapwi div-server-head-dis">
<div class="server-name">
<span class="overflow name-span">
<span id="listener-name-{{listener.id}}">{{listener.name|replace("'", "")}}</span>
@ -38,7 +38,14 @@
<div id="config-{{ listener.id }}">
{% set config = listener.config|string_to_dict %}
{% for c in config %}
{{ lang.words.server|title() }}: {{ copy_to_clipboard(value=c.backend_ip) }}, {{ lang.words.port }}: {{ c.port }}, {{ lang.words.weight }}: {{ c.weight }} <br />
<div>
<span id="backend_server_status_{{ c.backend_ip|replace('.', '') }}" class="server-status-small serverNone"></span>
{{ lang.words.server|title() }}: {{ copy_to_clipboard(value=c.backend_ip) }}, {{ lang.words.port }}: {{ c.port }}, {{ lang.words.weight }}: {{ c.weight }}
</div>
<script>
checkUdpBackendStatus('{{ listener.id }}', '{{ c.backend_ip }}')
setInterval(checkUdpBackendStatus, 60000, '{{listener.id}}', '{{ c.backend_ip }}');
</script>
{% endfor %}
</div>
</div>

View File

@ -1,8 +1,11 @@
from typing import Union
from flask import render_template, g, jsonify
from flask.views import MethodView
from flask_pydantic import validate
from flask_jwt_extended import jwt_required
from playhouse.shortcuts import model_to_dict
from pydantic import IPvAnyAddress
import app.modules.roxywi.auth as roxywi_auth
import app.modules.roxywi.common as roxywi_common
@ -10,11 +13,12 @@ import app.modules.common.common as common
import app.modules.db.udp as udp_sql
import app.modules.db.ha_cluster as ha_sql
import app.modules.db.server as server_sql
import app.modules.server.server as server_mod
import app.modules.service.udp as udp_mod
import app.modules.service.installation as service_mod
from app.middleware import get_user_params, check_services, page_for_admin, check_group
from app.modules.common.common_classes import SupportClass
from app.modules.roxywi.class_models import BaseResponse, IdResponse, UdpListenerRequest, GroupQuery
from app.modules.roxywi.class_models import BaseResponse, IdResponse, UdpListenerRequest, GroupQuery, DomainName, DataStrResponse
class UDPListener(MethodView):
@ -498,3 +502,67 @@ class UDPListenerActionView(MethodView):
return BaseResponse().model_dump(mode='json')
except Exception as e:
return roxywi_common.handle_json_exceptions(e, f'Cannot {action} listener')
class UDPListenerBackendStatusView(MethodView):
methods = ['GET']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()]
@staticmethod
@validate()
def get(service: str, listener_id: int, backend_ip: Union[IPvAnyAddress, DomainName]):
"""
UDP Listener Backend Status View
---
tags:
- UDP listener
parameters:
- in: path
name: listener_id
required: true
description: The ID of the UDP listener.
type: integer
- in: path
name: backend_ip
required: true
description: The IP address of the backend server.
type: string
responses:
200:
description: Success. Returns the backend status for the given UDP listener.
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: The backend status (e.g., 'yes', 'no').
400:
description: Bad request. Invalid listener_id or backend_ip.
404:
description: Not found. The specified UDP listener or backend was not found.
"""
try:
listener = udp_sql.get_listener(listener_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get UDP listeners')
if listener.cluster_id:
cluster = ha_sql.get_cluster(listener.cluster_id)
router_id = ha_sql.get_router_id(cluster.id, 1)
slaves = ha_sql.select_cluster_slaves(cluster.id, router_id)
for slave in slaves:
server_ip = server_sql.get_server(slave[0]).ip
elif listener.server_id:
server_ip = server_sql.get_server(listener.server_id).ip
else:
return roxywi_common.handler_exceptions_for_json_data(Exception(''), 'Cannot get UDP listeners')
cmd = (f"sudo kill -s $(keepalived --signum=DATA) $(cat /var/run/keepalived-udp-{listener_id}.pid) && "
f"sudo grep {backend_ip} -A 3 /tmp/keepalived_check.data |grep Up |awk '{{print $3}}'")
status = server_mod.ssh_command(server_ip, cmd)
status = status.replace('\r\n', '')
return DataStrResponse(data=status).model_dump(mode='json')