mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-27 02:46:21 +03:00
632 lines
15 KiB
Go
632 lines
15 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"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
vmModels "github.com/alchemillahq/sylve/internal/db/models/vm"
|
|
"github.com/alchemillahq/sylve/internal/logger"
|
|
"github.com/alchemillahq/sylve/pkg/utils"
|
|
"github.com/beevik/etree"
|
|
)
|
|
|
|
func (s *Service) ModifyWakeOnLan(rid uint, enabled bool) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
|
|
err := s.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("wo_l", enabled).Error
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after WoL modification")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ModifyBootOrder(rid uint, startAtBoot bool, bootOrder int) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
|
|
err := s.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Updates(map[string]interface{}{
|
|
"start_order": bootOrder,
|
|
"start_at_boot": startAtBoot,
|
|
}).Error
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after boot order modification")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ModifyClock(rid uint, timeOffset string) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireConnection(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if timeOffset != "utc" && timeOffset != "localtime" {
|
|
return fmt.Errorf("invalid_time_offset: %s", timeOffset)
|
|
}
|
|
|
|
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(rid)))
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
|
|
}
|
|
|
|
state, _, err := s.conn().DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_domain_state: %w", err)
|
|
}
|
|
|
|
if state != 5 {
|
|
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
|
|
}
|
|
|
|
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_xml: %w", err)
|
|
}
|
|
|
|
root := doc.Root()
|
|
if root == nil {
|
|
return fmt.Errorf("invalid_domain_xml: root_missing")
|
|
}
|
|
|
|
clockEl := doc.FindElement("//clock")
|
|
if clockEl == nil {
|
|
clockEl = root.CreateElement("clock")
|
|
}
|
|
|
|
attr := clockEl.SelectAttr("offset")
|
|
if attr == nil {
|
|
clockEl.CreateAttr("offset", timeOffset)
|
|
} else {
|
|
attr.Value = timeOffset
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err := s.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("time_offset", timeOffset).Error; err != nil {
|
|
return fmt.Errorf("failed_to_update_time_offset_in_db: %w", err)
|
|
}
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after time offset modification")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ModifySerial(rid uint, enabled bool) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireConnection(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var pre vmModels.VM
|
|
if err := s.DB.Model(&vmModels.VM{}).Where("rid = ?", rid).First(&pre).Error; err != nil {
|
|
return fmt.Errorf("failed_to_fetch_vm_from_db: %w", err)
|
|
}
|
|
|
|
if pre.Serial == enabled {
|
|
return nil
|
|
}
|
|
|
|
domain, err := s.conn().DomainLookupByName(strconv.Itoa(int(rid)))
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_lookup_domain_by_name: %w", err)
|
|
}
|
|
|
|
state, _, err := s.conn().DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_domain_state: %w", err)
|
|
}
|
|
|
|
if state != 5 {
|
|
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
|
|
}
|
|
|
|
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_xml: %w", err)
|
|
}
|
|
|
|
root := doc.Root()
|
|
if root == nil {
|
|
return fmt.Errorf("invalid_domain_xml: root_missing")
|
|
}
|
|
|
|
master := "/dev/nmdm" + strconv.Itoa(int(rid)) + "A"
|
|
|
|
// remove any existing <serial>/<console> for this nmdm pair
|
|
devicesEl := doc.FindElement("//devices")
|
|
if devicesEl != nil {
|
|
children := append([]*etree.Element{}, devicesEl.ChildElements()...)
|
|
for _, el := range children {
|
|
if el.Tag != "serial" && el.Tag != "console" {
|
|
continue
|
|
}
|
|
if src := el.FindElement("source"); src != nil {
|
|
if a := src.SelectAttr("master"); a != nil && a.Value == master {
|
|
devicesEl.RemoveChild(el)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if enabled {
|
|
if devicesEl == nil {
|
|
devicesEl = etree.NewElement("devices")
|
|
root.AddChild(devicesEl)
|
|
}
|
|
serialEl := etree.NewElement("serial")
|
|
serialEl.CreateAttr("type", "nmdm")
|
|
|
|
sourceEl := etree.NewElement("source")
|
|
sourceEl.CreateAttr("master", master)
|
|
sourceEl.CreateAttr("slave", "/dev/nmdm"+strconv.Itoa(int(rid))+"B")
|
|
serialEl.AddChild(sourceEl)
|
|
|
|
devicesEl.AddChild(serialEl)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err := s.DB.Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("serial", enabled).Error; err != nil {
|
|
return fmt.Errorf("failed_to_update_serial_in_db: %w", err)
|
|
}
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after serial modification")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ModifyShutdownWaitTime(rid uint, waitTime int) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
|
|
err := s.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("shutdown_wait_time", waitTime).Error
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after shutdown wait time modification")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ModifyCloudInitData(rid uint, data string, metadata string, networkConfig string) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
|
|
if data == "" && metadata != "" || data != "" && metadata == "" {
|
|
return fmt.Errorf("both_data_and_metadata_must_be_provided")
|
|
}
|
|
|
|
if data != "" && metadata != "" {
|
|
if utils.IsValidYAML(data) == false || utils.IsValidYAML(metadata) == false {
|
|
return fmt.Errorf("invalid_yaml_in_cloud_init_data_or_metadata")
|
|
}
|
|
}
|
|
|
|
if networkConfig != "" {
|
|
if utils.IsValidYAML(networkConfig) == false {
|
|
return fmt.Errorf("invalid_yaml_in_cloud_init_network_config")
|
|
}
|
|
}
|
|
|
|
err := s.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Updates(map[string]interface{}{
|
|
"cloud_init_data": data,
|
|
"cloud_init_meta_data": metadata,
|
|
"cloud_init_network_config": networkConfig,
|
|
}).Error
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_update_cloud_init_data_in_db: %w", err)
|
|
}
|
|
|
|
return s.SyncVMDisks(rid)
|
|
}
|
|
|
|
func (s *Service) ModifyIgnoreUMSRs(rid uint, ignore bool) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireConnection(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var vm vmModels.VM
|
|
if err := s.DB.Where("rid = ?", rid).First(&vm).Error; err != nil {
|
|
return fmt.Errorf("failed_to_fetch_vm_from_db: %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)
|
|
}
|
|
|
|
state, _, err := s.conn().DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_domain_state: %w", err)
|
|
}
|
|
|
|
if state != 5 {
|
|
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
|
|
}
|
|
|
|
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_xml: %w", err)
|
|
}
|
|
|
|
root := doc.Root()
|
|
if root == nil {
|
|
return fmt.Errorf("invalid_domain_xml: root_missing")
|
|
}
|
|
|
|
bhyveCmdEl := doc.FindElement("//bhyve:commandline")
|
|
if bhyveCmdEl == nil {
|
|
bhyveCmdEl = root.CreateElement("bhyve:commandline")
|
|
}
|
|
|
|
for {
|
|
found := false
|
|
children := bhyveCmdEl.ChildElements()
|
|
for _, el := range children {
|
|
if el.Tag == "bhyve:arg" || el.Tag == "arg" {
|
|
if a := el.SelectAttr("value"); a != nil && a.Value == "-w" {
|
|
bhyveCmdEl.RemoveChild(el)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
break
|
|
}
|
|
}
|
|
|
|
if ignore {
|
|
argEl := etree.NewElement("bhyve:arg")
|
|
argEl.CreateAttr("value", "-w")
|
|
bhyveCmdEl.AddChild(argEl)
|
|
}
|
|
|
|
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.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("ignore_umsr", ignore).Error
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after ignore MSR modification")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ModifyQemuGuestAgent(rid uint, enabled bool) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireConnection(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var vm vmModels.VM
|
|
if err := s.DB.Where("rid = ?", rid).First(&vm).Error; err != nil {
|
|
return fmt.Errorf("failed_to_fetch_vm_from_db: %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)
|
|
}
|
|
|
|
state, _, err := s.conn().DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_domain_state: %w", err)
|
|
}
|
|
|
|
if state != 5 {
|
|
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
|
|
}
|
|
|
|
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_xml: %w", err)
|
|
}
|
|
|
|
root := doc.Root()
|
|
if root == nil {
|
|
return fmt.Errorf("invalid_domain_xml: root_missing")
|
|
}
|
|
|
|
bhyveCmdEl := doc.FindElement("//bhyve:commandline")
|
|
if bhyveCmdEl == nil {
|
|
bhyveCmdEl = root.CreateElement("bhyve:commandline")
|
|
}
|
|
|
|
for {
|
|
found := false
|
|
children := bhyveCmdEl.ChildElements()
|
|
for _, el := range children {
|
|
if el.Tag == "bhyve:arg" || el.Tag == "arg" {
|
|
if a := el.SelectAttr("value"); a != nil && strings.Contains(a.Value, "org.qemu.guest_agent.0=") {
|
|
bhyveCmdEl.RemoveChild(el)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
break
|
|
}
|
|
}
|
|
|
|
if enabled {
|
|
dataPath, err := s.GetVMConfigDirectory(vm.RID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_vm_data_path: %w", err)
|
|
}
|
|
|
|
used := parseUsedIndicesFromElement(bhyveCmdEl)
|
|
index := 10
|
|
for index < 30 && used[index] {
|
|
index++
|
|
}
|
|
if index >= 30 {
|
|
return fmt.Errorf("no_free_indices_available_for_qemu_guest_agent")
|
|
}
|
|
|
|
qgaArg := fmt.Sprintf("-s %d:0,virtio-console,org.qemu.guest_agent.0=%s",
|
|
index,
|
|
filepath.Join(dataPath, "qga.sock"),
|
|
)
|
|
|
|
argEl := etree.NewElement("bhyve:arg")
|
|
argEl.CreateAttr("value", qgaArg)
|
|
bhyveCmdEl.AddChild(argEl)
|
|
}
|
|
|
|
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.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("qemu_guest_agent", enabled).Error
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_update_qemu_guest_agent_in_db: %w", err)
|
|
}
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after QEMU guest agent modification")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ModifyTPMEmulation(rid uint, enabled bool) error {
|
|
if err := s.requireVMMutationOwnership(rid); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireConnection(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var vm vmModels.VM
|
|
if err := s.DB.Where("rid = ?", rid).First(&vm).Error; err != nil {
|
|
return fmt.Errorf("failed_to_fetch_vm_from_db: %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)
|
|
}
|
|
|
|
state, _, err := s.conn().DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_domain_state: %w", err)
|
|
}
|
|
|
|
if state != 5 {
|
|
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
|
|
}
|
|
|
|
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_xml: %w", err)
|
|
}
|
|
|
|
root := doc.Root()
|
|
if root == nil {
|
|
return fmt.Errorf("invalid_domain_xml: root_missing")
|
|
}
|
|
|
|
bhyveCmdEl := doc.FindElement("//bhyve:commandline")
|
|
if bhyveCmdEl == nil {
|
|
bhyveCmdEl = root.CreateElement("bhyve:commandline")
|
|
}
|
|
|
|
for {
|
|
found := false
|
|
children := bhyveCmdEl.ChildElements()
|
|
for _, el := range children {
|
|
if el.Tag == "bhyve:arg" || el.Tag == "arg" {
|
|
if a := el.SelectAttr("value"); a != nil && len(a.Value) >= 5 && a.Value[:5] == "-ltpm" {
|
|
bhyveCmdEl.RemoveChild(el)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
break
|
|
}
|
|
}
|
|
|
|
if enabled {
|
|
dataPath, err := s.GetVMConfigDirectory(vm.RID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_get_vm_data_path: %w", err)
|
|
}
|
|
|
|
tpmArg := fmt.Sprintf("-ltpm,swtpm,%s", filepath.Join(dataPath, fmt.Sprintf("%d_tpm.socket", vm.RID)))
|
|
|
|
argEl := etree.NewElement("bhyve:arg")
|
|
argEl.CreateAttr("value", tpmArg)
|
|
bhyveCmdEl.AddChild(argEl)
|
|
} else {
|
|
err := s.StopTPM(vm.RID)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "tpm_socket_not_found") {
|
|
logger.L.Err(err).Msg("Failed to stop TPM")
|
|
}
|
|
}
|
|
}
|
|
|
|
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.DB.
|
|
Model(&vmModels.VM{}).
|
|
Where("rid = ?", rid).
|
|
Update("tpm_emulation", enabled).Error
|
|
if err != nil {
|
|
return fmt.Errorf("failed_to_update_tpm_emulation_in_db: %w", err)
|
|
}
|
|
|
|
err = s.WriteVMJson(rid)
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msg("Failed to write VM JSON after TPM emulation modification")
|
|
}
|
|
|
|
return nil
|
|
}
|