Files
Sylve/internal/services/libvirt/libvirt_storage.go
T
2026-03-25 02:54:15 +05:30

1048 lines
27 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 (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/alchemillahq/gzfs"
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) CreateVMDisk(rid uint, storage vmModels.Storage, ctx context.Context) error {
usable, err := s.System.GetUsablePools(ctx)
if err != nil {
return fmt.Errorf("failed_to_get_usable_pools: %w", err)
}
var target *gzfs.ZPool
for _, pool := range usable {
if pool.Name == storage.Pool {
target = pool
break
}
}
if target == nil {
return fmt.Errorf("pool_not_found: %s", storage.Pool)
}
var datasets []*gzfs.Dataset
if storage.Type == vmModels.VMStorageTypeRaw || storage.Type == vmModels.VMStorageTypeZVol {
switch storage.Type {
case vmModels.VMStorageTypeRaw:
datasets, err = s.GZFS.ZFS.ListByType(
ctx,
gzfs.DatasetTypeFilesystem,
false,
fmt.Sprintf("%s/sylve/virtual-machines/%d/raw-%d", target.Name, rid, storage.ID),
)
case vmModels.VMStorageTypeZVol:
datasets, err = s.GZFS.ZFS.ListByType(
ctx,
gzfs.DatasetTypeVolume,
false,
fmt.Sprintf("%s/sylve/virtual-machines/%d/zvol-%d", target.Name, rid, storage.ID),
)
}
if err != nil {
if !strings.Contains(err.Error(), "dataset does not exist") {
return fmt.Errorf("failed_to_get_datasets: %w", err)
}
}
}
// var dataset *zfs.Dataset
var dataset *gzfs.Dataset
if len(datasets) == 0 {
if target.Free < uint64(storage.Size) {
return fmt.Errorf("insufficient_space_in_pool: %s", storage.Pool)
}
var recordSize string
if storage.RecordSize != 0 {
recordSize = strconv.Itoa(storage.RecordSize)
} else {
recordSize = "1M"
}
var volblocksize string
if storage.VolBlockSize != 0 {
volblocksize = strconv.Itoa(storage.VolBlockSize)
} else {
volblocksize = "16K"
}
props := map[string]string{
"compression": "zstd",
"logbias": "throughput",
"primarycache": "metadata",
"secondarycache": "all",
}
switch storage.Type {
case vmModels.VMStorageTypeRaw:
props["atime"] = "off"
dataset, err = s.GZFS.ZFS.CreateFilesystem(
ctx,
fmt.Sprintf("%s/sylve/virtual-machines/%d/raw-%d", target.Name, rid, storage.ID),
utils.MergeMaps(props, map[string]string{
"recordsize": recordSize,
}),
)
case vmModels.VMStorageTypeZVol:
dataset, err = s.GZFS.ZFS.CreateVolume(
ctx,
fmt.Sprintf("%s/sylve/virtual-machines/%d/zvol-%d", target.Name, rid, storage.ID),
uint64(storage.Size),
utils.MergeMaps(props, map[string]string{
"volblocksize": volblocksize,
"volmode": "dev",
}),
)
}
if err != nil {
return fmt.Errorf("failed_to_create_dataset: %w", err)
}
} else {
dataset = datasets[0]
}
if storage.Type == vmModels.VMStorageTypeRaw {
imagePath := filepath.Join(dataset.Mountpoint, fmt.Sprintf("%d.img", storage.ID))
if _, err := os.Stat(imagePath); err == nil {
logger.L.Info().Msgf("Disk image %s already exists, skipping creation", imagePath)
} else {
if err := utils.CreateOrTruncateFile(imagePath, storage.Size); err != nil {
_ = dataset.Destroy(ctx, true, false)
return fmt.Errorf("failed_to_create_or_truncate_image_file: %w", err)
}
}
}
if storage.DatasetID != nil && *storage.DatasetID > 0 {
var existingDataset vmModels.VMStorageDataset
if err := s.DB.First(&existingDataset, "id = ?", *storage.DatasetID).Error; err == nil {
if strings.TrimSpace(existingDataset.Name) == strings.TrimSpace(dataset.Name) {
return nil
}
}
}
storageDataset := vmModels.VMStorageDataset{
Pool: target.Name,
Name: dataset.Name,
GUID: dataset.GUID,
}
if err := s.DB.Create(&storageDataset).Error; err != nil {
_ = dataset.Destroy(ctx, true, false)
return fmt.Errorf("failed_to_create_storage_dataset_record: %w", err)
}
storage.DatasetID = &storageDataset.ID
if err := s.DB.Save(&storage).Error; err != nil {
_ = dataset.Destroy(ctx, true, false)
return fmt.Errorf("failed_to_update_storage_with_dataset_id: %w", err)
}
return nil
}
func (s *Service) SyncVMDisks(rid uint) error {
if err := s.requireConnection(); err != nil {
return err
}
off, err := s.IsDomainShutOff(rid)
if err != nil {
return fmt.Errorf("failed_to_check_vm_shutoff: %w", err)
}
if !off {
return fmt.Errorf("domain_state_not_shutoff: %d", rid)
}
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_xml: %w", err)
}
bhyveCommandline := doc.FindElement("//commandline")
if bhyveCommandline == nil || bhyveCommandline.Space != "bhyve" {
root := doc.Root()
if root.SelectAttr("xmlns:bhyve") == nil {
root.CreateAttr("xmlns:bhyve", "http://libvirt.org/schemas/domain/bhyve/1.0")
}
bhyveCommandline = root.CreateElement("bhyve:commandline")
}
for _, arg := range bhyveCommandline.ChildElements() {
valAttr := arg.SelectAttr("value")
if valAttr == nil {
continue
}
val := valAttr.Value
if val == "" {
continue
}
emulations := []string{
string(libvirtServiceInterfaces.AHCICDStorageEmulation),
string(libvirtServiceInterfaces.AHCIHDStorageEmulation),
string(libvirtServiceInterfaces.NVMEStorageEmulation),
string(libvirtServiceInterfaces.VirtIOStorageEmulation),
}
if utils.PartialStringInSlice(val, emulations) {
bhyveCommandline.RemoveChild(arg)
}
}
vm, err := s.GetVMByRID(rid)
if err != nil {
return fmt.Errorf("failed_to_get_vm_by_id: %w", err)
}
var storages []vmModels.Storage
if err := s.DB.
Where("vm_id = ?", vm.ID).
Order("boot_order ASC").
Find(&storages).Error; err != nil {
return fmt.Errorf("failed_to_get_vm_storages: %w", err)
}
argValues := []string{}
used := parseUsedIndicesFromElement(bhyveCommandline)
currentIndex := 10
for _, storage := range storages {
if !storage.Enable {
continue
}
for currentIndex < 30 && used[currentIndex] {
currentIndex++
}
if currentIndex >= 30 {
return fmt.Errorf("no free indices available")
}
index := currentIndex
used[index] = true
currentIndex++
argCommon := fmt.Sprintf("-s %d:0,%s", index, storage.Emulation)
var argValue string
var diskValue string
if storage.Type == vmModels.VMStorageTypeRaw {
diskValue = fmt.Sprintf("/%s/sylve/virtual-machines/%d/raw-%d/%d.img",
storage.Pool,
rid,
storage.ID,
storage.ID,
)
} else if storage.Type == vmModels.VMStorageTypeZVol {
diskValue = fmt.Sprintf("/dev/zvol/%s/sylve/virtual-machines/%d/zvol-%d",
storage.Pool,
rid,
storage.ID,
)
} else if storage.Type == vmModels.VMStorageTypeDiskImage {
diskValue, err = s.FindISOByUUID(storage.DownloadUUID, true)
if err != nil {
return fmt.Errorf("failed_to_get_iso_path_by_uuid: %w", err)
}
diskValue = fmt.Sprintf("%s,ro", diskValue)
}
argValue = fmt.Sprintf("%s,%s", argCommon, diskValue)
argValues = append(argValues, argValue)
}
err = s.CreateCloudInitISO(vm)
if err != nil {
logger.L.Error().Err(err).Msg("vm: sync_vm_disks: failed_to_create_cloud_init_iso")
}
if vm.CloudInitData != "" {
cloudInitISOPath, err := s.GetCloudInitISOPath(vm.RID)
if err != nil {
logger.L.Warn().Err(err).Msg("vm: sync_vm_disks: failed_to_get_cloud_init_iso_path")
} else if cloudInitISOPath != "" {
for currentIndex < 30 && used[currentIndex] {
currentIndex++
}
if currentIndex >= 30 {
return fmt.Errorf("no_free_indices_available_for_cloud_init_iso")
}
used[currentIndex] = true
argValue := fmt.Sprintf("-s %d:0,ahci-cd,%s,ro", currentIndex, cloudInitISOPath)
argValues = append(argValues, argValue)
}
}
for _, val := range argValues {
argElement := bhyveCommandline.CreateElement("bhyve:arg")
argElement.CreateAttr("value", val)
}
newXML, 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(newXML); 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 disk sync")
}
return nil
}
func (s *Service) RemoveStorageXML(rid uint, storage vmModels.Storage) 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_xml: %w", err)
}
bhyveCommandline := doc.FindElement("//commandline")
if bhyveCommandline == nil || bhyveCommandline.Space != "bhyve" {
root := doc.Root()
if root.SelectAttr("xmlns:bhyve") == nil {
root.CreateAttr("xmlns:bhyve", "http://libvirt.org/schemas/domain/bhyve/1.0")
}
bhyveCommandline = root.CreateElement("bhyve:commandline")
}
var filePath string
if storage.Type == vmModels.VMStorageTypeDiskImage &&
storage.DownloadUUID != "" {
filePath, err = s.FindISOByUUID(storage.DownloadUUID, true)
if err != nil {
return fmt.Errorf("failed_to_find_iso_by_uuid: %w", err)
}
} else if storage.Type == vmModels.VMStorageTypeRaw {
filePath = fmt.Sprintf("%s/sylve/virtual-machines/%d/raw-%d/%d.img",
storage.Pool,
rid,
storage.ID,
storage.ID,
)
} else if storage.Type == vmModels.VMStorageTypeZVol {
filePath = fmt.Sprintf("%s/sylve/virtual-machines/%d/zvol-%d",
storage.Pool,
rid,
storage.ID,
)
}
if filePath == "" {
return fmt.Errorf("unable_to_determine_storage_path")
}
for _, arg := range bhyveCommandline.ChildElements() {
valAttr := arg.SelectAttr("value")
if valAttr == nil {
continue
}
val := valAttr.Value
if val == "" {
continue
}
if (storage.Type == vmModels.VMStorageTypeDiskImage ||
storage.Type == vmModels.VMStorageTypeRaw ||
storage.Type == vmModels.VMStorageTypeZVol) &&
strings.Contains(val, filePath) {
bhyveCommandline.RemoveChild(arg)
}
}
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)
}
return nil
}
func (s *Service) StorageDetach(req libvirtServiceInterfaces.StorageDetachRequest) error {
if err := s.requireVMMutationOwnership(req.RID); err != nil {
return err
}
off, err := s.IsDomainShutOff(req.RID)
if err != nil {
return fmt.Errorf("failed_to_check_vm_shutoff: %w", err)
}
if !off {
return fmt.Errorf("domain_state_not_shutoff: %d", req.RID)
}
vm, err := s.GetVMByRID(req.RID)
if err != nil {
return fmt.Errorf("failed_to_get_vm_by_id: %w", err)
}
var storage vmModels.Storage
if err := s.DB.
Preload("Dataset").
First(&storage, "id = ? AND vm_id = ?", req.StorageId, vm.ID).
Error; err != nil {
return fmt.Errorf("failed_to_find_storage_record: %w", err)
}
if err := s.RemoveStorageXML(req.RID, storage); err != nil {
logger.L.Error().Err(err).Msg("vm: storage_detach: failed_to_remove_storage_xml")
}
if err := s.DB.Delete(&storage).Error; err != nil {
return fmt.Errorf("failed_to_delete_storage_record: %w", err)
}
if storage.DatasetID != nil {
var dataset vmModels.VMStorageDataset
if err := s.DB.First(&dataset, "id = ?", *storage.DatasetID).Error; err != nil {
return fmt.Errorf("failed_to_find_storage_dataset_record: %w", err)
}
if err := s.DB.Delete(&dataset).Error; err != nil {
return fmt.Errorf("failed_to_delete_storage_dataset_record: %w", err)
}
}
return nil
}
func (s *Service) GetNextBootOrderIndex(vmId int) (int, error) {
var maxBootOrder sql.NullInt64
err := s.DB.
Model(&vmModels.Storage{}).
Where("vm_id = ?", vmId).
Select("MAX(boot_order)").
Scan(&maxBootOrder).Error
if err != nil {
return 0, fmt.Errorf("failed_to_get_max_boot_order: %w", err)
}
if maxBootOrder.Valid {
return int(maxBootOrder.Int64) + 1, nil
}
return 0, nil
}
func (s *Service) ValidateBootOrderIndex(vmId int, bootOrder int) (bool, error) {
var count int64
err := s.DB.
Model(&vmModels.Storage{}).
Where("vm_id = ? AND boot_order = ?", vmId, bootOrder).
Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed_to_validate_boot_order_index: %w", err)
}
return count == 0, nil
}
func (s *Service) StorageImport(req libvirtServiceInterfaces.StorageAttachRequest, vm vmModels.VM, ctx context.Context) error {
var storage vmModels.Storage
storage.Name = req.Name
storage.VMID = vm.ID
if req.Pool == nil || strings.TrimSpace(*req.Pool) == "" {
storage.Pool = ""
} else {
storage.Pool = *req.Pool
}
if storage.Pool == "" &&
(req.StorageType == libvirtServiceInterfaces.StorageTypeRaw ||
req.StorageType == libvirtServiceInterfaces.StorageTypeZVOL) {
{
return fmt.Errorf("invalid_pool")
}
}
storage.Emulation = vmModels.VMStorageEmulationType(req.Emulation)
storage.BootOrder = *req.BootOrder
storage.Enable = true
if req.StorageType == libvirtServiceInterfaces.StorageTypeRaw {
exists, err := utils.FileExists(req.RawPath)
if err != nil {
return fmt.Errorf("failed_to_check_raw_path_exists: %w", err)
}
if !exists {
return fmt.Errorf("raw_path_does_not_exist: %s", req.RawPath)
}
info, err := os.Stat(req.RawPath)
if err != nil {
return fmt.Errorf("failed_to_stat_raw_path: %w", err)
}
storage.Type = vmModels.VMStorageTypeRaw
storage.Size = info.Size()
if err := s.DB.Create(&storage).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_record: %w", err)
}
if err := s.CreateVMDisk(vm.RID, storage, ctx); err != nil {
return fmt.Errorf("failed_to_create_vm_disk: %w", err)
}
datasetPath := fmt.Sprintf("/%s/sylve/virtual-machines/%d/raw-%d/%d.img",
storage.Pool,
vm.RID,
storage.ID,
storage.ID,
)
err = os.Remove(datasetPath)
if err != nil {
logger.L.Warn().Err(err).Msg("failed_to_remove_created_image_file")
}
if err := utils.CopyFile(req.RawPath, datasetPath); err != nil {
return fmt.Errorf("failed_to_copy_raw_file_to_dataset: %w", err)
}
} else if req.StorageType == libvirtServiceInterfaces.StorageTypeZVOL {
datasets, err := s.GZFS.ZFS.ListByType(
ctx,
gzfs.DatasetTypeVolume,
true,
*req.Pool,
)
if err != nil {
return fmt.Errorf("failed_to_get_zvols_in_pool: %w", err)
}
var found *gzfs.Dataset
for _, ds := range datasets {
if ds.GUID == req.Dataset {
found = ds
break
}
}
if found == nil {
return fmt.Errorf("zvol_dataset_not_found_in_pool: %s", req.Dataset)
}
var sourcePool string
parts := strings.SplitN(found.Name, "/", 2)
if len(parts) > 0 {
sourcePool = parts[0]
}
var volSize int64
volSizeProp, ok := found.Properties["volsize"]
if ok {
volSize, err = strconv.ParseInt(volSizeProp.Value, 10, 64)
if err != nil {
return fmt.Errorf("failed_to_parse_volsize: %w", err)
}
} else {
return fmt.Errorf("volsize_property_not_found_in_zvol_dataset")
}
if volSize <= 0 {
return fmt.Errorf("invalid_volsize: %d", volSize)
}
storage.Size = volSize
storage.Type = vmModels.VMStorageTypeZVol
if err := s.DB.Create(&storage).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_record: %w", err)
}
if sourcePool == *req.Pool {
targetDatasetPath := fmt.Sprintf("%s/sylve/virtual-machines/%d/zvol-%d",
*req.Pool,
vm.RID,
storage.ID,
)
dataset, err := found.Rename(ctx, targetDatasetPath, false)
if err != nil || dataset == nil {
_ = s.DB.Delete(&storage).Error
return fmt.Errorf("failed_to_rename_zvol_dataset: %w", err)
}
storageDataset := vmModels.VMStorageDataset{
Pool: *req.Pool,
Name: dataset.Name,
GUID: dataset.GUID,
}
if err := s.DB.Create(&storageDataset).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_dataset_record: %w", err)
}
storage.DatasetID = &storageDataset.ID
if err := s.DB.Save(&storage).Error; err != nil {
return fmt.Errorf("failed_to_update_storage_with_dataset_id: %w", err)
}
} else {
if err := s.CreateVMDisk(vm.RID, storage, ctx); err != nil {
return fmt.Errorf("failed_to_create_vm_disk: %w", err)
}
snapshotName := fmt.Sprintf("import-snap-%d-%d", vm.RID, storage.ID)
snapshot, err := found.Snapshot(ctx, snapshotName, false)
if err != nil {
return fmt.Errorf("failed_to_create_snapshot_of_imported_zvol: %w", err)
}
targetDatasetPath := fmt.Sprintf("%s/sylve/virtual-machines/%d/zvol-%d",
*req.Pool,
vm.RID,
storage.ID,
)
targetDatasets, err := s.GZFS.ZFS.ListByType(
ctx,
gzfs.DatasetTypeVolume,
false,
targetDatasetPath,
)
if err != nil {
return fmt.Errorf("failed_to_get_target_zvols: %w", err)
}
if len(targetDatasets) == 0 {
return fmt.Errorf("target_zvol_dataset_not_found: %s", targetDatasetPath)
}
targetDataset := targetDatasets[0]
_, err = snapshot.SendToDataset(ctx, targetDataset.Name, true)
if err != nil {
return fmt.Errorf("failed_to_send_snapshot_to_dataset: %w", err)
}
if err := snapshot.Destroy(ctx, true, false); err != nil {
logger.L.Warn().Err(err).Msg("failed_to_destroy_import_snapshot")
}
}
} else if req.StorageType == libvirtServiceInterfaces.StorageTypeDiskImage {
imagePath, err := s.FindISOByUUID(req.UUID, true)
if err != nil {
return fmt.Errorf("failed_to_find_iso_by_uuid: %w", err)
}
info, err := os.Stat(imagePath)
if err != nil {
return fmt.Errorf("failed_to_stat_iso_path: %w", err)
}
storage.Type = vmModels.VMStorageTypeDiskImage
storage.Size = info.Size()
storage.DownloadUUID = req.UUID
if err := s.DB.Create(&storage).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_record: %w", err)
}
}
return s.SyncVMDisks(vm.RID)
}
func (s *Service) StorageNew(req libvirtServiceInterfaces.StorageAttachRequest, vm vmModels.VM, ctx context.Context) error {
var storage vmModels.Storage
storage.Name = req.Name
storage.VMID = vm.ID
if req.Pool == nil || strings.TrimSpace(*req.Pool) == "" {
storage.Pool = ""
} else {
storage.Pool = *req.Pool
}
storage.Emulation = vmModels.VMStorageEmulationType(req.Emulation)
storage.Size = *req.Size
storage.BootOrder = *req.BootOrder
storage.Enable = true
if req.StorageType == libvirtServiceInterfaces.StorageTypeRaw {
storage.Type = vmModels.VMStorageTypeRaw
if err := s.DB.Create(&storage).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_record: %w", err)
}
if err := s.CreateVMDisk(vm.RID, storage, ctx); err != nil {
return fmt.Errorf("failed_to_create_vm_disk: %w", err)
}
diskPath := fmt.Sprintf("/%s/sylve/virtual-machines/%d/raw-%d/%d.img",
storage.Pool,
vm.RID,
storage.ID,
storage.ID,
)
exists, err := utils.FileExists(diskPath)
if err != nil {
return fmt.Errorf("failed_to_check_created_disk_path_exists: %w", err)
}
if !exists {
return fmt.Errorf("created_disk_path_does_not_exist_after_creation: %s", diskPath)
}
} else if req.StorageType == libvirtServiceInterfaces.StorageTypeZVOL {
storage.Type = vmModels.VMStorageTypeZVol
if err := s.DB.Create(&storage).Error; err != nil {
return fmt.Errorf("failed_to_create_storage_record: %w", err)
}
if err := s.CreateVMDisk(vm.RID, storage, ctx); err != nil {
return fmt.Errorf("failed_to_create_vm_disk: %w", err)
}
}
return s.SyncVMDisks(vm.RID)
}
func (s *Service) StorageAttach(req libvirtServiceInterfaces.StorageAttachRequest, ctx context.Context) error {
if err := s.requireVMMutationOwnership(req.RID); err != nil {
return err
}
if req.Name == "" ||
strings.TrimSpace(req.Name) == "" ||
len(req.Name) == 0 ||
len(req.Name) > 128 {
return fmt.Errorf("invalid_storage_name")
}
vm, err := s.GetVMByRID(req.RID)
if err != nil {
return fmt.Errorf("failed_to_get_vm_by_id: %w", err)
}
off, err := s.IsDomainShutOff(req.RID)
if err != nil {
return fmt.Errorf("failed_to_check_vm_shutoff: %w", err)
}
if !off {
return fmt.Errorf("domain_state_not_shutoff: %d", req.RID)
}
var bootOrder int
if req.BootOrder != nil {
bootOrder = *req.BootOrder
} else {
nextIndex, err := s.GetNextBootOrderIndex(int(vm.ID))
if err != nil {
return fmt.Errorf("failed_to_get_next_boot_order_index: %w", err)
}
bootOrder = nextIndex
}
valid, err := s.ValidateBootOrderIndex(int(vm.ID), bootOrder)
if err != nil {
return fmt.Errorf("failed_to_validate_boot_order_index: %w", err)
}
if !valid {
return fmt.Errorf("boot_order_index_already_in_use: %d", bootOrder)
}
if (req.Pool == nil || strings.TrimSpace(*req.Pool) == "") &&
(req.StorageType == libvirtServiceInterfaces.StorageTypeRaw ||
req.StorageType == libvirtServiceInterfaces.StorageTypeZVOL) {
return fmt.Errorf("invalid_pool")
}
if req.StorageType != libvirtServiceInterfaces.StorageTypeDiskImage {
err = s.CreateStorageParent(vm.RID, *req.Pool, ctx)
if err != nil {
return fmt.Errorf("failed_to_create_storage_parent: %w", err)
}
}
req.BootOrder = &bootOrder
switch req.AttachType {
case libvirtServiceInterfaces.StorageAttachTypeImport:
return s.StorageImport(req, vm, ctx)
case libvirtServiceInterfaces.StorageAttachTypeNew:
return s.StorageNew(req, vm, ctx)
}
return fmt.Errorf("invalid_storage_attach_type: %s", req.AttachType)
}
func (s *Service) StorageUpdate(req libvirtServiceInterfaces.StorageUpdateRequest, ctx context.Context) error {
if strings.TrimSpace(req.Name) == "" || len(req.Name) > 128 {
return fmt.Errorf("invalid_storage_name")
}
var current vmModels.Storage
if err := s.DB.
Preload("Dataset").
First(&current, "id = ?", req.ID).Error; err != nil {
return fmt.Errorf("failed_to_find_storage_record: %w", err)
}
var vm vmModels.VM
if err := s.DB.First(&vm, "id = ?", current.VMID).Error; err != nil {
return fmt.Errorf("failed_to_find_vm_record: %w", err)
}
if err := s.requireVMMutationOwnership(vm.RID); err != nil {
return err
}
off, err := s.IsDomainShutOff(vm.RID)
if err != nil {
return fmt.Errorf("failed_to_check_vm_shutoff: %w", err)
}
if !off {
return fmt.Errorf("domain_state_not_shutoff: %d", vm.RID)
}
if req.BootOrder != nil && *req.BootOrder != current.BootOrder {
var count int64
if err := s.DB.
Model(&vmModels.Storage{}).
Where("vm_id = ? AND boot_order = ? AND id != ?", current.VMID, *req.BootOrder, current.ID).
Count(&count).Error; err != nil {
return fmt.Errorf("failed_to_validate_boot_order_index: %w", err)
}
if count > 0 {
return fmt.Errorf("boot_order_index_already_in_use: %d", *req.BootOrder)
}
current.BootOrder = *req.BootOrder
}
if req.Size == nil && current.Type != vmModels.VMStorageTypeDiskImage {
return fmt.Errorf("size_required_for_storage_type: %s", current.Type)
}
if req.Size != nil && *req.Size != current.Size {
newSize := *req.Size
if newSize < current.Size {
return fmt.Errorf("shrinking_storage_not_supported")
}
growBy := newSize - current.Size
if current.Pool != "" && growBy > 0 {
pool, err := s.GZFS.Zpool.Get(ctx, current.Pool)
if err != nil || pool == nil {
return err
}
if pool.Free < uint64(growBy) {
return fmt.Errorf("insufficient_space_in_pool: %s", current.Pool)
}
}
switch current.Type {
case vmModels.VMStorageTypeRaw:
imagePath := fmt.Sprintf("/%s/sylve/virtual-machines/%d/raw-%d/%d.img",
current.Pool,
vm.RID,
current.ID,
current.ID,
)
if err := utils.CreateOrResizeFile(imagePath, newSize); err != nil {
return fmt.Errorf("failed_to_resize_raw_image_file: %w", err)
}
case vmModels.VMStorageTypeZVol:
dsList, err := s.GZFS.ZFS.ListByType(ctx, gzfs.DatasetTypeVolume, false, current.Dataset.Name)
if err != nil {
return fmt.Errorf("failed_to_get_zvol_dataset: %w", err)
}
if len(dsList) == 0 {
return fmt.Errorf("zvol_dataset_not_found: %s", current.Dataset.Name)
}
ds := dsList[0]
var volSize uint64
volSizeProp, ok := ds.Properties["volsize"]
if ok {
volSize = gzfs.ParseSize(volSizeProp.Value)
}
volSize = gzfs.ParseSize(volSizeProp.Value)
newVolSize := uint64(newSize)
if newVolSize < volSize {
return fmt.Errorf("new_size_must_be_greater_than_or_equal_to_current_volsize")
}
err = ds.SetProperties(ctx, "volsize", fmt.Sprintf("%d", newVolSize))
if err != nil {
return fmt.Errorf("failed_to_set_zvol_volsize: %w", err)
}
case vmModels.VMStorageTypeDiskImage:
return fmt.Errorf("size_edit_not_supported_for_disk_image_storage")
default:
return fmt.Errorf("size_edit_not_supported_for_storage_type: %s", current.Type)
}
current.Size = newSize
}
current.Name = req.Name
current.Emulation = vmModels.VMStorageEmulationType(req.Emulation)
if req.Enable != nil {
current.Enable = *req.Enable
}
if err := s.DB.Save(&current).Error; err != nil {
return fmt.Errorf("failed_to_update_storage_record: %w", err)
}
if err := s.SyncVMDisks(vm.RID); err != nil {
return fmt.Errorf("failed_to_sync_vm_disks: %w", err)
}
return nil
}
func (s *Service) CreateStorageParent(rid uint, poolName string, ctx context.Context) error {
pools, err := s.System.GetUsablePools(ctx)
if err != nil {
return fmt.Errorf("failed_to_get_usable_pools: %w", err)
}
var created []*gzfs.Dataset
for _, pool := range pools {
if poolName != "" && pool.Name != poolName {
continue
}
target := fmt.Sprintf("%s/sylve/virtual-machines/%d", pool.Name, rid)
datasets, _ := s.GZFS.ZFS.ListByType(
ctx,
gzfs.DatasetTypeFilesystem,
false,
target,
)
if len(datasets) > 0 {
continue
}
props := map[string]string{
"compression": "zstd",
"logbias": "throughput",
"primarycache": "metadata",
"secondarycache": "all",
}
ds, err := s.GZFS.ZFS.CreateFilesystem(ctx, target, props)
if err != nil {
for _, createdDS := range created {
_ = createdDS.Destroy(ctx, true, false)
}
return fmt.Errorf("failed_to_create_%s: %w", target, err)
}
created = append(created, ds)
}
return nil
}