0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 18:42:58 +00:00

feat(vm): add 'path_in_datastore' disk argument (#606)

* feat(vm): add 'path_in_datastore' disk argument

Provide access to actual in-datastore path to disk image,
and experimental support for attaching other VM's disks or host devices.

Signed-off-by: Oto Petřík <oto.petrik@gmail.com>

* chore: added to `/example` for acceptance testing

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

---------

Signed-off-by: Oto Petřík <oto.petrik@gmail.com>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Oto Petřík 2023-10-12 04:13:09 +02:00 committed by GitHub
parent 29894bda23
commit aeb5e88bc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 245 additions and 44 deletions

View File

@ -240,6 +240,10 @@ output "ubuntu_vm_public_key" {
- `unsafe` - Write directly to the disk bypassing the host cache.
- `datastore_id` - (Optional) The identifier for the datastore to create
the disk in (defaults to `local-lvm`).
- `path_in_datastore` - (Optional) The in-datastore path to the disk image.
***Experimental.***Use to attach another VM's disks,
or (as root only) host's filesystem paths (`datastore_id` empty string).
See "*Example: Attached disks*".
- `discard` - (Optional) Whether to pass discard/trim requests to the
underlying storage. Supported values are `on`/`ignore` (defaults
to `ignore`).
@ -514,6 +518,74 @@ target node. If you need certain disks to be on specific datastores, set
the `datastore_id` argument of the disks in the `disks` block to move the disks
to the correct datastore after the cloning and migrating succeeded.
## Example: Attached disks
In this example VM `data_vm` holds two data disks, and is not used as an actual VM,
but only as a container for the disks.
It does not have any OS installation, it is never started.
VM `data_user_vm` attaches those disks as `scsi1` and `scsi2`.
**VM `data_user_vm` can be *re-created/replaced* without losing data stored on disks
owned by `data_vm`.**
This functionality is **experimental**.
Do *not* simultaneously run more than one VM using same disk. For most filesystems,
attaching one disk to multiple VM will cause errors or even data corruption.
Do *not* move or resize `data_vm` disks.
(Resource `data_user_vm` should reject attempts to move or resize non-owned disks.)
```terraform
resource "proxmox_virtual_environment_vm" "data_vm" {
node_name = "first-node"
started = false
on_boot = false
disk {
datastore_id = "local-zfs"
file_format = "raw"
interface = "scsi0"
size = 1
}
disk {
datastore_id = "local-zfs"
file_format = "raw"
interface = "scsi1"
size = 4
}
}
resource "proxmox_virtual_environment_vm" "data_user_vm" {
# boot disk
disk {
datastore_id = "local-zfs"
file_format = "raw"
interface = "scsi0"
size = 8
}
# attached disks from data_vm
dynamic "disk" {
for_each = { for idx, val in proxmox_virtual_environment_vm.data_vm.disk : idx => val }
iterator = data_disk
content {
datastore_id = data_disk.value["datastore_id"]
path_in_datastore = data_disk.value["path_in_datastore"]
file_format = data_disk.value["file_format"]
size = data_disk.value["size"]
# assign from scsi1 and up
interface = "scsi${data_disk.key + 1}"
}
}
# remainder of VM configuration
...
}
````
## Import
Instances can be imported using the `node_name` and the `vm_id`, e.g.,

View File

@ -164,6 +164,40 @@ resource "proxmox_virtual_environment_vm" "example" {
# mapping = "gpu"
# pcie = true
#}
# attached disks from data_vm
dynamic "disk" {
for_each = {for idx, val in proxmox_virtual_environment_vm.data_vm.disk : idx => val}
iterator = data_disk
content {
datastore_id = data_disk.value["datastore_id"]
path_in_datastore = data_disk.value["path_in_datastore"]
file_format = data_disk.value["file_format"]
size = data_disk.value["size"]
# assign from scsi1 and up
interface = "scsi${data_disk.key + 1}"
}
}
}
resource "proxmox_virtual_environment_vm" "data_vm" {
name = "terraform-provider-proxmox-data-vm"
node_name = data.proxmox_virtual_environment_nodes.example.names[0]
started = false
on_boot = false
disk {
datastore_id = local.datastore_id
file_format = "raw"
interface = "scsi0"
size = 1
}
disk {
datastore_id = local.datastore_id
file_format = "raw"
interface = "scsi1"
size = 4
}
}
output "resource_proxmox_virtual_environment_vm_example_id" {

View File

@ -187,6 +187,46 @@ type CustomStorageDevice struct {
SizeInt *int
}
// PathInDatastore returns path part of FileVolume or nil if it is not yet allocated.
func (r CustomStorageDevice) PathInDatastore() *string {
probablyDatastoreID, pathInDatastore, hasDatastoreID := strings.Cut(r.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 (r CustomStorageDevice) IsOwnedBy(vmID int) bool {
pathInDatastore := r.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
}
return strings.HasPrefix(*pathInDatastore, fmt.Sprintf("vm-%d-", vmID))
}
// CustomStorageDevices handles QEMU SATA device parameters.
type CustomStorageDevices map[string]CustomStorageDevice

View File

@ -177,6 +177,7 @@ const (
mkResourceVirtualEnvironmentVMDisk = "disk"
mkResourceVirtualEnvironmentVMDiskInterface = "interface"
mkResourceVirtualEnvironmentVMDiskDatastoreID = "datastore_id"
mkResourceVirtualEnvironmentVMDiskPathInDatastore = "path_in_datastore"
mkResourceVirtualEnvironmentVMDiskFileFormat = "file_format"
mkResourceVirtualEnvironmentVMDiskFileID = "file_id"
mkResourceVirtualEnvironmentVMDiskSize = "size"
@ -599,14 +600,15 @@ func VM() *schema.Resource {
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkResourceVirtualEnvironmentVMDiskDatastoreID: dvResourceVirtualEnvironmentVMDiskDatastoreID,
mkResourceVirtualEnvironmentVMDiskFileID: dvResourceVirtualEnvironmentVMDiskFileID,
mkResourceVirtualEnvironmentVMDiskInterface: dvResourceVirtualEnvironmentVMDiskInterface,
mkResourceVirtualEnvironmentVMDiskSize: dvResourceVirtualEnvironmentVMDiskSize,
mkResourceVirtualEnvironmentVMDiskIOThread: dvResourceVirtualEnvironmentVMDiskIOThread,
mkResourceVirtualEnvironmentVMDiskSSD: dvResourceVirtualEnvironmentVMDiskSSD,
mkResourceVirtualEnvironmentVMDiskDiscard: dvResourceVirtualEnvironmentVMDiskDiscard,
mkResourceVirtualEnvironmentVMDiskCache: dvResourceVirtualEnvironmentVMDiskCache,
mkResourceVirtualEnvironmentVMDiskDatastoreID: dvResourceVirtualEnvironmentVMDiskDatastoreID,
mkResourceVirtualEnvironmentVMDiskPathInDatastore: nil,
mkResourceVirtualEnvironmentVMDiskFileID: dvResourceVirtualEnvironmentVMDiskFileID,
mkResourceVirtualEnvironmentVMDiskInterface: dvResourceVirtualEnvironmentVMDiskInterface,
mkResourceVirtualEnvironmentVMDiskSize: dvResourceVirtualEnvironmentVMDiskSize,
mkResourceVirtualEnvironmentVMDiskIOThread: dvResourceVirtualEnvironmentVMDiskIOThread,
mkResourceVirtualEnvironmentVMDiskSSD: dvResourceVirtualEnvironmentVMDiskSSD,
mkResourceVirtualEnvironmentVMDiskDiscard: dvResourceVirtualEnvironmentVMDiskDiscard,
mkResourceVirtualEnvironmentVMDiskCache: dvResourceVirtualEnvironmentVMDiskCache,
},
}, nil
},
@ -623,6 +625,13 @@ func VM() *schema.Resource {
Optional: true,
Default: dvResourceVirtualEnvironmentVMDiskDatastoreID,
},
mkResourceVirtualEnvironmentVMDiskPathInDatastore: {
Type: schema.TypeString,
Description: "The in-datastore path to disk image",
Computed: true,
Optional: true,
Default: nil,
},
mkResourceVirtualEnvironmentVMDiskFileFormat: {
Type: schema.TypeString,
Description: "The file format",
@ -3013,6 +3022,12 @@ func vmGetDiskDeviceObjects(
block := diskEntry.(map[string]interface{})
datastoreID, _ := block[mkResourceVirtualEnvironmentVMDiskDatastoreID].(string)
pathInDatastore := ""
if untyped, hasPathInDatastore := block[mkResourceVirtualEnvironmentVMDiskPathInDatastore]; hasPathInDatastore {
pathInDatastore = untyped.(string)
}
fileFormat, _ := block[mkResourceVirtualEnvironmentVMDiskFileFormat].(string)
fileID, _ := block[mkResourceVirtualEnvironmentVMDiskFileID].(string)
size, _ := block[mkResourceVirtualEnvironmentVMDiskSize].(int)
@ -3039,7 +3054,16 @@ func vmGetDiskDeviceObjects(
if fileID != "" {
diskDevice.Enabled = false
} else {
diskDevice.FileVolume = fmt.Sprintf("%s:%d", datastoreID, size)
if pathInDatastore != "" {
if datastoreID != "" {
diskDevice.FileVolume = fmt.Sprintf("%s:%s", datastoreID, pathInDatastore)
} else {
// FileVolume is absolute path in the host filesystem
diskDevice.FileVolume = pathInDatastore
}
} else {
diskDevice.FileVolume = fmt.Sprintf("%s:%d", datastoreID, size)
}
}
diskDevice.ID = &datastoreID
@ -3813,24 +3837,34 @@ func vmReadCustom(
disk := map[string]interface{}{}
fileIDParts := strings.Split(dd.FileVolume, ":")
datastoreID, pathInDatastore, hasDatastoreID := strings.Cut(dd.FileVolume, ":")
if !hasDatastoreID {
// when no ':' separator is found, 'Cut' places the whole string to 'datastoreID',
// we want it in 'pathInDatastore' (it is absolute filesystem path)
pathInDatastore = datastoreID
datastoreID = ""
}
disk[mkResourceVirtualEnvironmentVMDiskDatastoreID] = fileIDParts[0]
disk[mkResourceVirtualEnvironmentVMDiskDatastoreID] = datastoreID
disk[mkResourceVirtualEnvironmentVMDiskPathInDatastore] = pathInDatastore
if dd.Format == nil {
disk[mkResourceVirtualEnvironmentVMDiskFileFormat] = dvResourceVirtualEnvironmentVMDiskFileFormat
// disk format may not be returned by config API if it is default for the storage, and that may be different
// from the default qcow2, so we need to read it from the storage API to make sure we have the correct value
files, err := api.Node(nodeName).ListDatastoreFiles(ctx, fileIDParts[0])
if err != nil {
diags = append(diags, diag.FromErr(err)...)
continue
}
for _, v := range files {
if v.VolumeID == dd.FileVolume {
disk[mkResourceVirtualEnvironmentVMDiskFileFormat] = v.FileFormat
break
if datastoreID != "" {
// disk format may not be returned by config API if it is default for the storage, and that may be different
// from the default qcow2, so we need to read it from the storage API to make sure we have the correct value
files, err := api.Node(nodeName).ListDatastoreFiles(ctx, datastoreID)
if err != nil {
diags = append(diags, diag.FromErr(err)...)
continue
}
for _, v := range files {
if v.VolumeID == dd.FileVolume {
disk[mkResourceVirtualEnvironmentVMDiskFileFormat] = v.FileFormat
break
}
}
}
} else {
@ -5602,29 +5636,48 @@ func vmUpdateDiskLocationAndSize(
}
if *oldDisk.ID != *diskNewEntries[prefix][oldKey].ID {
deleteOriginalDisk := types.CustomBool(true)
if oldDisk.IsOwnedBy(vmID) {
deleteOriginalDisk := types.CustomBool(true)
diskMoveBodies = append(
diskMoveBodies,
&vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: *oldDisk.Interface,
TargetStorage: *diskNewEntries[prefix][oldKey].ID,
},
)
diskMoveBodies = append(
diskMoveBodies,
&vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: *oldDisk.Interface,
TargetStorage: *diskNewEntries[prefix][oldKey].ID,
},
)
// Cannot be done while VM is running.
shutdownForDisksRequired = true
// Cannot be done while VM is running.
shutdownForDisksRequired = true
} else {
return diag.Errorf(
"Cannot move %s:%s to datastore %s in VM %d configuration, it is not owned by this VM!",
*oldDisk.ID,
*oldDisk.PathInDatastore(),
*diskNewEntries[prefix][oldKey].ID,
vmID,
)
}
}
if *oldDisk.SizeInt < *diskNewEntries[prefix][oldKey].SizeInt {
diskResizeBodies = append(
diskResizeBodies,
&vms.ResizeDiskRequestBody{
Disk: *oldDisk.Interface,
Size: *diskNewEntries[prefix][oldKey].Size,
},
)
if oldDisk.IsOwnedBy(vmID) {
diskResizeBodies = append(
diskResizeBodies,
&vms.ResizeDiskRequestBody{
Disk: *oldDisk.Interface,
Size: *diskNewEntries[prefix][oldKey].Size,
},
)
} else {
return diag.Errorf(
"Cannot resize %s:%s in VM %d configuration, it is not owned by this VM!",
*oldDisk.ID,
*oldDisk.PathInDatastore(),
vmID,
)
}
}
}
}

View File

@ -189,16 +189,18 @@ func TestVMSchema(t *testing.T) {
test.AssertOptionalArguments(t, diskSchema, []string{
mkResourceVirtualEnvironmentVMDiskDatastoreID,
mkResourceVirtualEnvironmentVMDiskPathInDatastore,
mkResourceVirtualEnvironmentVMDiskFileFormat,
mkResourceVirtualEnvironmentVMDiskFileID,
mkResourceVirtualEnvironmentVMDiskSize,
})
test.AssertValueTypes(t, diskSchema, map[string]schema.ValueType{
mkResourceVirtualEnvironmentVMDiskDatastoreID: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskFileFormat: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskFileID: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskSize: schema.TypeInt,
mkResourceVirtualEnvironmentVMDiskDatastoreID: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskPathInDatastore: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskFileFormat: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskFileID: schema.TypeString,
mkResourceVirtualEnvironmentVMDiskSize: schema.TypeInt,
})
diskSpeedSchema := test.AssertNestedSchemaExistence(