mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-07 00:05:14 +00:00
* 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
465 lines
14 KiB
Go
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
|
|
}
|