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() +}