v8.0: Add DELETE endpoint for version deletion in ServiceConfigView

Added support for deleting configuration file versions via the DELETE method in ServiceConfigVersionsView. Updated corresponding routes, templates, and scripts to handle this functionality.
pull/399/head
Aidaho 2024-08-20 10:39:40 +03:00
parent 89022e59be
commit 41a8c90556
11 changed files with 193 additions and 103 deletions

View File

@ -55,7 +55,7 @@ bp.add_url_rule('/ha/<service>/<int:cluster_id>/vips', view_func=HAVIPsView.as_v
register_api_id_ip(ServiceView, 'service', '/status', ['GET'])
register_api_id_ip(ServiceBackendView, 'service_backend', '/backend', ['GET'])
register_api_id_ip(ServiceConfigView, 'config_view')
register_api_id_ip(ServiceConfigVersionsView, 'config_version', '/versions', methods=['GET'])
register_api_id_ip(ServiceConfigVersionsView, 'config_version', '/versions', methods=['GET', 'DELETE'])
register_api_id_ip(CheckerView, 'checker', '/tools')
register_api_id_ip(InstallView, 'install', '/install', methods=['POST'])
register_api_id_ip(ServiceActionView, 'service_action', '/<any(start, stop, reload, restart):action>', methods=['GET'])

View File

@ -288,7 +288,7 @@ def select_table_metrics(group_id):
avg_cur_1h, avg_cur_24h, avg_cur_3d, max_con_1h, max_con_24h, max_con_3d from
(select servers.ip from servers where haproxy_metrics = 1 ) as ip,
(select servers.ip, servers.hostname as hostname from servers left join metrics as metr on servers.ip = metr.serv where servers.metrics = 1 %s) as hostname,
(select servers.ip, servers.hostname as hostname from servers left join metrics as metr on servers.ip = metr.serv where servers.haproxy_metrics = 1 %s) as hostname,
(select servers.ip,round(avg(metr.sess_rate), 1) as avg_sess_1h from servers
left join metrics as metr on metr.serv = servers.ip
@ -383,7 +383,7 @@ def select_table_metrics(group_id):
avg_cur_24h, avg_cur_3d, max_con_1h, max_con_24h, max_con_3d from
(select servers.ip from servers where haproxy_metrics = 1 ) as ip,
(select servers.ip, servers.hostname as hostname from servers left join metrics as metr on servers.ip = metr.serv where servers.metrics = 1 %s) as hostname,
(select servers.ip, servers.hostname as hostname from servers left join metrics as metr on servers.ip = metr.serv where servers.haproxy_metrics = 1 %s) as hostname,
(select servers.ip,round(avg(metr.sess_rate), 1) as avg_sess_1h from servers
left join metrics as metr on metr.serv = servers.ip

View File

@ -181,6 +181,10 @@ class ConfigFileNameQuery(BaseModel):
version: Optional[str] = None
class VersionsForDelete(BaseModel):
versions: List[str]
class ConfigRequest(BaseModel):
action: Literal['save', 'test', 'reload', 'restart']
file_name: Optional[str] = None

View File

@ -17,10 +17,11 @@ import app.modules.config.common as config_common
import app.modules.config.section as section_mod
import app.modules.service.haproxy as service_haproxy
import app.modules.server.server as server_mod
from app.views.service.views import ServiceConfigView
from app.views.service.views import ServiceConfigView, ServiceConfigVersionsView
from app.modules.roxywi.class_models import DataStrResponse
bp.add_url_rule('/<service>/<server_id>', view_func=ServiceConfigView.as_view('config_view_ip'), methods=['POST'])
bp.add_url_rule('/<service>/<server_id>/versions', view_func=ServiceConfigVersionsView.as_view('config_version'), methods=['DELETE'])
@bp.before_request
@ -136,48 +137,16 @@ def config(service, serv, edit, config_file_name, new):
return render_template('config.html', **kwargs)
@bp.route('/versions/<service>', defaults={'server_ip': None}, methods=['GET', 'POST'])
@bp.route('/versions/<service>/<server_ip>', methods=['GET', 'POST'])
@bp.route('/versions/<service>', defaults={'server_ip': None}, methods=['GET'])
@bp.route('/versions/<service>/<server_ip>', methods=['GET'])
@check_services
@get_user_params(disable=1)
def versions(service, server_ip):
roxywi_auth.page_for_admin(level=3)
after_save = ''
file = set()
stderr = ''
file_format = config_common.get_file_format(service)
if request.form.get('del'):
after_save = 1
for get in request.form.getlist('do_delete'):
if file_format in get and server_ip in get:
try:
if config_sql.delete_config_version(service, get):
try:
os.remove(get)
except OSError as e:
if 'No such file or directory' in str(e):
pass
else:
config_dir = config_common.get_config_dir('haproxy')
os.remove(os.path.join(config_dir, get))
try:
file.add(get + "\n")
roxywi_common.logging(
server_ip, f"Version of config has been deleted: {get}", login=1, keep_history=1,
service=service
)
except Exception:
pass
except OSError as e:
stderr = "Error: %s - %s." % (e.filename, e.strerror)
kwargs = {
'serv': server_ip,
'aftersave': after_save,
'file': file,
'service': service,
'stderr': stderr,
'lang': g.user_params['lang']
}
return render_template('delver.html', **kwargs)
@ -193,48 +162,48 @@ def list_of_version(service):
return config_mod.list_of_versions(server_ip, service, config_ver, for_delver)
@bp.route('/versions/<service>/<server_ip>/<configver>', defaults={'save': None}, methods=['GET', 'POST'])
@bp.route('/versions/<service>/<server_ip>/<configver>/save', defaults={'save': 1}, methods=['GET', 'POST'])
@bp.route('/versions/<service>/<server_ip>/<configver>', methods=['GET'])
@check_services
@get_user_params(disable=1)
def show_version(service, server_ip, configver, save):
def show_version(service, server_ip, configver):
roxywi_auth.page_for_admin(level=3)
service_desc = service_sql.select_service(service)
config_dir = config_common.get_config_dir('haproxy')
configver = config_dir + configver
aftersave = 0
stderr = ''
if save:
aftersave = 1
save_action = request.form.get('save')
try:
roxywi_common.logging(
server_ip, f"Version of config has been uploaded {configver}", login=1, keep_history=1, service=service
)
except Exception:
pass
if service == 'keepalived':
stderr = config_mod.upload_and_restart(server_ip, configver, save_action, service)
elif service in ('nginx', 'apache'):
config_file_name = config_sql.select_remote_path_from_version(server_ip=server_ip, service=service, local_path=configver)
stderr = config_mod.master_slave_upload_and_restart(server_ip, configver, save_action, service_desc.slug, config_file_name=config_file_name)
else:
stderr = config_mod.master_slave_upload_and_restart(server_ip, configver, save_action, service)
kwargs = {
'serv': server_ip,
'aftersave': aftersave,
'configver': configver,
'service': service,
'stderr': stderr,
'lang': g.user_params['lang']
}
return render_template('configver.html', **kwargs)
@bp.route('/versions/<service>/<server_ip>/<configver>/save', methods=['POST'])
@check_services
@get_user_params()
def save_version(service, server_ip, configver):
roxywi_auth.page_for_admin(level=3)
config_dir = config_common.get_config_dir('haproxy')
configver = config_dir + configver
service_desc = service_sql.select_service(service)
save_action = request.json.get('action')
try:
roxywi_common.logging(
server_ip, f"Version of config has been uploaded {configver}", login=1, keep_history=1, service=service
)
except Exception:
pass
if service == 'keepalived':
stderr = config_mod.upload_and_restart(server_ip, configver, save_action, service)
elif service in ('nginx', 'apache'):
config_file_name = config_sql.select_remote_path_from_version(server_ip=server_ip, service=service, local_path=configver)
stderr = config_mod.master_slave_upload_and_restart(server_ip, configver, save_action, service_desc.slug, config_file_name=config_file_name)
else:
stderr = config_mod.master_slave_upload_and_restart(server_ip, configver, save_action, service)
return DataStrResponse(data=stderr).model_dump(mode='json'), 201
@bp.route('/section/haproxy/<server_ip>')
@get_user_params()
def haproxy_section(server_ip):

View File

@ -959,7 +959,7 @@ $( function() {
});
let add_server_var = '<br /><input name="servers" title="Backend IP" size=14 placeholder="xxx.xxx.xxx.xxx" class="form-control second-server" style="margin: 2px 0 4px 0;">: ' +
'<input name="server_port" required title="Backend port" size=3 placeholder="yyy" class="form-control second-server add_server_number" type="number"> ' +
'port check: <input name="port_check" required title="Maxconn. Default 200" size=5 value="200" class="form-control add_server_number" type="number">' +
'Port check: <input name="port_check" required title="Maxconn. Default 200" size=5 value="200" class="form-control add_server_number" type="number">' +
' maxconn: <input name="server_maxconn" required title="Maxconn. Default 200" size=5 value="200" class="form-control add_server_number" type="number">'
$('[name=add-server-input]').click(function () {
$("[name=add_servers]").append(add_server_var);

View File

@ -82,4 +82,73 @@ $( function() {
});
e.preventDefault();
});
$("#save_version").on("click", ":submit", function (e) {
let frm = $('#save_version');
let unindexed_array = frm.serializeArray();
let indexed_array = {};
$.map(unindexed_array, function (n, i) {
if (n['value'] != 'undefined') {
indexed_array[n['name']] = n['value'];
}
});
indexed_array['action'] = $(this).val();
$.ajax({
url: frm.attr('action'),
dataType: 'json',
data: JSON.stringify(indexed_array),
type: frm.attr('method'),
contentType: "application/json; charset=UTF-8",
success: function (data) {
toastr.clear();
data.data = data.data.replace(/\n/g, "<br>");
returnNiceCheckingConfig(data.data);
if (data.status === 'failed') {
toastr.warning(data.error)
}
}
});
e.preventDefault();
});
$("#delete_versions_form").on("click", ":submit", function (e) {
let frm = $('#delete_versions_form');
let unindexed_array = frm.serializeArray();
let indexed_array = {};
indexed_array['versions'] = [];
$.map(unindexed_array, function (n, i) {
if (n['value'] != 'undefined') {
if (n['name'] === 'versions') {
indexed_array['versions'].push(n['value']);
} else {
indexed_array[n['name']] = n['value'];
}
}
});
indexed_array['action'] = $(this).val();
$.ajax({
url: frm.attr('action'),
dataType: 'json',
data: JSON.stringify(indexed_array),
type: frm.attr('method'),
contentType: "application/json; charset=UTF-8",
statusCode: {
204: function (xhr) {
toastr.success('The versions of configs have been deleted');
showListOfVersion(1);
},
404: function (xhr) {
toastr.success('The versions of configs have been deleted');
showListOfVersion(1);
}
},
success: function (data) {
if (data) {
if (data.status === "failed") {
toastr.error(data);
}
}
}
});
e.preventDefault();
});
})

View File

@ -368,7 +368,7 @@
<br>
{% if role <= 3 %}
{% if not is_serv_protected or role <= 2 %}
<form action="/config/versions/{{service}}/{{serv}}/{{configver}}/save" method="post" class="left-space">
<form action="/config/versions/{{service}}/{{serv}}/{{configver}}/save" id="save_version" method="post" class="left-space">
<input type="hidden" value="{{serv}}" name="serv">
<input type="hidden" value="{{service}}" name="service">
<input type="hidden" value="{{configver}}" name="configver">
@ -405,5 +405,4 @@
function expand_button(event) {
$("#expand_link").click();
}
</script>

View File

@ -1,5 +1,6 @@
{% import 'languages/'+lang|default('en')+'.html' as lang1 %}
{% from 'include/input_macros.html' import copy_to_clipboard %}
<script src="/static/js/configshow.js"></script>
{% if for_delver == '1' %}
<script>
$(document).ready(function() {
@ -47,7 +48,7 @@
margin: 0;
}
</style>
<form action="{{action}}" method="post">
<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>
<tr class="overviewHead">
@ -76,7 +77,7 @@
<tr>
<td style="padding-left: 15px">
<label for="{{c.id}}" id="select_{{c.id}}"></label>
<input type="checkbox" value="{{c.local_path}}" name="do_delete" id="{{c.id}}">
<input type="checkbox" value="{{c.local_path}}" name="versions" id="{{c.id}}">
</td>
<td>
{% for u in users %}

View File

@ -2,6 +2,7 @@
{% block title %}{{ lang.menu_links.versions.h2 }} {{ lang.words[service] }}{% endblock %}
{% block h2 %}{{ lang.menu_links.versions.h2 }} {{ lang.words[service] }}{% endblock %}
{% block content %}
<script src="/static/js/configshow.js"></script>
<p>
<form action="/config/versions/{{service}}/{{serv}}" method="post" class="left-space">
<input type="hidden" id="service" value="{{service}}">
@ -9,19 +10,6 @@
<button type="submit" value="open" name="open" class="btn btn-default">{{lang.words.open|title()}}</button>
</form>
</p>
{% if not aftersave %}
{% if stderr %}
{% include 'include/errors.html' %}
{% endif %}
<div id="config_version_div"></div>
<script>showListOfVersion(0)</script>
{% endif %}
{% if aftersave %}
<div class="alert alert-info alert-two-row">{{lang.phrases.version_has_been_uploaded}}: {{ configver }} </div>
{% if 'is valid' not in stderr %}
{% include 'include/errors.html' %}
{% else %}
<div class="alert alert-success">{{lang.words.config|title()}} {{lang.words.is}} {{lang.words.valid}}</div>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -18,25 +18,11 @@
{% endif %}
</form>
</p>
{% if aftersave %}
<div class="alert alert-info"><b>{{lang.phrases.files_been_deleted}}:</b><br /> </div>
{% if stderr %}
{% include 'include/errors.html' %}
{% else %}
<div class="alert alert-success" style="margin-left: var(--indent);">
{% for f in file %}
{{f}}
{% endfor %}
</div>
{% endif %}
{% endif %}
<div id="config_version_div"></div>
{% if not aftersave %}
<div class="add-note addName alert-info" style="width: inherit; margin-right: 15px; margin-top: 40%">
{{lang.phrases.work_with_prev}} {{ service[0]|upper}}{{service[1:] }}. {{lang.phrases.roll_back}}
</div>
{% endif %}
{% if serv and not aftersave %}
{% if serv %}
{% for select in g.user_params['servers'] %}
{% if select.2 == serv %}
<script>showListOfVersion(1)</script>

View File

@ -1,3 +1,4 @@
import os
from typing import Union, Literal
from flask.views import MethodView
@ -6,6 +7,7 @@ from flask import jsonify, g
from flask_jwt_extended import jwt_required
import app.modules.db.sql as sql
import app.modules.db.config as config_sql
import app.modules.db.server as server_sql
import app.modules.db.service as service_sql
import app.modules.roxywi.common as roxywi_common
@ -16,7 +18,8 @@ import app.modules.service.action as service_action
import app.modules.service.common as service_common
from app.middleware import get_user_params, page_for_admin, check_group, check_services
from app.modules.roxywi.exception import RoxywiResourceNotFound
from app.modules.roxywi.class_models import BaseResponse, ErrorResponse, DataResponse, DataStrResponse, ConfigFileNameQuery, ConfigRequest
from app.modules.roxywi.class_models import BaseResponse, ErrorResponse, DataResponse, DataStrResponse, \
ConfigFileNameQuery, ConfigRequest, VersionsForDelete
from app.modules.common.common_classes import SupportClass
@ -404,10 +407,10 @@ class ServiceConfigView(MethodView):
class ServiceConfigVersionsView(MethodView):
methods = ['GET', 'POST']
methods = ['GET', 'POST', 'DELETE']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=4), check_group()]
def get(self, service, server_id: Union[int, str]):
def get(self, service: str, server_id: Union[int, str]):
"""
This endpoint returns a list of configuration file versions for a specified service on a specific server.
---
@ -439,3 +442,74 @@ class ServiceConfigVersionsView(MethodView):
file_format = config_common.get_file_format(service)
files = roxywi_common.get_files(config_dir, file_format, server_ip)
return DataResponse(data=files).model_dump(mode='json')
@validate(body=VersionsForDelete)
def delete(self, service: str, server_id: Union[int, str], body: VersionsForDelete):
"""
This endpoint deletes specified configuration file versions for a particular service on a specific server.
---
tags:
- Service config
parameters:
- in: path
name: service
type: string
enum: [haproxy, nginx, apache, keepalived]
required: true
description: The type of service (haproxy, nginx, apache, keepalived)
- in: path
name: server_id
type: string
required: true
description: The ID or IP of the server
- in: body
name: body
description: JSON array of paths to version files to be deleted
schema:
type: array
items:
type: string
description: Path to the version file
required: true
responses:
204:
description: 'Successful operation, specified configuration file versions are deleted'
400:
description: 'Invalid service type or server ID'
404:
description: 'Service or server not found'
500:
description: 'Internal server error'
"""
file = set()
file_format = config_common.get_file_format(service)
try:
server_ip = SupportClass(False).return_server_ip_or_id(server_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e ,'')
for get in body.versions:
if file_format in get and server_ip in get:
try:
if config_sql.delete_config_version(service, get):
try:
os.remove(get)
except OSError as e:
if 'No such file or directory' in str(e):
pass
else:
config_dir = config_common.get_config_dir('haproxy')
os.remove(os.path.join(config_dir, get))
try:
file.add(get + "\n")
roxywi_common.logging(
server_ip, f"Version of config has been deleted: {get}", login=1, keep_history=1,
service=service
)
except Exception:
pass
except OSError as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete config version')
return BaseResponse().model_dump(mode='json'), 204