v8.2.1: Refactor HA and Config Compare modules, optimize service installation flow, and add Spanish language support

- Replaced unused AJAX and JS methods (`cleanProvisioningProcess`, `showErrorStatus`) in HA management with simpler, streamlined equivalents. Moved HA cluster data fetching logic to the new endpoint (`HAClustersView`).
- Introduced `compareConfig()` method to improve configuration diff rendering. Removed redundant templates (`compare.html`), leveraging external Diff2Html library.
- Simplified HA service installation logic in `Reconfigure`, enabling task-based tracking. Removed redundant progress logic (`increaseProgressValue`, `checkInstallResp`).
- Added support for Spanish localization (`es-ES.html`, updated `languages.html` template).
- Replaced Python PagerDuty SDK for a more actively maintained library.
- Fixed description rendering logic in HA cluster templates.
- Updated Nginx to support multiple aliases per server.
- Minor dependency updates and comments cleanup in Ansible WAF tasks.
master
Aidaho 2025-07-03 17:57:53 +03:00
parent a3eb55749d
commit 073a86be80
49 changed files with 1768 additions and 461 deletions

View File

@ -15,7 +15,7 @@ from app.views.service.haproxy_section_views import ListenSectionView, UserListS
from app.views.service.nginx_section_views import UpstreamSectionView, ProxyPassSectionView
from app.views.service.lets_encrypt_views import LetsEncryptsView, LetsEncryptView
from app.views.service.haproxy_lists_views import HaproxyListView
from app.views.ha.views import HAView, HAVIPView, HAVIPsView
from app.views.ha.views import HAView, HAVIPView, HAVIPsView, HAClustersView
from app.views.user.views import UserView, UserGroupView, UserRoles
from app.views.udp.views import UDPListener, UDPListeners, UDPListenerActionView, UDPListenerBackendStatusView, UdpListenerCheckerView
from app.views.channel.views import ChannelView, ChannelsView
@ -58,6 +58,7 @@ def register_api_id_ip(view, endpoint, url: str = '', methods: list = ['GET', 'P
register_api(HAView, 'ha_cluster', '/ha/<service>', 'cluster_id')
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'])
bp.add_url_rule('/ha/<service>/clusters', view_func=HAClustersView.as_view('ha_clusters'), 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'])

View File

@ -55,7 +55,13 @@ def get_present_time():
:return: The current time in UTC.
:rtype: datetime.datetime
"""
present = datetime.now(timezone('UTC'))
try:
present = datetime.now(timezone('UTC'))
except ValueError as e:
if "Timezone offset does not match system offset" in str(e):
present = datetime.now()
else:
raise e
formatted_present = present.strftime('%b %d %H:%M:%S %Y %Z')
return datetime.strptime(formatted_present, '%b %d %H:%M:%S %Y %Z')

View File

@ -470,7 +470,7 @@ def show_config(server_ip: str, service: str, config_file_name: str, configver:
cfg = configs_dir + configver
try:
with open(cfg, 'r') as file:
with open(cfg, 'r', encoding='utf-8', errors='replace') as file:
conf = file.readlines()
except Exception as e:
raise Exception(f'error: Cannot read config file: {e}')

View File

@ -0,0 +1,22 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, Version
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
try:
Version.update(version='8.2.1').execute()
except Exception as e:
print(f"Error updating version: {str(e)}")
raise e
def down():
"""Roll back the migration."""
try:
Version.update(version='8.2.1').execute()
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -68,7 +68,7 @@ def waf_overview(serv: str, waf_service: str, claims: dict) -> str:
return render_template('ajax/overviewWaf.html', service_status=servers_sorted, role=role, waf_service=waf_service, lang=lang)
def change_waf_mode(waf_mode: str, server_id: int, service: str):
def change_waf_mode(waf_mode: str, server_id: int, service: str) -> None:
serv = server_sql.get_server(server_id)
if service == 'haproxy':
@ -77,11 +77,7 @@ def change_waf_mode(waf_mode: str, server_id: int, service: str):
config_dir = sql.get_setting('nginx_dir')
commands = f"sudo sed -i 's/^SecRuleEngine.*/SecRuleEngine {waf_mode}/' {config_dir}/waf/modsecurity.conf"
try:
server_mod.ssh_command(serv.ip, commands)
except Exception as e:
return str(e)
server_mod.ssh_command(serv.ip, commands)
roxywi_common.logging(serv.hostname, f'Has been changed WAF mod to {waf_mode}')
@ -99,11 +95,7 @@ def switch_waf_rule(serv: str, enable: int, rule_id: int):
cmd = "sudo sed -i 's!#" + rule_file_path + "!" + rule_file_path + "!' " + conf_file_path
en_for_log = 'enabled'
try:
roxywi_common.logging('WAF', f' Has been {en_for_log} WAF rule: {rule_file} for the server {serv}')
except Exception:
pass
roxywi_common.logging('WAF', f' Has been {en_for_log} WAF rule: {rule_file} for the server {serv}')
waf_sql.update_enable_waf_rules(rule_id, serv, enable)
server_mod.ssh_command(serv, cmd)
@ -126,10 +118,6 @@ def create_waf_rule(serv: str, service: str, json_data: dict) -> int:
cmd = f"sudo echo Include {rule_file_path} >> {conf_file_path} && sudo touch {rule_file_path}"
server_mod.ssh_command(serv, cmd)
last_id = waf_sql.insert_new_waf_rule(new_waf_rule, rule_file, new_rule_desc, service, serv)
try:
roxywi_common.logging('WAF', f'A new rule has been created {rule_file} on the server {serv}')
except Exception:
pass
roxywi_common.logging('WAF', f'A new rule has been created {rule_file} on the server {serv}')
return last_id

View File

@ -33,15 +33,15 @@ def ssh_command(server_ip: str, commands: str, **kwargs):
stdin, stdout, stderr = ssh.run_command(command, timeout=timeout)
stdin.close()
except Exception as e:
roxywi_common.handle_exceptions(e, server_ip, 'Something wrong with SSH connection. Probably sudo with password', roxywi=1)
roxywi_common.handle_exceptions(e, server_ip, 'Something wrong with SSH connection. Probably sudo with password')
if stderr:
for line in stderr.readlines():
if line:
roxywi_common.handle_exceptions(line, server_ip, line, roxywi=1)
roxywi_common.handle_exceptions(line, server_ip, line)
if stdout.channel.recv_exit_status() and kwargs.get('rc'):
roxywi_common.handle_exceptions(stdout.read().decode('utf-8'), server_ip, f'Cannot perform SSH command: {command} ', roxywi=1)
roxywi_common.handle_exceptions(stdout.read().decode('utf-8'), server_ip, f'Cannot perform SSH command: {command} ')
if kwargs.get('raw'):
return stdout.readlines()
@ -51,7 +51,7 @@ def ssh_command(server_ip: str, commands: str, **kwargs):
else:
return stdout.read().decode(encoding='UTF-8')
except Exception as e:
roxywi_common.handle_exceptions(e, server_ip, '', roxywi=1)
roxywi_common.handle_exceptions(e, server_ip, '')
def subprocess_execute(cmd):
@ -481,7 +481,7 @@ def delete_server(server_id: int) -> None:
history_sql.delete_action_history(server_id)
server_sql.delete_system_info(server_id)
service_sql.delete_service_settings(server_id)
roxywi_common.logging(server.ip, f'The server {server.hostname} has been deleted', roxywi=1, login=1)
roxywi_common.logging(server.ip, f'The server {server.hostname} has been deleted', login=1)
os.system(f'ssh-keygen -R {server.ip}')
@ -508,7 +508,7 @@ def change_server_services(server_id: int, server_name: str, server_services: di
try:
if service_sql.update_server_services(server_id, services_status[1], services_status[2], services_status[4], services_status[3]):
roxywi_common.logging('Roxy-WI server', f'Active services have been updated for {server_name}', roxywi=1, login=1)
roxywi_common.logging('Roxy-WI server', f'Active services have been updated for {server_name}', login=1)
return 'ok'
except Exception as e:
return f'error: {e}'

View File

@ -1,10 +1,10 @@
import os
import app.modules.db.sql as sql
from app.modules.service.installation import run_ansible
from app.modules.service.installation import run_ansible_thread
def generate_exporter_inc(server_ip: str, ver: str, exporter: str) -> object:
def generate_exporter_inv(server_ip: str, ver: str, exporter: str) -> object:
inv = {"server": {"hosts": {}}}
server_ips = [server_ip]
inv['server']['hosts'][server_ip] = {
@ -27,10 +27,11 @@ def generate_exporter_inc(server_ip: str, ver: str, exporter: str) -> object:
return inv, server_ips
def install_exporter(server_ip: str, ver: str, exporter: str) -> object:
def install_exporter(server_ip: str, ver: str, exporter: str) -> int:
service = f'{exporter.title()} exporter'
try:
inv, server_ips = generate_exporter_inc(server_ip, ver, exporter)
return run_ansible(inv, server_ips, f'{exporter}_exporter'), 201
inv, server_ips = generate_exporter_inv(server_ip, ver, exporter)
except Exception as e:
raise Exception(f'error: Cannot install {service}: {e}')
raise Exception(f'Cannot generate {exporter} inventory: {e}')
return run_ansible_thread(inv, server_ips, f'{exporter}_exporter', service)

View File

@ -31,7 +31,7 @@ def create_cluster(cluster: HAClusterRequest, group_id: int) -> int:
try:
cluster_id = ha_sql.create_cluster(cluster.name, cluster.syn_flood, group_id, cluster.description)
roxywi_common.logging(cluster_id, 'New cluster has been created', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, 'New cluster has been created', keep_history=1, service='HA cluster')
except Exception as e:
raise Exception(f'error: Cannot create new HA cluster: {e}')
@ -41,7 +41,7 @@ def create_cluster(cluster: HAClusterRequest, group_id: int) -> int:
try:
service_id = service_sql.select_service_id_by_slug(service)
ha_sql.insert_cluster_services(cluster_id, service_id)
roxywi_common.logging(cluster_id, f'Service {service} has been enabled on the cluster', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Service {service} has been enabled on the cluster', keep_history=1, service='HA cluster')
except Exception as e:
raise Exception(f'error: Cannot add service {service}: {e}')
@ -58,7 +58,7 @@ def create_cluster(cluster: HAClusterRequest, group_id: int) -> int:
vip_id = HaClusterVip.insert(cluster_id=cluster_id, router_id=router_id, vip=cluster.vip,
return_master=cluster.return_master).execute()
roxywi_common.logging(cluster_id, f'New vip {cluster.vip} has been created and added to the cluster',
keep_history=1, roxywi=1, service='HA cluster')
keep_history=1, service='HA cluster')
except Exception as e:
raise Exception(f'error: Cannon add VIP: {e}')
@ -113,9 +113,9 @@ def update_cluster(cluster: HAClusterRequest, cluster_id: int, group_id: int) ->
try:
add_or_update_virt(cluster, servers, cluster_id, virt.vip_id, group_id)
except Exception as e:
roxywi_common.logging(cluster_id, f'Cannot update cluster virtual server for VIP {virt.vip}: {e}', roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cannot update cluster virtual server for VIP {virt.vip}: {e}', service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster {cluster.name} has been updated', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster {cluster.name} has been updated', keep_history=1, service='HA cluster')
def delete_cluster(cluster_id: int) -> None:
@ -133,7 +133,7 @@ def delete_cluster(cluster_id: int) -> None:
HaCluster.delete().where(HaCluster.id == cluster_id).execute()
except HaCluster.DoesNotExist:
raise RoxywiResourceNotFound
roxywi_common.logging(cluster_id, 'Cluster has been deleted', roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, 'Cluster has been deleted', service='HA cluster')
def update_vip(cluster_id: int, router_id: int, cluster: Union[HAClusterRequest, HAClusterVIP], group_id: int) -> None:
@ -158,11 +158,11 @@ def update_vip(cluster_id: int, router_id: int, cluster: Union[HAClusterRequest,
try:
if ha_sql.check_ha_virt(vip_id):
ha_sql.delete_ha_virt(vip_id)
roxywi_common.logging(cluster_id, f'Cluster virtual server for VIP: {cluster.vip} has been deleted', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster virtual server for VIP: {cluster.vip} has been deleted', keep_history=1, service='HA cluster')
except Exception as e:
roxywi_common.logging(cluster_id, f'Cannot delete cluster virtual server for VIP {cluster.vip}: {e}', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cannot delete cluster virtual server for VIP {cluster.vip}: {e}', keep_history=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster VIP {cluster.vip} has been updated', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster VIP {cluster.vip} has been updated', keep_history=1, service='HA cluster')
def insert_vip(cluster_id: int, cluster: HAClusterVIP, group_id: int) -> int:
@ -205,7 +205,7 @@ def insert_vip(cluster_id: int, cluster: HAClusterVIP, group_id: int) -> int:
if cluster.virt_server:
add_or_update_virt(cluster, servers, cluster_id, vip_id, group_id)
roxywi_common.logging(cluster_id, f'New cluster VIP: {vip} has been created', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'New cluster VIP: {vip} has been created', keep_history=1, service='HA cluster')
return vip_id
@ -272,12 +272,13 @@ def add_or_update_virt(cluster: Union[HAClusterRequest, HAClusterVIP], servers:
if ha_sql.check_ha_virt(vip_id):
vip_from_db = ha_sql.select_cluster_vip_by_vip_id(cluster_id, vip_id)
vip = vip_from_db.vip
kwargs['ip'] = vip
if vip != kwargs['ip']:
kwargs['ip'] = vip
try:
ha_sql.update_ha_virt_ip(vip_id, **kwargs)
roxywi_common.logging(cluster_id, f'Cluster virtual server for VIP {vip} has been updated', keep_history=1, roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cluster virtual server for VIP {vip} has been updated', keep_history=1, service='HA cluster')
except Exception as e:
roxywi_common.logging(cluster_id, f'Cannot update cluster virtual server for VIP {vip}: {e}', roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'Cannot update cluster virtual server for VIP {vip}: {e}', service='HA cluster')
else:
try:
server = server_sql.get_server(master_id)
@ -292,14 +293,14 @@ def add_or_update_virt(cluster: Union[HAClusterRequest, HAClusterVIP], servers:
kwargs.setdefault('firewall_enable', server.firewall_enable)
kwargs.setdefault('group_id', group_id)
virt_id = server_sql.add_server(**kwargs)
roxywi_common.logging(cluster_id, f'New cluster virtual server for VIP: {vip} has been created', keep_history=1, roxywi=1,
roxywi_common.logging(cluster_id, f'New cluster virtual server for VIP: {vip} has been created', keep_history=1,
service='HA cluster')
except Exception as e:
roxywi_common.logging(cluster_id, f'error: Cannot create new cluster virtual server for VIP: {vip}: {e}', roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'error: Cannot create new cluster virtual server for VIP: {vip}: {e}', service='HA cluster')
try:
HaClusterVirt.insert(cluster_id=cluster_id, virt_id=virt_id, vip_id=vip_id).execute()
except Exception as e:
roxywi_common.logging(cluster_id, f'error: Cannot save cluster virtual server for VIP: {vip}: {e}', roxywi=1, service='HA cluster')
roxywi_common.logging(cluster_id, f'error: Cannot save cluster virtual server for VIP: {vip}: {e}', service='HA cluster')
def _create_or_update_master_slaves_servers(cluster_id: int, servers: dict, router_id: int, create: bool = False) -> None:

View File

@ -1,6 +1,8 @@
import os
import json
import random
import threading
from datetime import datetime
from typing import Union, Literal
from packaging import version
@ -18,6 +20,7 @@ import app.modules.common.common as common
import app.modules.server.server as server_mod
import app.modules.roxywi.common as roxywi_common
from app.modules.server.ssh import return_ssh_keys_path
from app.modules.db.db_model import InstallationTasks
from app.modules.roxywi.class_models import ServiceInstall, HAClusterRequest, HaproxyGlobalRequest, \
HaproxyDefaultsRequest, HaproxyConfigRequest
@ -171,7 +174,6 @@ def generate_section_inv(json_data: dict, cfg: str, service: Literal['haproxy',
"cfg": cfg,
"action": 'create'
}
return inv
@ -322,7 +324,7 @@ def run_ansible(inv: dict, server_ips: list, ansible_role: str) -> dict:
os.remove(inventory)
if result.rc != 0:
raise Exception('Something wrong with installation, check <a href="/app/logs/internal?log_file=roxy-wi.error.log" target="_blank" class="link">Apache logs</a> for details')
raise Exception('Something wrong with installation, check <a href="/logs/internal?log_file=roxy-wi.error.log" target="_blank" class="link">Apache logs</a> for details')
return result.stats
@ -376,13 +378,12 @@ def run_ansible_locally(inv: dict, ansible_role: str) -> dict:
os.remove(inventory)
if result.rc != 0:
raise Exception('Something wrong with installation, check <a href="/app/logs/internal?log_file=roxy-wi.error.log" target="_blank" class="link">Apache logs</a> for details')
raise Exception('Something wrong with installation, check <a href="/logs/internal?log_file=roxy-wi.error.log" target="_blank" class="link">Apache logs</a> for details')
return result.stats
def service_actions_after_install(server_ips: str, service: str, json_data) -> None:
is_docker = None
update_functions = {
'haproxy': service_sql.update_haproxy,
'nginx': service_sql.update_nginx,
@ -433,7 +434,7 @@ def _create_default_config_in_db(server_id: int) -> None:
add_sql.insert_new_section(server_id, 'listen', 'stats', stats_config)
def install_service(service: str, json_data: Union[str, ServiceInstall, HAClusterRequest], cluster_id: int = None) -> dict:
def install_service(service: str, json_data: Union[str, ServiceInstall, HAClusterRequest], cluster_id: int = None) -> int:
generate_functions = {
'haproxy': generate_haproxy_inv,
'nginx': generate_service_inv,
@ -453,14 +454,14 @@ def install_service(service: str, json_data: Union[str, ServiceInstall, HACluste
except Exception as e:
raise Exception(f'Cannot activate {service} on server {server_ips}: {e}')
try:
return run_ansible(inv, server_ips, service)
return run_ansible_thread(inv, server_ips, service, service.title())
except Exception as e:
raise Exception(f'Cannot install {service}: {e}')
def _install_ansible_collections():
old_ansible_server = ''
collections = ('community.general', 'ansible.posix', 'community.docker', 'community.grafana', 'ansible.netcommon')
collections = ('community.general', 'ansible.posix', 'community.docker', 'community.grafana', 'ansible.netcommon', 'ansible.utils')
trouble_link = 'Read <a href="https://roxy-wi.org/troubleshooting#ansible_collection" target="_blank" class="link">troubleshooting</a>'
proxy = sql.get_setting('proxy')
proxy_cmd = ''
@ -481,3 +482,33 @@ def _install_ansible_collections():
else:
if exit_code != 0:
raise Exception(f'error: Ansible collection installation was not successful: {exit_code}. {trouble_link}')
def run_ansible_thread(inv: dict, server_ips: list, ansible_role: str, service_name: str) -> int:
server_ids = []
claims = roxywi_common.get_jwt_token_claims()
for server_ip in server_ips:
server_id = server_sql.get_server_by_ip(server_ip).server_id
server_ids.append(server_id)
task_id = InstallationTasks.insert(
service_name=service_name, server_ids=server_ids, user_id=claims['user_id'], group_id=claims['group']
).execute()
thread = threading.Thread(target=run_installations, args=(inv, server_ips, ansible_role, task_id))
thread.start()
return task_id
def run_installations(inv: dict, server_ips: list, service: str, task_id: int) -> None:
try:
InstallationTasks.update(status='running').where(InstallationTasks.id == task_id).execute()
output = run_ansible(inv, server_ips, service)
if len(output['failures']) > 0 or len(output['dark']) > 0:
InstallationTasks.update(
status='failed', finish_date=datetime.now(), error=f'Cannot install {service}. Check Apache error log'
).where(InstallationTasks.id == task_id).execute()
roxywi_common.logging('', f'error: Cannot install {service}')
InstallationTasks.update(status='completed', finish_date=datetime.now()).where(InstallationTasks.id == task_id).execute()
except Exception as e:
InstallationTasks.update(status='failed', finish_date=datetime.now(), error=str(e)).where(InstallationTasks.id == task_id).execute()
roxywi_common.logging('', f'error: Cannot install {service}: {e}')

View File

@ -1,7 +1,7 @@
import json
import pika
import pdpyras
import pagerduty
import requests
import telebot
from telebot import apihelper
@ -256,7 +256,7 @@ def pd_send_mess(mess, level, server_ip=None, service_id=None, alert_type=None,
try:
proxy = sql.get_setting('proxy')
session = pdpyras.EventsAPISession(token)
session = pagerduty.EventsApiV2Client(token)
if server_ip:
dedup_key = f'{server_ip} {service_id} {alert_type}'
else:

View File

@ -1,6 +1,6 @@
import os
from flask import render_template, request, g
from flask import render_template, request, g, jsonify
from flask_jwt_extended import jwt_required, get_jwt
from app.routes.config import bp
@ -139,6 +139,7 @@ def config(service, serv, edit, config_file_name, new):
'config_file_name': config_file_name,
'is_serv_protected': is_serv_protected,
'service_desc': service_sql.select_service(service),
'user_subscription': roxywi_common.return_user_subscription(),
'lang': g.user_params['lang']
}
@ -310,7 +311,8 @@ def show_compare_config(service, serv):
'stderr': '',
'error': '',
'service_desc': service_sql.select_service(service),
'lang': g.user_params['lang']
'lang': g.user_params['lang'],
'user_subscription': roxywi_common.return_user_subscription(),
}
return render_template('config.html', **kwargs)
@ -325,10 +327,10 @@ def show_configs_for_compare(service, server_ip):
@bp.post('/compare/<service>/<server_ip>/show')
@check_services
def show_compare(service, server_ip):
left = common.checkAjaxInput(request.form.get('left'))
right = common.checkAjaxInput(request.form.get('right'))
return config_mod.compare_config(service, left, right)
left = request.json.get('left')
right = request.json.get('right')
compare = config_mod.compare_config(service, left, right)
return jsonify({'compare': compare})
@bp.route('/map/haproxy/<server_ip>/show')

View File

@ -17,6 +17,7 @@ import app.modules.service.installation as service_mod
import app.modules.service.exporter_installation as exp_installation
from app.views.install.views import InstallView
from app.modules.roxywi.class_models import DomainName
from app.modules.db.db_model import InstallationTasks
bp.add_url_rule(
@ -61,7 +62,8 @@ def install_exporter(exporter):
return jsonify({'status': 'failed', 'error': 'Wrong exporter'})
try:
return exp_installation.install_exporter(server_ip, ver, exporter)
task_id = exp_installation.install_exporter(server_ip, ver, exporter)
return jsonify({"status": "accepted", "tasks_ids": [task_id]}), 202
except Exception as e:
return jsonify({'status': 'failed', 'error': f'Cannot install {exporter.title()} exporter: {e}'})
@ -122,3 +124,9 @@ def check_geoip(service: Literal['haproxy', 'nginx'], server_ip: Union[IPvAnyAdd
service_dir = common.return_nice_path(sql.get_setting(f'{service}_dir'))
cmd = f"ls {service_dir}geoip/"
return server_mod.ssh_command(str(server_ip), cmd)
@bp.route('/task-status/<int:task_id>')
def get_task_status(task_id):
task = InstallationTasks.get(id=task_id)
return jsonify({'task_id': task_id, 'status': task.status, 'service_name': task.service_name, 'error': task.error}), 200

View File

@ -1,13 +1,15 @@
import os
from typing import Union, Literal
from flask import render_template, g, abort, jsonify, send_from_directory
from flask import render_template, g, abort, jsonify, send_from_directory, request
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from pydantic import IPvAnyAddress
import requests
from app import app, cache, jwt
from app.routes.main import bp
import app.modules.db.sql as sql
import app.modules.db.user as user_sql
import app.modules.db.server as server_sql
import app.modules.db.service as service_sql
@ -164,3 +166,35 @@ def service_history(service: str, server_ip: Union[IPvAnyAddress, DomainName, in
@cache.cached()
def show_roxywi_version():
return jsonify(roxy.versions())
@app.route("/api/gpt", methods=["POST"])
def ask_gpt():
data = request.get_json()
prompt = data.get("prompt", "")
try:
proxies = common.return_proxy_dict()
headers = {
"Content-Type": "application/json",
}
payload = {
"prompt": prompt,
"license": sql.get_setting('license')
}
response = requests.post(
"https://roxy-wi.org/api/gpt",
headers=headers,
json=payload,
proxies=proxies,
timeout=20
)
response.raise_for_status()
result = response.json()
if "error" in result:
return jsonify({"error": result["error"]}), 500
content = result["response"]
return jsonify({"response": content})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@ -64,7 +64,7 @@ vrrp_instance VI_{{router}} {
virtual_routes {
{% for k, slave in vip.items() %}
{% if k == ansible_host and slave is mapping %}
{{ (ansible_facts[slave.eth]["ipv4"]["address"] + "/" + ansible_facts[slave.eth]["ipv4"]["netmask"]) | ipaddr("0") }} dev {{ slave.eth }} src {{ vip.vip }} scope link table ha_cluster_{{ router }}
{{ (ansible_facts[slave.eth]["ipv4"]["address"] + "/" + ansible_facts[slave.eth]["ipv4"]["netmask"]) | ansible.utils.ipaddr("0") }} dev {{ slave.eth }} src {{ vip.vip }} scope link table ha_cluster_{{ router }}
{% endif %}
{% endfor %}
}

View File

@ -1,7 +1,7 @@
# Roxy-WI MANAGED do not edit it directly
{% if config.scheme == 'https' %}
server {
listen {{ config.port }} ssl{% if nginx_proxy.http2 %} http2{% endif %};
listen {{ config.port }} ssl{% if config.http2 %} http2{% endif %};
ssl_certificate {{ config.ssl_crt }};
ssl_certificate_key {{ config.ssl_key }};
{% if nginx_proxy.security.hsts %}
@ -12,7 +12,7 @@ server {
listen {{ config.port }};
{% endif %}
server_name {{ config.name }};
server_name {{ config.name }}{% if config.name_aliases %} {{ config.name_aliases | join(' ') }} {% endif %};
access_log /var/log/nginx/{{ config.name }}_access.log main buffer=16k flush=1m;
error_log /var/log/nginx/{{ config.name }}_error.log;

View File

@ -1,8 +1,6 @@
---
# Main server configuration
nginx_proxy:
http2: false # Enable HTTP/2 (requires SSL)
# Static files configuration
static_files:
enabled: false # Enable static file handling

View File

@ -34,6 +34,15 @@
- hap_config
- nginx_config
- apache_config
- name: Add CRON job for HAProxy
cron:
name: "Roxy-WI S3 Backup configs for server {{ SERVER }} {{ BUCKET }} {{ item }}"
special_time: "{{ TIME }}"
job: "{{ which_s3cmd.stdout }} sync /var/lib/roxy-wi/configs/{{ item }}/{{ SERVER }}*.cfg s3://{{ BUCKET }}/{{ SERVER }}/{{ item }}/ {{ keys }}"
with_items:
- hap_config
when: action == "add"
- name: Delete S3 Job

View File

@ -92,7 +92,7 @@
- gcc
- libpcre3-dev
- wget
- libcurl4-nss-dev
- libcurl4-openssl-dev
- libyajl-dev
- libxml2
- automake

View File

@ -1,104 +1,9 @@
.padding20{ width: 160px;}
.server-creating {padding-bottom: 10px;}
.proccessing, .processing_error, .proccessing_done {font-weight: bold; color: var(--blue-color);}
.proccessing_done {color: var(--green-color);}
.proccessing::before {
display: none;
font-family: "Font Awesome 5 Solid";
content: "\f35a";
}
.proccessing_done::before {
display: none;
font-family: "Font Awesome 5 Solid";
content: "\f058";
}
/*.proccessing .fa-arrow-alt-circle-right {*/
/* padding-right: 10px !important;*/
/* margin-bottom: -1px !important;*/
/*}*/
.proccessing .fa-arrow-alt-circle-right, .processing_error .svg-inline--fa {
padding-right: 10px !important;
margin-bottom: -1px !important;
}
.proccessing_done .fa-check-circle {
padding-right: 10px !important;
margin-bottom: -1px !important;
}
.processing_error { color: var(--red-color);}
.processing_warning { color: #efba22;}
.processing_error::before, .processing_warning::before {
display: none;
font-family: "Font Awesome 5 Solid";
content: "\f06a";
}
#creating-progress, #editing-progress {
clear: both;
width: 100%;
height: 10px;
}
.progress-bar-striped {
overflow: hidden;
height: 10px;
background-color: #f5f5f5;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-bar-striped > div {
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 40px 40px;
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 20px;
color: #ffffff;
text-align: center;
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-transition: width 2s ease;
-moz-transition: width 2s ease;
-o-transition: width 2s ease;
transition: width 2s ease;
animation: progress-bar-stripes 1s linear infinite;
background-color: #5d9ceb;
}
.progress-bar-striped p{
margin: 0;
}
@keyframes progress-bar-stripes {
0% {
background-position: 40px 0;
}
100% {
background-position: 0 0;
}
}
.alert-danger {
margin-bottom: 10px;
margin-top: 0;
}
.headers {
padding-left: 10px;
padding-top: 5px;
padding-bottom: 5px;
color: #aaa;
background-color: #eee;
.padding20 {
width: 160px;
}
.validateTips {
margin: 0;
}
.provisioning_table {
width: 99%;
}
.provisioning_table tr:last-of-type {
border: none;
}
.first-collumn {
width: 15%;
}
.fa-plus, .fa-edit, .fa-trash-alt {
width: 3px;
cursor: pointer;
@ -119,4 +24,4 @@
}
.div-server-hapwi {
height: 135px;
}
}

View File

@ -255,35 +255,6 @@ pre {
margin-left: -310px;
}
}
.configShow, .diff {
overflow: auto;
width: 95%;
border: 1px solid #DCDCDC;
border-radius: 5px;
}
.config {
height: 60%;
width: 60% !important;
}
.diffHead {
background-color: #F5F5F5;
padding: 10px;
}
.lineDog {
background-color: #dbedff;
margin-bottom: 10px;
padding: 7px;
}
.lineDiffMinus, .lineDiffPlus, .lineDiff {
padding-left: var(--indent);
white-space: pre;
}
.lineDiffMinus {
background-color: #ffdce0;
}
.lineDiffPlus {
background-color: #cdffd8;
}
.param {
font-weight: bold;
padding-left: 13px;

View File

@ -53,6 +53,14 @@ $( function() {
$("#add_header").click(function () {
make_actions_for_adding_header('#header_div');
});
$("#show_alias").on("click", function () {
$("#name_alias_div").show();
$("#add_name_alias").show();
$("#show_alias").hide();
});
$("#add_name_alias").click(function () {
make_actions_for_adding_alias('#name_alias_div');
});
for (let section_type of ['ssl_key', 'ssl_crt']) {
let cert_type = section_type.split('_')[1];
$("#" + section_type).autocomplete({
@ -114,6 +122,19 @@ function make_actions_for_adding_header(section_id) {
$( "select" ).selectmenu();
$('[name=headers_method]').selectmenu({width: 180});
}
var alias_option = '<p style="border-bottom: 1px solid #ddd; padding-bottom: 10px;" id="new_name_alias_p">\n' +
'<input type="text" name="name_alias" data-help="Domain name or IP" size="" style="" placeholder="www.example.com" title="Domain name or IP" class="form-control">' +
'\t<span class="minus minus-style" id="new_name_alias_minus" title="Delete this alias"></span>' +
'</p>'
function make_actions_for_adding_alias(section_id) {
let random_id = makeid(3);
$(section_id).append(alias_option);
$('#new_name_alias_minus').attr('onclick', 'deleteId(\''+random_id+'\')');
$('#new_name_alias_minus').attr('id', '');
$('#new_name_alias_p').attr('id', random_id);
$('#new_name_alias_minus').attr('id', '');
$.getScript(awesome);
}
function deleteId(id) {
$('#' + id).remove();
}
@ -192,6 +213,7 @@ function getNginxFormData($form, form_name) {
let indexed_array = {};
indexed_array['locations'] = [];
indexed_array['backend_servers'] = [];
indexed_array['name_aliases'] = [];
let headers = [];
$.map(unindexed_array, function (n, i) {
@ -222,10 +244,23 @@ function getNginxFormData($form, form_name) {
} else {
indexed_array['ssl_offloading'] = false;
}
} else if (n['name'] === 'http2') {
if ($('input[name="http2"]').is(':checked')) {
indexed_array['http2'] = true;
} else {
indexed_array['http2'] = false;
}
} else {
indexed_array[n['name']] = n['value'];
}
});
$('#name_alias_div p').each(function (){
let name = $(this).children("input[name='name_alias']").val();
if (name === undefined || name === '') {
return;
}
indexed_array['name_aliases'].push(name);
});
$('#'+form_name+' span[name="add_servers"] p').each(function (){
let server = $(this).children("input[name='servers']").val();
if (server === undefined || server === '') {
@ -239,7 +274,7 @@ function getNginxFormData($form, form_name) {
});
let elementsForDelete = [
'servers', 'server_port', 'max_fails', 'fail_timeout', 'proxy_connect_timeout', 'proxy_read_timeout', 'proxy_send_timeout',
'headers_res', 'header_name', 'header_value', 'upstream', 'server'
'headers_res', 'header_name', 'header_value', 'upstream', 'server', 'name_alias'
]
for (let element of elementsForDelete) {
delete indexed_array[element]

View File

@ -152,3 +152,55 @@ $( function() {
e.preventDefault();
});
})
function compareConfig(div_id, diff) {
const container = document.getElementById(div_id);
container.innerHTML = "";
const diffUi = new Diff2HtmlUI(container, diff, {
inputFormat: 'diff',
matching: 'lines',
fileListToggle: false,
drawFileList: true,
fileContentToggle: false,
outputFormat: 'line-by-line', // или 'line-by-line'
});
diffUi.draw();
diffUi.highlightCode();
}
function showCompare() {
$.ajax({
url: "/config/compare/" + $("#service").val() + "/" + $("#serv").val() + "/show",
data: JSON.stringify({
left: $('#left').val(),
right: $("#right").val(),
}),
contentType: "application/json; charset=utf-8",
type: "POST",
success: function (data) {
if (data.status === 'failed') {
toastr.error(data);
} else {
toastr.clear();
compareConfig('ajax', data.compare);
}
}
});
}
function showCompareConfigs() {
clearAllAjaxFields();
$('#ajax-config_file_name').empty();
$.ajax({
url: "/config/compare/" + $("#service").val() + "/" + $("#serv").val() + "/files",
type: "GET",
success: function (data) {
if (data.indexOf('error:') != '-1') {
toastr.error(data);
} else {
toastr.clear();
$("#ajax-compare").html(data);
$("input[type=submit], button").button();
$("select").selectmenu();
window.history.pushState("Show compare config", "Show compare config", '/config/compare/' + $("#service").val() + '/' + $("#serv").val());
}
}
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -746,6 +746,23 @@ function openNginxSection(section) {
}
}
if (section_type === 'proxy_pass') {
if (data.name_aliases) {
if (data.name_aliases.length > 0) {
i = 0;
$("#add_name_alias").on("click", function () {
$("#name_alias_div").show();
$("#add_name_alias").show();
$("#show_alias").hide();
});
$('#add_name_alias').trigger('click');
for (let alias of data.name_aliases) {
console.log(alias)
make_actions_for_adding_alias('#name_alias_div')
$(section_id + ' input[name="name_alias"]').get(i).value = alias;
i++;
}
}
}
for (let location of data.locations) {
if (location.headers) {
if (location.length > 0) {

View File

@ -35,20 +35,6 @@ $( function() {
get_interface($(this), server_ip);
});
});
function cleanProvisioningProccess(div_id, success_div, error_id, warning_id, progres_id) {
$(div_id).empty();
$(success_div).empty();
$(success_div).hide();
$(error_id).empty();
$(error_id).hide();
$(warning_id).empty();
$(warning_id).hide();
$(progres_id).css('width', '5%');
$(div_id).each(function () {
$(this).remove('');
});
$.getScript(awesome);
}
function confirmDeleteCluster(cluster_id) {
$("#dialog-confirm").dialog({
resizable: false,
@ -370,6 +356,9 @@ function saveCluster(jsonData, cluster_id=0, edited=0, reconfigure=0) {
if (!edited) {
cluster_id = data.id;
getHaCluster(cluster_id, true);
setTimeout(function () {
setInterval(checkHaClusterStatus, 10000, cluster_id);
}, 2500);
} else {
getHaCluster(cluster_id);
$("#cluster-" + cluster_id).addClass("update", 1000);
@ -386,74 +375,41 @@ function saveCluster(jsonData, cluster_id=0, edited=0, reconfigure=0) {
}
function Reconfigure(jsonData, cluster_id) {
let servers = JSON.parse(JSON.stringify(jsonData));
$("#wait-mess").html(wait_mess);
$("#wait-mess").show();
let total_installation = 1;
if (servers['services']['haproxy']['enabled']) {
total_installation = total_installation + 1;
}
if (servers['services']['nginx']['enabled']) {
total_installation = total_installation + 1;
}
if (servers['services']['apache']['enabled']) {
total_installation = total_installation + 1;
}
let server_creating_title = $("#server_creating1").attr('title');
let server_creating = $("#server_creating1").dialog({
autoOpen: false,
width: 574,
modal: true,
title: server_creating_title,
close: function () {
cleanProvisioningProccess('#server_creating1 ul li', '#created-mess', '#creating-error', '#creating-warning', '#creating-progress');
},
buttons: {
Close: function () {
$(this).dialog("close");
cleanProvisioningProccess('#server_creating1 ul li', '#created-mess', '#creating-error', '#creating-warning', '#creating-progress');
}
}
});
server_creating.dialog('open');
let progress_step = 100 / total_installation;
$.when(installServiceCluster(jsonData, 'keepalived', progress_step, cluster_id)).done(function () {
$.when(installServiceCluster(jsonData, 'keepalived', cluster_id)).done(function () {
if (servers['services']['haproxy']['enabled']) {
$.when(installServiceCluster(jsonData, 'haproxy', progress_step, cluster_id)).done(function () {
$.when(installServiceCluster(jsonData, 'haproxy', cluster_id)).done(function () {
if (servers['services']['nginx']['enabled']) {
$.when(installServiceCluster(jsonData, 'nginx', progress_step, cluster_id)).done(function () {
$.when(installServiceCluster(jsonData, 'nginx', cluster_id)).done(function () {
if (servers['services']['apache']['enabled']) {
installServiceCluster(jsonData, 'apache', progress_step, cluster_id);
installServiceCluster(jsonData, 'apache', cluster_id);
}
});
} else {
if (servers['services']['apache']['enabled']) {
installServiceCluster(jsonData, 'apache', progress_step, cluster_id);
installServiceCluster(jsonData, 'apache', cluster_id);
}
}
});
} else {
if (servers['services']['nginx']['enabled']) {
$.when(installServiceCluster(jsonData, 'nginx', progress_step, cluster_id)).done(function () {
$.when(installServiceCluster(jsonData, 'nginx', cluster_id)).done(function () {
if (servers['services']['apache']['enabled']) {
installServiceCluster(jsonData, 'apache', progress_step, cluster_id);
installServiceCluster(jsonData, 'apache', cluster_id);
}
});
} else {
if (servers['services']['apache']['enabled']) {
installServiceCluster(jsonData, 'apache', progress_step, cluster_id);
installServiceCluster(jsonData, 'apache', cluster_id);
}
}
}
});
}
function installServiceCluster(jsonData, service, progress_step, cluster_id) {
function installServiceCluster(jsonData, service, cluster_id) {
let servers = JSON.parse(JSON.stringify(jsonData));
servers['cluster_id'] = cluster_id;
let li_id = 'creating-' + service + '-';
let install_mess = translate_div.attr('data-installing');
let timeout_mess = translate_div.attr('data-roxywi_timeout');
let something_wrong = translate_div.attr('data-something_wrong');
$('#server_creating_list').append('<li id="' + li_id + servers['cluster_id'] + '" class="server-creating proccessing">' + install_mess + ' ' + nice_service_name[service] + '</li>');
return $.ajax({
url: "/install/" + service,
type: "POST",
@ -461,63 +417,21 @@ function installServiceCluster(jsonData, service, progress_step, cluster_id) {
contentType: "application/json; charset=utf-8",
statusCode: {
500: function () {
showErrorStatus(nice_service_name[service], servers["name"], li_id, servers['cluster_id'], progress_step, something_wrong);
toastr.error(something_wrong);
},
504: function () {
showErrorStatus(nice_service_name[service], servers["name"], li_id, servers['cluster_id'], progress_step, timeout_mess);
toastr.error(timeout_mess);
},
},
success: function (data) {
if (data.status === 'failed') {
showErrorStatus(nice_service_name[service], servers["name"], li_id, servers['cluster_id'], progress_step, something_wrong);
toastr.error(data.error);
} else {
checkInstallResp(data, servers['cluster_id'], progress_step, servers["name"], li_id, nice_service_name[service]);
runInstallationTaskCheck(data.tasks_ids);
}
}
});
}
function showErrorStatus(service_name, server_name, li_id, server_id, progress_step, message) {
let check_apache_log = translate_div.attr('data-check_apache_log');
$('#' + li_id + server_id).removeClass('proccessing');
$('#' + li_id + server_id).addClass('processing_error');
$.getScript(awesome);
$('#creating-error').show();
$('#creating-error').append('<div>' + message + ' ' + service_name + ' ' + server_name + '. '+check_apache_log+'</div>');
increaseProgressValue(progress_step);
}
function checkInstallResp(output, server_id, progress_step, name, li_id, service_name) {
output = JSON.parse(JSON.stringify(output));
let was_installed = translate_div.attr('data-was_installed');
let something_wrong = translate_div.attr('data-something_wrong');
for (let k in output['ok']) {
$('#' + li_id + server_id).removeClass('proccessing');
$('#' + li_id + server_id).addClass('proccessing_done');
$('#created-mess').show();
$('#created-mess').append('<div>' + service_name + ' ' + was_installed +' ' + k + '</div>');
}
for (let k in output['failures']) {
showErrorStatus(service_name, k, li_id, server_id, progress_step, something_wrong);
}
for (let k in output['dark']) {
showErrorStatus(service_name, k, li_id, server_id, progress_step, something_wrong);
}
increaseProgressValue(progress_step);
$.getScript(awesome);
}
function increaseProgressValue(progress_step) {
let progress_id = '#creating-progress';
let waid_id = '#wait-mess';
progress_step = Math.ceil(parseFloat(progress_step));
let cur_proggres_value = $(progress_id).css('width').split('px')[0] / $(progress_id).parent().width() * 100;
let new_progress = Math.ceil(parseFloat(cur_proggres_value)) + progress_step;
new_progress = Math.ceil(parseFloat(new_progress));
if (parseFloat(new_progress) > 90) {
$(waid_id).hide();
new_progress = parseFloat(100);
$('.progress-bar-striped > div').css('animation', '');
}
$(progress_id).css('width', new_progress+'%');
}
function add_vip_ha_cluster(cluster_id, cluster_name, vip_id='', vip='', edited=0) {
let save_word = translate_div.attr('data-save');
let tabel_title = $("#add-vip-table").attr('title');
@ -848,6 +762,11 @@ function clearClusterDialog(edited=0) {
function getHaCluster(cluster_id, new_cluster=false) {
$.ajax({
url: "/ha/cluster/get/" + cluster_id,
statusCode: {
404: function (xhr) {
$('#cluster-' + cluster_id).remove();
}
},
success: function (data) {
data = data.replace(/^\s+|\s+$/g, '');
if (data.indexOf('error:') != '-1') {
@ -910,7 +829,82 @@ function checkHaClusterStatus(cluster_id) {
listener_div.removeClass('div-server-head-dis');
listener_div.attr('title', 'Not all services are UP');
}
checkHAClusterServices(cluster_id);
}
});
NProgress.configure({showSpinner: true});
}
function checkHAClusterServices(cluster_id) {
$.ajax({
url: api_prefix + "/ha/cluster/" + cluster_id,
success: function (data) {
for (const [key, value] of Object.entries(data.services)) {
if (value['enabled']) {
checkHaClusterService(key, data.vip, cluster_id);
}
}
}
});
}
function checkHaClusterService(service, vip_ip, cluster_id) {
$.ajax({
url: api_prefix + "/service/" + service + "/" + vip_ip + "/status",
statusCode: {
500: function (data) {
console.log(data.error);
},
504: function (data) {
console.log(data.error);
},
},
success: function (data) {
let service_div = $('#' + service + '-' + cluster_id + '-status');
if (data.status === 'failed') {
service_div.removeClass('serverNone');
service_div.removeClass('serverUp');
service_div.addClass('serverDown');
service_div.attr('title', service + ' is Down');
} else if (data.status === 'running') {
service_div.removeClass('serverNone');
service_div.removeClass('serverDown');
service_div.addClass('serverUp');
service_div.attr('title', service + ' is UP');
} else {
service_div.removeClass('serverNone');
service_div.removeClass('serverUp');
service_div.addClass('serverDown');
service_div.attr('title', service + ' is Down');
}
}
});
}
function getClusters() {
$.ajax({
url: api_prefix + "/ha/cluster/clusters",
success: function (data) {
let clusters = document.querySelectorAll('.up-pannel > [id^="cluster-"]');
let clusterNumbers = Array.from(clusters).map(div =>
parseInt(div.id.replace('cluster-', ''), 10)
);
if (data.status === 'failed') {
toastr.error(data.error);
return;
}
if (data.length > 0) {
const dataIds = data.map(item => item.id);
const idsToDelete = clusterNumbers.filter(id => !dataIds.includes(id));
for (const [key, value] of Object.entries(data)) {
if (!clusterNumbers.includes(value.id)) {
getHaCluster(value.id, true);
} else {
getHaCluster(value.id, false);
}
}
for (let id of idsToDelete) {
$('#cluster-' + id).remove();
}
}
}
});
}

View File

@ -134,7 +134,6 @@ function checkGeoipInstallation() {
});
}
function installService(service) {
$("#ajax").html('')
let syn_flood = 0;
let docker = 0;
let select_id = '#' + service + 'addserv';
@ -165,7 +164,6 @@ function installService(service) {
jsonData['servers'].push(server);
jsonData['services'][service]['enabled'] = 1;
jsonData['services'][service]['docker'] = docker;
$("#ajax").html(wait_mess);
$.ajax({
url: "/install/" + service + "/" + $(select_id).val(),
500: function () {
@ -181,16 +179,12 @@ function installService(service) {
if (data.status === 'failed') {
toastr.error(data.error);
} else {
parseAnsibleJsonOutput(data, nice_names[service], select_id);
$(select_id).trigger("selectmenuchange");
$("#ajax").empty();
runInstallationTaskCheck(data.tasks_ids);
}
}
});
}
function installExporter(exporter) {
$("#ajaxmon").html('');
$("#ajaxmon").html(wait_mess);
let exporter_id = '#' + exporter + '_exp_addserv';
let ext_prom = 0;
let nice_exporter_name = nice_names[exporter] + ' exporter';
@ -202,7 +196,6 @@ function installExporter(exporter) {
"exporter_v": $('#' + exporter + 'expver').val(),
"ext_prom": ext_prom,
}
$("#ajax").html(wait_mess);
$.ajax({
url: "/install/exporter/" + exporter,
500: function () {
@ -218,9 +211,7 @@ function installExporter(exporter) {
if (data.status === 'failed') {
toastr.error(data.error);
} else {
parseAnsibleJsonOutput(data, nice_names[service], exporter_id);
$(exporter_id).trigger("selectmenuchange");
$("#ajax").empty();
runInstallationTaskCheck(data.tasks_ids);
}
}
});

View File

@ -1,30 +0,0 @@
{% import 'languages/'+lang|default('en')+'.html' as lang %}
<div class="out left-space">
<div class="diff">
{% set plus = [] %}
{% set minus = [] %}
{% set total_change = 0 %}
{% for line in stdout -%}
{%- if loop.index0 == 0 -%}
<div class="diffHead">{{ line }}<br />
{%- elif loop.index0 == 1 %}
{{ line }}</div>
{% elif line.startswith('-') and loop.index0 > 1 %}
<div class="lineDiffMinus">{{ line }}</div>
{% do minus.append(1) %}
{% elif line.startswith('+') and loop.index0 > 2 %}
<div class="lineDiffPlus">{{ line }}</div>
{% do plus.append(1) %}
{% elif line.startswith('@') %}
<div class="lineDog">{{ line }}</div>
{% else %}
<div class="lineDiff">{{ line }}</div>
{%- endif %}
{% endfor %}
{% set total_change = minus + plus %}
<div class="diffHead">
{{lang.words.total|title()}} {{lang.words.changes}}: {{ total_change|length }}, {{lang.words.additions}}: {{ plus|length }} & {{lang.words.deletions}}: {{ minus|length }}
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
{% import 'languages/'+lang|default('en')+'.html' as lang %}
{% from 'include/input_macros.html' import input, checkbox, select %}
<div class="left-space">
<div>
<h4>{{lang.words.config|title()}} {% if config_file_name != 'undefined' %}{{config_file_name.replace('92', '/')}}{%endif%} {{lang.words.from}} {{serv}} ({{hostname}})</h4>
<p class="accordion-expand-holder">
{% if role <= 3 %}
@ -25,7 +25,7 @@
</p>
</div>
<div class="configShow left-space">
<div class="configShow">
{% set i = 0 -%}
{% set section_name = {} %}
{% for line in conf -%}

View File

@ -5,7 +5,7 @@
<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>
<span id="cluster-desc-{{cluster.id}}">{% if cluster.desc != '' %} ({{cluster.description|replace("'", "")}}) {% endif %}</span>
<span id="cluster-desc-{{cluster.id}}">{% if cluster.description != '' and cluster.description != 'None' %} ({{cluster.description|replace("'", "")}}) {% endif %}</span>
</a>
<span class="server-action">
{% if g.user_params['role'] <= 3 %}
@ -60,6 +60,7 @@
{% if c_s.cluster_id|string() == cluster.id|string() %}
{% for service in services %}
{% if c_s.service_id|string() == service.service_id|string() %}
<span id="{{ service.slug }}-{{ cluster.id }}-status" class="serverNone server-status-small" title="Service is none"></span>
<a href="/service/{{service.slug}}">{{service.service}}</a>
{% endif %}
{% endfor %}

View File

@ -1,5 +1,8 @@
{% import 'languages/'+lang|default('en')+'.html' as lang1 %}
{% from 'include/input_macros.html' import copy_to_clipboard %}
<link rel="stylesheet" href="/static/js/diff2html/diff2html.min.css">
<script src="/static/js/diff2html/diff2html.min.js"></script>
<script src="/static/js/diff2html/diff2html-ui.min.js"></script>
<script src="/static/js/configshow.js"></script>
{% if for_delver == '1' %}
<script>
@ -38,16 +41,6 @@
}
}
</script>
<style>
.diff {
overflow: auto;
width: 100%;
border: 1px solid #DCDCDC;
border-radius: 5px;
background-color: #fff;
margin: 0;
}
</style>
<form action="/config/{{ service }}/{{ server_ip }}/versions" method="delete" id="delete_versions_form">
<table class="overview hover order-column display compact" id="table_version">
<thead>
@ -97,8 +90,9 @@
{% else %}
<a id="link_show_diff_{{c.id}}" onclick="show_diff('{{c.id}}')" title="Show a difference between this config and previous one" class="link">{{lang1.words.show|title()}} {{lang1.words.diff2}}</a>
<div id="show_diff_{{c.id}}" style="display: none;">
{% set stdout = c.diff.split('\n') %}
{% include 'ajax/compare.html' %}
{% if c.diff|length > 0 %}
<script>compareConfig('show_diff_{{c.id}}', `{{ c.diff }}`)</script>
{% endif %}
</div>
{% endif %}
</td>

View File

@ -18,18 +18,17 @@
<span title="{{lang.words.last|title()}} {{lang.words.activity}}: {{last_login_date}}, IP: {{USER.last_login_ip}}">{{ USER.username }}</span>
</td>
<td class="third-collumn-wi">
{% for group in users_groups %}
{% if USER.user_id|string() == group.user_id|string() %}
<span title="{{ group.groups.name }}: {{ group.groups.description }}"> {{ group.groups.name }}</span>
{% endif %}
{% endfor %}
{%- set spans = [] -%}
{%- for group in users_groups if group.user_id == USER.user_id -%}
{%- set _ = spans.append('<span title="' ~ group.groups.name ~ ': ' ~ group.groups.description ~ '">' ~ group.groups.name ~ '</span>') -%}
{%- endfor -%}
{{ spans | join(', ') | safe }}
</td>
<td>
{% for r in roles %}
{% if r.role_id == USER.role_id|int() %}
{{ r.name }}
{% endif %}
{% endfor %}
{{ roles
| selectattr('role_id', 'equalto', USER.role_id | int)
| map(attribute='name')
| first }}
</td>
<td></td>
<td></td>
@ -50,18 +49,17 @@
<span title="{{lang.words.last|title()}} {{lang.words.activity}}: {{last_login_date}}, IP: {{USER.last_login_ip}}">{{ USER.username }}</span>
</td>
<td class="third-collumn-wi">
{% for group in users_groups %}
{% if group.user_id|string() == USER.user_id|string() %}
<span title="{{ group.groups.name }}: {{ group.groups.description }}"> {{ group.groups.name }}</span>
{% endif %}
{% endfor %}
{%- set spans = [] -%}
{%- for group in users_groups if group.user_id == USER.user_id -%}
{%- set _ = spans.append('<span title="' ~ group.groups.name ~ ': ' ~ group.groups.description ~ '">' ~ group.groups.name ~ '</span>') -%}
{%- endfor -%}
{{ spans | join(', ') | safe }}
</td>
<td>
{% for r in roles %}
{% if r.role_id == USER.role_id|int() %}
{{ r.name }}
{% endif %}
{% endfor %}
{{ roles
| selectattr('role_id', 'equalto', USER.role_id | int)
| map(attribute='name')
| first }}
</td>
<td></td>
<td></td>

View File

@ -45,7 +45,7 @@
{% endif %}
{% block content %}{% endblock %}
<div id="ajax-compare"></div>
<div id="ajax"></div>
<div id="ajax" class="left-space"></div>
</div>
<div id="show-updates" style="display: none;">
<div>

View File

@ -24,10 +24,14 @@
<script src="/static/js/codemirror/mode/nginx.js"></script>
<script src="/static/js/codemirror/mode/haproxy.js"></script>
<script src="/static/js/codemirror/keymap/sublime.js"></script>
<link rel="stylesheet" href="/static/js/diff2html/diff2html.min.css">
<script src="/static/js/diff2html/diff2html.min.js"></script>
<script src="/static/js/diff2html/diff2html-ui.min.js"></script>
<script src="/static/js/configshow.js"></script>
<script src="/static/js/add.js"></script>
<script src="/static/js/add_nginx.js"></script>
<script src="/static/js/edit_config.js"></script>
{% if is_serv_protected and g.user_params['role'] > 2 %}
<meta http-equiv="refresh" content="0; url=/service">
{% else %}
@ -57,7 +61,6 @@
<a href="/admin#backup" class="ui-button ui-widget ui-corner-all" title="Git">Git</a>
{% endif %}
</form>
</p>
<div id="ajax-config_file_name"></div>
{% endif %}
@ -196,5 +199,246 @@
});
{% endif %}
</script>
{% if user_subscription.user_status != 0 %}
<script>
function explainSelected(mode) {
const selectedText = myCodeMirror.getSelection();
const from = myCodeMirror.getCursor("from");
const to = myCodeMirror.getCursor("to");
if (!selectedText.trim()) {
alert("{{ lang.phrases.select_part }}");
return;
}
let prompt = "";
let previewMode = "replace"; // default
if (mode === "explain") {
prompt = `{{ lang.phrases.explain_config }} {{ service }}. {{ lang.phrases.just_explain }}`;
previewMode = "explanation"; // не заменяем текст
} else if (mode === "optimize") {
prompt = `{{ lang.phrases.optimize_config }}`;
} else if (mode === "commented") {
prompt = `{{ lang.phrases.comment_config }}`;
} else if (mode === "syntax") {
prompt = `{{ lang.phrases.syntax_config }}`;
}
NProgress.start();
fetch("/api/gpt", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
prompt: `${prompt}\n\n${selectedText}`
})
})
.then(response => response.text().then(text => {
if (!response.ok) {
// Пробуем получить error из JSON, если он там есть
let message = text;
try {
const json = JSON.parse(text);
if (json.error) {
message = json.error;
}
} catch (_) {
// Тело не JSON — оставим как есть
}
NProgress.done();
return alert(`Ошибка API (${response.status}):\n\n${message}`);
}
let data;
try {
data = JSON.parse(text);
} catch (e) {
NProgress.done();
alert("{{ lang.phrases.error_parsing }}");
return;
}
let result = typeof data.response === "string" ? data.response.trim() : "";
if (!result) {
NProgress.done();
return alert("{{ lang.phrases.empty_ans }}");
}
result = result.replace(/^```(?:[a-zA-Z]+)?\s*/gm, "");
result = result.replace(/```$/gm, "");
if (previewMode === "explanation" || previewMode === "syntax") {
showExplanation(result);
} else {
showPreview(selectedText, result, from, to);
}
NProgress.done();
}))
.catch(err => {
NProgress.done();
alert("Error: " + err.message);
console.error(err);
});
}
let previewOld = null;
let previewNew = null;
let previewFrom = null;
let previewTo = null;
function showExplanation(text) {
const container = document.getElementById("previewContent");
container.innerHTML = `<pre style="white-space:pre-wrap;">${text}</pre>`;
document.getElementById("applyButton").style.display = "none";
document.getElementById("previewModal").style.display = "block";
}
function showPreview(oldCode, newCode, from, to) {
previewOld = oldCode;
previewNew = newCode;
previewFrom = from;
previewTo = to;
const diffString = createUnifiedDiff(oldCode, newCode);
const container = document.getElementById("previewContent");
// Очищаем старое содержимое
container.innerHTML = "";
const diffUi = new Diff2HtmlUI(container, diffString, {
drawFileList: false,
matching: 'lines',
outputFormat: 'side-by-side'
});
diffUi.draw();
diffUi.highlightCode();
document.getElementById("previewModal").style.display = "block";
document.getElementById("applyButton").style.display = "inline-block";
}
function closePreview() {
document.getElementById("previewModal").style.display = "none";
document.querySelector("#previewModal button").disabled = false;
previewOld = previewNew = previewFrom = previewTo = null;
}
function applyPreview() {
if (previewNew && previewFrom && previewTo) {
myCodeMirror.replaceRange(previewNew.trim(), previewFrom, previewTo);
}
closePreview();
}
function createUnifiedDiff(oldText, newText) {
const oldLines = oldText.trim().split("\n");
const newLines = newText.trim().split("\n");
const fileName = "config.conf";
const header = `diff --git a/${fileName} b/${fileName}\n--- a/${fileName}\n+++ b/${fileName}\n`;
const diffLines = [header];
const maxLines = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLines; i++) {
const oldLine = oldLines[i] || "";
const newLine = newLines[i] || "";
if (oldLine === newLine) {
diffLines.push(" " + oldLine);
} else {
if (oldLine) diffLines.push("-" + oldLine);
if (newLine) diffLines.push("+" + newLine);
}
}
return diffLines.join("\n");
}
function doClipboardAction(action) {
const text = myCodeMirror.getSelection();
if (!text && action !== "paste") return;
navigator.clipboard[action === "paste" ? "readText" : "writeText"](
action === "paste" ? undefined : text
).then(result => {
if (action === "cut") {
myCodeMirror.replaceSelection("");
} else if (action === "paste") {
myCodeMirror.replaceSelection(result);
}
hideCustomMenu();
}).catch(err => {
alert("Clipboard error: " + err.message);
console.error(err);
});
}
function handleContextAction(mode) {
hideCustomMenu();
explainSelected(mode);
}
function hideCustomMenu() {
document.getElementById("customContextMenu").style.display = "none";
}
document.addEventListener("contextmenu", function (e) {
const isInCodeMirror = e.target.closest(".CodeMirror");
if (!isInCodeMirror) return;
e.preventDefault(); // блокируем стандартное меню
const menu = document.getElementById("customContextMenu");
menu.style.left = `${e.pageX + 8}px`;
menu.style.top = `${e.pageY}px`;
menu.style.display = "block";
setTimeout(() => {
document.addEventListener("click", hideCustomMenu, { once: true });
}, 1);
});
</script>
<style>
.context-item {
padding: 6px 12px;
cursor: pointer;
}
.context-item:hover {
background-color: #f0f0f0;
}
</style>
<div id="customContextMenu" style="
display: none;
position: absolute;
background: #fff;
border: 1px solid #ccc;
z-index: 10000;
padding: 5px;
font-family: sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
min-width: 180px;
border-radius: 6px;
">
<div class="context-item" onclick="doClipboardAction('cut')">✂️ {{ lang.words.w_cut|title() }}</div>
<div class="context-item" onclick="doClipboardAction('copy')">📋 {{ lang.words.w_copy|title() }}</div>
<div class="context-item" onclick="doClipboardAction('paste')">📥 {{ lang.words.w_paste|title() }}</div>
<hr>
<div class="context-item" onclick="handleContextAction('explain')">💬 {{ lang.phrases.explain_selection }}</div>
<div class="context-item" onclick="handleContextAction('optimize')">⚙️ {{ lang.phrases.optimize_selection }}</div>
<div class="context-item" onclick="handleContextAction('commented')">📝 {{ lang.phrases.enter_comment }}</div>
<div class="context-item" onclick="handleContextAction('syntax')">✅ {{ lang.phrases.check_syntax }}</div>
</div>
{% endif %}
{% endif %}
<div id="previewModal" style="display:none; position:fixed; top:10%; left:50%; transform:translateX(-50%);
background:white; border:1px solid #ccc; padding:20px; z-index:9999; width:80%; max-height:80%; overflow:auto; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3>{{ lang.phrases.preview_change }}</h3>
<div id="previewContent"></div>
<div id="previewButtons" style="text-align:right; margin-top:10px;">
<button id="applyButton" onclick="applyPreview()">{{ lang.words.apply|title() }}</button>
<button onclick="closePreview()">{{ lang.words.cancel|title() }}</button>
</div>
</div>
{% endblock %}

View File

@ -22,11 +22,7 @@
{% if g.user_params['role'] <= 3 %}
<div class="add-button add-button-big" title="{{lang.phrases.create_ha}}" onclick="createHaClusterStep1();">+ {{lang.ha_page.create_ha}}</div>
{% endif %}
<div class="up-pannel" class="sortable">
{% for cluster in clusters %}
<div id="cluster-{{cluster.id}}" class="div-server-hapwi animated-background"></div>
{% endfor %}
</div>
<div class="up-pannel" class="sortable"></div>
<div id="create-ha-cluster-step-1" style="display: none;">
<table class="overview" id="create-ha-cluster-step-1-overview"
title="{{lang.words.create|title()}} {{lang.ha_page.ha}} {{lang.words.cluster}}"
@ -184,21 +180,9 @@
<div id="dialog-confirm" style="display: none;">
<p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>{{lang.phrases.are_you_sure}}</p>
</div>
<div id="server_creating1" style="display: none;" title="{{lang.phrases.creating_ha}}">
<ul style="padding: 20px 20px 0px 20px;font-size: 15px;" id="server_creating_list"></ul>
<div id="created-mess" class="alert alert-success" style="display:none;"></div>
<div id="creating-error" class="alert alert-danger" style="display:none;"></div>
<div id="creating-warning" class="alert alert-warning" style="display:none;"></div>
<div id="wait-mess"></div>
<div class="progress-bar-striped">
<div id="creating-progress" style="width: 5%;"></div>
</div>
</div>
<script>
{% for cluster in clusters %}
getHaCluster('{{cluster.id}}');
setInterval(getHaCluster, 60000, '{{cluster.id}}');
{% endfor %}
getClusters();
setInterval(getClusters, 60000);
</script>
{% endif %}
{% endblock %}

View File

@ -15,7 +15,7 @@
<div class="tooltip tooltipTop"><b>{{lang.words.note|title()}}:</b> {{lang.phrases.master_slave}}</div>
</td>
<td rowspan="5" class="add-note addName alert-info">
Proxy pass is a Nginx directive that forwards incoming client requests to a specified backend server (like Apache, Node.js, or a microservice) and relays the response back to the client. It supports protocols like HTTP, HTTPS, WebSocket, and gRPC, making it versatile for reverse proxying. Key parameters include:
Proxy pass is an Nginx directive that forwards incoming client requests to a specified backend server (like Apache, Node.js, or a microservice) and relays the response back to the client. It supports protocols like HTTP, HTTPS, WebSocket, and gRPC, making it versatile for reverse proxying. Key parameters include:
<br />
<br />
Header control: Uses proxy_set_header to forward client IP (X-Real-IP) or protocol (X-Forwarded-Proto).
@ -33,6 +33,21 @@ Timeouts: Timeouts (proxy_connect_timeout).
{{ input('proxy_pass', name='name', title="Domain name or IP", placeholder="example.com", required='required') }}
</td>
</tr>
<tr>
<td class="addName">{{lang.words.name|title()}} alias:</td>
<td class="addOption">
<span title="{{lang.words.add|title()}} {{lang.words.name}}" id="show_alias" class="link add-server"></span>
<div id="name_alias_div" style="display: none">
<p style="border-bottom: 1px solid #ddd; padding-bottom: 10px;" id="name_alias_p">
{{ input('proxy_pass_alias', name='name_alias', title="Domain name or IP", placeholder="www.example.com") }}
<span class="minus minus-style" onclick="deleteId('name_alias_p')" title="{{lang.words.delete|title()}}"></span>
</p>
</div>
<span>
<a class="link add-server" id="add_name_alias" title="{{lang.words.add|title()}} {{lang.words.name}}" style="display: none;"></a>
</span>
</td>
</tr>
<tr>
<td class="addName">
HTTP {{lang.words.scheme|title()}}:
@ -47,6 +62,7 @@ Timeouts: Timeouts (proxy_connect_timeout).
<span class="tooltip tooltipTop">{{lang.words.enter2|title()}} {{lang.words.name}} {{lang.words.of}} {{lang.words.file2}}, {{lang.add_page.desc.press_down}}:</span><br />
{{ input('ssl_key', placeholder='cert.key') }}
{{ input('ssl_crt', placeholder='cert.crt') }}
<p>{{ checkbox('http2', title=lang.add_page.desc.enable_http2, desc='Enable HTTP2') }}</p>
</div>
</td>
</tr>
@ -89,6 +105,9 @@ Timeouts: Timeouts (proxy_connect_timeout).
</span>
<br>
Upstream: {{ input('proxy_pass-upstream', name='upstream', placeholder='upstream_config') }}
<div class="tooltip tooltipTop">
<b>{{lang.words.note|title()}}</b>: {{lang.add_nginx_page.desc.def_backend}}, <span title="{{lang.words.create|title()}} {{lang.words.upstream}}" class="redirectUpstream link">{{lang.add_nginx_page.desc.def_backend_exit}}</span>.
</div>
</p>
</td>
</tr>

View File

@ -121,13 +121,11 @@
}
}
%}
{% set services = dict() %}
{% set services = {
"hapservers_desc": "Checker monitors the service. If the service changes its status, Checker will alert via Telegram, Slack, Email, Web",
"last_edit": "Last edit"
}
%}
{% set errors = dict() %}
{% set errors = {
"cannot_get_info": "Cannot get information about",
"something_wrong": "Something went wrong with installation on",
@ -237,8 +235,20 @@
},
}
%}
{% set phrases = dict() %}
{% set phrases = {
"explain_config": "Explain what this configuration fragment does.",
"just_explain": "Just give an explanation, don't rewrite.",
"optimize_config": "Optimize the configuration fragment. Keep the syntax.",
"comment_config": "Insert comments (starting with #) into each line of this fragment. Keep the config working. Keep the syntax.",
"syntax_config": "Check syntax. Keep the config working. Keep the syntax.",
"empty_ans": "Empty answer.",
"explain_selection": "Explain the highlighted",
"optimize_selection": "Optimize config",
"enter_comment": "Add comments",
"preview_change": "Preview changes",
"select_part": "Select a section of the config.",
"error_parsing": "Error parsing JSON from server.",
"check_syntax": "Check syntaxis",
"config_file_name": "Config file name",
"no_events_added": "No events added yet.",
"upload_and_restart": "Upload and restart",
@ -358,6 +368,7 @@
"back_des1": "A 'backend' section describes a set of servers to which the proxy will connect to forward incoming connections.",
"ssl_offloading": "The term SSL termination means that you are performing all encryption and decryption at the edge of your network, such as at the load balancer.",
"http_https": "Enable redirection from HTTP scheme to HTTPS scheme",
"enable_http2": "Enable HTTP 2",
"maxconn_desc": "This value should not exceed the global maxconn. Default global maxconn value",
"maxconn_fix": "Fix the maximum number of concurrent connections on a frontend",
"antibot": "Unfortunately, a large portion of bots are used for malicious reasons. Their intentions include web scraping, spamming, request flooding, brute forcing, and vulnerability scanning. For example, bots may scrape your price lists so that competitors can consistently undercut you or build a competitive solution using your data. Or they may try to locate forums and comment sections where they can post spam. At other times, theyre scanning your site looking for security weaknesses",
@ -369,7 +380,6 @@
"cookie": "To send a client to the same server where they were sent previously in order to reuse a session on that server, you can enable cookie-based session persistence. Add a cookie directive to the backend section and set the cookie parameter to a unique value on each server line.",
"c_prefix": "This keyword indicates that instead of relying on a dedicated cookie for the persistence, an existing one will be completed",
"c_nocache": "This option is recommended in conjunction with the insert mode when there is a cache between the client and HAProxy",
"c_nocache": "This option is recommended in conjunction with the insert mode when there is a cache between the client and HAProxy",
"c_postonly": "This option ensures that cookie insertion will only be performed on responses to POST requests",
"c_dynamic": "Activate dynamic cookies. When used, a session cookie is dynamically created for each server",
"def_backend": "If you want to use the default backend",
@ -389,7 +399,7 @@
"userlist_group": "It is also possible to attach users to this group by using a comma separated list of names preceded by 'users' keyword",
"userlist_desc": "It is possible to control access to frontend/backend/listen sections or to http stats by allowing only authenticated and authorized users. To do this, it is required to create at least one userlist and to define users",
"servers": "In this section you can create, edit and delete servers. And after use them as autocomplete in the 'Add' sections",
"servers": "In this section you can create, edit and delete options. And after use them as autocomplete in the 'Add' sections",
"options": "In this section you can create, edit and delete options. And then use them as autofill in the 'Add' sections",
"paste_cert": "Paste the contents of the certificate file",
"paste_cert_desc": "This pem file will be used to create https connection with HAProxy, NGINX or Apache",
"lists_howto": "In this section you can create and edit black and white lists. And after use them in the HAProxy configs or in the 'Add proxy' pages. Read how to use it in this",
@ -426,6 +436,8 @@
"keepalive": "The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. When this number is exceeded, the least recently used connections are closed.",
"fail_timeout": "The time during which the specified number of unsuccessful attempts to communicate with the server should happen to consider the server unavailable; and the period of time the server will be considered unavailable.",
"max_fails": "Sets the number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter. By default, the number of unsuccessful attempts is set to 1.",
"def_backend": "If you want to use upstream",
"def_backend_exit": "upstream must exist",
}
}
%}
@ -686,7 +698,6 @@
"action": "action",
"actions": "actions",
"command": "command",
"save": "save",
"change": "change",
"change2": "change",
"changes": "changes",
@ -767,6 +778,8 @@
"name": "name",
"article": "article",
"w_copy": "copy",
"w_cut": "cut",
"w_paste": "paste",
"for": "for",
"history": "history",
"history2": "history",
@ -811,7 +824,6 @@
"message": "message",
"menu": "menu",
"language": "language",
"apply": "apply",
"logout": "logout",
"last": "last",
"last2": "last",
@ -870,7 +882,6 @@
"of": "of",
"display": "display",
"default_backend": "Default backend",
"rule": "rule",
"existing": "existing",
"domain": "domain",
"domains": "domains",
@ -920,8 +931,6 @@
"instance_type": "Instance type",
"filter": "filter",
"rule_name": "Rule name",
"rule": "rule",
"rules": "rules",
"send": "send",
"additions": "additions",
"deletions": "deletions",
@ -944,7 +953,6 @@
"cert_expire": "Cert Expire",
"Hostname": "Hostname",
"maps": "maps",
"map": "map",
"method": "method",
"tools": "tools",
"next": "next",

View File

@ -0,0 +1,964 @@
{% set lang_short = 'es-ES' %}
{% set menu_links = dict() %}
{% set menu_links = {
"overview": {
"link": "Resumen",
"title": "Estado del servidor y servicios",
"h2": "Resumen"
},
"hapservers": {
"link": "Resumen",
"h2": "Resumen de servicios",
"haproxy": {
"title": "Resumen de servidores HAProxy"
},
"nginx": {
"title": "Resumen de servidores NGINX"
},
"apache": {
"title": "Resumen de servidores Apache"
},
"keepalived": {
"title": "Resumen de Keepalived"
},
},
"config": {
"link": "Configuración",
"h2": "Trabajando con configuraciones",
"haproxy": {
"title": "Trabajando con configuraciones de HAProxy"
},
"nginx": {
"title": "Trabajando con configuraciones de NGINX"
},
"apache": {
"title": "Trabajando con configuraciones de Apache"
},
"keepalived": {
"title": "Trabajando con configuraciones de Keepalived"
},
},
"stats": {
"link": "Estadísticas",
"h2": "Estadísticas de",
"haproxy": {
"title": "Estadísticas de HAProxy"
},
"nginx": {
"title": "Estadísticas de NGINX"
},
"apache": {
"title": "Estadísticas de Apache"
},
},
"logs": {
"link": "Registros",
"title": "registros",
"h2": "Registros de"
},
"metrics": {
"link": "Métricas",
"title": "métricas",
"h2": "Métricas de",
},
"add_proxy": {
"link": "Añadir proxy",
"title": "Añadir proxy: Crear proxy"
},
"versions": {
"link": "Versiones",
"h2": "Trabajando con versiones de configuración de",
"haproxy": {
"title": "Trabajando con versiones de configuración de HAProxy"
},
"nginx": {
"title": "Trabajando con versiones de configuración de NGINX"
},
"apache": {
"title": "Trabajando con versiones de configuración de Apache"
},
"keepalived": {
"title": "Trabajando con versiones de configuración de Keepalived"
},
},
"ssl": {
"link": "SSL",
"title": "Añadir proxy: Subir certificados SSL"
},
"lists": {
"link": "Listas",
"title": "Añadir proxy: Crear y subir listas blancas o negras"
},
"ha": {
"title": "Crear y configurar un clúster de alta disponibilidad",
},
"monitoring": {
"link": "Supervisión",
"title": "Herramientas de supervisión",
"smon": {
"dashboard": "SMON: Tablero",
"status_page": "SMON: Página de estado",
"history": "SMON: Historial",
"agent": "SMON: Agente",
},
"checker_history": "Checker: Historial",
"port_scan": "Escáner de puertos",
"net_tools": "Red: herramientas",
},
"servers": {
"link": "Servidores",
"title": "Gestión de servidores"
},
"admin_area": {
"link": "Área de administración",
"title": "Área de administración"
},
"history": {
"title": "Historial de"
},
"udp": {
"title": "Escuchas UDP"
}
}
%}
{% set services = {
"hapservers_desc": "Checker supervisa el servicio. Si el servicio cambia su estado, Checker alertará vía Telegram, Slack, Email, Web",
"last_edit": "Última edición"
}
%}
{% set errors = {
"cannot_get_info": "No se puede obtener información sobre",
"something_wrong": "Algo salió mal con la instalación en",
"check_logs": "revisa los registros",
"select_server": "Primero selecciona un servidor",
"empty_name": "El nombre no puede estar vacío",
}
%}
{% set settings = {
"rabbitmq": {
"rabbitmq_host": "Host del servidor RabbitMQ",
"rabbitmq_port": "Puerto del servidor RabbitMQ",
"rabbitmq_vhost": "Vhost del servidor RabbitMQ",
"rabbitmq_queue": "Cola del servidor RabbitMQ",
"rabbitmq_user": "Usuario del servidor RabbitMQ",
"rabbitmq_password": "Contraseña del servidor RabbitMQ",
},
"nginx": {
"nginx_path_logs": "Ruta de los registros de NGINX",
"nginx_stats_user": "Usuario para acceder a la página de estadísticas de NGINX",
"nginx_stats_password": "Contraseña para la página de estadísticas de NGINX",
"nginx_stats_port": "Puerto de estadísticas para la página web de NGINX",
"nginx_stats_page": "URI de estadísticas para la página web de NGINX",
"nginx_dir": "Ruta al directorio de NGINX con archivos de configuración",
"nginx_config_path": "Ruta al archivo principal de configuración de NGINX",
"nginx_container_name": "Nombre del contenedor Docker para el servicio NGINX",
},
"monitoring": {
"port_scan_interval": "Intervalo de comprobación para el escáner de puertos (en minutos)",
"portscanner_keep_history_range": "Periodo de retención del historial del escáner de puertos",
"checker_keep_history_range": "Periodo de retención del historial de Checker",
"checker_maxconn_threshold": "Valor umbral para alertas de maxconn, en %",
"checker_check_interval": "Intervalo de comprobación de Checker (en minutos)",
"action_keep_history_range": "Periodo de retención del historial de acciones (en días)",
},
"smon": {
"master_ip": "IP o nombre para conectar con el maestro SMON",
"master_port": "Puerto para conectar con el maestro SMON",
"agent_port": "Puerto del agente SMON",
"smon_keep_history_range": "Periodo de retención del historial de SMON",
"smon_ssl_expire_warning_alert": "Alerta de advertencia sobre expiración del certificado SSL (en días)",
"smon_ssl_expire_critical_alert": "Alerta crítica sobre expiración del certificado SSL (en días)",
},
"main": {
"time_zone": "Zona horaria",
"license": "Clave de licencia",
"proxy": "Dirección IP y puerto del servidor proxy. Usa proto://ip:puerto",
"session_ttl": "TTL para la sesión de usuario (en días)",
"token_ttl": "TTL para el token de usuario (en días)",
"tmp_config_path": "Ruta al directorio temporal. Debe ser válida y tener como propietario al usuario especificado en la configuración SSH",
"cert_path": "Ruta al directorio SSL. El propietario debe ser el usuario de la configuración SSH. La ruta debe existir",
"maxmind_key": "Clave de licencia para descargar la base de datos GeoLite2. Puedes crearla en maxmind.com",
},
"mail": {
"mail_ssl": "Activar TLS",
"mail_from": "Dirección del remitente",
"mail_smtp_host": "Dirección del servidor SMTP",
"mail_smtp_port": "Puerto del servidor SMTP",
"mail_smtp_user": "Usuario para autenticación",
"mail_smtp_password": "Contraseña para autenticación",
},
"logs": {
"syslog_server_enable": "Habilitar recepción de registros desde un servidor syslog",
"syslog_server": "Dirección IP del servidor syslog",
"log_time_storage": "Periodo de retención de los registros de actividad del usuario (en días)",
"apache_log_path": "Ruta de los registros de Apache",
},
"ldap": {
"ldap_enable": "Habilitar LDAP",
"ldap_server": "Dirección IP del servidor LDAP",
"ldap_port": "Puerto LDAP (por defecto se usa 389 o 636)",
"ldap_user": "Usuario LDAP",
"ldap_password": "Contraseña LDAP",
"ldap_base": "Dominio base. Ejemplo: dc=dominio, dc=com",
"ldap_domain": "Dominio LDAP para iniciar sesión",
"ldap_class_search": "Clase para buscar el usuario",
"ldap_user_attribute": "Atributo por el cual buscar usuarios",
"ldap_search_field": "Dirección de correo electrónico del usuario",
"ldap_type": "Usar LDAPS",
},
"haproxy": {
"haproxy_path_logs": "Ruta de los registros de HAProxy",
"haproxy_stats_user": "Usuario para acceder a la página de estadísticas de HAProxy",
"haproxy_stats_password": "Contraseña para la página de estadísticas de HAProxy",
"haproxy_stats_port": "Puerto de estadísticas para la página web de HAProxy",
"haproxy_stats_page": "URI de estadísticas para la página web de HAProxy",
"haproxy_dir": "Ruta al directorio de HAProxy con archivos de configuración",
"haproxy_config_path": "Ruta al archivo principal de configuración de HAProxy",
"server_state_file": "Ruta al archivo de estado de HAProxy",
"haproxy_sock": "Ruta al archivo de socket de HAProxy",
"haproxy_sock_port": "Puerto del socket de HAProxy",
"haproxy_container_name": "Nombre del contenedor Docker para el servicio HAProxy",
},
"apache": {
"apache_path_logs": "Ruta de los registros de Apache",
"apache_stats_user": "Usuario para acceder a la página de estadísticas de Apache",
"apache_stats_password": "Contraseña para la página de estadísticas de Apache",
"apache_stats_port": "Puerto de estadísticas para la página web de Apache",
"apache_stats_page": "URI de estadísticas para la página web de Apache",
"apache_dir": "Ruta al directorio de Apache con archivos de configuración",
"apache_config_path": "Ruta al archivo principal de configuración de Apache",
"apache_container_name": "Nombre del contenedor Docker para el servicio Apache",
},
"keepalived": {
"keepalived_config_path": "Ruta al archivo principal de configuración de Keepalived",
"keepalived_path_logs": "Ruta de los registros de Keepalived",
},
}
%}
{% set phrases = {
"config_file_name": "Nombre del archivo de configuración",
"no_events_added": "Aún no se han añadido eventos.",
"upload_and_restart": "Subir y reiniciar",
"upload_and_reload": "Subir y recargar",
"save_and_restart": "Guardar y reiniciar",
"save_and_reload": "Guardar y recargar",
"save_title": "Solo guardar, sin reiniciar el servicio",
"return_to_config": "Volver a la vista de configuración",
"check_config": "Comprobar configuración",
"master_slave": "Si reconfiguras el servidor Maestro, el Esclavo se reconfigurará automáticamente",
"read_about_files": "Puedes leer la descripción de todos los archivos de registro",
"read_about_parameters": "Puedes leer la descripción de todos los parámetros",
"read_howto": "lee el manual en este",
"howto_user": "Cómo usar",
"select_file": "Selecciona un archivo",
"read_how_it_works": "Puedes leer cómo funciona",
"metrics_not_installed": "No tienes instalado el servicio de Métricas.",
"how_to_install_metrics": "cómo instalar el servicio de Métricas",
"checker_not_installed": "No tienes instalado el servicio Checker",
"how_to_install_checker": "cómo instalar el servicio Checker",
"auto_start_not_installed": "No tienes instalado el servicio de inicio automático",
"enable_avg_table": "Activar la visualización de la tabla de promedios",
"disable_avg_table": "Desactivar la visualización de la tabla de promedios",
"protected_title": "Si la protección está habilitada, el servidor no se puede editar excepto por los administradores",
"slave_for_title": "Las acciones con la configuración maestra se aplicarán automáticamente al esclavo",
"server_unknown": "Estado del servidor desconocido",
"no_sub": "No estás suscrito",
"pls_sub": "Por favor suscríbete para acceder a esta función",
"no_av_feat": "Esta función no está disponible en tu plan",
"fwd_warn": "Solo hay reglas de las cadenas INPUT, IN_public_allow y OUTPUT",
"search_in_ad": "Buscar usuario en AD",
"are_you_sure": "¿Estás seguro?",
"delete_dialog": "La eliminación es irreversible, se perderán todos los datos",
"fields_mark": "Campos marcados con",
"are_required": "son obligatorios",
"scan_title": "Escanear el servidor en busca de servicios HAProxy, NGINX, Keepalived y Firewalld",
"superAdmin_pass": "No puedes editar la contraseña del rol superAdmin",
"superAdmin_services": "No puedes editar los servicios del rol superAdmin",
"pass_mismatched": "Las contraseñas no coinciden",
"private_key_note": "Clave privada. Nota: la clave pública debe estar preinstalada en todos los servidores a los que planeas conectarte",
"send_test_mes": "Enviar mensaje de prueba",
"alert_service_change_status": "Alerta sobre el cambio de estado del servicio",
"alert_backend_change_status": "Alerta sobre el cambio de estado del backend",
"alert_number_conn": "Alerta si el número de conexiones está a punto de alcanzar el límite",
"alert_master_backup": "Alerta sobre el cambio de estado Maestro/Backup",
"new_version": "Hay una nueva versión de Roxy-WI. Consulta el",
"all_alerts_enabled": "Alertas habilitadas",
"disable_alerts": "Desactivar alertas",
"work_with_prev": "Aquí puedes trabajar con versiones anteriores de configuraciones",
"roll_back": "Revertir a ellas, ver o eliminar",
"files_been_deleted": "Se han eliminado los siguientes archivos",
"version_has_been_uploaded": "Se ha subido y guardado la siguiente versión del archivo de configuración como",
"new_config_has_been_saved": "Se ha guardado una nueva configuración como",
"view_and_upload": "Ver y subir esta versión de la configuración",
"int_vrrp": "Interfaz para la dirección VRRP en un servidor",
"howto_ha": "Cómo crear un clúster de alta disponibilidad",
"been_installed": "ha sido instalado",
"wait_mess": "Por favor no cierres ni recargues la página. Espera a que finalice la tarea. Esto puede tardar un poco",
"you_are_editing": "Estás editando",
"section_from_server": "sección del servidor",
"how_to_install": "cómo instalar",
"port_check": "Comprobación de puerto",
"possible_service_name": "Nombre de servicio posible",
"server_info": "Información del servidor",
"user_groups": "Grupos de usuarios",
"comparing_config": "Comparando archivos de configuración",
"select_older_config": "Selecciona una configuración anterior",
"select_newer_config": "Selecciona una configuración más reciente",
"not_checked": "Sin comprobación",
"show_not_checked": "Mostrar servidores sin comprobar",
"read_desc_runtime_api": "Puedes leer la descripción de todos los parámetros de la API en tiempo de ejecución",
"read_desc_statuses": "Puedes leer la descripción de los estados",
"login_or_pass_incorrect": "Usuario o contraseña incorrectos",
"can_try_again": "Puedes intentarlo de nuevo en",
"is_not_installed": "no está instalado",
"server_is_inaccessible_for_editing": "Este servidor no puede ser editado excepto por el rol de administrador",
"creating_ha": "Creando un nuevo clúster de alta disponibilidad",
"adding_vrrp": "Añadiendo una nueva dirección VRRP",
"find_in_log": "Buscar en un archivo de registro. Soporta expresiones regulares.",
"exclude_in_log": "Excluir de la búsqueda en un archivo de registro. Soporta expresiones regulares.",
"bytes_in": "Bytes entrantes",
"bytes_out": "Bytes salientes",
"current_ses": "Sesiones actuales",
"total_ses": "Sesiones totales",
"ReqPerSec": "ReqPorSeg",
"BytesPerSec": "BytesPorSeg",
"became_master": "Se convirtió en Maestro",
"resource_record_type": "Tipo de registro de recurso",
"add_to_smon_desc": "Añadir el servidor para comprobación por ping de SMON",
"create_page_status": "Crear página de estado",
"not_in_cluster": "No pertenece a un clúster",
"ssh_passphrase": "Frase de paso para la clave SSH",
"check_interval": "Intervalo de comprobación.",
"check_interval_title": "Intervalo de comprobación. En segundos.",
}
%}
{% set roles = {
"superAdmin": "Tiene el nivel más alto de permisos administrativos y controla las acciones de todos los demás usuarios",
"admin": "Tiene acceso a todo excepto al área de administración",
"user": "Tiene los mismos derechos que el administrador, pero sin acceso a la página de Servidores",
"guest": "Acceso de solo lectura"
}
%}
{% set add_page = {
"desc": {
"port_check": "Una verificación básica a nivel TCP intenta conectarse al puerto TCP del servidor. La verificación es válida cuando el servidor responde con un paquete SYN/ACK.",
"maxconn": "Número total de conexiones permitidas a nivel de proceso. Esto evita aceptar demasiadas conexiones a la vez, protegiendo de quedarse sin memoria.",
"server_template": "Crear la lista de servidores desde la plantilla",
"def_check": "Parámetros por defecto",
"saved_options": "Estas son las opciones que guardaste en la pestaña 'Opciones'",
"press_down": "o presiona el botón 'abajo'",
"ip_port": "Si la dirección IP del listener está vacía, escuchará en todas las direcciones IP. Empieza a escribir o presiona abajo. Haz clic en + para añadir múltiples pares IP-puerto. Si usas VRRP, deja el campo IP vacío. Si asignas una IP VRRP, el servidor esclavo no se iniciará.",
"listener_desc1": "Una sección 'listen' define un proxy completo con sus partes frontend y backend combinadas en una sola sección. Es útil principalmente para tráfico TCP.",
"listener_desc2": "Todos los nombres de proxy deben formarse con letras, dígitos, '-', '_', '.' y ':'. Los nombres ACL distinguen entre mayúsculas y minúsculas, por lo que 'www' y 'WWW' son diferentes proxies.",
"listener_desc3": "Históricamente, los nombres de proxy podían superponerse, lo que causaba problemas en los registros. Desde la conmutación de contenido, dos proxies con capacidades superpuestas deben tener nombres diferentes. Aún se permite que un frontend y un backend compartan el mismo nombre.",
"front_desc1": "Una sección 'frontend' describe un conjunto de sockets que aceptan conexiones de clientes.",
"back_des1": "Una sección 'backend' describe un conjunto de servidores a los que el proxy se conectará para reenviar las conexiones entrantes.",
"ssl_offloading": "La terminación SSL significa que se realiza todo el cifrado y descifrado en el borde de la red, como en el balanceador de carga.",
"http_https": "Activar redirección de HTTP a HTTPS",
"enable_http2": "Habilitar HTTP/2",
"maxconn_desc": "Este valor no debe exceder el maxconn global. Valor por defecto global de maxconn",
"maxconn_fix": "Fijar el número máximo de conexiones simultáneas en un frontend",
"antibot": "Muchos bots se usan con fines maliciosos: scraping, spam, flooding, fuerza bruta, escaneo de vulnerabilidades, etc. Por ejemplo, pueden copiar tus listas de precios o escanear foros para hacer spam.",
"slow_attack": "En un ataque Slow POST, el atacante envía un encabezado POST válido, pero el cuerpo se transmite extremadamente lento, como 1 byte cada 2 minutos.",
"http_compression": "La compresión HTTP reduce el tamaño de la respuesta antes de enviarla al cliente, reduciendo el uso de ancho de banda y latencia.",
"forward_for": "HAProxy puede sobrescribir la IP de origen del cliente con su propia al reenviar tráfico, pero puede incluir la IP original en la cabecera X-Forwarded-For.",
"redispatch": "En modo HTTP, si un servidor con cookie está caído, los clientes seguirán intentando usarlo. 'option redispatch' fuerza la redistribución a otro servidor.",
"force_close": "HAProxy puede añadir la cabecera HTTP 'X-Forwarded-For' con la IP del cliente. El servidor debe configurarse para usar solo la última aparición de esta cabecera.",
"cookie": "Para mantener la sesión en el mismo servidor, usa persistencia basada en cookies en la sección backend.",
"c_prefix": "En lugar de una cookie dedicada, se completará una ya existente.",
"c_nocache": "Recomendado junto con el modo insert si hay un caché entre el cliente y HAProxy.",
"c_postonly": "Inserta cookies solo en respuestas a peticiones POST.",
"c_dynamic": "Activa cookies dinámicas. Se crea una cookie de sesión por servidor.",
"def_backend": "Si deseas usar el backend por defecto",
"def_backend_exit": "el backend debe existir",
"port_for_bind": "Puerto para hacer bind",
"bind_ip_pair": "Asociar otro par IP-puerto",
"no_def_backend": "Si no se cumple ninguna condición, se usará el backend definido como 'default_backend'. Si no se define, se devuelve un error 503.",
"circuit_breaking": "El patrón Circuit Breaker previene que errores persistentes afecten al sistema. Al detectar fallos, 'abre el circuito' y bloquea llamadas.",
"peers_master": "Si usas HAProxy en clúster Master-Master, los nombres locales deben coincidir con los nombres en peers.",
"peers_slave": "Para Master-Slave, el nombre del servidor maestro debe coincidir con el peer configurado.",
"peers": "La sección peers permite replicar datos de tablas de sesión entre instancias de HAProxy.",
"userlist": "Aquí puedes crear listas de usuarios y usarlas en las secciones 'Añadir'.",
"userlist_name": "Nombre de la lista de usuarios",
"userlist_pass": "Contraseña del usuario. Por defecto es 'insecure-password'.",
"userlist_user_grp": "Grupo de usuario",
"userlist_user": "Puedes añadir grupos al usuario con una lista separada por comas.",
"userlist_group": "También puedes añadir usuarios al grupo usando 'users nombre1, nombre2'",
"userlist_desc": "Para proteger el acceso a frontend/backend/listen o estadísticas HTTP, se requiere al menos una lista de usuarios.",
"servers": "Aquí puedes crear, editar y eliminar servidores para autocompletado en 'Añadir'.",
"options": "Aquí puedes crear, editar y eliminar opciones para autocompletado en 'Añadir'.",
"paste_cert": "Pega el contenido del archivo del certificado",
"paste_cert_desc": "Este archivo .pem se usará para conexiones HTTPS en HAProxy, NGINX o Apache",
"lists_howto": "Aquí puedes crear y editar listas negras y blancas. Luego úsalas en configuraciones HAProxy o en 'Añadir proxy'.",
"lists_new_line": "Cada dirección nueva debe ir en una línea nueva",
"was_success_added": "fue añadido exitosamente",
"create_ssl_proxy": "Crear proxy HTTPS con terminación SSL en HAProxy. Se enviará tráfico HTTP a los backends. Necesitas tener",
"create_ssl_front": "Crear frontend HTTPS con terminación SSL en HAProxy. Se enviará tráfico HTTP a los backends. Necesitas tener",
"create_ssl_backend": "Crear backend HTTPS con terminación SSL en HAProxy. Se enviará tráfico HTTP a los backends. Necesitas tener",
"create_https_proxy": "Crear proxy HTTPS sin terminación SSL en HAProxy. Se enviará tráfico HTTPS a los backends",
"create_https_front": "Crear frontend HTTPS sin terminación SSL en HAProxy. Se enviará tráfico HTTPS a los backends",
"create_https_backend": "Crear backend HTTPS sin terminación SSL en HAProxy. Se enviará tráfico HTTPS a los backends",
"option_temp": "Crear, editar y eliminar opciones con parámetros. Luego úsalas en 'Añadir'.",
"server_temp": "Crear, editar y eliminar servidores. Luego úsalos en 'Añadir'.",
"use_add": "Y úsalos en las secciones 'Añadir'",
"comma_separated": "Puedes especificar varios, separados por coma o espacio",
"create_nginx_ssl_proxy": "Crear proxy HTTPS con terminación SSL en NGINX. Se enviará tráfico HTTP a los backends. Necesitas tener",
"create_nginx_proxy": "El proxy pass en NGINX reenvía solicitudes al backend y devuelve la respuesta. Permite balanceo, terminación SSL y enrutamiento."
},
"buttons": {
"disable_ssl_check": "Desactivar verificación SSL",
"disable_ssl_verify": "Desactivar verificación SSL en servidores",
"set_options": "Establecer opciones",
"set_options_m": "Establecer opciones manualmente",
"show_full_settings": "Mostrar lista completa de configuraciones",
"show_full_settings": "Ocultar lista completa de configuraciones"
}
}
%}
{% set add_nginx_page = {
"desc": {
"upstream_desc1": "El módulo upstream se usa para definir grupos de servidores.",
"upstream_desc2": "Define un grupo de servidores. Los servidores pueden escuchar en diferentes puertos. Además, se pueden mezclar servidores que escuchen en TCP y sockets de dominio UNIX.",
"upstream_desc3": "Por defecto, las peticiones se distribuyen entre los servidores utilizando un método de balanceo round-robin ponderado.",
"keepalive": "El parámetro connections establece el número máximo de conexiones keepalive inactivas con los servidores upstream que se conservan en la caché de cada proceso trabajador. Cuando se excede este número, se cierran las conexiones menos utilizadas recientemente.",
"fail_timeout": "El tiempo durante el cual debe ocurrir el número especificado de intentos fallidos para comunicarse con el servidor, para considerarlo no disponible; y el periodo durante el cual se considerará no disponible.",
"max_fails": "Establece el número de intentos fallidos para comunicarse con el servidor que deben ocurrir en la duración establecida por el parámetro fail_timeout para considerar que el servidor no está disponible durante ese tiempo. Por defecto, el número de intentos es 1.",
"def_backend": "Si desea utilizar upstream",
"def_backend_exit": "debe existir aguas arriba",
}
}
%}
{% set admin_page = {
"desc": {
"latest_repo": "Roxy-WI intentará instalar la última versión del servicio desde el repositorio oficial",
"install_as_docker": "Instalar servicio como contenedor Docker",
"no_ansible": "No has instalado",
"before_install": "Antes de instalar cualquier exportador, primero instala",
"been_installed": "servidores han sido instalados",
"there_are_no": "No hay servidores Grafana ni Prometheus",
"country_codes": "Códigos de países",
"smon_desc": "SMON significa <b>S</b>imple <b>MON</b>itoring",
"checker_desc": "Checker está diseñado para monitorizar los servicios HAProxy, NGINX, Apache y Keepalived, así como los backends de HAProxy y maxconn",
"auto_start_desc": "El servicio Auto Start permite reiniciar los servicios HAProxy, NGINX, Apache y Keepalived si están caídos",
"metrics_desc": "Recoge el número de conexiones para los servicios HAProxy, NGINX, Apache y HAProxy WAF",
"p_s_desc": "Escanea el servidor en busca de puertos abiertos y guarda el historial",
"socket_desc": "Socket es un servicio para enviar alertas y notificaciones",
"a_new_version": "Hay una nueva versión disponible",
"no_new_version": "No hay una nueva versión disponible",
"main_app": "La aplicación principal",
"for_updating": "Para actualizar, debes usar el RPM o DEB de Roxy-WI",
"how_to_using_repo": "cómo comenzar a usar el repositorio.",
"proxy_settings": "Si el servidor Roxy-WI utiliza un proxy para conectarse a Internet, añade la configuración del proxy a yum.conf."
}
}
%}
{% set smon_page = {
"desc": {
"before_use": "antes de usar",
"smon_is_not_run": "El servicio SMON no está en ejecución.",
"run_smon": "Ejecutar el servicio SMON",
"not_installed": "No tienes instalado el servicio SMON",
"not_added": "No tienes servidores añadidos en el servicio SMON",
"create_server": "Crea tu primer servidor",
"see_check": "para ver si se ha añadido una nueva verificación",
"do_not_sort": "No ordenar",
"do_not_sort_by_status": "No ordenar por estado",
"sort_status": "Ordenar por estado",
"status_summary": "Resumen de estado",
"enabled_checks": "Verificaciones habilitadas",
"http_status_check": "Verificación de estado HTTP",
"body_status_check": "Verificación del contenido de la respuesta",
"resp_time": "Tiempo de respuesta",
"last_resp_time": "Último tiempo de respuesta",
"UP": "ACTIVO",
"DOWN": "INACTIVO",
"HTTP_FAILURE": "FALLA HTTP",
"BODY_FAILURE": "FALLA DE CONTENIDO",
"UNKNOWN": "DESCONOCIDO",
"PORT_DOWN": "PUERTO INACTIVO",
"DISABLED": "DESHABILITADO",
"packet_size": "Tamaño de paquete",
"add_agent": "Agregar agente",
"total_checks": "Verificaciones totales",
"not_assign_to_agent": "La verificación no está asignada a ningún agente"
}
}
%}
{% set p_s_page = {
"desc": {
"is_enabled_and_up": "El escáner de puertos está habilitado y el servicio está ACTIVO",
"is_enabled_and_down": "El escáner de puertos está habilitado, pero el servicio está INACTIVO",
"scanning_ports": "Escaneando puertos abiertos/filtrados del servidor",
"total_open_ports": "Total de puertos abiertos",
"p_s_title": "Panel del escáner de puertos",
"p_s_title_history": "Historial del escáner de puertos para"
}
}
%}
{% set ha_page = {
"ha": "Clúster de alta disponibilidad",
"has": "Clústeres de alta disponibilidad",
"create_virt_server": "Roxy-WI añadirá la dirección VRRP como un servidor separado",
"return_master_desc": "Volver al maestro",
"return_master": "Si se marca esta opción, el maestro de Keepalived retomará la VRRP cuando el servicio vuelva a estar activo",
"try_install": "Roxy-WI intentará instalar",
"as_docker": "como contenedor Docker",
"add_vip": "Añadir VIP al clúster HA",
"create_ha": "Crear clúster de alta disponibilidad",
"start_enter": "Empieza a escribir el nombre de la interfaz de red para añadir VIP",
"roxywi_timeout": "Tiempo de espera de respuesta de Roxy-WI",
"check_apache_log": "Consulta el <a href='/logs/internal' target='_blank' title='Registros internos'>registro de errores de Apache</a> para más información.",
"was_installed": "fue instalado en",
"use_src": "Usar la dirección VIP como origen",
"use_src_help": "Usar la dirección VIP como origen para conexiones salientes a backends",
"save_apply": "<b>Guardar</b> - solo guarda la configuración del clúster HA en Roxy-WI, sin aplicar cambios en los servidores. <br /> <b>Aplicar</b> - recrea la configuración en los miembros del clúster y instala servicios adicionales."
}
%}
{% set udp_page = {
"save_apply": "<b>Guardar</b> - guarda la configuración del listener UDP en Roxy-WI sin aplicar cambios. <br /> <b>Aplicar</b> - recrea la configuración en los servidores o nodos individuales.",
"listener_ip": "IP para asociar el listener UDP. Empieza a escribir",
"listener_port": "Puerto para asociar el listener UDP",
"balancing_type": "Tipo de balanceo",
"check_backends": "Habilitar verificación de backends",
"retry": "Número de reintentos",
"retry_title": "Número máximo de reintentos",
"delay_before_retry_title": "Retraso entre reintentos",
"delay_before_retry": "Retraso antes de reintentar",
"delay_loop_title": "Intervalo entre verificaciones en segundos",
"delay_loop": "Intervalo entre verificaciones"
}
%}
{% set nettools_page = {
"ip_or_name": "Introduce IP o nombre",
"dns_name": "Introduce un nombre DNS",
"server_portscann": "Introduce un servidor para escaneo de puertos"
}
%}
{% set words = {
"apply": "aplicar",
"true": "verdadero",
"false": "falso",
"cache": "caché",
"compression": "compresión",
"acceleration": "aceleración",
"start": "iniciar",
"start2": "iniciar",
"stop": "detener",
"restart": "reiniciar",
"service": "servicio",
"service2": "servicio",
"services": "servicios",
"services2": "servicios",
"services3": "servicios",
"services4": "servicios",
"started": "iniciado",
"reload": "recargar",
"reloading": "recargando",
"stopped": "detenido",
"no_desc": "sin descripción",
"process_num": "número de procesos",
"version": "versión",
"version2": "versión",
"versions": "versiones",
"error": "error",
"addresses": "direcciones",
"address": "dirección",
"virtual": "virtual",
"hosts": "hosts",
"time_range": "intervalo de tiempo",
"server": "servidor",
"server2": "servidor",
"status": "estado",
"current": "actual",
"current2": "actual",
"total": "total",
"server_status": "estado del servidor",
"services_status": "estado de los servicios",
"compare": "comparar",
"map": "mapa",
"about": "acerca de",
"help": "ayuda",
"contacts": "contactos",
"cabinet": "panel",
"legal": "legal",
"alert": "alerta",
"alert2": "alerta",
"alerts": "alertas",
"manage": "gestionar",
"user": "usuario",
"user2": "usuario",
"user3": "usuario",
"users": "usuarios",
"users2": "usuarios",
"username": "nombre de usuario",
"servers": "servidores",
"servers2": "servidores",
"creds": "credenciales",
"creds2": "credenciales",
"settings": "configuración",
"settings2": "configuración",
"install": "instalar",
"installation": "instalación",
"installing": "instalando",
"proxy": "proxy",
"provisioning": "provisión",
"backup": "respaldo",
"backup2": "respaldo",
"configs": "configuraciones",
"configs2": "configuraciones",
"config": "configuración",
"from": "desde",
"view": "ver",
"internal": "interno",
"internal2": "interno",
"log": "registro",
"logs": "registros",
"logs2": "registros",
"admin_area": "área de administración",
"group": "grupo",
"group2": "grupo",
"groups": "grupos",
"groups2": "grupos",
"groups3": "grupos",
"w_update": "actualizar",
"updating": "actualizando",
"monitoring": "monitorización",
"auto": "auto",
"refresh": "refrescar",
"refresh2": "refrescar",
"no": "no",
"not": "no",
"yes": "sí",
"interval": "intervalo",
"desc": "descripción",
"login": "iniciar sesión",
"login2": "iniciar sesión",
"role": "rol",
"roles": "roles",
"subs": "suscripción",
"show_all": "mostrar todo",
"plan": "plan",
"pay_method": "método de pago",
"active": "activo",
"actives": "activos",
"open": "abrir",
"opened": "abierto",
"edit": "editar",
"delete": "eliminar",
"add": "añadir",
"added": "añadido",
"save": "guardar",
"saved": "guardado",
"saving": "guardando",
"expand_all": "expandir todo",
"collapse_all": "colapsar todo",
"raw": "bruto",
"stats": "estadísticas",
"note": "nota",
"back": "atrás",
"show": "mostrar",
"run": "ejecutar",
"running": "en ejecución",
"running2": "en ejecución",
"statistics": "estadísticas",
"rollback": "reversión",
"previous": "anterior",
"to": "a",
"listener": "listener",
"frontends": "frontends",
"frontend": "frontend",
"backends": "backends",
"backend": "backend",
"maintain": "mantener",
"drain": "vaciar",
"drains": "drenajes",
"number": "número",
"rows": "filas",
"row": "fila",
"find": "buscar",
"exclude": "excluir",
"file": "archivo",
"file2": "archivo",
"files": "archivos",
"here": "aquí",
"action": "acción",
"actions": "acciones",
"command": "comando",
"change": "cambiar",
"change2": "cambiar",
"changes": "cambios",
"enter": "ingresar",
"enter2": "ingresar",
"lists": "listas",
"list": "lista",
"sessions": "sesiones",
"session": "sesión",
"and": "y",
"select": "seleccionar",
"select2": "seleccionar",
"new": "nuevo",
"new2": "nuevo",
"new3": "nuevo",
"new4": "nuevo",
"port": "puerto",
"ports": "puertos",
"table": "tabla",
"w_get": "obtener",
"dynamically": "dinámicamente",
"set": "establecer",
"type": "tipo",
"typing": "escribiendo",
"size": "tamaño",
"is": "es",
"w_empty": "vacío",
"used": "usado",
"w_clear": "limpiar",
"this": "esto",
"this2": "esto",
"this3": "esto",
"this4": "esto",
"entry": "entrada",
"age": "edad",
"protocol": "protocolo",
"rate": "tasa",
"expire": "expirar",
"more": "más",
"info": "información",
"source": "origen",
"overview": "resumen",
"personal": "personal",
"read": "leer",
"second": "segundo",
"seconds": "segundos",
"seconds2": "segundos",
"minute": "minuto",
"minute2": "minuto",
"minutes": "minutos",
"minutes2": "minutos",
"hour": "hora",
"hours": "horas",
"hours2": "horas",
"day": "día",
"days": "días",
"metrics": "métricas",
"every": "cada",
"every2": "cada",
"every3": "cada",
"hide": "ocultar",
"average": "promedio",
"peak": "pico",
"connect": "conectar",
"connections": "conexiones",
"connections2": "conexiones",
"enable": "habilitar",
"enabled": "habilitado",
"enabled2": "habilitado",
"virt": "virtual",
"virtual": "virtual",
"check": "verificar",
"check2": "verificar",
"checks": "verificaciones",
"checking": "verificando",
"protected": "protegido",
"slave_for": "Esclavo para",
"name": "nombre",
"article": "artículo",
"w_copy": "copiar",
"for": "para",
"history": "historial",
"history2": "historial",
"history3": "historial",
"rule": "regla",
"rules": "reglas",
"rules2": "reglas",
"on": "activado",
"dest": "destino",
"target": "objetivo",
"w_input": "entrada",
"output": "salida",
"password": "contraseña",
"email": "correo electrónico",
"w_a": "un",
"w_an": "una",
"key": "clave",
"token": "token",
"channel": "canal",
"channels": "canales",
"job": "trabajo",
"cancel": "cancelar",
"repository": "repositorio",
"init": "inicializar",
"period": "período",
"the": "el",
"scan": "escanear",
"is_there": "hay",
"confirm": "confirmar",
"confirmation": "confirmación",
"one": "uno",
"one2": "uno",
"or": "o",
"upload": "subir",
"uploading": "subiendo",
"uploaded": "subido",
"test": "prueba",
"test2": "prueba",
"disabled": "deshabilitado",
"via": "vía",
"web_panel": "panel web",
"message": "mensaje",
"menu": "menú",
"language": "idioma",
"logout": "cerrar sesión",
"last": "último",
"last2": "último",
"activity": "actividad",
"never": "nunca",
"is_online": "está en línea",
"is_offline": "está desconectado",
"valid": "válido",
"remote": "remoto",
"remote2": "remoto",
"local": "local",
"path": "ruta",
"create": "crear",
"created": "creado",
"creating": "creando",
"diff": "diferencia",
"diff2": "diferencia",
"diff3": "diferencia",
"master": "maestro",
"slave": "esclavo",
"slaves": "esclavos",
"interface": "interfaz",
"as": "como",
"stay": "permanecer",
"protection": "protección",
"return": "regresar",
"cluster": "clúster",
"existing": "existente",
"existing2": "existente",
"success": "éxito",
"option": "opción",
"option2": "opción",
"options": "opciones",
"template": "plantilla",
"templates": "plantillas",
"userlists": "listas de usuarios",
"whitelist": "lista blanca",
"whitelists": "listas blancas",
"blacklist": "lista negra",
"blacklists": "listas negras",
"mode": "modo",
"balance": "balanceo",
"health": "salud",
"cert": "certificado",
"cert_name": "nombre del certificado",
"certs": "certificados",
"certs2": "certificados",
"advanced": "avanzado",
"generate": "generar",
"generated": "generado",
"server_template": "plantilla de servidor",
"custom": "personalizado",
"param": "parámetro",
"param2": "parámetro",
"params": "parámetros",
"of": "de",
"display": "mostrar",
"default_backend": "backend por defecto",
"existing": "existente",
"domain": "dominio",
"domains": "dominios",
"all": "todo",
"just": "solo",
"without": "sin",
"work": "trabajo",
"working": "trabajando",
"section": "sección",
"section2": "sección",
"use": "usar",
"available": "disponible",
"external": "externo",
"in": "en",
"folder": "carpeta",
"folder2": "carpeta",
"clone": "clonar",
"date": "fecha",
"time": "hora",
"page": "página",
"pages": "páginas",
"body": "cuerpo",
"level": "nivel",
"host": "host",
"uptime": "tiempo activo",
"downtime": "tiempo inactivo",
"record_type": "tipo de registro",
"upstream": "upstream",
"haproxy": "HAProxy",
"nginx": "NGINX",
"apache": "Apache",
"keepalived": "Keepalived",
"scan": "escanear",
"notify": "notificar",
"notification": "notificación",
"keeping": "manteniendo",
"keep": "mantener",
"close": "cerrar",
"state": "estado",
"latest": "último",
"cloud": "nube",
"provider": "proveedor",
"region": "región",
"OS": "SO",
"created_at": "creado el",
"edited_at": "editado el",
"instance_type": "tipo de instancia",
"filter": "filtro",
"rule_name": "nombre de la regla",
"send": "enviar",
"additions": "adiciones",
"deletions": "eliminaciones",
"recent": "reciente",
"already": "ya",
"disable": "deshabilitar",
"worker": "trabajador",
"worker2": "trabajador",
"processes": "procesos",
"position": "posición",
"global": "global",
"none": "ninguno",
"headers": "cabeceras",
"value": "valor",
"if": "si",
"then": "entonces",
"response": "respuesta",
"average": "promedio",
"average2": "promedio",
"cert_expire": "expiración del certificado",
"Hostname": "Nombre del host",
"maps": "mapas",
"map": "mapa",
"method": "método",
"tools": "herramientas",
"next": "siguiente",
"agent": "agente",
"agent2": "agente",
"reconfigure": "reconfigurar",
"listener": "listener",
"listeners": "listeners",
"weight": "peso",
"where": "dónde",
"shared": "compartido",
"retries": "reintentos",
"address": "dirección",
"calculate": "calcular",
"calculator": "calculadora",
"netmask": "máscara de red",
"theme": "tema",
"dark": "oscuro",
"light": "claro",
"types": "tipos",
"min": "mínimo",
"length": "longitud"
}
%}

View File

@ -121,13 +121,11 @@
}
}
%}
{% set services = dict() %}
{% set services = {
"hapservers_desc": "Checker surveille le service. Si le service change d\'état, Vérifier l\'alerte via Telegram, Slack, Email, Web",
"last_edit": "Dernière modif."
}
%}
{% set errors = dict() %}
{% set errors = {
"cannot_get_info": "Impossible d\'obtenir des informations sur",
"something_wrong": "Un problème est survenu lors de l\'installation",
@ -237,7 +235,6 @@
},
}
%}
{% set phrases = dict() %}
{% set phrases = {
"config_file_name": "Nom du fichier de Config",
"no_events_added": "Aucun événement ajouté pour le moment.",
@ -358,6 +355,7 @@
"back_des1": "Une section 'backend' décrit un ensemble de serveurs auxquels le proxy se connectera pour transmettre les connexions entrantes.",
"ssl_offloading": "Le terme 'terminaison SSL' signifie que vous effectuez tous les cryptages et décryptages à la périphérie de votre réseau, par exemple au niveau de l\'équilibreur de charge.",
"http_https": "Activer la redirection de HTTP vers HTTPS",
"enable_http2": "Activer HTTP 2",
"maxconn_desc": "Cette valeur ne doit pas dépasser le maxconn global. Valeur par défaut du maxconn global",
"maxconn_fix": "Fixer le nombre maximum de connexions simultanées sur un frontend",
"antibot": "Malheureusement, une grande partie des bots sont utilisés pour des raisons malveillantes. Parmi leurs intentions figurent le scraping de sites Web, le spamming, l\'inondation de requêtes, le forçage brutal et l\'analyse de vulnérabilités. Par exemple, les robots peuvent récupérer vos listes de prix afin que vos concurrents puissent systématiquement vous vendre moins cher ou élaborer une solution concurrentielle à partir de vos données. Ils peuvent aussi essayer de localiser des forums et des sections de commentaires où ils pourront envoyer du spam. Dans d\'autres cas, ils analysent votre site à la recherche de failles de sécurité.",
@ -369,7 +367,6 @@
"cookie": "Pour envoyer un client au même serveur que celui où il a été envoyé précédemment afin de réutiliser une session sur ce serveur, vous pouvez activer la persistance de la session basée sur les cookies. Ajoutez une directive cookie à la section backend et définissez le paramètre cookie à une valeur unique sur chaque ligne de serveur.",
"c_prefix": "Ce mot-clé indique qu\'au lieu de s\'appuyer sur un cookie dédié pour la persistance, un cookie existant sera complété.",
"c_nocache": "Cette option est recommandée en conjonction avec le mode insertion lorsqu\'il existe un cache entre le client et HAProxy",
"c_nocache": "Cette option est recommandée en conjonction avec le mode insertion lorsqu\'il existe un cache entre le client et HAProxy",
"c_postonly": "Cette option garantit que l\'insertion des cookies ne sera effectuée que sur les réponses aux demandes POST",
"c_dynamic": "Activer les cookies dynamiques. Lorsqu\'elle est utilisée, un cookie de session est créé dynamiquement pour chaque serveur",
"def_backend": "Si vous voulez utiliser le backend par défaut",
@ -389,7 +386,7 @@
"userlist_group": "Il est également possible de rattacher des utilisateurs à ce groupe en utilisant une liste de noms séparés par des virgules et précédés du mot-clé 'users'.",
"userlist_desc": "Il est possible de contrôler l\'accès aux sections frontend/backend/listen ou aux statistiques http en autorisant uniquement les utilisateurs authentifiés et autorisés. Pour ce faire, il est nécessaire de créer au moins une liste d\'utilisateurs et de définir des utilisateurs",
"servers": "Dans cette section, vous pouvez créer, modifier et supprimer des serveurs. Vous pouvez ensuite les utiliser en tant qu\'autocomplétion dans les sections 'Ajouter'.",
"servers": "Dans cette section, vous pouvez créer, modifier et supprimer des options. Et ensuite les utiliser comme autocomplétion dans les sections 'Ajouter'",
"options": "Dans cette section, vous pouvez créer, modifier et supprimer des options. Et puis utilisez-les comme remplissage automatique dans les sections « Ajouter »",
"paste_cert": "Collez le contenu du fichier de certificat",
"paste_cert_desc": "Ce fichier pem sera utilisé pour créer une connexion https avec HAProxy, NGINX ou Apache",
"lists_howto": "Dans cette section, vous pouvez créer et modifier des listes noires et blanches. Vous pouvez ensuite les utiliser dans la configuration de HAProxy ou dans les pages 'Ajouter un proxy'. Lisez comment l\'utiliser dans cette section",
@ -426,6 +423,8 @@
"keepalive": "Le paramètre connections définit le nombre maximal de connexions keepalive inactives vers des serveurs en amont qui sont conservées dans le cache de chaque processus d\'un job. Lorsque ce nombre est dépassé, les connexions les moins récemment utilisées sont fermées.",
"fail_timeout": "Le temps pendant lequel le nombre spécifié de tentatives infructueuses de communication avec le serveur doit se produire pour considérer le serveur comme indisponible ; et la période pendant laquelle le serveur sera considéré comme indisponible.",
"max_fails": "Définit le nombre de tentatives infructueuses de communication avec le serveur qui doivent se produire dans la durée définie par le paramètre fail_timeout pour considérer le serveur comme indisponible pendant une durée également définie par le paramètre fail_timeout. Par défaut, le nombre de tentatives infructueuses est fixé à 1.",
"def_backend": "Si vous souhaitez utiliser en amont",
"def_backend_exit": "l'amont doit exister",
}
}
%}
@ -686,7 +685,6 @@
"action": "action",
"actions": "actions",
"command": "commande",
"save": "sauvegarder",
"change": "changer",
"change2": "changer",
"changes": "changements",
@ -811,7 +809,6 @@
"message": "message",
"menu": "menu",
"language": "langage",
"apply": "appliquer",
"logout": "déconnexion",
"last": "dernier",
"last2": "dernier",
@ -920,8 +917,6 @@
"instance_type": "Type d\'instance",
"filter": "filtrer",
"rule_name": "Nom de la règle",
"rule": "règle",
"rules": "règles",
"send": "envoyer",
"additions": "ajouts",
"deletions": "suppressions",
@ -944,7 +939,6 @@
"cert_expire": "Expiration du certificat",
"Hostname": "Nome de anfitrião",
"maps": "cartes",
"map": "carte",
"method": "méthode",
"tools": "outils",
"next": "suivante",

View File

@ -4,5 +4,6 @@
'fr': 'Français',
'pt-br': 'Português',
'zh': '中文',
'es-ES': 'Español'
}
%}

View File

@ -121,13 +121,11 @@
}
}
%}
{% set services = dict() %}
{% set services = {
"hapservers_desc": "O Checker está moditorando o serviço. Quando o serviço mudar o estado, o Checker vou enviar uma notificação via Telegram, Slack, Email, Web",
"last_edit": "Editado por último"
}
%}
{% set errors = dict() %}
{% set errors = {
"cannot_get_info": "Não é possível obter informações sobre",
"something_wrong": "Algo deu errado com a instalação de",
@ -237,7 +235,6 @@
},
}
%}
{% set phrases = dict() %}
{% set phrases = {
"config_file_name": "Nome de arquivo de configuração",
"no_events_added": "Nenhum evento adicionado.",
@ -358,6 +355,7 @@
"back_des1": "Uma seção 'backend' descreve um conjunto de servidores saos quais o proxy se conectará para encaminhar conexões de entrada.",
"ssl_offloading": "A terminação SSL significa que você está executando toda a encriptação e desencriptação na borda da sua rede, como no balanceador de carga.",
"http_https": "Ativar a redireição de HTTP para HTTPS",
"enable_http2": "Habilitar HTTP 2",
"maxconn_desc": "Este valor não deve exceder o maxconn global. Valor maxconn global padrão",
"maxconn_fix": "Fixar o número máximo de conexões simultâneas",
"antibot": "Infelizmente, uma grande parte dos bots são usados com intenções maliciosas. Essas intenções incluem web scraping, spam, inundação de pedidos, bruteforce e verificação de vulnerabilidades.Por exemplo, os bots podem raspar suas listas de preços para que os concorrentes possam reduzir consistentemente você ou criar uma solução competitiva usando seus dados. Os bots podem também tentar localizar fóruns e seções de comentários onde possam postar spam. Eles podem procurar vulnerabilidades no seu site",
@ -369,7 +367,6 @@
"cookie": "Para enviar um cliente para o mesmo servidor onde foi enviado anteriormente para reutilizar uma sessão nesse servidor, você pode ativar a persistência de sessão baseada em cookie. Adicione uma diretiva de cookie à seção de back-end e defina o parâmetro de cookie para um valor exclusivo em cada linha do config do servidor",
"c_prefix": "Esta palavra-chave indica que, em vez de depender de um cookie dedicado para a persistência, um cookie já existente será concluído",
"c_nocache": "Esta opção é recomendada em conjunto com o modo de inserção quando houver um cache entre o cliente e o HAProxy",
"c_nocache": "Esta opção é recomendada em conjunto com o modo de inserção quando houver um cache entre o cliente e o HAProxy",
"c_postonly": "Esta opção garante que os cookies serão inseridos somente em respostas as pedidos POST",
"c_dynamic": "Ativar cookies dinâmicos. Quando você usar cookies dinâmicos, o cookie de sessão cookies é criado dinamicamente para cada servidor",
"def_backend": "Se você quer usar o backend padrão",
@ -389,7 +386,7 @@
"userlist_group": "Também é possível anexar usuários a este grupo usando uma lista separada por vírgulas de nomes precedidos pela palavra-chave 'users'",
"userlist_desc": "É possível controlar o acesso às seções Frontend/Backend/Listen ou às estatísticas de HTTP, permitindo apenas usuários autenticados e autorizados. Para fazer isso, é necessário criar pelo menos uma lista de usuários e definir usuários",
"servers": "Nesta seção você pode criar, editar e excluir servidores e de usá-los como preenchimento automático nas seções 'Adicionar'",
"servers": "Nesta seção você pode criar, editar e excluir opções e de usá-los como preenchimento automático nas seções 'Adicionar'",
"options": "Nesta seção você pode criar, editar e excluir opções. E então use-os como preenchimento automático nas seções 'Adicionar'",
"paste_cert": "Cole o conteúdo do arquivo de certificado",
"paste_cert_desc": "Este arquivo pem será usado para criar uma conexão com HAProxy, NGINX ou Apache",
"lists_howto": "Nesta seção você pode criar, editar listas brancas e negras e de usá-los como preenchimento automático nas seções 'Adicionar um proxy'. Leia aqui como fazer isso",
@ -426,6 +423,8 @@
"keepalive": "O parâmetro de conexões define o número máximo de conexões keep-alive ociosas para servidores upstream que são preservadas no cache de cada processo de trabalho. Quando esse número é excedido, as conexões usadas menos recentemente são fechadas.",
"fail_timeout": "O interval durante o qual o número especificado de tentativas malsucedidas de comunicação com o servidor deve acontecer para considerar o servidor indisponível; e o período de tempo em que o servidor será considerado indisponível.",
"max_fails": "Define o número de tentativas malsucedidas de comunicação com o servidor que devem ocorrer na duração definida pelo parâmetro fail_timeout para considerar o servidor indisponível por uma duração também definida pelo parâmetro fail_timeout. Por padrão, o número de tentativas malsucedidas é definido como 1.",
"def_backend": "Se você quiser usar o upstream",
"def_backend_exit": "a montante deve existir",
}
}
%}
@ -686,7 +685,6 @@
"action": "action",
"actions": "actions",
"command": "command",
"save": "sakvar",
"change": "mudar",
"change2": "mudar",
"changes": "mudanças",
@ -811,7 +809,6 @@
"message": "mensagem",
"menu": "menu",
"language": "idioma",
"apply": "aplicar",
"logout": "logout",
"last": "last",
"last2": "last",
@ -870,7 +867,6 @@
"of": "of",
"display": "exibir",
"default_backend": "Default backend",
"rule": "regra",
"existing": "existente",
"domain": "domínio",
"domains": "domínios",
@ -920,8 +916,6 @@
"instance_type": "Tipo de instância",
"filter": "filtro",
"rule_name": "Nome de regra",
"rule": "regra",
"rules": "regras",
"send": "enviar",
"additions": "adicionados",
"deletions": "apagados",
@ -944,7 +938,6 @@
"cert_expire": "Expiração do certificado",
"Hostname": "Nom d'hôte",
"maps": "mapas",
"map": "mapa",
"method": "método",
"tools": "ferramentas",
"next": "próxima",

View File

@ -121,13 +121,11 @@
}
}
%}
{% set services = dict() %}
{% set services = {
"hapservers_desc": "Checker следит за сервисом. Если сервис изменит свой статус, Checker оповестит через Telegram, Slack, Email, Web",
"last_edit": "Посл. редак."
}
%}
{% set errors = dict() %}
{% set errors = {
"cannot_get_info": "Невозможно получить информацию о",
"something_wrong": "Что-то пошло не так",
@ -237,8 +235,20 @@
},
}
%}
{% set phrases = dict() %}
{% set phrases = {
"explain_config": "Объясни, что делает этот фрагмент конфигурации",
"just_explain": "Просто дай пояснение, не переписывай.",
"optimize_config": "Оптимизируй конфигурационный фрагмент. Сохрани синтаксис.",
"comment_config": "Вставь комментарии (начиная с #) в каждую строку этого фрагмента. Сохрани конфиг рабочим. Сохрани синтаксис.",
"syntax_config": "Проверьте синтаксис. Сохраните работоспособность конфигурации. Сохраните синтаксис.",
"empty_ans": "Пустой ответ.",
"explain_selection": "Объяснить выделенное",
"optimize_selection": "Оптимизировать конфиг",
"enter_comment": "Вставить комментарии",
"preview_change": "Предпросмотр изменений",
"select_part": "Выделите участок конфига.",
"error_parsing": "Ошибка парсинга JSON от сервера.",
"check_syntax": "Проверить синтаксис",
"config_file_name": "Имя конфиг файла",
"no_events_added": "События еще не добавлены.",
"upload_and_restart": "Загрузить и перезагрузить",
@ -358,6 +368,7 @@
"back_des1": "Раздел «Бэкэнд» описывает набор серверов, к которым прокси-сервер будет подключаться для переадресации входящих соединений.",
"ssl_offloading": "Термин «SSL termination» означает, что вы выполняете все операции шифрования и дешифрования на границе вашей сети, например, на балансировщике нагрузки.",
"http_https": "Включить перенаправление со схемы HTTP на схему HTTPS",
"enable_http2": "Включить HTTP 2",
"maxconn_fix": "Зафиксировать максимальное количество одновременных подключений на внешнем интерфейсе",
"maxconn_desc": "Это значение не должно превышать глобальное значение maxconn. Глобальное значение maxconn по умолчанию",
"antibot": "К сожалению, большая часть ботов используется в злонамеренных целях. Их намерения включают веб-скрапинг, рассылку спама, флуд запросов, перебор и сканирование уязвимостей. Например, боты могут очищать ваши прайс-листы, чтобы конкуренты могли последовательно подрезать вас или создавать конкурентоспособное решение, используя ваши данные. Или они могут попытаться найти форумы и разделы комментариев, где они могут размещать спам. В других случаях они сканируют ваш сайт в поисках слабых мест в системе безопасности",
@ -369,7 +380,6 @@
"cookie": "Чтобы отправить клиента на тот же сервер, на который он был отправлен ранее, чтобы повторно использовать сеанс на этом сервере, вы можете включить сохранение сеанса на основе файлов cookie. Добавьте директиву cookie в раздел backend и установите для параметра cookie уникальное значение в каждой строке сервера.",
"c_prefix": "Это ключевое слово указывает, что вместо того, чтобы полагаться на выделенный файл cookie для постоянства, существующий будет заполнен.",
"c_nocache": "Этот вариант рекомендуется в сочетании с режимом вставки, когда между клиентом и HAProxy есть кеш",
"c_nocache": "Этот вариант рекомендуется в сочетании с режимом вставки, когда между клиентом и HAProxy есть кеш",
"c_postonly": "Этот параметр гарантирует, что вставка файлов cookie будет выполняться только в ответах на запросы POST",
"c_dynamic": "Активируйте динамические файлы cookie. При использовании cookie сеанса динамически создается для каждого сервера",
"def_backend": "Если вы хотите использовать бэкэнд по умолчанию",
@ -426,6 +436,8 @@
"keepalive": "Параметр Connections задает максимальное количество неактивных активных подключений к вышестоящим серверам, которые сохраняются в кэше каждого рабочего процесса. Когда это число превышено, последние использовавшиеся соединения закрываются.",
"fail_timeout": "Время, в течение которого должно произойти заданное количество неудачных попыток связи с сервером, чтобы сервер считался недоступным; и период времени, в течение которого сервер будет считаться недоступным.",
"max_fails": "Задает количество неудачных попыток связи с сервером, которое должно произойти в течение времени, заданного параметром fail_timeout, чтобы сервер считался недоступным в течение времени, также заданного параметром fail_timeout. По умолчанию количество неудачных попыток равно 1.",
"def_backend": "Если хотите использовать upstream",
"def_backend_exit": "upstream должен существовать",
}
}
%}
@ -686,7 +698,6 @@
"action": "действие",
"actions": "действия",
"command": "команда",
"save": "сохранить",
"change": "изменение",
"change2": "изменить",
"changes": "изменений",
@ -767,6 +778,8 @@
"name": "имя",
"article": "статье",
"w_copy": "копировать",
"w_cut": "вырезать",
"w_paste": "вставить",
"for": "для",
"history": "история",
"history2": "историю",
@ -811,7 +824,6 @@
"message": "сообщение",
"menu": "меню",
"language": "язык",
"apply": "применить",
"logout": "выйти",
"last": "последняя",
"last2": "последнее",
@ -870,7 +882,6 @@
"of": "",
"display": "показать",
"default_backend": "Бэкенд по умолчанию",
"rule": "правило",
"existing": "существующие",
"domain": "домен",
"domains": "домены",
@ -920,8 +931,6 @@
"instance_type": "Тип инстанса",
"filter": "фильтр",
"rule_name": "Название правила",
"rule": "правило",
"rules": "правила",
"send": "отправить",
"additions": "дополнения",
"deletions": "удаления",
@ -944,7 +953,6 @@
"cert_expire": "Срок действия сертификата",
"Hostname": "Имя хоста",
"maps": "карты",
"map": "карта",
"method": "метод",
"tools": "инструменты",
"next": "дальше",

View File

@ -423,6 +423,8 @@
"keepalive": "connections 参数设置每个工作进程中作为缓存保留的上游服务器的最大空闲 keepalive 连接数。当超过此数量时,最久未使用的连接将被关闭。",
"fail_timeout": "在指定时间内发生指定次数的与服务器通信失败,以将服务器视为不可用的时间;以及服务器将被视为不可用的时间段。",
"max_fails": "设置在 fail_timeout 参数定义的时间段内,与服务器通信失败的最大尝试次数,以将服务器视为不可用。此失败时间段同样由 fail_timeout 参数定义。默认情况下,失败的最大尝试次数为 1。",
"def_backend": "如果你想使用上游",
"def_backend_exit": "上游必须存在",
}
}
%}

View File

@ -84,7 +84,7 @@
{{ lang.words.server|title() }}
</td>
<td>
{{ select('new-le-server_id', values=all_servers, is_servers='true', by_id=1) }}
{{ select('new-le-server_id', values=g.user_params['servers'], is_servers='true', by_id=1) }}
</td>
</tr>
<tr>

View File

@ -11,7 +11,8 @@ import app.modules.common.common as common
import app.modules.service.ha_cluster as ha_cluster
import app.modules.service.installation as service_mod
from app.middleware import get_user_params, page_for_admin, check_group, check_services
from app.modules.roxywi.class_models import BaseResponse, IdResponse, HAClusterRequest, HAClusterVIP
from app.modules.common.common_classes import SupportClass
from app.modules.roxywi.class_models import BaseResponse, IdResponse, HAClusterRequest, HAClusterVIP, GroupQuery
class HAView(MethodView):
@ -125,7 +126,6 @@ class HAView(MethodView):
if not cluster_id:
if request.method == 'GET':
kwargs = {
'clusters': ha_sql.select_clusters(self.group_id),
'is_needed_tool': common.is_tool('ansible'),
'user_subscription': roxywi_common.return_user_subscription(),
'lang': g.user_params['lang'],
@ -349,7 +349,7 @@ class HAView(MethodView):
if body.reconfigure:
try:
self._install_service(body, cluster_id)
return self._install_service(body, cluster_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot reconfigure cluster')
@ -389,26 +389,25 @@ class HAView(MethodView):
@staticmethod
def _install_service(body: HAClusterRequest, cluster_id: int):
tasks_ids = []
try:
output = service_mod.install_service('keepalived', body, cluster_id)
task_id = service_mod.install_service('keepalived', body, cluster_id)
tasks_ids.append(task_id)
except Exception as e:
raise e
if len(output['failures']) > 0 or len(output['dark']) > 0:
raise Exception('Cannot install Keepalived. Check Apache error log')
cluster_services = ha_cluster.get_services_dict(body)
for service, value in cluster_services.items():
if not value['enabled']:
continue
else:
try:
output = service_mod.install_service(service, body)
task_id = service_mod.install_service(service, body)
tasks_ids.append(task_id)
except Exception as e:
raise e
if len(output['failures']) > 0 or len(output['dark']) > 0:
raise Exception(f'Cannot install {service.title()}. Check Apache error log')
return jsonify({"status": "accepted", "tasks_ids": [tasks_ids]}), 202
class HAVIPView(MethodView):
@ -740,3 +739,64 @@ class HAVIPsView(MethodView):
vips = ha_sql.select_cluster_vips(cluster_id)
vips = [model_to_dict(vip, recurse=False) for vip in vips]
return jsonify(vips)
class HAClustersView(MethodView):
methods = ["GET"]
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()]
@validate(query=GroupQuery)
def get(self, service: str, query: GroupQuery):
"""
Get HA clusters list.
---
tags:
- HA Clusters
summary: Retrieve a list of clusters based on a specific group ID.
description: Returns a list of clusters for a given group ID. The response contains information about clusters such as `description`, `group_id`, `id`, `name`, `pos`, and other attributes.
parameters:
- name: service
in: path
type: string
required: true
description: The service identifier. Can be only "cluster".
- name: group_id
in: query
required: false
type: integer
description: The ID of the group used to filter clusters. Only for superAdmin role.
responses:
200:
description: A list of clusters.
schema:
type: array
items:
type: object
properties:
description:
type: string
description: Description of the cluster.
group_id:
type: integer
description: ID of the group the cluster belongs to.
id:
type: integer
description: Cluster ID.
name:
type: string
description: Name of the cluster.
pos:
type: integer
description: Position of the cluster.
syn_flood:
type: integer
description: SYN flood state of the cluster.
"""
clusters_list = []
group_id = SupportClass.return_group_id(query)
clusters = ha_sql.select_clusters(group_id)
for cluster in clusters:
clusters_list.append(model_to_dict(cluster, recurse=False))
return jsonify(clusters_list)

View File

@ -1,6 +1,6 @@
from typing import Union, Literal
from flask import request
from flask import request, jsonify
from flask.views import MethodView
from flask_pydantic import validate
from flask_jwt_extended import jwt_required
@ -132,19 +132,17 @@ class InstallView(MethodView):
if not body.services:
body.services = {service: HAClusterService(enabled=1, docker=body.docker)}
try:
output = service_mod.install_service(service, body)
task_id = service_mod.install_service(service, body)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, f'Cannot install {service.title()}')
if 'api' in request.url:
try:
service_sql.update_hapwi_server(server_id, body.checker, body.metrics, body.auto_start, service)
if len(output['failures']) > 0 or len(output['dark']) > 0:
raise Exception(f'Cannot install {service.title()}. Check Apache error log')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, f'Cannot update Tools settings for {service.title()}')
else:
return output
return jsonify({"status": "accepted", "tasks_ids": [task_id]}), 202
return IdStrResponse(id=f'{server_id}-{service}').model_dump(mode='json'), 201
@validate(body=ServiceInstall)

View File

@ -225,7 +225,8 @@ class UDPListener(MethodView):
roxywi_common.logging(listener_id, f'UDP listener {body.name} has been created', keep_history=1,
roxywi=1, service='UDP Listener')
if body.reconfigure:
self._reconfigure(listener_id, 'install')
task_id = self._reconfigure(listener_id, 'install')
return jsonify({"status": "accepted", "tasks_ids": [task_id], 'id': listener_id}), 202
return IdResponse(id=listener_id).model_dump(mode='json')
except Exception as e:
return roxywi_common.handle_json_exceptions(e, 'Cannot create UDP listener')
@ -377,9 +378,8 @@ class UDPListener(MethodView):
except Exception as e:
raise Exception(e)
try:
output = service_mod.run_ansible(inv, server_ips, 'udp')
if len(output['failures']) > 0 or len(output['dark']) > 0:
raise Exception(f'Cannot {action} UDP listener. Check Apache error log')
task_id = service_mod.run_ansible_thread(inv, server_ips, 'udp', 'UDP listener')
return task_id
except Exception as e:
raise Exception(f'Cannot {action} UDP listener: {e}')

View File

@ -13,7 +13,7 @@ peewee>=3.14.10
PyMySQL>=1.0.2
distro>=1.2.0
psutil>=5.9.1
pdpyras>=4.5.2
pagerduty>=2.1.2
pika>=1.3.1
Werkzeug==3.0.3
Flask==3.0.3