mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 10:33:46 +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:
parent
29894bda23
commit
aeb5e88bc9
@ -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.,
|
||||
|
@ -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" {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user