Files
Sylve/internal/services/libvirt/libvirt_network.go
T

732 lines
18 KiB
Go

// SPDX-License-Identifier: BSD-2-Clause
//
// Copyright (c) 2025 The FreeBSD Foundation.
//
// This software was developed by Hayzam Sherif <hayzam@alchemilla.io>
// of Alchemilla Ventures Pvt. Ltd. <hello@alchemilla.io>,
// under sponsorship from the FreeBSD Foundation.
package libvirt
import (
"fmt"
"strconv"
"strings"
networkModels "github.com/alchemillahq/sylve/internal/db/models/network"
vmModels "github.com/alchemillahq/sylve/internal/db/models/vm"
libvirtServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/libvirt"
"github.com/alchemillahq/sylve/internal/logger"
"github.com/alchemillahq/sylve/pkg/utils"
"github.com/beevik/etree"
)
func (s *Service) NetworkDetach(rid uint, networkId uint) error {
if err := s.requireVMMutationOwnership(rid); err != nil {
return err
}
if err := s.requireConnection(); err != nil {
return err
}
inactive, err := s.IsDomainInactive(rid)
if err != nil {
return fmt.Errorf("failed_to_check_vm_inactive: %w", err)
}
if !inactive {
return fmt.Errorf("vm_is_active: cannot_detach_network")
}
xmlDesc, err := s.GetVMXML(rid)
if err != nil {
return fmt.Errorf("failed_to_get_vm_xml: %w", err)
}
var network vmModels.Network
if err := s.DB.
Preload("AddressObj").
Preload("AddressObj.Entries").
First(&network, "id = ?", networkId).Error; err != nil {
return fmt.Errorf("failed_to_find_network: %w", err)
}
if err := network.AfterFind(s.DB); err != nil {
return err
}
var mac string
if network.AddressObj == nil || len(network.AddressObj.Entries) == 0 {
logger.L.Debug().Msgf("Network detach: network_mac_address_missing for network ID %d, proceeding with detach", networkId)
} else {
mac = strings.TrimSpace(strings.ToLower(network.AddressObj.Entries[0].Value))
}
if len(mac) != 0 {
doc := etree.NewDocument()
if err := doc.ReadFromString(xmlDesc); err != nil {
return fmt.Errorf("failed_to_parse_vm_xml: %w", err)
}
found := false
for _, iface := range doc.FindElements("//interface[@type='bridge']") {
macEl := iface.FindElement("mac")
if macEl == nil {
continue
}
addrAttr := macEl.SelectAttr("address")
if addrAttr == nil {
continue
}
if strings.EqualFold(strings.TrimSpace(addrAttr.Value), mac) {
iface.Parent().RemoveChild(iface)
found = true
logger.L.Debug().Msgf("Removed interface with MAC: %s", addrAttr.Value)
break
}
}
if !found {
logger.L.Debug().Msgf("Network detach: network_interface_not_found_in_xml: %s", mac)
if err := s.DB.Delete(&network).Error; err != nil {
return fmt.Errorf("failed_to_delete_network_record: %w", err)
}
return nil
}
newXML, err := doc.WriteToString()
if err != nil {
return fmt.Errorf("failed_to_serialize_modified_xml: %w", err)
}
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(rid)))
if err != nil {
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
}
if err := s.conn().DomainUndefineFlags(domain, 0); err != nil {
return fmt.Errorf("failed_to_undefine_domain: %w", err)
}
if _, err := s.conn().DomainDefineXML(newXML); err != nil {
return fmt.Errorf("failed_to_define_domain_with_modified_xml: %w", err)
}
}
if err := s.DB.Delete(&network).Error; err != nil {
return fmt.Errorf("failed_to_delete_network_record: %w", err)
}
err = s.WriteVMJson(rid)
if err != nil {
logger.L.Error().Err(err).Msg("Failed to write VM JSON after network detach")
}
return nil
}
func (s *Service) NetworkAttach(req libvirtServiceInterfaces.NetworkAttachRequest) error {
if err := s.requireVMMutationOwnership(req.RID); err != nil {
return err
}
if err := s.requireConnection(); err != nil {
return err
}
inactive, err := s.IsDomainInactive(req.RID)
if err != nil {
return fmt.Errorf("failed_to_check_vm_inactive: %w", err)
}
if !inactive {
return fmt.Errorf("vm_is_active: cannot_attach_network")
}
if req.Emulation == "" || (req.Emulation != "virtio" && req.Emulation != "e1000") {
return fmt.Errorf("invalid_emulation_type: %s", req.Emulation)
}
macObjId := uint(0)
if req.MacId != nil {
macObjId = *req.MacId
}
swType := ""
var stdSwitch networkModels.StandardSwitch
if err := s.DB.First(&stdSwitch, "name = ?", req.SwitchName).Error; err == nil {
swType = "standard"
}
var manualSwitch networkModels.ManualSwitch
if err := s.DB.First(&manualSwitch, "name = ?", req.SwitchName).Error; err == nil {
swType = "manual"
}
if swType == "" {
return fmt.Errorf("switch_not_found: %s", req.SwitchName)
}
vms, err := s.ListVMs()
if err != nil {
return fmt.Errorf("failed_to_list_vms: %w", err)
}
var vm *vmModels.VM
for _, v := range vms {
if v.RID == req.RID {
vm = &v
break
}
}
var sw any
switch swType {
case "standard":
sw = stdSwitch
case "manual":
sw = manualSwitch
default:
return fmt.Errorf("unknown_switch_type: %s", swType)
}
if macObjId == 0 {
macAddress := utils.GenerateRandomMAC()
var base string
switch v := sw.(type) {
case networkModels.StandardSwitch:
base = fmt.Sprintf("%s-%s", vm.Name, v.Name)
case networkModels.ManualSwitch:
base = fmt.Sprintf("%s-%s", vm.Name, v.Name)
default:
return fmt.Errorf("invalid switch type %T", v)
}
name := base
for i := 0; ; i++ {
if i > 0 {
name = fmt.Sprintf("%s-%d", base, i)
}
var exists int64
if err := s.DB.
Model(&networkModels.Object{}).
Where("name = ?", name).
Limit(1).
Count(&exists).Error; err != nil {
return fmt.Errorf("failed_to_check_mac_object_exists: %w", err)
}
if exists == 0 {
break
}
}
macObj := networkModels.Object{
Name: name,
Type: "Mac",
}
if err := s.DB.Create(&macObj).Error; err != nil {
return fmt.Errorf("failed_to_create_mac_object: %w", err)
}
macEntry := networkModels.ObjectEntry{
ObjectID: macObj.ID,
Value: macAddress,
}
if err := s.DB.Create(&macEntry).Error; err != nil {
return fmt.Errorf("failed_to_create_mac_entry: %w", err)
}
macObjId = macObj.ID
} else {
var macObj networkModels.Object
if err := s.DB.Preload("Entries").First(&macObj, macObjId).Error; err != nil {
return fmt.Errorf("failed_to_find_mac_object: %w", err)
}
if macObj.Type != "Mac" {
return fmt.Errorf("invalid_mac_object_type: %s", macObj.Type)
}
if len(macObj.Entries) == 0 {
return fmt.Errorf("mac_object_has_no_entries: %d", macObjId)
}
var otherNetworks []vmModels.Network
if err := s.DB.Where("mac_id = ? AND vm_id != ?", macObjId, vm.ID).
Find(&otherNetworks).Error; err != nil {
return fmt.Errorf("failed_to_find_other_networks_using_mac_object: %w", err)
}
}
var switchId uint
switch v := sw.(type) {
case networkModels.StandardSwitch:
switchId = v.ID
case networkModels.ManualSwitch:
switchId = v.ID
default:
return fmt.Errorf("invalid switch type %T", v)
}
network := vmModels.Network{
VMID: vm.ID,
SwitchID: switchId,
SwitchType: swType,
MacID: &macObjId,
Emulation: req.Emulation,
}
if err := s.DB.Create(&network).Error; err != nil {
return fmt.Errorf("failed_to_create_network_record: %w", err)
}
var macAddress string
if macObjId != 0 {
var macObj networkModels.Object
if err := s.DB.Preload("Entries").First(&macObj, macObjId).Error; err != nil {
return fmt.Errorf("failed_to_find_mac_object: %w", err)
}
if len(macObj.Entries) == 0 {
return fmt.Errorf("mac_object_has_no_entries: %d", macObjId)
}
macAddress = macObj.Entries[0].Value
}
xmlDesc, err := s.GetVMXML(req.RID)
if err != nil {
return fmt.Errorf("failed_to_get_vm_xml: %w", err)
}
doc := etree.NewDocument()
if err := doc.ReadFromString(xmlDesc); err != nil {
return fmt.Errorf("failed_to_parse_vm_xml: %w", err)
}
domainEl := doc.SelectElement("domain")
if domainEl == nil {
return fmt.Errorf("malformed_vm_xml: missing <domain> element")
}
devicesEl := domainEl.FindElement("devices")
if devicesEl == nil {
devicesEl = etree.NewElement("devices")
domainEl.AddChild(devicesEl)
}
ifaceEl := etree.NewElement("interface")
ifaceEl.CreateAttr("type", "bridge")
macEl := etree.NewElement("mac")
macEl.CreateAttr("address", macAddress)
ifaceEl.AddChild(macEl)
sourceEl := etree.NewElement("source")
switch swType {
case "manual":
manualSwitch := sw.(networkModels.ManualSwitch)
sourceEl.CreateAttr("bridge", manualSwitch.Bridge)
case "standard":
stdSwitch := sw.(networkModels.StandardSwitch)
sourceEl.CreateAttr("bridge", stdSwitch.BridgeName)
}
ifaceEl.AddChild(sourceEl)
modelEl := etree.NewElement("model")
modelEl.CreateAttr("type", network.Emulation)
ifaceEl.AddChild(modelEl)
devicesEl.AddChild(ifaceEl)
newXML, err := doc.WriteToString()
if err != nil {
return fmt.Errorf("failed_to_serialize_modified_xml: %w", err)
}
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(req.RID)))
if err != nil {
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
}
if err := s.conn().DomainUndefineFlags(domain, 0); err != nil {
return fmt.Errorf("failed_to_undefine_domain: %w", err)
}
if _, err := s.conn().DomainDefineXML(newXML); err != nil {
return fmt.Errorf("failed_to_define_domain_with_modified_xml: %w", err)
}
err = s.WriteVMJson(vm.RID)
if err != nil {
logger.L.Error().Err(err).Msg("Failed to write VM JSON after network attach")
}
return nil
}
func (s *Service) NetworkUpdate(req libvirtServiceInterfaces.NetworkUpdateRequest) error {
var network vmModels.Network
if err := s.DB.
Preload("AddressObj").
Preload("AddressObj.Entries").
First(&network, "id = ?", req.NetworkID).Error; err != nil {
return fmt.Errorf("failed_to_find_network: %w", err)
}
var vm vmModels.VM
if err := s.DB.First(&vm, "id = ?", network.VMID).Error; err != nil {
return fmt.Errorf("failed_to_find_vm: %w", err)
}
if err := s.requireVMMutationOwnership(vm.RID); err != nil {
return err
}
if err := s.requireConnection(); err != nil {
return err
}
if req.Emulation == "" || (req.Emulation != "virtio" && req.Emulation != "e1000") {
return fmt.Errorf("invalid_emulation_type: %s", req.Emulation)
}
inactive, err := s.IsDomainInactive(vm.RID)
if err != nil {
return fmt.Errorf("failed_to_check_vm_inactive: %w", err)
}
if !inactive {
return fmt.Errorf("vm_is_active: cannot_update_network")
}
oldMac := ""
if network.AddressObj != nil && len(network.AddressObj.Entries) > 0 {
oldMac = strings.TrimSpace(strings.ToLower(network.AddressObj.Entries[0].Value))
}
if oldMac == "" {
return fmt.Errorf("network_mac_address_missing")
}
switchID := uint(0)
switchType := ""
bridgeName := ""
dbSwName := ""
var stdSwitch networkModels.StandardSwitch
if err := s.DB.First(&stdSwitch, "name = ?", req.SwitchName).Error; err == nil {
switchID = stdSwitch.ID
switchType = "standard"
bridgeName = stdSwitch.BridgeName
dbSwName = stdSwitch.Name
} else {
var manualSwitch networkModels.ManualSwitch
if err := s.DB.First(&manualSwitch, "name = ?", req.SwitchName).Error; err == nil {
switchID = manualSwitch.ID
switchType = "manual"
bridgeName = manualSwitch.Bridge
dbSwName = manualSwitch.Name
}
}
if switchType == "" || switchID == 0 {
return fmt.Errorf("switch_not_found: %s", req.SwitchName)
}
var macObjId uint
if network.MacID != nil {
macObjId = *network.MacID
}
forceNewMac := false
if req.MacId != nil {
if *req.MacId == 0 {
forceNewMac = true
} else {
macObjId = *req.MacId
}
}
newMac := ""
if forceNewMac || macObjId == 0 {
macAddress := utils.GenerateRandomMAC()
base := fmt.Sprintf("%s-%s", vm.Name, dbSwName)
name := base
for i := 0; ; i++ {
if i > 0 {
name = fmt.Sprintf("%s-%d", base, i)
}
var exists int64
if err := s.DB.
Model(&networkModels.Object{}).
Where("name = ?", name).
Limit(1).
Count(&exists).Error; err != nil {
return fmt.Errorf("failed_to_check_mac_object_exists: %w", err)
}
if exists == 0 {
break
}
}
macObj := networkModels.Object{
Name: name,
Type: "Mac",
}
if err := s.DB.Create(&macObj).Error; err != nil {
return fmt.Errorf("failed_to_create_mac_object: %w", err)
}
macEntry := networkModels.ObjectEntry{
ObjectID: macObj.ID,
Value: macAddress,
}
if err := s.DB.Create(&macEntry).Error; err != nil {
return fmt.Errorf("failed_to_create_mac_entry: %w", err)
}
macObjId = macObj.ID
newMac = macAddress
} else {
var macObj networkModels.Object
if err := s.DB.Preload("Entries").First(&macObj, macObjId).Error; err != nil {
return fmt.Errorf("failed_to_find_mac_object: %w", err)
}
if macObj.Type != "Mac" {
return fmt.Errorf("invalid_mac_object_type: %s", macObj.Type)
}
if len(macObj.Entries) == 0 {
return fmt.Errorf("mac_object_has_no_entries: %d", macObjId)
}
var count int64
if err := s.DB.Model(&vmModels.Network{}).
Where("mac_id = ? AND id != ?", macObjId, network.ID).
Count(&count).Error; err != nil {
return fmt.Errorf("failed_to_find_other_networks_using_mac_object: %w", err)
}
if count > 0 {
return fmt.Errorf("mac_object_already_in_use: %d", macObjId)
}
newMac = macObj.Entries[0].Value
}
if strings.TrimSpace(newMac) == "" {
newMac = oldMac
}
xmlDesc, err := s.GetVMXML(vm.RID)
if err != nil {
return fmt.Errorf("failed_to_get_vm_xml: %w", err)
}
doc := etree.NewDocument()
if err := doc.ReadFromString(xmlDesc); err != nil {
return fmt.Errorf("failed_to_parse_vm_xml: %w", err)
}
var ifaceEl *etree.Element
for _, iface := range doc.FindElements("//interface[@type='bridge']") {
macEl := iface.FindElement("mac")
if macEl == nil {
continue
}
addrAttr := macEl.SelectAttr("address")
if addrAttr == nil {
continue
}
if strings.EqualFold(strings.TrimSpace(addrAttr.Value), oldMac) {
ifaceEl = iface
break
}
}
if ifaceEl == nil {
return fmt.Errorf("network_interface_not_found_in_xml: %s", oldMac)
}
macEl := ifaceEl.FindElement("mac")
if macEl == nil {
macEl = etree.NewElement("mac")
ifaceEl.AddChild(macEl)
}
if addrAttr := macEl.SelectAttr("address"); addrAttr != nil {
addrAttr.Value = newMac
} else {
macEl.CreateAttr("address", newMac)
}
sourceEl := ifaceEl.FindElement("source")
if sourceEl == nil {
sourceEl = etree.NewElement("source")
ifaceEl.AddChild(sourceEl)
}
if bridgeAttr := sourceEl.SelectAttr("bridge"); bridgeAttr != nil {
bridgeAttr.Value = bridgeName
} else {
sourceEl.CreateAttr("bridge", bridgeName)
}
modelEl := ifaceEl.FindElement("model")
if modelEl == nil {
modelEl = etree.NewElement("model")
ifaceEl.AddChild(modelEl)
}
if typeAttr := modelEl.SelectAttr("type"); typeAttr != nil {
typeAttr.Value = req.Emulation
} else {
modelEl.CreateAttr("type", req.Emulation)
}
newXML, err := doc.WriteToString()
if err != nil {
return fmt.Errorf("failed_to_serialize_modified_xml: %w", err)
}
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(vm.RID)))
if err != nil {
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
}
if err := s.conn().DomainUndefineFlags(domain, 0); err != nil {
return fmt.Errorf("failed_to_undefine_domain: %w", err)
}
if _, err := s.conn().DomainDefineXML(newXML); err != nil {
return fmt.Errorf("failed_to_define_domain_with_modified_xml: %w", err)
}
network.SwitchID = switchID
network.SwitchType = switchType
network.Emulation = req.Emulation
updates := map[string]any{
"switch_id": network.SwitchID,
"switch_type": network.SwitchType,
"emulation": network.Emulation,
}
if macObjId != 0 {
updates["mac_id"] = macObjId
} else {
updates["mac_id"] = nil
}
if err := s.DB.Model(&vmModels.Network{}).
Where("id = ?", network.ID).
Updates(updates).Error; err != nil {
return fmt.Errorf("failed_to_update_network_record: %w", err)
}
err = s.WriteVMJson(vm.RID)
if err != nil {
logger.L.Error().Err(err).Msg("Failed to write VM JSON after network update")
}
return nil
}
func (s *Service) FindAndChangeMAC(rid uint, oldMac string, newMac string) error {
if err := s.requireConnection(); err != nil {
return err
}
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(rid)))
if err != nil {
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
}
xml, err := s.conn().DomainGetXMLDesc(domain, 0)
if err != nil {
return fmt.Errorf("failed_to_get_domain_xml_desc: %w", err)
}
doc := etree.NewDocument()
if err := doc.ReadFromString(xml); err != nil {
return fmt.Errorf("failed_to_parse_domain_xml: %w", err)
}
oldMac = strings.ToLower(oldMac)
newMac = strings.ToLower(newMac)
macEl := doc.FindElement("//mac[@address='" + oldMac + "']")
if macEl == nil {
return fmt.Errorf("mac_address_not_found_in_xml: %s", oldMac)
}
addrAttr := macEl.SelectAttr("address")
if addrAttr != nil {
addrAttr.Value = newMac
} else {
macEl.CreateAttr("address", newMac)
}
out, err := doc.WriteToString()
if err != nil {
return fmt.Errorf("failed to serialize XML: %w", err)
}
if err := s.conn().DomainUndefineFlags(domain, 0); err != nil {
return fmt.Errorf("failed_to_undefine_domain: %w", err)
}
if _, err := s.conn().DomainDefineXML(out); err != nil {
return fmt.Errorf("failed_to_define_domain_with_modified_xml: %w", err)
}
err = s.WriteVMJson(rid)
if err != nil {
logger.L.Error().Err(err).Msg("Failed to write VM JSON after MAC modification")
}
return nil
}
func (s *Service) FindVmByMac(mac string) (vmModels.VM, error) {
mac = strings.ToLower(strings.TrimSpace(mac))
var netIf vmModels.Network
var vm vmModels.VM
err := s.DB.
Joins("LEFT JOIN objects ON networks.mac_id = objects.id").
Joins("LEFT JOIN object_entries ON object_entries.object_id = objects.id").
Where("LOWER(object_entries.value) = ?", mac).
First(&netIf).Error
if err != nil {
return vm, fmt.Errorf("failed_to_find_network: %w", err)
}
if err := s.DB.First(&vm, "id = ?", netIf.VMID).Error; err != nil {
return vm, fmt.Errorf("failed_to_find_vm: %w", err)
}
if vm.WoL == false {
return vm, fmt.Errorf("vm_wol_disabled: %s", vm.Name)
}
return vm, nil
}