mirror of https://github.com/Aidaho12/haproxy-wi
parent
2b1f3b647a
commit
447739c644
|
@ -962,9 +962,6 @@ def insert_new_ssh(name, enable, group, username, password):
|
|||
Cred.insert(name=name, enable=enable, groups=group, username=username, password=password).execute()
|
||||
except Exception as e:
|
||||
out_error(e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def delete_ssh(ssh_id):
|
||||
|
@ -3027,7 +3024,7 @@ def select_remote_path_from_version(server_ip: str, service: str, local_path: st
|
|||
|
||||
|
||||
def insert_system_info(
|
||||
server_id: int, os_info: str, sys_info: str, cpu: str, ram: str, network: str, disks: str
|
||||
server_id: int, os_info: str, sys_info: dict, cpu: dict, ram: dict, network: dict, disks: dict
|
||||
):
|
||||
try:
|
||||
SystemInfo.insert(
|
||||
|
@ -3112,9 +3109,6 @@ def insert_user_name(user_name):
|
|||
UserName.insert(UserName=user_name).execute()
|
||||
except Exception as e:
|
||||
out_error(e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def select_user_name():
|
||||
|
|
|
@ -16,7 +16,7 @@ def return_error_message():
|
|||
return 'error: All fields must be completed'
|
||||
|
||||
|
||||
def get_user_group(**kwargs) -> str:
|
||||
def get_user_group(**kwargs) -> int:
|
||||
user_group = ''
|
||||
|
||||
try:
|
||||
|
@ -104,8 +104,6 @@ def get_files(folder, file_format, server_ip=None) -> list:
|
|||
|
||||
|
||||
def logging(server_ip: str, action: str, **kwargs) -> None:
|
||||
login = ''
|
||||
cur_date = get_date.return_date('logs')
|
||||
cur_date_in_log = get_date.return_date('date_in_log')
|
||||
log_path = get_config_var.get_config_var('main', 'log_path')
|
||||
|
||||
|
@ -126,18 +124,17 @@ def logging(server_ip: str, action: str, **kwargs) -> None:
|
|||
user_uuid = request.cookies.get('uuid')
|
||||
login = sql.get_user_name_by_uuid(user_uuid)
|
||||
except Exception:
|
||||
if kwargs.get('login'):
|
||||
login = kwargs.get('login')
|
||||
login = ''
|
||||
|
||||
if kwargs.get('roxywi') == 1:
|
||||
if kwargs.get('login'):
|
||||
mess = f"{cur_date_in_log} from {ip} user: {login}, group: {user_group}, {action} on: {server_ip}\n"
|
||||
else:
|
||||
mess = f"{cur_date_in_log} {action} from {ip}\n"
|
||||
log_file = f"{log_path}/roxy-wi-{cur_date}.log"
|
||||
log_file = f"{log_path}/roxy-wi.log"
|
||||
else:
|
||||
mess = f"{cur_date_in_log} from {ip} user: {login}, group: {user_group}, {action} on: {server_ip}\n"
|
||||
log_file = f"{log_path}/config_edit-{cur_date}.log"
|
||||
mess = f"{cur_date_in_log} from {ip} user: {login}, group: {user_group}, {action} on: {server_ip} {kwargs.get('service')}\n"
|
||||
log_file = f"{log_path}/config_edit.log"
|
||||
|
||||
if kwargs.get('keep_history'):
|
||||
try:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import psutil
|
||||
|
||||
import modules.db.sql as sql
|
||||
import modules.server.server as server_mod
|
||||
import app.modules.db.sql as sql
|
||||
import app.modules.server.server as server_mod
|
||||
|
||||
|
||||
def show_ram_metrics(metrics_type: str) -> dict:
|
||||
|
|
|
@ -28,7 +28,7 @@ def create_user(new_user: str, email: str, password: str, role: str, activeuser:
|
|||
except Exception as e:
|
||||
roxywi_common.logging('error: Cannot send email for a new user', e, roxywi=1, login=1)
|
||||
except Exception as e:
|
||||
roxywi_common.handle_exceptions(e, 'Roxy-WI server', f'Cannot create a new user', roxywi=1, login=1)
|
||||
roxywi_common.handle_exceptions(e, 'Roxy-WI server', 'Cannot create a new user', roxywi=1, login=1)
|
||||
|
||||
|
||||
def delete_user(user_id: int) -> str:
|
||||
|
|
|
@ -4,11 +4,11 @@ from cryptography.fernet import Fernet
|
|||
import paramiko
|
||||
from flask import render_template, request
|
||||
|
||||
import modules.db.sql as sql
|
||||
import modules.common.common as common
|
||||
import app.modules.db.sql as sql
|
||||
import app.modules.common.common as common
|
||||
from app.modules.server import ssh_connection
|
||||
import modules.roxywi.common as roxywi_common
|
||||
import modules.roxy_wi_tools as roxy_wi_tools
|
||||
import app.modules.roxywi.common as roxywi_common
|
||||
import app.modules.roxy_wi_tools as roxy_wi_tools
|
||||
|
||||
error_mess = common.error_mess
|
||||
get_config = roxy_wi_tools.GetConfigVar()
|
||||
|
@ -82,9 +82,12 @@ def create_ssh_cred() -> str:
|
|||
if username is None or name is None:
|
||||
return error_mess
|
||||
else:
|
||||
if sql.insert_new_ssh(name, enable, group, username, password):
|
||||
roxywi_common.logging('Roxy-WI server', f'New SSH credentials {name} has been created', roxywi=1, login=1)
|
||||
return render_template('ajax/new_ssh.html', groups=sql.select_groups(), sshs=sql.select_ssh(name=name), page=page, lang=lang)
|
||||
try:
|
||||
sql.insert_new_ssh(name, enable, group, username, password)
|
||||
except Exception as e:
|
||||
roxywi_common.handle_exceptions(e, 'Roxy-WI server', 'Cannot create new SSH credentials', roxywi=1, login=1)
|
||||
roxywi_common.logging('Roxy-WI server', f'New SSH credentials {name} has been created', roxywi=1, login=1)
|
||||
return render_template('ajax/new_ssh.html', groups=sql.select_groups(), sshs=sql.select_ssh(name=name), page=page, lang=lang)
|
||||
|
||||
|
||||
def create_ssh_cread_api(name: str, enable: str, group: str, username: str, password: str) -> bool:
|
||||
|
|
|
@ -16,6 +16,7 @@ class SshConnection:
|
|||
self.ssh_key_name = ssh_settings['key']
|
||||
self.ssh_passphrase = ssh_settings['passphrase']
|
||||
|
||||
# noinspection PyExceptClausesOrder
|
||||
def __enter__(self):
|
||||
kwargs = {
|
||||
'hostname': self.server_ip,
|
||||
|
@ -112,7 +113,7 @@ class SshConnection:
|
|||
yield stdout.channel.recv(len(stdout.channel.in_buffer))
|
||||
|
||||
# chunked read to prevent stalls
|
||||
while (not channel.closed or channel.recv_ready() or channel.recv_stderr_ready()):
|
||||
while not channel.closed or channel.recv_ready() or channel.recv_stderr_ready():
|
||||
# stop if channel was closed prematurely,
|
||||
# and there is no data in the buffers.
|
||||
got_chunk = False
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask_login import login_required
|
|||
|
||||
from app.routes.metric import bp
|
||||
import app.modules.db.sql as sql
|
||||
from middleware import check_services, get_user_params
|
||||
from app.middleware import check_services, get_user_params
|
||||
import app.modules.common.common as common
|
||||
import app.modules.server.server as server_mod
|
||||
import app.modules.roxywi.metrics as metric
|
||||
|
@ -14,7 +14,7 @@ import app.modules.roxywi.common as roxywi_common
|
|||
@bp.before_request
|
||||
@login_required
|
||||
def before_request():
|
||||
""" Protect all of the admin endpoints. """
|
||||
""" Protect all the admin endpoints. """
|
||||
pass
|
||||
|
||||
|
||||
|
@ -86,15 +86,14 @@ def table_metrics(service):
|
|||
group_id = roxywi_common.get_user_group(id=1)
|
||||
|
||||
if service in ('nginx', 'apache'):
|
||||
metrics = sql.select_service_table_metrics(service, group_id)
|
||||
table_stat = sql.select_service_table_metrics(service, group_id)
|
||||
else:
|
||||
metrics = sql.select_table_metrics(group_id)
|
||||
table_stat = sql.select_table_metrics(group_id)
|
||||
|
||||
return render_template('ajax/table_metrics.html', table_stat=metrics, service=service, lang=lang)
|
||||
return render_template('ajax/table_metrics.html', table_stat=table_stat, service=service, lang=lang)
|
||||
|
||||
|
||||
@bp.post('/<service>/<server_ip>')
|
||||
@check_services
|
||||
def show_metric(service, server_ip):
|
||||
server_ip = common.is_ip_or_dns(server_ip)
|
||||
hostname = sql.get_hostname_by_server_ip(server_ip)
|
||||
|
|
|
@ -83,12 +83,32 @@ def create_server():
|
|||
user_subscription = roxywi_common.return_unsubscribed_user_status()
|
||||
roxywi_common.logging('Roxy-WI server', f'Cannot get a user plan: {e}', roxywi=1)
|
||||
|
||||
try:
|
||||
if add_to_smon:
|
||||
if add_to_smon:
|
||||
try:
|
||||
user_group = roxywi_common.get_user_group(id=1)
|
||||
smon_mod.create_smon(hostname, ip, 0, 1, 0, 0, hostname, desc, 0, 0, 0, 56, 'ping', 0, 0, user_group, 0)
|
||||
except Exception as e:
|
||||
roxywi_common.logging(ip, f'error: Cannot add server {hostname} to SMON: {e}')
|
||||
json_data = {
|
||||
"name": hostname,
|
||||
"ip": ip,
|
||||
"port": "0",
|
||||
"enabled": "1",
|
||||
"url": "",
|
||||
"body": "",
|
||||
"group": hostname,
|
||||
"desc": f"Ping {hostname}",
|
||||
"tg": "0",
|
||||
"slack": "0",
|
||||
"pd": "0",
|
||||
"resolver": "",
|
||||
"record_type": "",
|
||||
"packet_size": "56",
|
||||
"http_method": "",
|
||||
"check_type": "ping",
|
||||
"agent_id": "1",
|
||||
"interval": "120",
|
||||
}
|
||||
smon_mod.create_smon(json_data, user_group)
|
||||
except Exception as e:
|
||||
roxywi_common.logging(ip, f'error: Cannot add server {hostname} to SMON: {e}', roxywi=1)
|
||||
|
||||
roxywi_common.logging(ip, f'A new server {hostname} has been created', roxywi=1, login=1, keep_history=1, service='server')
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask_login import login_required
|
|||
from datetime import datetime
|
||||
|
||||
from app.routes.smon import bp
|
||||
from middleware import get_user_params
|
||||
from app.middleware import get_user_params
|
||||
from app.modules.db.db_model import conn
|
||||
import app.modules.db.sql as sql
|
||||
import app.modules.db.smon as smon_sql
|
||||
|
@ -44,6 +44,7 @@ def smon_main_dashboard():
|
|||
'telegrams': sql.get_user_telegram_by_group(group_id),
|
||||
'slacks': sql.get_user_pd_by_group(group_id),
|
||||
'pds': sql.get_user_slack_by_group(group_id),
|
||||
'sort': request.args.get('sort', None)
|
||||
}
|
||||
|
||||
return render_template('smon/dashboard.html', **kwargs)
|
||||
|
|
|
@ -63,7 +63,7 @@ ul#browse_history li:before {
|
|||
ul#browse_history li+li:before {
|
||||
content: "->";
|
||||
color: #767676;
|
||||
margin: 0 5px 0px 5px;
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
#browse_history a {
|
||||
color: #767676;
|
||||
|
@ -669,9 +669,6 @@ ul{
|
|||
.ui-corner-all {
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
/*.ui-visual-focus {*/
|
||||
/* box-shadow: none !important;*/
|
||||
/*}*/
|
||||
.ui-state-focus {
|
||||
border: none !important;
|
||||
}
|
||||
|
@ -872,7 +869,7 @@ label {
|
|||
width: 5px;
|
||||
height: 5px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.server-action {
|
||||
float: right;
|
||||
|
@ -965,6 +962,7 @@ label {
|
|||
width: 48.8%;
|
||||
float: left;
|
||||
margin-left: var(--indent);
|
||||
margin-bottom: var(--indent);
|
||||
}
|
||||
.chart-container_overview {
|
||||
width: 93.3%;
|
||||
|
@ -1132,7 +1130,7 @@ label {
|
|||
}
|
||||
#logo_span img {
|
||||
width: 250px;
|
||||
margin: 35px 0px auto 120px;
|
||||
margin: 35px 0 auto 120px;
|
||||
}
|
||||
.wrong-login {
|
||||
margin-right: -150px;
|
||||
|
@ -1378,7 +1376,6 @@ label {
|
|||
}
|
||||
.portlet-header {
|
||||
cursor: move;
|
||||
margin-right: -10px;
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
bottom: 3px;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<label for="ssh_enable-{{ssh.id}}">{{lang.words.enable|title()}} SSH {{lang.words.key}}</label><input type="checkbox" id="ssh_enable-{{ssh.id}}">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if page != "servers.py" %}
|
||||
{% if page != "servers" %}
|
||||
<td>
|
||||
<select id="sshgroup-{{ssh.id}}" name="sshgroup-{{ssh.id}}">
|
||||
{% for group in groups %}
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
{% endfor %}
|
||||
<b>{{lang.smon_page.desc.status_summary}}: {{lang.smon_page.desc.UP}}: {{up|length}}, {{lang.smon_page.desc.DOWN}}: {{down|length}}, {{lang.words.disabled|title()}}: {{dis|length}}</b>
|
||||
</div>
|
||||
<a title="{{lang.smon_page.desc.do_not_sort_by_status}}" onclick="showSmon('not_sort')">{{lang.smon_page.desc.do_not_sort}}</a> |
|
||||
<a id="sort_by_status" title="{{lang.smon_page.desc.sort_status}}" onclick="sort_by_status()">{{lang.smon_page.desc.sort_status}}</a> |
|
||||
<a title="SMOM: {{lang.words.dashboard|title()}} - Roxy-WI" onclick="showSmon('refresh');">{{lang.words.refresh|title()}}</a>
|
||||
<a title="{{lang.smon_page.desc.do_not_sort_by_status}}" style="cursor: pointer" onclick="showSmon('not_sort')">{{lang.smon_page.desc.do_not_sort}}</a> |
|
||||
<a id="sort_by_status" title="{{lang.smon_page.desc.sort_status}}" style="cursor: pointer" onclick="sort_by_status()">{{lang.smon_page.desc.sort_status}}</a> |
|
||||
<a title="SMOM: {{lang.words.dashboard|title()}} - Roxy-WI" style="cursor: pointer" onclick="showSmon('refresh');">{{lang.words.refresh2|title()}}</a>
|
||||
</div>
|
||||
{% set group = [] %}
|
||||
{% set group_prev = [] %}
|
||||
|
|
|
@ -76,12 +76,10 @@
|
|||
</div>
|
||||
<p>
|
||||
<a href="/app/config/{{service}}/{{serv}}/show" class="ui-button ui-widget ui-corner-all" title="{{lang.phrases.return_to_config}}">{{lang.words.back|title()}}</a>
|
||||
{% if service != 'keepalived' %}
|
||||
<button type="submit" value="test" name="save" class="btn btn-default" title="{{lang.words.check|title()}} {{lang.words.config}} {{lang.words.without}} {{lang.words.saving}}">{{lang.phrases.check_config}}</button>
|
||||
{% endif %}
|
||||
<button type="submit" value="save" name="save" class="btn btn-default" title="{{lang.phrases.save_title}}">{{lang.words.save|title()}}</button>
|
||||
{% if is_restart|int == 0 %}
|
||||
<button type="submit" value="" name="" class="btn btn-default">{{lang.phrases.save_and_restart}}</button>
|
||||
<button type="submit" value="restart" name="" class="btn btn-default">{{lang.phrases.save_and_restart}}</button>
|
||||
{% endif %}
|
||||
<button type="submit" value="reload" name="save" class="btn btn-default">{{lang.phrases.save_and_reload}}</button>
|
||||
{% if service != 'keepalived' %}
|
||||
|
|
|
@ -21,9 +21,6 @@
|
|||
</div>
|
||||
{% elif smon|length == 0 %}
|
||||
<div style="text-align: center;">
|
||||
{% if g.user_params['role'] <= 3 %}
|
||||
<div class="add-button add-button-big" title="{{lang.words.add|title()}} {{ lang.words.check2 }}" onclick="openSmonDialog('http')">+ {{lang.words.add|title()}} {{ lang.words.check2 }}</div>
|
||||
{% endif %}
|
||||
<br />
|
||||
<h3>{{lang.smon_page.desc.not_added}}</h3>
|
||||
<img src="{{ url_for('static', filename='images/no_servers.png')}}" alt="There is no server">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/var/www/haproxy-wi/log/backup.log {
|
||||
/var/www/roxy-wi/log/backup.log {
|
||||
daily
|
||||
rotate 10
|
||||
missingok
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/var/www/roxy-wi/log/roxy-wi.log {
|
||||
daily
|
||||
rotate 10
|
||||
missingok
|
||||
notifempty
|
||||
create 0644 apache apache
|
||||
dateext
|
||||
sharedscripts
|
||||
}
|
||||
|
||||
/var/www/roxy-wi/log/config_edit.log {
|
||||
daily
|
||||
rotate 10
|
||||
missingok
|
||||
notifempty
|
||||
create 0644 apache apache
|
||||
dateext
|
||||
sharedscripts
|
||||
}
|
|
@ -85,10 +85,10 @@ function renderHttpChart(data, labels, server) {
|
|||
font: {
|
||||
size: 20,
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
|
@ -211,9 +211,6 @@ function renderChart(data, labels, server) {
|
|||
font: {
|
||||
size: 20,
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
|
@ -224,6 +221,7 @@ function renderChart(data, labels, server) {
|
|||
family: 'BlinkMacSystemFont'
|
||||
}
|
||||
},
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,9 +279,9 @@ function renderServiceChart(data, labels, server, service) {
|
|||
font: {
|
||||
size: 20
|
||||
},
|
||||
padding: {
|
||||
top: 0
|
||||
}
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
|
@ -293,7 +291,8 @@ function renderServiceChart(data, labels, server, service) {
|
|||
size: '10',
|
||||
family: 'BlinkMacSystemFont'
|
||||
}
|
||||
}
|
||||
},
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
|
@ -341,20 +340,20 @@ function getApacheChartData(server) {
|
|||
},
|
||||
type: "POST",
|
||||
success: function (result) {
|
||||
var data = [];
|
||||
let data = [];
|
||||
data.push(result.chartData.curr_con);
|
||||
data.push(result.chartData.server);
|
||||
var labels = result.chartData.labels;
|
||||
let labels = result.chartData.labels;
|
||||
renderServiceChart(data, labels, server, 'apache');
|
||||
}
|
||||
});
|
||||
}
|
||||
function loadMetrics() {
|
||||
var service = $('#service').val();
|
||||
let service = $('#service').val();
|
||||
$.ajax({
|
||||
url: "/app/metrics/" + service + "/table-metrics",
|
||||
beforeSend: function () {
|
||||
$('#table_metrics').html('<img class="loading_full_page" src="/app/static/images/loading.gif" />')
|
||||
$('#table_metrics').html('<img class="loading_full_page" src="/app/static/images/loading.gif" alt="loading..."/>')
|
||||
},
|
||||
type: "GET",
|
||||
success: function (data) {
|
||||
|
@ -375,11 +374,11 @@ function getChartDataHapWiRam(ip) {
|
|||
token: $('#token').val()
|
||||
},
|
||||
beforeSend: function() {
|
||||
$('#ram').html('<img class="loading_hapwi_overview" src="/app/static/images/loading.gif" />')
|
||||
$('#ram').html('<img class="loading_hapwi_overview" src="/app/static/images/loading.gif" alt="loading..." />')
|
||||
},
|
||||
type: "POST",
|
||||
success: function (result) {
|
||||
var data = [];
|
||||
let data = [];
|
||||
data.push(result.chartData.rams);
|
||||
// Получение значений из строки и разделение их на массив
|
||||
const ramsData = data[0].trim().split(' ');
|
||||
|
@ -502,7 +501,6 @@ function renderChartHapWiCpu(data) {
|
|||
labels: {
|
||||
color: 'rgb(255, 99, 132)',
|
||||
font: { size: 10, family: 'BlinkMacSystemFont' },
|
||||
color: 'black',
|
||||
boxWidth: 13,
|
||||
padding: 5
|
||||
},
|
||||
|
@ -559,7 +557,8 @@ $( function() {
|
|||
});
|
||||
});
|
||||
function removeData() {
|
||||
for (i = 0; i < charts.length; i++) {
|
||||
let chart;
|
||||
for (let i = 0; i < charts.length; i++) {
|
||||
chart = charts[i];
|
||||
chart.destroy();
|
||||
}
|
||||
|
@ -571,9 +570,9 @@ function showOverviewHapWI() {
|
|||
NProgress.configure({showSpinner: false});
|
||||
}
|
||||
function updatingCpuRamCharts() {
|
||||
if (cur_url[0] == 'overview.py') {
|
||||
if (cur_url[0] == 'overview') {
|
||||
showOverviewHapWI();
|
||||
} else if (cur_url[0] == 'hapservers.py' && cur_url[1].split('=')[0] == 'service') {
|
||||
} else if (cur_url[0] == 'service' && cur_url[2]) {
|
||||
NProgress.configure({showSpinner: false});
|
||||
showOverviewHapWI();
|
||||
getChartData(server_ip);
|
||||
|
@ -644,7 +643,6 @@ function renderSMONChart(data, labels, server) {
|
|||
labels: {
|
||||
color: 'rgb(255, 99, 132)',
|
||||
font: { size: 10, family: 'BlinkMacSystemFont' },
|
||||
color: 'black',
|
||||
boxWidth: 13,
|
||||
padding: 5
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ function sort_by_status() {
|
|||
$(".dis").prependTo("#dis_services");
|
||||
$('.group').remove();
|
||||
$('.group_name').detach();
|
||||
window.history.pushState("SMON Dashboard", "SMON Dashboard", cur_url[0]+"?action=view&sort=by_status");
|
||||
window.history.pushState("SMON Dashboard", "SMON Dashboard", "?sort=by_status");
|
||||
}
|
||||
function showSmon(action) {
|
||||
let sort = '';
|
||||
|
@ -30,6 +30,9 @@ function showSmon(action) {
|
|||
sort = '';
|
||||
}
|
||||
}
|
||||
if (action === 'not_sort') {
|
||||
window.history.pushState("SMON Dashboard", "SMON Dashboard", "/app/smon/dashboard");
|
||||
}
|
||||
$.ajax({
|
||||
url: "/app/smon/refresh",
|
||||
data: {
|
||||
|
|
Loading…
Reference in New Issue