package service import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "sort" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/vm" "github.com/1Panel-dev/1Panel/agent/utils/vm/protocol" "github.com/1Panel-dev/1Panel/agent/utils/vm/types" "github.com/jinzhu/copier" "github.com/shirou/gopsutil/v4/mem" "gorm.io/gorm" ) type VMService struct { provider vm.VMProvider locksMu sync.Mutex locks map[string]*vmLockEntry } type vmLockEntry struct { mu sync.Mutex refs int } // Share the lock across service instances. The metadata written before the // asynchronous task starts reserves a slot, including while a VM is creating. var vmCreateMu sync.Mutex const ( defaultVMResourceName = "default" defaultVMDiskDir = "/var/lib/libvirt/images" diskBusSATA = "sata" diskBusVirtIO = "virtio" osTypeWindows = "windows" vmMemoryMinMiB = int64(128) vmDiskMinGiB = int64(1) vmMemoryReserveRatio = 0.1 vmMemoryReserveMin = uint64(1024 * 1024 * 1024) bytePerMiB = uint64(1024 * 1024) bytePerGiB = uint64(1024 * 1024 * 1024) ) var libvirtServiceNames = []string{ "libvirtd.service", "virtqemud.service", "virtnetworkd.service", "libvirt-bin.service", "libvirtd", "virtqemud", "virtnetworkd", "libvirt-bin", "libvirt", } type IVMService interface { CheckEnvironment() (dto.VMEnvironment, error) HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) EnableEnvironment() error SyncResourcesOnStartup() error SyncVMs() error Search(req dto.SearchWithPage) (int64, interface{}, error) Get(name string, checkDisks bool) (dto.VMInfo, error) GetStats(name string) (dto.VMStats, error) Create(req dto.VMCreate, limit int) (dto.VMTaskResp, error) ListTemplates() ([]dto.VMTemplateInfo, error) CreateTemplate(req dto.VMTemplateCreate) (dto.VMTaskResp, error) DeleteTemplate(req dto.VMTemplateDelete) (dto.VMTaskResp, error) Update(req dto.VMUpdate) (dto.VMTaskResp, error) ResizeDisk(req dto.VMDiskResize) (dto.VMTaskResp, error) Rename(req dto.VMRename) (dto.VMTaskResp, error) Delete(req dto.VMDelete) (dto.VMTaskResp, error) ListSnapshots(req dto.VMSnapshotSearch) ([]dto.VMSnapshotInfo, error) CreateSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) RecoverSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) DeleteSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) ScanOrphans() ([]dto.VMOrphanItem, error) CleanOrphans(req dto.VMOrphanClean) ([]dto.VMOrphanItem, error) Operate(req dto.VMOperate) error Console(name string) (*dto.VMGraphics, error) SearchIso(req dto.SearchPageWithType) (int64, interface{}, error) ListIso() ([]dto.VMIsoInfo, error) CreateIso(req dto.VMIsoCreate) error UpdateIso(req dto.VMIsoUpdate) error DeleteIso(req dto.VMIsoDelete) error StartIsoImport(req dto.VMIsoImportStart) (uint, error) UpdateIsoImport(req dto.VMIsoImportUpdate) error ListNetworks() ([]dto.VMNetworkItem, error) ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) CreateNetwork(req dto.VMNetworkCreate) error SyncNetworks() error UpdateNetwork(req dto.VMNetworkUpdate) error DeleteNetwork(req dto.VMResourceDelete) error ListStorages() ([]dto.VMStorageItem, error) CreateStorage(req dto.VMStorageCreate) error SyncStorages() error UpdateStorage(req dto.VMStorageUpdate) error DeleteStorage(req dto.VMResourceDelete) error } func (v *VMService) ScanOrphans() ([]dto.VMOrphanItem, error) { paths, err := protectedVMPaths(0) if err != nil { return nil, err } return v.provider.ScanOrphans(context.Background(), paths) } func (v *VMService) CleanOrphans(req dto.VMOrphanClean) ([]dto.VMOrphanItem, error) { paths, err := protectedVMPaths(0) if err != nil { return nil, err } return v.provider.CleanOrphans(context.Background(), req.Items, paths) } func protectedVMPaths(excludeVMID uint) ([]string, error) { vms, err := vmRepo.List() if err != nil { return nil, err } templates, err := vmRepo.ListTemplates() if err != nil { return nil, err } paths := make([]string, 0, 2*len(vms)+len(templates)) for _, item := range vms { if item.ID == excludeVMID { continue } if item.DiskPath != "" { paths = append(paths, item.DiskPath) } if item.ImagePath != "" { paths = append(paths, item.ImagePath) } } for _, item := range templates { if item.DiskPath != "" { paths = append(paths, item.DiskPath) } } return paths, nil } func NewIVMService() IVMService { provider, _ := vm.NewVMProvider() return &VMService{provider: provider, locks: make(map[string]*vmLockEntry)} } func (v *VMService) CheckEnvironment() (dto.VMEnvironment, error) { arch := runtime.GOARCH env := dto.VMEnvironment{ Arch: arch, ArchSupported: arch == "amd64" || arch == "arm64", } if !env.ArchSupported { return env, nil } helperExist := v.provider.HelperStatus() env.HelperExist = helperExist if !helperExist { env.HelperMessage = buserr.New("ErrVMHelperNotFound").Error() } helperReady := helperExist if helperExist { checkItems, checkErr := v.provider.CheckEnvironment(context.Background()) if checkErr != nil { helperReady = false env.HelperMessage = checkErr.Error() } for _, item := range checkItems { switch item.Name { case "kvm": env.KVMExist = item.IsExist env.KVMMessage = item.Message case "storage": env.StorageExist = item.IsExist env.StorageMessage = item.Message case "libvirt": env.LibvirtExist = item.IsExist env.LibvirtActive = item.Passed || item.IsActive env.LibvirtMessage = item.Message } } } if libvirtExist, _, _ := v.loadLibvirtServiceStatus(); libvirtExist { env.LibvirtExist = true } env.Ready = helperReady && env.KVMExist && env.StorageExist && env.LibvirtActive return env, nil } func (v *VMService) HealthCheck(ctx context.Context, name string) (dto.VMHealthCheckResult, error) { if !v.helperExists() { return dto.VMHealthCheckResult{}, buserr.New("ErrVMHelperNotFound") } return v.provider.HealthCheck(ctx, name) } func (v *VMService) EnableEnvironment() error { client, err := controller.New() if err != nil { return err } found := false for _, service := range libvirtServiceNames { exist, _ := client.IsExist(service) if !exist { continue } found = true if err := client.Operate("start", service); err != nil { return err } } if !found { return buserr.New("ErrRecordNotFound") } return nil } func (v *VMService) loadLibvirtServiceStatus() (bool, bool, bool) { client, err := controller.New() if err != nil { return false, false, false } isExist := false isEnable := false isActive := false for _, service := range libvirtServiceNames { exist, _ := client.IsExist(service) if !exist { continue } isExist = true if enable, _ := client.IsEnable(service); enable { isEnable = true } if active, _ := client.IsActive(service); active { isActive = true } if isEnable && isActive { break } } return isExist, isEnable, isActive } func (v *VMService) SyncResourcesOnStartup() error { env, err := v.CheckEnvironment() if err != nil { return err } if !env.HelperExist { global.LOG.Info("skip VM resource sync on startup: 1panel-kvm not found") return nil } if !env.KVMExist { global.LOG.Info("skip VM resource sync on startup: kvm not found") return nil } if !env.LibvirtActive { global.LOG.Info("skip VM resource sync on startup: libvirt is not active") return nil } if err := v.SyncNetworks(); err != nil { return fmt.Errorf("sync VM networks failed: %w", err) } if err := v.SyncStorages(); err != nil { return fmt.Errorf("sync VM storage pools failed: %w", err) } if err := v.SyncVMs(); err != nil { return fmt.Errorf("sync virtual machines failed: %w", err) } global.LOG.Info("VM networks, storage pools, and virtual machines synced on startup") return nil } // SyncVMs imports libvirt domains into the panel metadata and refreshes metadata // for already registered VMs. Metadata maintained only by the panel, such as the // description and image path, is intentionally retained. func (v *VMService) SyncVMs() error { vmCreateMu.Lock() defer vmCreateMu.Unlock() if !v.helperExists() { return buserr.New("ErrVMHelperNotFound") } liveItems, err := v.provider.List(context.Background()) if err != nil { return err } v.fillVMStoragePools(liveItems) records, err := vmRepo.List() if err != nil { return err } recordsByName := make(map[string]model.VirtualMachine, len(records)) for _, record := range records { recordsByName[record.Name] = record } liveByName := make(map[string]struct{}, len(liveItems)) for _, live := range liveItems { if live.Name == "" { continue } liveByName[live.Name] = struct{}{} if record, ok := recordsByName[live.Name]; ok { if err := vmRepo.Update(record.ID, vmSyncMetadataMap(live)); err != nil { return err } continue } if err := vmRepo.Create(&model.VirtualMachine{ Name: live.Name, UUID: live.UUID, CPU: live.CPU, Memory: live.Memory, DiskSize: live.DiskSize, DiskBus: defaultDiskBus(live.DiskBus), DiskPath: live.DiskPath, StoragePool: live.StoragePool, ISOPaths: live.ISOPaths, Network: live.Network, BootOrder: live.BootOrder, AutoStart: live.AutoStart, Status: live.Status, }); err != nil { return err } } for _, record := range records { if _, ok := liveByName[record.Name]; ok || record.UUID == "" { continue } if err := vmRepo.Update(record.ID, map[string]interface{}{"status": strings.ToLower(constant.StatusDeleted)}); err != nil { return err } } return nil } func vmSyncMetadataMap(item dto.VMInfo) map[string]interface{} { vars := map[string]interface{}{ "uuid": item.UUID, "cpu": item.CPU, "memory": item.Memory, "disk_bus": defaultDiskBus(item.DiskBus), "disk_path": item.DiskPath, "iso_paths": item.ISOPaths, "network": item.Network, "boot_order": item.BootOrder, "auto_start": item.AutoStart, "status": item.Status, } if item.StoragePool != "" { vars["storage_pool"] = item.StoragePool } if item.DiskSize > 0 { vars["disk_size"] = item.DiskSize } return vars } func (v *VMService) Search(req dto.SearchWithPage) (int64, interface{}, error) { options := []repo.DBOption{repo.WithOrderDesc("created_at")} if req.Info != "" { options = append(options, repo.WithByName(req.Info)) } total, records, err := vmRepo.Page(req.Page, req.PageSize, options...) if err != nil { return 0, nil, err } var domains []dto.VMInfo if v.helperExists() { domains, err = v.provider.List(context.Background()) if err != nil { global.LOG.Warnf("[xpack] list live VM domains failed, fallback to metadata: %v", err) } } domainByName := make(map[string]dto.VMInfo, len(domains)) for _, domain := range domains { domainByName[domain.Name] = domain } list := make([]dto.VMInfo, 0, len(records)) for _, record := range records { if domain, ok := domainByName[record.Name]; ok { mergeVMMetadata(&domain, record) list = append(list, domain) continue } var item dto.VMInfo if err := copier.Copy(&item, &record); err != nil { return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } v.fillMetadataVMStatus(&item) list = append(list, item) } return total, list, nil } func (v *VMService) Get(name string, checkDisks bool) (dto.VMInfo, error) { if !v.helperExists() { return dto.VMInfo{}, buserr.New("ErrVMHelperNotFound") } item, err := v.provider.Get(context.Background(), name, checkDisks) if err != nil { return dto.VMInfo{}, err } if record, err := vmRepo.Get(repo.WithByName(name)); err == nil { mergeVMMetadata(&item, record) } return item, nil } func (v *VMService) GetStats(name string) (dto.VMStats, error) { if !v.helperExists() { return dto.VMStats{}, buserr.New("ErrVMHelperNotFound") } return v.provider.Stats(context.Background(), name) } func (v *VMService) Create(req dto.VMCreate, limit int) (dto.VMTaskResp, error) { vmCreateMu.Lock() defer vmCreateMu.Unlock() if limit > 0 { count, err := vmRepo.Count() if err != nil { return dto.VMTaskResp{}, err } if count >= int64(limit) { return dto.VMTaskResp{}, buserr.WithMap("ErrVMCountLimit", map[string]interface{}{"limit": limit}, nil) } } if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } if _, err := vmRepo.Get(repo.WithByName(req.Name)); err == nil { return dto.VMTaskResp{}, buserr.New("ErrRecordExist") } else if err != gorm.ErrRecordNotFound { return dto.VMTaskResp{}, err } if err := v.validateMemoryLimit(req.Memory, ""); err != nil { return dto.VMTaskResp{}, err } if req.TemplateID == 0 && len(req.ISOPaths) == 0 { return dto.VMTaskResp{}, buserr.WithDetail("ErrVMInvalidInput", "ISO path is required", nil) } cfg := types.VMConfig{ Name: req.Name, CPU: req.CPU, MemoryMiB: req.Memory, DiskSizeGiB: req.DiskSize, DiskBus: resolveDiskBus(req.OSType, req.DiskBus), DiskPath: req.DiskPath, StoragePool: req.StoragePool, ISOPaths: req.ISOPaths, ImagePath: req.ImagePath, Network: req.Network, BootOrder: req.BootOrder, AutoStart: req.AutoStart, StartAfterCreate: req.StartAfterCreate, } cfg.BootOrder = "hd,cdrom" if req.TemplateID != 0 { template, err := vmRepo.GetTemplate(repo.WithByID(req.TemplateID)) if err != nil { return dto.VMTaskResp{}, err } if _, statErr := os.Stat(template.DiskPath); statErr != nil { return dto.VMTaskResp{}, statErr } cfg.ImagePath = template.DiskPath cfg.DiskBus = defaultDiskBus(template.DiskBus) cfg.ISOPaths = []string{} cfg.BootOrder = "hd" if template.DiskSize > 0 && cfg.DiskSizeGiB < template.DiskSize { return dto.VMTaskResp{}, buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size must be at least %d GiB", template.DiskSize), nil) } if cfg.DiskSizeGiB == 0 { cfg.DiskSizeGiB = 1 } if cfg.StoragePool == "" { cfg.StoragePool = template.StoragePool } } if err := v.validateDiskLimit(cfg.DiskSizeGiB, cfg.StoragePool); err != nil { return dto.VMTaskResp{}, err } createMetadataReq := v.buildCreateMetadataReq(req, cfg) if createMetadataReq.DiskPath != "" { if _, err := os.Lstat(createMetadataReq.DiskPath); err == nil { return dto.VMTaskResp{}, buserr.WithDetail("ErrVMImageAlreadyExists", createMetadataReq.DiskPath, nil) } else if !os.IsNotExist(err) { return dto.VMTaskResp{}, err } } createTaskKey := "VMCreateTask" if req.TemplateID != 0 { createTaskKey = "VMCreateFromTemplateTask" } taskName := vmTaskName(createTaskKey, req.Name) taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeVm, req.TaskID, 0) if err != nil { return dto.VMTaskResp{}, err } if err := v.saveMetadata(createMetadataReq, dto.VMInfo{DiskPath: createMetadataReq.DiskPath}, taskItem.Task.ID); err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMLock(req.Name, func() error { var domain dto.VMInfo var err error if req.TemplateID != 0 { domain, err = v.provider.CreateFromTemplate(context.Background(), cfg) } else { domain, err = v.provider.Create(context.Background(), cfg) } if err != nil { return err } v.syncCreatedDefaultResourceMetadata(cfg) return v.saveMetadata(createMetadataReq, domain, t.Task.ID) }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) ListTemplates() ([]dto.VMTemplateInfo, error) { records, err := vmRepo.ListTemplates() if err != nil { return nil, err } items := make([]dto.VMTemplateInfo, 0, len(records)) for _, record := range records { item := dto.VMTemplateInfo{ ID: record.ID, Name: record.Name, SourceVM: record.SourceVM, CPU: record.CPU, Memory: record.Memory, DiskSize: record.DiskSize, DiskBus: defaultDiskBus(record.DiskBus), DiskPath: record.DiskPath, StoragePool: record.StoragePool, Network: record.Network, BootOrder: record.BootOrder, CreatedAt: record.CreatedAt, Description: record.Description, } if stat, statErr := os.Stat(record.DiskPath); statErr == nil { item.IsExist = true item.Size = stat.Size() } items = append(items, item) } return items, nil } func (v *VMService) CreateTemplate(req dto.VMTemplateCreate) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } if item, _ := vmRepo.GetTemplate(repo.WithByName(req.TemplateName)); item.ID != 0 { return dto.VMTaskResp{}, buserr.New("ErrRecordExist") } source, err := v.Get(req.Name, false) if err != nil { return dto.VMTaskResp{}, err } storagePool := req.StoragePool if storagePool == "" { storagePool = source.StoragePool } if storagePool == "" { storagePool = "default" } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskName := vmTemplateTaskName("VMTemplateCreateTask", req.Name, req.TemplateName) taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMLock(req.Name, func() error { tpl, err := v.provider.CreateTemplate(context.Background(), types.VMTemplateCreate{ Name: req.TemplateName, SourceName: req.Name, StoragePool: storagePool, }) if err != nil { return err } templateRecord := &model.VMTemplate{ Name: req.TemplateName, SourceVM: req.Name, CPU: source.CPU, Memory: source.Memory, DiskSize: tpl.DiskSize, DiskBus: defaultDiskBus(source.DiskBus), DiskPath: tpl.DiskPath, StoragePool: storagePool, Network: source.Network, BootOrder: source.BootOrder, Description: req.Description, } if err := vmRepo.CreateTemplate(templateRecord); err != nil { if tpl.DiskPath != "" { _ = os.Remove(tpl.DiskPath) } return err } return nil }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) DeleteTemplate(req dto.VMTemplateDelete) (dto.VMTaskResp, error) { record, err := vmRepo.GetTemplate(repo.WithByID(req.ID)) if err != nil { return dto.VMTaskResp{}, err } taskName := vmTaskName("VMTemplateDeleteTask", record.Name) taskItem, err := task.NewTask(taskName, task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { if req.DeleteDisk && record.DiskPath != "" { if err := os.Remove(record.DiskPath); err != nil && !os.IsNotExist(err) { return err } } return vmRepo.DeleteTemplate(repo.WithByID(record.ID)) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) Update(req dto.VMUpdate) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } if err := v.validateMemoryLimit(req.Memory, req.Name); err != nil { return dto.VMTaskResp{}, err } cfg := types.VMConfig{ Name: req.Name, CPU: req.CPU, MemoryMiB: req.Memory, ISOPaths: req.ISOPaths, Network: req.Network, BootOrder: req.BootOrder, AutoStart: req.AutoStart, } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskName := vmTaskName("VMUpdateTask", req.Name) taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMLock(req.Name, func() error { domain, err := v.provider.Update(context.Background(), cfg) if err != nil { return err } return v.updateMetadata(req, domain, t.Task.ID) }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) ResizeDisk(req dto.VMDiskResize) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskName := i18n.GetMsgWithMap("VMDiskResizeTask", map[string]interface{}{"name": req.Name, "disk": req.Target}) taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMLock(req.Name, func() error { domain, err := v.provider.ResizeDisk(context.Background(), req) if err != nil { return err } vars := map[string]interface{}{"disk_path": domain.DiskPath, "task_id": t.Task.ID} if domain.DiskSize > 0 { vars["disk_size"] = domain.DiskSize } return vmRepo.Update(record.ID, vars) }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) Rename(req dto.VMRename) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } if req.Name == req.NewName { return dto.VMTaskResp{}, buserr.New("ErrRecordExist") } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } if item, _ := vmRepo.Get(repo.WithByName(req.NewName)); item.ID != 0 { return dto.VMTaskResp{}, buserr.New("ErrRecordExist") } taskName := vmRenameTaskName("VMRenameTask", req.Name, req.NewName) taskItem, err := task.NewTask(taskName, task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMRenameLock(req.Name, req.NewName, func() error { domain, err := v.provider.Rename(context.Background(), req.Name, req.NewName) if err != nil { return err } if err := v.renameMetadata(record, req, domain, t.Task.ID); err != nil { _, _ = v.provider.Rename(context.Background(), req.NewName, req.Name) return err } return nil }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) Delete(req dto.VMDelete) (dto.VMTaskResp, error) { record, err := vmRepo.Get(repo.WithByID(req.ID)) if err != nil { return dto.VMTaskResp{}, err } if !v.helperExists() && !req.Force { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } taskName := vmTaskName("VMDeleteTask", record.Name) taskItem, err := task.NewTask(taskName, task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error { return v.withVMLock(record.Name, func() error { if !v.helperExists() { if !req.Force { return buserr.New("ErrVMHelperNotFound") } t.Log(i18n.GetMsgWithMap("VMForceDeleteMetadataOnly", map[string]interface{}{"name": record.Name})) } else { paths, err := protectedVMPaths(record.ID) if err != nil { return err } if record.ImagePath != "" { paths = append(paths, record.ImagePath) } if err := v.provider.Delete(context.Background(), record.Name, req.Force, true, paths); err != nil { var helperErr *protocol.Error if errors.As(err, &helperErr) && helperErr.Code == "MAIN_DISK_DELETE_FAILED" { return buserr.WithErr("ErrVMCommandFailed", err) } if !req.Force { return err } t.Log(i18n.GetMsgWithMap("VMForceDeleteMetadataAfterLibvirtFailed", map[string]interface{}{"name": record.Name, "err": err})) } } if record.ID != 0 { return vmRepo.Delete(repo.WithByID(record.ID)) } return nil }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) ListSnapshots(req dto.VMSnapshotSearch) ([]dto.VMSnapshotInfo, error) { if !v.helperExists() { return nil, buserr.New("ErrVMHelperNotFound") } items, err := v.provider.ListSnapshots(context.Background(), req.Name) if err != nil { return nil, err } sortVMSnapshotItems(items) return items, nil } func sortVMSnapshotItems(items []dto.VMSnapshotInfo) { sort.SliceStable(items, func(i, j int) bool { if items[i].CreatedAt != items[j].CreatedAt { return items[i].CreatedAt > items[j].CreatedAt } return items[i].Name > items[j].Name }) } func (v *VMService) CreateSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotCreateTask", req), task.TaskCreate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotCreateStep", req), func(t *task.Task) error { return v.withVMLock(req.Name, func() error { return v.provider.CreateSnapshot(context.Background(), req.Name, req.SnapshotName, req.Description) }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) RecoverSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotRecoverTask", req), task.TaskUpdate, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotRecoverStep", req), func(t *task.Task) error { return v.withVMLock(req.Name, func() error { if err := v.provider.RecoverSnapshot(context.Background(), req.Name, req.SnapshotName); err != nil { return err } domain, err := v.provider.Get(context.Background(), req.Name, false) if err != nil { return buserr.WithErr("ErrVMSnapshotMetadataSync", err) } if domain.DiskSize <= 0 { return buserr.New("ErrVMSnapshotMetadataSync") } if err := vmRepo.Update(record.ID, map[string]interface{}{"disk_path": domain.DiskPath, "disk_size": domain.DiskSize}); err != nil { return buserr.WithErr("ErrVMSnapshotMetadataSync", err) } return nil }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func (v *VMService) DeleteSnapshot(req dto.VMSnapshotReq) (dto.VMTaskResp, error) { if !v.helperExists() { return dto.VMTaskResp{}, buserr.New("ErrVMHelperNotFound") } record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { return dto.VMTaskResp{}, err } taskItem, err := task.NewTask(vmSnapshotTaskName("VMSnapshotDeleteTask", req), task.TaskDelete, task.TaskScopeVm, req.TaskID, record.ID) if err != nil { return dto.VMTaskResp{}, err } taskItem.AddSubTaskWithOps(vmSnapshotTaskName("VMSnapshotDeleteStep", req), func(t *task.Task) error { return v.withVMLock(req.Name, func() error { return v.provider.DeleteSnapshot(context.Background(), req.Name, req.SnapshotName) }) }, nil, 0, 30*time.Minute) go taskItem.Execute() return dto.VMTaskResp{TaskID: taskItem.Task.ID}, nil } func vmSnapshotTaskName(key string, req dto.VMSnapshotReq) string { return i18n.GetMsgWithMap(key, map[string]interface{}{ "vm": req.Name, "snapshot": req.SnapshotName, }) } func vmTaskName(key, name string) string { return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name}) } func vmRenameTaskName(key, name, newName string) string { return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name, "newName": newName}) } func vmTemplateTaskName(key, name, template string) string { return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name, "template": template}) } func (v *VMService) Operate(req dto.VMOperate) error { switch req.Operate { case "start": return v.lifecycle(req.Name, v.provider.Start) case "shutdown": return v.lifecycle(req.Name, v.provider.Shutdown) case "reboot": return v.lifecycle(req.Name, v.provider.Reboot) case "destroy": return v.lifecycle(req.Name, v.provider.Destroy) case "suspend": return v.lifecycle(req.Name, v.provider.Suspend) case "resume": return v.lifecycle(req.Name, v.provider.Resume) default: return buserr.WithName("ErrNotSupportType", req.Operate) } } func (v *VMService) Console(name string) (*dto.VMGraphics, error) { if !v.helperExists() { return nil, buserr.New("ErrVMHelperNotFound") } return v.provider.Console(context.Background(), name) } func (v *VMService) lifecycle(name string, fn func(context.Context, string) error) error { if !v.helperExists() { return buserr.New("ErrVMHelperNotFound") } return v.withVMLock(name, func() error { return fn(context.Background(), name) }) } func (v *VMService) helperExists() bool { return v.provider.HelperStatus() } func (v *VMService) withVMLock(name string, fn func() error) error { lock := v.acquireVMLock(name) defer v.releaseVMLock(name, lock) return fn() } func (v *VMService) acquireVMLock(name string) *vmLockEntry { v.locksMu.Lock() if v.locks == nil { v.locks = make(map[string]*vmLockEntry) } lock := v.locks[name] if lock == nil { lock = &vmLockEntry{} v.locks[name] = lock } lock.refs++ v.locksMu.Unlock() lock.mu.Lock() return lock } func (v *VMService) releaseVMLock(name string, lock *vmLockEntry) { lock.mu.Unlock() v.locksMu.Lock() lock.refs-- if lock.refs == 0 && v.locks[name] == lock { delete(v.locks, name) } v.locksMu.Unlock() } func (v *VMService) withVMRenameLock(name string, newName string, fn func() error) error { if name > newName { name, newName = newName, name } return v.withVMLock(name, func() error { return v.withVMLock(newName, fn) }) } func (v *VMService) saveMetadata(req dto.VMCreate, domain dto.VMInfo, taskID string) error { record, err := vmRepo.Get(repo.WithByName(req.Name)) diskBus := defaultDiskBus(req.DiskBus) if domain.DiskBus != "" { diskBus = domain.DiskBus } if domain.UUID != "" { req.ISOPaths = domain.ISOPaths } data := &model.VirtualMachine{ Name: req.Name, UUID: domain.UUID, CPU: req.CPU, Memory: req.Memory, DiskSize: req.DiskSize, DiskBus: diskBus, DiskPath: domain.DiskPath, StoragePool: req.StoragePool, ISOPaths: req.ISOPaths, ImagePath: req.ImagePath, Network: req.Network, BootOrder: req.BootOrder, AutoStart: req.AutoStart, TaskID: taskID, CreateTaskID: taskID, Description: req.Description, } if err == nil && record.ID != 0 { return vmRepo.Update(record.ID, metadataUpdateMap(*data)) } if err != nil && err != gorm.ErrRecordNotFound { return err } return vmRepo.Create(data) } func (v *VMService) buildCreateMetadataReq(req dto.VMCreate, cfg types.VMConfig) dto.VMCreate { out := req out.DiskSize = cfg.DiskSizeGiB out.DiskBus = cfg.DiskBus out.DiskPath = cfg.DiskPath out.StoragePool = cfg.StoragePool out.ISOPaths = cfg.ISOPaths out.BootOrder = cfg.BootOrder if out.StoragePool == "" { out.StoragePool = defaultVMResourceName } if req.TemplateID != 0 { out.ImagePath = "" } if out.DiskPath == "" { out.DiskPath = v.defaultCreateDiskPath(out.Name, out.StoragePool) } return out } func (v *VMService) defaultCreateDiskPath(name string, storagePool string) string { if storagePool == "" { storagePool = defaultVMResourceName } if live, ok, err := v.findVMResource("storagePool", storagePool); err == nil && ok && live.Path != "" { return filepath.Join(live.Path, name+".qcow2") } if storagePool == defaultVMResourceName { return filepath.Join(defaultVMDiskDir, name+".qcow2") } return "" } func resolveDiskBus(osType string, diskBus string) string { if diskBus != "" { return diskBus } if osType == osTypeWindows { return diskBusSATA } return diskBusVirtIO } func defaultDiskBus(diskBus string) string { if diskBus == "" { return diskBusVirtIO } return diskBus } func (v *VMService) validateMemoryLimit(memoryMiB int64, currentName string) error { if memoryMiB < vmMemoryMinMiB { return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("memory must be at least %d MiB", vmMemoryMinMiB), nil) } memoryInfo, err := mem.VirtualMemory() if err != nil || memoryInfo.Total == 0 { return buserr.WithDetail("ErrVMInvalidInput", "failed to load host memory limit", err) } allocatedBytes, currentBytes, err := v.loadAllocatedVMMemory(currentName) if err != nil { return err } reservedBytes := uint64(float64(memoryInfo.Total) * vmMemoryReserveRatio) if reservedBytes < vmMemoryReserveMin { reservedBytes = vmMemoryReserveMin } if reservedBytes >= memoryInfo.Total { return buserr.WithDetail("ErrVMInvalidInput", "host memory is insufficient", nil) } availableBytes := memoryInfo.Total - reservedBytes if allocatedBytes > currentBytes { usedByOthers := allocatedBytes - currentBytes if usedByOthers >= availableBytes { return buserr.WithDetail("ErrVMInvalidInput", "host memory is insufficient", nil) } availableBytes -= usedByOthers } requestBytes := uint64(memoryMiB) * bytePerMiB if requestBytes > availableBytes { return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("memory exceeds available limit: %d MiB", availableBytes/bytePerMiB), nil) } return nil } func (v *VMService) loadAllocatedVMMemory(currentName string) (uint64, uint64, error) { domains, err := v.provider.List(context.Background()) if err != nil { return 0, 0, buserr.WithDetail("ErrVMInvalidInput", "failed to load virtual machine memory usage", err) } var allocated uint64 var current uint64 for _, domain := range domains { if !vmMemoryAllocatedStatus(domain.Status) { continue } memoryBytes := uint64(domain.Memory) * bytePerMiB allocated += memoryBytes if currentName != "" && domain.Name == currentName { current = memoryBytes } } return allocated, current, nil } func vmMemoryAllocatedStatus(status string) bool { switch strings.ToLower(status) { case "running", "blocked", "paused", "shutdown", "pmsuspended": return true default: return false } } func (v *VMService) validateDiskLimit(diskSizeGiB int64, storagePool string) error { if diskSizeGiB < vmDiskMinGiB { return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size must be at least %d GiB", vmDiskMinGiB), nil) } if storagePool == "" { storagePool = defaultVMResourceName } live, ok, err := v.findVMResource("storagePool", storagePool) if err != nil { return err } if !ok || live.Available == 0 { return nil } if uint64(diskSizeGiB)*bytePerGiB > live.Available { return buserr.WithDetail("ErrVMInvalidInput", fmt.Sprintf("disk size exceeds storage pool available space: %d GiB", live.Available/bytePerGiB), nil) } return nil } func (v *VMService) syncCreatedDefaultResourceMetadata(cfg types.VMConfig) { if cfg.Network == defaultVMResourceName { if err := v.upsertNetworkMetadata(defaultVMResourceName); err != nil { global.LOG.Warnf("[xpack] sync default VM network metadata failed: %v", err) } } storagePool := cfg.StoragePool if storagePool == "" { storagePool = defaultVMResourceName } if storagePool == defaultVMResourceName { if err := v.upsertStorageMetadata(defaultVMResourceName); err != nil { global.LOG.Warnf("[xpack] sync default VM storage metadata failed: %v", err) } } } func (v *VMService) upsertNetworkMetadata(name string) error { live, ok, err := v.findVMResource("network", name) if err != nil || !ok { return err } vars := map[string]interface{}{ "type": live.SubType, "bridge_name": live.BridgeName, "source_interface": live.SourceInterface, "cidr": live.CIDR, "gateway": live.Gateway, "dhcp_start": live.DHCPStart, "dhcp_end": live.DHCPEnd, "auto_start": live.AutoStart, "status": normalizeVMResourceStatus(live.Status), } record, err := vmRepo.GetNetwork(repo.WithByName(name)) if err == nil && record.ID != 0 { return vmRepo.UpdateNetwork(record.ID, vars) } if err != nil && err != gorm.ErrRecordNotFound { return err } return vmRepo.CreateNetwork(&model.VMNetwork{ Name: live.Name, Type: live.SubType, BridgeName: live.BridgeName, SourceInterface: live.SourceInterface, CIDR: live.CIDR, Gateway: live.Gateway, DHCPStart: live.DHCPStart, DHCPEnd: live.DHCPEnd, AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), }) } func (v *VMService) upsertStorageMetadata(name string) error { live, ok, err := v.findVMResource("storagePool", name) if err != nil || !ok { return err } vars := map[string]interface{}{ "type": live.SubType, "path": live.Path, "auto_start": live.AutoStart, "status": normalizeVMResourceStatus(live.Status), } record, err := vmRepo.GetStorage(repo.WithByName(name)) if err == nil && record.ID != 0 { return vmRepo.UpdateStorage(record.ID, vars) } if err != nil && err != gorm.ErrRecordNotFound { return err } return vmRepo.CreateStorage(&model.VMStorage{ Name: live.Name, Type: live.SubType, Path: live.Path, AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status), }) } func (v *VMService) findVMResource(resourceType, name string) (dto.VMResourceInfo, bool, error) { return v.findVMResourceWithContext(context.Background(), resourceType, name) } func (v *VMService) findVMResourceWithContext(ctx context.Context, resourceType, name string) (dto.VMResourceInfo, bool, error) { liveItems, err := v.provider.ListResources(ctx, resourceType) if err != nil { return dto.VMResourceInfo{}, false, err } for _, live := range liveItems { if live.Name == name { return live, true, nil } } return dto.VMResourceInfo{}, false, nil } func (v *VMService) updateMetadata(req dto.VMUpdate, domain dto.VMInfo, taskID string) error { record, err := vmRepo.Get(repo.WithByName(req.Name)) if err != nil { if err == gorm.ErrRecordNotFound { return nil } return err } return vmRepo.Update(record.ID, map[string]interface{}{ "uuid": domain.UUID, "cpu": req.CPU, "memory": req.Memory, "iso_paths": domain.ISOPaths, "network": req.Network, "boot_order": domain.BootOrder, "auto_start": req.AutoStart, "description": req.Description, "task_id": taskID, }) } func (v *VMService) renameMetadata(record model.VirtualMachine, req dto.VMRename, domain dto.VMInfo, taskID string) error { vars := metadataUpdateMap(record) vars["name"] = req.NewName vars["uuid"] = domain.UUID vars["cpu"] = domain.CPU vars["memory"] = domain.Memory if domain.DiskSize > 0 { vars["disk_size"] = domain.DiskSize } vars["disk_path"] = domain.DiskPath vars["iso_paths"] = domain.ISOPaths vars["network"] = domain.Network vars["boot_order"] = domain.BootOrder vars["auto_start"] = domain.AutoStart vars["task_id"] = taskID if err := vmRepo.Update(record.ID, vars); err != nil { return err } if err := vmRepo.UpdateTemplateSource(req.Name, req.NewName); err != nil { rollbackVars := metadataUpdateMap(record) rollbackVars["name"] = req.Name _ = vmRepo.Update(record.ID, rollbackVars) return err } return nil } func metadataUpdateMap(vm model.VirtualMachine) map[string]interface{} { return map[string]interface{}{ "uuid": vm.UUID, "cpu": vm.CPU, "memory": vm.Memory, "disk_size": vm.DiskSize, "disk_bus": vm.DiskBus, "disk_path": vm.DiskPath, "storage_pool": vm.StoragePool, "iso_paths": vm.ISOPaths, "image_path": vm.ImagePath, "network": vm.Network, "boot_order": vm.BootOrder, "auto_start": vm.AutoStart, "task_id": vm.TaskID, "create_task_id": vm.CreateTaskID, "description": vm.Description, } } func mergeVMMetadata(item *dto.VMInfo, record model.VirtualMachine) { item.ID = record.ID item.StoragePool = record.StoragePool item.ImagePath = record.ImagePath if item.DiskPath == "" { item.DiskPath = record.DiskPath } if item.DiskBus == "" { item.DiskBus = defaultDiskBus(record.DiskBus) } item.AutoStart = record.AutoStart item.TaskID = record.TaskID item.CreateTaskID = record.CreateTaskID item.Description = record.Description if item.UUID == "" { item.UUID = record.UUID } if item.DiskSize == 0 { item.DiskSize = record.DiskSize } if item.Status == "" { item.Status = record.Status } } func (v *VMService) fillMetadataVMStatus(item *dto.VMInfo) { if item.Status != "" { return } taskID := item.CreateTaskID if taskID == "" { taskID = item.TaskID } if taskID == "" { return } taskRecord, err := taskRepo.GetFirst(taskRepo.WithByID(taskID)) if err != nil { return } switch taskRecord.Status { case constant.StatusFailed: item.Status = "failed" case constant.StatusExecuting, constant.StatusWaiting, constant.StatusRunning: item.Status = "creating" } }