Files
lan-manager/server/services/pve.go
openclaw 8b80c2dd1a fix(pve): 修复编译错误,确保前后端可编译通过
- services/pve.go: 导入 db 包,DB -> db.DB
- handlers/pve.go: 重写为 Gin 风格(原为标准库 net/http)
- go.mod/go.sum: go mod tidy 更新依赖
- web/dist: 重新构建前端包含 PVEHosts 页面

[金渐层/K2.6-code-preview🐾]
2026-04-20 13:16:01 +08:00

427 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"strings"
"time"
"lan-manager/server/db"
"lan-manager/server/models"
"lan-manager/server/utils"
)
type PVEClient struct {
Host string
Port int
Username string
Password string
VerifySSL bool
CSRFToken string
Cookie string
httpClient *http.Client
}
func NewPVEClient(host *models.PVEHost, password string) *PVEClient {
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
Timeout: 30 * time.Second,
}
if !host.VerifySSL {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
return &PVEClient{
Host: host.Hostname,
Port: host.Port,
Username: host.Username,
Password: password,
VerifySSL: host.VerifySSL,
httpClient: client,
}
}
func (c *PVEClient) baseURL() string {
scheme := "https"
if !c.VerifySSL {
scheme = "http"
}
return fmt.Sprintf("%s://%s:%d", scheme, c.Host, c.Port)
}
// Login 获取 PVE 认证ticket
func (c *PVEClient) Login() error {
loginURL := c.baseURL() + "/api2/json/access/ticket"
data := fmt.Sprintf("username=%s&password=%s", c.Username, c.Password)
req, err := http.NewRequest("POST", loginURL, strings.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("login failed, status: %d", resp.StatusCode)
}
var result struct {
Data struct {
CSRFToken string `json:"CSRFPreventionToken"`
Cookie string `json:"PVEAuthCookie"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
c.CSRFToken = result.Data.CSRFToken
c.Cookie = result.Data.Cookie
return nil
}
// GetVMStatus 获取虚拟机状态
func (c *PVEClient) GetVMStatus(vmid string) (*models.PVEVMStatus, error) {
if c.CSRFToken == "" {
if err := c.Login(); err != nil {
return nil, err
}
}
// 获取 node 名称(通常是 pve
statusURL := c.baseURL() + "/api2/json/nodes/pve/qemu/" + vmid + "/status"
req, err := http.NewRequest("GET", statusURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Cookie", c.Cookie)
req.Header.Set("CSRFPreventionToken", c.CSRFToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
// token 过期,重新登录
if err := c.Login(); err != nil {
return nil, err
}
return c.GetVMStatus(vmid)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("get vm status failed, status: %d", resp.StatusCode)
}
var result struct {
Data struct {
Status string `json:"status"`
Uptime int64 `json:"uptime"`
CPU float64 `json:"cpu"`
MemoryUsed int64 `json:"mem"`
MemoryTotal int64 `json:"maxmem"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("decode failed: %v, body: %s", err, string(body))
}
return &models.PVEVMStatus{
Status: result.Data.Status,
Uptime: result.Data.Uptime,
CPU: result.Data.CPU * 100,
MemoryUsed: result.Data.MemoryUsed,
MemoryTotal: result.Data.MemoryTotal,
}, nil
}
// StartVM 启动虚拟机
func (c *PVEClient) StartVM(vmid string) error {
if c.CSRFToken == "" {
if err := c.Login(); err != nil {
return err
}
}
startURL := c.baseURL() + "/api2/json/nodes/pve/qemu/" + vmid + "/status/start"
req, err := http.NewRequest("POST", startURL, nil)
if err != nil {
return err
}
req.Header.Set("Cookie", c.Cookie)
req.Header.Set("CSRFPreventionToken", c.CSRFToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
if err := c.Login(); err != nil {
return err
}
return c.StartVM(vmid)
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("start vm failed, status: %d, body: %s", resp.StatusCode, string(body))
}
return nil
}
// StopVM 停止虚拟机
func (c *PVEClient) StopVM(vmid string) error {
if c.CSRFToken == "" {
if err := c.Login(); err != nil {
return err
}
}
stopURL := c.baseURL() + "/api2/json/nodes/pve/qemu/" + vmid + "/status/stop"
req, err := http.NewRequest("POST", stopURL, nil)
if err != nil {
return err
}
req.Header.Set("Cookie", c.Cookie)
req.Header.Set("CSRFPreventionToken", c.CSRFToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
if err := c.Login(); err != nil {
return err
}
return c.StopVM(vmid)
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("stop vm failed, status: %d, body: %s", resp.StatusCode, string(body))
}
return nil
}
// GetVMList 获取虚拟机列表
func (c *PVEClient) GetVMList() ([]struct {
VMID string `json:"vmid"`
Name string `json:"name"`
Status string `json:"status"`
}, error) {
if c.CSRFToken == "" {
if err := c.Login(); err != nil {
return nil, err
}
}
listURL := c.baseURL() + "/api2/json/cluster/resources?type=vm"
req, err := http.NewRequest("GET", listURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Cookie", c.Cookie)
req.Header.Set("CSRFPreventionToken", c.CSRFToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("get vm list failed, status: %d", resp.StatusCode)
}
var result struct {
Data []struct {
VMID string `json:"vmid"`
Name string `json:"name"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Data, nil
}
// PVEHostService PVE 主机服务
type PVEHostService struct{}
func NewPVEHostService() *PVEHostService {
return &PVEHostService{}
}
// GetAll 获取所有 PVE 主机
func (s *PVEHostService) GetAll() ([]models.PVEHost, error) {
rows, err := db.DB.Query(`SELECT id, name, hostname, port, username, password_enc, verify_ssl, created_at, updated_at FROM pve_hosts ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var hosts []models.PVEHost
for rows.Next() {
var h models.PVEHost
var verifySSL int
if err := rows.Scan(&h.ID, &h.Name, &h.Hostname, &h.Port, &h.Username, &h.PasswordEnc, &verifySSL, &h.CreatedAt, &h.UpdatedAt); err != nil {
return nil, err
}
h.VerifySSL = verifySSL == 1
hosts = append(hosts, h)
}
return hosts, nil
}
// GetByID 根据 ID 获取 PVE 主机
func (s *PVEHostService) GetByID(id int64) (*models.PVEHost, error) {
var h models.PVEHost
var verifySSL int
err := db.DB.QueryRow(`SELECT id, name, hostname, port, username, password_enc, verify_ssl, created_at, updated_at FROM pve_hosts WHERE id = ?`, id).
Scan(&h.ID, &h.Name, &h.Hostname, &h.Port, &h.Username, &h.PasswordEnc, &verifySSL, &h.CreatedAt, &h.UpdatedAt)
if err != nil {
return nil, err
}
h.VerifySSL = verifySSL == 1
return &h, nil
}
// Create 创建 PVE 主机
func (s *PVEHostService) Create(host *models.PVEHost) error {
// 加密密码
encPassword, err := utils.Encrypt(host.Password)
if err != nil {
return fmt.Errorf("encrypt password failed: %w", err)
}
result, err := db.DB.Exec(`INSERT INTO pve_hosts (name, hostname, port, username, password_enc, verify_ssl) VALUES (?, ?, ?, ?, ?, ?)`,
host.Name, host.Hostname, host.Port, host.Username, encPassword, boolToInt(host.VerifySSL))
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
host.ID = id
return nil
}
// Update 更新 PVE 主机
func (s *PVEHostService) Update(host *models.PVEHost) error {
if host.Password != "" {
// 密码有更新,重新加密
encPassword, err := utils.Encrypt(host.Password)
if err != nil {
return fmt.Errorf("encrypt password failed: %w", err)
}
_, err = db.DB.Exec(`UPDATE pve_hosts SET name=?, hostname=?, port=?, username=?, password_enc=?, verify_ssl=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`,
host.Name, host.Hostname, host.Port, host.Username, encPassword, boolToInt(host.VerifySSL), host.ID)
return err
}
_, err := db.DB.Exec(`UPDATE pve_hosts SET name=?, hostname=?, port=?, username=?, verify_ssl=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`,
host.Name, host.Hostname, host.Port, host.Username, boolToInt(host.VerifySSL), host.ID)
return err
}
// Delete 删除 PVE 主机
func (s *PVEHostService) Delete(id int64) error {
// 先清除关联机器的 PVE 字段
if _, err := db.DB.Exec(`UPDATE machines SET pve_host_id = NULL, pve_vmid = NULL WHERE pve_host_id = ?`, id); err != nil {
return err
}
_, err := db.DB.Exec(`DELETE FROM pve_hosts WHERE id = ?`, id)
return err
}
// GetDecryptedPassword 获取解密后的密码
func (s *PVEHostService) GetDecryptedPassword(host *models.PVEHost) (string, error) {
return utils.Decrypt(host.PasswordEnc)
}
// GetVMStatus 获取虚拟机状态
func (s *PVEHostService) GetVMStatus(hostID int64, vmid string) (*models.PVEVMStatus, error) {
host, err := s.GetByID(hostID)
if err != nil {
return nil, err
}
password, err := s.GetDecryptedPassword(host)
if err != nil {
return nil, err
}
client := NewPVEClient(host, password)
return client.GetVMStatus(vmid)
}
// StartVM 启动虚拟机
func (s *PVEHostService) StartVM(hostID int64, vmid string) error {
host, err := s.GetByID(hostID)
if err != nil {
return err
}
password, err := s.GetDecryptedPassword(host)
if err != nil {
return err
}
client := NewPVEClient(host, password)
return client.StartVM(vmid)
}
// StopVM 停止虚拟机
func (s *PVEHostService) StopVM(hostID int64, vmid string) error {
host, err := s.GetByID(hostID)
if err != nil {
return err
}
password, err := s.GetDecryptedPassword(host)
if err != nil {
return err
}
client := NewPVEClient(host, password)
return client.StopVM(vmid)
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}