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 from playhouse.shortcuts import model_to_dict import app.modules.db.sql as sql import app.modules.db.add as add_sql import app.modules.db.server as server_sql import app.modules.server.ssh as mod_ssh import app.modules.config.config as config_mod import app.modules.config.common as config_common import app.modules.service.installation as service_mod import app.modules.roxywi.common as roxywi_common from app.middleware import get_user_params, page_for_admin, check_group, check_services from app.modules.db.db_model import Server from app.modules.roxywi.class_models import BaseResponse, DataStrResponse, NginxUpstreamRequest, IdDataStrResponse, \ ErrorResponse, GenerateConfigRequest, NginxProxyPassRequest from app.modules.common.common_classes import SupportClass class NginxSectionView(MethodView): methods = ['GET', 'POST', 'PUT', 'DELETE'] decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()] @staticmethod def get(service: Literal['nginx'], section_type: str, section_name: str, server_id: Union[int, str]): 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_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: section = add_sql.get_section(server_id, section_type, section_name, service) output = {'server_id': section.server_id.server_id, **model_to_dict(section, recurse=False), 'id': f'{server_id}-{section_name}'} output.update(section.config) return jsonify(output) except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get NGINX section') def post(self, service: Literal['nginx'], section_type: str, server_id: Union[int, str], body: Union[NginxUpstreamRequest, NginxProxyPassRequest], query: GenerateConfigRequest ): 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 query.generate: cfg = '/tmp/nginx-generated-config.conf' os.system(f'touch {cfg}') inv = service_mod.generate_section_inv(body.model_dump(mode='json'), cfg, service) try: output = service_mod.run_ansible_locally(inv, 'nginx_section') if len(output['failures']) > 0 or len(output['dark']) > 0: raise Exception('Cannot create NGINX section. Check Apache error log') except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, f'Cannot create NGINX section: {e}') try: with open(cfg, 'r') as file: conf = file.read() except Exception as e: raise Exception(f'error: Cannot read config file: {e}') try: os.remove(cfg) except Exception: pass return DataStrResponse(data=conf).model_dump(mode='json'), 200 try: output = self._edit_config(service, server, body, 'create') except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create NGINX section') if 'Fatal' in output or 'error' in output: return ErrorResponse(error=output).model_dump(mode='json'), 500 try: add_sql.insert_new_section(server_id, section_type, body.name, body, service) except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot add NGINX section') return IdDataStrResponse(data=output, id=f'{server_id}-{body.name}').model_dump(mode='json'), 201 def put(self, service: Literal['nginx'], section_type: str, section_name: str, server_id: Union[int, str], body: Union[NginxUpstreamRequest, NginxProxyPassRequest] ): 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') try: output = self._edit_config(service, server, body, 'update') except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create HAProxy section') if 'Fatal' in output or 'error' in output: return ErrorResponse(error=output).model_dump(mode='json'), 500 else: try: add_sql.update_section(server_id, section_type, section_name, body, service) except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot update NGINX section') return DataStrResponse(data=output).model_dump(mode='json'), 201 def delete(self, service: Literal['nginx'], section_type: str, section_name: str, server_id: Union[int, str]): 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') try: config_file_name = self._create_config_path(service, section_type, section_name) with mod_ssh.ssh_connect(server.ip) as ssh: ssh.remove_sftp(config_file_name) add_sql.delete_section(server_id, section_type, section_name, service) except Exception as e: return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete NGINX section') return BaseResponse().model_dump(mode='json'), 204 @staticmethod def _create_config_path(service: str, config_type: str, name: str) -> str: service_dir = sql.get_setting(f'{service}_dir') if config_type == 'upstream': config_file_name = f'{service_dir}/conf.d/upstream_{name}.conf' else: config_file_name = f'{service_dir}/sites-enabled/proxy-pass_{name}.conf' return config_file_name def _edit_config(self, service, server: Server, body: NginxUpstreamRequest, action: Literal['create', 'update', 'delete'], **kwargs) -> str: cfg = config_common.generate_config_path(service, server.ip) config_file_name = self._create_config_path(service, body.type, body.name) if action in ('create', 'update'): inv = service_mod.generate_section_inv(body.model_dump(mode='json'), cfg, service) else: inv = service_mod.generate_section_inv_for_del(cfg, kwargs.get('section_type'), kwargs.get('section_name')) if action == 'update': config_mod.get_config(server.ip, cfg, service=service, config_file_name=config_file_name) os.system(f'mv {cfg} {cfg}.old') try: output = service_mod.run_ansible_locally(inv, 'nginx_section') except Exception as e: raise e if len(output['failures']) > 0 or len(output['dark']) > 0: raise Exception('Cannot create NGINX section. Check Apache error log') if body: if body.action: action = str(body.action) else: action = 'save' output = config_mod.master_slave_upload_and_restart(server.ip, cfg, action, service, config_file_name=config_file_name, oldcfg=f'{cfg}.old') return output class UpstreamSectionView(NginxSectionView): methods = ['GET', 'POST', 'PUT', 'DELETE'] decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()] def __init__(self): self.section_type = 'upstream' @validate() def get(self, service: Literal['nginx'], section_name: str, server_id: Union[int, str]): """ NginxUpstreamView API This is the NginxUpstreamView API where you can get configurations of NGINX sections. --- tags: - NGINX upstream section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of the section to fetch. responses: 200: description: NGINX upstream configuration. schema: type: object properties: config: type: object properties: backend_servers: type: array items: type: object properties: server: type: string port: type: integer max_fails: type: integer fail_timeout: type: integer name: type: string balance: type: string keepalive: type: integer id: type: string name: type: string server_id: type: integer type: type: string 400: description: Invalid parameters. 404: description: Section not found. 500: description: Internal server error. """ return super().get(service, self.section_type, section_name, server_id) @validate(body=NginxUpstreamRequest, query=GenerateConfigRequest) def post(self, service: Literal['nginx'], server_id: Union[int, str], body: NginxUpstreamRequest, query: GenerateConfigRequest ): """ NginxUpstreamView API This is the NginxUpstreamView API where you can create NGINX sections. --- tags: - NGINX upstream section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: body in: body required: true schema: type: object properties: backend_servers: type: array items: type: object properties: server: type: string port: type: integer max_fails: type: integer fail_timeout: type: integer name: type: string description: The name of the upstream balance: type: string description: Could be 'round_robin', 'ip_hash', 'least_conn' or 'random' default: round_robin keepalive: type: integer default: 32 responses: 200: description: NGINX section successfully created. 400: description: Invalid parameters. 500: description: Internal server error. """ return super().post(service, self.section_type, server_id, body, query) @validate(body=NginxUpstreamRequest) def put(self, service: Literal['nginx'], server_id: Union[int, str], section_name: str, body: NginxUpstreamRequest, query: GenerateConfigRequest ): """ This is the NginxUpstreamView API where you can update the NGINX sections. --- tags: - NGINX upstream section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of section to update. - name: body in: body required: true schema: type: object properties: backend_servers: type: array items: type: object properties: server: type: string port: type: integer max_fails: type: integer fail_timeout: type: integer name: type: string description: The name of the upstream to update. balance: type: string description: Could be 'round_robin', 'ip_hash', 'least_conn' or 'random' default: round_robin keepalive: type: integer default: 32 responses: 200: description: NGINX section successfully created. 400: description: Invalid parameters. 500: description: Internal server error. """ return super().put(service, self.section_type, section_name, server_id, body) @validate() def delete(self, service: Literal['nginx'], section_name: str, server_id: Union[int, str]): """ NginxUpstreamView sections API This is the NginxUpstreamView API where you can delete configurations of NGINX sections. --- tags: - NGINX upstream section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of the section to fetch. responses: 204: description: NGINX section configuration. """ return super().delete(service, self.section_type, section_name, server_id) class ProxyPassSectionView(NginxSectionView): methods = ['GET', 'POST', 'PUT', 'DELETE'] decorators = [jwt_required(), get_user_params(), check_services, page_for_admin(level=3), check_group()] def __init__(self): self.section_type = 'proxy_pass' @validate() def get(self, service: Literal['nginx'], section_name: str, server_id: Union[int, str]): """ NginxProxyPassView API This is the NginxProxyPassView API where you can get configurations of NGINX sections. --- tags: - NGINX proxy_pass section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of the section to fetch. responses: 200: description: Proxy Pass Section details retrieved successfully. schema: type: object properties: compression: type: boolean description: Indicates whether compression is enabled. compression_level: type: integer description: Specifies the compression level (from 1 to 9). compression_min_length: type: integer description: Minimum response size in bytes for compression to apply. compression_types: type: string description: MIME types (space-separated) that compression applies to. id: type: string description: Unique ID of the proxy pass section. locations: type: array items: type: object properties: location: type: string description: Path of the location block (e.g., `/`). headers: type: array description: List of headers for the location. items: type: object proxy_connect_timeout: type: integer description: Timeout value for connecting to the upstream server (in seconds). proxy_read_timeout: type: integer description: Timeout for reading data from the upstream server (in seconds). proxy_send_timeout: type: integer description: Timeout for sending data to the upstream server (in seconds). upstream: type: string description: Name of the upstream server. name: type: string description: Name of the proxy pass configuration. port: type: integer description: Port on which this proxy pass runs. scheme: type: string enum: - http - https description: Protocol used by the proxy. server_id: type: integer description: ID of the associated server. ssl_crt: type: string description: SSL certificate file name. ssl_key: type: string description: SSL private key file name. ssl_offloading: type: boolean description: Indicates whether SSL offloading is enabled. type: type: string enum: - proxy_pass description: Section type (e.g., "proxy_pass"). 400: description: Invalid parameters. 404: description: Section not found. 500: description: Internal server error. """ return super().get(service, self.section_type, section_name, server_id) @validate(body=NginxProxyPassRequest, query=GenerateConfigRequest) def post(self, service: Literal['nginx'], server_id: Union[int, str], body: NginxProxyPassRequest, query: GenerateConfigRequest ): """ NginxProxyPassView API This is the NginxProxyPassView API where you can create NGINX sections. --- tags: - NGINX proxy_pass section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: body in: body required: true schema: type: object required: - locations - name - scheme - port properties: locations: type: array description: List of locations associated with this proxy pass section. items: type: object required: - location - proxy_connect_timeout - proxy_read_timeout - proxy_send_timeout - upstream properties: location: type: string description: Path of the location block (e.g., `/`). default: / proxy_connect_timeout: type: string description: Timeout value for connecting to the upstream server (in seconds). default: 60 proxy_read_timeout: type: string description: Timeout for reading data from the upstream server (in seconds). default: 60 proxy_send_timeout: type: string description: Timeout for sending data to the upstream server (in seconds). default: 60 headers: type: array description: List of headers for the location (currently empty). items: type: object required: - action - name properties: action: type: string description: Action to perform on the header (e.g., "add_header"). enum: - add_header - proxy_set_header - proxy_hide_header name: type: string description: Name of the header. example: X-Real-IP value: type: string description: Value of the header. upstream: type: string description: Name of the upstream server. name: type: string description: Domain name or IP of the proxy pass section. scheme: type: string enum: - http - https description: Scheme (protocol) for the proxy pass section. port: type: integer description: Port number on which the proxy pass section operates. ssl_offloading: type: boolean description: Indicates whether SSL offloading is enabled. default: false ssl_key: type: string description: SSL private key file name. Need if scheme is https. ssl_crt: type: string description: SSL certificate file name. Need if scheme is https compression_types: type: string description: Space-separated list of MIME types to be compressed. default: text/plain text/css application/json application/javascript text/xml compression_min_length: type: string description: Minimum response size in bytes for compression to apply. default: 1024 compression_level: type: string description: The compression level (e.g., 1 to 9). default: 6 responses: 200: description: NGINX section successfully created. 400: description: Invalid parameters. 500: description: Internal server error. """ return super().post(service, self.section_type, server_id, body, query) @validate(body=NginxProxyPassRequest) def put(self, service: Literal['nginx'], server_id: Union[int, str], section_name: str, body: NginxProxyPassRequest, query: GenerateConfigRequest ): """ This is the NginxProxyPassView API where you can update the NGINX sections. --- tags: - NGINX proxy_pass section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of section to update. - name: body in: body required: true schema: type: object required: - locations - name - scheme - port properties: locations: type: array description: List of locations associated with this proxy pass section. items: type: object required: - location - proxy_connect_timeout - proxy_read_timeout - proxy_send_timeout - upstream properties: location: type: string description: Path of the location block (e.g., `/`). default: / proxy_connect_timeout: type: string description: Timeout value for connecting to the upstream server (in seconds). default: 60 proxy_read_timeout: type: string description: Timeout for reading data from the upstream server (in seconds). default: 60 proxy_send_timeout: type: string description: Timeout for sending data to the upstream server (in seconds). default: 60 headers: type: array description: List of headers for the location (currently empty). items: type: object required: - action - name properties: action: type: string description: Action to perform on the header (e.g., "add_header"). enum: - add_header - proxy_set_header - proxy_hide_header name: type: string description: Name of the header. example: X-Real-IP value: type: string description: Value of the header. upstream: type: string description: Name of the upstream server. name: type: string description: Domain name or IP of the proxy pass section. scheme: type: string enum: - http - https description: Scheme (protocol) for the proxy pass section. port: type: integer description: Port number on which the proxy pass section operates. ssl_offloading: type: boolean description: Indicates whether SSL offloading is enabled. default: false ssl_key: type: string description: SSL private key file name. Need if scheme is https. ssl_crt: type: string description: SSL certificate file name. Need if scheme is https compression_types: type: string description: Space-separated list of MIME types to be compressed. default: text/plain text/css application/json application/javascript text/xml compression_min_length: type: string description: Minimum response size in bytes for compression to apply. default: 1024 compression_level: type: string description: The compression level (e.g., 1 to 9). default: 6 responses: 200: description: NGINX section successfully created. 400: description: Invalid parameters. 500: description: Internal server error. """ return super().put(service, self.section_type, section_name, server_id, body) @validate() def delete(self, service: Literal['nginx'], section_name: str, server_id: Union[int, str]): """ NginxProxyPassView sections API This is the NginxProxyPassView API where you can delete configurations of NGINX sections. --- tags: - NGINX proxy_pass section parameters: - name: service in: path type: string required: true enum: - nginx description: The service to which this section belongs. - name: server_id in: path type: string required: true description: Server ID or IP address - name: section_name in: path type: string required: true description: The name of the section to fetch. responses: 204: description: NGINX section configuration. """ return super().delete(service, self.section_type, section_name, server_id)