You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

656 lines
26 KiB

import os
from typing import Union, Literal
from flask.views import MethodView
from flask_pydantic import validate
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
import app.modules.config.config as config_mod
import app.modules.config.common as config_common
import app.modules.server.server as server_mod
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, VersionsForDelete
from app.modules.common.common_classes import SupportClass
class ServiceView(MethodView):
methods = ['GET']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=4), check_group()]
def get(self, service: Literal['haproxy', 'nginx', 'apache', 'keepalived'], server_id: Union[int, str]):
"""
This endpoint retrieves information about a specific service.
---
tags:
- Service
parameters:
- in: path
name: service
type: 'string'
required: true
description: The type of service (haproxy, nginx, apache, keepalived)
- in: path
name: server_id
type: 'integer'
required: true
description: The ID or IP of the server
responses:
200:
description: Successful operation
schema:
type: 'object'
properties:
CurrConns:
type: 'string'
description: 'Current connections to HAProxy (only for HAProxy service)'
Maxconn:
type: 'string'
description: 'Maximum connections to HAProxy (only for HAProxy service)'
MaxconnReached:
type: 'string'
description: 'Max connections reached (only for HAProxy service)'
Memmax_MB:
type: 'string'
description: 'Maximum memory in MB (only for HAProxy service)'
PoolAlloc_MB:
type: 'string'
description: 'Memory pool allocated in MB (only for HAProxy service)'
PoolUsed_MB:
type: 'string'
description: 'Memory pool used in MB (only for HAProxy service)'
Uptime:
type: 'string'
description: 'Time the service has been active'
Version:
type: 'string'
description: 'Version of the service'
Process:
type: 'string'
description: 'Number of processes launched by the service'
Status:
type: 'string'
description: 'Status of the service'
default:
description: Unexpected error
"""
try:
server_id = SupportClass().return_server_ip_or_id(server_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, '')
try:
server = server_sql.get_server_with_group(server_id, g.user_params['group_id'])
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot find a server')
if service == 'haproxy':
cmd = 'echo "show info" |nc %s %s -w 1|grep -e "Ver\|CurrConns\|Maxco\|MB\|Uptime:\|Process_num"' % (
server.ip, sql.get_setting('haproxy_sock_port')
)
out = server_mod.subprocess_execute(cmd)
data = self.return_dict_from_out(out[0])
if len(data) == 0:
data = ErrorResponse(error='Cannot get information').model_dump(mode='json')
else:
data['Status'] = self._service_status(data['Process'])
data['auto_start'] = int(server.haproxy_active)
data['checker'] = int(server.haproxy_alert)
data['metrics'] = int(server.haproxy_metrics)
data['docker'] = int(service_sql.select_service_setting(server_id, service, 'dockerized'))
elif service == 'nginx':
is_dockerized = service_sql.select_service_setting(server_id, service, 'dockerized')
if is_dockerized == '1':
container_name = sql.get_setting(f'{service}_container_name')
cmd = (f"sudo docker exec -it {container_name} /usr/sbin/nginx -v 2>&1|awk '{{print $3}}' && "
f"docker ps -a -f name={container_name} --format '{{{{.Status}}}}' && ps ax |grep nginx:|grep -v grep |wc -l")
out = server_mod.ssh_command(server.ip, cmd)
out = out.replace('\n', '')
out1 = out.split('\r')
if out1[0] == 'from':
out1[0] = ''
out1[1] = out1[1].split(')')[1]
else:
out1[0] = out1[0].split('/')[1]
else:
cmd = ("/usr/sbin/nginx -v 2>&1|awk '{print $3}' && systemctl status nginx |grep -e 'Active'"
"|awk '{print $2, $9$10$11$12$13}' && ps ax |grep nginx:|grep -v grep |wc -l")
out = server_mod.ssh_command(server.ip, cmd)
out = out.replace('\n', '')
out1 = out.split('\r')
try:
out1[0] = out1[0].split('/')[1]
try:
out1[1] = out1[1].split(';')[1]
except IndexError:
out1[1] = out1[1].split(' ')[1]
except IndexError:
return ErrorResponse(error='NGINX service not found').model_dump(mode='json'), 404
try:
data = {
"Version": out1[0],
"Uptime": out1[1],
"Process": out1[2],
"Status": self._service_status(out1[2])}
except IndexError:
return ErrorResponse(error='NGINX service not found').model_dump(mode='json'), 404
except Exception as e:
data = ErrorResponse(error=str(e)).model_dump(mode='json')
data['auto_start'] = int(server.nginx_active)
data['checker'] = int(server.nginx_alert)
data['metrics'] = int(server.nginx_metrics)
data['docker'] = int(is_dockerized)
elif service == 'apache':
apache_stats_user = sql.get_setting('apache_stats_user')
apache_stats_password = sql.get_setting('apache_stats_password')
apache_stats_port = sql.get_setting('apache_stats_port')
apache_stats_page = sql.get_setting('apache_stats_page')
cmd = "curl -s -u %s:%s http://%s:%s/%s?auto |grep 'ServerVersion\|Processes\|ServerUptime:'" % \
(apache_stats_user, apache_stats_password, server.ip, apache_stats_port, apache_stats_page)
servers_with_status = list()
try:
out = server_mod.subprocess_execute(cmd)
if out != '':
for k in out:
servers_with_status.append(k)
data = {
"Version": servers_with_status[0][0].split('/')[1].split(' ')[0],
"Uptime": servers_with_status[0][1].split(':')[1].strip(),
"Process": servers_with_status[0][2].split(' ')[1],
"Status": self._service_status(servers_with_status[0][2].split(' ')[1])
}
except IndexError:
data = {
"Version": '',
"Uptime": '',
"Process": 0,
"Status": self._service_status('0')
}
except Exception as e:
data = ErrorResponse(error=str(e)).model_dump(mode='json')
data['auto_start'] = int(server.apache_active)
data['checker'] = int(server.apache_alert)
data['metrics'] = int(server.apache_metrics)
data['docker'] = int(service_sql.select_service_setting(server_id, service, 'dockerized'))
elif service == 'keepalived':
cmd = ("sudo /usr/sbin/keepalived -v 2>&1|head -1|awk '{print $2}' && sudo systemctl status keepalived |grep -e 'Active'"
"|awk '{print $2, $9$10$11$12$13}' && ps ax |grep 'keepalived '|grep -v udp|grep -v grep |wc -l")
try:
out = server_mod.ssh_command(server.ip, cmd)
out1 = out.split()
if out1[0].split('\r')[0] == '/usr/sbin/keepalived:':
return ErrorResponse(error='Keepalived service not found').model_dump(mode='json'), 404
data = {"Version": out1[0].split('\r')[0], "Uptime": out1[2], "Process": out1[3], 'Status': self._service_status(out1[3])}
except IndexError:
return ErrorResponse(error='Keepalived service not found').model_dump(mode='json'), 404
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get version')
data['service'] = service
data['server_id'] = server_id
data['id'] = f'{server_id}-{service}'
return jsonify(data)
@staticmethod
def return_dict_from_out(out):
data = {}
for k in out:
if "Ncat:" not in k:
k = k.split(':')
if k[0] == 'Process_num':
data['Process'] = k[1].strip()
else:
data[k[0]] = k[1].strip()
else:
data = {"error": "Cannot connect to HAProxy"}
return data
@staticmethod
def _service_status(process_num: str) -> str:
if process_num == '0':
return 'stopped'
return 'running'
class ServiceActionView(MethodView):
methods = ['GET']
decorators = [jwt_required(), get_user_params(), page_for_admin(level=3), check_group()]
@staticmethod
def get(service: str, server_id: Union[int, str], action: str):
"""
This endpoint performs a specified action on a certain service on a specific server.
---
tags:
- Service
parameters:
- in: path
name: service
type: 'integer'
required: true
description: The type of service (haproxy, nginx, apache, keepalived, waf_haproxy, waf_nginx)
- in: path
name: server_id
type: 'integer'
required: true
description: The ID or IP of the server
- in: path
name: action
type: 'string'
required: true
description: The action to be performed on the service (start, stop, reload, restart)
responses:
200:
description: Successful operation
default:
description: Unexpected error
"""
if service not in ('haproxy', 'nginx', 'apache', 'keepalived', 'waf_haproxy', 'waf_nginx'):
return roxywi_common.handler_exceptions_for_json_data(RoxywiResourceNotFound(), 'Cannot find a server')
try:
server_id = SupportClass().return_server_ip_or_id(server_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot find a server')
try:
server = server_sql.get_server_with_group(server_id, g.user_params['group_id'])
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot find a server')
try:
service_action.common_action(server.ip, action, service)
return BaseResponse().model_dump(mode='json')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, f'Cannot do {action}')
class ServiceBackendView(MethodView):
methods = ['GET', 'POST']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=4), check_group()]
@staticmethod
def get(service: str, server_id: Union[int, str]):
"""
This endpoint retrieves information about service backends.
---
tags:
- Service
parameters:
- in: path
name: service
type: 'integer'
required: true
description: The type of service (haproxy, nginx, apache, keepalived)
- in: path
name: server_id
type: 'integer'
required: true
description: The ID or IP of the server
responses:
200:
description: Successful operation
schema:
type: 'array'
items:
ppoperties:
type: 'string'
description: 'List of backends (only for HAProxy and Keepalived services)'
default:
description: Unexpected error
"""
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, '')
try:
backends = service_common.overview_backends(server_ip, service)
return DataResponse(data=backends).model_dump(mode='json')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Backends not found')
class ServiceConfigView(MethodView):
methods = ['GET', 'POST']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()]
@validate(query=ConfigFileNameQuery)
def get(self, service: str, server_id: Union[int, str], query: ConfigFileNameQuery):
"""
This endpoint retrieves the configuration file for the specified service on a certain server.
---
tags:
- Service config
parameters:
- in: path
name: service
type: 'integer'
required: true
description: The type of service (haproxy, nginx, apache, keepalived)
- in: path
name: server_id
type: 'integer'
required: true
description: The ID or IP of the server
- in: query
name: file_path
type: 'string'
required: false
description: The full path to the configuration file
- in: query
name: version
type: 'string'
required: false
description: The version of the configuration file
responses:
200:
description: 'Successful operation, returns the required configuration file'
default:
description: Unexpected error
"""
if service in ('nginx', 'apache') and (query.file_path is None and query.version is None):
return ErrorResponse(error=f'There is must be "file_path" as query parameter for {service.title()}')
if query.file_path:
query.file_path = query.file_path.replace('/', '92')
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, '')
if query.version:
configs_dir = config_common.get_config_dir(service)
if '..' in configs_dir:
return ErrorResponse(error='nice try').model_dump(mode='json')
cfg = configs_dir + query.version
else:
cfg = config_common.generate_config_path(service, server_ip)
try:
config_mod.get_config(server_ip, cfg, service=service, config_file_name=query.file_path)
except Exception as e:
return ErrorResponse(error=str(e)).model_dump(mode='json')
try:
with open(cfg, 'r') as file:
conf = file.readlines()
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get config')
return DataResponse(data=conf).model_dump(mode='json')
@validate(body=ConfigRequest)
def post(self, service: str, server_id: Union[int, str], body: ConfigRequest):
"""
Update service configuration
---
tags:
- Service config
parameters:
- name: service
in: path
type: string
required: true
description: The service name, one of [haproxy, nginx, apache, keepalived].
- name: server_id
in: path
type: string
required: true
description: Server Identifier - either ID or IP.
- name: body
in: body
schema:
type: object
properties:
action:
type: string
description: The action to be performed.
required: true
config:
type: string
description: The configuration to be saved.
required: true
file_path:
type: string
description: Path to the configuration file. Only for NGINX and Apache services.
config_local_path:
type: string
description: Local path for the configuration, if updated from it. It could be used for uploading a version of config file.
required:
- action
responses:
200:
description: Post request received successful
schema:
type: object
properties:
status:
type: string
data:
type: string
description: Configuration check result
default:
description: Unexpected error
"""
if service in ('nginx', 'apache') and (body.file_path is None):
return ErrorResponse(error=f'There is must be "file_path" as json parameter for {service.title()}')
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, '')
try:
cfg = config_mod.return_cfg(service, server_ip, body.file_path)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get config')
try:
with open(cfg, "a") as conf:
conf.write(body.config)
except IOError as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot write config')
try:
if service == 'keepalived':
stderr = config_mod.upload_and_restart(server_ip, cfg, body.action, service, oldcfg=body.config_local_path)
else:
stderr = config_mod.master_slave_upload_and_restart(server_ip, cfg, body.action, service, oldcfg=body.config_local_path,
config_file_name=body.file_path)
except Exception as e:
return f'error: {e}', 200
if body.action != 'test':
config_mod.diff_config(body.config_local_path, cfg)
return DataStrResponse(data=stderr).model_dump(mode='json'), 201
class ServiceConfigList(MethodView):
methods = ['GET']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()]
def get(self, service: str, server_id: Union[int, str]):
"""
Retrieve the list of configuration files for the given service
---
tags:
- Service configs list
parameters:
- name: service
in: path
type: string
required: true
enum: ['nginx', 'apache']
description: Type of service (nginx or apache)
- name: server_id
in: path
type: string
required: true
description: Server ID or IP address
responses:
200:
description: List of configuration files
schema:
type: object
properties:
data:
type: array
items:
type: string
example: [
"/etc/nginx/conf.d/default.conf",
"/etc/nginx/waf/modsecurity.conf",
"/etc/nginx/waf/rulescrs-setup.conf",
"/etc/nginx/waf/waf.conf",
"/etc/nginx/nginx.conf"
]
"""
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, 'Cannot find the server')
files = []
service_config_dir = sql.get_setting(f'{service}_dir')
try:
return_files = server_mod.get_remote_files(server_ip, service_config_dir, 'conf')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get configs')
return_files = return_files.split('\t\t')
for file in return_files:
if '\r\n' in file:
for f in file.split('\r\n'):
if f == '':
continue
elif '\t' in f:
for f1 in f.split('\t'):
files.append(f1)
else:
files.append(f)
elif file == '':
continue
else:
files.append(file)
files.append(sql.get_setting(f'{service}_config_path'))
return DataResponse(data=files).model_dump(mode='json')
class ServiceConfigVersionsView(MethodView):
methods = ['GET', 'POST', 'DELETE']
decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=4), check_group()]
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.
---
tags:
- Service config
parameters:
- in: path
name: service
type: 'integer'
required: true
description: The type of service (haproxy, nginx, apache, keepalived)
- in: path
name: server_id
type: 'integer'
required: true
description: The ID or IP of the server
responses:
200:
description: 'Successful operation, returns list of configuration file versions'
default:
description: Unexpected error
"""
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, '')
config_dir = config_common.get_config_dir(service)
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