Files
1Panel/agent/app/service/vm-network.go
ssongliu c06c4e338b feat(vm): enable community virtual machines and migrate xpack data (#14279)
* feat(vm): enable community virtual machines and migrate xpack data

* fix(vm): stop ISO download polling on failure or cancellation

* fix(vm): respect deleteFile when deleting failed ISO imports
2026-11-02 18:22:58 +08:00

465 lines
14 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"sort"
"strings"
"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/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/vm/types"
"gorm.io/gorm"
)
func (v *VMService) ListNetworks() ([]dto.VMNetworkItem, error) {
records, err := vmRepo.ListNetworks()
if err != nil {
return nil, err
}
items := v.networkRecordsToItems(records)
return items, nil
}
func (v *VMService) ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) {
return listNetworkBridges("/sys/class/net")
}
func (v *VMService) ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
return listMacvtapInterfaces("/sys/class/net", interfaces)
}
func (v *VMService) CreateNetwork(req dto.VMNetworkCreate) error {
if req.Type == "macvtap" {
interfaces, err := v.ListNetworkInterfaces()
if err != nil {
return err
}
found := false
for _, item := range interfaces {
if item.Name == req.SourceInterface {
found = true
break
}
}
if !found {
return fmt.Errorf("unavailable macvtap source interface: %s", req.SourceInterface)
}
} else {
req.SourceInterface = ""
}
if req.Type != "nat" {
req.CIDR, req.Gateway, req.DHCPStart, req.DHCPEnd = "", "", "", ""
}
if req.Type == "macvtap" {
req.BridgeName = ""
}
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
unlock, err := vmResourceLocks.lock(ctx, "network", req.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByName(req.Name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if itemNetwork.ID != 0 {
return buserr.New("ErrRecordExist")
}
if req.Type == "bridge" && isManagedNetworkBridge(req.BridgeName) {
return buserr.New("ErrVMBridgeManaged")
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
if _, exists, err := v.findVMResourceWithContext(ctx, "network", req.Name); err != nil {
return err
} else if exists {
return buserr.New("ErrRecordExist")
}
record := model.VMNetwork{
Name: req.Name, Type: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description,
}
if err := vmRepo.CreateNetwork(&record); err != nil {
return err
}
if err := v.provider.CreateResource(ctx, types.VMResourceCreate{
Type: "network", Name: req.Name, SubType: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart,
}); err != nil {
if updateErr := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusFailed, "error": err.Error(),
}); updateErr != nil {
return errors.Join(err, fmt.Errorf("save network creation failure: %w", updateErr))
}
return err
}
return vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusRunning, "error": "",
})
}
func (v *VMService) SyncNetworks() error {
revision, idle := vmResourceLocks.snapshot("network")
if !idle {
return nil
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(context.Background(), "network")
if err != nil {
return err
}
return vmResourceLocks.applySnapshot("network", revision, func() error {
records, err := vmRepo.ListNetworks()
if err != nil {
return err
}
liveByName := map[string]dto.VMResourceInfo{}
for _, live := range liveItems {
if live.Name == "" {
continue
}
liveByName[live.Name] = live
}
recordsByName := map[string]model.VMNetwork{}
for _, record := range records {
if vmResourceStatusEquals(record.Status, vmResourceCreating) {
record.Status = constant.StatusFailed
record.Error = "Network creation was interrupted before its result was saved"
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": record.Status, "error": record.Error,
}); err != nil {
return err
}
}
recordsByName[record.Name] = record
}
for name, live := range liveByName {
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),
}
if record, ok := recordsByName[name]; ok {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if err := vmRepo.UpdateNetwork(record.ID, vars); err != nil {
return err
}
continue
}
if err := 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),
}); err != nil {
return err
}
}
for _, record := range recordsByName {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if _, ok := liveByName[record.Name]; !ok {
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusDeleted,
}); err != nil {
return err
}
}
}
return nil
})
}
func (v *VMService) UpdateNetwork(req dto.VMNetworkUpdate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
unlock, err := vmResourceLocks.lock(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if itemNetwork.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
vars := map[string]interface{}{}
if req.Description != nil {
vars["description"] = *req.Description
}
if req.AutoStart != nil || req.Status != nil {
if vmResourceCreationIncomplete(itemNetwork.Status) {
return buserr.WithDetail("ErrInvalidParams", "network creation has not succeeded", nil)
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
live, ok, err := v.findVMResourceWithContext(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
if !ok {
_ = vmRepo.UpdateNetwork(req.ID, map[string]interface{}{"status": constant.StatusDeleted})
return buserr.WithDetail("ErrResourceNotExists", "Network does not exist", nil)
}
if live.Status != "" {
vars["status"] = normalizeVMResourceStatus(live.Status)
}
}
if req.AutoStart != nil {
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, AutoStart: req.AutoStart,
}); err != nil {
return err
}
vars["auto_start"] = *req.AutoStart
}
if req.Status != nil {
status := normalizeVMResourceStatus(*req.Status)
if !validVMResourceOperateStatus(status) {
return buserr.WithDetail("ErrInvalidParams", status, nil)
}
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, Status: &status,
}); err != nil {
return err
}
vars["status"] = status
}
if len(vars) == 0 {
return nil
}
return vmRepo.UpdateNetwork(req.ID, vars)
}
func (v *VMService) DeleteNetwork(req dto.VMResourceDelete) error {
ctx, cancel := vmResourceOperationContext(req.Force)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrRecordNotFound")
}
return err
}
unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "network", itemNetwork.Name, req.Force)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if len(loadVMResourceRefs("network")[itemNetwork.Name]) > 0 {
return buserr.New("ErrVMNetworkInUse")
}
if cleanup {
if err := v.deleteVMResource(ctx, "network", itemNetwork.Name, req.Force); err != nil {
return err
}
}
return vmRepo.DeleteNetwork(repo.WithByID(req.ID))
}
func (v *VMService) networkRecordsToItems(records []model.VMNetwork) []dto.VMNetworkItem {
items := make([]dto.VMNetworkItem, 0, len(records))
vmRefs := loadVMResourceRefs("network")
usedIPs := v.loadVMNetworkIPUsages(records)
for _, record := range records {
status := normalizeVMResourceStatus(record.Status)
items = append(items, dto.VMNetworkItem{
ID: record.ID, Name: record.Name, Type: record.Type,
BridgeName: record.BridgeName, SourceInterface: record.SourceInterface, CIDR: record.CIDR, Gateway: record.Gateway,
DHCPStart: record.DHCPStart, DHCPEnd: record.DHCPEnd, AutoStart: record.AutoStart,
Description: record.Description, CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
Status: status, Error: record.Error, VMCount: len(vmRefs[record.Name]), VMs: vmRefs[record.Name],
Registered: true, UsedIPs: usedIPs[record.Name],
})
}
return items
}
func (v *VMService) loadVMNetworkIPUsages(records []model.VMNetwork) map[string][]dto.VMNetworkIPUsage {
usages := map[string][]dto.VMNetworkIPUsage{}
if !v.helperExists() {
return usages
}
domains, err := v.provider.List(context.Background())
if err != nil {
return usages
}
vmRecords, err := vmRepo.List()
if err != nil {
return usages
}
vmRecordsByName := map[string]model.VirtualMachine{}
for _, record := range vmRecords {
vmRecordsByName[record.Name] = record
}
networkNames := map[string]struct{}{}
bridgeToNetwork := map[string]string{}
for _, record := range records {
networkNames[record.Name] = struct{}{}
if record.BridgeName != "" {
bridgeToNetwork[record.BridgeName] = record.Name
}
}
for _, domain := range domains {
if record, ok := vmRecordsByName[domain.Name]; ok {
mergeVMMetadata(&domain, record)
}
for _, item := range domain.Interfaces {
ip := strings.TrimSpace(item.IP)
if ip == "" {
continue
}
networkName := resolveVMInterfaceNetwork(item.Source, domain.Network, networkNames, bridgeToNetwork)
if networkName == "" {
continue
}
usages[networkName] = append(usages[networkName], dto.VMNetworkIPUsage{
IP: ip, VMName: domain.Name, MAC: item.MAC, Source: item.Source,
})
}
}
for name := range usages {
sort.Slice(usages[name], func(i, j int) bool {
if usages[name][i].IP != usages[name][j].IP {
return usages[name][i].IP < usages[name][j].IP
}
return strings.ToLower(usages[name][i].VMName) < strings.ToLower(usages[name][j].VMName)
})
}
return usages
}
func resolveVMInterfaceNetwork(source, vmNetwork string, networkNames map[string]struct{}, bridgeToNetwork map[string]string) string {
if _, ok := networkNames[source]; ok {
return source
}
if _, ok := networkNames[vmNetwork]; ok {
return vmNetwork
}
return bridgeToNetwork[source]
}
func listNetworkBridges(netClassPath string) ([]dto.VMNetworkBridgeItem, error) {
entries, err := os.ReadDir(netClassPath)
if err != nil {
if os.IsNotExist(err) {
return []dto.VMNetworkBridgeItem{}, nil
}
return nil, err
}
items := make([]dto.VMNetworkBridgeItem, 0)
for _, entry := range entries {
name := entry.Name()
netPath := netClassPath + "/" + name
info, err := os.Stat(netPath)
if err != nil || !info.IsDir() {
continue
}
if _, err := os.Stat(netPath + "/bridge"); err != nil {
continue
}
item := dto.VMNetworkBridgeItem{
Name: name,
Recommended: true,
}
switch {
case name == "docker0" || strings.HasPrefix(name, "br-"):
item.Recommended = false
item.Description = "docker"
case name == "virbr0" || strings.HasPrefix(name, "virbr"):
item.Recommended = false
item.Description = "libvirt"
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Recommended != items[j].Recommended {
return items[i].Recommended
}
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
})
return items, nil
}
func isManagedNetworkBridge(name string) bool {
return name == "docker0" || strings.HasPrefix(name, "br-") || name == "virbr0" || strings.HasPrefix(name, "virbr")
}
func listMacvtapInterfaces(netClassPath string, interfaces []net.Interface) ([]dto.VMNetworkInterfaceItem, error) {
items := make([]dto.VMNetworkInterfaceItem, 0)
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) != 6 {
continue
}
netPath := filepath.Join(netClassPath, iface.Name)
if _, err := os.Stat(filepath.Join(netPath, "device")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
continue
}
excluded := false
for _, name := range []string{"master", "wireless", "bridge"} {
if _, err := os.Stat(filepath.Join(netPath, name)); err == nil {
excluded = true
break
} else if !os.IsNotExist(err) {
return nil, err
}
}
if excluded {
continue
}
addresses, err := iface.Addrs()
if err != nil {
return nil, err
}
item := dto.VMNetworkInterfaceItem{Name: iface.Name, Addresses: []string{}}
for _, address := range addresses {
item.Addresses = append(item.Addresses, address.String())
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
return items, nil
}