diff --git a/cmd/server/main.go b/cmd/server/main.go
index 5aaed735b956495aa746732a6c2356b9590691b5..f1ec227cee34e25dcfd041f1bdaf199d8d27146c 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -17,6 +17,7 @@ import (
"virt-manager-go/internal/api"
"virt-manager-go/internal/config"
+ "virt-manager-go/internal/console"
"virt-manager-go/internal/domain"
"virt-manager-go/internal/service"
@@ -68,10 +69,22 @@ func main() {
cfg = config.Default()
}
- // 初始化服务
+ // 初始化所有服务
connMgr := service.NewConnectionService(logger)
vmService := service.NewVMService(connMgr, logger)
-
+ storageService := service.NewStoragePoolService(connMgr, logger)
+ networkService := service.NewNetworkService(connMgr, logger)
+ snapshotService := service.NewSnapshotService(connMgr, vmService, logger)
+ migrationService := service.NewMigrationService(connMgr, logger)
+ batchService := service.NewBatchService(vmService, snapshotService, logger)
+ eventService := service.NewEventService(connMgr, logger)
+
+ // 初始化控制台代理
+ serialProxy := console.NewSerialProxy(connMgr, logger)
+ // graphicsProxy := console.NewGraphicsProxy(connMgr, logger)
+
+ // 自动连接本地 libvirt
+ ctx := context.Background()
// 自动连接本地 libvirt(可选)
if cfg.Libvirt.AutoConnect {
ctx := context.Background()
@@ -83,11 +96,25 @@ func main() {
"connection_id": connID,
"uri": cfg.Libvirt.DefaultURI,
}).Info("Auto-connected to libvirt")
+ // 启动事件监听
+ if err := eventService.StartListening(ctx, connID); err != nil {
+ logger.WithError(err).Warn("Failed to start event listening")
+ }
}
}
- // 创建路由
- router := api.NewRouter(vmService, connMgr, logger)
+ router := api.NewRouter(
+ vmService,
+ storageService,
+ networkService,
+ snapshotService,
+ migrationService,
+ batchService,
+ eventService,
+ serialProxy,
+ connMgr,
+ logger,
+ )
// 注册Swagger路由
router.Engine().GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
diff --git a/docs/API_EXAMPLES.md b/docs/API_EXAMPLES.md
new file mode 100644
index 0000000000000000000000000000000000000000..a244b35ad53971ac4585d66b02e52365ab60351a
--- /dev/null
+++ b/docs/API_EXAMPLES.md
@@ -0,0 +1,690 @@
+# virt-manager-go 完整 API 使用示例
+
+## 目录
+- [存储池管理](#存储池管理)
+- [虚拟网络管理](#虚拟网络管理)
+- [快照管理](#快照管理)
+- [虚拟机迁移](#虚拟机迁移)
+- [批量操作](#批量操作)
+- [事件通知](#事件通知)
+- [VNC/SPICE 控制台](#图形控制台)
+
+---
+
+## 存储池管理
+
+### 1. 创建目录存储池
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/storage/pools \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "default-pool",
+ "type": "dir",
+ "target": {
+ "path": "/var/lib/libvirt/images",
+ "permissions": {
+ "mode": "0755",
+ "owner": 0,
+ "group": 0
+ }
+ },
+ "autostart": true
+ }'
+```
+
+### 2. 创建 NFS 网络存储池
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/storage/pools \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "nfs-pool",
+ "type": "netfs",
+ "source": {
+ "host": "192.168.1.100",
+ "dir": "/export/vms",
+ "format": {
+ "type": "nfs"
+ }
+ },
+ "target": {
+ "path": "/mnt/nfs-storage"
+ }
+ }'
+```
+
+### 3. 列出所有存储池
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/storage/pools
+```
+
+### 4. 创建存储卷(虚拟磁盘)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/storage/pools/default-pool/volumes \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "vm-disk-01.qcow2",
+ "capacity": 107374182400,
+ "format": "qcow2"
+ }'
+```
+
+### 5. 克隆存储卷
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/storage/pools/default-pool/volumes/vm-disk-01.qcow2/clone \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "vm-disk-02.qcow2"
+ }'
+```
+
+### 6. 调整存储卷大小
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/storage/pools/default-pool/volumes/vm-disk-01.qcow2/resize \
+ -H "Content-Type: application/json" \
+ -d '{
+ "new_size": 214748364800
+ }'
+```
+
+---
+
+## 虚拟网络管理
+
+### 1. 创建 NAT 网络
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/networks \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "default",
+ "bridge": "virbr0",
+ "forward": "nat",
+ "domain": "local",
+ "ip": [
+ {
+ "address": "192.168.122.1",
+ "netmask": "255.255.255.0",
+ "dhcp": {
+ "start": "192.168.122.2",
+ "end": "192.168.122.254",
+ "hosts": [
+ {
+ "mac": "52:54:00:11:22:33",
+ "name": "server1",
+ "ip": "192.168.122.10"
+ }
+ ]
+ }
+ }
+ ],
+ "autostart": true
+ }'
+```
+
+### 2. 创建隔离网络
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/networks \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "isolated-net",
+ "bridge": "virbr1",
+ "forward": "none",
+ "ip": [
+ {
+ "address": "10.0.0.1",
+ "netmask": "255.255.255.0",
+ "dhcp": {
+ "start": "10.0.0.10",
+ "end": "10.0.0.100"
+ }
+ }
+ ]
+ }'
+```
+
+### 3. 列出所有网络
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/networks
+```
+
+### 4. 获取 DHCP 租约
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/networks/default/dhcp-leases
+```
+
+### 5. 启动/停止网络
+
+```bash
+# 启动网络
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/networks/default/start
+
+# 停止网络
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/networks/default/stop
+```
+
+---
+
+## 快照管理
+
+### 1. 创建磁盘快照(虚拟机运行时)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "before-update",
+ "description": "Snapshot before system update",
+ "memory": false,
+ "disk_only": true,
+ "quiesce": true
+ }'
+```
+
+### 2. 创建内存快照(包含 RAM 状态)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "live-snapshot",
+ "description": "Live snapshot with memory",
+ "memory": true,
+ "quiesce": false
+ }'
+```
+
+### 3. 列出所有快照
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots
+```
+
+### 4. 恢复到快照
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots/before-update/revert
+```
+
+### 5. 删除快照
+
+```bash
+curl -X DELETE http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots/before-update
+```
+
+### 6. 获取当前快照
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/snapshots/current
+```
+
+---
+
+## 虚拟机迁移
+
+### 1. 实时迁移(Live Migration)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/migrate \
+ -H "Content-Type: application/json" \
+ -d '{
+ "dest_uri": "qemu+ssh://user@192.168.1.200/system",
+ "config": {
+ "live": true,
+ "persistent": true,
+ "undefine": false,
+ "compressed": true,
+ "bandwidth": 100,
+ "auto_converge": true,
+ "timeout": "30m"
+ }
+ }'
+
+# 响应
+{
+ "data": {
+ "id": "mig-12345-67890",
+ "vm_name": "ubuntu-vm",
+ "source_uri": "qemu:///system",
+ "dest_uri": "qemu+ssh://user@192.168.1.200/system",
+ "status": "pending",
+ "progress": 0,
+ "start_time": "2025-10-28T10:00:00Z"
+ }
+}
+```
+
+### 2. 获取迁移状态
+
+```bash
+curl http://localhost:8080/api/v1/migrations/mig-12345-67890
+
+# 响应
+{
+ "data": {
+ "id": "mig-12345-67890",
+ "status": "running",
+ "progress": 45.5,
+ "data_remaining": 2147483648,
+ "data_total": 4294967296,
+ "mem_remaining": 536870912,
+ "mem_total": 1073741824
+ }
+}
+```
+
+### 3. 取消迁移
+
+```bash
+curl -X POST http://localhost:8080/api/v1/migrations/mig-12345-67890/cancel
+```
+
+### 4. 离线迁移(通过共享存储)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/vms/ubuntu-vm/migrate \
+ -H "Content-Type: application/json" \
+ -d '{
+ "dest_uri": "qemu+ssh://user@192.168.1.200/system",
+ "config": {
+ "live": false,
+ "offline": true,
+ "shared_storage": true,
+ "persistent": true
+ }
+ }'
+```
+
+---
+
+## 批量操作
+
+### 1. 批量启动虚拟机
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/batch/vms/start \
+ -H "Content-Type: application/json" \
+ -d '{
+ "vm_names": ["vm-01", "vm-02", "vm-03", "vm-04", "vm-05"]
+ }'
+
+# 响应
+{
+ "data": {
+ "batch_id": "batch-abc123",
+ "total": 5,
+ "successful": 4,
+ "failed": 1,
+ "results": [
+ {"target": "vm-01", "success": true},
+ {"target": "vm-02", "success": true},
+ {"target": "vm-03", "success": false, "error": "VM not found"},
+ {"target": "vm-04", "success": true},
+ {"target": "vm-05", "success": true}
+ ],
+ "duration": "5.2s"
+ }
+}
+```
+
+### 2. 批量停止虚拟机(强制)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/batch/vms/stop \
+ -H "Content-Type: application/json" \
+ -d '{
+ "vm_names": ["vm-01", "vm-02", "vm-03"],
+ "force": true
+ }'
+```
+
+### 3. 批量创建快照
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/batch/snapshots/create \
+ -H "Content-Type: application/json" \
+ -d '{
+ "vm_names": ["vm-01", "vm-02", "vm-03"],
+ "config": {
+ "name": "backup-2025-10-28",
+ "description": "Daily backup snapshot",
+ "memory": false,
+ "disk_only": true,
+ "quiesce": true
+ }
+ }'
+```
+
+### 4. 批量更新虚拟机配置
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/batch/vms/update \
+ -H "Content-Type: application/json" \
+ -d '{
+ "updates": [
+ {
+ "vm_name": "vm-01",
+ "config": {
+ "cpus": 4,
+ "memory": 8192
+ }
+ },
+ {
+ "vm_name": "vm-02",
+ "config": {
+ "cpus": 2,
+ "memory": 4096
+ }
+ }
+ ]
+ }'
+```
+
+### 5. 批量删除虚拟机
+
+```bash
+curl -X POST http://localhost:8080/api/v1/connections/{conn_id}/batch/vms/delete \
+ -H "Content-Type: application/json" \
+ -d '{
+ "vm_names": ["old-vm-01", "old-vm-02"],
+ "remove_storage": true
+ }'
+```
+
+### 6. 获取批量操作状态
+
+```bash
+curl http://localhost:8080/api/v1/connections/{conn_id}/batch/status/batch-abc123
+```
+
+---
+
+## 事件通知
+
+### 1. 启动事件监听
+
+```bash
+curl -X POST http://localhost:8080/api/v1/events/connections/{conn_id}/listen
+```
+
+### 2. 订阅事件流(Server-Sent Events)
+
+```bash
+# 使用 curl
+curl -N http://localhost:8080/api/v1/events/subscribe \
+ -H "Content-Type: application/json" \
+ -d '{
+ "types": ["vm", "network"],
+ "categories": ["lifecycle"],
+ "sources": ["conn-id-12345"]
+ }'
+
+# 输出示例
+data: {"id":"evt-001","type":"vm","category":"lifecycle","source":"conn-id-12345","target":"ubuntu-vm","action":"started","timestamp":"2025-10-28T10:00:00Z"}
+
+data: {"id":"evt-002","type":"vm","category":"lifecycle","source":"conn-id-12345","target":"centos-vm","action":"stopped","timestamp":"2025-10-28T10:05:00Z"}
+```
+
+### 3. JavaScript 客户端示例
+
+```javascript
+// 订阅事件
+const eventSource = new EventSource('/api/v1/events/subscribe');
+
+eventSource.addEventListener('event', (e) => {
+ const event = JSON.parse(e.data);
+ console.log('Event received:', event);
+
+ // 处理不同类型的事件
+ switch(event.type) {
+ case 'vm':
+ handleVMEvent(event);
+ break;
+ case 'network':
+ handleNetworkEvent(event);
+ break;
+ case 'storage':
+ handleStorageEvent(event);
+ break;
+ }
+});
+
+eventSource.onerror = (error) => {
+ console.error('EventSource error:', error);
+};
+
+function handleVMEvent(event) {
+ if (event.action === 'started') {
+ updateVMStatus(event.target, 'running');
+ } else if (event.action === 'stopped') {
+ updateVMStatus(event.target, 'shutoff');
+ }
+}
+```
+
+### 4. 获取历史事件
+
+```bash
+# 获取最近 100 个事件
+curl http://localhost:8080/api/v1/events/history?limit=100
+
+# 使用过滤器
+curl -X POST http://localhost:8080/api/v1/events/history \
+ -H "Content-Type: application/json" \
+ -d '{
+ "types": ["vm"],
+ "actions": ["started", "stopped"],
+ "start_time": "2025-10-28T00:00:00Z",
+ "end_time": "2025-10-28T23:59:59Z"
+ }'
+```
+
+---
+
+## 图形控制台
+
+### 1. 获取 VNC 连接信息
+
+```bash
+curl http://localhost:8080/api/v1/console/vnc/{conn_id}/ubuntu-vm/info
+
+# 响应
+{
+ "data": {
+ "proxy_url": "vnc://192.168.122.1:5900",
+ "ws_path": "/api/v1/console/vnc/{conn_id}/ubuntu-vm?token=abc123...",
+ "token": "abc123...",
+ "expires_at": "2025-10-28T11:00:00Z"
+ }
+}
+```
+
+### 2. VNC WebSocket 客户端示例(noVNC)
+
+```html
+
+
+
+ VNC Console
+
+
+
+
+
+
+
+```
+
+### 3. SPICE WebSocket 客户端示例
+
+```javascript
+const spiceUrl = `ws://localhost:8080/api/v1/console/spice/${connId}/${vmName}?token=${token}`;
+const ws = new WebSocket(spiceUrl);
+
+ws.binaryType = 'arraybuffer';
+
+ws.onopen = () => {
+ console.log('SPICE connected');
+};
+
+ws.onmessage = (event) => {
+ // 处理 SPICE 协议数据
+ const data = new Uint8Array(event.data);
+ processSpiceData(data);
+};
+
+ws.onerror = (error) => {
+ console.error('SPICE error:', error);
+};
+```
+
+### 4. 串行控制台 WebSocket 示例
+
+```javascript
+const serialUrl = `ws://localhost:8080/api/v1/console/serial/${connId}/${vmName}`;
+const ws = new WebSocket(serialUrl);
+
+// 连接终端模拟器(如 xterm.js)
+const term = new Terminal();
+term.open(document.getElementById('terminal'));
+
+ws.onmessage = (event) => {
+ term.write(event.data);
+};
+
+term.onData((data) => {
+ ws.send(data);
+});
+```
+
+---
+
+## 高级功能示例
+
+### 1. 自动化部署流程
+
+```bash
+#!/bin/bash
+# 自动化创建和配置虚拟机
+
+CONN_ID="your-connection-id"
+BASE_URL="http://localhost:8080/api/v1"
+
+# 1. 创建存储卷
+curl -X POST "$BASE_URL/connections/$CONN_ID/storage/pools/default/volumes" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "web-server-disk.qcow2",
+ "capacity": 53687091200,
+ "format": "qcow2"
+ }'
+
+# 2. 创建虚拟机
+VM_RESPONSE=$(curl -X POST "$BASE_URL/connections/$CONN_ID/vms" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "web-server-01",
+ "memory": 4096,
+ "cpus": 2,
+ "install_iso": "/var/lib/libvirt/images/ubuntu-22.04.iso",
+ "disks": [{
+ "type": "file",
+ "device": "disk",
+ "source": "/var/lib/libvirt/images/web-server-disk.qcow2",
+ "format": "qcow2",
+ "bus": "virtio"
+ }],
+ "networks": [{
+ "type": "network",
+ "source": "default",
+ "model": "virtio"
+ }]
+ }')
+
+echo "VM created: $VM_RESPONSE"
+
+# 3. 等待安装完成后创建快照
+sleep 1800 # 等待 30 分钟
+curl -X POST "$BASE_URL/connections/$CONN_ID/vms/web-server-01/snapshots" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "fresh-install",
+ "description": "Clean installation snapshot"
+ }'
+```
+
+### 2. 监控和告警集成
+
+```python
+import requests
+import json
+from sseclient import SSEClient
+
+# 订阅事件流
+url = 'http://localhost:8080/api/v1/events/subscribe'
+headers = {'Content-Type': 'application/json'}
+filter_data = {
+ 'types': ['vm', 'host'],
+ 'categories': ['health', 'error']
+}
+
+response = requests.post(url, headers=headers, data=json.dumps(filter_data), stream=True)
+client = SSEClient(response)
+
+for event in client.events():
+ data = json.loads(event.data)
+
+ # 检查错误事件
+ if data['category'] == 'error':
+ send_alert(f"Error in {data['target']}: {data['detail']}")
+
+ # 检查虚拟机崩溃
+ if data['action'] == 'crashed':
+ restart_vm(data['source'], data['target'])
+```
+
+---
+
+## 总结
+
+此 API 提供了完整的虚拟化管理功能:
+
+✅ **已实现功能**:
+- ✨ 虚拟机生命周期管理
+- 💾 存储池和存储卷管理
+- 🌐 虚拟网络管理(NAT、桥接、隔离)
+- 📸 快照管理(磁盘快照、内存快照)
+- 🚀 实时/离线虚拟机迁移
+- ⚡ 批量操作(并发执行)
+- 📡 实时事件通知(SSE)
+- 🖥️ VNC/SPICE/串行控制台代理
+
+所有功能均采用 RESTful API 设计,支持异步操作和实时监控。
\ No newline at end of file
diff --git a/internal/api/handlers/storage_handler.go b/internal/api/handlers/storage_handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..04d60d23d1d63da06749d11ba62fce2a187cfef5
--- /dev/null
+++ b/internal/api/handlers/storage_handler.go
@@ -0,0 +1,784 @@
+// internal/api/handlers/storage_handler.go
+package handlers
+
+import (
+ "io"
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+
+ "virt-manager-go/internal/console"
+ "virt-manager-go/internal/domain"
+)
+
+type StorageHandler struct {
+ storageService domain.StoragePoolService
+ logger *logrus.Logger
+}
+
+func NewStorageHandler(storageService domain.StoragePoolService, logger *logrus.Logger) *StorageHandler {
+ return &StorageHandler{
+ storageService: storageService,
+ logger: logger,
+ }
+}
+
+// ListPools 列出存储池
+func (h *StorageHandler) ListPools(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ pools, err := h.storageService.ListPools(c.Request.Context(), connID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": pools, "count": len(pools)})
+}
+
+// CreatePool 创建存储池
+func (h *StorageHandler) CreatePool(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var config domain.StoragePoolConfig
+ if err := c.ShouldBindJSON(&config); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ pool, err := h.storageService.CreatePool(c.Request.Context(), connID, &config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"data": pool})
+}
+
+// GetPool 获取存储池信息
+func (h *StorageHandler) GetPool(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ pool, err := h.storageService.GetPool(c.Request.Context(), connID, poolName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": pool})
+}
+
+// DeletePool 删除存储池
+func (h *StorageHandler) DeletePool(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ if err := h.storageService.DeletePool(c.Request.Context(), connID, poolName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Storage pool deleted successfully"})
+}
+
+// StartPool 启动存储池
+func (h *StorageHandler) StartPool(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ if err := h.storageService.StartPool(c.Request.Context(), connID, poolName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Storage pool started successfully"})
+}
+
+// StopPool 停止存储池
+func (h *StorageHandler) StopPool(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ if err := h.storageService.StopPool(c.Request.Context(), connID, poolName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Storage pool stopped successfully"})
+}
+
+// RefreshPool 刷新存储池
+func (h *StorageHandler) RefreshPool(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ if err := h.storageService.RefreshPool(c.Request.Context(), connID, poolName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Storage pool refreshed successfully"})
+}
+
+// ListVolumes 列出存储卷
+func (h *StorageHandler) ListVolumes(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ volumes, err := h.storageService.ListVolumes(c.Request.Context(), connID, poolName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": volumes, "count": len(volumes)})
+}
+
+// CreateVolume 创建存储卷
+func (h *StorageHandler) CreateVolume(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+
+ var config domain.StorageVolumeConfig
+ if err := c.ShouldBindJSON(&config); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ volume, err := h.storageService.CreateVolume(c.Request.Context(), connID, poolName, &config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"data": volume})
+}
+
+// GetVolume 获取存储卷信息
+func (h *StorageHandler) GetVolume(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+ volumeName := c.Param("volume_name")
+
+ volume, err := h.storageService.GetVolume(c.Request.Context(), connID, poolName, volumeName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": volume})
+}
+
+// DeleteVolume 删除存储卷
+func (h *StorageHandler) DeleteVolume(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+ volumeName := c.Param("volume_name")
+
+ if err := h.storageService.DeleteVolume(c.Request.Context(), connID, poolName, volumeName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Storage volume deleted successfully"})
+}
+
+// CloneVolume 克隆存储卷
+func (h *StorageHandler) CloneVolume(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+ volumeName := c.Param("volume_name")
+
+ var req struct {
+ NewName string `json:"new_name" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ err := h.storageService.CloneVolume(c.Request.Context(), connID, poolName, volumeName, req.NewName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "Storage volume cloned successfully"})
+}
+
+// ResizeVolume 调整存储卷大小
+func (h *StorageHandler) ResizeVolume(c *gin.Context) {
+ connID := c.Param("connection_id")
+ poolName := c.Param("pool_name")
+ volumeName := c.Param("volume_name")
+
+ var req struct {
+ NewSize uint64 `json:"new_size" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ }
+ if err := h.storageService.ResizeVolume(c.Request.Context(), connID, poolName, volumeName, req.NewSize); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "Storage volume resized successfully"})
+}
+
+// NetworkHandler 网络处理器
+type NetworkHandler struct {
+ networkService domain.NetworkService
+ logger *logrus.Logger
+}
+
+func NewNetworkHandler(networkService domain.NetworkService, logger *logrus.Logger) *NetworkHandler {
+ return &NetworkHandler{
+ networkService: networkService,
+ logger: logger,
+ }
+}
+
+// ListNetworks 列出虚拟网络
+func (h *NetworkHandler) ListNetworks(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ networks, err := h.networkService.ListNetworks(c.Request.Context(), connID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": networks, "count": len(networks)})
+}
+
+// CreateNetwork 创建虚拟网络
+func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var config domain.NetworkConfig
+ if err := c.ShouldBindJSON(&config); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ network, err := h.networkService.CreateNetwork(c.Request.Context(), connID, &config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"data": network})
+}
+
+// GetNetwork 获取虚拟网络信息
+func (h *NetworkHandler) GetNetwork(c *gin.Context) {
+ connID := c.Param("connection_id")
+ networkName := c.Param("network_name")
+
+ network, err := h.networkService.GetNetwork(c.Request.Context(), connID, networkName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"data": network})
+
+}
+
+// DeleteNetwork 删除虚拟网络
+func (h *NetworkHandler) DeleteNetwork(c *gin.Context) {
+ connID := c.Param("connection_id")
+ networkName := c.Param("network_name")
+
+ if err := h.networkService.DeleteNetwork(c.Request.Context(), connID, networkName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Network deleted successfully"})
+}
+
+// StartNetwork 启动虚拟网络
+func (h *NetworkHandler) StartNetwork(c *gin.Context) {
+ connID := c.Param("connection_id")
+ networkName := c.Param("network_name")
+
+ if err := h.networkService.StartNetwork(c.Request.Context(), connID, networkName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Network started successfully"})
+}
+
+// StopNetwork 停止虚拟网络
+func (h *NetworkHandler) StopNetwork(c *gin.Context) {
+ connID := c.Param("connection_id")
+ networkName := c.Param("network_name")
+
+ if err := h.networkService.StopNetwork(c.Request.Context(), connID, networkName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Network stopped successfully"})
+}
+
+// GetDHCPLeases 获取 DHCP 租约
+func (h *NetworkHandler) GetDHCPLeases(c *gin.Context) {
+ connID := c.Param("connection_id")
+ networkName := c.Param("network_name")
+
+ leases, err := h.networkService.GetDHCPLeases(c.Request.Context(), connID, networkName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": leases, "count": len(leases)})
+}
+
+// SnapshotHandler 快照处理器
+type SnapshotHandler struct {
+ snapshotService domain.SnapshotService
+ logger *logrus.Logger
+}
+
+func NewSnapshotHandler(snapshotService domain.SnapshotService, logger *logrus.Logger) *SnapshotHandler {
+ return &SnapshotHandler{
+ snapshotService: snapshotService,
+ logger: logger,
+ }
+}
+
+// ListSnapshots 列出快照
+func (h *SnapshotHandler) ListSnapshots(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ snapshots, err := h.snapshotService.ListSnapshots(c.Request.Context(), connID, vmName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": snapshots, "count": len(snapshots)})
+}
+
+// CreateSnapshot 创建快照
+func (h *SnapshotHandler) CreateSnapshot(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ var config domain.SnapshotConfig
+ if err := c.ShouldBindJSON(&config); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ snapshot, err := h.snapshotService.CreateSnapshot(c.Request.Context(), connID, vmName, &config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"data": snapshot})
+}
+
+// GetSnapshot 获取快照信息
+func (h *SnapshotHandler) GetSnapshot(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+ snapshotName := c.Param("snapshot_name")
+
+ snapshot, err := h.snapshotService.GetSnapshot(c.Request.Context(), connID, vmName, snapshotName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": snapshot})
+}
+
+// DeleteSnapshot 删除快照
+func (h *SnapshotHandler) DeleteSnapshot(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+ snapshotName := c.Param("snapshot_name")
+ deleteChildren := c.Query("delete_children") == "true"
+
+ if err := h.snapshotService.DeleteSnapshot(c.Request.Context(), connID, vmName, snapshotName, deleteChildren); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Snapshot deleted successfully"})
+}
+
+// RevertToSnapshot 恢复到快照
+func (h *SnapshotHandler) RevertToSnapshot(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+ snapshotName := c.Param("snapshot_name")
+
+ if err := h.snapshotService.RevertToSnapshot(c.Request.Context(), connID, vmName, snapshotName); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Reverted to snapshot successfully"})
+}
+
+// MigrationHandler 迁移处理器
+type MigrationHandler struct {
+ migrationService domain.MigrationService
+ logger *logrus.Logger
+}
+
+func NewMigrationHandler(migrationService domain.MigrationService, logger *logrus.Logger) *MigrationHandler {
+ return &MigrationHandler{
+ migrationService: migrationService,
+ logger: logger,
+ }
+}
+
+// MigrateVM 迁移虚拟机
+func (h *MigrationHandler) MigrateVM(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ var req struct {
+ DestURI string `json:"dest_uri" binding:"required"`
+ Config *domain.MigrationConfig `json:"config"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if req.Config == nil {
+ req.Config = &domain.MigrationConfig{Live: true}
+ }
+
+ job, err := h.migrationService.MigrateVM(c.Request.Context(), connID, vmName, req.DestURI, req.Config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusAccepted, gin.H{"data": job})
+}
+
+// GetMigrationStatus 获取迁移状态
+func (h *MigrationHandler) GetMigrationStatus(c *gin.Context) {
+ jobID := c.Param("job_id")
+
+ status, err := h.migrationService.GetMigrationStatus(c.Request.Context(), jobID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": status})
+}
+
+// CancelMigration 取消迁移
+func (h *MigrationHandler) CancelMigration(c *gin.Context) {
+ jobID := c.Param("job_id")
+
+ if err := h.migrationService.CancelMigration(c.Request.Context(), jobID); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Migration cancelled successfully"})
+}
+
+// BatchHandler 批量操作处理器
+type BatchHandler struct {
+ batchService domain.BatchService
+ logger *logrus.Logger
+}
+
+func NewBatchHandler(batchService domain.BatchService, logger *logrus.Logger) *BatchHandler {
+ return &BatchHandler{
+ batchService: batchService,
+ logger: logger,
+ }
+}
+
+// BatchStartVMs 批量启动虚拟机
+func (h *BatchHandler) BatchStartVMs(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var req struct {
+ VMNames []string `json:"vm_names" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ result, err := h.batchService.BatchStartVMs(c.Request.Context(), connID, req.VMNames)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+}
+
+// BatchStopVMs 批量停止虚拟机
+func (h *BatchHandler) BatchStopVMs(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var req struct {
+ VMNames []string `json:"vm_names" binding:"required"`
+ Force bool `json:"force"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ result, err := h.batchService.BatchStopVMs(c.Request.Context(), connID, req.VMNames, req.Force)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+}
+
+// BatchRebootVMs 批量重启虚拟机
+func (h *BatchHandler) BatchRebootVMs(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var req struct {
+ VMNames []string `json:"vm_names" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+ result, err := h.batchService.BatchRebootVMs(c.Request.Context(), connID, req.VMNames)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+
+}
+
+// BatchDeleteVMs 批量删除虚拟机
+func (h *BatchHandler) BatchDeleteVMs(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var req struct {
+ VMNames []string `json:"vm_names" binding:"required"`
+ Force bool `json:"force"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ result, err := h.batchService.BatchDeleteVMs(c.Request.Context(), connID, req.VMNames, req.Force)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+}
+
+// BatchCreateSnapshots 批量创建快照
+func (h *BatchHandler) BatchCreateSnapshots(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ var req struct {
+ VMNames []string `json:"vm_names" binding:"required"`
+ Config *domain.SnapshotConfig `json:"config" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ result, err := h.batchService.BatchCreateSnapshots(c.Request.Context(), connID, req.VMNames, req.Config)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+}
+
+// BatchDeleteSnapshots 批量删除快照
+
+// BatchUpdateVMs 批量更新虚拟机
+
+// GetBatchStatus 获取批量操作状态
+func (h *BatchHandler) GetBatchStatus(c *gin.Context) {
+ jobID := c.Param("job_id")
+
+ status, err := h.batchService.GetBatchStatus(c.Request.Context(), jobID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": status})
+}
+
+// EventHandler 事件处理器
+type EventHandler struct {
+ eventService domain.EventService
+ logger *logrus.Logger
+}
+
+func NewEventHandler(eventService domain.EventService, logger *logrus.Logger) *EventHandler {
+ return &EventHandler{
+ eventService: eventService,
+ logger: logger,
+ }
+}
+
+// SubscribeEvents 订阅事件 (SSE)
+func (h *EventHandler) SubscribeEvents(c *gin.Context) {
+ var filter domain.EventFilter
+ if err := c.ShouldBindJSON(&filter); err != nil {
+ // 如果没有提供过滤器,使用默认(所有事件)
+ filter = domain.EventFilter{}
+ }
+
+ eventCh, err := h.eventService.Subscribe(c.Request.Context(), &filter)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 设置 SSE 响应头
+ c.Header("Content-Type", "text/event-stream")
+ c.Header("Cache-Control", "no-cache")
+ c.Header("Connection", "keep-alive")
+ c.Header("X-Accel-Buffering", "no")
+
+ // 流式发送事件
+ c.Stream(func(w io.Writer) bool {
+ select {
+ case event, ok := <-eventCh:
+ if !ok {
+ return false
+ }
+ c.SSEvent("event", event)
+ return true
+ case <-c.Request.Context().Done():
+ return false
+ }
+ })
+}
+
+// GetEvents 获取历史事件
+func (h *EventHandler) GetEvents(c *gin.Context) {
+ var filter domain.EventFilter
+ if err := c.ShouldBindJSON(&filter); err != nil {
+ // 使用查询参数作为过滤器
+ filter = domain.EventFilter{}
+ }
+
+ limit := 100
+ if l := c.Query("limit"); l != "" {
+ if parsed, err := strconv.Atoi(l); err == nil {
+ limit = parsed
+ }
+ }
+
+ events, err := h.eventService.GetEvents(c.Request.Context(), &filter, limit)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": events, "count": len(events)})
+}
+
+// StartEventListener 启动事件监听
+func (h *EventHandler) StartEventListener(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ if err := h.eventService.StartListening(c.Request.Context(), connID); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Event listener started"})
+}
+
+func (h *EventHandler) StopEventListener(c *gin.Context) {
+ connID := c.Param("connection_id")
+
+ if err := h.eventService.StopListening(c.Request.Context(), connID); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Event listener stopped"})
+}
+
+// GraphicsHandler 图形控制台处理器
+type GraphicsHandler struct {
+ graphicsProxy *console.GraphicsProxy
+ logger *logrus.Logger
+}
+
+func NewGraphicsHandler(graphicsProxy *console.GraphicsProxy, logger *logrus.Logger) *GraphicsHandler {
+ return &GraphicsHandler{
+ graphicsProxy: graphicsProxy,
+ logger: logger,
+ }
+}
+
+// GetVNCInfo 获取 VNC 信息
+func (h *GraphicsHandler) GetVNCInfo(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ info, err := h.graphicsProxy.GetVNCInfo(c.Request.Context(), connID, vmName)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": info})
+}
+
+// HandleVNCWebSocket 处理 VNC WebSocket
+func (h *GraphicsHandler) HandleVNCWebSocket(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ if err := h.graphicsProxy.HandleVNCWebSocket(c.Writer, c.Request, connID, vmName); err != nil {
+ h.logger.WithError(err).Error("VNC WebSocket handler error")
+ }
+}
+
+// HandleSPICEWebSocket 处理 SPICE WebSocket
+func (h *GraphicsHandler) HandleSPICEWebSocket(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+
+ if err := h.graphicsProxy.HandleSPICEWebSocket(c.Writer, c.Request, connID, vmName); err != nil {
+ h.logger.WithError(err).Error("SPICE WebSocket handler error")
+ }
+}
diff --git a/internal/api/router.go b/internal/api/router.go
index 41d1a34f3c5ee14a7842abc41255e5cbeeac9427..50c0b68058021cb9a46a7035f9dee7c0c765d9cc 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -14,15 +14,29 @@ import (
)
type Router struct {
- engine *gin.Engine
- vmHandler *handlers.VMHandler
- connHandler *handlers.ConnectionHandler
- serialProxy domain.ConsoleProxy
- logger *logrus.Logger
+ engine *gin.Engine
+ vmHandler *handlers.VMHandler
+ storageHandler *handlers.StorageHandler
+ networkHandler *handlers.NetworkHandler
+ snapshotHandler *handlers.SnapshotHandler
+ migrationHandler *handlers.MigrationHandler
+ batchHandler *handlers.BatchHandler
+ eventHandler *handlers.EventHandler
+ connHandler *handlers.ConnectionHandler
+ serialProxy domain.ConsoleProxy
+ graphicsProxy console.GraphicsProxy
+ logger *logrus.Logger
}
func NewRouter(
vmService domain.VMService,
+ storageService domain.StoragePoolService,
+ networkService domain.NetworkService,
+ snapshotService domain.SnapshotService,
+ migrationService domain.MigrationService,
+ batchService domain.BatchService,
+ eventService domain.EventService,
+ serialProxy domain.ConsoleProxy,
connMgr domain.ConnectionManager,
logger *logrus.Logger,
) *Router {
@@ -39,17 +53,28 @@ func NewRouter(
// 创建处理器
vmHandler := handlers.NewVMHandler(vmService, logger)
connHandler := handlers.NewConnectionHandler(connMgr, logger)
- serialProxy := console.NewSerialProxy(connMgr, logger)
+ storageHandler := handlers.NewStorageHandler(storageService, logger)
+ networkHandler := handlers.NewNetworkHandler(networkService, logger)
+ snapshotHandler := handlers.NewSnapshotHandler(snapshotService, logger)
+ migrationHandler := handlers.NewMigrationHandler(migrationService, logger)
+ batchHandler := handlers.NewBatchHandler(batchService, logger)
+ eventHandler := handlers.NewEventHandler(eventService, logger)
router := &Router{
- engine: engine,
- vmHandler: vmHandler,
- connHandler: connHandler,
- serialProxy: serialProxy,
- logger: logger,
+ engine: engine,
+ vmHandler: vmHandler,
+ connHandler: connHandler,
+ storageHandler: storageHandler,
+ networkHandler: networkHandler,
+ snapshotHandler: snapshotHandler,
+ migrationHandler: migrationHandler,
+ batchHandler: batchHandler,
+ eventHandler: eventHandler,
+ serialProxy: serialProxy,
+ logger: logger,
}
- router.setupRoutes()
+ router.SetupExtendedRoutes()
return router
}
@@ -59,6 +84,149 @@ func (r *Router) Engine() *gin.Engine {
return r.engine
}
+func (r *Router) SetupExtendedRoutes() {
+ // 健康检查
+ r.engine.GET("/health", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "status": "ok",
+ "service": "virt-manager-go",
+ })
+ })
+
+ v1 := r.engine.Group("/api/v1")
+ {
+ // ===== 连接管理 =====
+ connections := v1.Group("/connections")
+ {
+ connections.POST("", r.connHandler.Connect)
+ connections.GET("", r.connHandler.ListConnections)
+ connections.DELETE("/:connection_id", r.connHandler.Disconnect)
+ connections.GET("/:connection_id/capabilities", r.connHandler.GetCapabilities)
+
+ // ===== 虚拟机管理 =====
+ vms := connections.Group("/:connection_id/vms")
+ {
+ vms.GET("", r.vmHandler.ListVMs)
+ vms.POST("", r.vmHandler.CreateVM)
+ vms.GET("/:vm_name", r.vmHandler.GetVM)
+ vms.DELETE("/:vm_name", r.vmHandler.DeleteVM)
+ vms.PATCH("/:vm_name", r.vmHandler.UpdateVM)
+
+ // 虚拟机操作
+ vms.POST("/:vm_name/start", r.vmHandler.StartVM)
+ vms.POST("/:vm_name/shutdown", r.vmHandler.ShutdownVM)
+ vms.POST("/:vm_name/reboot", r.vmHandler.RebootVM)
+ vms.POST("/:vm_name/suspend", r.vmHandler.SuspendVM)
+ vms.POST("/:vm_name/resume", r.vmHandler.ResumeVM)
+ vms.POST("/:vm_name/clone", r.vmHandler.CloneVM)
+
+ // 配置管理
+ vms.GET("/:vm_name/xml", r.vmHandler.GetVMXML)
+
+ // 性能监控
+ vms.GET("/:vm_name/metrics", r.vmHandler.GetVMMetrics)
+ vms.GET("/:vm_name/metrics/stream", r.vmHandler.StreamVMMetrics)
+
+ // ===== 快照管理 =====
+ snapshots := vms.Group("/:vm_name/snapshots")
+ {
+ snapshots.GET("", r.snapshotHandler.ListSnapshots)
+ snapshots.POST("", r.snapshotHandler.CreateSnapshot)
+ snapshots.GET("/:snapshot_name", r.snapshotHandler.GetSnapshot)
+ snapshots.DELETE("/:snapshot_name", r.snapshotHandler.DeleteSnapshot)
+ snapshots.POST("/:snapshot_name/revert", r.snapshotHandler.RevertToSnapshot)
+ }
+
+ // ===== 迁移 =====
+ vms.POST("/:vm_name/migrate", r.migrationHandler.MigrateVM)
+ }
+
+ // ===== 存储池管理 =====
+ storage := connections.Group("/:connection_id/storage")
+ {
+ // 存储池
+ storage.GET("/pools", r.storageHandler.ListPools)
+ storage.POST("/pools", r.storageHandler.CreatePool)
+ storage.GET("/pools/:pool_name", r.storageHandler.GetPool)
+ storage.DELETE("/pools/:pool_name", r.storageHandler.DeletePool)
+ storage.POST("/pools/:pool_name/start", r.storageHandler.StartPool)
+ storage.POST("/pools/:pool_name/stop", r.storageHandler.StopPool)
+ storage.POST("/pools/:pool_name/refresh", r.storageHandler.RefreshPool)
+
+ // 存储卷
+ storage.GET("/pools/:pool_name/volumes", r.storageHandler.ListVolumes)
+ storage.POST("/pools/:pool_name/volumes", r.storageHandler.CreateVolume)
+ storage.GET("/pools/:pool_name/volumes/:volume_name", r.storageHandler.GetVolume)
+ storage.DELETE("/pools/:pool_name/volumes/:volume_name", r.storageHandler.DeleteVolume)
+ storage.POST("/pools/:pool_name/volumes/:volume_name/clone", r.storageHandler.CloneVolume)
+ storage.POST("/pools/:pool_name/volumes/:volume_name/resize", r.storageHandler.ResizeVolume)
+ }
+
+ // ===== 虚拟网络管理 =====
+ networks := connections.Group("/:connection_id/networks")
+ {
+ networks.GET("", r.networkHandler.ListNetworks)
+ networks.POST("", r.networkHandler.CreateNetwork)
+ networks.GET("/:network_name", r.networkHandler.GetNetwork)
+ networks.DELETE("/:network_name", r.networkHandler.DeleteNetwork)
+ networks.POST("/:network_name/start", r.networkHandler.StartNetwork)
+ networks.POST("/:network_name/stop", r.networkHandler.StopNetwork)
+ networks.GET("/:network_name/dhcp-leases", r.networkHandler.GetDHCPLeases)
+ }
+
+ // ===== 批量操作 =====
+ batch := connections.Group("/:connection_id/batch")
+ {
+ batch.POST("/vms/start", r.batchHandler.BatchStartVMs)
+ batch.POST("/vms/stop", r.batchHandler.BatchStopVMs)
+ batch.POST("/vms/reboot", r.batchHandler.BatchRebootVMs)
+ batch.POST("/vms/delete", r.batchHandler.BatchDeleteVMs)
+ batch.POST("/snapshots/create", r.batchHandler.BatchCreateSnapshots)
+ // batch.POST("/snapshots/delete", r.batchHandler.BatchDeleteSnapshots)
+ // batch.POST("/vms/update", r.batchHandler.BatchUpdateVMs)
+ batch.GET("/status/:batch_id", r.batchHandler.GetBatchStatus)
+ }
+ }
+
+ // ===== 迁移管理 =====
+ migrations := v1.Group("/migrations")
+ {
+ migrations.GET("/:job_id", r.migrationHandler.GetMigrationStatus)
+ migrations.POST("/:job_id/cancel", r.migrationHandler.CancelMigration)
+ }
+
+ // ===== 控制台 WebSocket =====
+ console := v1.Group("/console")
+ {
+ // 串行控制台
+ console.GET("/serial/:connection_id/:vm_name", func(c *gin.Context) {
+ connID := c.Param("connection_id")
+ vmName := c.Param("vm_name")
+ if err := r.serialProxy.HandleWebSocket(c.Writer, c.Request, connID, vmName); err != nil {
+ r.logger.WithError(err).Error("Serial console error")
+ }
+ })
+
+ // // VNC 控制台
+ // console.GET("/vnc/:connection_id/:vm_name/info", r.graphicsHandler.GetVNCInfo)
+ // console.GET("/vnc/:connection_id/:vm_name", r.graphicsHandler.HandleVNCWebSocket)
+
+ // // SPICE 控制台
+ // console.GET("/spice/:connection_id/:vm_name/info", r.graphicsHandler.GetSPICEInfo)
+ // console.GET("/spice/:connection_id/:vm_name", r.graphicsHandler.HandleSPICEWebSocket)
+ }
+
+ // ===== 事件系统 =====
+ events := v1.Group("/events")
+ {
+ events.POST("/subscribe", r.eventHandler.SubscribeEvents) // SSE
+ events.GET("/history", r.eventHandler.GetEvents)
+ events.POST("/connections/:connection_id/listen", r.eventHandler.StartEventListener)
+ events.POST("/connections/:connection_id/unlisten", r.eventHandler.StopEventListener)
+ }
+ }
+}
+
func (r *Router) setupRoutes() {
// 健康检查
r.engine.GET("/health", func(c *gin.Context) {
diff --git a/internal/api/router_extended.go b/internal/api/router_extended.go
new file mode 100644
index 0000000000000000000000000000000000000000..c1cf7ca437192e25056a255822e740b93be8c204
--- /dev/null
+++ b/internal/api/router_extended.go
@@ -0,0 +1,128 @@
+// internal/api/router_extended.go
+package api
+
+// ============================================
+// 使用示例和完整的 main.go
+// ============================================
+
+/*
+// cmd/server/main.go
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/sirupsen/logrus"
+
+ "virt-manager-go/internal/api"
+ "virt-manager-go/internal/console"
+ "virt-manager-go/internal/service"
+)
+
+func main() {
+ flag.Parse()
+
+ logger := setupLogger("info")
+ logger.Info("Starting virt-manager-go with extended features")
+
+ // 初始化所有服务
+ connMgr := service.NewConnectionService(logger)
+ vmService := service.NewVMService(connMgr, logger)
+ storageService := service.NewStoragePoolService(connMgr, logger)
+ networkService := service.NewNetworkService(connMgr, logger)
+ snapshotService := service.NewSnapshotService(connMgr, vmService, logger)
+ migrationService := service.NewMigrationService(connMgr, logger)
+ batchService := service.NewBatchService(vmService, snapshotService, logger)
+ eventService := service.NewEventService(connMgr, logger)
+
+ // 初始化控制台代理
+ serialProxy := console.NewSerialProxy(connMgr, logger)
+ graphicsProxy := console.NewGraphicsProxy(connMgr, logger)
+
+ // 创建路由
+ engine := gin.New()
+ engine.Use(gin.Recovery())
+
+ api.SetupExtendedRoutes(
+ engine,
+ vmService,
+ connMgr,
+ storageService,
+ networkService,
+ snapshotService,
+ migrationService,
+ batchService,
+ eventService,
+ serialProxy,
+ graphicsProxy,
+ logger,
+ )
+
+ // 自动连接本地 libvirt
+ ctx := context.Background()
+ connID, err := connMgr.Connect(ctx, "qemu:///system", r.nil)
+ if err != nil {
+ logger.WithError(err).Warn("Failed to auto-connect")
+ } else {
+ logger.WithField("connection_id", r.connID).Info("Auto-connected to libvirt")
+
+ // 启动事件监听
+ if err := eventService.StartListening(ctx, connID); err != nil {
+ logger.WithError(err).Warn("Failed to start event listening")
+ }
+ }
+
+ // 启动服务器
+ addr := "0.0.0.0:8080"
+ go func() {
+ if err := engine.Run(addr); err != nil {
+ logger.WithError(err).Fatal("Server failed")
+ }
+ }()
+
+ logger.WithField("address", r.addr).Info("Server started with all features")
+ logger.Info("📚 API Documentation:")
+ logger.Info(" Virtual Machines: /api/v1/connections/{id}/vms")
+ logger.Info(" Storage Pools: /api/v1/connections/{id}/storage/pools")
+ logger.Info(" Networks: /api/v1/connections/{id}/networks")
+ logger.Info(" Snapshots: /api/v1/connections/{id}/vms/{name}/snapshots")
+ logger.Info(" Batch Operations: /api/v1/connections/{id}/batch")
+ logger.Info(" Events (SSE): /api/v1/events/subscribe")
+ logger.Info(" Serial Console: ws://localhost:8080/api/v1/console/serial/{conn}/{vm}")
+ logger.Info(" VNC Console: ws://localhost:8080/api/v1/console/vnc/{conn}/{vm}")
+
+ // 优雅关闭
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+
+ logger.Info("Shutting down...")
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // 清理资源
+ connections, _ := connMgr.ListConnections(ctx)
+ for _, conn := range connections {
+ eventService.StopListening(conn.ID)
+ connMgr.Disconnect(ctx, conn.ID)
+ }
+
+ logger.Info("Server stopped gracefully")
+}
+
+func setupLogger(level string) *logrus.Logger {
+ logger := logrus.New()
+ logger.SetFormatter(&logrus.TextFormatter{
+ FullTimestamp: true,
+ })
+ logLevel, _ := logrus.ParseLevel(level)
+ logger.SetLevel(logLevel)
+ return logger
+}
+*/
diff --git a/internal/console/graphics_proxy.go b/internal/console/graphics_proxy.go
new file mode 100644
index 0000000000000000000000000000000000000000..1636cde4ed4bfdc35ac1dcbaa6c1e21e3c8b8a60
--- /dev/null
+++ b/internal/console/graphics_proxy.go
@@ -0,0 +1,462 @@
+// internal/console/graphics_proxy.go
+package console
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirtxml"
+
+ "virt-manager-go/internal/domain"
+)
+
+// GraphicsProxy VNC/SPICE 图形代理
+type GraphicsProxy struct {
+ connMgr domain.ConnectionManager
+ logger *logrus.Logger
+ upgrader websocket.Upgrader
+
+ // 代理会话管理
+ sessions map[string]*graphicsSession
+ sessionsMu sync.RWMutex
+
+ // 令牌管理(用于认证)
+ tokens map[string]*proxyToken
+ tokensMu sync.RWMutex
+}
+
+type graphicsSession struct {
+ id string
+ vmName string
+ sessionType string // vnc, spice
+ wsConn *websocket.Conn
+ tcpConn net.Conn
+ ctx context.Context
+ cancel context.CancelFunc
+ logger *logrus.Logger
+}
+
+type proxyToken struct {
+ token string
+ connID string
+ vmName string
+ proxyType string
+ expiresAt time.Time
+}
+
+func NewGraphicsProxy(connMgr domain.ConnectionManager, logger *logrus.Logger) *GraphicsProxy {
+ return &GraphicsProxy{
+ connMgr: connMgr,
+ logger: logger,
+ upgrader: websocket.Upgrader{
+ ReadBufferSize: 16384,
+ WriteBufferSize: 16384,
+ CheckOrigin: func(r *http.Request) bool {
+ return true // 生产环境需要验证 Origin
+ },
+ },
+ sessions: make(map[string]*graphicsSession),
+ tokens: make(map[string]*proxyToken),
+ }
+}
+
+// GetVNCInfo 获取 VNC 连接信息
+func (p *GraphicsProxy) GetVNCInfo(ctx context.Context, connID, vmName string) (*domain.ProxyInfo, error) {
+ conn, err := p.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ dom, err := conn.LookupDomainByName(vmName)
+ if err != nil {
+ return nil, fmt.Errorf("VM not found: %w", err)
+ }
+ defer dom.Free()
+
+ // 解析 XML 获取 VNC 配置
+ xmlData, err := dom.GetXMLDesc(0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get VM XML: %w", err)
+ }
+
+ domainConfig := &libvirtxml.Domain{}
+ if err := domainConfig.Unmarshal(xmlData); err != nil {
+ return nil, fmt.Errorf("failed to parse VM XML: %w", err)
+ }
+
+ // 查找 VNC 图形设备
+ var vncPort int
+ var vncListen string
+ for _, gfx := range domainConfig.Devices.Graphics {
+ if gfx.VNC != nil {
+ vncPort = gfx.VNC.Port
+ if len(gfx.VNC.Listeners) > 0 && gfx.VNC.Listeners[0].Address != nil {
+ vncListen = gfx.VNC.Listeners[0].Address.Address
+ }
+ break
+ }
+ }
+
+ if vncPort == 0 {
+ return nil, fmt.Errorf("VNC not configured for this VM")
+ }
+
+ // 生成访问令牌
+ token := p.generateToken(connID, vmName, "vnc")
+
+ proxyInfo := &domain.ProxyInfo{
+ ProxyURL: fmt.Sprintf("vnc://%s:%d", vncListen, vncPort),
+ WSPath: fmt.Sprintf("/api/v1/console/vnc/%s/%s?token=%s", connID, vmName, token),
+ Token: token,
+ ExpiresAt: time.Now().Add(1 * time.Hour),
+ }
+
+ return proxyInfo, nil
+}
+
+// GetSPICEInfo 获取 SPICE 连接信息
+func (p *GraphicsProxy) GetSPICEInfo(ctx context.Context, connID, vmName string) (*domain.ProxyInfo, error) {
+ conn, err := p.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ dom, err := conn.LookupDomainByName(vmName)
+ if err != nil {
+ return nil, fmt.Errorf("VM not found: %w", err)
+ }
+ defer dom.Free()
+
+ xmlData, err := dom.GetXMLDesc(0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get VM XML: %w", err)
+ }
+
+ domainConfig := &libvirtxml.Domain{}
+ if err := domainConfig.Unmarshal(xmlData); err != nil {
+ return nil, fmt.Errorf("failed to parse VM XML: %w", err)
+ }
+
+ // 查找 SPICE 图形设备
+ var spicePort int
+ var spiceListen string
+ for _, gfx := range domainConfig.Devices.Graphics {
+ if gfx.Spice != nil {
+ spicePort = gfx.Spice.Port
+ if len(gfx.Spice.Listeners) > 0 && gfx.Spice.Listeners[0].Address != nil {
+ spiceListen = gfx.Spice.Listeners[0].Address.Address
+ }
+ break
+ }
+ }
+
+ if spicePort == 0 {
+ return nil, fmt.Errorf("SPICE not configured for this VM")
+ }
+
+ token := p.generateToken(connID, vmName, "spice")
+
+ proxyInfo := &domain.ProxyInfo{
+ ProxyURL: fmt.Sprintf("spice://%s:%d", spiceListen, spicePort),
+ WSPath: fmt.Sprintf("/api/v1/console/spice/%s/%s?token=%s", connID, vmName, token),
+ Token: token,
+ ExpiresAt: time.Now().Add(1 * time.Hour),
+ }
+
+ return proxyInfo, nil
+}
+
+// HandleVNCWebSocket 处理 VNC WebSocket 连接
+func (p *GraphicsProxy) HandleVNCWebSocket(w http.ResponseWriter, r *http.Request, connID, vmName string) error {
+ // 验证令牌
+ token := r.URL.Query().Get("token")
+ if !p.validateToken(token, connID, vmName, "vnc") {
+ http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
+ return fmt.Errorf("invalid token")
+ }
+
+ // 升级到 WebSocket
+ ws, err := p.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return fmt.Errorf("failed to upgrade to websocket: %w", err)
+ }
+
+ p.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "vm": vmName,
+ "remote": r.RemoteAddr,
+ "type": "vnc",
+ }).Info("VNC WebSocket connection established")
+
+ // 获取 VNC 连接信息
+ proxyInfo, err := p.GetVNCInfo(r.Context(), connID, vmName)
+ if err != nil {
+ ws.Close()
+ return err
+ }
+
+ // 连接到 VNC 服务器
+ vncConn, err := net.DialTimeout("tcp", proxyInfo.ProxyURL[6:], 10*time.Second)
+ if err != nil {
+ ws.Close()
+ return fmt.Errorf("failed to connect to VNC server: %w", err)
+ }
+
+ // 创建代理会话
+ ctx, cancel := context.WithCancel(r.Context())
+ session := &graphicsSession{
+ id: fmt.Sprintf("vnc-%s-%d", vmName, time.Now().Unix()),
+ vmName: vmName,
+ sessionType: "vnc",
+ wsConn: ws,
+ tcpConn: vncConn,
+ ctx: ctx,
+ cancel: cancel,
+ logger: p.logger,
+ }
+
+ // 注册会话
+ p.sessionsMu.Lock()
+ p.sessions[session.id] = session
+ p.sessionsMu.Unlock()
+
+ // 清理会话
+ defer func() {
+ p.sessionsMu.Lock()
+ delete(p.sessions, session.id)
+ p.sessionsMu.Unlock()
+
+ vncConn.Close()
+ ws.Close()
+ cancel()
+
+ p.logger.WithField("session_id", session.id).Info("VNC session closed")
+ }()
+
+ // 启动双向代理
+ return p.proxyBidirectional(session)
+}
+
+// HandleSPICEWebSocket 处理 SPICE WebSocket 连接
+func (p *GraphicsProxy) HandleSPICEWebSocket(w http.ResponseWriter, r *http.Request, connID, vmName string) error {
+ // 验证令牌
+ token := r.URL.Query().Get("token")
+ if !p.validateToken(token, connID, vmName, "spice") {
+ http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
+ return fmt.Errorf("invalid token")
+ }
+
+ // 升级到 WebSocket
+ ws, err := p.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return fmt.Errorf("failed to upgrade to websocket: %w", err)
+ }
+
+ p.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "vm": vmName,
+ "remote": r.RemoteAddr,
+ "type": "spice",
+ }).Info("SPICE WebSocket connection established")
+
+ // 获取 SPICE 连接信息
+ proxyInfo, err := p.GetSPICEInfo(r.Context(), connID, vmName)
+ if err != nil {
+ ws.Close()
+ return err
+ }
+
+ // 连接到 SPICE 服务器
+ spiceConn, err := net.DialTimeout("tcp", proxyInfo.ProxyURL[8:], 10*time.Second)
+ if err != nil {
+ ws.Close()
+ return fmt.Errorf("failed to connect to SPICE server: %w", err)
+ }
+
+ // 创建代理会话
+ ctx, cancel := context.WithCancel(r.Context())
+ session := &graphicsSession{
+ id: fmt.Sprintf("spice-%s-%d", vmName, time.Now().Unix()),
+ vmName: vmName,
+ sessionType: "spice",
+ wsConn: ws,
+ tcpConn: spiceConn,
+ ctx: ctx,
+ cancel: cancel,
+ logger: p.logger,
+ }
+
+ // 注册会话
+ p.sessionsMu.Lock()
+ p.sessions[session.id] = session
+ p.sessionsMu.Unlock()
+
+ // 清理会话
+ defer func() {
+ p.sessionsMu.Lock()
+ delete(p.sessions, session.id)
+ p.sessionsMu.Unlock()
+
+ spiceConn.Close()
+ ws.Close()
+ cancel()
+
+ p.logger.WithField("session_id", session.id).Info("SPICE session closed")
+ }()
+
+ // 启动双向代理
+ return p.proxyBidirectional(session)
+}
+
+// proxyBidirectional 双向代理数据传输
+func (p *GraphicsProxy) proxyBidirectional(session *graphicsSession) error {
+ errCh := make(chan error, 2)
+
+ // WebSocket -> TCP
+ go p.wsToTCP(session, errCh)
+
+ // TCP -> WebSocket
+ go p.tcpToWS(session, errCh)
+
+ // 等待任一方向出错或上下文取消
+ select {
+ case err := <-errCh:
+ if err != nil && err != io.EOF {
+ session.logger.WithError(err).Warn("Graphics proxy error")
+ }
+ return err
+ case <-session.ctx.Done():
+ return session.ctx.Err()
+ }
+}
+
+// wsToTCP WebSocket 到 TCP 的数据转发
+func (p *GraphicsProxy) wsToTCP(session *graphicsSession, errCh chan<- error) {
+ defer session.cancel()
+
+ for {
+ select {
+ case <-session.ctx.Done():
+ return
+ default:
+ }
+
+ msgType, data, err := session.wsConn.ReadMessage()
+ if err != nil {
+ if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
+ errCh <- fmt.Errorf("websocket read error: %w", err)
+ }
+ return
+ }
+
+ if msgType != websocket.BinaryMessage {
+ continue
+ }
+
+ if _, err := session.tcpConn.Write(data); err != nil {
+ errCh <- fmt.Errorf("tcp write error: %w", err)
+ return
+ }
+ }
+}
+
+// tcpToWS TCP 到 WebSocket 的数据转发
+func (p *GraphicsProxy) tcpToWS(session *graphicsSession, errCh chan<- error) {
+ defer session.cancel()
+
+ buffer := make([]byte, 32768) // 32KB 缓冲区
+
+ for {
+ select {
+ case <-session.ctx.Done():
+ return
+ default:
+ }
+
+ n, err := session.tcpConn.Read(buffer)
+ if err != nil {
+ if err != io.EOF {
+ errCh <- fmt.Errorf("tcp read error: %w", err)
+ }
+ return
+ }
+
+ if n == 0 {
+ continue
+ }
+
+ if err := session.wsConn.WriteMessage(websocket.BinaryMessage, buffer[:n]); err != nil {
+ errCh <- fmt.Errorf("websocket write error: %w", err)
+ return
+ }
+ }
+}
+
+// generateToken 生成访问令牌
+func (p *GraphicsProxy) generateToken(connID, vmName, proxyType string) string {
+ tokenBytes := make([]byte, 32)
+ rand.Read(tokenBytes)
+ token := hex.EncodeToString(tokenBytes)
+
+ p.tokensMu.Lock()
+ p.tokens[token] = &proxyToken{
+ token: token,
+ connID: connID,
+ vmName: vmName,
+ proxyType: proxyType,
+ expiresAt: time.Now().Add(1 * time.Hour),
+ }
+ p.tokensMu.Unlock()
+
+ // 启动令牌清理
+ go p.cleanupExpiredToken(token)
+
+ return token
+}
+
+// validateToken 验证令牌
+func (p *GraphicsProxy) validateToken(token, connID, vmName, proxyType string) bool {
+ p.tokensMu.RLock()
+ defer p.tokensMu.RUnlock()
+
+ t, exists := p.tokens[token]
+ if !exists {
+ return false
+ }
+
+ if time.Now().After(t.expiresAt) {
+ return false
+ }
+
+ return t.connID == connID && t.vmName == vmName && t.proxyType == proxyType
+}
+
+// cleanupExpiredToken 清理过期令牌
+func (p *GraphicsProxy) cleanupExpiredToken(token string) {
+ time.Sleep(1 * time.Hour)
+
+ p.tokensMu.Lock()
+ delete(p.tokens, token)
+ p.tokensMu.Unlock()
+}
+
+// GetActiveSessions 获取活跃会话
+func (p *GraphicsProxy) GetActiveSessions() []*graphicsSession {
+ p.sessionsMu.RLock()
+ defer p.sessionsMu.RUnlock()
+
+ sessions := make([]*graphicsSession, 0, len(p.sessions))
+ for _, s := range p.sessions {
+ sessions = append(sessions, s)
+ }
+ return sessions
+}
diff --git a/internal/domain/batch.go b/internal/domain/batch.go
new file mode 100644
index 0000000000000000000000000000000000000000..b726f7a855c89bfc1d955e2b99441346b0dfb42d
--- /dev/null
+++ b/internal/domain/batch.go
@@ -0,0 +1,65 @@
+// internal/domain/batch.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// BatchService 批量操作服务接口
+type BatchService interface {
+ // 批量虚拟机操作
+ BatchStartVMs(ctx context.Context, connID string, vmNames []string) (*BatchResult, error)
+ BatchStopVMs(ctx context.Context, connID string, vmNames []string, force bool) (*BatchResult, error)
+ BatchRebootVMs(ctx context.Context, connID string, vmNames []string) (*BatchResult, error)
+ BatchDeleteVMs(ctx context.Context, connID string, vmNames []string, removeStorage bool) (*BatchResult, error)
+
+ // 批量快照操作
+ BatchCreateSnapshots(ctx context.Context, connID string, vmNames []string, config *SnapshotConfig) (*BatchResult, error)
+ BatchDeleteSnapshots(ctx context.Context, connID string, operations []SnapshotOperation) (*BatchResult, error)
+
+ // 批量配置更新
+ BatchUpdateVMs(ctx context.Context, connID string, updates []VMUpdate) (*BatchResult, error)
+
+ // 获取批量操作状态
+ GetBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error)
+}
+
+// BatchResult 批量操作结果
+type BatchResult struct {
+ BatchID string `json:"batch_id"`
+ Total int `json:"total"`
+ Successful int `json:"successful"`
+ Failed int `json:"failed"`
+ Results []OperationResult `json:"results"`
+ StartTime time.Time `json:"start_time"`
+ EndTime time.Time `json:"end_time"`
+ Duration time.Duration `json:"duration"`
+}
+
+// OperationResult 单个操作结果
+type OperationResult struct {
+ Target string `json:"target"` // VM 名称或其他目标
+ Success bool `json:"success"`
+ Error string `json:"error,omitempty"`
+}
+
+// BatchStatus 批量操作状态
+type BatchStatus struct {
+ BatchID string `json:"batch_id"`
+ Status string `json:"status"` // running, completed, failed
+ Progress float64 `json:"progress"`
+ Results *BatchResult `json:"results,omitempty"`
+}
+
+// VMUpdate 虚拟机更新操作
+type VMUpdate struct {
+ VMName string `json:"vm_name"`
+ Config *VMConfig `json:"config"`
+}
+
+// SnapshotOperation 快照操作
+type SnapshotOperation struct {
+ VMName string `json:"vm_name"`
+ SnapshotName string `json:"snapshot_name"`
+}
diff --git a/internal/domain/event.go b/internal/domain/event.go
new file mode 100644
index 0000000000000000000000000000000000000000..1763f58912001ad5e5b29ea1af67cd294ecba472
--- /dev/null
+++ b/internal/domain/event.go
@@ -0,0 +1,68 @@
+// internal/domain/event.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// EventService 事件服务接口
+type EventService interface {
+ // 订阅事件
+ Subscribe(ctx context.Context, filter *EventFilter) (<-chan *Event, error)
+
+ // 取消订阅
+ Unsubscribe(subscriptionID string)
+
+ // 获取历史事件
+ GetEvents(ctx context.Context, filter *EventFilter, limit int) ([]*Event, error)
+
+ // 启动事件监听
+ StartListening(ctx context.Context, connID string) error
+
+ // 停止事件监听
+ StopListening(ctx context.Context, connID string) error
+}
+
+// Event 事件
+type Event struct {
+ ID string `json:"id"`
+ Type EventType `json:"type"`
+ Category EventCategory `json:"category"`
+ Source string `json:"source"` // 连接 ID
+ Target string `json:"target"` // VM/网络/存储池名称
+ Action string `json:"action"` // started, stopped, created, deleted, etc.
+ Detail string `json:"detail"`
+ Metadata map[string]any `json:"metadata,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type EventType string
+
+const (
+ EventTypeVM EventType = "vm"
+ EventTypeNetwork EventType = "network"
+ EventTypeStorage EventType = "storage"
+ EventTypeHost EventType = "host"
+ EventTypeError EventType = "error"
+)
+
+type EventCategory string
+
+const (
+ EventCategoryLifecycle EventCategory = "lifecycle"
+ EventCategoryConfig EventCategory = "config"
+ EventCategoryHealth EventCategory = "health"
+ EventCategoryError EventCategory = "error"
+)
+
+// EventFilter 事件过滤器
+type EventFilter struct {
+ Types []EventType `json:"types,omitempty"`
+ Categories []EventCategory `json:"categories,omitempty"`
+ Sources []string `json:"sources,omitempty"` // 连接 ID 列表
+ Targets []string `json:"targets,omitempty"` // VM 名称列表
+ Actions []string `json:"actions,omitempty"`
+ StartTime *time.Time `json:"start_time,omitempty"`
+ EndTime *time.Time `json:"end_time,omitempty"`
+}
diff --git a/internal/domain/interfaces.go b/internal/domain/interfaces.go
index 3b3ec61e1a10b55398368292fed2aaa5edc10561..d56aaffae8fff5223d28814bf033962abbec4dfa 100644
--- a/internal/domain/interfaces.go
+++ b/internal/domain/interfaces.go
@@ -66,7 +66,7 @@ type ConsoleProxy interface {
type XMLBuilder interface {
BuildVMXML(config *VMCreateConfig) (string, error)
BuildDiskXML(disk *DiskConfig) (string, error)
- BuildNetworkXML(net *NetworkConfig) (string, error)
+ // BuildNetworkXML(net *NetworkConfig) (string, error)
ParseVMXML(xmlData string) (*VMCreateConfig, error)
CloneVMXML(srcXML, newName string) (string, error)
}
@@ -145,13 +145,13 @@ type DiskConfig struct {
Bootable bool `json:"bootable"`
}
-// NetworkConfig 网络配置
-type NetworkConfig struct {
- Type string `json:"type"` // network, bridge
- Source string `json:"source"` // 网络名称或桥接名
- Model string `json:"model"` // virtio, e1000
- MAC string `json:"mac"` // MAC 地址(可选)
-}
+// // NetworkConfig 网络配置
+// type NetworkConfig struct {
+// Type string `json:"type"` // network, bridge
+// Source string `json:"source"` // 网络名称或桥接名
+// Model string `json:"model"` // virtio, e1000
+// MAC string `json:"mac"` // MAC 地址(可选)
+// }
// GraphicsConfig 图形配置
type GraphicsConfig struct {
@@ -263,16 +263,16 @@ type DiskInfo struct {
Bootable bool `json:"bootable"`
}
-// NetworkInfo 网络信息
-type NetworkInfo struct {
- Type string `json:"type"`
- Source string `json:"source"`
- MAC string `json:"mac"`
- Model string `json:"model"`
- State string `json:"state"`
- RxBytes uint64 `json:"rx_bytes"`
- TxBytes uint64 `json:"tx_bytes"`
-}
+// // NetworkInfo 网络信息
+// type NetworkInfo struct {
+// Type string `json:"type"`
+// Source string `json:"source"`
+// MAC string `json:"mac"`
+// Model string `json:"model"`
+// State string `json:"state"`
+// RxBytes uint64 `json:"rx_bytes"`
+// TxBytes uint64 `json:"tx_bytes"`
+// }
// GraphicsInfo 图形信息
type GraphicsInfo struct {
diff --git a/internal/domain/migration.go b/internal/domain/migration.go
new file mode 100644
index 0000000000000000000000000000000000000000..9b6e551e19987dd8918e8c58431f1ec79ed6719c
--- /dev/null
+++ b/internal/domain/migration.go
@@ -0,0 +1,66 @@
+// internal/domain/migration.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// MigrationService 迁移服务接口
+type MigrationService interface {
+ // 实时迁移
+ MigrateVM(ctx context.Context, srcConnID, vmName, destURI string, config *MigrationConfig) (*MigrationJob, error)
+
+ // 获取迁移任务状态
+ GetMigrationStatus(ctx context.Context, jobID string) (*MigrationJob, error)
+
+ // 取消迁移
+ CancelMigration(ctx context.Context, jobID string) error
+
+ // 离线迁移(导出/导入)
+ ExportVM(ctx context.Context, connID, vmName, exportPath string) error
+ ImportVM(ctx context.Context, connID, importPath string) (*VMInfo, error)
+}
+
+// MigrationConfig 迁移配置
+type MigrationConfig struct {
+ Live bool `json:"live"` // 实时迁移
+ Persistent bool `json:"persistent"` // 持久化定义
+ Undefine bool `json:"undefine"` // 迁移后删除源
+ Offline bool `json:"offline"` // 离线迁移
+ Compressed bool `json:"compressed"` // 压缩传输
+ Bandwidth uint64 `json:"bandwidth"` // 带宽限制 (MB/s)
+ Tunnelled bool `json:"tunnelled"` // 通过 libvirtd 隧道
+ SharedStorage bool `json:"shared_storage"` // 共享存储
+ AutoConverge bool `json:"auto_converge"` // 自动收敛
+ PostCopy bool `json:"post_copy"` // 后复制模式
+ Parallel int `json:"parallel"` // 并行连接数
+ Timeout time.Duration `json:"timeout"` // 超时时间
+}
+
+// MigrationJob 迁移任务
+type MigrationJob struct {
+ ID string `json:"id"`
+ VMName string `json:"vm_name"`
+ SourceURI string `json:"source_uri"`
+ DestURI string `json:"dest_uri"`
+ Status MigrationStatus `json:"status"`
+ Progress float64 `json:"progress"` // 0-100
+ DataRemaining uint64 `json:"data_remaining"` // bytes
+ DataTotal uint64 `json:"data_total"` // bytes
+ MemRemaining uint64 `json:"mem_remaining"` // bytes
+ MemTotal uint64 `json:"mem_total"` // bytes
+ StartTime time.Time `json:"start_time"`
+ EndTime *time.Time `json:"end_time,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type MigrationStatus string
+
+const (
+ MigrationStatusPending MigrationStatus = "pending"
+ MigrationStatusRunning MigrationStatus = "running"
+ MigrationStatusCompleted MigrationStatus = "completed"
+ MigrationStatusFailed MigrationStatus = "failed"
+ MigrationStatusCancelled MigrationStatus = "cancelled"
+)
diff --git a/internal/domain/network.go b/internal/domain/network.go
new file mode 100644
index 0000000000000000000000000000000000000000..48013f0706a0b7295ba8893859ab4037eaa071cb
--- /dev/null
+++ b/internal/domain/network.go
@@ -0,0 +1,96 @@
+// internal/domain/network.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// NetworkService 虚拟网络服务接口
+type NetworkService interface {
+ // 网络管理
+ ListNetworks(ctx context.Context, connID string) ([]*NetworkInfo, error)
+ GetNetwork(ctx context.Context, connID, networkName string) (*NetworkInfo, error)
+ CreateNetwork(ctx context.Context, connID string, config *NetworkConfig) (*NetworkInfo, error)
+ DeleteNetwork(ctx context.Context, connID, networkName string) error
+
+ // 网络操作
+ StartNetwork(ctx context.Context, connID, networkName string) error
+ StopNetwork(ctx context.Context, connID, networkName string) error
+ SetAutostart(ctx context.Context, connID, networkName string, autostart bool) error
+
+ // DHCP 租约管理
+ GetDHCPLeases(ctx context.Context, connID, networkName string) ([]*DHCPLease, error)
+}
+
+// NetworkInfo 网络信息
+type NetworkInfo struct {
+ Name string `json:"name"`
+ UUID string `json:"uuid"`
+ Active bool `json:"active"`
+ Persistent bool `json:"persistent"`
+ Autostart bool `json:"autostart"`
+ Bridge string `json:"bridge"`
+ Forward ForwardMode `json:"forward"`
+ IP []NetworkIP `json:"ip"`
+ Domain string `json:"domain"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type ForwardMode string
+
+const (
+ ForwardNone ForwardMode = "none" // 隔离网络
+ ForwardNAT ForwardMode = "nat" // NAT 模式
+ ForwardRoute ForwardMode = "route" // 路由模式
+ ForwardBridge ForwardMode = "bridge" // 桥接模式
+)
+
+type NetworkIP struct {
+ Address string `json:"address"`
+ Netmask string `json:"netmask"`
+ DHCP *NetworkDHCP `json:"dhcp,omitempty"`
+}
+
+type NetworkDHCP struct {
+ Start string `json:"start"`
+ End string `json:"end"`
+ Hosts []DHCPHost `json:"hosts,omitempty"`
+}
+
+type DHCPHost struct {
+ MAC string `json:"mac"`
+ Name string `json:"name"`
+ IP string `json:"ip"`
+}
+
+// NetworkConfig 网络创建配置
+type NetworkConfig struct {
+ Name string `json:"name" binding:"required"`
+ Bridge string `json:"bridge"`
+ Forward ForwardMode `json:"forward"`
+ Domain string `json:"domain"`
+ IP []NetworkIPConfig `json:"ip"`
+ Autostart bool `json:"autostart"`
+}
+
+type NetworkIPConfig struct {
+ Address string `json:"address" binding:"required"`
+ Netmask string `json:"netmask" binding:"required"`
+ DHCP *NetworkDHCPConfig `json:"dhcp,omitempty"`
+}
+
+type NetworkDHCPConfig struct {
+ Start string `json:"start" binding:"required"`
+ End string `json:"end" binding:"required"`
+ Hosts []DHCPHost `json:"hosts,omitempty"`
+}
+
+// DHCPLease DHCP 租约
+type DHCPLease struct {
+ IPAddress string `json:"ip_address"`
+ MACAddress string `json:"mac_address"`
+ Hostname string `json:"hostname"`
+ ExpiryTime time.Time `json:"expiry_time"`
+ Type string `json:"type"` // ipv4, ipv6
+}
diff --git a/internal/domain/snapshot.go b/internal/domain/snapshot.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d55c7db07de8af02ffffbb73f704fed4299adcb
--- /dev/null
+++ b/internal/domain/snapshot.go
@@ -0,0 +1,52 @@
+// internal/domain/snapshot.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// SnapshotService 快照服务接口
+type SnapshotService interface {
+ // 快照管理
+ ListSnapshots(ctx context.Context, connID, vmName string) ([]*SnapshotInfo, error)
+ GetSnapshot(ctx context.Context, connID, vmName, snapshotName string) (*SnapshotInfo, error)
+ CreateSnapshot(ctx context.Context, connID, vmName string, config *SnapshotConfig) (*SnapshotInfo, error)
+ DeleteSnapshot(ctx context.Context, connID, vmName, snapshotName string, deleteChildren bool) error
+
+ // 快照操作
+ RevertToSnapshot(ctx context.Context, connID, vmName, snapshotName string) error
+ GetCurrentSnapshot(ctx context.Context, connID, vmName string) (*SnapshotInfo, error)
+}
+
+// SnapshotInfo 快照信息
+type SnapshotInfo struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ State SnapshotState `json:"state"`
+ Parent string `json:"parent,omitempty"`
+ Children []string `json:"children,omitempty"`
+ IsCurrent bool `json:"is_current"`
+ CreatedAt time.Time `json:"created_at"`
+ DiskSize uint64 `json:"disk_size"`
+ Memory bool `json:"memory"`
+}
+
+type SnapshotState string
+
+const (
+ SnapshotStateRunning SnapshotState = "running"
+ SnapshotStateShutoff SnapshotState = "shutoff"
+ SnapshotStatePaused SnapshotState = "paused"
+ SnapshotStateDiskOnly SnapshotState = "disk-only"
+)
+
+// SnapshotConfig 快照配置
+type SnapshotConfig struct {
+ Name string `json:"name" binding:"required"`
+ Description string `json:"description"`
+ Memory bool `json:"memory"` // 包含内存状态
+ Quiesce bool `json:"quiesce"` // 文件系统静默
+ Atomic bool `json:"atomic"` // 原子操作
+ DiskOnly bool `json:"disk_only"` // 仅磁盘快照
+}
diff --git a/internal/domain/storage.go b/internal/domain/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..3626e93eab40852e8dc174885819d28842e45a04
--- /dev/null
+++ b/internal/domain/storage.go
@@ -0,0 +1,132 @@
+// internal/domain/storage.go
+package domain
+
+import (
+ "context"
+ "time"
+)
+
+// StoragePoolService 存储池服务接口
+type StoragePoolService interface {
+ // 存储池管理
+ ListPools(ctx context.Context, connID string) ([]*StoragePoolInfo, error)
+ GetPool(ctx context.Context, connID, poolName string) (*StoragePoolInfo, error)
+ CreatePool(ctx context.Context, connID string, config *StoragePoolConfig) (*StoragePoolInfo, error)
+ DeletePool(ctx context.Context, connID, poolName string) error
+
+ // 存储池操作
+ StartPool(ctx context.Context, connID, poolName string) error
+ StopPool(ctx context.Context, connID, poolName string) error
+ RefreshPool(ctx context.Context, connID, poolName string) error
+ SetAutostart(ctx context.Context, connID, poolName string, autostart bool) error
+
+ // 存储卷管理
+ ListVolumes(ctx context.Context, connID, poolName string) ([]*StorageVolumeInfo, error)
+ GetVolume(ctx context.Context, connID, poolName, volumeName string) (*StorageVolumeInfo, error)
+ CreateVolume(ctx context.Context, connID, poolName string, config *StorageVolumeConfig) (*StorageVolumeInfo, error)
+ CloneVolume(ctx context.Context, connID, poolName, srcVolume, destVolume string) error
+ DeleteVolume(ctx context.Context, connID, poolName, volumeName string) error
+ ResizeVolume(ctx context.Context, connID, poolName, volumeName string, newSize uint64) error
+}
+
+// StoragePoolInfo 存储池信息
+type StoragePoolInfo struct {
+ Name string `json:"name"`
+ UUID string `json:"uuid"`
+ State StoragePoolState `json:"state"`
+ Type string `json:"type"`
+ Capacity uint64 `json:"capacity"` // bytes
+ Allocation uint64 `json:"allocation"` // bytes
+ Available uint64 `json:"available"` // bytes
+ Autostart bool `json:"autostart"`
+ Persistent bool `json:"persistent"`
+ Path string `json:"path"`
+ Volumes int `json:"volumes"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type StoragePoolState string
+
+const (
+ PoolStateInactive StoragePoolState = "inactive"
+ PoolStateBuilding StoragePoolState = "building"
+ PoolStateRunning StoragePoolState = "running"
+ PoolStateDegraded StoragePoolState = "degraded"
+ PoolStateInaccessible StoragePoolState = "inaccessible"
+)
+
+// StoragePoolConfig 存储池配置
+type StoragePoolConfig struct {
+ Name string `json:"name" binding:"required"`
+ Type string `json:"type" binding:"required"` // dir, fs, netfs, logical, disk, iscsi, scsi, rbd, sheepdog, gluster, zfs
+ Target *PoolTargetConfig `json:"target"`
+ Source *PoolSourceConfig `json:"source,omitempty"`
+ Autostart bool `json:"autostart"`
+}
+
+type PoolTargetConfig struct {
+ Path string `json:"path"`
+ Permissions *PoolPermissionsConfig `json:"permissions,omitempty"`
+}
+
+type PoolPermissionsConfig struct {
+ Mode string `json:"mode"` // e.g., "0755"
+ Owner int `json:"owner"` // UID
+ Group int `json:"group"` // GID
+}
+
+type PoolSourceConfig struct {
+ Host string `json:"host,omitempty"`
+ Dir string `json:"dir,omitempty"`
+ Device []PoolSourceDevice `json:"devices,omitempty"`
+ Name string `json:"name,omitempty"`
+ Format *PoolSourceFormatConfig `json:"format,omitempty"`
+}
+
+type PoolSourceDevice struct {
+ Path string `json:"path"`
+}
+
+type PoolSourceFormatConfig struct {
+ Type string `json:"type"`
+}
+
+// StorageVolumeInfo 存储卷信息
+type StorageVolumeInfo struct {
+ Name string `json:"name"`
+ Key string `json:"key"`
+ Path string `json:"path"`
+ Type StorageVolumeType `json:"type"`
+ Format string `json:"format"`
+ Capacity uint64 `json:"capacity"` // bytes
+ Allocation uint64 `json:"allocation"` // bytes
+ Target VolumeTargetInfo `json:"target"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type StorageVolumeType string
+
+const (
+ VolumeTypeFile StorageVolumeType = "file"
+ VolumeTypeBlock StorageVolumeType = "block"
+ VolumeTypeDir StorageVolumeType = "dir"
+ VolumeTypeNetwork StorageVolumeType = "network"
+)
+
+type VolumeTargetInfo struct {
+ Path string `json:"path"`
+ Format string `json:"format"`
+ Permissions struct {
+ Mode string `json:"mode"`
+ Owner int `json:"owner"`
+ Group int `json:"group"`
+ } `json:"permissions"`
+}
+
+// StorageVolumeConfig 存储卷配置
+type StorageVolumeConfig struct {
+ Name string `json:"name" binding:"required"`
+ Capacity uint64 `json:"capacity" binding:"required"` // bytes
+ Allocation uint64 `json:"allocation"` // bytes (预分配)
+ Format string `json:"format"` // qcow2, raw, etc.
+}
diff --git a/internal/libvirt/xmlbuilder/xml_builder.go b/internal/libvirt/xmlbuilder/xml_builder.go
index 6cfdb3f77fd03985386cfcd891fa08b73404fa19..664f89903d59cac4d0ef8a5da42c601b4fd824d7 100644
--- a/internal/libvirt/xmlbuilder/xml_builder.go
+++ b/internal/libvirt/xmlbuilder/xml_builder.go
@@ -126,22 +126,22 @@ func (b *xmlBuilder) BuildVMXML(config *domain.VMCreateConfig) (string, error) {
domainConfig.OS.BootDevices[i] = libvirtxml.DomainBootDevice{Dev: dev}
}
- // 添加网络接口
- if len(config.Networks) == 0 {
- // 默认网络
- config.Networks = []*domain.NetworkConfig{
- {
- Type: "network",
- Source: "default",
- Model: "virtio",
- },
- }
- }
- for _, netCfg := range config.Networks {
- if err := b.addNetwork(domainConfig, netCfg); err != nil {
- return "", err
- }
- }
+ // // 添加网络接口
+ // if len(config.Networks) == 0 {
+ // // 默认网络
+ // config.Networks = []*domain.NetworkConfig{
+ // {
+ // Type: "network",
+ // Source: "default",
+ // Model: "virtio",
+ // },
+ // }
+ // }
+ // for _, netCfg := range config.Networks {
+ // if err := b.addNetwork(domainConfig, netCfg); err != nil {
+ // return "", err
+ // }
+ // }
// 添加图形设备
if config.Graphics != nil {
@@ -248,40 +248,40 @@ func (b *xmlBuilder) addCDROM(domainConfig *libvirtxml.Domain, isoPath string, b
}
// addNetwork 添加网络接口
-func (b *xmlBuilder) addNetwork(domainConfig *libvirtxml.Domain, netCfg *domain.NetworkConfig) error {
- if netCfg.Model == "" {
- netCfg.Model = "virtio"
- }
-
- iface := libvirtxml.DomainInterface{
- Model: &libvirtxml.DomainInterfaceModel{
- Type: netCfg.Model,
- },
- }
-
- // 设置 MAC 地址
- if netCfg.MAC != "" {
- iface.MAC = &libvirtxml.DomainInterfaceMAC{Address: netCfg.MAC}
- }
-
- // 设置网络源
- if netCfg.Type == "network" {
- iface.Source = &libvirtxml.DomainInterfaceSource{
- Network: &libvirtxml.DomainInterfaceSourceNetwork{
- Network: netCfg.Source,
- },
- }
- } else if netCfg.Type == "bridge" {
- iface.Source = &libvirtxml.DomainInterfaceSource{
- Bridge: &libvirtxml.DomainInterfaceSourceBridge{
- Bridge: netCfg.Source,
- },
- }
- }
-
- domainConfig.Devices.Interfaces = append(domainConfig.Devices.Interfaces, iface)
- return nil
-}
+// func (b *xmlBuilder) addNetwork(domainConfig *libvirtxml.Domain, netCfg *domain.NetworkConfig) error {
+// if netCfg.Model == "" {
+// netCfg.Model = "virtio"
+// }
+
+// iface := libvirtxml.DomainInterface{
+// Model: &libvirtxml.DomainInterfaceModel{
+// Type: netCfg.Model,
+// },
+// }
+
+// // 设置 MAC 地址
+// if netCfg.MAC != "" {
+// iface.MAC = &libvirtxml.DomainInterfaceMAC{Address: netCfg.MAC}
+// }
+
+// // 设置网络源
+// if netCfg.Type == "network" {
+// iface.Source = &libvirtxml.DomainInterfaceSource{
+// Network: &libvirtxml.DomainInterfaceSourceNetwork{
+// Network: netCfg.Source,
+// },
+// }
+// } else if netCfg.Type == "bridge" {
+// iface.Source = &libvirtxml.DomainInterfaceSource{
+// Bridge: &libvirtxml.DomainInterfaceSourceBridge{
+// Bridge: netCfg.Source,
+// },
+// }
+// }
+
+// domainConfig.Devices.Interfaces = append(domainConfig.Devices.Interfaces, iface)
+// return nil
+// }
// addGraphics 添加图形设备
func (b *xmlBuilder) addGraphics(domainConfig *libvirtxml.Domain, gfxCfg *domain.GraphicsConfig) {
@@ -399,22 +399,22 @@ func (b *xmlBuilder) BuildDiskXML(disk *domain.DiskConfig) (string, error) {
}
// BuildNetworkXML 构建单个网络接口 XML
-func (b *xmlBuilder) BuildNetworkXML(net *domain.NetworkConfig) (string, error) {
- domainConfig := &libvirtxml.Domain{
- Devices: &libvirtxml.DomainDeviceList{},
- }
+// func (b *xmlBuilder) BuildNetworkXML(net *domain.NetworkConfig) (string, error) {
+// domainConfig := &libvirtxml.Domain{
+// Devices: &libvirtxml.DomainDeviceList{},
+// }
- if err := b.addNetwork(domainConfig, net); err != nil {
- return "", err
- }
+// if err := b.addNetwork(domainConfig, net); err != nil {
+// return "", err
+// }
- xml, err := domainConfig.Marshal()
- if err != nil {
- return "", err
- }
+// xml, err := domainConfig.Marshal()
+// if err != nil {
+// return "", err
+// }
- return xml, nil
-}
+// return xml, nil
+// }
// ParseVMXML 解析虚拟机 XML
func (b *xmlBuilder) ParseVMXML(xmlData string) (*domain.VMCreateConfig, error) {
@@ -470,31 +470,31 @@ func (b *xmlBuilder) ParseVMXML(xmlData string) (*domain.VMCreateConfig, error)
config.Disks = append(config.Disks, diskCfg)
}
- // 解析网络
- config.Networks = make([]*domain.NetworkConfig, 0)
- for _, iface := range domainConfig.Devices.Interfaces {
- netCfg := &domain.NetworkConfig{}
+ // // 解析网络
+ // config.Networks = make([]*domain.NetworkConfig, 0)
+ // for _, iface := range domainConfig.Devices.Interfaces {
+ // netCfg := &domain.NetworkConfig{}
- if iface.Model != nil {
- netCfg.Model = iface.Model.Type
- }
+ // if iface.Model != nil {
+ // netCfg.Model = iface.Model.Type
+ // }
- if iface.MAC != nil {
- netCfg.MAC = iface.MAC.Address
- }
+ // if iface.MAC != nil {
+ // netCfg.MAC = iface.MAC.Address
+ // }
- if iface.Source != nil {
- if iface.Source.Network != nil {
- netCfg.Type = "network"
- netCfg.Source = iface.Source.Network.Network
- } else if iface.Source.Bridge != nil {
- netCfg.Type = "bridge"
- netCfg.Source = iface.Source.Bridge.Bridge
- }
- }
+ // if iface.Source != nil {
+ // if iface.Source.Network != nil {
+ // netCfg.Type = "network"
+ // netCfg.Source = iface.Source.Network.Network
+ // } else if iface.Source.Bridge != nil {
+ // netCfg.Type = "bridge"
+ // netCfg.Source = iface.Source.Bridge.Bridge
+ // }
+ // }
- config.Networks = append(config.Networks, netCfg)
- }
+ // config.Networks = append(config.Networks, netCfg)
+ // }
return config, nil
}
diff --git a/internal/service/batch_service.go b/internal/service/batch_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec5c35707b1aa0b14ea0c68a031b8a79a48655d4
--- /dev/null
+++ b/internal/service/batch_service.go
@@ -0,0 +1,360 @@
+// ============================================
+// internal/service/batch_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+
+ "virt-manager-go/internal/domain"
+)
+
+type batchService struct {
+ vmService domain.VMService
+ snapshotService domain.SnapshotService
+ logger *logrus.Logger
+
+ // 批量操作状态跟踪
+ batches map[string]*domain.BatchStatus
+ batchesMu sync.RWMutex
+}
+
+func NewBatchService(
+ vmService domain.VMService,
+ snapshotService domain.SnapshotService,
+ logger *logrus.Logger,
+) domain.BatchService {
+ return &batchService{
+ vmService: vmService,
+ snapshotService: snapshotService,
+ logger: logger,
+ batches: make(map[string]*domain.BatchStatus),
+ }
+}
+
+// BatchStartVMs 批量启动虚拟机
+func (s *batchService) BatchStartVMs(ctx context.Context, connID string, vmNames []string) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(vmNames),
+ }).Info("Starting batch VM start operation")
+
+ return s.executeBatchOperation(ctx, "start", vmNames, func(ctx context.Context, vmName string) error {
+ return s.vmService.StartVM(ctx, connID, vmName)
+ })
+}
+
+// BatchStopVMs 批量停止虚拟机
+func (s *batchService) BatchStopVMs(ctx context.Context, connID string, vmNames []string, force bool) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(vmNames),
+ "force": force,
+ }).Info("Starting batch VM stop operation")
+
+ return s.executeBatchOperation(ctx, "stop", vmNames, func(ctx context.Context, vmName string) error {
+ return s.vmService.ShutdownVM(ctx, connID, vmName, force)
+ })
+}
+
+// BatchRebootVMs 批量重启虚拟机
+func (s *batchService) BatchRebootVMs(ctx context.Context, connID string, vmNames []string) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(vmNames),
+ }).Info("Starting batch VM reboot operation")
+
+ return s.executeBatchOperation(ctx, "reboot", vmNames, func(ctx context.Context, vmName string) error {
+ return s.vmService.RebootVM(ctx, connID, vmName)
+ })
+}
+
+// BatchDeleteVMs 批量删除虚拟机
+func (s *batchService) BatchDeleteVMs(ctx context.Context, connID string, vmNames []string, removeStorage bool) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(vmNames),
+ "remove_storage": removeStorage,
+ }).Info("Starting batch VM delete operation")
+
+ return s.executeBatchOperation(ctx, "delete", vmNames, func(ctx context.Context, vmName string) error {
+ return s.vmService.DeleteVM(ctx, connID, vmName, removeStorage)
+ })
+}
+
+// BatchCreateSnapshots 批量创建快照
+func (s *batchService) BatchCreateSnapshots(ctx context.Context, connID string, vmNames []string, config *domain.SnapshotConfig) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(vmNames),
+ "snapshot_name": config.Name,
+ }).Info("Starting batch snapshot creation")
+
+ return s.executeBatchOperation(ctx, "create_snapshot", vmNames, func(ctx context.Context, vmName string) error {
+ // 为每个虚拟机创建唯一的快照名称
+ snapConfig := *config
+ snapConfig.Name = fmt.Sprintf("%s-%s", config.Name, vmName)
+
+ _, err := s.snapshotService.CreateSnapshot(ctx, connID, vmName, &snapConfig)
+ return err
+ })
+}
+
+// BatchDeleteSnapshots 批量删除快照
+func (s *batchService) BatchDeleteSnapshots(ctx context.Context, connID string, operations []domain.SnapshotOperation) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(operations),
+ }).Info("Starting batch snapshot deletion")
+
+ batchID := uuid.New().String()
+ result := &domain.BatchResult{
+ BatchID: batchID,
+ Total: len(operations),
+ Results: make([]domain.OperationResult, 0, len(operations)),
+ StartTime: time.Now(),
+ }
+
+ // 并发执行
+ var wg sync.WaitGroup
+ resultsCh := make(chan domain.OperationResult, len(operations))
+
+ for _, op := range operations {
+ wg.Add(1)
+ go func(op domain.SnapshotOperation) {
+ defer wg.Done()
+
+ target := fmt.Sprintf("%s/%s", op.VMName, op.SnapshotName)
+ err := s.snapshotService.DeleteSnapshot(ctx, connID, op.VMName, op.SnapshotName, false)
+
+ opResult := domain.OperationResult{
+ Target: target,
+ Success: err == nil,
+ }
+ if err != nil {
+ opResult.Error = err.Error()
+ }
+
+ resultsCh <- opResult
+ }(op)
+ }
+
+ // 等待所有操作完成
+ go func() {
+ wg.Wait()
+ close(resultsCh)
+ }()
+
+ // 收集结果
+ for opResult := range resultsCh {
+ result.Results = append(result.Results, opResult)
+ if opResult.Success {
+ result.Successful++
+ } else {
+ result.Failed++
+ }
+ }
+
+ result.EndTime = time.Now()
+ result.Duration = result.EndTime.Sub(result.StartTime)
+
+ return result, nil
+}
+
+// BatchUpdateVMs 批量更新虚拟机配置
+func (s *batchService) BatchUpdateVMs(ctx context.Context, connID string, updates []domain.VMUpdate) (*domain.BatchResult, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "count": len(updates),
+ }).Info("Starting batch VM update operation")
+
+ batchID := uuid.New().String()
+ result := &domain.BatchResult{
+ BatchID: batchID,
+ Total: len(updates),
+ Results: make([]domain.OperationResult, 0, len(updates)),
+ StartTime: time.Now(),
+ }
+
+ // 并发执行
+ var wg sync.WaitGroup
+ resultsCh := make(chan domain.OperationResult, len(updates))
+
+ for _, update := range updates {
+ wg.Add(1)
+ go func(update domain.VMUpdate) {
+ defer wg.Done()
+
+ err := s.vmService.UpdateVMConfig(ctx, connID, update.VMName, update.Config)
+
+ opResult := domain.OperationResult{
+ Target: update.VMName,
+ Success: err == nil,
+ }
+ if err != nil {
+ opResult.Error = err.Error()
+ }
+
+ resultsCh <- opResult
+ }(update)
+ }
+
+ // 等待所有操作完成
+ go func() {
+ wg.Wait()
+ close(resultsCh)
+ }()
+
+ // 收集结果
+ for opResult := range resultsCh {
+ result.Results = append(result.Results, opResult)
+ if opResult.Success {
+ result.Successful++
+ } else {
+ result.Failed++
+ }
+ }
+
+ result.EndTime = time.Now()
+ result.Duration = result.EndTime.Sub(result.StartTime)
+
+ return result, nil
+}
+
+// GetBatchStatus 获取批量操作状态
+func (s *batchService) GetBatchStatus(ctx context.Context, batchID string) (*domain.BatchStatus, error) {
+ s.batchesMu.RLock()
+ defer s.batchesMu.RUnlock()
+
+ status, exists := s.batches[batchID]
+ if !exists {
+ return nil, fmt.Errorf("batch operation not found")
+ }
+
+ return status, nil
+}
+
+// === 辅助方法 ===
+
+// executeBatchOperation 执行批量操作的通用方法
+func (s *batchService) executeBatchOperation(
+ ctx context.Context,
+ operation string,
+ targets []string,
+ operationFunc func(context.Context, string) error,
+) (*domain.BatchResult, error) {
+ batchID := uuid.New().String()
+
+ // 创建批量操作状态
+ status := &domain.BatchStatus{
+ BatchID: batchID,
+ Status: "running",
+ Progress: 0,
+ }
+
+ s.batchesMu.Lock()
+ s.batches[batchID] = status
+ s.batchesMu.Unlock()
+
+ result := &domain.BatchResult{
+ BatchID: batchID,
+ Total: len(targets),
+ Results: make([]domain.OperationResult, 0, len(targets)),
+ StartTime: time.Now(),
+ }
+
+ // 使用信号量限制并发数
+ semaphore := make(chan struct{}, 10) // 最多 10 个并发操作
+ var wg sync.WaitGroup
+ resultsCh := make(chan domain.OperationResult, len(targets))
+
+ // 启动进度更新
+ go s.updateBatchProgress(ctx, batchID, len(targets), resultsCh)
+
+ for _, target := range targets {
+ wg.Add(1)
+ go func(target string) {
+ defer wg.Done()
+
+ // 获取信号量
+ semaphore <- struct{}{}
+ defer func() { <-semaphore }()
+
+ err := operationFunc(ctx, target)
+
+ opResult := domain.OperationResult{
+ Target: target,
+ Success: err == nil,
+ }
+ if err != nil {
+ opResult.Error = err.Error()
+ s.logger.WithError(err).WithFields(logrus.Fields{
+ "operation": operation,
+ "target": target,
+ }).Warn("Batch operation failed for target")
+ }
+
+ resultsCh <- opResult
+ }(target)
+ }
+
+ // 等待所有操作完成
+ go func() {
+ wg.Wait()
+ close(resultsCh)
+ }()
+
+ // 收集结果
+ for opResult := range resultsCh {
+ result.Results = append(result.Results, opResult)
+ if opResult.Success {
+ result.Successful++
+ } else {
+ result.Failed++
+ }
+ }
+
+ result.EndTime = time.Now()
+ result.Duration = result.EndTime.Sub(result.StartTime)
+
+ // 更新最终状态
+ s.batchesMu.Lock()
+ status.Status = "completed"
+ status.Progress = 100
+ status.Results = result
+ s.batchesMu.Unlock()
+
+ s.logger.WithFields(logrus.Fields{
+ "batch_id": batchID,
+ "operation": operation,
+ "total": result.Total,
+ "successful": result.Successful,
+ "failed": result.Failed,
+ "duration": result.Duration,
+ }).Info("Batch operation completed")
+
+ return result, nil
+}
+
+// updateBatchProgress 更新批量操作进度
+func (s *batchService) updateBatchProgress(ctx context.Context, batchID string, total int, resultsCh <-chan domain.OperationResult) {
+ completed := 0
+
+ for range resultsCh {
+ completed++
+ progress := float64(completed) / float64(total) * 100
+
+ s.batchesMu.Lock()
+ if status, exists := s.batches[batchID]; exists {
+ status.Progress = progress
+ }
+ s.batchesMu.Unlock()
+ }
+}
diff --git a/internal/service/event_service.go b/internal/service/event_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d146ccf56de62e85409a9fc2fbc1ca545178d79
--- /dev/null
+++ b/internal/service/event_service.go
@@ -0,0 +1,387 @@
+// ============================================
+// internal/service/event_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirt"
+
+ "virt-manager-go/internal/domain"
+)
+
+type eventService struct {
+ connMgr domain.ConnectionManager
+ logger *logrus.Logger
+
+ // 订阅管理
+ subscriptions map[string]*subscription
+ subscriptionsMu sync.RWMutex
+
+ // 事件历史
+ eventHistory []*domain.Event
+ eventHistoryMu sync.RWMutex
+ maxHistorySize int
+
+ // 事件监听器
+ listeners map[string]*eventListener
+ listenersMu sync.RWMutex
+}
+
+type subscription struct {
+ id string
+ filter *domain.EventFilter
+ eventCh chan *domain.Event
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+type eventListener struct {
+ connID string
+ conn *libvirt.Connect
+ callbackID int
+ stopCh chan struct{}
+ eventService *eventService
+}
+
+func NewEventService(connMgr domain.ConnectionManager, logger *logrus.Logger) domain.EventService {
+ return &eventService{
+ connMgr: connMgr,
+ logger: logger,
+ subscriptions: make(map[string]*subscription),
+ eventHistory: make([]*domain.Event, 0, 1000),
+ maxHistorySize: 1000,
+ listeners: make(map[string]*eventListener),
+ }
+}
+
+// Subscribe 订阅事件
+func (s *eventService) Subscribe(ctx context.Context, filter *domain.EventFilter) (<-chan *domain.Event, error) {
+ subID := uuid.New().String()
+
+ subCtx, cancel := context.WithCancel(ctx)
+ sub := &subscription{
+ id: subID,
+ filter: filter,
+ eventCh: make(chan *domain.Event, 100), // 缓冲 100 个事件
+ ctx: subCtx,
+ cancel: cancel,
+ }
+
+ s.subscriptionsMu.Lock()
+ s.subscriptions[subID] = sub
+ s.subscriptionsMu.Unlock()
+
+ s.logger.WithField("subscription_id", subID).Info("Event subscription created")
+
+ // 自动清理订阅
+ go func() {
+ <-subCtx.Done()
+ s.Unsubscribe(subID)
+ }()
+
+ return sub.eventCh, nil
+}
+
+// Unsubscribe 取消订阅
+func (s *eventService) Unsubscribe(subscriptionID string) {
+ s.subscriptionsMu.Lock()
+ defer s.subscriptionsMu.Unlock()
+
+ if sub, exists := s.subscriptions[subscriptionID]; exists {
+ sub.cancel()
+ close(sub.eventCh)
+ delete(s.subscriptions, subscriptionID)
+ s.logger.WithField("subscription_id", subscriptionID).Info("Event subscription removed")
+ }
+}
+
+// GetEvents 获取历史事件
+func (s *eventService) GetEvents(ctx context.Context, filter *domain.EventFilter, limit int) ([]*domain.Event, error) {
+ s.eventHistoryMu.RLock()
+ defer s.eventHistoryMu.RUnlock()
+
+ if limit <= 0 || limit > len(s.eventHistory) {
+ limit = len(s.eventHistory)
+ }
+
+ // 应用过滤器
+ events := make([]*domain.Event, 0, limit)
+ for i := len(s.eventHistory) - 1; i >= 0 && len(events) < limit; i-- {
+ event := s.eventHistory[i]
+ if s.matchesFilter(event, filter) {
+ events = append(events, event)
+ }
+ }
+
+ return events, nil
+}
+
+// StartListening 启动事件监听
+func (s *eventService) StartListening(ctx context.Context, connID string) error {
+ s.listenersMu.Lock()
+ defer s.listenersMu.Unlock()
+
+ // 检查是否已经在监听
+ if _, exists := s.listeners[connID]; exists {
+ return fmt.Errorf("already listening on connection: %s", connID)
+ }
+
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return err
+ }
+
+ // 注册 libvirt 事件回调
+ callbackID, err := conn.DomainEventLifecycleRegister(nil, func(c *libvirt.Connect, d *libvirt.Domain, event *libvirt.DomainEventLifecycle) {
+ s.handleDomainLifecycleEvent(connID, d, event)
+ })
+
+ if err != nil {
+ return fmt.Errorf("failed to register event callback: %w", err)
+ }
+
+ listener := &eventListener{
+ connID: connID,
+ conn: conn,
+ callbackID: callbackID,
+ stopCh: make(chan struct{}),
+ eventService: s,
+ }
+
+ s.listeners[connID] = listener
+
+ // 启动事件循环
+ go s.runEventLoop(ctx, listener)
+
+ s.logger.WithField("connection", connID).Info("Started event listening")
+
+ return nil
+}
+
+// StopListening 停止事件监听
+func (s *eventService) StopListening(ctx context.Context, connID string) error {
+ s.listenersMu.Lock()
+ defer s.listenersMu.Unlock()
+
+ if listener, exists := s.listeners[connID]; exists {
+ close(listener.stopCh)
+ listener.conn.DomainEventDeregister(listener.callbackID)
+ delete(s.listeners, connID)
+ s.logger.WithField("connection", connID).Info("Stopped event listening")
+ }
+
+ return nil
+}
+
+// runEventLoop 运行事件循环
+func (s *eventService) runEventLoop(ctx context.Context, listener *eventListener) {
+ ticker := time.NewTicker(1 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-listener.stopCh:
+ return
+ case <-ticker.C:
+ // libvirt 事件循环需要定期调用
+ if err := libvirt.EventRunDefaultImpl(); err != nil {
+ s.logger.WithError(err).Error("Event loop error")
+ }
+ }
+ }
+}
+
+// handleDomainLifecycleEvent 处理虚拟机生命周期事件
+func (s *eventService) handleDomainLifecycleEvent(connID string, dom *libvirt.Domain, libvirtEvent *libvirt.DomainEventLifecycle) {
+ domName, _ := dom.GetName()
+
+ action := s.mapLifecycleEvent(libvirtEvent.Event)
+ detail := s.mapLifecycleDetail(libvirtEvent.Detail)
+
+ event := &domain.Event{
+ ID: uuid.New().String(),
+ Type: domain.EventTypeVM,
+ Category: domain.EventCategoryLifecycle,
+ Source: connID,
+ Target: domName,
+ Action: action,
+ Detail: detail,
+ Timestamp: time.Now(),
+ Metadata: map[string]any{
+ "event_type": libvirtEvent.Event,
+ "event_detail": libvirtEvent.Detail,
+ },
+ }
+
+ s.publishEvent(event)
+}
+
+// publishEvent 发布事件
+func (s *eventService) publishEvent(event *domain.Event) {
+ // 添加到历史
+ s.addToHistory(event)
+
+ // 发送给所有订阅者
+ s.subscriptionsMu.RLock()
+ defer s.subscriptionsMu.RUnlock()
+
+ for _, sub := range s.subscriptions {
+ if s.matchesFilter(event, sub.filter) {
+ select {
+ case sub.eventCh <- event:
+ case <-sub.ctx.Done():
+ default:
+ // 通道已满,丢弃事件
+ s.logger.Warn("Event channel full, dropping event")
+ }
+ }
+ }
+
+ s.logger.WithFields(logrus.Fields{
+ "type": event.Type,
+ "target": event.Target,
+ "action": event.Action,
+ }).Debug("Event published")
+}
+
+// addToHistory 添加到历史记录
+func (s *eventService) addToHistory(event *domain.Event) {
+ s.eventHistoryMu.Lock()
+ defer s.eventHistoryMu.Unlock()
+
+ s.eventHistory = append(s.eventHistory, event)
+
+ // 限制历史记录大小
+ if len(s.eventHistory) > s.maxHistorySize {
+ s.eventHistory = s.eventHistory[len(s.eventHistory)-s.maxHistorySize:]
+ }
+}
+
+// matchesFilter 检查事件是否匹配过滤器
+func (s *eventService) matchesFilter(event *domain.Event, filter *domain.EventFilter) bool {
+ if filter == nil {
+ return true
+ }
+
+ // 检查类型
+ if len(filter.Types) > 0 {
+ matched := false
+ for _, t := range filter.Types {
+ if event.Type == t {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+
+ // 检查分类
+ if len(filter.Categories) > 0 {
+ matched := false
+ for _, c := range filter.Categories {
+ if event.Category == c {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+
+ // 检查来源
+ if len(filter.Sources) > 0 {
+ matched := false
+ for _, s := range filter.Sources {
+ if event.Source == s {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+
+ // 检查目标
+ if len(filter.Targets) > 0 {
+ matched := false
+ for _, t := range filter.Targets {
+ if event.Target == t {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+
+ // 检查动作
+ if len(filter.Actions) > 0 {
+ matched := false
+ for _, a := range filter.Actions {
+ if event.Action == a {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+
+ // 检查时间范围
+ if filter.StartTime != nil && event.Timestamp.Before(*filter.StartTime) {
+ return false
+ }
+ if filter.EndTime != nil && event.Timestamp.After(*filter.EndTime) {
+ return false
+ }
+
+ return true
+}
+
+// mapLifecycleEvent 映射生命周期事件
+func (s *eventService) mapLifecycleEvent(event libvirt.DomainEventType) string {
+ switch event {
+ case libvirt.DOMAIN_EVENT_DEFINED:
+ return "defined"
+ case libvirt.DOMAIN_EVENT_UNDEFINED:
+ return "undefined"
+ case libvirt.DOMAIN_EVENT_STARTED:
+ return "started"
+ case libvirt.DOMAIN_EVENT_SUSPENDED:
+ return "suspended"
+ case libvirt.DOMAIN_EVENT_RESUMED:
+ return "resumed"
+ case libvirt.DOMAIN_EVENT_STOPPED:
+ return "stopped"
+ case libvirt.DOMAIN_EVENT_SHUTDOWN:
+ return "shutdown"
+ case libvirt.DOMAIN_EVENT_PMSUSPENDED:
+ return "pmsuspended"
+ case libvirt.DOMAIN_EVENT_CRASHED:
+ return "crashed"
+ default:
+ return "unknown"
+ }
+}
+
+// mapLifecycleDetail 映射生命周期详情
+func (s *eventService) mapLifecycleDetail(detail int) string {
+ // 这里可以根据不同的事件类型映射详细信息
+ return fmt.Sprintf("detail-%d", detail)
+}
diff --git a/internal/service/migration_service.go b/internal/service/migration_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..6551a558ee956ac21517520fa835bf188f2f465e
--- /dev/null
+++ b/internal/service/migration_service.go
@@ -0,0 +1,331 @@
+// ============================================
+// internal/service/migration_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirt"
+
+ "virt-manager-go/internal/domain"
+)
+
+type migrationService struct {
+ connMgr domain.ConnectionManager
+ logger *logrus.Logger
+
+ // 迁移任务跟踪
+ jobs map[string]*domain.MigrationJob
+ jobsMu sync.RWMutex
+}
+
+func NewMigrationService(connMgr domain.ConnectionManager, logger *logrus.Logger) domain.MigrationService {
+ return &migrationService{
+ connMgr: connMgr,
+ logger: logger,
+ jobs: make(map[string]*domain.MigrationJob),
+ }
+}
+
+// MigrateVM 迁移虚拟机
+func (s *migrationService) MigrateVM(ctx context.Context, srcConnID, vmName, destURI string, config *domain.MigrationConfig) (*domain.MigrationJob, error) {
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "dest_uri": destURI,
+ "live": config.Live,
+ }).Info("Starting VM migration")
+
+ // 获取源连接
+ srcConn, err := s.connMgr.GetConnection(srcConnID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get source connection: %w", err)
+ }
+
+ // 查找虚拟机
+ dom, err := srcConn.LookupDomainByName(vmName)
+ if err != nil {
+ return nil, fmt.Errorf("VM not found: %w", err)
+ }
+ defer dom.Free()
+
+ // 创建迁移任务
+ jobID := uuid.New().String()
+ srcURI, _ := srcConn.GetURI()
+
+ job := &domain.MigrationJob{
+ ID: jobID,
+ VMName: vmName,
+ SourceURI: srcURI,
+ DestURI: destURI,
+ Status: domain.MigrationStatusPending,
+ StartTime: time.Now(),
+ }
+
+ s.jobsMu.Lock()
+ s.jobs[jobID] = job
+ s.jobsMu.Unlock()
+
+ // 异步执行迁移
+ go s.performMigration(ctx, dom, destURI, config, job)
+
+ return job, nil
+}
+
+// performMigration 执行迁移
+func (s *migrationService) performMigration(ctx context.Context, dom *libvirt.Domain, destURI string, config *domain.MigrationConfig, job *domain.MigrationJob) {
+ // 更新状态为运行中
+ s.updateJobStatus(job.ID, domain.MigrationStatusRunning, nil)
+
+ // 构建迁移标志
+ flags := s.buildMigrationFlags(config)
+
+ // 设置迁移参数
+ params := &libvirt.DomainMigrateParameters{}
+ if config.Bandwidth > 0 {
+ params.Bandwidth = config.Bandwidth
+ }
+
+ // 创建超时上下文
+ migCtx := ctx
+ if config.Timeout > 0 {
+ var cancel context.CancelFunc
+ migCtx, cancel = context.WithTimeout(ctx, config.Timeout)
+ defer cancel()
+ }
+
+ // 启动迁移监控
+ done := make(chan error, 1)
+ go s.monitorMigration(migCtx, dom, job, done)
+
+ // 执行迁移
+ _, err := dom.Migrate3(nil, params, uint32(flags))
+
+ // 等待监控完成
+ <-done
+
+ if err != nil {
+ s.logger.WithError(err).Error("Migration failed")
+ s.updateJobStatus(job.ID, domain.MigrationStatusFailed, err)
+ return
+ }
+
+ s.logger.WithField("vm", job.VMName).Info("Migration completed successfully")
+ s.updateJobStatus(job.ID, domain.MigrationStatusCompleted, nil)
+}
+
+// monitorMigration 监控迁移进度
+func (s *migrationService) monitorMigration(ctx context.Context, dom *libvirt.Domain, job *domain.MigrationJob, done chan error) {
+ defer close(done)
+
+ ticker := time.NewTicker(2 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ done <- ctx.Err()
+ return
+ case <-ticker.C:
+ // 获取迁移统计信息
+ stats, err := dom.GetJobStats(libvirt.DOMAIN_JOB_STATS_COMPLETED)
+ if err != nil {
+ continue
+ }
+
+ // 更新任务进度
+ s.jobsMu.Lock()
+ if stats.DataTotal > 0 {
+ job.Progress = float64(stats.DataProcessed) / float64(stats.DataTotal) * 100
+ }
+ job.DataTotal = stats.DataTotal
+ job.DataRemaining = stats.DataRemaining
+ job.MemTotal = stats.MemTotal
+ job.MemRemaining = stats.MemRemaining
+ s.jobsMu.Unlock()
+
+ // 检查是否完成
+ if stats.Type == libvirt.DOMAIN_JOB_COMPLETED {
+ return
+ }
+ }
+ }
+}
+
+// GetMigrationStatus 获取迁移状态
+func (s *migrationService) GetMigrationStatus(ctx context.Context, jobID string) (*domain.MigrationJob, error) {
+ s.jobsMu.RLock()
+ defer s.jobsMu.RUnlock()
+
+ job, exists := s.jobs[jobID]
+ if !exists {
+ return nil, fmt.Errorf("migration job not found")
+ }
+
+ return job, nil
+}
+
+// CancelMigration 取消迁移
+func (s *migrationService) CancelMigration(ctx context.Context, jobID string) error {
+ s.jobsMu.RLock()
+ job, exists := s.jobs[jobID]
+ s.jobsMu.RUnlock()
+
+ if !exists {
+ return fmt.Errorf("migration job not found")
+ }
+
+ if job.Status != domain.MigrationStatusRunning {
+ return fmt.Errorf("cannot cancel migration in status: %s", job.Status)
+ }
+
+ // 获取源连接和域
+ srcConn, err := s.connMgr.GetConnection(job.SourceURI)
+ if err != nil {
+ return fmt.Errorf("failed to get source connection: %w", err)
+ }
+
+ dom, err := srcConn.LookupDomainByName(job.VMName)
+ if err != nil {
+ return fmt.Errorf("VM not found: %w", err)
+ }
+ defer dom.Free()
+
+ // 中止迁移
+ if err := dom.AbortJob(); err != nil {
+ return fmt.Errorf("failed to abort migration: %w", err)
+ }
+
+ s.updateJobStatus(jobID, domain.MigrationStatusCancelled, nil)
+ s.logger.WithField("job_id", jobID).Info("Migration cancelled")
+
+ return nil
+}
+
+// ExportVM 导出虚拟机(离线迁移)
+func (s *migrationService) ExportVM(ctx context.Context, connID, vmName, exportPath string) error {
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "export_path": exportPath,
+ }).Info("Exporting VM")
+
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return err
+ }
+
+ dom, err := conn.LookupDomainByName(vmName)
+ if err != nil {
+ return fmt.Errorf("VM not found: %w", err)
+ }
+ defer dom.Free()
+
+ // 确保虚拟机已关闭
+ state, _, err := dom.GetState()
+ if err != nil {
+ return fmt.Errorf("failed to get VM state: %w", err)
+ }
+ if state != libvirt.DOMAIN_SHUTOFF {
+ return fmt.Errorf("VM must be shut off before export")
+ }
+
+ // 获取虚拟机 XML
+ xmlData, err := dom.GetXMLDesc(0)
+ if err != nil {
+ return fmt.Errorf("failed to get VM XML: %w", err)
+ }
+
+ // TODO: 实现实际的导出逻辑
+ // 1. 保存 XML 到文件
+ // 2. 复制所有磁盘文件
+ // 3. 创建 OVF/OVA 包(可选)
+
+ s.logger.WithField("vm", vmName).Info("VM exported successfully")
+ _ = xmlData // 使用 xmlData
+
+ return nil
+}
+
+// ImportVM 导入虚拟机
+func (s *migrationService) ImportVM(ctx context.Context, connID, importPath string) (*domain.VMInfo, error) {
+ s.logger.WithField("import_path", importPath).Info("Importing VM")
+
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO: 实现实际的导入逻辑
+ // 1. 读取 XML 配置
+ // 2. 复制磁盘文件到目标位置
+ // 3. 更新 XML 中的路径
+ // 4. 定义虚拟机
+
+ _ = conn // 使用 conn
+
+ return nil, fmt.Errorf("import not yet implemented")
+}
+
+// === 辅助方法 ===
+
+func (s *migrationService) buildMigrationFlags(config *domain.MigrationConfig) libvirt.DomainMigrateFlags {
+ flags := libvirt.DOMAIN_MIGRATE_LIVE
+
+ if config.Live {
+ flags |= libvirt.DOMAIN_MIGRATE_LIVE
+ }
+ if config.Persistent {
+ flags |= libvirt.DOMAIN_MIGRATE_PERSIST_DEST
+ }
+ if config.Undefine {
+ flags |= libvirt.DOMAIN_MIGRATE_UNDEFINE_SOURCE
+ }
+ if config.Offline {
+ flags |= libvirt.DOMAIN_MIGRATE_OFFLINE
+ }
+ if config.Compressed {
+ flags |= libvirt.DOMAIN_MIGRATE_COMPRESSED
+ }
+ if config.Tunnelled {
+ flags |= libvirt.DOMAIN_MIGRATE_TUNNELLED
+ }
+ if config.SharedStorage {
+ flags |= libvirt.DOMAIN_MIGRATE_NON_SHARED_DISK
+ }
+ if config.AutoConverge {
+ flags |= libvirt.DOMAIN_MIGRATE_AUTO_CONVERGE
+ }
+ if config.PostCopy {
+ flags |= libvirt.DOMAIN_MIGRATE_POSTCOPY
+ }
+ if config.Parallel > 0 {
+ flags |= libvirt.DOMAIN_MIGRATE_PARALLEL
+ }
+
+ return flags
+}
+
+func (s *migrationService) updateJobStatus(jobID string, status domain.MigrationStatus, err error) {
+ s.jobsMu.Lock()
+ defer s.jobsMu.Unlock()
+
+ if job, exists := s.jobs[jobID]; exists {
+ job.Status = status
+ if status == domain.MigrationStatusCompleted || status == domain.MigrationStatusFailed || status == domain.MigrationStatusCancelled {
+ now := time.Now()
+ job.EndTime = &now
+ if status == domain.MigrationStatusCompleted {
+ job.Progress = 100
+ }
+ }
+ if err != nil {
+ job.Error = err.Error()
+ }
+ }
+}
diff --git a/internal/service/network_service.go b/internal/service/network_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..e8b74da28926a21f547b61855dc029af45e0c8a9
--- /dev/null
+++ b/internal/service/network_service.go
@@ -0,0 +1,365 @@
+// ============================================
+// internal/service/network_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirt"
+ "libvirt.org/go/libvirtxml"
+
+ "virt-manager-go/internal/domain"
+)
+
+type networkService struct {
+ connMgr domain.ConnectionManager
+ logger *logrus.Logger
+}
+
+func NewNetworkService(connMgr domain.ConnectionManager, logger *logrus.Logger) domain.NetworkService {
+ return &networkService{
+ connMgr: connMgr,
+ logger: logger,
+ }
+}
+
+// ListNetworks 列出所有网络
+func (s *networkService) ListNetworks(ctx context.Context, connID string) ([]*domain.NetworkInfo, error) {
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ networks, err := conn.ListAllNetworks(libvirt.CONNECT_LIST_NETWORKS_ACTIVE | libvirt.CONNECT_LIST_NETWORKS_INACTIVE)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list networks: %w", err)
+ }
+
+ networkInfos := make([]*domain.NetworkInfo, 0, len(networks))
+ for _, net := range networks {
+ netInfo, err := s.networkToInfo(&net)
+ net.Free()
+ if err != nil {
+ s.logger.WithError(err).Warn("Failed to convert network to info")
+ continue
+ }
+ networkInfos = append(networkInfos, netInfo)
+ }
+
+ return networkInfos, nil
+}
+
+// GetNetwork 获取网络信息
+func (s *networkService) GetNetwork(ctx context.Context, connID, networkName string) (*domain.NetworkInfo, error) {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return nil, err
+ }
+ defer net.Free()
+
+ return s.networkToInfo(net)
+}
+
+// CreateNetwork 创建虚拟网络
+func (s *networkService) CreateNetwork(ctx context.Context, connID string, config *domain.NetworkConfig) (*domain.NetworkInfo, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "network": config.Name,
+ }).Info("Creating virtual network")
+
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ // 构建网络 XML
+ xmlData, err := s.buildNetworkXML(config)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build network XML: %w", err)
+ }
+
+ s.logger.WithField("xml", xmlData).Debug("Generated network XML")
+
+ // 定义网络
+ net, err := conn.NetworkDefineXML(xmlData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to define network: %w", err)
+ }
+ defer net.Free()
+
+ // 启动网络
+ if err := net.Create(); err != nil {
+ return nil, fmt.Errorf("failed to start network: %w", err)
+ }
+
+ // 设置自动启动
+ if config.Autostart {
+ if err := net.SetAutostart(true); err != nil {
+ s.logger.WithError(err).Warn("Failed to set autostart")
+ }
+ }
+
+ s.logger.WithField("network", config.Name).Info("Virtual network created successfully")
+
+ return s.networkToInfo(net)
+}
+
+// DeleteNetwork 删除虚拟网络
+func (s *networkService) DeleteNetwork(ctx context.Context, connID, networkName string) error {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return err
+ }
+ defer net.Free()
+
+ // 检查是否活跃
+ active, err := net.IsActive()
+ if err != nil {
+ return fmt.Errorf("failed to check network state: %w", err)
+ }
+
+ // 如果活跃,先停止
+ if active {
+ if err := net.Destroy(); err != nil {
+ return fmt.Errorf("failed to stop network: %w", err)
+ }
+ }
+
+ // 取消定义
+ if err := net.Undefine(); err != nil {
+ return fmt.Errorf("failed to undefine network: %w", err)
+ }
+
+ s.logger.WithField("network", networkName).Info("Virtual network deleted")
+ return nil
+}
+
+// StartNetwork 启动网络
+func (s *networkService) StartNetwork(ctx context.Context, connID, networkName string) error {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return err
+ }
+ defer net.Free()
+
+ if err := net.Create(); err != nil {
+ return fmt.Errorf("failed to start network: %w", err)
+ }
+
+ s.logger.WithField("network", networkName).Info("Virtual network started")
+ return nil
+}
+
+// StopNetwork 停止网络
+func (s *networkService) StopNetwork(ctx context.Context, connID, networkName string) error {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return err
+ }
+ defer net.Free()
+
+ if err := net.Destroy(); err != nil {
+ return fmt.Errorf("failed to stop network: %w", err)
+ }
+
+ s.logger.WithField("network", networkName).Info("Virtual network stopped")
+ return nil
+}
+
+// SetAutostart 设置自动启动
+func (s *networkService) SetAutostart(ctx context.Context, connID, networkName string, autostart bool) error {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return err
+ }
+ defer net.Free()
+
+ if err := net.SetAutostart(autostart); err != nil {
+ return fmt.Errorf("failed to set autostart: %w", err)
+ }
+
+ return nil
+}
+
+// GetDHCPLeases 获取 DHCP 租约
+func (s *networkService) GetDHCPLeases(ctx context.Context, connID, networkName string) ([]*domain.DHCPLease, error) {
+ net, err := s.getNetwork(connID, networkName)
+ if err != nil {
+ return nil, err
+ }
+ defer net.Free()
+
+ leases, err := net.GetDHCPLeases()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get DHCP leases: %w", err)
+ }
+
+ dhcpLeases := make([]*domain.DHCPLease, 0, len(leases))
+ for _, lease := range leases {
+ dhcpLeases = append(dhcpLeases, &domain.DHCPLease{
+ IPAddress: lease.IPaddr,
+ MACAddress: lease.Mac,
+ Hostname: lease.Hostname,
+ ExpiryTime: time.Unix(lease.Expirytime, 0),
+ Type: fmt.Sprintf("ipv%d", lease.Type),
+ })
+ }
+
+ return dhcpLeases, nil
+}
+
+// === 辅助方法 ===
+
+func (s *networkService) getNetwork(connID, networkName string) (*libvirt.Network, error) {
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ net, err := conn.LookupNetworkByName(networkName)
+ if err != nil {
+ return nil, fmt.Errorf("network not found: %w", err)
+ }
+
+ return net, nil
+}
+
+func (s *networkService) networkToInfo(net *libvirt.Network) (*domain.NetworkInfo, error) {
+ name, err := net.GetName()
+ if err != nil {
+ return nil, err
+ }
+
+ uuid, err := net.GetUUIDString()
+ if err != nil {
+ return nil, err
+ }
+
+ active, _ := net.IsActive()
+ persistent, _ := net.IsPersistent()
+ autostart, _ := net.GetAutostart()
+
+ // 解析 XML 获取详细信息
+ xmlData, err := net.GetXMLDesc(0)
+ if err != nil {
+ return nil, err
+ }
+
+ netConfig := &libvirtxml.Network{}
+ if err := netConfig.Unmarshal(xmlData); err != nil {
+ return nil, err
+ }
+
+ netInfo := &domain.NetworkInfo{
+ Name: name,
+ UUID: uuid,
+ Active: active,
+ Persistent: persistent,
+ Autostart: autostart,
+ CreatedAt: time.Now(),
+ }
+
+ if netConfig.Bridge != nil {
+ netInfo.Bridge = netConfig.Bridge.Name
+ }
+
+ if netConfig.Forward != nil {
+ netInfo.Forward = domain.ForwardMode(netConfig.Forward.Mode)
+ }
+
+ if netConfig.Domain != nil {
+ netInfo.Domain = netConfig.Domain.Name
+ }
+
+ // 解析 IP 配置
+ for _, ip := range netConfig.IPs {
+ netIP := domain.NetworkIP{
+ Address: ip.Address,
+ Netmask: ip.Netmask,
+ }
+
+ if ip.DHCP != nil {
+ netIP.DHCP = &domain.NetworkDHCP{
+ Start: ip.DHCP.Ranges[0].Start,
+ End: ip.DHCP.Ranges[0].End,
+ }
+
+ for _, host := range ip.DHCP.Hosts {
+ netIP.DHCP.Hosts = append(netIP.DHCP.Hosts, domain.DHCPHost{
+ MAC: host.MAC,
+ Name: host.Name,
+ IP: host.IP,
+ })
+ }
+ }
+
+ netInfo.IP = append(netInfo.IP, netIP)
+ }
+
+ return netInfo, nil
+}
+
+func (s *networkService) buildNetworkXML(config *domain.NetworkConfig) (string, error) {
+ netConfig := &libvirtxml.Network{
+ Name: config.Name,
+ }
+
+ // 设置桥接
+ if config.Bridge != "" {
+ netConfig.Bridge = &libvirtxml.NetworkBridge{
+ Name: config.Bridge,
+ STP: "on",
+ }
+ }
+
+ // 设置转发模式
+ if config.Forward != "" {
+ netConfig.Forward = &libvirtxml.NetworkForward{
+ Mode: string(config.Forward),
+ }
+ }
+
+ // 设置域名
+ if config.Domain != "" {
+ netConfig.Domain = &libvirtxml.NetworkDomain{
+ Name: config.Domain,
+ }
+ }
+
+ // 设置 IP 配置
+ for _, ipCfg := range config.IP {
+ netIP := libvirtxml.NetworkIP{
+ Address: ipCfg.Address,
+ Netmask: ipCfg.Netmask,
+ }
+
+ if ipCfg.DHCP != nil {
+ netIP.DHCP = &libvirtxml.NetworkDHCP{
+ Ranges: []libvirtxml.NetworkDHCPRange{
+ {
+ Start: ipCfg.DHCP.Start,
+ End: ipCfg.DHCP.End,
+ },
+ },
+ }
+
+ // 添加静态主机
+ for _, host := range ipCfg.DHCP.Hosts {
+ netIP.DHCP.Hosts = append(netIP.DHCP.Hosts, libvirtxml.NetworkDHCPHost{
+ MAC: host.MAC,
+ Name: host.Name,
+ IP: host.IP,
+ })
+ }
+ }
+
+ netConfig.IPs = append(netConfig.IPs, netIP)
+ }
+
+ return netConfig.Marshal()
+}
diff --git a/internal/service/snapshot_service.go b/internal/service/snapshot_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..ba90b0d5851ff4821de0a55008a6527f499c1a5e
--- /dev/null
+++ b/internal/service/snapshot_service.go
@@ -0,0 +1,310 @@
+// ============================================
+// internal/service/snapshot_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirt"
+ "libvirt.org/go/libvirtxml"
+
+ "virt-manager-go/internal/domain"
+)
+
+type snapshotService struct {
+ connMgr domain.ConnectionManager
+ vmService domain.VMService
+ logger *logrus.Logger
+}
+
+func NewSnapshotService(connMgr domain.ConnectionManager, vmService domain.VMService, logger *logrus.Logger) domain.SnapshotService {
+ return &snapshotService{
+ connMgr: connMgr,
+ vmService: vmService,
+ logger: logger,
+ }
+}
+
+// ListSnapshots 列出虚拟机所有快照
+func (s *snapshotService) ListSnapshots(ctx context.Context, connID, vmName string) ([]*domain.SnapshotInfo, error) {
+ dom, err := s.getDomain(connID, vmName)
+ if err != nil {
+ return nil, err
+ }
+ defer dom.Free()
+
+ snapshots, err := dom.ListAllSnapshots(0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list snapshots: %w", err)
+ }
+
+ // 获取当前快照
+ currentSnap, _ := dom.SnapshotCurrent(0)
+ var currentName string
+ if currentSnap != nil {
+ currentName, _ = currentSnap.GetName()
+ currentSnap.Free()
+ }
+
+ snapshotInfos := make([]*domain.SnapshotInfo, 0, len(snapshots))
+ for _, snap := range snapshots {
+ snapInfo, err := s.snapshotToInfo(&snap, currentName)
+ snap.Free()
+ if err != nil {
+ s.logger.WithError(err).Warn("Failed to convert snapshot to info")
+ continue
+ }
+ snapshotInfos = append(snapshotInfos, snapInfo)
+ }
+
+ return snapshotInfos, nil
+}
+
+// GetSnapshot 获取快照信息
+func (s *snapshotService) GetSnapshot(ctx context.Context, connID, vmName, snapshotName string) (*domain.SnapshotInfo, error) {
+ snap, currentName, err := s.getSnapshot(connID, vmName, snapshotName)
+ if err != nil {
+ return nil, err
+ }
+ defer snap.Free()
+
+ return s.snapshotToInfo(snap, currentName)
+}
+
+// CreateSnapshot 创建快照
+func (s *snapshotService) CreateSnapshot(ctx context.Context, connID, vmName string, config *domain.SnapshotConfig) (*domain.SnapshotInfo, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "vm": vmName,
+ "snapshot": config.Name,
+ }).Info("Creating VM snapshot")
+
+ dom, err := s.getDomain(connID, vmName)
+ if err != nil {
+ return nil, err
+ }
+ defer dom.Free()
+
+ // 构建快照 XML
+ xmlData, err := s.buildSnapshotXML(config)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build snapshot XML: %w", err)
+ }
+
+ s.logger.WithField("xml", xmlData).Debug("Generated snapshot XML")
+
+ // 设置快照标志
+ flags := libvirt.DOMAIN_SNAPSHOT_CREATE_ATOMIC
+ if config.Memory {
+ // 内存快照(需要虚拟机运行)
+ state, _, err := dom.GetState()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get VM state: %w", err)
+ }
+ if state != libvirt.DOMAIN_RUNNING {
+ return nil, fmt.Errorf("VM must be running for memory snapshot")
+ }
+ } else {
+ flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
+ }
+
+ if config.Quiesce {
+ flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE
+ }
+
+ // 创建快照
+ snap, err := dom.SnapshotCreateXML(xmlData, uint32(flags))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create snapshot: %w", err)
+ }
+ defer snap.Free()
+
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "snapshot": config.Name,
+ }).Info("Snapshot created successfully")
+
+ return s.snapshotToInfo(snap, config.Name)
+}
+
+// DeleteSnapshot 删除快照
+func (s *snapshotService) DeleteSnapshot(ctx context.Context, connID, vmName, snapshotName string, deleteChildren bool) error {
+ snap, _, err := s.getSnapshot(connID, vmName, snapshotName)
+ if err != nil {
+ return err
+ }
+ defer snap.Free()
+
+ flags := libvirt.DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY
+ if deleteChildren {
+ flags |= libvirt.DOMAIN_SNAPSHOT_DELETE_CHILDREN
+ }
+
+ if err := snap.Delete(uint32(flags)); err != nil {
+ return fmt.Errorf("failed to delete snapshot: %w", err)
+ }
+
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "snapshot": snapshotName,
+ }).Info("Snapshot deleted")
+
+ return nil
+}
+
+// RevertToSnapshot 恢复到快照
+func (s *snapshotService) RevertToSnapshot(ctx context.Context, connID, vmName, snapshotName string) error {
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "snapshot": snapshotName,
+ }).Info("Reverting to snapshot")
+
+ snap, _, err := s.getSnapshot(connID, vmName, snapshotName)
+ if err != nil {
+ return err
+ }
+ defer snap.Free()
+
+ flags := libvirt.DOMAIN_SNAPSHOT_REVERT_RUNNING | libvirt.DOMAIN_SNAPSHOT_REVERT_FORCE
+
+ if err := snap.RevertToSnapshot(uint32(flags)); err != nil {
+ return fmt.Errorf("failed to revert to snapshot: %w", err)
+ }
+
+ s.logger.WithFields(logrus.Fields{
+ "vm": vmName,
+ "snapshot": snapshotName,
+ }).Info("Successfully reverted to snapshot")
+
+ return nil
+}
+
+// GetCurrentSnapshot 获取当前快照
+func (s *snapshotService) GetCurrentSnapshot(ctx context.Context, connID, vmName string) (*domain.SnapshotInfo, error) {
+ dom, err := s.getDomain(connID, vmName)
+ if err != nil {
+ return nil, err
+ }
+ defer dom.Free()
+
+ snap, err := dom.SnapshotCurrent(0)
+ if err != nil {
+ return nil, fmt.Errorf("no current snapshot: %w", err)
+ }
+ defer snap.Free()
+
+ name, _ := snap.GetName()
+ return s.snapshotToInfo(snap, name)
+}
+
+// === 辅助方法 ===
+
+func (s *snapshotService) getDomain(connID, vmName string) (*libvirt.Domain, error) {
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ dom, err := conn.LookupDomainByName(vmName)
+ if err != nil {
+ return nil, fmt.Errorf("VM not found: %w", err)
+ }
+
+ return dom, nil
+}
+
+func (s *snapshotService) getSnapshot(connID, vmName, snapshotName string) (*libvirt.DomainSnapshot, string, error) {
+ dom, err := s.getDomain(connID, vmName)
+ if err != nil {
+ return nil, "", err
+ }
+ defer dom.Free()
+
+ snap, err := dom.SnapshotLookupByName(snapshotName, 0)
+ if err != nil {
+ return nil, "", fmt.Errorf("snapshot not found: %w", err)
+ }
+
+ // 获取当前快照名称
+ currentSnap, _ := dom.SnapshotCurrent(0)
+ var currentName string
+ if currentSnap != nil {
+ currentName, _ = currentSnap.GetName()
+ currentSnap.Free()
+ }
+
+ return snap, currentName, nil
+}
+
+func (s *snapshotService) snapshotToInfo(snap *libvirt.DomainSnapshot, currentName string) (*domain.SnapshotInfo, error) {
+ name, err := snap.GetName()
+ if err != nil {
+ return nil, err
+ }
+
+ // 解析快照 XML
+ xmlData, err := snap.GetXMLDesc(0)
+ if err != nil {
+ return nil, err
+ }
+
+ snapConfig := &libvirtxml.DomainSnapshot{}
+ if err := snapConfig.Unmarshal(xmlData); err != nil {
+ return nil, err
+ }
+
+ snapInfo := &domain.SnapshotInfo{
+ Name: name,
+ Description: snapConfig.Description,
+ IsCurrent: name == currentName,
+ CreatedAt: time.Now(),
+ }
+
+ if snapConfig.Parent != nil {
+ snapInfo.Parent = snapConfig.Parent.Name
+ }
+
+ if snapConfig.State != nil {
+ snapInfo.State = domain.SnapshotState(*snapConfig.State)
+ }
+
+ if snapConfig.Memory != nil {
+ snapInfo.Memory = snapConfig.Memory.Snapshot == "internal" || snapConfig.Memory.Snapshot == "external"
+ }
+
+ // 获取子快照
+ children, err := snap.ListAllChildren(0)
+ if err == nil {
+ for _, child := range children {
+ childName, _ := child.GetName()
+ snapInfo.Children = append(snapInfo.Children, childName)
+ child.Free()
+ }
+ }
+
+ return snapInfo, nil
+}
+
+func (s *snapshotService) buildSnapshotXML(config *domain.SnapshotConfig) (string, error) {
+ snapConfig := &libvirtxml.DomainSnapshot{
+ Name: config.Name,
+ Description: config.Description,
+ }
+
+ if config.Memory {
+ snapConfig.Memory = &libvirtxml.DomainSnapshotMemory{
+ Snapshot: "internal",
+ }
+ } else if config.DiskOnly {
+ snapConfig.Memory = &libvirtxml.DomainSnapshotMemory{
+ Snapshot: "no",
+ }
+ }
+
+ return snapConfig.Marshal()
+}
diff --git a/internal/service/storage_pool_service.go b/internal/service/storage_pool_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..f4b23ae7c744adb150298f41538e21c6638bb99f
--- /dev/null
+++ b/internal/service/storage_pool_service.go
@@ -0,0 +1,538 @@
+// ============================================
+// internal/service/storage_pool_service.go
+// ============================================
+
+package service
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "libvirt.org/go/libvirt"
+ "libvirt.org/go/libvirtxml"
+
+ "virt-manager-go/internal/domain"
+)
+
+type storagePoolService struct {
+ connMgr domain.ConnectionManager
+ logger *logrus.Logger
+}
+
+func NewStoragePoolService(connMgr domain.ConnectionManager, logger *logrus.Logger) domain.StoragePoolService {
+ return &storagePoolService{
+ connMgr: connMgr,
+ logger: logger,
+ }
+}
+
+// ListPools 列出所有存储池
+func (s *storagePoolService) ListPools(ctx context.Context, connID string) ([]*domain.StoragePoolInfo, error) {
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ pools, err := conn.ListAllStoragePools(libvirt.CONNECT_LIST_STORAGE_POOLS_ACTIVE | libvirt.CONNECT_LIST_STORAGE_POOLS_INACTIVE)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list storage pools: %w", err)
+ }
+
+ poolInfos := make([]*domain.StoragePoolInfo, 0, len(pools))
+ for _, pool := range pools {
+ poolInfo, err := s.poolToInfo(&pool)
+ pool.Free()
+ if err != nil {
+ s.logger.WithError(err).Warn("Failed to convert pool to info")
+ continue
+ }
+ poolInfos = append(poolInfos, poolInfo)
+ }
+
+ return poolInfos, nil
+}
+
+// GetPool 获取存储池信息
+func (s *storagePoolService) GetPool(ctx context.Context, connID, poolName string) (*domain.StoragePoolInfo, error) {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return nil, err
+ }
+ defer pool.Free()
+
+ return s.poolToInfo(pool)
+}
+
+// CreatePool 创建存储池
+func (s *storagePoolService) CreatePool(ctx context.Context, connID string, config *domain.StoragePoolConfig) (*domain.StoragePoolInfo, error) {
+ s.logger.WithFields(logrus.Fields{
+ "connection": connID,
+ "pool_name": config.Name,
+ "pool_type": config.Type,
+ }).Info("Creating storage pool")
+
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ // 构建存储池 XML
+ xmlData, err := s.buildPoolXML(config)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build pool XML: %w", err)
+ }
+
+ s.logger.WithField("xml", xmlData).Debug("Generated pool XML")
+
+ // 定义存储池
+ pool, err := conn.StoragePoolDefineXML(xmlData, 0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to define storage pool: %w", err)
+ }
+ defer pool.Free()
+
+ // 构建存储池(创建目录等)
+ if err := pool.Build(libvirt.STORAGE_POOL_BUILD_NEW); err != nil {
+ s.logger.WithError(err).Warn("Failed to build pool, may already exist")
+ }
+
+ // 启动存储池
+ if err := pool.Create(0); err != nil {
+ return nil, fmt.Errorf("failed to start storage pool: %w", err)
+ }
+
+ // 设置自动启动
+ if config.Autostart {
+ if err := pool.SetAutostart(true); err != nil {
+ s.logger.WithError(err).Warn("Failed to set autostart")
+ }
+ }
+
+ s.logger.WithField("pool_name", config.Name).Info("Storage pool created successfully")
+
+ return s.poolToInfo(pool)
+}
+
+// DeletePool 删除存储池
+func (s *storagePoolService) DeletePool(ctx context.Context, connID, poolName string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ // 检查是否活跃
+ active, err := pool.IsActive()
+ if err != nil {
+ return fmt.Errorf("failed to check pool state: %w", err)
+ }
+
+ // 如果活跃,先停止
+ if active {
+ if err := pool.Destroy(); err != nil {
+ return fmt.Errorf("failed to stop pool: %w", err)
+ }
+ }
+
+ // 删除存储池(可选:删除物理存储)
+ if err := pool.Delete(libvirt.STORAGE_POOL_DELETE_NORMAL); err != nil {
+ s.logger.WithError(err).Warn("Failed to delete pool storage")
+ }
+
+ // 取消定义
+ if err := pool.Undefine(); err != nil {
+ return fmt.Errorf("failed to undefine pool: %w", err)
+ }
+
+ s.logger.WithField("pool_name", poolName).Info("Storage pool deleted")
+ return nil
+}
+
+// StartPool 启动存储池
+func (s *storagePoolService) StartPool(ctx context.Context, connID, poolName string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ if err := pool.Create(0); err != nil {
+ return fmt.Errorf("failed to start pool: %w", err)
+ }
+
+ s.logger.WithField("pool_name", poolName).Info("Storage pool started")
+ return nil
+}
+
+// StopPool 停止存储池
+func (s *storagePoolService) StopPool(ctx context.Context, connID, poolName string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ if err := pool.Destroy(); err != nil {
+ return fmt.Errorf("failed to stop pool: %w", err)
+ }
+
+ s.logger.WithField("pool_name", poolName).Info("Storage pool stopped")
+ return nil
+}
+
+// RefreshPool 刷新存储池
+func (s *storagePoolService) RefreshPool(ctx context.Context, connID, poolName string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ if err := pool.Refresh(0); err != nil {
+ return fmt.Errorf("failed to refresh pool: %w", err)
+ }
+
+ s.logger.WithField("pool_name", poolName).Info("Storage pool refreshed")
+ return nil
+}
+
+// SetAutostart 设置自动启动
+func (s *storagePoolService) SetAutostart(ctx context.Context, connID, poolName string, autostart bool) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ if err := pool.SetAutostart(autostart); err != nil {
+ return fmt.Errorf("failed to set autostart: %w", err)
+ }
+
+ return nil
+}
+
+// ListVolumes 列出存储卷
+func (s *storagePoolService) ListVolumes(ctx context.Context, connID, poolName string) ([]*domain.StorageVolumeInfo, error) {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return nil, err
+ }
+ defer pool.Free()
+
+ volumes, err := pool.ListAllStorageVolumes(0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list volumes: %w", err)
+ }
+
+ volumeInfos := make([]*domain.StorageVolumeInfo, 0, len(volumes))
+ for _, vol := range volumes {
+ volInfo, err := s.volumeToInfo(&vol)
+ vol.Free()
+ if err != nil {
+ s.logger.WithError(err).Warn("Failed to convert volume to info")
+ continue
+ }
+ volumeInfos = append(volumeInfos, volInfo)
+ }
+
+ return volumeInfos, nil
+}
+
+// CreateVolume 创建存储卷
+func (s *storagePoolService) CreateVolume(ctx context.Context, connID, poolName string, config *domain.StorageVolumeConfig) (*domain.StorageVolumeInfo, error) {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return nil, err
+ }
+ defer pool.Free()
+
+ // 构建卷 XML
+ xmlData, err := s.buildVolumeXML(config)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build volume XML: %w", err)
+ }
+
+ // 创建卷
+ vol, err := pool.StorageVolCreateXML(xmlData, 0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create volume: %w", err)
+ }
+ defer vol.Free()
+
+ s.logger.WithFields(logrus.Fields{
+ "pool": poolName,
+ "volume": config.Name,
+ }).Info("Storage volume created")
+
+ return s.volumeToInfo(vol)
+}
+
+// CloneVolume 克隆存储卷
+func (s *storagePoolService) CloneVolume(ctx context.Context, connID, poolName, srcVolume, destVolume string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ // 获取源卷
+ srcVol, err := pool.LookupStorageVolByName(srcVolume)
+ if err != nil {
+ return fmt.Errorf("source volume not found: %w", err)
+ }
+ defer srcVol.Free()
+
+ // 获取源卷 XML 并修改名称
+ srcXML, err := srcVol.GetXMLDesc(0)
+ if err != nil {
+ return fmt.Errorf("failed to get source volume XML: %w", err)
+ }
+
+ volConfig := &libvirtxml.StorageVolume{}
+ if err := volConfig.Unmarshal(srcXML); err != nil {
+ return fmt.Errorf("failed to parse volume XML: %w", err)
+ }
+
+ volConfig.Name = destVolume
+ volConfig.Key = ""
+
+ destXML, err := volConfig.Marshal()
+ if err != nil {
+ return fmt.Errorf("failed to marshal volume XML: %w", err)
+ }
+
+ // 克隆卷
+ destVol, err := pool.StorageVolCreateXMLFrom(destXML, srcVol, 0)
+ if err != nil {
+ return fmt.Errorf("failed to clone volume: %w", err)
+ }
+ defer destVol.Free()
+
+ s.logger.WithFields(logrus.Fields{
+ "pool": poolName,
+ "source": srcVolume,
+ "dest": destVolume,
+ }).Info("Storage volume cloned")
+
+ return nil
+}
+
+// DeleteVolume 删除存储卷
+func (s *storagePoolService) DeleteVolume(ctx context.Context, connID, poolName, volumeName string) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ vol, err := pool.LookupStorageVolByName(volumeName)
+ if err != nil {
+ return fmt.Errorf("volume not found: %w", err)
+ }
+ defer vol.Free()
+
+ if err := vol.Delete(0); err != nil {
+ return fmt.Errorf("failed to delete volume: %w", err)
+ }
+
+ s.logger.WithFields(logrus.Fields{
+ "pool": poolName,
+ "volume": volumeName,
+ }).Info("Storage volume deleted")
+
+ return nil
+}
+
+// ResizeVolume 调整存储卷大小
+func (s *storagePoolService) ResizeVolume(ctx context.Context, connID, poolName, volumeName string, newSize uint64) error {
+ pool, err := s.getPool(connID, poolName)
+ if err != nil {
+ return err
+ }
+ defer pool.Free()
+
+ vol, err := pool.LookupStorageVolByName(volumeName)
+ if err != nil {
+ return fmt.Errorf("volume not found: %w", err)
+ }
+ defer vol.Free()
+
+ if err := vol.Resize(newSize, 0); err != nil {
+ return fmt.Errorf("failed to resize volume: %w", err)
+ }
+
+ s.logger.WithFields(logrus.Fields{
+ "pool": poolName,
+ "volume": volumeName,
+ "new_size": newSize,
+ }).Info("Storage volume resized")
+
+ return nil
+}
+
+// === 辅助方法 ===
+
+func (s *storagePoolService) getPool(connID, poolName string) (*libvirt.StoragePool, error) {
+ conn, err := s.connMgr.GetConnection(connID)
+ if err != nil {
+ return nil, err
+ }
+
+ pool, err := conn.LookupStoragePoolByName(poolName)
+ if err != nil {
+ return nil, fmt.Errorf("storage pool not found: %w", err)
+ }
+
+ return pool, nil
+}
+
+func (s *storagePoolService) poolToInfo(pool *libvirt.StoragePool) (*domain.StoragePoolInfo, error) {
+ name, err := pool.GetName()
+ if err != nil {
+ return nil, err
+ }
+
+ uuid, err := pool.GetUUIDString()
+ if err != nil {
+ return nil, err
+ }
+
+ info, err := pool.GetInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ autostart, _ := pool.GetAutostart()
+ persistent, _ := pool.IsPersistent()
+
+ // 获取存储池路径
+ xmlData, _ := pool.GetXMLDesc(0)
+ poolConfig := &libvirtxml.StoragePool{}
+ var path string
+ if xmlData != "" && poolConfig.Unmarshal(xmlData) == nil && poolConfig.Target != nil {
+ path = poolConfig.Target.Path
+ }
+
+ // 计算卷数量
+ volumes, _ := pool.ListAllStorageVolumes(0)
+ volumeCount := len(volumes)
+ for _, vol := range volumes {
+ vol.Free()
+ }
+
+ return &domain.StoragePoolInfo{
+ Name: name,
+ UUID: uuid,
+ State: s.libvirtPoolStateToState(libvirt.StoragePoolState(info.State)),
+ Capacity: info.Capacity,
+ Allocation: info.Allocation,
+ Available: info.Available,
+ Autostart: autostart,
+ Persistent: persistent,
+ Path: path,
+ Volumes: volumeCount,
+ CreatedAt: time.Now(),
+ }, nil
+}
+
+func (s *storagePoolService) libvirtPoolStateToState(state libvirt.StoragePoolState) domain.StoragePoolState {
+ switch state {
+ case libvirt.STORAGE_POOL_INACTIVE:
+ return domain.PoolStateInactive
+ case libvirt.STORAGE_POOL_BUILDING:
+ return domain.PoolStateBuilding
+ case libvirt.STORAGE_POOL_RUNNING:
+ return domain.PoolStateRunning
+ case libvirt.STORAGE_POOL_DEGRADED:
+ return domain.PoolStateDegraded
+ case libvirt.STORAGE_POOL_INACCESSIBLE:
+ return domain.PoolStateInaccessible
+ default:
+ return domain.PoolStateInactive
+ }
+}
+
+func (s *storagePoolService) volumeToInfo(vol *libvirt.StorageVolume) (*domain.StorageVolumeInfo, error) {
+ name, err := vol.GetName()
+ if err != nil {
+ return nil, err
+ }
+
+ key, _ := vol.GetKey()
+ path, _ := vol.GetPath()
+
+ info, err := vol.GetInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ return &domain.StorageVolumeInfo{
+ Name: name,
+ Key: key,
+ Path: path,
+ Type: domain.VolumeTypeFile,
+ Capacity: info.Capacity,
+ Allocation: info.Allocation,
+ CreatedAt: time.Now(),
+ }, nil
+}
+
+func (s *storagePoolService) buildPoolXML(config *domain.StoragePoolConfig) (string, error) {
+ poolConfig := &libvirtxml.StoragePool{
+ Type: config.Type,
+ Name: config.Name,
+ }
+
+ if config.Target != nil {
+ poolConfig.Target = &libvirtxml.StoragePoolTarget{
+ Path: config.Target.Path,
+ }
+ }
+
+ if config.Source != nil {
+ poolConfig.Source = &libvirtxml.StoragePoolSource{}
+ if config.Source.Host != "" {
+ poolConfig.Source.Host = []libvirtxml.StoragePoolSourceHost{
+ {Name: config.Source.Host},
+ }
+ }
+ if config.Source.Dir != "" {
+ poolConfig.Source.Dir = &libvirtxml.StoragePoolSourceDir{
+ Path: config.Source.Dir,
+ }
+ }
+ }
+
+ return poolConfig.Marshal()
+}
+
+func (s *storagePoolService) buildVolumeXML(config *domain.StorageVolumeConfig) (string, error) {
+ if config.Format == "" {
+ config.Format = "qcow2"
+ }
+
+ volConfig := &libvirtxml.StorageVolume{
+ Type: "file",
+ Name: config.Name,
+ Capacity: &libvirtxml.StorageVolumeSize{
+ Value: config.Capacity,
+ Unit: "B",
+ },
+ Target: &libvirtxml.StorageVolumeTarget{
+ Format: &libvirtxml.StorageVolumeTargetFormat{
+ Type: config.Format,
+ },
+ },
+ }
+
+ if config.Allocation > 0 {
+ volConfig.Allocation = &libvirtxml.StorageVolumeSize{
+ Value: config.Allocation,
+ Unit: "B",
+ }
+ }
+
+ return volConfig.Marshal()
+}