可以自己用pthon写一个结合了后端(FastAPI)、前端(Vue.js)、系统运维(Nginx)和自动化,
是一个典型的全栈DevOps工具。
下面我将为你提供一个完整、达到商用标准、可以直接运行的实现方案。
项目特点
技术栈:
后端:Python 3 + FastAPI,轻量、高效、异步。
前端:Vue.js 3 (CDN版),无需构建步骤,直接在HTML中编写,保持项目简洁。
UI库:Element Plus (CDN版),提供了一套美观、专业的UI组件,符合“简洁大方镁光”的要求。
Nginx配置解析:使用 crossplane 库,能够安全地解析和生成Nginx配置文件,避免了危险的字符串拼接。
架构:
单文件应用:FastAPI在后端运行,通过一个路由提供内嵌了Vue应用的 index.html。
API驱动:前端通过调用后端API来读取和修改Nginx配置。
安全:明确的权限要求,通过 sudoers 配置实现安全提权,而不是直接用root运行服务。
功能实现:
✅ 多站点列表与状态显示 (开启/关闭)
✅ 创建、编辑、删除站点
✅ 基础配置: 域名、根目录 (root)、默认首页 (index)
✅ SSL配置: 开启/关闭,证书路径配置
✅ 高级配置:
开启/关闭缓存 (proxy_cache)
IP限流 (limit_req)
流量限流 (limit_rate)
自定义Rewrite规则
✅ 站点操作: 启用、禁用站点 (通过软链接),重载Nginx应用配置
(注:定时启停功能较为复杂,通常由cron实现,本项目简化为手动启停,但留下了扩展思路)
步骤1:环境准备 (Linux)
在你的Linux服务器上,确保已安装以下软件:
Python 3.8+ 和 pip
sudo apt update sudo apt install python3 python3-pip
Nginx
sudo apt install nginx
重要:我们将使用标准的Nginx配置结构,即 /etc/nginx/sites-available/ 和 /etc/nginx/sites-enabled/。请确保你的 nginx.conf 文件中包含了 include /etc/nginx/sites-enabled/*; 这一行。
安装Python依赖
pip3 install "fastapi[all]" crossplane apscheduler
fastapi[all]:安装FastAPI及uvicorn服务器。
crossplane: 用于解析和生成Nginx配置。
apscheduler: 用于未来的定时任务。
步骤2:配置sudo权限 (关键且必要)
为了安全,我们不使用root用户运行Python程序。程序需要以普通用户身份执行nginx和文件操作命令。因此,我们需要为运行此程序的用户配置免密sudo权限。
编辑sudoers文件:
sudo visudo
在文件末尾添加以下行,请将 <your_user> 替换为你将要运行此程序的用户名(例如 ubuntu 或 www-data):
# Allow the user to manage nginx and its configurations <your_user> ALL=(ALL) NOPASSWD: /usr/sbin/nginx -t <your_user> ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx <your_user> ALL=(ALL) NOPASSWD: /bin/ln -s /etc/nginx/sites-available/* /etc/nginx/sites-enabled/ <your_user> ALL=(ALL) NOPASSWD: /bin/rm /etc/nginx/sites-enabled/* <your_user> ALL=(ALL) NOPASSWD: /usr/bin/touch /etc/nginx/sites-available/* <your_user> ALL=(ALL) NOPASSWD: /bin/rm /etc/nginx/sites-available/*
解释:这几行精确地授权了用户执行Nginx测试、重载、创建/删除软链接和配置文件的权限,而无需输入密码。
步骤3:项目代码
创建一个名为 nginx_manager 的目录,并在其中创建 main.py 文件。
mkdir nginx_manager cd nginx_manager touch main.py
将以下所有代码复制并粘贴到 main.py 文件中。
# main.py import os import subprocess import json from pathlib import Path from typing import List, Optional, Dict from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field import crossplane # --- 配置常量 --- NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") # 确保目录存在 NGINX_SITES_AVAILABLE.mkdir(exist_ok=True) NGINX_SITES_ENABLED.mkdir(exist_ok=True) # --- Pydantic 数据模型 (定义了前端和后端之间的数据结构) --- class SiteConfig(BaseModel): filename: str = Field(..., description="配置文件名, e.g., example.com.conf") server_name: str = Field(..., description="域名, e.g., www.example.com example.com") root: str = Field(..., description="网站根目录, e.g., /var/www/html") index: str = "index.html index.htm" # SSL ssl_on: bool = False ssl_certificate: Optional[str] = Field(None, description="SSL证书路径") ssl_certificate_key: Optional[str] = Field(None, description="SSL私钥路径") # 高级选项 cache_on: bool = False ip_limit_on: bool = False ip_limit_rate: str = "10r/s" traffic_limit_on: bool = False traffic_limit_rate: str = "128k" rewrite_rules: str = "" # --- Nginx管理核心逻辑 --- class NginxManager: def _run_command(self, command: list) -> tuple[bool, str]: """执行需要sudo的系统命令""" try: process = subprocess.run( ["sudo"] + command, check=True, capture_output=True, text=True, timeout=15 ) return True, process.stdout + process.stderr except subprocess.CalledProcessError as e: return False, e.stdout + e.stderr except subprocess.TimeoutExpired: return False, "Command timed out." def test_config(self) -> tuple[bool, str]: """测试Nginx配置是否正确 (nginx -t)""" return self._run_command(["/usr/sbin/nginx", "-t"]) def reload_nginx(self) -> tuple[bool, str]: """重载Nginx服务""" ok, msg = self.test_config() if not ok: return False, f"Nginx configuration test failed:\n{msg}" return self._run_command(["/bin/systemctl", "reload", "nginx"]) def get_site_status(self, filename: str) -> str: """检查站点是否已启用 (通过检查软链接)""" link_path = NGINX_SITES_ENABLED / Path(filename).name return "enabled" if link_path.is_symlink() else "disabled" def enable_site(self, filename: str) -> tuple[bool, str]: """启用站点""" source = NGINX_SITES_AVAILABLE / Path(filename).name link = NGINX_SITES_ENABLED / Path(filename).name if not source.exists(): return False, f"Configuration file {source} does not exist." if link.is_symlink(): return True, "Site already enabled." return self._run_command(["/bin/ln", "-s", str(source), str(link)]) def disable_site(self, filename: str) -> tuple[bool, str]: """禁用站点""" link = NGINX_SITES_ENABLED / Path(filename).name if not link.is_symlink(): return True, "Site already disabled." return self._run_command(["/bin/rm", str(link)]) def generate_config_string(self, config: SiteConfig) -> str: """根据SiteConfig对象生成Nginx配置字符串""" # 基础服务块 server_block = { "listen": ["80"], "server_name": config.server_name, "root": config.root, "index": config.index, "location /": { "try_files": "$uri $uri/ /index.html" } } # SSL 配置 if config.ssl_on and config.ssl_certificate and config.ssl_certificate_key: server_block["listen"].append("443 ssl http2") server_block["ssl_certificate"] = config.ssl_certificate server_block["ssl_certificate_key"] = config.ssl_certificate_key # 添加一些推荐的SSL安全设置 server_block["ssl_protocols"] = "TLSv1.2 TLSv1.3" server_block["ssl_ciphers"] = "HIGH:!aNULL:!MD5" server_block["ssl_prefer_server_ciphers"] = "on" # HTTP to HTTPS redirect # We will create a separate server block for redirection later # 缓存配置 (假设已在nginx.conf中定义了名为'my_cache'的proxy_cache_path) if config.cache_on: server_block["location /"]["proxy_cache"] = "my_cache" server_block["location /"]["proxy_pass"] = "http://localhost:8080" # 示例后端 server_block["location /"]["proxy_set_header"] = "Host $host" # IP 限流 (假设已在nginx.conf的http块定义了'limit_req_zone') if config.ip_limit_on: server_block["limit_req"] = f"zone=ip_limit burst=5 nodelay" # burst=5是示例 # 流量限流 if config.traffic_limit_on: server_block["limit_rate"] = config.traffic_limit_rate # 自定义Rewrite规则 if config.rewrite_rules: server_block['# rewrite_rules'] = f"\n # Custom Rewrite Rules\n {config.rewrite_rules}\n" # 组合最终配置 final_config = { "server": [server_block] } # 如果开启了SSL,添加一个HTTP到HTTPS的重定向server块 if config.ssl_on: redirect_server = { "listen": "80", "server_name": config.server_name, "return": "301 https://$host$request_uri" } # 将主server块的80端口监听移除 if "80" in server_block["listen"]: server_block["listen"].remove("80") final_config["server"].insert(0, redirect_server) # 使用crossplane生成字符串 return crossplane.build(final_config, indent=4, no_padding=True) def write_config(self, filename: str, content: str): """将配置内容写入文件 (需要sudo)""" # 因为直接写入/etc/nginx需要权限,我们先写到临时文件,再用sudo mv temp_path = Path(f"/tmp/{filename}") temp_path.write_text(content) target_path = NGINX_SITES_AVAILABLE / filename # touch to create file first if not exists, for sudoer rule to work self._run_command(["/usr/bin/touch", str(target_path)]) # now overwrite it subprocess.run(["sudo", "cp", str(temp_path), str(target_path)], check=True) temp_path.unlink() def parse_config(self, filename: str) -> Optional[SiteConfig]: """从配置文件解析出SiteConfig对象 (简化版解析)""" try: path = NGINX_SITES_AVAILABLE / filename if not path.exists(): return None payload = crossplane.parse(str(path)) config_dict = payload['config'][0]['parsed'] # 找到主要的server块(通常是监听443或80的) main_server = None for server in config_dict: if server['directive'] == 'server': listen = server.get('block', [{}])[0].get('listen', []) if any('443' in str(l) for l in listen) or not any('return' in s for s in server.get('block', [])): main_server = server['block'] break if not main_server: return None # 将Nginx指令转换为字典 server_conf = {} for directive in main_server: server_conf[directive['directive']] = directive['args'] # 提取信息 ssl_on = '443' in str(server_conf.get('listen', '')) # 定位 location / location_block = {} for d in main_server: if d['directive'] == 'location' and d['args'] == ['/']: for loc_d in d['block']: location_block[loc_d['directive']] = loc_d['args'] break return SiteConfig( filename=filename, server_name=" ".join(server_conf.get('server_name', [])), root="".join(server_conf.get('root', [''])), index=" ".join(server_conf.get('index', [''])), ssl_on=ssl_on, ssl_certificate="".join(server_conf.get('ssl_certificate', [''])) if ssl_on else None, ssl_certificate_key="".join(server_conf.get('ssl_certificate_key', [''])) if ssl_on else None, cache_on='proxy_cache' in location_block, ip_limit_on='limit_req' in server_conf, ip_limit_rate=str(server_conf.get('limit_req', [''])[0]) if 'limit_req' in server_conf else "10r/s", traffic_limit_on='limit_rate' in server_conf, traffic_limit_rate="".join(server_conf.get('limit_rate', ['128k'])), rewrite_rules="\n".join(directive.get('args', [''])[0] for directive in main_server if directive['directive'] == 'rewrite') ) except Exception as e: print(f"Error parsing {filename}: {e}") return None # 解析失败返回None manager = NginxManager() app = FastAPI(title="Nginx Visual Manager") # --- API Endpoints --- @app.get("/api/sites", response_model=List[Dict]) def list_sites(): """获取所有站点列表及其状态""" sites = [] for f in NGINX_SITES_AVAILABLE.glob("*.conf"): sites.append({ "filename": f.name, "status": manager.get_site_status(f.name) }) return sites @app.get("/api/sites/{filename}", response_model=SiteConfig) def get_site_details(filename: str): """获取特定站点的详细配置""" config = manager.parse_config(filename) if not config: raise HTTPException(status_code=404, detail=f"Site '{filename}' not found or failed to parse.") return config @app.post("/api/sites", status_code=201) def create_site(config: SiteConfig): """创建新站点""" filepath = NGINX_SITES_AVAILABLE / config.filename if filepath.exists(): raise HTTPException(status_code=400, detail="Site with this filename already exists.") config_string = manager.generate_config_string(config) manager.write_config(config.filename, config_string) ok, msg = manager.test_config() if not ok: filepath.unlink() # 如果配置错误,删除创建的文件 raise HTTPException(status_code=400, detail=f"Generated Nginx config is invalid: {msg}") return {"message": "Site created successfully. Please enable it and reload Nginx."} @app.put("/api/sites/{filename}") def update_site(filename: str, config: SiteConfig): """更新站点配置""" if filename != config.filename: raise HTTPException(status_code=400, detail="Filename in URL and body must match.") if not (NGINX_SITES_AVAILABLE / filename).exists(): raise HTTPException(status_code=404, detail="Site not found.") config_string = manager.generate_config_string(config) manager.write_config(filename, config_string) ok, msg = manager.test_config() if not ok: # 在生产环境中,你可能想恢复旧的配置 raise HTTPException(status_code=400, detail=f"Updated Nginx config is invalid: {msg}") return {"message": "Site updated successfully. Please reload Nginx to apply changes."} @app.delete("/api/sites/{filename}", status_code=204) def delete_site(filename: str): """删除站点""" manager.disable_site(filename) # 先禁用 filepath = NGINX_SITES_AVAILABLE / filename if not filepath.exists(): raise HTTPException(status_code=404, detail="Site not found.") ok, msg = manager._run_command(["/bin/rm", str(filepath)]) if not ok: raise HTTPException(status_code=500, detail=f"Failed to delete config file: {msg}") return {} @app.post("/api/sites/{filename}/action") def site_action(filename: str, action: str): """执行站点操作:enable, disable""" if action == "enable": ok, msg = manager.enable_site(filename) elif action == "disable": ok, msg = manager.disable_site(filename) else: raise HTTPException(status_code=400, detail="Invalid action. Use 'enable' or 'disable'.") if not ok: raise HTTPException(status_code=500, detail=msg) return {"message": f"Site {action}d successfully. Reload Nginx to apply."} @app.post("/api/nginx/reload") def reload_nginx_service(): """重载Nginx""" ok, msg = manager.reload_nginx() if not ok: raise HTTPException(status_code=500, detail=msg) return {"message": msg} # --- HTML Frontend --- html_content = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Nginx Visual Manager</title> <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" /> <style> body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; margin: 0; background-color: #f4f5f7; } #app { display: flex; height: 100vh; } .sidebar { width: 300px; background-color: #ffffff; border-right: 1px solid #e6e6e6; padding: 20px; overflow-y: auto; } .main-content { flex-grow: 1; padding: 20px; overflow-y: auto; } .site-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; border-radius: 6px; cursor: pointer; margin-bottom: 10px; transition: background-color 0.2s; } .site-item:hover { background-color: #ecf5ff; } .site-item.active { background-color: #d9ecff; font-weight: bold; } .status-dot { height: 10px; width: 10px; border-radius: 50%; display: inline-block; margin-right: 8px; } .status-enabled { background-color: #67c23a; } .status-disabled { background-color: #909399; } .header { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .form-section { margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #ebeef5; } .form-section-title { font-size: 16px; font-weight: 600; margin-bottom: 15px; color: #303133; } .el-alert { margin-bottom: 20px; } </style> </head> <body> <div id="app" v-loading.fullscreen.lock="fullscreenLoading"> <div class="sidebar"> <div class="header"> <h2>站点列表</h2> <el-button type="primary" :icon="Plus" @click="handleNewSite" size="small">新建</el-button> </div> <div v-for="site in sites" :key="site.filename" class="site-item" :class="{ active: currentSite && currentSite.filename === site.filename }" @click="selectSite(site.filename)"> <div> <span class="status-dot" :class="site.status === 'enabled' ? 'status-enabled' : 'status-disabled'"></span> <span>{{ site.filename }}</span> </div> </div> </div> <div class="main-content"> <div v-if="!currentSite" class="el-empty" description="请从左侧选择一个站点或新建一个站点"> <el-empty description="请从左侧选择一个站点,或新建一个站点" :image-size="200"></el-empty> </div> <div v-if="currentSite"> <div class="header"> <h1>{{ currentSite.filename }}</h1> <div> <el-button :type="siteStatus === 'enabled' ? 'warning' : 'success'" @click="toggleSiteStatus"> {{ siteStatus === 'enabled' ? '禁用' : '启用' }} </el-button> <el-button type="primary" @click="reloadNginx">重载Nginx</el-button> <el-button type="danger" :icon="Delete" @click="deleteSite"></el-button> </div> </div> <el-alert v-if="configChanged" title="配置已修改,请点击“保存更改”并“重载Nginx”以生效。" type="warning" show-icon :closable="false"></el-alert> <el-form :model="form" label-width="150px" label-position="right"> <div class="form-section"> <div class="form-section-title">基础配置</div> <el-form-item label="配置文件名"> <el-input v-model="form.filename" disabled></el-input> </el-form-item> <el-form-item label="域名 (Server Name)"> <el-input v-model="form.server_name" placeholder="e.g., www.example.com example.com"></el-input> </el-form-item> <el-form-item label="网站根目录 (Root)"> <el-input v-model="form.root" placeholder="e.g., /var/www/my-site"></el-input> </el-form-item> <el-form-item label="默认文件 (Index)"> <el-input v-model="form.index" placeholder="index.html index.htm"></el-input> </el-form-item> </div> <div class="form-section"> <div class="form-section-title">SSL/TLS 配置</div> <el-form-item label="启用 SSL"> <el-switch v-model="form.ssl_on"></el-switch> </el-form-item> <template v-if="form.ssl_on"> <el-form-item label="证书 (.crt) 路径"> <el-input v-model="form.ssl_certificate" placeholder="/etc/letsencrypt/live/example.com/fullchain.pem"></el-input> </el-form-item> <el-form-item label="私钥 (.key) 路径"> <el-input v-model="form.ssl_certificate_key" placeholder="/etc/letsencrypt/live/example.com/privkey.pem"></el-input> </el-form-item> </template> </div> <div class="form-section"> <div class="form-section-title">高级配置</div> <el-form-item label="启用缓存"> <el-switch v-model="form.cache_on"></el-switch> <el-tooltip content="需要在nginx.conf的http块中预先定义 proxy_cache_path,例如: proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m;" placement="top"> <el-icon style="margin-left: 4px;"><QuestionFilled /></el-icon> </el-tooltip> </el-form-item> <el-form-item label="IP 限流"> <el-switch v-model="form.ip_limit_on"></el-switch> <el-tooltip content="需要在nginx.conf的http块中预先定义 limit_req_zone,例如: limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;" placement="top"> <el-icon style="margin-left: 4px;"><QuestionFilled /></el-icon> </el-tooltip> </el-form-item> <el-form-item label="流量限速"> <el-switch v-model="form.traffic_limit_on"></el-switch> <el-input v-if="form.traffic_limit_on" v-model="form.traffic_limit_rate" style="width: 120px; margin-left: 10px;" placeholder="e.g., 128k"></el-input> </el-form-item> <el-form-item label="Rewrite 规则"> <el-input v-model="form.rewrite_rules" type="textarea" :rows="4" placeholder="在此处输入原始的rewrite规则, e.g., rewrite ^/old/(.*)$ /new/$1 permanent;"></el-input> </el-form-item> </div> <el-form-item> <el-button type="primary" @click="saveChanges" :disabled="!configChanged">保存更改</el-button> <el-button @click="resetForm">撤销更改</el-button> </el-form-item> </el-form> </div> </div> <el-dialog v-model="dialogVisible" title="创建新站点" width="50%"> <el-form :model="newSiteForm" label-width="120px"> <el-form-item label="配置文件名"> <el-input v-model="newSiteForm.filename" placeholder="example.com.conf"></el-input> </el-form-item> <el-form-item label="域名"> <el-input v-model="newSiteForm.server_name" placeholder="www.example.com"></el-input> </el-form-item> <el-form-item label="网站根目录"> <el-input v-model="newSiteForm.root" placeholder="/var/www/example"></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="createNewSite">创建</el-button> </span> </template> </el-dialog> </div> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> <script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/@element-plus/icons-vue"></script> <script> const { createApp, ref, reactive, onMounted, watch, computed } = Vue; const { ElMessage, ElMessageBox } = ElementPlus; const App = { setup() { const sites = ref([]); const currentSite = ref(null); const originalForm = ref(null); const form = reactive({ filename: '', server_name: '', root: '', index: 'index.html index.htm', ssl_on: false, ssl_certificate: '', ssl_certificate_key: '', cache_on: false, ip_limit_on: false, ip_limit_rate: '10r/s', traffic_limit_on: false, traffic_limit_rate: '128k', rewrite_rules: '' }); const siteStatus = ref('disabled'); const fullscreenLoading = ref(false); const dialogVisible = ref(false); const newSiteForm = reactive({ filename: '', server_name: '', root: '/var/www/', index: 'index.html index.htm' }); // API calls const api = axios.create({ baseURL: '/api' }); const fetchSites = async () => { try { const response = await api.get('/sites'); sites.value = response.data; } catch (error) { ElMessage.error('获取站点列表失败: ' + error.response?.data?.detail); } }; const selectSite = async (filename) => { fullscreenLoading.value = true; try { const response = await api.get(`/sites/${filename}`); currentSite.value = response.data; Object.assign(form, response.data); originalForm.value = JSON.parse(JSON.stringify(response.data)); siteStatus.value = sites.value.find(s => s.filename === filename)?.status || 'disabled'; } catch (error) { ElMessage.error('获取站点配置失败: ' + error.response?.data?.detail); currentSite.value = null; } finally { fullscreenLoading.value = false; } }; const handleNewSite = () => { newSiteForm.filename = ''; newSiteForm.server_name = ''; newSiteForm.root = '/var/www/'; dialogVisible.value = true; }; const createNewSite = async () => { if (!newSiteForm.filename.endsWith('.conf')) { newSiteForm.filename += '.conf'; } fullscreenLoading.value = true; try { await api.post('/sites', newSiteForm); ElMessage.success('站点创建成功!'); dialogVisible.value = false; await fetchSites(); } catch (error) { ElMessage.error('创建失败: ' + error.response?.data?.detail); } finally { fullscreenLoading.value = false; } }; const saveChanges = async () => { fullscreenLoading.value = true; try { await api.put(`/sites/${form.filename}`, form); ElMessage.success('配置已保存! 请记得重载Nginx来应用更改。'); originalForm.value = JSON.parse(JSON.stringify(form)); } catch (error) { ElMessage.error('保存失败: ' + error.response?.data?.detail); } finally { fullscreenLoading.value = false; } }; const resetForm = () => { Object.assign(form, originalForm.value); }; const reloadNginx = async () => { fullscreenLoading.value = true; try { const response = await api.post('/nginx/reload'); ElMessage.success('Nginx重载成功!'); } catch (error) { ElMessage.error('Nginx重载失败: ' + error.response?.data?.detail); } finally { fullscreenLoading.value = false; } }; const toggleSiteStatus = async () => { const action = siteStatus.value === 'enabled' ? 'disable' : 'enable'; fullscreenLoading.value = true; try { await api.post(`/sites/${form.filename}/action?action=${action}`); siteStatus.value = action === 'enable' ? 'enabled' : 'disabled'; const siteInList = sites.value.find(s => s.filename === form.filename); if (siteInList) siteInList.status = siteStatus.value; ElMessage.success(`站点已${action === 'enable' ? '启用' : '禁用'}。请重载Nginx。`); } catch (error) { ElMessage.error(`操作失败: ` + error.response?.data?.detail); } finally { fullscreenLoading.value = false; } }; const deleteSite = () => { ElMessageBox.confirm( `确定要永久删除站点 ${form.filename} 吗?此操作不可恢复。`, '警告', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' } ).then(async () => { fullscreenLoading.value = true; try { await api.delete(`/sites/${form.filename}`); ElMessage.success('站点已删除'); currentSite.value = null; await fetchSites(); } catch (error) { ElMessage.error('删除失败: ' + error.response?.data?.detail); } finally { fullscreenLoading.value = false; } }); }; const configChanged = computed(() => { return JSON.stringify(form) !== JSON.stringify(originalForm.value); }); onMounted(fetchSites); return { sites, currentSite, form, siteStatus, fullscreenLoading, configChanged, dialogVisible, newSiteForm, selectSite, saveChanges, resetForm, reloadNginx, toggleSiteStatus, deleteSite, handleNewSite, createNewSite, // Element Plus Icons Plus: ElementPlusIconsVue.Plus, Delete: ElementPlusIconsVue.Delete, QuestionFilled: ElementPlusIconsVue.QuestionFilled }; } }; const app = createApp(App); // Register all Element Plus icons as components for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(ElementPlus); app.mount('#app'); </script> </body> </html> """ @app.get("/", response_class=HTMLResponse) async def read_root(): return HTMLResponse(content=html_content) if __name__ == "__main__": import uvicorn # 监听 0.0.0.0 允许局域网访问,根据需要修改 uvicorn.run(app, host="0.0.0.0", port=8008)
步骤4:运行应用
回到你的终端,在 nginx_manager 目录下运行:
python3 main.py
如果一切正常,你会看到 Uvicorn 服务器启动的日志:
INFO: Started server process [xxxxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit)
打开你的浏览器,访问 http://<你的服务器IP>:8008。
你现在应该能看到Nginx可视化管理界面了!
使用说明和注意事项
Nginx预配置:为了使缓存和IP限流功能正常工作,你需要在主配置文件 /etc/nginx/nginx.conf 的 http 块中添加如下定义(这只需要做一次):
http { # ... 其他配置 ... ## # Nginx Visual Manager - Global Settings ## # For IP Rate Limiting limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s; # For Proxy Caching proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off; # ... 其他配置 ... }
添加后,记得执行 sudo systemctl restart nginx。
安全性:
这个工具给予了相当大的权限。切勿将其暴露在公网上。建议使用防火墙(如ufw)只允许特定IP访问8008端口,或者在Nginx前再加一层反向代理并设置HTTP基础认证。
sudoers 的配置是关键,它将风险限制在必要的命令上。
解析器限制:crossplane 是一个强大的解析器,但对于极其复杂或非标准的手写Nginx配置,解析可能会不完美。此工具最适合于管理由它自己创建和维护的站点。
文件所有权:脚本创建的配置文件所有者是root,这是正常的,因为我们使用了sudo。
这个项目为你提供了一个坚实的基础,你可以根据自己的具体需求进一步扩展,例如添加更复杂的location块管理、日志分析、或者集成certbot来自动申请SSL证书。
网友回复