package vms import ( "fmt" "net/url" "strings" "unicode" "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) // CustomStorageDevice handles QEMU SATA device parameters. type CustomStorageDevice struct { AIO *string `json:"aio,omitempty" url:"aio,omitempty"` BackupEnabled *types.CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"` BurstableReadSpeedMbps *int `json:"mbps_rd_max,omitempty" url:"mbps_rd_max,omitempty"` Cache *string `json:"cache,omitempty" url:"cache,omitempty"` BurstableWriteSpeedMbps *int `json:"mbps_wr_max,omitempty" url:"mbps_wr_max,omitempty"` Discard *string `json:"discard,omitempty" url:"discard,omitempty"` Enabled bool `json:"-" url:"-"` FileVolume string `json:"file" url:"file"` Format *string `json:"format,omitempty" url:"format,omitempty"` IOThread *types.CustomBool `json:"iothread,omitempty" url:"iothread,omitempty,int"` SSD *types.CustomBool `json:"ssd,omitempty" url:"ssd,omitempty,int"` MaxReadSpeedMbps *int `json:"mbps_rd,omitempty" url:"mbps_rd,omitempty"` MaxWriteSpeedMbps *int `json:"mbps_wr,omitempty" url:"mbps_wr,omitempty"` Media *string `json:"media,omitempty" url:"media,omitempty"` Size *types.DiskSize `json:"size,omitempty" url:"size,omitempty"` Interface *string `json:"-" url:"-"` DatastoreID *string `json:"-" url:"-"` FileID *string `json:"-" url:"-"` } // PathInDatastore returns path part of FileVolume or nil if it is not yet allocated. func (d CustomStorageDevice) PathInDatastore() *string { probablyDatastoreID, pathInDatastore, hasDatastoreID := strings.Cut(d.FileVolume, ":") if !hasDatastoreID { // when no ':' separator is found, 'Cut' places the whole string to 'probablyDatastoreID', // we want it in 'pathInDatastore' (as it is absolute filesystem path) pathInDatastore = probablyDatastoreID return &pathInDatastore } pathInDatastoreWithoutDigits := strings.Map( func(c rune) rune { if c < '0' || c > '9' { return -1 } return c }, pathInDatastore) if pathInDatastoreWithoutDigits == "" { // FileVolume is not yet allocated, it is in the "STORAGE_ID:SIZE_IN_GiB" format return nil } return &pathInDatastore } // IsOwnedBy returns true, if CustomStorageDevice is owned by given VM. // Not yet allocated volumes are not owned by any VM. func (d CustomStorageDevice) IsOwnedBy(vmID int) bool { pathInDatastore := d.PathInDatastore() if pathInDatastore == nil { // not yet allocated volume, consider disk not owned by any VM // NOTE: if needed, create IsOwnedByOtherThan(vmId) instead of changing this return value. return false } // ZFS uses "local-zfs:vm-123-disk-0" if strings.HasPrefix(*pathInDatastore, fmt.Sprintf("vm-%d-", vmID)) { return true } // directory uses "local:123/vm-123-disk-0" if strings.HasPrefix(*pathInDatastore, fmt.Sprintf("%d/vm-%d-", vmID, vmID)) { return true } return false } // IsCloudInitDrive returns true, if CustomStorageDevice is a cloud-init drive. func (d CustomStorageDevice) IsCloudInitDrive(vmID int) bool { return d.Media != nil && *d.Media == "cdrom" && strings.Contains(d.FileVolume, fmt.Sprintf("vm-%d-cloudinit", vmID)) } // StorageInterface returns the storage interface of the CustomStorageDevice, // e.g. "virtio" or "scsi" for "virtio0" or "scsi2". func (d CustomStorageDevice) StorageInterface() string { for i, r := range *d.Interface { if unicode.IsDigit(r) { return (*d.Interface)[:i] } } // panic(fmt.Sprintf("cannot determine storage interface for disk interface '%s'", *d.Interface)) return "" } // EncodeOptions converts a CustomStorageDevice's common options a URL value. func (d CustomStorageDevice) EncodeOptions() string { values := []string{} if d.AIO != nil { values = append(values, fmt.Sprintf("aio=%s", *d.AIO)) } if d.BackupEnabled != nil { if *d.BackupEnabled { values = append(values, "backup=1") } else { values = append(values, "backup=0") } } if d.IOThread != nil { if *d.IOThread { values = append(values, "iothread=1") } else { values = append(values, "iothread=0") } } if d.SSD != nil { if *d.SSD { values = append(values, "ssd=1") } else { values = append(values, "ssd=0") } } if d.Discard != nil && *d.Discard != "" { values = append(values, fmt.Sprintf("discard=%s", *d.Discard)) } if d.Cache != nil && *d.Cache != "" { values = append(values, fmt.Sprintf("cache=%s", *d.Cache)) } if d.BurstableReadSpeedMbps != nil { values = append(values, fmt.Sprintf("mbps_rd_max=%d", *d.BurstableReadSpeedMbps)) } if d.BurstableWriteSpeedMbps != nil { values = append(values, fmt.Sprintf("mbps_wr_max=%d", *d.BurstableWriteSpeedMbps)) } if d.MaxReadSpeedMbps != nil { values = append(values, fmt.Sprintf("mbps_rd=%d", *d.MaxReadSpeedMbps)) } if d.MaxWriteSpeedMbps != nil { values = append(values, fmt.Sprintf("mbps_wr=%d", *d.MaxWriteSpeedMbps)) } return strings.Join(values, ",") } // EncodeValues converts a CustomStorageDevice struct to a URL value. func (d CustomStorageDevice) EncodeValues(key string, v *url.Values) error { values := []string{ fmt.Sprintf("file=%s", d.FileVolume), } if d.Format != nil { values = append(values, fmt.Sprintf("format=%s", *d.Format)) } if d.Media != nil { values = append(values, fmt.Sprintf("media=%s", *d.Media)) } if d.Size != nil { values = append(values, fmt.Sprintf("size=%d", *d.Size)) } values = append(values, d.EncodeOptions()) v.Add(key, strings.Join(values, ",")) return nil } // Copy returns a deep copy of the CustomStorageDevice. func (d CustomStorageDevice) Copy() *CustomStorageDevice { return &CustomStorageDevice{ AIO: types.CopyString(d.AIO), BackupEnabled: d.BackupEnabled.Copy(), BurstableReadSpeedMbps: types.CopyInt(d.BurstableReadSpeedMbps), Cache: types.CopyString(d.Cache), BurstableWriteSpeedMbps: types.CopyInt(d.BurstableWriteSpeedMbps), Discard: types.CopyString(d.Discard), Enabled: d.Enabled, FileVolume: d.FileVolume, Format: types.CopyString(d.Format), IOThread: d.IOThread.Copy(), SSD: d.SSD.Copy(), MaxReadSpeedMbps: types.CopyInt(d.MaxReadSpeedMbps), MaxWriteSpeedMbps: types.CopyInt(d.MaxWriteSpeedMbps), Media: types.CopyString(d.Media), Size: d.Size.Copy(), Interface: types.CopyString(d.Interface), DatastoreID: types.CopyString(d.DatastoreID), FileID: types.CopyString(d.FileID), } } // CustomStorageDevices handles map of QEMU storage device per disk interface. type CustomStorageDevices map[string]*CustomStorageDevice // ByStorageInterface returns a map of CustomStorageDevices filtered by the given storage interface. func (d CustomStorageDevices) ByStorageInterface(storageInterface string) CustomStorageDevices { result := make(CustomStorageDevices) for k, v := range d { if v.StorageInterface() == storageInterface { result[k] = v } } return result } // EncodeValues converts a CustomStorageDevices array to multiple URL values. func (d CustomStorageDevices) EncodeValues(_ string, v *url.Values) error { for s, d := range d { if d.Enabled { if err := d.EncodeValues(s, v); err != nil { return fmt.Errorf("error encoding storage device %s: %w", s, err) } } } return nil }