Files
lan-manager/server/handlers/pve.go
shirainbown 6f507b319e feat: merge PVE VM management + local features
- Merge PVE (Proxmox VE) virtual machine management from remote
  - PVE host CRUD API
  - VM status query / start / stop operations
  - PVE database tables and models
  - Frontend VM status display and control buttons
  - SPA routing fix for nested asset paths

- Keep local features:
  - Change password functionality
  - Log cleanup service
  - Docker containerization (Dockerfile + docker-compose)
  - Empty machine ping log spam fix
  - settings table for admin password storage

- Update machines handlers to support PVE fields
- Update frontend API client with PVE endpoints
2026-06-18 22:33:45 +08:00

340 lines
8.5 KiB
Go

package handlers
import (
"database/sql"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"lan-manager/server/db"
"lan-manager/server/middleware"
"lan-manager/server/models"
"lan-manager/server/services"
)
type PVEHandler struct {
service *services.PVEHostService
}
func NewPVEHandler() *PVEHandler {
return &PVEHandler{
service: services.NewPVEHostService(),
}
}
// List 获取 PVE 主机列表
func (h *PVEHandler) List(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
hosts, err := h.service.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 密码不返回给前端
for i := range hosts {
hosts[i].PasswordEnc = ""
}
c.JSON(http.StatusOK, hosts)
}
// Get 获取单个 PVE 主机
func (h *PVEHandler) Get(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
host, err := h.service.GetByID(id)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
host.PasswordEnc = ""
host.Password = ""
c.JSON(http.StatusOK, host)
}
// Create 创建 PVE 主机
func (h *PVEHandler) Create(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
var host models.PVEHost
if err := c.ShouldBindJSON(&host); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if host.Name == "" || host.Hostname == "" || host.Username == "" || host.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing required fields"})
return
}
if host.Port == 0 {
host.Port = 8006
}
if err := h.service.Create(&host); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
idPtr := host.ID
middleware.LogOperation("create", "pve_host", &idPtr, host.Name, "", host.Hostname+":"+strconv.Itoa(host.Port), c.ClientIP(), middleware.CurrentUser(c))
host.PasswordEnc = ""
host.Password = ""
c.JSON(http.StatusCreated, host)
}
// Update 更新 PVE 主机
func (h *PVEHandler) Update(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var host models.PVEHost
if err := c.ShouldBindJSON(&host); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
host.ID = id
// 如果密码为空,说明不更新密码
if host.Password == "" {
existing, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
host.PasswordEnc = existing.PasswordEnc
}
if err := h.service.Update(&host); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
middleware.LogOperation("update", "pve_host", &id, host.Name, "", host.Hostname+":"+strconv.Itoa(host.Port), c.ClientIP(), middleware.CurrentUser(c))
host.PasswordEnc = ""
c.JSON(http.StatusOK, host)
}
// Delete 删除 PVE 主机
func (h *PVEHandler) Delete(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
// 获取主机名用于日志
host, _ := h.service.GetByID(id)
hostName := ""
if host != nil {
hostName = host.Name
}
if err := h.service.Delete(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
middleware.LogOperation("delete", "pve_host", &id, hostName, "", "", c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// HostStatus 检测 PVE 节点连接状态
func (h *PVEHandler) HostStatus(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
status, err := h.service.GetHostStatus(id)
if err != nil {
c.JSON(http.StatusOK, gin.H{"status": "offline", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": status})
}
// HostVMList 获取 PVE 节点上的虚拟机列表
func (h *PVEHandler) HostVMList(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
vms, err := h.service.GetHostVMList(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, vms)
}
// VMStatus 获取虚拟机状态
func (h *PVEHandler) VMStatus(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
machineID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var pveHostID *int64
var pveVMID sql.NullString
err = db.DB.QueryRow(`SELECT pve_host_id, pve_vmid FROM machines WHERE id = ?`, machineID).Scan(&pveHostID, &pveVMID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "machine not found"})
return
}
if pveHostID == nil || !pveVMID.Valid || pveVMID.String == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "machine not linked to PVE VM"})
return
}
status, err := h.service.GetVMStatus(*pveHostID, pveVMID.String)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, status)
}
// VMStart 启动虚拟机
func (h *PVEHandler) VMStart(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
machineID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var pveHostID *int64
var pveVMID sql.NullString
err = db.DB.QueryRow(`SELECT pve_host_id, pve_vmid FROM machines WHERE id = ?`, machineID).Scan(&pveHostID, &pveVMID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "machine not found"})
return
}
if pveHostID == nil || !pveVMID.Valid || pveVMID.String == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "machine not linked to PVE VM"})
return
}
if err := h.service.StartVM(*pveHostID, pveVMID.String); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 记录操作日志
machineName := ""
db.DB.QueryRow("SELECT hostname FROM machines WHERE id = ?", machineID).Scan(&machineName)
idPtr := machineID
middleware.LogOperation("start", "vm", &idPtr, machineName, "", "vmid:"+pveVMID.String, c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, gin.H{"message": "started"})
}
// VMStop 停止虚拟机
func (h *PVEHandler) VMStop(c *gin.Context) {
if !middleware.IsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
machineID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var pveHostID *int64
var pveVMID sql.NullString
err = db.DB.QueryRow(`SELECT pve_host_id, pve_vmid FROM machines WHERE id = ?`, machineID).Scan(&pveHostID, &pveVMID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "machine not found"})
return
}
if pveHostID == nil || !pveVMID.Valid || pveVMID.String == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "machine not linked to PVE VM"})
return
}
if err := h.service.StopVM(*pveHostID, pveVMID.String); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 记录操作日志
machineName := ""
db.DB.QueryRow("SELECT hostname FROM machines WHERE id = ?", machineID).Scan(&machineName)
idPtr := machineID
middleware.LogOperation("stop", "vm", &idPtr, machineName, "", "vmid:"+pveVMID.String, c.ClientIP(), middleware.CurrentUser(c))
c.JSON(http.StatusOK, gin.H{"message": "stopped"})
}