Compare commits
No commits in common. "main" and "betaV0.1" have entirely different histories.
595
README.md
595
README.md
@ -1,580 +1,51 @@
|
|||||||
# VPS Hub - 多平台VPS监控与管理系统
|
## 核云IDC服务商VPS自动监测重启程序
|
||||||
|
|
||||||
<div align="center">
|
### API接口信息
|
||||||
|
|
||||||

|
- **获取登录Token**
|
||||||
|
`https://www.heyunidc.cn/v1/login_api?account={phone number/email acc}&password={API KEY}`
|
||||||
|
备注:POST方法
|
||||||
|
|
||||||
**一个强大的多平台VPS监控、管理和自动重启系统**
|
- **获取VPS列表**
|
||||||
|
`https://www.heyunidc.cn/v1/hosts?page=1&limit=100`
|
||||||
|
备注:GET方法,使用"Authorization: JWT {Token}"进行认证
|
||||||
|
注意:若您的VPS数量超过100,需要更改limit数量
|
||||||
|
|
||||||
[功能特性](#-功能特性) • [快速开始](#-快速开始) • [架构设计](#-架构设计) • [API文档](#-api文档) • [常见问题](#-常见问题)
|
- **获取VPS状态**
|
||||||
|
`https://www.heyunidc.cn/v1/hosts/{id}/module/status?type=host`
|
||||||
|
备注:GET方法,使用"Authorization: JWT {Token}"进行认证
|
||||||
|
|
||||||
</div>
|
- **操作VPS**
|
||||||
|
`PUT https://www.heyunidc.cn/v1/hosts/:id/module/hard_reboot`
|
||||||
|
备注:PUT方法,使用"Authorization: JWT {Token}"进行认证
|
||||||
|
操作类型:hard_reboot - 硬重启,on - 开机,off - 关机,reboot - 重启
|
||||||
|
|
||||||
---
|
### 使用方法
|
||||||
|
|
||||||
## 📋 目录
|
#### 获取API_KEY
|
||||||
|
如图所示:
|
||||||
- [功能特性](#-功能特性)
|
|
||||||
- [系统架构](#-系统架构)
|
|
||||||
- [技术栈](#-技术栈)
|
|
||||||
- [快速开始](#-快速开始)
|
|
||||||
- [配置说明](#-配置说明)
|
|
||||||
- [使用指南](#-使用指南)
|
|
||||||
- [数据库设计](#-数据库设计)
|
|
||||||
- [API文档](#-api文档)
|
|
||||||
- [监控服务](#-监控服务)
|
|
||||||
- [安全说明](#-安全说明)
|
|
||||||
- [常见问题](#-常见问题)
|
|
||||||
- [许可证](#-许可证)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ 功能特性
|
|
||||||
|
|
||||||
### 🎯 核心功能
|
|
||||||
|
|
||||||
- **多平台支持**:支持魔方平台(智简魔方)、阿里云、腾讯云等主流VPS平台
|
|
||||||
- **实时监控**:每5分钟自动Ping检测所有VPS的可用性
|
|
||||||
- **自动开机**:检测到VPS关机时自动执行开机操作
|
|
||||||
- **状态管理**:实时显示VPS的运行状态(开机/关机/未知)
|
|
||||||
- **详细信息**:展示CPU、内存、磁盘、带宽、操作系统等配置信息
|
|
||||||
|
|
||||||
### 🔧 管理功能
|
|
||||||
|
|
||||||
- **Web管理面板**:美观的响应式Web界面
|
|
||||||
- **手动刷新**:一键刷新VPS列表和详细信息
|
|
||||||
- **远程控制**:支持开机、关机、硬重启操作
|
|
||||||
- **配置管理**:轻松添加和管理多个VPS平台配置
|
|
||||||
- **统计信息**:实时统计VPS总数、运行中数量、已关机数量
|
|
||||||
|
|
||||||
### 📊 监控功能
|
|
||||||
|
|
||||||
- **Ping检测**:定期检测VPS网络可达性
|
|
||||||
- **延迟记录**:记录每次Ping的延迟时间
|
|
||||||
- **状态持久化**:将VPS状态保存到SQLite数据库
|
|
||||||
- **历史数据**:保留30天的Ping历史记录
|
|
||||||
- **每日摘要**:每天0点生成前一天的可用性统计报告
|
|
||||||
|
|
||||||
### 🚀 智能优化
|
|
||||||
|
|
||||||
- **API调用优化**:Ping成功时不调用API,减少不必要的请求
|
|
||||||
- **Token缓存**:JWT Token缓存2小时,避免频繁登录
|
|
||||||
- **动态更新**:只更新实际变化的字段,避免数据覆盖
|
|
||||||
- **防重复机制**:使用唯一约束防止重复数据
|
|
||||||
- **错误重试**:开机失败后自动重试并记录真实状态
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ 系统架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ Web前端 (index.php) │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
|
||||||
│ │ VPS列表 │ │ 操作控制 │ │ 统计信息展示 │ │
|
|
||||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
|
||||||
└──────────────────────┬──────────────────────────────┘
|
|
||||||
│ HTTP POST/GET
|
|
||||||
┌──────────────────────▼──────────────────────────────┐
|
|
||||||
│ PHP后端 (mofangidc.php) │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
|
||||||
│ │ API封装 │ │Token管理 │ │ 数据库操作 │ │
|
|
||||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
|
||||||
└──────────────────────┬──────────────────────────────┘
|
|
||||||
│ SQLite
|
|
||||||
┌──────────────────────▼──────────────────────────────┐
|
|
||||||
│ 数据库层 (SQLite) │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
|
||||||
│ │ vps.db │ │vpslist.db│ │ status.db │ │
|
|
||||||
│ │(配置) │ │ (VPS列表)│ │ (监控记录) │ │
|
|
||||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
|
||||||
└──────────────────────┬──────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌──────────────────────▼──────────────────────────────┐
|
|
||||||
│ Python监控服务 (monitor.py) │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
|
||||||
│ │ Ping检测 │ │ API调用 │ │ 定时任务 │ │
|
|
||||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 架构特点
|
|
||||||
|
|
||||||
1. **前后端分离**:PHP负责Web界面和API调用,Python负责后台监控
|
|
||||||
2. **多数据库设计**:三个独立的SQLite数据库,职责清晰
|
|
||||||
3. **混合架构**:PHP处理用户交互,Python处理定时任务
|
|
||||||
4. **适配器模式**:支持多平台扩展,易于添加新平台
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💻 技术栈
|
|
||||||
|
|
||||||
### 后端
|
|
||||||
- **PHP 7.4+**:Web服务器端逻辑
|
|
||||||
- **Python 3.6+**:后台监控服务
|
|
||||||
- **SQLite 3**:轻量级数据库
|
|
||||||
|
|
||||||
### 前端
|
|
||||||
- **HTML5/CSS3**:页面结构和样式
|
|
||||||
- **JavaScript**:交互逻辑
|
|
||||||
- **响应式设计**:支持移动端访问
|
|
||||||
|
|
||||||
### 依赖库
|
|
||||||
- **PHP**: cURL, PDO-SQLite
|
|
||||||
- **Python**: requests, schedule, sqlite3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
|
|
||||||
- PHP 7.4 或更高版本
|
|
||||||
- Python 3.6 或更高版本
|
|
||||||
- SQLite 3
|
|
||||||
- Linux/Windows/macOS
|
|
||||||
|
|
||||||
### 安装步骤
|
|
||||||
|
|
||||||
#### 1. 克隆项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.masonliu.com/MasonLiu/VPSHUB.git
|
|
||||||
cd VPSHUB
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 配置Web
|
|
||||||
|
|
||||||
直接将VPSHUB文件夹下的所有文件放置于网站路径下即可
|
|
||||||
|
|
||||||
**温馨提示**:
|
|
||||||
|
|
||||||
请将app路径设置为禁止外网访问(或添加Basic认证)。
|
|
||||||
|
|
||||||
#### 3. 一键快速启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod 755 app/install.sh
|
|
||||||
./app/install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 访问Web界面
|
|
||||||
|
|
||||||
浏览器访问:`http://your-domain.com/`
|
|
||||||
|
|
||||||
首次访问会自动跳转到配置页面,添加第一个VPS平台配置。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
### 添加VPS平台配置
|
|
||||||
|
|
||||||
1. 访问 `config_add.php?pass=YOUR_API_PASS`
|
|
||||||
2. 填写配置信息:
|
|
||||||
- **API标识**:自定义名称(唯一)
|
|
||||||
- **网站类型**:选择平台(目前支持魔方平台)
|
|
||||||
- **网站链接**:API根域名(不要包含路径)
|
|
||||||
- **账户**:登录账号
|
|
||||||
- **API密钥**:登录密码或API Key
|
|
||||||
- **自动监控**:是否启用自动监控
|
|
||||||
|
|
||||||
3. 点击"保存配置"
|
|
||||||
|
|
||||||
### 配置示例
|
|
||||||
|
|
||||||
**魔方平台配置**:
|
|
||||||
```
|
|
||||||
API标识: 核云IDC
|
|
||||||
网站类型: 魔方平台
|
|
||||||
网站链接: https://www.heyunidc.cn
|
|
||||||
账户: 邮箱或电话号码
|
|
||||||
API密钥: your_api_key
|
|
||||||
自动监控: ✓
|
|
||||||
```
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
⚠️ **重要提示**:
|
#### Web端
|
||||||
- 网站链接只填写根域名,不要包含 `/v1` 或 `/api` 等路径
|
将web目录部分部署到服务器上,分配域名或直接IP访问即可
|
||||||
- API标识必须唯一,不能重复
|
|
||||||
- 确保账户和密码正确
|
|
||||||
|
|
||||||
---
|
首次使用会进入配置页,依次输入API_PASS,ACCOUNT,API_KEY(其中API_PASS是您自定义的网站访问密码,支持大小写字母以及数字)
|
||||||
|
|
||||||
## 📖 使用指南
|
随后访问`http://example.com/?pass=API_PASS`即可
|
||||||
|
|
||||||
### Web管理面板
|
切记:切勿将app部分放置于网站目录下
|
||||||
|
|
||||||
#### 查看VPS列表
|
#### 监控端
|
||||||
|
|
||||||
访问 `index.php?pass=YOUR_API_PASS`,可以看到:
|
**安装:**
|
||||||
- 按配置分组的VPS列表
|
1. chmod +x ./install.sh
|
||||||
- 每个VPS的状态、IP、配置信息
|
2. ./install.sh
|
||||||
- 实时统计数据
|
|
||||||
|
|
||||||
#### 操作VPS
|
系统将自动注册名为idc-monitor的system服务
|
||||||
|
|
||||||
每个VPS卡片提供三个操作按钮:
|
注意:若您的服务器必须要求禁ping,程序可能无法正常运行,在安装脚本前,请手动将config.yml中的WAY改为http,然后再启动安装脚本
|
||||||
- **⚡ 开机**:启动VPS
|
|
||||||
- **🔴 关机**:关闭VPS
|
|
||||||
- **🔄 硬重启**:强制重启VPS
|
|
||||||
|
|
||||||
操作后会显示成功/失败提示,5秒后自动消失。
|
**卸载:**
|
||||||
|
1. chmod +x ./uninstall.sh
|
||||||
#### 刷新VPS列表
|
2. ./uninstall.sh
|
||||||
|
|
||||||
点击顶部工具栏的 "🔄 手动刷新VPS列表" 按钮:
|
|
||||||
1. 从API获取最新的VPS列表
|
|
||||||
2. 更新数据库中的VPS信息
|
|
||||||
3. 获取每个VPS的详细配置(CPU、内存、磁盘、带宽、系统)
|
|
||||||
|
|
||||||
### 监控服务
|
|
||||||
|
|
||||||
#### 启动监控
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl start idc_monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 查看状态
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status idc_monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#### 停止监控
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl stop idc_monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 重启监控
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart idc_monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 监控流程
|
|
||||||
|
|
||||||
```
|
|
||||||
每5分钟执行一次监控循环
|
|
||||||
↓
|
|
||||||
1. Ping所有VPS
|
|
||||||
├─ Ping成功 → 标记status='on',不调用API
|
|
||||||
└─ Ping失败 → 标记为abnormal,等待下一步
|
|
||||||
↓
|
|
||||||
2. 检查Ping失败的VPS
|
|
||||||
├─ 全部正常 → 跳过
|
|
||||||
└─ 有失败的 → 调用API查询真实状态
|
|
||||||
├─ 状态为'on' → 可能是禁Ping,更新数据库
|
|
||||||
└─ 状态为'off' → 执行开机操作
|
|
||||||
├─ 等待60秒
|
|
||||||
└─ 验证开机结果(最多重试2次)
|
|
||||||
↓
|
|
||||||
3. 清理30天前的旧数据
|
|
||||||
↓
|
|
||||||
等待5分钟后下一次循环
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗄️ 数据库设计
|
|
||||||
|
|
||||||
### 数据库文件
|
|
||||||
|
|
||||||
| 文件名 | 用途 | 主要表 |
|
|
||||||
|--------|------|--------|
|
|
||||||
| `vps.db` | 存储平台配置 | `configs` |
|
|
||||||
| `vpslist.db` | 存储VPS列表 | `vps_list` |
|
|
||||||
| `status.db` | 存储监控记录 | `ping_status`, `vps_summary` |
|
|
||||||
|
|
||||||
### 表结构
|
|
||||||
|
|
||||||
#### configs 表 (vps.db)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE configs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
api_label TEXT NOT NULL UNIQUE, -- API标识
|
|
||||||
site_type TEXT NOT NULL, -- 平台类型
|
|
||||||
site_url TEXT, -- API地址
|
|
||||||
account TEXT NOT NULL, -- 账户
|
|
||||||
api_key TEXT NOT NULL, -- API密钥
|
|
||||||
auto_monitor BOOLEAN DEFAULT 1, -- 是否自动监控
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### vps_list 表 (vpslist.db)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE vps_list (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
config_id INTEGER NOT NULL, -- 配置ID
|
|
||||||
vps_id INTEGER NOT NULL, -- VPS平台ID
|
|
||||||
domain TEXT, -- 域名
|
|
||||||
ip_address TEXT, -- IP地址
|
|
||||||
product_name TEXT, -- 产品名称
|
|
||||||
cpu_cores INTEGER, -- CPU核数
|
|
||||||
memory_size TEXT, -- 内存大小
|
|
||||||
disk_size TEXT, -- 磁盘大小
|
|
||||||
bandwidth TEXT, -- 带宽
|
|
||||||
os_type TEXT, -- 操作系统
|
|
||||||
status TEXT, -- 状态(on/off/unknown)
|
|
||||||
section BOOLEAN DEFAULT 0, -- 是否监控
|
|
||||||
last_check TIMESTAMP, -- 最后检查时间
|
|
||||||
FOREIGN KEY (config_id) REFERENCES configs(id),
|
|
||||||
UNIQUE(config_id, vps_id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ping_status 表 (status.db)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE ping_status (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL, -- VPS ID
|
|
||||||
target TEXT NOT NULL, -- 目标(IP或域名)
|
|
||||||
status TEXT NOT NULL, -- 状态(normal/abnormal)
|
|
||||||
latency_ms REAL, -- 延迟(ms)
|
|
||||||
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### vps_summary 表 (status.db)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE vps_summary (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL, -- VPS ID
|
|
||||||
date DATE NOT NULL, -- 日期
|
|
||||||
avg_latency_ms REAL, -- 平均延迟
|
|
||||||
max_latency_ms REAL, -- 最大延迟
|
|
||||||
min_latency_ms REAL, -- 最小延迟
|
|
||||||
count_under_100 INTEGER, -- <100ms次数
|
|
||||||
count_100_to_300 INTEGER, -- 100-300ms次数
|
|
||||||
count_300_to_500 INTEGER, -- 300-500ms次数
|
|
||||||
count_abnormal INTEGER, -- 异常次数
|
|
||||||
availability TEXT, -- 可用性评分
|
|
||||||
UNIQUE(vps_id, date)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📡 API文档
|
|
||||||
|
|
||||||
### 魔方平台API
|
|
||||||
|
|
||||||
- `POST /login_api?account={acc}&password={api key}` - 登录获取JWT Token
|
|
||||||
|
|
||||||
以下接口通过请求头进行认证
|
|
||||||
Authorization: JWT {token}
|
|
||||||
|
|
||||||
- `GET /hosts?page=&limit=` - 获取VPS列表
|
|
||||||
- `GET /hosts/{id}` - 获取VPS详情
|
|
||||||
- `GET /hosts/{id}/module/status?type=host` - 获取VPS状态
|
|
||||||
- `PUT /hosts/{id}/module/on` - 开机
|
|
||||||
- `PUT /hosts/{id}/module/off` - 关机
|
|
||||||
- `PUT /hosts/{id}/module/hard_reboot` - 硬重启
|
|
||||||
|
|
||||||
## 🔍 监控服务
|
|
||||||
|
|
||||||
### 配置文件
|
|
||||||
|
|
||||||
监控服务配置文件位于:`/etc/systemd/system/idc_monitor.service`
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=VPS Hub Monitor Service
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=www-data
|
|
||||||
WorkingDirectory=/path/to/VPSHUB/app
|
|
||||||
ExecStart=/usr/bin/python3 monitor.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志文件
|
|
||||||
|
|
||||||
- **正常日志**:`app/logs/monitor.log`
|
|
||||||
- **错误日志**:`app/logs/error.log`
|
|
||||||
|
|
||||||
### 定时任务
|
|
||||||
|
|
||||||
- **监控循环**:每5分钟执行一次
|
|
||||||
- **每日摘要**:每天00:00生成前一天的统计报告
|
|
||||||
- **数据清理**:每次循环清理30天前的Ping记录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 安全说明
|
|
||||||
|
|
||||||
### 访问控制
|
|
||||||
|
|
||||||
- **API密码保护**:所有页面都需要通过 `?pass=API_PASS` 验证
|
|
||||||
- **密码文件**:存储在 `app/pass.php`,权限设置为 600
|
|
||||||
- **数据库保护**:Nginx配置禁止直接访问 `.db` 文件,或者限制访问app路径需要basic认证
|
|
||||||
|
|
||||||
### Token安全
|
|
||||||
|
|
||||||
- **Token缓存**:JWT Token缓存2小时,减少登录次数
|
|
||||||
- **自动刷新**:Token失效时自动重新登录
|
|
||||||
- **缓存文件**:`app/token.php`,权限设置为 600
|
|
||||||
|
|
||||||
### 最佳实践
|
|
||||||
|
|
||||||
1. **修改默认密码**:首次使用后立即修改API密码
|
|
||||||
2. **HTTPS**:生产环境务必使用HTTPS
|
|
||||||
3. **防火墙**:限制访问IP范围
|
|
||||||
4. **定期备份**:定期备份数据库文件
|
|
||||||
5. **日志审计**:定期检查日志文件
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ 常见问题
|
|
||||||
|
|
||||||
### Q1: 页面显示"0台VPS"怎么办?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
1. 点击"手动刷新VPS列表"按钮
|
|
||||||
2. 检查配置是否正确(特别是site_url不要包含路径)
|
|
||||||
3. 查看PHP错误日志:`tail -f /var/log/php/error.log`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q2: 监控服务无法启动?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
```bash
|
|
||||||
# 检查服务状态
|
|
||||||
sudo systemctl status idc_monitor
|
|
||||||
|
|
||||||
# 查看详细日志
|
|
||||||
journalctl -u idc_monitor -n 50
|
|
||||||
|
|
||||||
# 常见原因:
|
|
||||||
# 1. Python依赖未安装:pip install -r requirements.txt
|
|
||||||
# 2. 数据库文件权限问题:chown www-data:www-data app/db/*.db
|
|
||||||
# 3. 配置文件路径错误:检查service文件中的WorkingDirectory
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q3: exec函数被禁用怎么办?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
系统已经做了兼容处理:
|
|
||||||
- 如果exec被禁用,会显示友好提示
|
|
||||||
- 需要手动执行安装脚本:`sudo bash app/install.sh`
|
|
||||||
- 监控服务仍然可以正常运行
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q4: VPS状态显示"未知"?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
可能原因:
|
|
||||||
1. 数据库中还没有该VPS的记录
|
|
||||||
2. 多次调用导致状态被覆盖(已修复)
|
|
||||||
3. API返回异常
|
|
||||||
|
|
||||||
解决方法:
|
|
||||||
- 手动刷新VPS列表
|
|
||||||
- 检查日志确认API调用是否正常
|
|
||||||
- 等待下一次监控循环
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q5: 如何添加新的VPS平台?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
1. 在 `app/monitor.py` 中创建新的适配器类
|
|
||||||
2. 继承 `PlatformAdapter` 基类
|
|
||||||
3. 实现必要的方法(login, get_vps_list, get_vps_status等)
|
|
||||||
4. 在 `load_configs_and_create_adapters()` 中添加判断逻辑
|
|
||||||
|
|
||||||
示例:
|
|
||||||
```python
|
|
||||||
class AliyunAdapter(PlatformAdapter):
|
|
||||||
def login(self):
|
|
||||||
# 实现阿里云登录逻辑
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_vps_list(self):
|
|
||||||
# 实现获取VPS列表逻辑
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q6: 数据库文件在哪里?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
- `app/db/vps.db` - 配置数据库
|
|
||||||
- `app/db/vpslist.db` - VPS列表数据库
|
|
||||||
- `app/db/status.db` - 监控状态数据库
|
|
||||||
|
|
||||||
可以使用SQLite工具查看:
|
|
||||||
```bash
|
|
||||||
sqlite3 app/db/vpslist.db
|
|
||||||
.tables
|
|
||||||
SELECT * FROM vps_list;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Q7: 如何备份数据?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
```bash
|
|
||||||
# 备份所有数据库
|
|
||||||
cp app/db/*.db backup/$(date +%Y%m%d)/
|
|
||||||
|
|
||||||
# 或使用sqlite3导出
|
|
||||||
sqlite3 app/db/vpslist.db ".dump" > backup/vpslist.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 许可证
|
|
||||||
|
|
||||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## 📧 联系方式
|
|
||||||
|
|
||||||
- **项目地址**: [https://git.masonliu.com/MasonLiu/VPSHUB](https://git.masonliu.com/MasonLiu/VPSHUB)
|
|
||||||
- **作者**: MasonLiu
|
|
||||||
- **邮箱**: admin@masonliu.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🙏 致谢
|
|
||||||
|
|
||||||
感谢以下开源项目:
|
|
||||||
- [PHP](https://www.php.net/)
|
|
||||||
- [Python](https://www.python.org/)
|
|
||||||
- [SQLite](https://www.sqlite.org/)
|
|
||||||
- [智简魔方财务系统](https://www.idcsmart.com/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
**⭐ 如果这个项目对您有帮助,请给个Star!**
|
|
||||||
|
|
||||||
Made with ❤️ by MasonLiu
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
7
app/config.yml
Normal file
7
app/config.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ACCOUNT: '' # 填写核云IDC账号(手机号或邮箱)
|
||||||
|
API_KEY: '' # 填写核云IDC API密钥
|
||||||
|
WAY: ping # 填写检测方式:ping 或 http,默认为ping
|
||||||
|
DOMAIN: '' # 当WAY为http时,填写要检测的域名(多个域名用英文逗号分隔)
|
||||||
|
SPAN: 300 # 监控间隔时间(秒),默认300秒(5分钟)
|
||||||
|
EXCEPTION_IPS: [] # 例外IP列表,这些IP关机时不会自动开机(例如:["1.2.3.4", "5.6.7.8"])
|
||||||
|
JWT: '' # JWT Token(自动管理,无需手动填写)
|
||||||
@ -1,221 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/logger.php';
|
|
||||||
|
|
||||||
class DBHelper {
|
|
||||||
private $db;
|
|
||||||
private $dbPath;
|
|
||||||
|
|
||||||
public function __construct($dbPath) {
|
|
||||||
$this->dbPath = $dbPath;
|
|
||||||
|
|
||||||
$dir = dirname($dbPath);
|
|
||||||
if (!is_dir($dir)) {
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->db = new PDO("sqlite:" . $dbPath);
|
|
||||||
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
$this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
throw new Exception("数据库连接失败: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行查询并返回所有结果
|
|
||||||
*/
|
|
||||||
public function query($sql, $params = []) {
|
|
||||||
try {
|
|
||||||
$stmt = $this->db->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
return $stmt->fetchAll();
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::query');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行查询并返回单条结果
|
|
||||||
*/
|
|
||||||
public function queryOne($sql, $params = []) {
|
|
||||||
try {
|
|
||||||
$stmt = $this->db->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
return $stmt->fetch();
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::queryOne');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行插入、更新、删除操作
|
|
||||||
*/
|
|
||||||
public function execute($sql, $params = []) {
|
|
||||||
try {
|
|
||||||
$stmt = $this->db->prepare($sql);
|
|
||||||
return $stmt->execute($params);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
Logger::error("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::execute');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入数据并返回最后插入的ID
|
|
||||||
*/
|
|
||||||
public function insert($sql, $params = []) {
|
|
||||||
try {
|
|
||||||
$stmt = $this->db->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
return $this->db->lastInsertId();
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
Logger::error("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::insert');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function beginTransaction() {
|
|
||||||
return $this->db->beginTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function commit() {
|
|
||||||
return $this->db->commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rollBack() {
|
|
||||||
return $this->db->rollBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getConnection() {
|
|
||||||
return $this->db;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 便捷函数:获取vps.db的DBHelper实例
|
|
||||||
function getVpsDB() {
|
|
||||||
static $db = null;
|
|
||||||
if ($db === null) {
|
|
||||||
$dbPath = __DIR__ . '/db/vps.db';
|
|
||||||
$db = new DBHelper($dbPath);
|
|
||||||
initVpsTables($db);
|
|
||||||
}
|
|
||||||
return $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 便捷函数:获取vpslist.db的DBHelper实例
|
|
||||||
function getVpsListDB() {
|
|
||||||
static $db = null;
|
|
||||||
if ($db === null) {
|
|
||||||
$dbPath = __DIR__ . '/db/vpslist.db';
|
|
||||||
$db = new DBHelper($dbPath);
|
|
||||||
initVpsListTables($db);
|
|
||||||
}
|
|
||||||
return $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 便捷函数:获取status.db的DBHelper实例
|
|
||||||
function getStatusDB() {
|
|
||||||
static $db = null;
|
|
||||||
if ($db === null) {
|
|
||||||
$dbPath = __DIR__ . '/db/status.db';
|
|
||||||
$db = new DBHelper($dbPath);
|
|
||||||
initStatusTables($db);
|
|
||||||
}
|
|
||||||
return $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化vps.db表结构
|
|
||||||
*/
|
|
||||||
function initVpsTables($db) {
|
|
||||||
$db->execute("
|
|
||||||
CREATE TABLE IF NOT EXISTS configs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
api_label TEXT NOT NULL UNIQUE,
|
|
||||||
site_type TEXT NOT NULL,
|
|
||||||
site_url TEXT,
|
|
||||||
account TEXT NOT NULL,
|
|
||||||
api_key TEXT NOT NULL,
|
|
||||||
auto_monitor BOOLEAN DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(site_url, account)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化vpslist.db表结构
|
|
||||||
*/
|
|
||||||
function initVpsListTables($db) {
|
|
||||||
$db->execute("
|
|
||||||
CREATE TABLE IF NOT EXISTS vps_list (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
config_id INTEGER NOT NULL,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
domain TEXT,
|
|
||||||
ip_address TEXT,
|
|
||||||
product_name TEXT,
|
|
||||||
cpu_cores INTEGER,
|
|
||||||
memory_size TEXT,
|
|
||||||
disk_size TEXT,
|
|
||||||
bandwidth TEXT,
|
|
||||||
os_type TEXT,
|
|
||||||
status TEXT,
|
|
||||||
amount TEXT,
|
|
||||||
nextduedate INTEGER,
|
|
||||||
section BOOLEAN DEFAULT 0,
|
|
||||||
last_check TIMESTAMP,
|
|
||||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)');
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)');
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_unique ON vps_list(vps_id, ip_address)');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化status.db表结构
|
|
||||||
*/
|
|
||||||
function initStatusTables($db) {
|
|
||||||
// Ping状态记录表
|
|
||||||
$db->execute("
|
|
||||||
CREATE TABLE IF NOT EXISTS ping_status (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
target TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
latency_ms REAL,
|
|
||||||
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// VPS摘要统计表
|
|
||||||
$db->execute("
|
|
||||||
CREATE TABLE IF NOT EXISTS vps_summary (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
avg_latency_ms REAL,
|
|
||||||
max_latency_ms REAL,
|
|
||||||
min_latency_ms REAL,
|
|
||||||
count_under_100 INTEGER DEFAULT 0,
|
|
||||||
count_100_to_300 INTEGER DEFAULT 0,
|
|
||||||
count_300_to_500 INTEGER DEFAULT 0,
|
|
||||||
count_abnormal INTEGER DEFAULT 0,
|
|
||||||
availability TEXT,
|
|
||||||
UNIQUE(vps_id, date)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// 创建索引
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)');
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)');
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)');
|
|
||||||
$db->execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)');
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@ -1,593 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""VPS Hub 数据库管理模块"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
|
||||||
"""数据库管理器"""
|
|
||||||
|
|
||||||
def __init__(self, db_dir=None):
|
|
||||||
if db_dir is None:
|
|
||||||
db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db')
|
|
||||||
|
|
||||||
self.db_dir = db_dir
|
|
||||||
os.makedirs(db_dir, exist_ok=True)
|
|
||||||
|
|
||||||
self.vps_db = os.path.join(db_dir, 'vps.db')
|
|
||||||
self.vpslist_db = os.path.join(db_dir, 'vpslist.db')
|
|
||||||
self.status_db = os.path.join(db_dir, 'status.db')
|
|
||||||
|
|
||||||
self.init_vps_db()
|
|
||||||
self.init_vpslist_db()
|
|
||||||
self.init_status_db()
|
|
||||||
|
|
||||||
def get_connection(self, db_path):
|
|
||||||
"""获取数据库连接"""
|
|
||||||
conn = sqlite3.connect(db_path)
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
return conn
|
|
||||||
|
|
||||||
def init_vps_db(self):
|
|
||||||
"""初始化vps.db - VPS配置表"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS configs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
api_label TEXT NOT NULL UNIQUE,
|
|
||||||
site_type TEXT NOT NULL,
|
|
||||||
site_url TEXT,
|
|
||||||
account TEXT NOT NULL,
|
|
||||||
api_key TEXT NOT NULL,
|
|
||||||
auto_monitor BOOLEAN DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(site_url, account)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def init_vpslist_db(self):
|
|
||||||
"""初始化vpslist.db - VPS列表缓存表"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS vps_list (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
config_id INTEGER NOT NULL,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
domain TEXT,
|
|
||||||
ip_address TEXT,
|
|
||||||
product_name TEXT,
|
|
||||||
cpu_cores INTEGER,
|
|
||||||
memory_size TEXT,
|
|
||||||
disk_size TEXT,
|
|
||||||
bandwidth TEXT,
|
|
||||||
os_type TEXT,
|
|
||||||
status TEXT,
|
|
||||||
amount TEXT,
|
|
||||||
nextduedate INTEGER,
|
|
||||||
section BOOLEAN DEFAULT 0,
|
|
||||||
last_check TIMESTAMP,
|
|
||||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_unique ON vps_list(vps_id, ip_address)')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def init_status_db(self):
|
|
||||||
"""初始化status.db - Ping状态记录和摘要统计表"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS ping_status (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
target TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
latency_ms REAL,
|
|
||||||
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS vps_summary (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
vps_id INTEGER NOT NULL,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
avg_latency_ms REAL,
|
|
||||||
max_latency_ms REAL,
|
|
||||||
min_latency_ms REAL,
|
|
||||||
count_under_100 INTEGER DEFAULT 0,
|
|
||||||
count_100_to_300 INTEGER DEFAULT 0,
|
|
||||||
count_300_to_500 INTEGER DEFAULT 0,
|
|
||||||
count_abnormal INTEGER DEFAULT 0,
|
|
||||||
availability TEXT,
|
|
||||||
UNIQUE(vps_id, date)
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# ==================== vps.db 操作 ====================
|
|
||||||
|
|
||||||
def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True):
|
|
||||||
"""添加VPS配置,基于site_url和account去重"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
existing = cursor.execute(
|
|
||||||
'SELECT id FROM configs WHERE site_url = ? AND account = ?',
|
|
||||||
(site_url, account)
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
return existing['id']
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
''', (api_label, site_type, site_url, account, api_key, auto_monitor))
|
|
||||||
|
|
||||||
config_id = cursor.lastrowid
|
|
||||||
conn.commit()
|
|
||||||
return config_id
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_all_configs(self):
|
|
||||||
"""获取所有配置"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM configs ORDER BY id')
|
|
||||||
configs = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return configs
|
|
||||||
|
|
||||||
def get_config_by_id(self, config_id):
|
|
||||||
"""根据ID获取配置"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM configs WHERE id = ?', (config_id,))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return dict(row) if row else None
|
|
||||||
|
|
||||||
def update_config(self, config_id, **kwargs):
|
|
||||||
"""更新配置"""
|
|
||||||
if not kwargs:
|
|
||||||
return False
|
|
||||||
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
|
|
||||||
values = list(kwargs.values())
|
|
||||||
values.append(config_id)
|
|
||||||
|
|
||||||
cursor.execute(f'UPDATE configs SET {fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?', values)
|
|
||||||
|
|
||||||
affected = cursor.rowcount
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return affected > 0
|
|
||||||
|
|
||||||
def delete_config(self, config_id):
|
|
||||||
"""删除配置"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('DELETE FROM configs WHERE id = ?', (config_id,))
|
|
||||||
|
|
||||||
affected = cursor.rowcount
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return affected > 0
|
|
||||||
|
|
||||||
def reset_config_ids(self):
|
|
||||||
"""重置configs表的ID序列,从1开始连续编号"""
|
|
||||||
conn = self.get_connection(self.vps_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor.execute('SELECT * FROM configs ORDER BY id')
|
|
||||||
configs = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
if not configs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS configs_backup')
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE configs_backup AS SELECT * FROM configs
|
|
||||||
''')
|
|
||||||
|
|
||||||
cursor.execute('DELETE FROM configs')
|
|
||||||
cursor.execute("DELETE FROM sqlite_sequence WHERE name='configs'")
|
|
||||||
|
|
||||||
for config in configs:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
''', (
|
|
||||||
config['api_label'],
|
|
||||||
config['site_type'],
|
|
||||||
config['site_url'],
|
|
||||||
config['account'],
|
|
||||||
config['api_key'],
|
|
||||||
config['auto_monitor'],
|
|
||||||
config['created_at'],
|
|
||||||
config['updated_at']
|
|
||||||
))
|
|
||||||
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS configs_backup')
|
|
||||||
conn.commit()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# ==================== vpslist.db 操作 ====================
|
|
||||||
|
|
||||||
def get_next_available_id(self):
|
|
||||||
"""获取下一个可用的ID(填补空缺)"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT id FROM vps_list ORDER BY id')
|
|
||||||
ids = [row['id'] for row in cursor.fetchall()]
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if not ids:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
for i in range(1, max(ids) + 1):
|
|
||||||
if i not in ids:
|
|
||||||
return i
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def add_vps(self, config_id, vps_id, domain=None, ip_address=None, product_name=None, section=False, custom_id=None):
|
|
||||||
"""添加VPS到列表,支持指定ID以填补空缺"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
if custom_id is None:
|
|
||||||
custom_id = self.get_next_available_id()
|
|
||||||
|
|
||||||
if custom_id:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO vps_list (id, config_id, vps_id, domain, ip_address, product_name, section, last_check)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
''', (custom_id, config_id, vps_id, domain, ip_address, product_name, section))
|
|
||||||
else:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, section, last_check)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
''', (config_id, vps_id, domain, ip_address, product_name, section))
|
|
||||||
|
|
||||||
record_id = cursor.lastrowid
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return record_id
|
|
||||||
|
|
||||||
def batch_add_vps(self, vps_list):
|
|
||||||
"""批量添加VPS,基于vps_id和ip_address去重"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
for vps in vps_list:
|
|
||||||
existing = cursor.execute(
|
|
||||||
'SELECT id FROM vps_list WHERE vps_id = ? AND ip_address = ?',
|
|
||||||
(vps['vps_id'], vps.get('ip_address'))
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
continue
|
|
||||||
|
|
||||||
custom_id = self.get_next_available_id()
|
|
||||||
|
|
||||||
if custom_id:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO vps_list
|
|
||||||
(id, config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, section, last_check)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
''', (
|
|
||||||
custom_id,
|
|
||||||
vps['config_id'],
|
|
||||||
vps['vps_id'],
|
|
||||||
vps.get('domain'),
|
|
||||||
vps.get('ip_address'),
|
|
||||||
vps.get('product_name'),
|
|
||||||
vps.get('cpu_cores'),
|
|
||||||
vps.get('memory_size'),
|
|
||||||
vps.get('disk_size'),
|
|
||||||
vps.get('bandwidth'),
|
|
||||||
vps.get('os_type'),
|
|
||||||
vps.get('amount'),
|
|
||||||
vps.get('nextduedate'),
|
|
||||||
vps.get('section', False)
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO vps_list
|
|
||||||
(config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, section, last_check)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
''', (
|
|
||||||
vps['config_id'],
|
|
||||||
vps['vps_id'],
|
|
||||||
vps.get('domain'),
|
|
||||||
vps.get('ip_address'),
|
|
||||||
vps.get('product_name'),
|
|
||||||
vps.get('cpu_cores'),
|
|
||||||
vps.get('memory_size'),
|
|
||||||
vps.get('disk_size'),
|
|
||||||
vps.get('bandwidth'),
|
|
||||||
vps.get('os_type'),
|
|
||||||
vps.get('amount'),
|
|
||||||
vps.get('nextduedate'),
|
|
||||||
vps.get('section', False)
|
|
||||||
))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_all_vps(self):
|
|
||||||
"""获取所有VPS"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM vps_list ORDER BY config_id, vps_id')
|
|
||||||
vps_list = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return vps_list
|
|
||||||
|
|
||||||
def get_vps_by_config(self, config_id):
|
|
||||||
"""根据配置ID获取VPS列表"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM vps_list WHERE config_id = ? ORDER BY vps_id', (config_id,))
|
|
||||||
vps_list = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return vps_list
|
|
||||||
|
|
||||||
def update_vps_details(self, vps_id, **kwargs):
|
|
||||||
"""更新VPS详细信息"""
|
|
||||||
if not kwargs:
|
|
||||||
return False
|
|
||||||
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
|
|
||||||
values = list(kwargs.values())
|
|
||||||
values.append(vps_id)
|
|
||||||
|
|
||||||
cursor.execute(f'UPDATE vps_list SET {fields}, last_check = CURRENT_TIMESTAMP WHERE vps_id = ?', values)
|
|
||||||
|
|
||||||
affected = cursor.rowcount
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return affected > 0
|
|
||||||
|
|
||||||
def update_vps_status(self, vps_id, status):
|
|
||||||
"""更新VPS状态"""
|
|
||||||
return self.update_vps_details(vps_id, status=status)
|
|
||||||
|
|
||||||
def get_monitored_vps(self):
|
|
||||||
"""获取所有标记为需要监控的VPS"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM vps_list WHERE section = 1 ORDER BY config_id, vps_id')
|
|
||||||
vps_list = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return vps_list
|
|
||||||
|
|
||||||
def reset_vps_list_ids(self):
|
|
||||||
"""重置vps_list表的ID序列,从1开始连续编号"""
|
|
||||||
conn = self.get_connection(self.vpslist_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor.execute('SELECT * FROM vps_list ORDER BY id')
|
|
||||||
vps_items = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
if not vps_items:
|
|
||||||
return True
|
|
||||||
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS vps_list_backup')
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE vps_list_backup AS SELECT * FROM vps_list
|
|
||||||
''')
|
|
||||||
|
|
||||||
cursor.execute('DELETE FROM vps_list')
|
|
||||||
cursor.execute("DELETE FROM sqlite_sequence WHERE name='vps_list'")
|
|
||||||
|
|
||||||
for vps in vps_items:
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, status, section, last_check)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
''', (
|
|
||||||
vps['config_id'],
|
|
||||||
vps['vps_id'],
|
|
||||||
vps.get('domain'),
|
|
||||||
vps.get('ip_address'),
|
|
||||||
vps.get('product_name'),
|
|
||||||
vps.get('cpu_cores'),
|
|
||||||
vps.get('memory_size'),
|
|
||||||
vps.get('disk_size'),
|
|
||||||
vps.get('bandwidth'),
|
|
||||||
vps.get('os_type'),
|
|
||||||
vps.get('amount'),
|
|
||||||
vps.get('nextduedate'),
|
|
||||||
vps.get('status'),
|
|
||||||
vps.get('section', 0),
|
|
||||||
vps.get('last_check')
|
|
||||||
))
|
|
||||||
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS vps_list_backup')
|
|
||||||
conn.commit()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# ==================== status.db 操作 ====================
|
|
||||||
|
|
||||||
def save_ping_status(self, vps_id, target, status, latency_ms=None):
|
|
||||||
"""保存Ping状态记录"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT INTO ping_status (vps_id, target, status, latency_ms)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
''', (vps_id, target, status, latency_ms))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_ping_records(self, vps_id, date=None):
|
|
||||||
"""获取Ping记录"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
if date:
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT * FROM ping_status
|
|
||||||
WHERE vps_id = ? AND DATE(check_time) = ?
|
|
||||||
ORDER BY check_time
|
|
||||||
''', (vps_id, date))
|
|
||||||
else:
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT * FROM ping_status
|
|
||||||
WHERE vps_id = ?
|
|
||||||
ORDER BY check_time DESC
|
|
||||||
''', (vps_id,))
|
|
||||||
|
|
||||||
records = [dict(row) for row in cursor.fetchall()]
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return records
|
|
||||||
|
|
||||||
def cleanup_old_ping_records(self, days=30):
|
|
||||||
"""清理旧的Ping记录"""
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
cursor.execute('DELETE FROM ping_status WHERE check_time < ?', (cutoff_date,))
|
|
||||||
|
|
||||||
deleted = cursor.rowcount
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return deleted
|
|
||||||
|
|
||||||
def save_vps_summary(self, vps_id, date, avg_latency, max_latency, min_latency,
|
|
||||||
count_under_100, count_100_to_300, count_300_to_500,
|
|
||||||
count_abnormal, availability):
|
|
||||||
"""保存VPS摘要统计"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute('''
|
|
||||||
INSERT OR REPLACE INTO vps_summary
|
|
||||||
(vps_id, date, avg_latency_ms, max_latency_ms, min_latency_ms,
|
|
||||||
count_under_100, count_100_to_300, count_300_to_500, count_abnormal, availability)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
''', (vps_id, date, avg_latency, max_latency, min_latency,
|
|
||||||
count_under_100, count_100_to_300, count_300_to_500,
|
|
||||||
count_abnormal, availability))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_vps_summary(self, vps_id, date=None):
|
|
||||||
"""获取VPS摘要统计"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
if date:
|
|
||||||
cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? AND date = ?', (vps_id, date))
|
|
||||||
else:
|
|
||||||
cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? ORDER BY date DESC LIMIT 1', (vps_id,))
|
|
||||||
|
|
||||||
row = cursor.fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return dict(row) if row else None
|
|
||||||
|
|
||||||
def get_all_summaries(self, date=None):
|
|
||||||
"""获取所有VPS的摘要统计"""
|
|
||||||
conn = self.get_connection(self.status_db)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
if date:
|
|
||||||
cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,))
|
|
||||||
else:
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT vs.* FROM vps_summary vs
|
|
||||||
INNER JOIN (
|
|
||||||
SELECT vps_id, MAX(date) as max_date
|
|
||||||
FROM vps_summary
|
|
||||||
GROUP BY vps_id
|
|
||||||
) latest ON vs.vps_id = latest.vps_id AND vs.date = latest.max_date
|
|
||||||
ORDER BY vs.vps_id
|
|
||||||
''')
|
|
||||||
|
|
||||||
summaries = [dict(row) for row in cursor.fetchall()]
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return summaries
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试代码
|
|
||||||
db = DatabaseManager()
|
|
||||||
print("数据库初始化完成")
|
|
||||||
print(f"vps.db: {db.vps_db}")
|
|
||||||
print(f"vpslist.db: {db.vpslist_db}")
|
|
||||||
print(f"status.db: {db.status_db}")
|
|
||||||
202
app/install.sh
202
app/install.sh
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# VPS Hub 监控程序 - 安装脚本
|
# 核云IDC服务商VPS自动监测重启程序 - 安装脚本
|
||||||
# 该脚本会创建systemd服务,持续化运行monitor.py
|
# 该脚本会检查配置并创建systemd服务,持续化运行monitor.py
|
||||||
|
|
||||||
# 颜色定义
|
# 颜色定义
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@ -13,11 +13,12 @@ NC='\033[0m' # No Color
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# 定义服务名称
|
# 定义服务名称
|
||||||
SERVICE_NAME="idc_monitor"
|
SERVICE_NAME="idc-monitor"
|
||||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
CONFIG_FILE="${SCRIPT_DIR}/config.yml"
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo " VPS Hub 监控程序 - 安装向导"
|
echo " 核云IDC VPS监控程序 - 安装向导"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@ -80,12 +81,189 @@ fi
|
|||||||
|
|
||||||
echo -e "${GREEN}✅${NC} 检测到pip: ${PIP_CMD}"
|
echo -e "${GREEN}✅${NC} 检测到pip: ${PIP_CMD}"
|
||||||
|
|
||||||
|
# 检查配置文件是否存在
|
||||||
|
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||||
|
echo -e "${RED}错误: 配置文件不存在: ${CONFIG_FILE}${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 读取现有配置
|
||||||
|
echo ""
|
||||||
|
echo "正在检查配置文件..."
|
||||||
|
|
||||||
|
# 使用grep解析YAML配置
|
||||||
|
ACCOUNT=$(grep "^ACCOUNT:" "${CONFIG_FILE}" | sed 's/ACCOUNT:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
API_KEY=$(grep "^API_KEY:" "${CONFIG_FILE}" | sed 's/API_KEY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
WAY=$(grep "^WAY:" "${CONFIG_FILE}" | sed 's/WAY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
DOMAIN=$(grep "^DOMAIN:" "${CONFIG_FILE}" | sed 's/DOMAIN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
SPAN=$(grep "^SPAN:" "${CONFIG_FILE}" | sed 's/SPAN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
EXCEPTION_IPS=$(grep "^EXCEPTION_IPS:" "${CONFIG_FILE}" | sed 's/EXCEPTION_IPS:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
|
||||||
|
|
||||||
|
NEED_UPDATE=false
|
||||||
|
|
||||||
|
# 检查ACCOUNT
|
||||||
|
if [ -z "$ACCOUNT" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e -n "${RED}请输入核云IDC账号(手机号或邮箱): ${NC}"
|
||||||
|
read ACCOUNT
|
||||||
|
if [ -z "$ACCOUNT" ]; then
|
||||||
|
echo -e "${RED}错误: 账号不能为空${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEED_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查API_KEY
|
||||||
|
if [ -z "$API_KEY" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e -n "${RED}请输入核云IDC API密钥: ${NC}"
|
||||||
|
read API_KEY
|
||||||
|
if [ -z "$API_KEY" ]; then
|
||||||
|
echo -e "${RED}错误: API密钥不能为空${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEED_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查WAY
|
||||||
|
if [ -z "$WAY" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e "${RED}请选择检测方式:${NC}"
|
||||||
|
echo " 1. ping - Ping检测IP地址(默认)"
|
||||||
|
echo " 2. http - HTTP检测域名"
|
||||||
|
echo -e -n "${RED}请输入选项 1/2,直接回车默认为1: ${NC}"
|
||||||
|
read WAY_CHOICE
|
||||||
|
|
||||||
|
if [ "$WAY_CHOICE" = "2" ]; then
|
||||||
|
WAY="http"
|
||||||
|
else
|
||||||
|
WAY="ping"
|
||||||
|
fi
|
||||||
|
NEED_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果WAY为http,检查DOMAIN
|
||||||
|
if [ "$WAY" = "http" ] && [ -z "$DOMAIN" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e "${RED}请输入要检测的域名(多个域名用英文逗号分隔):${NC}"
|
||||||
|
echo "例如: example.com,test.com,demo.com"
|
||||||
|
echo -e -n "${RED}域名: ${NC}"
|
||||||
|
read DOMAIN
|
||||||
|
if [ -z "$DOMAIN" ]; then
|
||||||
|
echo -e "${RED}错误: 域名不能为空${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEED_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查SPAN
|
||||||
|
if [ -z "$SPAN" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e -n "${RED}请输入监控间隔时间(秒,直接回车默认300秒): ${NC}"
|
||||||
|
read SPAN_INPUT
|
||||||
|
if [ -n "$SPAN_INPUT" ]; then
|
||||||
|
if [[ "$SPAN_INPUT" =~ ^[0-9]+$ ]]; then
|
||||||
|
if [ "$SPAN_INPUT" -lt 60 ]; then
|
||||||
|
echo -e "${RED}警告: 间隔时间过短,建议至少60秒${NC}"
|
||||||
|
fi
|
||||||
|
SPAN=$SPAN_INPUT
|
||||||
|
else
|
||||||
|
echo -e "${RED}警告: 输入无效,使用默认值300秒${NC}"
|
||||||
|
SPAN=300
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SPAN=300
|
||||||
|
fi
|
||||||
|
NEED_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 询问是否设置例外IP
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo -e "${RED}是否设置例外IP列表?(这些IP关机时不会自动开机)${NC}"
|
||||||
|
echo -e -n "${RED}请输入例外IP,多个IP用英文逗号分隔,直接回车跳过: ${NC}"
|
||||||
|
read EXCEPTION_INPUT
|
||||||
|
|
||||||
|
if [ -n "$EXCEPTION_INPUT" ]; then
|
||||||
|
EXCEPTION_IPS="$EXCEPTION_INPUT"
|
||||||
|
NEED_UPDATE=true
|
||||||
|
else
|
||||||
|
if [ -z "$EXCEPTION_IPS" ]; then
|
||||||
|
EXCEPTION_IPS=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
if [ "$NEED_UPDATE" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
# 使用Python更新YAML文件
|
||||||
|
${PYTHON_CMD} << PYEOF
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
config_file = "${CONFIG_FILE}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
except:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
config['ACCOUNT'] = "${ACCOUNT}"
|
||||||
|
config['API_KEY'] = "${API_KEY}"
|
||||||
|
config['WAY'] = "${WAY}"
|
||||||
|
if "${WAY}" == "http":
|
||||||
|
config['DOMAIN'] = "${DOMAIN}"
|
||||||
|
config['SPAN'] = ${SPAN}
|
||||||
|
|
||||||
|
# 处理例外IP
|
||||||
|
exception_ips_str = "${EXCEPTION_IPS}"
|
||||||
|
if exception_ips_str:
|
||||||
|
config['EXCEPTION_IPS'] = [ip.strip() for ip in exception_ips_str.split(',') if ip.strip()]
|
||||||
|
else:
|
||||||
|
config['EXCEPTION_IPS'] = []
|
||||||
|
|
||||||
|
# 清除JWT字段(重新生成)
|
||||||
|
if 'JWT' in config:
|
||||||
|
del config['JWT']
|
||||||
|
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
|
yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
|
||||||
|
|
||||||
|
print("\033[32m✅ 配置已保存\033[0m")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}错误: 配置保存失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅${NC} 配置检查完成,无需更新"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "当前配置:"
|
||||||
|
echo " 账号: ${ACCOUNT}"
|
||||||
|
echo " 检测方式: ${WAY}"
|
||||||
|
if [ "$WAY" = "http" ]; then
|
||||||
|
echo " 域名: ${DOMAIN}"
|
||||||
|
fi
|
||||||
|
echo " 监控间隔: ${SPAN}秒"
|
||||||
|
if [ -n "$EXCEPTION_IPS" ]; then
|
||||||
|
echo " 例外IP: ${EXCEPTION_IPS}"
|
||||||
|
else
|
||||||
|
echo " 例外IP: 无"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo "注意: VPS配置请通过网页 config_add.php 进行管理"
|
|
||||||
|
|
||||||
# 安装Python依赖
|
# 安装Python依赖
|
||||||
if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then
|
if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then
|
||||||
echo ""
|
|
||||||
echo "正在安装Python依赖包..."
|
echo "正在安装Python依赖包..."
|
||||||
cd ${SCRIPT_DIR}
|
cd ${SCRIPT_DIR}
|
||||||
if ${PIP_CMD} install -r requirements.txt; then
|
if ${PIP_CMD} install -r requirements.txt; then
|
||||||
@ -105,11 +283,10 @@ if [ ! -f "${SCRIPT_DIR}/monitor.py" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 创建日志目录和数据库目录
|
# 创建日志目录
|
||||||
LOG_DIR="${SCRIPT_DIR}/logs"
|
LOG_DIR="${SCRIPT_DIR}/logs"
|
||||||
DB_DIR="${SCRIPT_DIR}/db"
|
mkdir -p ${LOG_DIR}
|
||||||
mkdir -p ${LOG_DIR} ${DB_DIR}
|
chmod 755 ${LOG_DIR}
|
||||||
chmod 755 ${LOG_DIR} ${DB_DIR}
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "正在创建systemd服务..."
|
echo "正在创建systemd服务..."
|
||||||
@ -117,7 +294,7 @@ echo "正在创建systemd服务..."
|
|||||||
# 创建systemd服务文件
|
# 创建systemd服务文件
|
||||||
cat > ${SERVICE_FILE} << EOF
|
cat > ${SERVICE_FILE} << EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=VPS Hub Monitor Service
|
Description=Heyun IDC VPS Monitor Service
|
||||||
After=network.target
|
After=network.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
@ -182,7 +359,6 @@ echo -e " ${YELLOW}停止服务:${NC} systemctl stop ${SERVICE_NAME}"
|
|||||||
echo -e " ${YELLOW}重启服务:${NC} systemctl restart ${SERVICE_NAME}"
|
echo -e " ${YELLOW}重启服务:${NC} systemctl restart ${SERVICE_NAME}"
|
||||||
echo -e " ${YELLOW}卸载服务:${NC} cd ${SCRIPT_DIR} && ./uninstall.sh"
|
echo -e " ${YELLOW}卸载服务:${NC} cd ${SCRIPT_DIR} && ./uninstall.sh"
|
||||||
echo ""
|
echo ""
|
||||||
echo "配置文件位置: 通过网页 config_add.php 管理"
|
echo "配置文件位置: ${CONFIG_FILE}"
|
||||||
echo "日志文件位置: ${LOG_DIR}"
|
echo "日志文件位置: ${LOG_DIR}"
|
||||||
echo "数据库文件位置: ${DB_DIR}"
|
|
||||||
echo ""
|
echo ""
|
||||||
102
app/logger.php
102
app/logger.php
@ -1,102 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* PHP日志管理类
|
|
||||||
* 统一处理所有PHP文件的日志记录
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Logger {
|
|
||||||
private static $logFile = __DIR__ . '/logs/php.log';
|
|
||||||
private static $initialized = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化日志系统
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
if (self::$initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保日志目录存在
|
|
||||||
$logDir = dirname(self::$logFile);
|
|
||||||
if (!is_dir($logDir)) {
|
|
||||||
mkdir($logDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建日志文件(如果不存在)
|
|
||||||
if (!file_exists(self::$logFile)) {
|
|
||||||
touch(self::$logFile);
|
|
||||||
chmod(self::$logFile, 0666);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置PHP错误日志路径
|
|
||||||
ini_set('error_log', self::$logFile);
|
|
||||||
ini_set('log_errors', 1);
|
|
||||||
ini_set('display_errors', 0); // 生产环境不显示错误
|
|
||||||
|
|
||||||
self::$initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录日志
|
|
||||||
* @param string $message 日志消息
|
|
||||||
* @param string $level 日志级别 (INFO, WARNING, ERROR, DEBUG)
|
|
||||||
* @param string $source 来源文件/函数
|
|
||||||
*/
|
|
||||||
public static function log($message, $level = 'INFO', $source = '') {
|
|
||||||
self::init();
|
|
||||||
|
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
|
||||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
|
||||||
$caller = $source ?: (isset($backtrace[1]) ? basename($backtrace[1]['file']) . ':' . $backtrace[1]['line'] : 'unknown');
|
|
||||||
|
|
||||||
$logEntry = "[{$timestamp}] [{$level}] [{$caller}] {$message}" . PHP_EOL;
|
|
||||||
|
|
||||||
file_put_contents(self::$logFile, $logEntry, FILE_APPEND | LOCK_EX);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录信息日志
|
|
||||||
*/
|
|
||||||
public static function info($message, $source = '') {
|
|
||||||
self::log($message, 'INFO', $source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录警告日志
|
|
||||||
*/
|
|
||||||
public static function warning($message, $source = '') {
|
|
||||||
self::log($message, 'WARNING', $source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录错误日志
|
|
||||||
*/
|
|
||||||
public static function error($message, $source = '') {
|
|
||||||
self::log($message, 'ERROR', $source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录调试日志
|
|
||||||
*/
|
|
||||||
public static function debug($message, $source = '') {
|
|
||||||
self::log($message, 'DEBUG', $source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志文件路径
|
|
||||||
*/
|
|
||||||
public static function getLogFile() {
|
|
||||||
return self::$logFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空日志文件
|
|
||||||
*/
|
|
||||||
public static function clear() {
|
|
||||||
file_put_contents(self::$logFile, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动初始化
|
|
||||||
Logger::init();
|
|
||||||
?>
|
|
||||||
1015
app/monitor.py
1015
app/monitor.py
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,2 @@
|
|||||||
requests
|
requests
|
||||||
pyyaml
|
pyyaml
|
||||||
schedule
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# VPS Hub 监控程序 - 卸载脚本
|
# 核云IDC服务商VPS自动监测重启程序 - 卸载脚本
|
||||||
# 该脚本会移除systemd服务
|
# 该脚本会移除systemd服务
|
||||||
|
|
||||||
# 颜色定义
|
# 颜色定义
|
||||||
@ -13,7 +13,7 @@ NC='\033[0m' # No Color
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# 定义服务名称
|
# 定义服务名称
|
||||||
SERVICE_NAME="idc_monitor"
|
SERVICE_NAME="idc-monitor"
|
||||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|||||||
39
app/开发文档.md
Normal file
39
app/开发文档.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
### 程序运行
|
||||||
|
|
||||||
|
1. 用户需先向程序config内添加以下几项定义量:
|
||||||
|
ACCOUNT,API_KEY,WAY
|
||||||
|
|
||||||
|
2. 用户运行install.sh脚本
|
||||||
|
脚本会先检测config.yml中的配置文件
|
||||||
|
缺少ACCOUNT,API_KEY,WAY向用户询问填写什么,同时WAY默认为ping(问句以红色展示)
|
||||||
|
若用户填写http,则让用户填写域名(可以访问的路径),通过英文逗号分隔域名
|
||||||
|
随后用户可添加例外参数,脚本检测到例外内的IP主机关机时,直接跳过,不进行开机
|
||||||
|
|
||||||
|
3. 脚本运行
|
||||||
|
创建名为idc_monitor的systemd服务,持续化运行当前路径下的main.py
|
||||||
|
|
||||||
|
### 程序逻辑
|
||||||
|
1. 1. 隔一段时间(SPAN)便根据WAY来探测目标存活
|
||||||
|
若为ping,则直接ping一遍所有的IP地址
|
||||||
|
若为域名,则使用HEAD方法探测相应域名是否正常响应
|
||||||
|
|
||||||
|
1. 2. 若存在不通的情况,进行如下操作
|
||||||
|
使用config.yml中的JWT请求VPS列表并挨个查询VPS状态是否为on(开机)
|
||||||
|
若返回响应码为405,即响应报文为
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"status": 405,
|
||||||
|
"msg": "请登陆后再试"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
时,重新请求JWT并存储,随后再次请求VPS列表并查询状态
|
||||||
|
|
||||||
|
1. 3. 若均为开机状态,记录日志:什么时间点 - 发生了什么情况,实际没有机器关机,可能是禁ping或者服务器网站状态异常
|
||||||
|
|
||||||
|
1. 4. 若发现有机器关机,则使用开机接口(on)进行开机
|
||||||
|
|
||||||
|
1. 5. 所有机器都操作完成后隔60秒,再次查询刚才尝试开机操作的几台机器是否开机成功,若不超过则再来一遍
|
||||||
|
|
||||||
|
1. 6. 尝试开机操作两次后还是非开机状态,则中断本次循环,进入SPAN间隔准备下一次循环
|
||||||
|
|
||||||
|
1. 7. 以上所有操作均需记录为日志,分为正常日志和异常日志(例如:未发现存在机器关机,开机失败等)
|
||||||
424
config_add.php
424
config_add.php
@ -1,424 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* VPS配置管理页面
|
|
||||||
* 用于添加和管理多平台VPS配置
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/app/db_helper.php';
|
|
||||||
require_once __DIR__ . '/mofangidc.php';
|
|
||||||
|
|
||||||
// Token文件路径(存储API_PASS)
|
|
||||||
$tokenFile = __DIR__ . '/app/pass.php'; # 更新路径为app/pass.php,避免与其他配置文件混淆
|
|
||||||
|
|
||||||
// 检查是否首次访问(需要设置API_PASS)
|
|
||||||
$needSetup = !file_exists($tokenFile);
|
|
||||||
|
|
||||||
// 处理API_PASS设置
|
|
||||||
if ($needSetup && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_pass'])) {
|
|
||||||
$apiPass = trim($_POST['api_pass']);
|
|
||||||
|
|
||||||
if (empty($apiPass)) {
|
|
||||||
$setupError = '密码不能为空!';
|
|
||||||
} elseif (strlen($apiPass) < 8) {
|
|
||||||
$setupError = '密码至少8位!';
|
|
||||||
} elseif (!preg_match('/^[a-zA-Z0-9]+$/', $apiPass)) {
|
|
||||||
$setupError = '密码只能包含字母和数字!';
|
|
||||||
} else {
|
|
||||||
// 保存API_PASS
|
|
||||||
$content = "<?php\n";
|
|
||||||
$content .= "// API访问密码 - 自动生成\n";
|
|
||||||
$content .= "// 警告: 不要手动修改此文件\n\n";
|
|
||||||
$content .= "define('API_PASS', '" . addslashes($apiPass) . "');\n";
|
|
||||||
$content .= "?>\n";
|
|
||||||
|
|
||||||
if (file_put_contents($tokenFile, $content)) {
|
|
||||||
chmod($tokenFile, 0600);
|
|
||||||
header('Location: config_add.php?pass=' . urlencode($apiPass));
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$setupError = '保存失败,请检查目录权限!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果需要设置密码,显示设置页面
|
|
||||||
if ($needSetup) {
|
|
||||||
?>n
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
|
||||||
<link href="./static/initial.css" rel="stylesheet" type="text/css">
|
|
||||||
<title>初始设置 - VPS Hub</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="config-container">
|
|
||||||
<div class="config-header">
|
|
||||||
<h1>🔐 初始设置</h1>
|
|
||||||
<p>请设置管理面板的访问密码</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (isset($setupError)): ?>
|
|
||||||
<div class="error-message">
|
|
||||||
❌ <?php echo htmlspecialchars($setupError); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
💡 提示:此密码用于保护配置管理页面的访问权限。<br>
|
|
||||||
密码要求:至少8位,只能包含字母(a-z,A-Z)和数字(0-9)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" onsubmit="return validatePassword()">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="api_pass">设置访问密码</label>
|
|
||||||
<input type="password" id="api_pass" name="api_pass" required
|
|
||||||
placeholder="至少8位字母和数字"
|
|
||||||
pattern="[a-zA-Z0-9]{8,}"
|
|
||||||
title="至少8位,只能包含字母和数字">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="setup_pass" class="btn-submit">
|
|
||||||
✅ 保存并继续
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function validatePassword() {
|
|
||||||
var password = document.getElementById('api_pass').value;
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
alert('密码至少需要8位!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[a-zA-Z0-9]+$/.test(password)) {
|
|
||||||
alert('密码只能包含字母和数字!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<?php
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载API_PASS
|
|
||||||
if (file_exists($tokenFile)) {
|
|
||||||
include $tokenFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证pass参数
|
|
||||||
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
|
|
||||||
|
|
||||||
if (!defined('API_PASS') || $pass !== API_PASS) {
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode(['status' => 403, 'msg' => '访问密码错误'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理添加配置
|
|
||||||
$message = '';
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
if (isset($_POST['add_config'])) {
|
|
||||||
$apiLabel = trim($_POST['api_label']);
|
|
||||||
$siteType = trim($_POST['site_type']);
|
|
||||||
$siteUrl = trim($_POST['site_url']);
|
|
||||||
$account = trim($_POST['account']);
|
|
||||||
$apiKey = trim($_POST['api_key']);
|
|
||||||
$autoMonitor = isset($_POST['auto_monitor']) ? 1 : 0;
|
|
||||||
|
|
||||||
if (empty($apiLabel) || empty($siteType) || empty($siteUrl) || empty($account) || empty($apiKey)) {
|
|
||||||
$message = '<div class="error-message">❌ API标识、网站类型、网站链接、账户和密钥不能为空!</div>';
|
|
||||||
} else {
|
|
||||||
// 验证URL格式:不允许包含路径
|
|
||||||
$parsedUrl = parse_url($siteUrl);
|
|
||||||
if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) {
|
|
||||||
$message = '<div class="error-message">❌ 网站链接格式错误!请输入完整的URL(例如: https://www.example.com)</div>';
|
|
||||||
} elseif (isset($parsedUrl['path']) && $parsedUrl['path'] !== '/') {
|
|
||||||
$message = '<div class="error-message">❌ 网站链接严禁包含路径部分!只允许输入根域名(例如: https://www.example.com),不要添加 /v1、/api 等路径</div>';
|
|
||||||
} else {
|
|
||||||
$db = getVpsDB();
|
|
||||||
|
|
||||||
// 检查API标识是否已存在
|
|
||||||
$existingConfig = $db->queryOne('SELECT id FROM configs WHERE api_label = ?', [$apiLabel]);
|
|
||||||
if ($existingConfig) {
|
|
||||||
$message = '<div class="error-message">❌ API标识 "' . htmlspecialchars($apiLabel) . '" 已存在,请使用其他标识!</div>';
|
|
||||||
} else {
|
|
||||||
$configId = $db->insert(
|
|
||||||
'INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor) VALUES (?, ?, ?, ?, ?, ?)',
|
|
||||||
[$apiLabel, $siteType, $siteUrl, $account, $apiKey, $autoMonitor]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($configId) {
|
|
||||||
$message = '<div class="success-message">✅ 配置添加成功!ID: ' . $configId . '</div>';
|
|
||||||
|
|
||||||
// 如果是首次添加配置,提示运行install.sh
|
|
||||||
$configCount = $db->queryOne('SELECT COUNT(*) as count FROM configs')['count'];
|
|
||||||
if ($configCount == 1) {
|
|
||||||
$message .= '<div class="info-box">💡 这是第一个配置,请运行 <code>app/install.sh</code> 安装监控服务</div>';
|
|
||||||
|
|
||||||
// 尝试自动执行install.sh(需要PHP有执行权限)
|
|
||||||
$installScript = __DIR__ . '/app/install.sh';
|
|
||||||
if (file_exists($installScript)) {
|
|
||||||
// 检查exec函数是否可用
|
|
||||||
if (!function_exists('exec')) {
|
|
||||||
$message .= '<div class="warning-message">⚠️ exec函数被禁用,无法自动安装监控服务。请手动执行: sudo bash app/install.sh</div>';
|
|
||||||
} else {
|
|
||||||
// 先设置可执行权限
|
|
||||||
chmod($installScript, 0755);
|
|
||||||
|
|
||||||
// 执行安装脚本
|
|
||||||
$output = [];
|
|
||||||
$returnVar = 0;
|
|
||||||
exec("bash {$installScript} 2>&1", $output, $returnVar);
|
|
||||||
|
|
||||||
if ($returnVar === 0) {
|
|
||||||
$message .= '<div class="success-message">✅ 监控服务已自动安装并启动</div>';
|
|
||||||
} else {
|
|
||||||
$message .= '<div class="error-message">⚠️ 自动安装失败,请手动执行: sudo bash app/install.sh</div>';
|
|
||||||
// 输出错误信息以便调试
|
|
||||||
if (!empty($output)) {
|
|
||||||
$message .= '<details><summary>查看错误详情</summary><pre>' . htmlspecialchars(implode("\n", $output)) . '</pre></details>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动刷新VPS列表
|
|
||||||
if ($siteType === 'mofang') {
|
|
||||||
refreshVpsListForConfig($configId);
|
|
||||||
$message .= '<div class="info-box">✅ 已自动获取VPS列表</div>';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$message = '<div class="error-message">❌ 配置添加失败!</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif (isset($_POST['delete_config'])) {
|
|
||||||
$configId = intval($_POST['config_id']);
|
|
||||||
$db = getVpsDB();
|
|
||||||
$db->execute('DELETE FROM configs WHERE id = ?', [$configId]);
|
|
||||||
$message = '<div class="success-message">✅ 配置已删除</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有配置
|
|
||||||
$db = getVpsDB();
|
|
||||||
$configs = $db->query('SELECT * FROM configs ORDER BY id');
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
|
||||||
<link href="./static/style.css" rel="stylesheet" type="text/css">
|
|
||||||
<title>配置管理 - VPS Hub</title>
|
|
||||||
<style>
|
|
||||||
.config-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.config-table th, .config-table td {
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
.config-table th {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.config-table tr:hover {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
.btn-delete {
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-delete:hover {
|
|
||||||
background-color: #c82333;
|
|
||||||
}
|
|
||||||
.form-section {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.form-group-inline {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>⚙️ VPS配置管理</h1>
|
|
||||||
<p>添加和管理多平台VPS配置</p>
|
|
||||||
<a href="index.php?pass=<?php echo urlencode($pass); ?>" class="btn" style="float: right; margin-top: -60px;">返回首页</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<?php echo $message; ?>
|
|
||||||
|
|
||||||
<!-- 添加配置表单 -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h2>➕ 添加新配置</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group-inline">
|
|
||||||
<label>API标识 * <small>(例如: 核云、阿里云主账号)</small></label>
|
|
||||||
<input type="text" name="api_label" required placeholder="输入唯一标识" pattern="[a-zA-Z0-9\u4e00-\u9fa5_\-]+" title="只能包含字母、数字、中文、下划线和连字符">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group-inline">
|
|
||||||
<label>网站类型 *</label>
|
|
||||||
<select name="site_type" required onchange="toggleSiteUrl(this.value)">
|
|
||||||
<option value="mofang">魔方平台</option>
|
|
||||||
<option value="aliyun">阿里云(暂不支持)</option>
|
|
||||||
<option value="tencent">腾讯云(暂不支持)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group-inline" id="site_url_group">
|
|
||||||
<label>网站链接/API地址 *</label>
|
|
||||||
<input type="url" name="site_url" required placeholder="https://www.example.com" id="site_url_input">
|
|
||||||
<small style="color: #dc3545;">⚠️ 严禁包含路径部分(如 /v1、/api),只允许根域名</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group-inline">
|
|
||||||
<label>账户 * <small>(邮箱或手机号)</small></label>
|
|
||||||
<input type="text" name="account" required placeholder="输入账户">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group-inline">
|
|
||||||
<label>API密钥 *</label>
|
|
||||||
<input type="password" name="api_key" required placeholder="输入API密钥">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<input type="checkbox" name="auto_monitor" id="auto_monitor" checked>
|
|
||||||
<label for="auto_monitor">开启自动开机监控</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="add_config" class="btn btn-success">
|
|
||||||
✅ 添加配置
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配置列表 -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h2>📋 现有配置</h2>
|
|
||||||
|
|
||||||
<?php if (empty($configs)): ?>
|
|
||||||
<p style="color: #999; text-align: center; padding: 40px;">暂无配置,请添加第一个配置</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<table class="config-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>序号</th>
|
|
||||||
<th>API标识</th>
|
|
||||||
<th>网站类型</th>
|
|
||||||
<th>网站链接</th>
|
|
||||||
<th>账户</th>
|
|
||||||
<th>自动监控</th>
|
|
||||||
<th>创建时间</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($configs as $config): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?php echo $config['id']; ?></td>
|
|
||||||
<td><strong><?php echo htmlspecialchars($config['api_label']); ?></strong></td>
|
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$typeMap = [
|
|
||||||
'mofang' => '魔方',
|
|
||||||
'aliyun' => '阿里云',
|
|
||||||
'tencent' => '腾讯云'
|
|
||||||
];
|
|
||||||
echo $typeMap[$config['site_type']] ?? $config['site_type'];
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
<td><?php echo htmlspecialchars($config['site_url'] ?? '-'); ?></td>
|
|
||||||
<td><?php echo htmlspecialchars($config['account']); ?></td>
|
|
||||||
<td>
|
|
||||||
<?php echo $config['auto_monitor'] ? '✅ 开启' : '❌ 关闭'; ?>
|
|
||||||
</td>
|
|
||||||
<td><?php echo date('Y-m-d H:i', strtotime($config['created_at'])); ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" style="display: inline;"
|
|
||||||
onsubmit="return confirm('确认删除此配置?')">
|
|
||||||
<input type="hidden" name="config_id" value="<?php echo $config['id']; ?>">
|
|
||||||
<button type="submit" name="delete_config" class="btn-delete">
|
|
||||||
🗑️ 删除
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<center><div class="copyright">
|
|
||||||
© 2026 Gaming Master Cybersecurity |
|
|
||||||
<a href="https://git.masonliu.com/MasonLiu/VPSHUB" target="_blank">MasonLiu In Gitea</a>
|
|
||||||
</div></center>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function toggleSiteUrl(siteType) {
|
|
||||||
const urlGroup = document.getElementById('site_url_group');
|
|
||||||
const urlInput = document.getElementById('site_url_input');
|
|
||||||
|
|
||||||
if (siteType === 'mofang') {
|
|
||||||
urlInput.placeholder = '例如: https://www.heyunidc.cn';
|
|
||||||
} else if (siteType === 'aliyun') {
|
|
||||||
urlInput.placeholder = '例如: https://ecs.aliyuncs.com';
|
|
||||||
} else if (siteType === 'tencent') {
|
|
||||||
urlInput.placeholder = '例如: https://cvm.tencentcloudapi.com';
|
|
||||||
} else {
|
|
||||||
urlInput.placeholder = '例如: https://www.example.com';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
544
index.php
544
index.php
@ -1,544 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/app/db_helper.php';
|
|
||||||
require_once __DIR__ . '/app/logger.php';
|
|
||||||
require_once __DIR__ . '/mofangidc.php';
|
|
||||||
|
|
||||||
// 存储API_PASS
|
|
||||||
$tokenFile = __DIR__ . '/app/pass.php';
|
|
||||||
|
|
||||||
// 检查是否已设置API_PASS
|
|
||||||
if (!file_exists($tokenFile)) {
|
|
||||||
header('Location: config_add.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载API_PASS
|
|
||||||
include $tokenFile;
|
|
||||||
|
|
||||||
// 验证pass参数
|
|
||||||
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
|
|
||||||
|
|
||||||
if (!defined('API_PASS') || $pass !== API_PASS) {
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode(['status' => 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理操作请求
|
|
||||||
$message = '';
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$action = $_POST['action'] ?? '';
|
|
||||||
$hostId = intval($_POST['host_id'] ?? 0);
|
|
||||||
$configId = intval($_POST['config_id'] ?? 0);
|
|
||||||
|
|
||||||
if ($action && $hostId > 0 && $configId > 0) {
|
|
||||||
$result = null;
|
|
||||||
|
|
||||||
if ($action === 'reboot') {
|
|
||||||
$result = mofangHardReboot($configId, $hostId);
|
|
||||||
} elseif ($action === 'on') {
|
|
||||||
$result = mofangPowerOn($configId, $hostId);
|
|
||||||
} elseif ($action === 'off') {
|
|
||||||
$result = mofangPowerOff($configId, $hostId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result && isset($result['status']) && $result['status'] === 200) {
|
|
||||||
$message = '<div class="success-message"><h3>✅ 操作成功</h3><p>VPS #' . $hostId . ' 已成功执行操作</p></div>';
|
|
||||||
} else {
|
|
||||||
$errorMsg = $result['msg'] ?? '未知错误';
|
|
||||||
$message = '<div class="error-message"><h3>❌ 操作失败</h3><p>' . htmlspecialchars($errorMsg) . '</p></div>';
|
|
||||||
}
|
|
||||||
} elseif ($action === 'refresh_vps_list') {
|
|
||||||
// 手动刷新VPS列表
|
|
||||||
$count = refreshAllVpsLists();
|
|
||||||
$message = '<div class="success-message"><h3>✅ 刷新完成</h3><p>已成功刷新 ' . $count . ' 个配置的VPS列表</p></div>';
|
|
||||||
|
|
||||||
// 获取每个VPS的详细信息并更新数据库
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
$configDb = getVpsDB();
|
|
||||||
$configs = $configDb->query('SELECT * FROM configs');
|
|
||||||
|
|
||||||
foreach ($configs as $config) {
|
|
||||||
if ($config['site_type'] !== 'mofang') {
|
|
||||||
continue; // 目前只支持魔方平台
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取该配置下的所有VPS
|
|
||||||
$vpsItems = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$config['id']]);
|
|
||||||
|
|
||||||
foreach ($vpsItems as $vpsItem) {
|
|
||||||
$vpsId = $vpsItem['vps_id'];
|
|
||||||
|
|
||||||
// 获取VPS详细信息
|
|
||||||
$details = mofangGetVpsDetails($config['id'], $vpsId);
|
|
||||||
|
|
||||||
if ($details && isset($details['status']) && $details['status'] === 200 && isset($details['data']['host'])) {
|
|
||||||
$host = $details['data']['host'];
|
|
||||||
|
|
||||||
// 解析CPU和内存信息
|
|
||||||
$cpuCores = null;
|
|
||||||
$memorySize = null;
|
|
||||||
|
|
||||||
if (isset($host['config_option']) && is_array($host['config_option'])) {
|
|
||||||
foreach ($host['config_option'] as $option) {
|
|
||||||
if ($option['key'] === 'cpu' && isset($option['value'])) {
|
|
||||||
// 提取数字,例如 "16核" -> 16
|
|
||||||
preg_match('/(\d+)/', $option['value'], $matches);
|
|
||||||
if (!empty($matches)) {
|
|
||||||
$cpuCores = intval($matches[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($option['key'] === 'memory' && isset($option['value'])) {
|
|
||||||
$memorySize = $option['value']; // 例如 "16G"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据库
|
|
||||||
$listDb->execute(
|
|
||||||
'UPDATE vps_list SET cpu_cores = ?, memory_size = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
|
|
||||||
[$cpuCores, $memorySize, $config['id'], $vpsId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$message .= '<div class="success-message">✅ 已更新VPS详细信息(CPU、内存等)</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有配置
|
|
||||||
$db = getVpsDB();
|
|
||||||
$configs = $db->query('SELECT * FROM configs ORDER BY id');
|
|
||||||
|
|
||||||
// 构建配置ID到api_label的映射
|
|
||||||
$configMap = [];
|
|
||||||
foreach ($configs as $config) {
|
|
||||||
$configMap[$config['id']] = $config['api_label'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有VPS列表(从vpslist.db查询)
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
$vpsList = $listDb->query('SELECT * FROM vps_list ORDER BY config_id, vps_id');
|
|
||||||
|
|
||||||
// 为每个VPS添加api_label
|
|
||||||
foreach ($vpsList as &$vps) {
|
|
||||||
$configId = $vps['config_id'];
|
|
||||||
$vps['api_label'] = $configMap[$configId] ?? 'Unknown';
|
|
||||||
}
|
|
||||||
unset($vps);
|
|
||||||
|
|
||||||
Logger::info("Total VPS count: " . count($vpsList), 'index.php');
|
|
||||||
if (!empty($vpsList)) {
|
|
||||||
Logger::debug("First VPS: " . json_encode($vpsList[0]), 'index.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按api_label分组
|
|
||||||
$vpsByApiLabel = [];
|
|
||||||
foreach ($vpsList as $vps) {
|
|
||||||
$apiLabel = $vps['api_label'];
|
|
||||||
if (!isset($vpsByApiLabel[$apiLabel])) {
|
|
||||||
$vpsByApiLabel[$apiLabel] = [];
|
|
||||||
}
|
|
||||||
$vpsByApiLabel[$apiLabel][] = $vps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计信息
|
|
||||||
$totalCount = count($vpsList);
|
|
||||||
$runningCount = 0;
|
|
||||||
$offCount = 0;
|
|
||||||
|
|
||||||
foreach ($vpsList as $vps) {
|
|
||||||
if ($vps['status'] === 'on') {
|
|
||||||
$runningCount++;
|
|
||||||
} elseif ($vps['status'] === 'off') {
|
|
||||||
$offCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
|
||||||
<link href="./static/style.css" rel="stylesheet" type="text/css">
|
|
||||||
<title>VPS管理面板 - VPS Hub</title>
|
|
||||||
<style>
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.btn-refresh {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.btn-refresh:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
/* 消息自动消失动画 */
|
|
||||||
.success-message, .error-message {
|
|
||||||
transition: opacity 0.5s ease-out;
|
|
||||||
}
|
|
||||||
.fade-out {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.config-section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.config-title {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.vps-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
|
||||||
.vps-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
.vps-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
.card-header {
|
|
||||||
padding: 15px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.vps-id {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.status-badge {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.card-body {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
.info-row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.info-label {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.info-value {
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.power-status {
|
|
||||||
padding: 10px 15px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.power-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.power-on {
|
|
||||||
background-color: #28a745;
|
|
||||||
box-shadow: 0 0 8px #28a745;
|
|
||||||
}
|
|
||||||
.power-off {
|
|
||||||
background-color: #dc3545;
|
|
||||||
}
|
|
||||||
.power-process {
|
|
||||||
background-color: #ffc107;
|
|
||||||
box-shadow: 0 0 8px #ffc107;
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
.power-unknown {
|
|
||||||
background-color: #6c757d;
|
|
||||||
}
|
|
||||||
.power-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
padding: 15px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.btn-success {
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn-success:hover {
|
|
||||||
background-color: #218838;
|
|
||||||
}
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn-danger:hover {
|
|
||||||
background-color: #c82333;
|
|
||||||
}
|
|
||||||
.btn-warning {
|
|
||||||
background-color: #ffc107;
|
|
||||||
color: #333;
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
.btn-warning:hover {
|
|
||||||
background-color: #e0a800;
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.empty-state h3 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>🖥️ VPS Hub 管理面板</h1>
|
|
||||||
<p>实时查看和管理您的云服务器</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<?php echo $message; ?>
|
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
|
||||||
<div class="toolbar">
|
|
||||||
<form method="POST" style="display: inline;">
|
|
||||||
<button type="submit" name="action" value="refresh_vps_list" class="btn-refresh">
|
|
||||||
🔄 手动刷新VPS列表
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a href="config_add.php?pass=<?php echo urlencode($pass); ?>" class="btn btn-success">
|
|
||||||
➕ 添加配置源
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 统计信息 -->
|
|
||||||
<div class="stats-bar">
|
|
||||||
<div class="stats-item">
|
|
||||||
总计:<strong><?php echo $totalCount; ?></strong> 台服务器
|
|
||||||
</div>
|
|
||||||
<div class="stats-item">
|
|
||||||
运行中:<strong><?php echo $runningCount; ?></strong> 台
|
|
||||||
</div>
|
|
||||||
<div class="stats-item">
|
|
||||||
已关机:<strong><?php echo $offCount; ?></strong> 台
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (empty($configs)): ?>
|
|
||||||
<div class="empty-state">
|
|
||||||
<h3>📭 暂无配置</h3>
|
|
||||||
<p>请先添加VPS配置才能开始管理</p>
|
|
||||||
<a href="config_add.php?pass=<?php echo urlencode($pass); ?>" class="btn btn-success" style="margin-top: 20px;">
|
|
||||||
➕ 添加第一个配置
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<!-- 按api_label分组显示VPS -->
|
|
||||||
<?php foreach ($vpsByApiLabel as $apiLabel => $labelVps): ?>
|
|
||||||
<div class="config-section">
|
|
||||||
<div class="config-title">
|
|
||||||
<?php echo htmlspecialchars($apiLabel); ?>
|
|
||||||
- <?php echo count($labelVps); ?> 台VPS
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (empty($labelVps)): ?>
|
|
||||||
<div class="empty-state" style="background: white; padding: 40px;">
|
|
||||||
<p>此配置下暂无VPS,请点击“手动刷新VPS列表”获取</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="vps-grid">
|
|
||||||
<?php foreach ($labelVps as $vps):
|
|
||||||
$statusClass = 'power-unknown';
|
|
||||||
$statusText = '未知';
|
|
||||||
$statusColor = '#999';
|
|
||||||
|
|
||||||
if ($vps['status'] === 'on' || $vps['status'] === 'running') {
|
|
||||||
$statusClass = 'power-on';
|
|
||||||
$statusText = '运行中';
|
|
||||||
$statusColor = '#28a745';
|
|
||||||
} elseif ($vps['status'] === 'off' || $vps['status'] === 'stopped') {
|
|
||||||
$statusClass = 'power-off';
|
|
||||||
$statusText = '已关机';
|
|
||||||
$statusColor = '#dc3545';
|
|
||||||
} elseif ($vps['status'] === 'process' || $vps['status'] === 'pending' || $vps['status'] === 'rebooting') {
|
|
||||||
$statusClass = 'power-process';
|
|
||||||
$statusText = '处理中';
|
|
||||||
$statusColor = '#ffc107';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="vps-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="vps-id">#<?php echo $vps['vps_id']; ?></div>
|
|
||||||
<span class="status-badge" style="background-color: <?php echo $statusColor; ?>">
|
|
||||||
<?php echo $statusText; ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<?php if ($vps['ip_address']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">IP地址</span>
|
|
||||||
<span class="info-value"><?php echo htmlspecialchars($vps['ip_address']); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($vps['product_name']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">产品名称</span>
|
|
||||||
<span class="info-value"><?php echo htmlspecialchars($vps['product_name']); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($vps['cpu_cores'] || $vps['memory_size']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">CPU/内存</span>
|
|
||||||
<span class="info-value">
|
|
||||||
<?php
|
|
||||||
$parts = [];
|
|
||||||
if ($vps['cpu_cores']) {
|
|
||||||
$parts[] = $vps['cpu_cores'] . ' 核';
|
|
||||||
}
|
|
||||||
if ($vps['memory_size']) {
|
|
||||||
$parts[] = htmlspecialchars($vps['memory_size']);
|
|
||||||
}
|
|
||||||
echo implode(' / ', $parts);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($vps['disk_size'] || $vps['bandwidth'] || $vps['os_type']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">磁盘/带宽/系统</span>
|
|
||||||
<span class="info-value"><?php echo htmlspecialchars($vps['disk_size']) . ' / ' . htmlspecialchars($vps['bandwidth']) . ' / ' . htmlspecialchars($vps['os_type']); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($vps['amount']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">价格</span>
|
|
||||||
<span class="info-value"><?php echo htmlspecialchars($vps['amount']); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($vps['nextduedate']): ?>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">到期时间</span>
|
|
||||||
<span class="info-value"><?php echo date('Y-m-d', $vps['nextduedate']); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="power-status">
|
|
||||||
<span class="power-indicator <?php echo $statusClass; ?>"></span>
|
|
||||||
<span class="power-text"><?php echo $statusText; ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" style="margin: 0;">
|
|
||||||
<input type="hidden" name="host_id" value="<?php echo $vps['vps_id']; ?>">
|
|
||||||
<input type="hidden" name="config_id" value="<?php echo $vps['config_id']; ?>">
|
|
||||||
<div class="card-actions">
|
|
||||||
<button type="submit" name="action" value="on" class="btn btn-success" onclick="return confirm('确认开机?')">
|
|
||||||
⚡ 开机
|
|
||||||
</button>
|
|
||||||
<button type="submit" name="action" value="off" class="btn btn-danger" onclick="return confirm('确认关机?')">
|
|
||||||
🔴 关机
|
|
||||||
</button>
|
|
||||||
<button type="submit" name="action" value="reboot" class="btn btn-warning" onclick="return confirm('确认硬重启?')">
|
|
||||||
🔄 硬重启
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<center><div class="copyright">
|
|
||||||
© 2026 Gaming Master Cybersecurity |
|
|
||||||
<a href="https://git.masonliu.com/MasonLiu/VPSHUB" target="_blank">MasonLiu In Gitea</a>
|
|
||||||
</div></center>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 操作成功提示5秒后自动消失
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const messages = document.querySelectorAll('.success-message, .error-message');
|
|
||||||
|
|
||||||
messages.forEach(function(message) {
|
|
||||||
setTimeout(function() {
|
|
||||||
message.classList.add('fade-out');
|
|
||||||
|
|
||||||
// 等待动画完成后移除元素
|
|
||||||
setTimeout(function() {
|
|
||||||
message.style.display = 'none';
|
|
||||||
}, 500);
|
|
||||||
}, 5000); // 5秒后开始淡出
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
812
mofangidc.php
812
mofangidc.php
@ -1,812 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/app/db_helper.php';
|
|
||||||
require_once __DIR__ . '/app/logger.php';
|
|
||||||
|
|
||||||
define('TOKEN_CACHE_FILE', __DIR__ . '/app/token.php');
|
|
||||||
define('TOKEN_EXPIRE_TIME', 7200);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存的Token
|
|
||||||
*/
|
|
||||||
function getCachedToken($configId) {
|
|
||||||
if (!file_exists(TOKEN_CACHE_FILE)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
include TOKEN_CACHE_FILE;
|
|
||||||
|
|
||||||
if (!isset($cached_tokens[$configId])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cache = $cached_tokens[$configId];
|
|
||||||
$currentTime = time();
|
|
||||||
$tokenAge = $currentTime - $cache['timestamp'];
|
|
||||||
|
|
||||||
if ($tokenAge < TOKEN_EXPIRE_TIME) {
|
|
||||||
return $cache['token'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token已过期,清除该配置的缓存
|
|
||||||
unset($cached_tokens[$configId]);
|
|
||||||
saveTokensFile($cached_tokens);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存Token到缓存文件
|
|
||||||
*/
|
|
||||||
function saveToken($configId, $token) {
|
|
||||||
$cached_tokens = [];
|
|
||||||
if (file_exists(TOKEN_CACHE_FILE)) {
|
|
||||||
include TOKEN_CACHE_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cached_tokens[$configId] = [
|
|
||||||
'token' => $token,
|
|
||||||
'timestamp' => time()
|
|
||||||
];
|
|
||||||
|
|
||||||
saveTokensFile($cached_tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存tokens数组到文件
|
|
||||||
* @param array $cached_tokens tokens数组
|
|
||||||
*/
|
|
||||||
function saveTokensFile($cached_tokens) {
|
|
||||||
$content = "<?php\n";
|
|
||||||
$content .= "// Token缓存文件 - 自动生成\n";
|
|
||||||
$content .= "// 警告: 不要手动修改此文件\n\n";
|
|
||||||
$content .= '$cached_tokens = ' . var_export($cached_tokens, true) . ";\n";
|
|
||||||
$content .= "?>\n";
|
|
||||||
|
|
||||||
file_put_contents(TOKEN_CACHE_FILE, $content);
|
|
||||||
chmod(TOKEN_CACHE_FILE, 0600);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 魔方平台登录获取JWT Token
|
|
||||||
*/
|
|
||||||
function mofangLogin($siteUrl, $account, $apiKey) {
|
|
||||||
try {
|
|
||||||
$url = rtrim($siteUrl, '/') . '/v1/login_api';
|
|
||||||
$data = [
|
|
||||||
'account' => $account,
|
|
||||||
'password' => $apiKey
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
|
||||||
Logger::error("魔方登录请求失败,HTTP状态码: {$httpCode}", 'mofangLogin');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = json_decode($response, true);
|
|
||||||
|
|
||||||
if (isset($result['status']) && $result['status'] === 200 && isset($result['jwt'])) {
|
|
||||||
return $result['jwt'];
|
|
||||||
} else {
|
|
||||||
Logger::error("魔方登录失败: " . ($result['msg'] ?? '未知错误'), 'mofangLogin');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Logger::error("魔方登录异常: " . $e->getMessage(), 'mofangLogin');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取有效的Token(先查缓存,没有则重新登录)
|
|
||||||
*/
|
|
||||||
function getValidToken($configId) {
|
|
||||||
// 先尝试从缓存获取
|
|
||||||
$token = getCachedToken($configId);
|
|
||||||
if ($token) {
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存不存在或已过期,重新获取
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
Logger::error("配置ID {$configId} 不存在", 'getValidToken');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = mofangLogin($config['site_url'], $config['account'], $config['api_key']);
|
|
||||||
|
|
||||||
if ($token) {
|
|
||||||
saveToken($configId, $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送魔方API请求
|
|
||||||
*/
|
|
||||||
function mofangApiRequest($siteUrl, $endpoint, $method = 'GET', $data = [], $configId = null) {
|
|
||||||
// 获取Token
|
|
||||||
if ($configId) {
|
|
||||||
$token = getValidToken($configId);
|
|
||||||
} else {
|
|
||||||
// 如果没有configId,尝试从第一个配置获取
|
|
||||||
$db = getVpsDB();
|
|
||||||
$configs = $db->query('SELECT * FROM configs LIMIT 1');
|
|
||||||
if (!$configs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$config = $configs[0];
|
|
||||||
$token = getValidToken($config['id']);
|
|
||||||
$siteUrl = $config['site_url'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$token) {
|
|
||||||
return ['status' => 400, 'msg' => '获取Token失败'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = rtrim($siteUrl, '/') . $endpoint;
|
|
||||||
$headers = [
|
|
||||||
'Authorization: JWT ' . $token,
|
|
||||||
'Content-Type: application/json'
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
|
|
||||||
if ($method === 'POST') {
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
||||||
} elseif ($method === 'PUT') {
|
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
|
||||||
return ['status' => $httpCode, 'msg' => 'HTTP请求失败'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = json_decode($response, true);
|
|
||||||
|
|
||||||
// 如果Token失效(405),清除缓存并重试
|
|
||||||
if (isset($result['status']) && $result['status'] === 405 && $configId) {
|
|
||||||
// 清除缓存
|
|
||||||
$cached_tokens = [];
|
|
||||||
if (file_exists(TOKEN_CACHE_FILE)) {
|
|
||||||
include TOKEN_CACHE_FILE;
|
|
||||||
}
|
|
||||||
unset($cached_tokens[$configId]);
|
|
||||||
saveTokensFile($cached_tokens);
|
|
||||||
|
|
||||||
// 重试一次
|
|
||||||
return mofangApiRequest($siteUrl, $endpoint, $method, $data, $configId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取VPS列表
|
|
||||||
*/
|
|
||||||
function mofangGetVpsList($configId, $page = 1, $limit = 100) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts?page={$page}&limit={$limit}";
|
|
||||||
return mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取VPS状态
|
|
||||||
*/
|
|
||||||
function mofangGetVpsStatus($configId, $vpsId, $updateDb = true) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts/{$vpsId}/module/status?type=host";
|
|
||||||
$result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
|
|
||||||
|
|
||||||
// 如果获取成功且需要更新数据库
|
|
||||||
if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data'])) {
|
|
||||||
$statusData = $result['data'];
|
|
||||||
$rawStatus = $statusData['status'] ?? 'unknown';
|
|
||||||
|
|
||||||
// 状态映射:将魔方平台的原始状态转换为标准状态
|
|
||||||
$statusMap = [
|
|
||||||
'on' => 'on', // 开机
|
|
||||||
'off' => 'off', // 关机
|
|
||||||
'running' => 'on', // 运行中
|
|
||||||
'stopped' => 'off', // 已停止
|
|
||||||
'process' => 'process', // 处理中(开机/关机/重启过程中)
|
|
||||||
'pending' => 'process', // 等待中
|
|
||||||
'installing' => 'process', // 安装中
|
|
||||||
'rebooting' => 'process', // 重启中
|
|
||||||
'unknown' => 'unknown' // 未知
|
|
||||||
];
|
|
||||||
|
|
||||||
$status = $statusMap[$rawStatus] ?? $rawStatus;
|
|
||||||
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
|
|
||||||
// 先获取该VPS的IP地址,用于精确匹配
|
|
||||||
$vpsInfo = $listDb->queryOne(
|
|
||||||
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
|
|
||||||
[$configId, $vpsId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($vpsInfo) {
|
|
||||||
// 记录存在,执行UPDATE
|
|
||||||
$success = $listDb->execute(
|
|
||||||
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
|
|
||||||
[$status, $vpsInfo['id']]
|
|
||||||
);
|
|
||||||
if ($success) {
|
|
||||||
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'mofangGetVpsStatus');
|
|
||||||
} else {
|
|
||||||
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 状态更新失败", 'mofangGetVpsStatus');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 记录不存在,插入新记录
|
|
||||||
$success = $listDb->execute(
|
|
||||||
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
|
||||||
[$configId, $vpsId, $status]
|
|
||||||
);
|
|
||||||
if ($success) {
|
|
||||||
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}", 'mofangGetVpsStatus');
|
|
||||||
} else {
|
|
||||||
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 新记录插入失败", 'mofangGetVpsStatus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($updateDb && (!$result || !isset($result['status']) || $result['status'] !== 200)) {
|
|
||||||
// API调用失败,不更新数据库,保留原有状态
|
|
||||||
Logger::warning("[mofangGetVpsStatus] VPS {$vpsId} API调用失败,保留原有状态", 'mofangGetVpsStatus');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取VPS详细信息
|
|
||||||
*/
|
|
||||||
function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts/{$vpsId}";
|
|
||||||
$result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
|
|
||||||
|
|
||||||
// 如果获取成功且需要更新数据库
|
|
||||||
if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data']['host'])) {
|
|
||||||
$host = $result['data']['host'];
|
|
||||||
|
|
||||||
// 解析详细信息
|
|
||||||
$updates = []; // 存储要更新的字段
|
|
||||||
$values = [];
|
|
||||||
|
|
||||||
// 提取开关机状态(如果有)
|
|
||||||
if (isset($host['status'])) {
|
|
||||||
$rawStatus = $host['status'];
|
|
||||||
|
|
||||||
// 状态映射:将魔方平台的原始状态转换为标准状态
|
|
||||||
$statusMap = [
|
|
||||||
'on' => 'on', // 开机
|
|
||||||
'off' => 'off', // 关机
|
|
||||||
'running' => 'on', // 运行中
|
|
||||||
'stopped' => 'off', // 已停止
|
|
||||||
'process' => 'process', // 处理中(开机/关机/重启过程中)
|
|
||||||
'pending' => 'process', // 等待中
|
|
||||||
'installing' => 'process', // 安装中
|
|
||||||
'rebooting' => 'process', // 重启中
|
|
||||||
'unknown' => 'unknown' // 未知
|
|
||||||
];
|
|
||||||
|
|
||||||
$status = $statusMap[$rawStatus] ?? $rawStatus;
|
|
||||||
$updates[] = 'status = ?';
|
|
||||||
$values[] = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($host['config_option']) && is_array($host['config_option'])) {
|
|
||||||
foreach ($host['config_option'] as $option) {
|
|
||||||
switch ($option['key']) {
|
|
||||||
case 'cpu':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
preg_match('/(\d+)/', $option['value'], $matches);
|
|
||||||
if (!empty($matches)) {
|
|
||||||
$updates[] = 'cpu_cores = ?';
|
|
||||||
$values[] = intval($matches[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'memory':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$updates[] = 'memory_size = ?';
|
|
||||||
$values[] = $option['value'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'system_disk_size':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$updates[] = 'disk_size = ?';
|
|
||||||
$values[] = $option['value'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'bw':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$updates[] = 'bandwidth = ?';
|
|
||||||
$values[] = $option['value'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'os':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$updates[] = 'os_type = ?';
|
|
||||||
$values[] = $option['value'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有当有字段需要更新时才执行UPDATE
|
|
||||||
if (!empty($updates)) {
|
|
||||||
// 添加last_check更新
|
|
||||||
$updates[] = 'last_check = CURRENT_TIMESTAMP';
|
|
||||||
|
|
||||||
// 构建动态UPDATE语句
|
|
||||||
$setClause = implode(', ', $updates);
|
|
||||||
$values[] = $configId;
|
|
||||||
$values[] = $vpsId;
|
|
||||||
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
|
|
||||||
// 先检查记录是否存在
|
|
||||||
$existing = $listDb->queryOne(
|
|
||||||
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
|
|
||||||
[$configId, $vpsId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
// 记录存在,执行UPDATE
|
|
||||||
$listDb->execute(
|
|
||||||
"UPDATE vps_list SET {$setClause} WHERE config_id = ? AND vps_id = ?",
|
|
||||||
$values
|
|
||||||
);
|
|
||||||
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新" . (isset($status) ? ",状态: {$status}" : ""), 'mofangGetVpsDetails');
|
|
||||||
} else {
|
|
||||||
// 记录不存在,插入新记录(只插入获取到的字段)
|
|
||||||
$columns = [];
|
|
||||||
$insertValues = [];
|
|
||||||
$placeholders = [];
|
|
||||||
|
|
||||||
// 解析SET子句中的字段
|
|
||||||
foreach ($updates as $update) {
|
|
||||||
if (strpos($update, 'last_check') === false) {
|
|
||||||
list($column, $value) = explode(' = ', $update);
|
|
||||||
$columns[] = $column;
|
|
||||||
$insertValues[] = array_shift($values); // 从$values中取出对应的值
|
|
||||||
$placeholders[] = '?';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加config_id和vps_id
|
|
||||||
$columns[] = 'config_id';
|
|
||||||
$columns[] = 'vps_id';
|
|
||||||
$insertValues[] = $configId;
|
|
||||||
$insertValues[] = $vpsId;
|
|
||||||
|
|
||||||
if (!empty($columns)) {
|
|
||||||
$columnStr = implode(', ', $columns);
|
|
||||||
$placeholderStr = implode(', ', $placeholders);
|
|
||||||
$listDb->execute(
|
|
||||||
"INSERT INTO vps_list ({$columnStr}) VALUES ({$placeholderStr})",
|
|
||||||
$insertValues
|
|
||||||
);
|
|
||||||
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入", 'mofangGetVpsDetails');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VPS开机
|
|
||||||
*/
|
|
||||||
function mofangPowerOn($configId, $vpsId) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts/{$vpsId}/module/on";
|
|
||||||
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
|
|
||||||
|
|
||||||
// 开机操作后立即获取状态并更新数据库
|
|
||||||
if ($result && isset($result['status']) && $result['status'] === 200) {
|
|
||||||
sleep(2); // 等待2秒让状态生效
|
|
||||||
mofangGetVpsStatus($configId, $vpsId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VPS关机
|
|
||||||
*/
|
|
||||||
function mofangPowerOff($configId, $vpsId) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts/{$vpsId}/module/off";
|
|
||||||
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
|
|
||||||
|
|
||||||
// 关机操作后立即获取状态并更新数据库
|
|
||||||
if ($result && isset($result['status']) && $result['status'] === 200) {
|
|
||||||
sleep(2); // 等待2秒让状态生效
|
|
||||||
mofangGetVpsStatus($configId, $vpsId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VPS硬重启
|
|
||||||
*/
|
|
||||||
function mofangHardReboot($configId, $vpsId) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = "/v1/hosts/{$vpsId}/module/hard_reboot";
|
|
||||||
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
|
|
||||||
|
|
||||||
// 硬重启操作后立即获取状态并更新数据库
|
|
||||||
if ($result && isset($result['status']) && $result['status'] === 200) {
|
|
||||||
sleep(3); // 等待3秒让重启生效
|
|
||||||
mofangGetVpsStatus($configId, $vpsId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新指定配置的VPS列表并保存到数据库
|
|
||||||
*/
|
|
||||||
function refreshVpsListForConfig($configId) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取VPS列表
|
|
||||||
$result = mofangGetVpsList($configId);
|
|
||||||
|
|
||||||
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
|
|
||||||
Logger::error("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'), 'refreshVpsListForConfig');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$hosts = $result['data']['host'] ?? [];
|
|
||||||
|
|
||||||
if (empty($hosts)) {
|
|
||||||
return true; // 没有VPS也算成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量保存到数据库
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
|
|
||||||
foreach ($hosts as $host) {
|
|
||||||
// 解析详细信息
|
|
||||||
$cpuCores = null;
|
|
||||||
$memorySize = null;
|
|
||||||
$diskSize = null;
|
|
||||||
$bandwidth = null;
|
|
||||||
$osType = null;
|
|
||||||
|
|
||||||
if (isset($host['config_option']) && is_array($host['config_option'])) {
|
|
||||||
foreach ($host['config_option'] as $option) {
|
|
||||||
switch ($option['key']) {
|
|
||||||
case 'cpu':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
// 提取数字,例如 "16核" -> 16
|
|
||||||
preg_match('/(\d+)/', $option['value'], $matches);
|
|
||||||
if (!empty($matches)) {
|
|
||||||
$cpuCores = intval($matches[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'memory':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$memorySize = $option['value']; // 例如 "16G"
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'system_disk_size':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$diskSize = $option['value']; // 例如 "Lin50G,Win50G"
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'bw':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$bandwidth = $option['value']; // 例如 "70Mbps"
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'os':
|
|
||||||
if (isset($option['value'])) {
|
|
||||||
$osType = $option['value']; // 例如 "Debian-12.0_x64"
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基于vps_id和ip_address去重
|
|
||||||
$existing = $listDb->queryOne(
|
|
||||||
'SELECT id, status FROM vps_list WHERE vps_id = ? AND ip_address = ?',
|
|
||||||
[$host['id'], $host['dedicatedip'] ?? null]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
// 记录已存在,只更新非status字段,保留原有status
|
|
||||||
$listDb->execute(
|
|
||||||
'UPDATE vps_list SET domain = ?, product_name = ?, cpu_cores = ?, memory_size = ?, disk_size = ?, bandwidth = ?, os_type = ?, amount = ?, nextduedate = ?, section = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
|
|
||||||
[
|
|
||||||
$host['domain'] ?? null,
|
|
||||||
$host['product_name'] ?? null,
|
|
||||||
$cpuCores,
|
|
||||||
$memorySize,
|
|
||||||
$diskSize,
|
|
||||||
$bandwidth,
|
|
||||||
$osType,
|
|
||||||
$host['amount'] ?? null,
|
|
||||||
$host['nextduedate'] ?? null,
|
|
||||||
$config['auto_monitor'] ? 1 : 0,
|
|
||||||
$existing['id']
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 新记录,插入所有字段(status设为null,稍后通过状态接口获取)
|
|
||||||
$listDb->execute(
|
|
||||||
'INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, CURRENT_TIMESTAMP)',
|
|
||||||
[
|
|
||||||
$configId,
|
|
||||||
$host['id'],
|
|
||||||
$host['domain'] ?? null,
|
|
||||||
$host['dedicatedip'] ?? null,
|
|
||||||
$host['product_name'] ?? null,
|
|
||||||
$cpuCores,
|
|
||||||
$memorySize,
|
|
||||||
$diskSize,
|
|
||||||
$bandwidth,
|
|
||||||
$osType,
|
|
||||||
$host['amount'] ?? null,
|
|
||||||
$host['nextduedate'] ?? null,
|
|
||||||
$config['auto_monitor'] ? 1 : 0
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新列表后,批量获取每个VPS的状态
|
|
||||||
Logger::info("[refreshVpsListForConfig] 开始批量获取 {$configId} 配置下 " . count($hosts) . " 台VPS的状态", 'refreshVpsListForConfig');
|
|
||||||
|
|
||||||
foreach ($hosts as $host) {
|
|
||||||
// 调用专门的状态接口获取开关机状态
|
|
||||||
mofangGetVpsStatus($configId, $host['id'], true);
|
|
||||||
|
|
||||||
// 避免频繁请求,每次请求间隔0.5秒
|
|
||||||
usleep(500000); // 500毫秒
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger::info("[refreshVpsListForConfig] 配置 {$configId} 的VPS列表和状态刷新完成", 'refreshVpsListForConfig');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新所有配置的VPS列表
|
|
||||||
*/
|
|
||||||
function refreshAllVpsLists() {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$configs = $db->query('SELECT * FROM configs');
|
|
||||||
|
|
||||||
$successCount = 0;
|
|
||||||
|
|
||||||
foreach ($configs as $config) {
|
|
||||||
if (refreshVpsListForConfig($config['id'])) {
|
|
||||||
$successCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $successCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个VPS的状态并更新到数据库
|
|
||||||
*/
|
|
||||||
function updateVpsStatusToDb($configId, $vpsId) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
Logger::error("配置ID {$configId} 不存在", 'updateVpsStatusToDb');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API获取VPS状态(不自动更新数据库,由本函数统一处理)
|
|
||||||
$result = mofangGetVpsStatus($configId, $vpsId, false);
|
|
||||||
|
|
||||||
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
|
|
||||||
Logger::error("获取VPS {$vpsId} 状态失败,保留原有状态: " . ($result['msg'] ?? '未知错误'), 'updateVpsStatusToDb');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$statusData = $result['data'];
|
|
||||||
$rawStatus = $statusData['status'] ?? 'unknown';
|
|
||||||
$des = $statusData['des'] ?? '未知';
|
|
||||||
|
|
||||||
// 状态映射:将魔方平台的原始状态转换为标准状态
|
|
||||||
$statusMap = [
|
|
||||||
'on' => 'on', // 开机
|
|
||||||
'off' => 'off', // 关机
|
|
||||||
'running' => 'on', // 运行中
|
|
||||||
'stopped' => 'off', // 已停止
|
|
||||||
'process' => 'process', // 处理中(开机/关机/重启过程中)
|
|
||||||
'pending' => 'process', // 等待中
|
|
||||||
'installing' => 'process', // 安装中
|
|
||||||
'rebooting' => 'process', // 重启中
|
|
||||||
'unknown' => 'unknown' // 未知
|
|
||||||
];
|
|
||||||
|
|
||||||
$status = $statusMap[$rawStatus] ?? $rawStatus;
|
|
||||||
|
|
||||||
// 检查记录是否存在
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
$vpsInfo = $listDb->queryOne(
|
|
||||||
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
|
|
||||||
[$configId, $vpsId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($vpsInfo) {
|
|
||||||
// 记录存在,执行UPDATE
|
|
||||||
$success = $listDb->execute(
|
|
||||||
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
|
|
||||||
[$status, $vpsInfo['id']]
|
|
||||||
);
|
|
||||||
if ($success) {
|
|
||||||
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'updateVpsStatusToDb');
|
|
||||||
} else {
|
|
||||||
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 状态更新失败,保留原有状态", 'updateVpsStatusToDb');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 记录不存在,插入新记录
|
|
||||||
$success = $listDb->execute(
|
|
||||||
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
|
||||||
[$configId, $vpsId, $status]
|
|
||||||
);
|
|
||||||
if ($success) {
|
|
||||||
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}", 'updateVpsStatusToDb');
|
|
||||||
} else {
|
|
||||||
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 新记录插入失败", 'updateVpsStatusToDb');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'vps_id' => $vpsId,
|
|
||||||
'status' => $status,
|
|
||||||
'des' => $des,
|
|
||||||
'updated' => true
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量获取VPS状态并更新到数据库
|
|
||||||
*/
|
|
||||||
function batchUpdateVpsStatus($configId, $vpsIds = []) {
|
|
||||||
$db = getVpsDB();
|
|
||||||
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
return ['success' => 0, 'failed' => 0, 'error' => '配置不存在'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果未指定VPS IDs,获取该配置下的所有VPS
|
|
||||||
if (empty($vpsIds)) {
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
$vpsList = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$configId]);
|
|
||||||
$vpsIds = array_column($vpsList, 'vps_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($vpsIds)) {
|
|
||||||
return ['success' => 0, 'failed' => 0, 'error' => '没有VPS需要更新'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$successCount = 0;
|
|
||||||
$failedCount = 0;
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
$listDb = getVpsListDB();
|
|
||||||
$listDb->getConnection()->beginTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach ($vpsIds as $vpsId) {
|
|
||||||
$result = updateVpsStatusToDb($configId, $vpsId);
|
|
||||||
|
|
||||||
if ($result) {
|
|
||||||
$successCount++;
|
|
||||||
$results[] = $result;
|
|
||||||
} else {
|
|
||||||
$failedCount++;
|
|
||||||
$results[] = [
|
|
||||||
'vps_id' => $vpsId,
|
|
||||||
'status' => 'error',
|
|
||||||
'des' => '获取状态失败',
|
|
||||||
'updated' => false
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$listDb->getConnection()->commit();
|
|
||||||
Logger::info("[batchUpdateVpsStatus] 批量更新完成: 成功{$successCount}, 失败{$failedCount}", 'batchUpdateVpsStatus');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$listDb->getConnection()->rollBack();
|
|
||||||
Logger::error("[batchUpdateVpsStatus] 批量更新失败,已回滚: " . $e->getMessage(), 'batchUpdateVpsStatus');
|
|
||||||
return ['success' => 0, 'failed' => count($vpsIds), 'error' => '批量更新失败: ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => $successCount,
|
|
||||||
'failed' => $failedCount,
|
|
||||||
'total' => count($vpsIds),
|
|
||||||
'results' => $results
|
|
||||||
];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
205
static/config.js
205
static/config.js
@ -1,205 +0,0 @@
|
|||||||
/**
|
|
||||||
* VPS Hub 配置管理页面交互脚本
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换网站URL输入框的提示和默认值
|
|
||||||
* @param {string} siteType - 网站类型
|
|
||||||
*/
|
|
||||||
function toggleSiteUrl(siteType) {
|
|
||||||
const urlGroup = document.getElementById('site_url_group');
|
|
||||||
const urlInput = document.getElementById('site_url_input');
|
|
||||||
|
|
||||||
if (!urlInput) return;
|
|
||||||
|
|
||||||
switch(siteType) {
|
|
||||||
case 'mofang':
|
|
||||||
urlInput.placeholder = '留空使用默认值: https://www.heyunidc.cn/v1';
|
|
||||||
urlInput.required = false;
|
|
||||||
break;
|
|
||||||
case 'aliyun':
|
|
||||||
urlInput.placeholder = '例如: https://ecs.aliyuncs.com';
|
|
||||||
urlInput.required = true;
|
|
||||||
break;
|
|
||||||
case 'tencent':
|
|
||||||
urlInput.value = 'https://cvm.tencentcloudapi.com/';
|
|
||||||
urlInput.placeholder = '腾讯云API地址';
|
|
||||||
urlInput.required = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
urlInput.placeholder = '输入API地址';
|
|
||||||
urlInput.required = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证密码格式
|
|
||||||
* @returns {boolean} 是否通过验证
|
|
||||||
*/
|
|
||||||
function validatePassword() {
|
|
||||||
const passwordInput = document.getElementById('api_pass');
|
|
||||||
if (!passwordInput) return true;
|
|
||||||
|
|
||||||
const password = passwordInput.value;
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
alert('密码至少需要8位!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[a-zA-Z0-9]+$/.test(password)) {
|
|
||||||
alert('密码只能包含字母和数字!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认删除操作
|
|
||||||
* @param {string} message - 确认消息
|
|
||||||
* @returns {boolean} 是否确认
|
|
||||||
*/
|
|
||||||
function confirmDelete(message) {
|
|
||||||
return confirm(message || '确认删除此配置?');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示加载状态
|
|
||||||
* @param {HTMLElement} button - 按钮元素
|
|
||||||
*/
|
|
||||||
function showLoading(button) {
|
|
||||||
if (!button) return;
|
|
||||||
|
|
||||||
const originalText = button.textContent;
|
|
||||||
button.disabled = true;
|
|
||||||
button.textContent = '加载中...';
|
|
||||||
button.dataset.originalText = originalText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏加载状态
|
|
||||||
* @param {HTMLElement} button - 按钮元素
|
|
||||||
*/
|
|
||||||
function hideLoading(button) {
|
|
||||||
if (!button) return;
|
|
||||||
|
|
||||||
button.disabled = false;
|
|
||||||
button.textContent = button.dataset.originalText || '提交';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX提交表单
|
|
||||||
* @param {HTMLFormElement} form - 表单元素
|
|
||||||
* @param {Function} successCallback - 成功回调
|
|
||||||
* @param {Function} errorCallback - 失败回调
|
|
||||||
*/
|
|
||||||
function submitFormAjax(form, successCallback, errorCallback) {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const submitButton = form.querySelector('button[type="submit"]');
|
|
||||||
|
|
||||||
showLoading(submitButton);
|
|
||||||
|
|
||||||
fetch(form.action || window.location.href, {
|
|
||||||
method: form.method || 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
hideLoading(submitButton);
|
|
||||||
if (data.status === 200 || data.success) {
|
|
||||||
if (successCallback) successCallback(data);
|
|
||||||
} else {
|
|
||||||
if (errorCallback) errorCallback(data);
|
|
||||||
else alert('操作失败: ' + (data.msg || '未知错误'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
hideLoading(submitButton);
|
|
||||||
console.error('请求失败:', error);
|
|
||||||
if (errorCallback) errorCallback(error);
|
|
||||||
else alert('网络错误,请稍后重试');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新VPS列表
|
|
||||||
*/
|
|
||||||
function refreshVpsList() {
|
|
||||||
const button = document.querySelector('.btn-refresh');
|
|
||||||
if (!button) return;
|
|
||||||
|
|
||||||
showLoading(button);
|
|
||||||
|
|
||||||
// 创建临时表单提交
|
|
||||||
const form = document.createElement('form');
|
|
||||||
form.method = 'POST';
|
|
||||||
form.action = window.location.href;
|
|
||||||
|
|
||||||
const actionInput = document.createElement('input');
|
|
||||||
actionInput.type = 'hidden';
|
|
||||||
actionInput.name = 'action';
|
|
||||||
actionInput.value = 'refresh_vps_list';
|
|
||||||
|
|
||||||
form.appendChild(actionInput);
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化页面
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 自动聚焦第一个输入框
|
|
||||||
const firstInput = document.querySelector('input:not([type="hidden"]):not([type="checkbox"])');
|
|
||||||
if (firstInput && !firstInput.value) {
|
|
||||||
firstInput.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为所有删除按钮添加确认对话框
|
|
||||||
const deleteButtons = document.querySelectorAll('.btn-delete');
|
|
||||||
deleteButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function(e) {
|
|
||||||
if (!confirmDelete('确认删除此配置?此操作不可恢复!')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 为所有操作按钮添加确认对话框
|
|
||||||
const actionButtons = document.querySelectorAll('.btn-success, .btn-danger, .btn-warning');
|
|
||||||
actionButtons.forEach(button => {
|
|
||||||
if (button.type === 'submit' && button.name === 'action') {
|
|
||||||
button.addEventListener('click', function(e) {
|
|
||||||
let message = '';
|
|
||||||
switch(this.value) {
|
|
||||||
case 'on':
|
|
||||||
message = '确认开机?';
|
|
||||||
break;
|
|
||||||
case 'off':
|
|
||||||
message = '确认关机?';
|
|
||||||
break;
|
|
||||||
case 'reboot':
|
|
||||||
message = '确认硬重启?';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message && !confirm(message)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('VPS Hub 页面已加载');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出函数供全局使用
|
|
||||||
window.VPSHub = {
|
|
||||||
toggleSiteUrl,
|
|
||||||
validatePassword,
|
|
||||||
confirmDelete,
|
|
||||||
refreshVpsList,
|
|
||||||
showLoading,
|
|
||||||
hideLoading
|
|
||||||
};
|
|
||||||
351
view_log.php
351
view_log.php
@ -1,351 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/app/logger.php';
|
|
||||||
require_once __DIR__ . '/app/pass.php';
|
|
||||||
|
|
||||||
// 检查是否已登录(简单验证)
|
|
||||||
session_start();
|
|
||||||
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
|
|
||||||
// 如果没有登录,显示简单的密码验证
|
|
||||||
if (isset($_POST['password'])) {
|
|
||||||
// 使用pass.php中定义的API_PASS作为密码
|
|
||||||
if ($_POST['password'] === API_PASS) {
|
|
||||||
$_SESSION['admin_logged_in'] = true;
|
|
||||||
} else {
|
|
||||||
$error = '密码错误';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_SESSION['admin_logged_in'])) {
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>VPSHUB - 日志查看</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 50px; background: #f5f5f5; }
|
|
||||||
.login-box { max-width: 400px; margin: 100px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
||||||
h2 { margin-top: 0; color: #333; }
|
|
||||||
input[type="password"] { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
|
||||||
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
||||||
button:hover { background: #0056b3; }
|
|
||||||
.error { color: red; margin-bottom: 10px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-box">
|
|
||||||
<h2>🔐 日志查看权限验证</h2>
|
|
||||||
<?php if (isset($error)): ?>
|
|
||||||
<div class="error"><?php echo htmlspecialchars($error); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<form method="POST">
|
|
||||||
<input type="password" name="password" placeholder="请输入管理员密码" required>
|
|
||||||
<button type="submit">登录</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<?php
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理操作
|
|
||||||
$action = $_GET['action'] ?? '';
|
|
||||||
if ($action === 'clear' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
Logger::clear();
|
|
||||||
header('Location: view_log.php?cleared=1');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取日志文件
|
|
||||||
$logFile = Logger::getLogFile();
|
|
||||||
$logContent = '';
|
|
||||||
$logLines = [];
|
|
||||||
|
|
||||||
if (file_exists($logFile)) {
|
|
||||||
$logContent = file_get_contents($logFile);
|
|
||||||
$logLines = array_filter(explode("\n", trim($logContent)));
|
|
||||||
// 反转数组,最新的在前面
|
|
||||||
$logLines = array_reverse($logLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤级别
|
|
||||||
$filterLevel = $_GET['level'] ?? 'ALL';
|
|
||||||
$filteredLines = $logLines;
|
|
||||||
|
|
||||||
if ($filterLevel !== 'ALL') {
|
|
||||||
$filteredLines = array_filter($logLines, function($line) use ($filterLevel) {
|
|
||||||
return strpos($line, "[{$filterLevel}]") !== false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索关键词
|
|
||||||
$search = $_GET['search'] ?? '';
|
|
||||||
if ($search) {
|
|
||||||
$filteredLines = array_filter($filteredLines, function($line) use ($search) {
|
|
||||||
return stripos($line, $search) !== false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制显示行数
|
|
||||||
$limit = intval($_GET['limit'] ?? 100);
|
|
||||||
$filteredLines = array_slice($filteredLines, 0, $limit);
|
|
||||||
|
|
||||||
$cleared = isset($_GET['cleared']);
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>VPSHUB - 日志查看器</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: #f0f2f5;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 20px 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
padding: 20px 30px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.controls select,
|
|
||||||
.controls input,
|
|
||||||
.controls button {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.controls input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.controls button {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.controls button:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
.controls button.danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
.controls button.danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
.stats {
|
|
||||||
padding: 15px 30px;
|
|
||||||
background: #fff3cd;
|
|
||||||
border-bottom: 1px solid #ffc107;
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.log-container {
|
|
||||||
padding: 20px 30px;
|
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.log-line {
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.log-line.INFO {
|
|
||||||
background: #d1ecf1;
|
|
||||||
border-left: 3px solid #17a2b8;
|
|
||||||
}
|
|
||||||
.log-line.WARNING {
|
|
||||||
background: #fff3cd;
|
|
||||||
border-left: 3px solid #ffc107;
|
|
||||||
}
|
|
||||||
.log-line.ERROR {
|
|
||||||
background: #f8d7da;
|
|
||||||
border-left: 3px solid #dc3545;
|
|
||||||
}
|
|
||||||
.log-line.DEBUG {
|
|
||||||
background: #e2e3e5;
|
|
||||||
border-left: 3px solid #6c757d;
|
|
||||||
}
|
|
||||||
.timestamp {
|
|
||||||
color: #6c757d;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.level {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
.level.INFO { background: #17a2b8; color: white; }
|
|
||||||
.level.WARNING { background: #ffc107; color: black; }
|
|
||||||
.level.ERROR { background: #dc3545; color: white; }
|
|
||||||
.level.DEBUG { background: #6c757d; color: white; }
|
|
||||||
.source {
|
|
||||||
color: #495057;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.message {
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
.empty-state svg {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
.alert {
|
|
||||||
padding: 12px 20px;
|
|
||||||
margin: 20px 30px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #d4edda;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>📋 VPSHUB 日志查看器</h1>
|
|
||||||
<div>
|
|
||||||
<span style="font-size: 14px; opacity: 0.9;">
|
|
||||||
<?php echo date('Y-m-d H:i:s'); ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($cleared): ?>
|
|
||||||
<div class="alert">✅ 日志已清空</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<select name="level" onchange="window.location.href='view_log.php?level=' + this.value + '&search=<?php echo urlencode($search); ?>&limit=<?php echo $limit; ?>'">
|
|
||||||
<option value="ALL" <?php echo $filterLevel === 'ALL' ? 'selected' : ''; ?>>全部级别</option>
|
|
||||||
<option value="INFO" <?php echo $filterLevel === 'INFO' ? 'selected' : ''; ?>>INFO</option>
|
|
||||||
<option value="WARNING" <?php echo $filterLevel === 'WARNING' ? 'selected' : ''; ?>>WARNING</option>
|
|
||||||
<option value="ERROR" <?php echo $filterLevel === 'ERROR' ? 'selected' : ''; ?>>ERROR</option>
|
|
||||||
<option value="DEBUG" <?php echo $filterLevel === 'DEBUG' ? 'selected' : ''; ?>>DEBUG</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<form method="GET" style="flex: 1; display: flex; gap: 10px;">
|
|
||||||
<input type="hidden" name="level" value="<?php echo htmlspecialchars($filterLevel); ?>">
|
|
||||||
<input type="text" name="search" placeholder="搜索日志..." value="<?php echo htmlspecialchars($search); ?>">
|
|
||||||
<button type="submit">🔍 搜索</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<select onchange="window.location.href='view_log.php?level=<?php echo urlencode($filterLevel); ?>&search=<?php echo urlencode($search); ?>&limit=' + this.value">
|
|
||||||
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50行</option>
|
|
||||||
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100行</option>
|
|
||||||
<option value="200" <?php echo $limit === 200 ? 'selected' : ''; ?>>200行</option>
|
|
||||||
<option value="500" <?php echo $limit === 500 ? 'selected' : ''; ?>>500行</option>
|
|
||||||
<option value="1000" <?php echo $limit === 1000 ? 'selected' : ''; ?>>1000行</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<form method="POST" action="view_log.php?action=clear" onsubmit="return confirm('确定要清空所有日志吗?此操作不可恢复!');">
|
|
||||||
<button type="submit" class="danger">🗑️ 清空日志</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<button onclick="location.reload()">🔄 刷新</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>总行数:</strong> <?php echo count($logLines); ?>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>显示:</strong> <?php echo count($filteredLines); ?>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>文件大小:</strong> <?php echo number_format(filesize($logFile) / 1024, 2); ?> KB
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="log-container">
|
|
||||||
<?php if (empty($filteredLines)): ?>
|
|
||||||
<div class="empty-state">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>
|
|
||||||
</svg>
|
|
||||||
<h3>暂无日志记录</h3>
|
|
||||||
<p>当系统运行时,日志将显示在这里</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($filteredLines as $line): ?>
|
|
||||||
<?php
|
|
||||||
// 解析日志行
|
|
||||||
preg_match('/\[(.*?)\] \[(.*?)\] \[(.*?)\] (.*)/', $line, $matches);
|
|
||||||
if ($matches) {
|
|
||||||
$timestamp = $matches[1];
|
|
||||||
$level = $matches[2];
|
|
||||||
$source = $matches[3];
|
|
||||||
$message = $matches[4];
|
|
||||||
} else {
|
|
||||||
$timestamp = '';
|
|
||||||
$level = 'INFO';
|
|
||||||
$source = '';
|
|
||||||
$message = $line;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="log-line <?php echo htmlspecialchars($level); ?>">
|
|
||||||
<?php if ($timestamp): ?>
|
|
||||||
<span class="timestamp">[<?php echo htmlspecialchars($timestamp); ?>]</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="level <?php echo htmlspecialchars($level); ?>"><?php echo htmlspecialchars($level); ?></span>
|
|
||||||
<?php if ($source): ?>
|
|
||||||
<span class="source">[<?php echo htmlspecialchars($source); ?>]</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="message"><?php echo htmlspecialchars($message); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
505
web/index.php
Normal file
505
web/index.php
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
<?php
|
||||||
|
// ==================== 全局配置区域 ====================
|
||||||
|
define('CONFIG_FILE', __DIR__ . '/config.php'); // 配置文件路径
|
||||||
|
define('TOKEN_CACHE_FILE', __DIR__ . '/token_cache.php'); // Token缓存文件路径
|
||||||
|
define('TOKEN_EXPIRE_TIME', 3600); // Token过期时间(秒),默认1小时
|
||||||
|
|
||||||
|
// 加载配置文件
|
||||||
|
if (file_exists(CONFIG_FILE)) {
|
||||||
|
require_once CONFIG_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要显示配置页面
|
||||||
|
$needConfig = !defined('API_PASS') || !defined('ACCOUNT') || !defined('API_KEY') ||
|
||||||
|
empty(API_PASS) || empty(ACCOUNT) || empty(API_KEY);
|
||||||
|
|
||||||
|
// 处理配置提交
|
||||||
|
if ($needConfig && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_config'])) {
|
||||||
|
$apiPass = trim($_POST['api_pass']);
|
||||||
|
$account = trim($_POST['account']);
|
||||||
|
$apiKey = trim($_POST['api_key']);
|
||||||
|
|
||||||
|
if (empty($apiPass) || empty($account) || empty($apiKey)) {
|
||||||
|
$configError = '所有字段都不能为空!';
|
||||||
|
} else {
|
||||||
|
// 生成配置文件内容
|
||||||
|
$configContent = "<?php\n";
|
||||||
|
$configContent .= "// 核云IDC VPS管理面板配置文件\n";
|
||||||
|
$configContent .= "// 生成时间: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
$configContent .= "define('API_PASS', '" . addslashes($apiPass) . "'); // API访问密码\n";
|
||||||
|
$configContent .= "define('ACCOUNT', '" . addslashes($account) . "'); // 核云IDC账号(手机号或邮箱)\n";
|
||||||
|
$configContent .= "define('API_KEY', '" . addslashes($apiKey) . "'); // 核云IDC API KEY\n";
|
||||||
|
$configContent .= "define('BASE_URL', 'https://www.heyunidc.cn/v1'); // API基础URL\n";
|
||||||
|
|
||||||
|
// 写入配置文件
|
||||||
|
if (file_put_contents(CONFIG_FILE, $configContent)) {
|
||||||
|
// 重新加载配置
|
||||||
|
require_once CONFIG_FILE;
|
||||||
|
$needConfig = false;
|
||||||
|
} else {
|
||||||
|
$configError = '配置文件写入失败,请检查目录权限!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍需配置,显示配置页面
|
||||||
|
if ($needConfig) {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
||||||
|
<link href="./static/initial.css" rel="stylesheet" type="text/css">
|
||||||
|
<title>初始配置 - VPS管理面板</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="config-container">
|
||||||
|
<div class="config-header">
|
||||||
|
<h1>🔧 初始配置</h1>
|
||||||
|
<p>请填写以下信息以开始使用</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($configError)): ?>
|
||||||
|
<div class="error-message">
|
||||||
|
❌ <?php echo htmlspecialchars($configError); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
💡 提示:这些信息将保存在 <code>config.php</code> 文件中,请妥善保管。<br>
|
||||||
|
若您需要重置或更改配置,请删除 <code>config.php</code> 文件并重新运行。<br>
|
||||||
|
配置完成后请以该格式访问服务:example.com/?pass=API_PASS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" onsubmit="return validatePassword()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="api_pass">访问密码 (API_PASS)</label>
|
||||||
|
<input type="text" id="api_pass" name="api_pass" required placeholder="设置一个安全的访问密码" pattern="[a-zA-Z0-9]+" title="只允许字母和数字">
|
||||||
|
<div class="help-text">用于保护管理面板的访问权限<br>可选:字母(a-z,A-Z)和数字(0-9)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account">核云IDC账号 (ACCOUNT)</label>
|
||||||
|
<input type="text" id="account" name="account" required placeholder="手机号或邮箱">
|
||||||
|
<div class="help-text">您的核云IDC登录账号</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="api_key">API密钥 (API_KEY)</label>
|
||||||
|
<input type="password" id="api_key" name="api_key" required placeholder="输入API KEY">
|
||||||
|
<div class="help-text">在核云IDC控制台获取的API密钥</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" name="setup_config" class="btn-submit">
|
||||||
|
✅ 保存配置并开始使用
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<center><div class="copyright">
|
||||||
|
© 2026 Gaming Master Cybersecurity |
|
||||||
|
<a href="https://git.masonliu.com/MasonLiu/HeyunIDC" target="_blank">MasonLiu In Gitea</a>
|
||||||
|
</div></center>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function validatePassword() {
|
||||||
|
var password = document.getElementById('api_pass').value;
|
||||||
|
var pattern = /^[a-zA-Z0-9]+$/;
|
||||||
|
|
||||||
|
if (!pattern.test(password)) {
|
||||||
|
alert('格式不正确!\n\n密码仅允许包含:\n• 字母 (a-z, A-Z)\n• 数字 (0-9)\n\n请勿使用特殊符号或空格');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
alert('至少输入6个字符!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Token管理函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从PHP缓存文件读取Token
|
||||||
|
*/
|
||||||
|
function getCachedToken() {
|
||||||
|
if (!file_exists(TOKEN_CACHE_FILE)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包含文件获取变量
|
||||||
|
include TOKEN_CACHE_FILE;
|
||||||
|
|
||||||
|
if (!isset($cached_token) || !isset($cached_timestamp)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Token是否过期(1小时内有效)
|
||||||
|
$currentTime = time();
|
||||||
|
$tokenAge = $currentTime - $cached_timestamp;
|
||||||
|
|
||||||
|
if ($tokenAge < TOKEN_EXPIRE_TIME) {
|
||||||
|
return $cached_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token已过期,清除缓存
|
||||||
|
@unlink(TOKEN_CACHE_FILE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存Token到PHP缓存文件
|
||||||
|
*/
|
||||||
|
function saveTokenToCache($token) {
|
||||||
|
$timestamp = time();
|
||||||
|
$expireAt = date('Y-m-d H:i:s', $timestamp + TOKEN_EXPIRE_TIME);
|
||||||
|
|
||||||
|
$content = "<?php\n";
|
||||||
|
$content .= "// Token缓存文件 - 自动生成\n";
|
||||||
|
$content .= "// 生成时间: " . date('Y-m-d H:i:s', $timestamp) . "\n";
|
||||||
|
$content .= "// 过期时间: {$expireAt}\n";
|
||||||
|
$content .= "// 警告: 不要手动修改此文件\n\n";
|
||||||
|
$content .= "\$cached_token = '" . addslashes($token) . "';\n";
|
||||||
|
$content .= "\$cached_timestamp = {$timestamp};\n";
|
||||||
|
|
||||||
|
file_put_contents(TOKEN_CACHE_FILE, $content);
|
||||||
|
|
||||||
|
// 设置文件权限(如果可能)
|
||||||
|
if (function_exists('chmod')) {
|
||||||
|
@chmod(TOKEN_CACHE_FILE, 0600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登录Token(带缓存)
|
||||||
|
*/
|
||||||
|
function getLoginToken() {
|
||||||
|
// 首先尝试从缓存获取
|
||||||
|
$cachedToken = getCachedToken();
|
||||||
|
if ($cachedToken) {
|
||||||
|
return $cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存不存在或已过期,重新获取Token
|
||||||
|
$url = BASE_URL . '/login_api';
|
||||||
|
$data = [
|
||||||
|
'account' => ACCOUNT,
|
||||||
|
'password' => API_KEY
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
$token = isset($result['jwt']) ? $result['jwt'] : null;
|
||||||
|
|
||||||
|
// 保存新Token到缓存
|
||||||
|
if ($token) {
|
||||||
|
saveTokenToCache($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 功能实现区域 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送API请求
|
||||||
|
*/
|
||||||
|
function sendApiRequest($endpoint, $method = 'GET', $data = []) {
|
||||||
|
$token = getLoginToken();
|
||||||
|
if (!$token) {
|
||||||
|
return ['status' => 500, 'msg' => '获取Token失败,请检查账号和密码配置'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = BASE_URL . $endpoint;
|
||||||
|
$headers = [
|
||||||
|
'Authorization: JWT ' . $token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
} elseif ($method === 'PUT') {
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取VPS列表
|
||||||
|
*/
|
||||||
|
function getHostList($page = 1, $limit = 100) {
|
||||||
|
return sendApiRequest("/hosts?page={$page}&limit={$limit}", 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取VPS状态
|
||||||
|
*/
|
||||||
|
function getHostStatus($hostId) {
|
||||||
|
return sendApiRequest("/hosts/{$hostId}/module/status?type=host", 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作VPS
|
||||||
|
*/
|
||||||
|
function operateHost($hostId, $operation) {
|
||||||
|
return sendApiRequest("/hosts/{$hostId}/module/{$operation}", 'PUT');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 主逻辑区域 ====================
|
||||||
|
|
||||||
|
// 验证pass参数
|
||||||
|
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
|
||||||
|
|
||||||
|
if ($pass !== API_PASS) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['status' => 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理操作请求
|
||||||
|
$action = isset($_POST['action']) ? $_POST['action'] : '';
|
||||||
|
$hostId = isset($_POST['host_id']) ? intval($_POST['host_id']) : 0;
|
||||||
|
|
||||||
|
$result = null;
|
||||||
|
if ($action && $hostId > 0) {
|
||||||
|
if ($action === 'reboot') {
|
||||||
|
$result = operateHost($hostId, 'hard_reboot');
|
||||||
|
} elseif ($action === 'on') {
|
||||||
|
$result = operateHost($hostId, 'on');
|
||||||
|
} elseif ($action === 'off') {
|
||||||
|
$result = operateHost($hostId, 'off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取VPS列表和状态
|
||||||
|
$hosts = [];
|
||||||
|
$statusMap = [];
|
||||||
|
$domainstatus = [];
|
||||||
|
$totalCount = 0;
|
||||||
|
$errorMsg = '';
|
||||||
|
|
||||||
|
$listResult = getHostList();
|
||||||
|
if (isset($listResult['status']) && $listResult['status'] === 200) {
|
||||||
|
$data = $listResult['data'];
|
||||||
|
$hosts = $data['host'];
|
||||||
|
$domainstatus = $data['domainstatus'];
|
||||||
|
$totalCount = $data['total'];
|
||||||
|
|
||||||
|
// 批量获取所有VPS的状态
|
||||||
|
foreach ($hosts as $host) {
|
||||||
|
$statusResult = getHostStatus($host['id']);
|
||||||
|
if (isset($statusResult['status']) && $statusResult['status'] === 200) {
|
||||||
|
$statusMap[$host['id']] = $statusResult['data'];
|
||||||
|
} else {
|
||||||
|
$statusMap[$host['id']] = ['status' => 'unknown', 'des' => '未知'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errorMsg = isset($listResult['msg']) ? $listResult['msg'] : '获取VPS列表失败';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
||||||
|
<link href="./static/style.css" rel="stylesheet" type="text/css">
|
||||||
|
<title>VPS管理面板</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🖥️ 核云IDC VPS管理面板</h1>
|
||||||
|
<p>实时查看和管理您的云服务器</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<?php if (!empty($errorMsg)): ?>
|
||||||
|
<div class="error-message">
|
||||||
|
<h3>❌ 加载失败</h3>
|
||||||
|
<p><?php echo htmlspecialchars($errorMsg); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<?php if (!empty($result)): ?>
|
||||||
|
<?php if (isset($result['status']) && $result['status'] === 200): ?>
|
||||||
|
<div class="success-message">
|
||||||
|
<h3>✅ 操作成功</h3>
|
||||||
|
<p>VPS #<?php echo $hostId; ?> 已成功执行操作</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="error-message">
|
||||||
|
<h3>❌ 操作失败</h3>
|
||||||
|
<p><?php echo isset($result['msg']) ? htmlspecialchars($result['msg']) : '未知错误'; ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$cachedToken = getCachedToken();
|
||||||
|
if ($cachedToken):
|
||||||
|
// 从PHP文件读取时间戳
|
||||||
|
include TOKEN_CACHE_FILE;
|
||||||
|
$remainingTime = TOKEN_EXPIRE_TIME - (time() - $cached_timestamp);
|
||||||
|
$remainingMinutes = floor($remainingTime / 60);
|
||||||
|
?>
|
||||||
|
<div class="token-info">
|
||||||
|
✅ <strong>Token缓存生效中</strong> | 剩余有效期:约 <?php echo $remainingMinutes; ?> 分钟
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="stats-bar">
|
||||||
|
<div class="stats-item">
|
||||||
|
总计:<strong><?php echo $totalCount; ?></strong> 台服务器
|
||||||
|
</div>
|
||||||
|
<div class="stats-item">
|
||||||
|
运行中:<strong><?php
|
||||||
|
$runningCount = 0;
|
||||||
|
foreach ($statusMap as $status) {
|
||||||
|
if ($status['status'] === 'on') $runningCount++;
|
||||||
|
}
|
||||||
|
echo $runningCount;
|
||||||
|
?></strong> 台
|
||||||
|
</div>
|
||||||
|
<div class="stats-item">
|
||||||
|
已关机:<strong><?php
|
||||||
|
$offCount = 0;
|
||||||
|
foreach ($statusMap as $status) {
|
||||||
|
if ($status['status'] === 'off') $offCount++;
|
||||||
|
}
|
||||||
|
echo $offCount;
|
||||||
|
?></strong> 台
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-grid">
|
||||||
|
<?php foreach ($hosts as $host):
|
||||||
|
$statusKey = $host['domainstatus'];
|
||||||
|
$statusInfo = isset($domainstatus[$statusKey]) ? $domainstatus[$statusKey] : ['name' => $statusKey, 'color' => '#999'];
|
||||||
|
$powerStatus = isset($statusMap[$host['id']]) ? $statusMap[$host['id']] : ['status' => 'unknown', 'des' => '未知'];
|
||||||
|
|
||||||
|
$regDate = date('Y-m-d', $host['regdate']);
|
||||||
|
$nextDueDate = date('Y-m-d', $host['nextduedate']);
|
||||||
|
|
||||||
|
$powerClass = 'power-unknown';
|
||||||
|
if ($powerStatus['status'] === 'on') {
|
||||||
|
$powerClass = 'power-on';
|
||||||
|
} elseif ($powerStatus['status'] === 'off') {
|
||||||
|
$powerClass = 'power-off';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="vps-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="vps-id">#<?php echo $host['id']; ?></div>
|
||||||
|
<span class="status-badge" style="background-color: <?php echo $statusInfo['color']; ?>">
|
||||||
|
<?php echo htmlspecialchars($statusInfo['name']); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">域名</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($host['domain']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">IP地址</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($host['dedicatedip']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">产品名称</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($host['product_name']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">注册日期</span>
|
||||||
|
<span class="info-value"><?php echo $regDate; ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">到期日期</span>
|
||||||
|
<span class="info-value"><?php echo $nextDueDate; ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">金额</span>
|
||||||
|
<span class="info-value">¥<?php echo htmlspecialchars($host['amount']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">计费周期</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($host['billingcycle']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="power-status">
|
||||||
|
<span class="power-indicator <?php echo $powerClass; ?>"></span>
|
||||||
|
<span class="power-text"><?php echo htmlspecialchars($powerStatus['des']); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" style="margin: 0;">
|
||||||
|
<input type="hidden" name="host_id" value="<?php echo $host['id']; ?>">
|
||||||
|
<div class="card-actions">
|
||||||
|
<button type="submit" name="action" value="on" class="btn btn-success" onclick="return confirm('确认开机?')">
|
||||||
|
⚡ 开机
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="action" value="off" class="btn btn-danger" onclick="return confirm('确认关机?')">
|
||||||
|
🔴 关机
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="action" value="reboot" class="btn btn-warning" onclick="return confirm('确认硬重启?')" style="grid-column: span 2;">
|
||||||
|
🔄 硬重启
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<center><div class="copyright">
|
||||||
|
© 2026 Gaming Master Cybersecurity |
|
||||||
|
<a href="https://git.masonliu.com/MasonLiu/HeyunIDC" target="_blank">MasonLiu In Gitea</a>
|
||||||
|
</div></center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@ -336,124 +336,3 @@ body {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* VPS Hub 新增样式 */
|
|
||||||
|
|
||||||
.config-container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 50px auto;
|
|
||||||
background: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-header h1 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus,
|
|
||||||
.form-group select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #fee;
|
|
||||||
border-left: 4px solid #dc3545;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-message {
|
|
||||||
background: #efe;
|
|
||||||
border-left: 4px solid #28a745;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
background: #e7f3ff;
|
|
||||||
border-left: 4px solid #007bff;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #0056b3;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box code {
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 20px;
|
|
||||||
color: #999;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
69
新开发文档.md
69
新开发文档.md
@ -1,69 +0,0 @@
|
|||||||
### WEB运行
|
|
||||||
|
|
||||||
#### 配置文件 config_add.php
|
|
||||||
|
|
||||||
用户可访问config_add.php添加VPS配置项
|
|
||||||
|
|
||||||
初次访问该php页面时,用户需设置网页自定义访问密码:API_PASS(运行输入大小写字母以及数字,最低8位数)
|
|
||||||
|
|
||||||
存在API_PASS时,需跟参数?pass=API_PASS才允许访问
|
|
||||||
|
|
||||||
用户点击添加配置按钮,新列出一列,分别是:
|
|
||||||
旧数据也会读取并展示
|
|
||||||
序号(自动生成),网站类型(魔方、阿里云、腾讯云等),网站链接(若为魔方则设置为可填,腾讯云为https://cvm.tencentcloudapi.com/,阿里云后面再写),账户(魔方账户:邮箱或手机号,其他云后续添加),密钥(魔方输入API KEY),是否开启自动开机监控(魔方默认开启,其他云默认关闭)
|
|
||||||
|
|
||||||
1. 用户点击添加数据后,在./app/db/文件夹内生成sqlite数据库 vps.db用于存储该类数据
|
|
||||||
|
|
||||||
2. 首次添加后,自动调用所有API进行VPS列表查询,查询结果记录在./app/db中的vpslist.db中,之后此类查询每天仅需查询一次(是否开启自动开机监控标记为开启时,vps的最后一列section也标记为True)
|
|
||||||
|
|
||||||
3. 进行ping访问测试后,将结果记录于./app/db/中的status.db中,保留最近三十天数据
|
|
||||||
|
|
||||||
记录项:目标(IP地址或域名),状态(正常或异常),延时(单位ms 毫秒)
|
|
||||||
每天0点时,总结前一天中各VPS的ping响应数据,记录在该数据库中的新表 vps_summary 中。该表需包含以下字段:VPS标识(vps_id)、统计日期(date)、平均延迟(avg_latency_ms)、最大延迟(max_latency_ms)、最小延迟(min_latency_ms)。同时分级记录延迟分布情况:延迟低于100ms的次数(count_under_100)、100ms~300ms的次数(count_100_to_300)、300ms~500ms的次数(count_300_to_500),以及延迟超过500ms或ping异常的丢包/超时次数(count_abnormal)。根据延迟表现计算高可用评分:100ms以内权重最高,300ms以内为良好,500ms以内为可用,超过500ms标记为低可用,异常则标记为不可用。记录为新字段:服务器可用性(优秀/良好/一般/不可用)
|
|
||||||
|
|
||||||
4. 首次使用网页并添加数据后,调用./app/install.sh进行system服务注册
|
|
||||||
|
|
||||||
#### 首页 index.php
|
|
||||||
|
|
||||||
1. 查询是否存在API_PASS,若存在,则需跟参数?pass=API_PASS才能访问
|
|
||||||
|
|
||||||
2. 若不存在,则跳转至config_add.php页
|
|
||||||
|
|
||||||
(获取到的认证token存储于./app/token.php中,记录为两小时,后续调用API时超时或提示失效再重新获取)
|
|
||||||
|
|
||||||
3. 右上角放置“添加配置源”的按钮
|
|
||||||
|
|
||||||
4. 查询vps.db,并依次获取vps列表并查询其状态信息(使用v1/hosts/:id接口获取VPS的CPU核数,操作系统,内存大小,系统盘大小以及带宽大小),提供开机、关机、硬重启按钮
|
|
||||||
|
|
||||||
5. 左上角新建一个按钮,用于手动再次获取vps列表以及信息,不再自动获取信息。
|
|
||||||
|
|
||||||
#### 魔方页 mofangidc.php
|
|
||||||
|
|
||||||
所有智简魔方的操作API以及相关函数存储在此网页下
|
|
||||||
|
|
||||||
### Python运行
|
|
||||||
|
|
||||||
#### 安装
|
|
||||||
|
|
||||||
通过install.sh自动安装
|
|
||||||
|
|
||||||
功能点:
|
|
||||||
|
|
||||||
1. 探测当前系统是否存在idc_monitor的系统服务,若不存在则进入第二步,若存在则重启一下该服务
|
|
||||||
|
|
||||||
2. 读取python版本并探测是否存在pip,自动下载python-pip
|
|
||||||
|
|
||||||
3. pip下载所需环境和依赖项
|
|
||||||
|
|
||||||
4. 新建一个idc_system的系统服务,持续化运行当前路径下的monitor.py脚本
|
|
||||||
|
|
||||||
#### 读取配置
|
|
||||||
|
|
||||||
1. 读取vpslist中的数据,则对其进行ping测试
|
|
||||||
|
|
||||||
2. 测试结果保存至status.db中
|
|
||||||
|
|
||||||
(获取到的认证token存储于./app/token.php中,记录为两小时)
|
|
||||||
|
|
||||||
3. 若section为True,且ping测试失败,调用API对该机器进行状态查询,若返回状态为关机,则尝试开机,隔60秒后再进行状态查询并再次尝试开机,60秒后查询还是为异常的话,则记录异常,进入下一个循环(多台机器关机时,依次进行状态查询和开机,然后间隔60秒再进行操作,不同的机器不需要间隔)
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user