- 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
340 lines
8.5 KiB
Go
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"})
|
|
}
|
|
|