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