v8.1.6: Add support for editing specific config sections and templates

Enhanced configuration management by introducing support for editing specific sections in HAProxy configurations via `edit_section`. Added server templates with structured validation, improved file encoding handling, and addressed edge cases in multiple components for greater robustness.
pull/418/head v8.1.6
Aidaho 2025-03-14 12:29:16 +03:00
parent 32db39fdd5
commit 66f1760ca3
11 changed files with 68 additions and 24 deletions

View File

@ -460,7 +460,7 @@ def compare_config(service: str, left: str, right: str) -> str:
return render_template('ajax/compare.html', stdout=output, lang=lang)
def show_config(server_ip: str, service: str, config_file_name: str, configver: str, claims: dict) -> str:
def show_config(server_ip: str, service: str, config_file_name: str, configver: str, claims: dict, edit_section: str) -> str:
"""
Get and display the configuration file for a given server.
@ -516,7 +516,8 @@ def show_config(server_ip: str, service: str, config_file_name: str, configver:
'is_serv_protected': server_sql.is_serv_protected(server_ip),
'is_restart': service_sql.select_service_setting(server.server_id, service, 'restart'),
'lang': roxywi_common.get_user_lang_for_flask(),
'hostname': server.hostname
'hostname': server.hostname,
'edit_section': edit_section
}
return render_template('ajax/config_show.html', **kwargs)

View File

@ -209,6 +209,13 @@ class ConfigRequest(BaseModel):
config_local_path: Optional[str] = None
config: str
@root_validator(skip_on_failure=True)
@classmethod
def decode_config(cls, values):
if 'config' in values:
values['config'] = values['config'].encode('utf-8')
return values
class LoginRequest(BaseModel):
login: EscapedString
@ -407,6 +414,13 @@ class HaproxyServersCheck(BaseModel):
inter: Optional[int] = 2000
class HaproxyServersTemplate(BaseModel):
prefix: int
count: int
servers: Union[IPvAnyAddress, DomainName]
port: Annotated[int, Gt(1), Le(65535)]
class HaproxyCircuitBreaking(BaseModel):
observe: Literal['layer7', 'layer4']
error_limit: int
@ -415,7 +429,7 @@ class HaproxyCircuitBreaking(BaseModel):
class HaproxyConfigRequest(BaseModel):
balance: Optional[Literal['roundrobin', 'source', 'leastconn', 'first', 'rdp-cookie', 'uri', 'uri whole', 'static-rr']] = None
mode: Literal['tcp', 'http'] = 'http'
mode: Literal['tcp', 'http', 'log'] = 'http'
type: Literal['listen', 'frontend', 'backend']
name: EscapedString
option: Optional[str] = None
@ -425,6 +439,7 @@ class HaproxyConfigRequest(BaseModel):
headers: Optional[List[HaproxyHeaders]] = None
acls: Optional[List[HaproxyAcls]] = None
backend_servers: Optional[List[HaproxyBackendServer]] = None
servers_template: Optional[HaproxyServersTemplate] = None
blacklist: Optional[str] = ''
whitelist: Optional[str] = ''
ssl: Optional[HaproxySSL] = None

View File

@ -89,7 +89,7 @@ def show_roxy_log(
if syslog_server is None or syslog_server == '':
raise Exception('error: Syslog server is enabled, but there is no IP for syslog server')
if waf:
if waf and service == 'haproxy':
local_path_logs = '/var/log/waf.log'
commands = "sudo cat %s |tail -%s %s %s" % (local_path_logs, rows, grep_act, exgrep_act)

View File

@ -56,7 +56,7 @@ def ssh_command(server_ip: str, commands: str, **kwargs):
def subprocess_execute(cmd):
import subprocess
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True, errors='backslashreplace')
stdout, stderr = p.communicate()
output = stdout.splitlines()
return output, stderr

View File

@ -39,9 +39,10 @@ def show_config(service):
configver = request.json.get('configver')
server_ip = request.json.get('serv')
claims = get_jwt()
edit_section = request.json.get('edit_section')
try:
data = config_mod.show_config(server_ip, service, config_file_name, configver, claims)
data = config_mod.show_config(server_ip, service, config_file_name, configver, claims, edit_section)
return DataStrResponse(data=data).model_dump(mode='json'), 200
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, '')
@ -114,8 +115,9 @@ def config(service, serv, edit, config_file_name, new):
pass
try:
conf = open(cfg, "r")
conf = open(cfg, "rb")
config_read = conf.read()
config_read = config_read.decode('utf-8')
conf.close()
except IOError as e:
return f'Cannot read imported config file {e}', 200

View File

@ -41,7 +41,6 @@ def logs_internal():
selects.append(['roxy-wi.access.log', 'access.log'])
kwargs = {
'autorefresh': 1,
'selects': selects,
'serv': log_file,
'lang': g.user_params['lang']
@ -63,6 +62,8 @@ def logs(service, waf):
# hour1 = request.args.get('hour1')
# minute1 = request.args.get('minute1')
log_file = request.args.get('file')
service_desc = service_sql.select_service(service)
service_name = service_desc.service
if rows is None:
rows = 10
@ -70,17 +71,14 @@ def logs(service, waf):
grep = ''
if service in ('haproxy', 'nginx', 'keepalived', 'apache') and not waf:
service_desc = service_sql.select_service(service)
service_name = service_desc.service
servers = roxywi_common.get_dick_permit(service=service_desc.slug)
elif waf:
service_name = 'WAF'
servers = roxywi_common.get_dick_permit(haproxy=1)
servers = roxywi_common.get_dick_permit(service=service_desc.slug)
else:
return redirect(url_for('index'))
kwargs = {
'autorefresh': 1,
'servers': servers,
'serv': serv,
'service': service,

View File

@ -130,13 +130,17 @@
default-server observe {{ config.circuit_breaking.observe }} error-limit {{ config.circuit_breaking.error_limit }} on-error {{ config.circuit_breaking.on_error }}
{% endif -%}
{% if config.backend_servers != 'None' -%}
{% if config.backend_servers != 'None' and config.servers_template == 'None' -%}
{% for backend in config.backend_servers -%}
server {{ backend.server }} {{ backend.server }}:{{ backend.port }} port {{ backend.port_check }} {{ check_option }} {{ ssl_check_option }} maxconn {{ backend.maxconn }}{% if backend.send_proxy %} send-proxy{% endif %}{% if backend.backup %} backup {% endif %}
{% endfor -%}
{% endif -%}
{% if config.servers_template != 'None' -%}
server-template {{ config.servers_template.prefix }} {{ config.servers_template.count }} {{ config.servers_template.servers }}: {{ config.servers_template.port }} {{ check_option }}
{% endif -%}
{% if config.backends and config.backends != 'None' -%}
use_backend {{ config.backends }}
{% endif %}

View File

@ -40,7 +40,7 @@ function overviewHapserverBackends(serv, hostname, service) {
$("#top-" + hostname).empty();
for (let i in data.data) {
if (service === 'haproxy') {
div = `<a href="/config/section/haproxy/${serv}/${data.data[i]}" target="_blank" style="padding-right: 10px;">${data.data[i]}</a> `
div = `<a href="/config/haproxy/${serv}/show/?section=${data.data[i]}" target="_blank" style="padding-right: 10px;">${data.data[i]}</a> `
} else if (service === 'nginx' || service === 'apache') {
div = `<a href="/config/${service}/${serv}/show/${i}" target="_blank" style="padding-right: 10px;">${data.data[i]}</a>`;
} else {

View File

@ -154,7 +154,7 @@ function showLog() {
toastr.warning('Select a log file first')
return false;
} else {
file = file_from_get;
file = findGetParameter('file');
}
}
if ((file === undefined || file === null) && waf === '') {
@ -174,9 +174,9 @@ function showLog() {
if (service === 'None') {
service = 'haproxy';
}
if (waf && waf != 'haproxy' && waf != 'nginx' && waf != 'apache' && waf != 'keepalived') {
url = "/logs/" + service + "/waf/" + serv + "/" + rows;
waf = 1;
if (waf && waf != 'haproxy' && waf != 'apache' && waf != 'keepalived') {
file = findGetParameter('file');
url = "/logs/" + service + "/waf/" + serv + "/" + rows + '?file_from_get=' + file;
}
$.ajax( {
url: url,
@ -300,6 +300,7 @@ function showCompareConfigs() {
});
}
function showConfig() {
let edit_section = '';
let service = $('#service').val();
let config_file = $('#config_file_name').val()
let config_file_name = encodeURI(config_file);
@ -315,10 +316,16 @@ function showConfig() {
}
}
clearAllAjaxFields();
if (service === 'haproxy') {
edit_section = findGetParameter('section');
let edit_section_uri = '?section=' + edit_section;
}
let json_data = {
"serv": $("#serv").val(),
"service": service,
"config_file_name": config_file_name
"config_file_name": config_file_name,
"edit_section": edit_section
}
$.ajax({
url: "/config/" + service + "/show",
@ -332,7 +339,7 @@ function showConfig() {
toastr.clear();
$("#ajax").html(data.data);
$.getScript(configShow);
window.history.pushState("Show config", "Show config", "/config/" + service + "/" + $("#serv").val() + "/show/" + config_file_name);
window.history.pushState("Show config", "Show config", "/config/" + service + "/" + $("#serv").val() + "/show/" + config_file_name + edit_section_uri);
}
}
});

View File

@ -331,6 +331,16 @@
</span><div>
{% continue %}
{% endif %}
{% if line.startswith('log-forward') %}
</div><span class="param">{{ line }}
{% if role %}
<span class="accordion-link">
<a href="/config/section/haproxy/{{serv}}/{{ line }}">{{lang.words.edit|title()}}/{{lang.words.delete|title()}}</a>
</span>
{% endif %}
</span><div>
{% continue %}
{% endif %}
{% if "acl" in line or "option" in line or "server" in line %}
{% if "timeout" not in line and "default-server" not in line and "#use_backend" not in line and "#" not in line%}
<span class="numRow">
@ -410,6 +420,10 @@
function expand_button(event) {
$("#expand_link").click();
}
console.log("{{edit_section}}");
{% if service == 'haproxy' and edit_section != '' %}
openSection('{{ edit_section }}');
{% endif %}
</script>
<div id="edit-section" style="display: none;"></div>
<div id="dialog-confirm" style="display: none;">

View File

@ -388,7 +388,7 @@ class ServiceConfigView(MethodView):
return ErrorResponse(error=str(e)).model_dump(mode='json')
try:
with open(cfg, 'r') as file:
with open(cfg, 'rb') as file:
conf = file.readlines()
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get config')
@ -461,9 +461,9 @@ class ServiceConfigView(MethodView):
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get config')
try:
with open(cfg, "a") as conf:
with open(cfg, "ab") as conf:
conf.write(body.config)
except IOError as e:
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot write config')
try:
@ -476,7 +476,10 @@ class ServiceConfigView(MethodView):
return f'error: {e}', 200
if body.action != 'test':
config_mod.diff_config(body.config_local_path, cfg)
try:
config_mod.diff_config(body.config_local_path, cfg)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot compare configs')
return DataStrResponse(data=stderr).model_dump(mode='json'), 201