Skip to content

Commit ddccd0a

Browse files
hsnprsdhsnprsdbpg
authored
feat(vm): support custom cloud init drive file format (#2375)
* feat(vm): support custom cloud init drive file format Signed-off-by: Ehsan Poursaeed <hsnprsd@gmail.com> * Update proxmoxtf/resource/vm/vm.go Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Signed-off-by: Ehsan Poursaeed <hsnprsd@gmail.com> * fix pointer ref in `mkInitializationFileFormat` assignment Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Ehsan Poursaeed <hsnprsd@gmail.com> Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: hsnprsd <hsnprsd@hsnprsd.hsnprsd.local> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
1 parent 2afa8e5 commit ddccd0a

File tree

5 files changed

+135
-38
lines changed

5 files changed

+135
-38
lines changed

docs/resources/virtual_environment_vm.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ output "ubuntu_vm_public_key" {
377377
image to. Must be one of `ide0..3`, `sata0..5`, `scsi0..30`. Will be
378378
detected if the setting is missing but a cloud-init image is present,
379379
otherwise defaults to `ide2`.
380+
- `file_format` - (Optional) The file format.
381+
- `qcow2` - QEMU Disk Image v2.
382+
- `raw` - Raw Disk Image.
383+
- `vmdk` - VMware Disk Image.
380384
- `dns` - (Optional) The DNS configuration.
381385
- `domain` - (Optional) The DNS search domain.
382386
- `server` - (Optional) The DNS server. The `server` attribute is

fwprovider/test/resource_vm_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,49 @@ func TestAccResourceVMInitialization(t *testing.T) {
609609
name string
610610
step []resource.TestStep
611611
}{
612+
{"custom cloud-init drive file format", []resource.TestStep{{
613+
Config: te.RenderConfig(`
614+
resource "proxmox_virtual_environment_vm" "test_vm_cloudinit" {
615+
node_name = "{{.NodeName}}"
616+
started = false
617+
cpu {
618+
cores = 1
619+
}
620+
memory {
621+
dedicated = 1024
622+
}
623+
624+
initialization {
625+
datastore_id = "local"
626+
file_format = "raw"
627+
}
628+
}`),
629+
Check: ResourceAttributes("proxmox_virtual_environment_vm.test_vm_cloudinit", map[string]string{
630+
"initialization.0.datastore_id": "local",
631+
"initialization.0.file_format": "raw",
632+
}),
633+
}, {
634+
Config: te.RenderConfig(`
635+
resource "proxmox_virtual_environment_vm" "test_vm_cloudinit" {
636+
node_name = "{{.NodeName}}"
637+
started = false
638+
cpu {
639+
cores = 1
640+
}
641+
memory {
642+
dedicated = 1024
643+
}
644+
645+
initialization {
646+
datastore_id = "local"
647+
file_format = "qcow2"
648+
}
649+
}`),
650+
Check: ResourceAttributes("proxmox_virtual_environment_vm.test_vm_cloudinit", map[string]string{
651+
"initialization.0.datastore_id": "local",
652+
"initialization.0.file_format": "qcow2",
653+
}),
654+
}}},
612655
{"custom cloud-init: use SCSI interface", []resource.TestStep{{
613656
Config: te.RenderConfig(`
614657
resource "proxmox_virtual_environment_file" "cloud_config" {

proxmox/nodes/vms/custom_storage_device.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ func (d *CustomStorageDevice) IsOwnedBy(vmID int) bool {
110110

111111
// IsCloudInitDrive returns true, if CustomStorageDevice is a cloud-init drive.
112112
func (d *CustomStorageDevice) IsCloudInitDrive(vmID int) bool {
113-
return d.Media != nil && *d.Media == "cdrom" &&
114-
strings.Contains(d.FileVolume, fmt.Sprintf("vm-%d-cloudinit", vmID))
113+
return strings.Contains(d.FileVolume, fmt.Sprintf("vm-%d-cloudinit", vmID))
115114
}
116115

117116
// EncodeOptions converts a CustomStorageDevice's common options a URL value.

proxmoxtf/resource/vm/vm.go

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"encoding/base64"
1212
"errors"
1313
"fmt"
14+
"path/filepath"
1415
"regexp"
1516
"sort"
1617
"strconv"
@@ -225,6 +226,7 @@ const (
225226
mkInitialization = "initialization"
226227
mkInitializationDatastoreID = "datastore_id"
227228
mkInitializationInterface = "interface"
229+
mkInitializationFileFormat = "file_format"
228230
mkInitializationDNS = "dns"
229231
mkInitializationDNSDomain = "domain"
230232
mkInitializationDNSServers = "servers"
@@ -840,6 +842,13 @@ func VM() *schema.Resource {
840842
return newValue == ""
841843
},
842844
},
845+
mkInitializationFileFormat: {
846+
Type: schema.TypeString,
847+
Description: "The file format",
848+
Optional: true,
849+
Computed: true,
850+
ValidateDiagFunc: validators.FileFormat(),
851+
},
843852
mkInitializationDNS: {
844853
Type: schema.TypeList,
845854
Description: "The DNS configuration",
@@ -1769,16 +1778,16 @@ func vmCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnosti
17691778
}
17701779

17711780
// Check for an existing CloudInit IDE drive. If no such drive is found, return the specified `defaultValue`.
1772-
func findExistingCloudInitDrive(vmConfig *vms.GetResponseData, vmID int, defaultValue string) string {
1781+
func findExistingCloudInitDrive(vmConfig *vms.GetResponseData, vmID int, defaultValue string) (string, *vms.CustomStorageDevice) {
17731782
devs := vmConfig.StorageDevices.Filter(func(device *vms.CustomStorageDevice) bool {
17741783
return device.IsCloudInitDrive(vmID)
17751784
})
17761785

1777-
for iface := range devs {
1778-
return iface
1786+
for iface, device := range devs {
1787+
return iface, device
17791788
}
17801789

1781-
return defaultValue
1790+
return defaultValue, nil
17821791
}
17831792

17841793
// Return a pointer to the storage device configuration based on a name. The device name is assumed to be a
@@ -2203,21 +2212,27 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m any) diag.Diag
22032212
initializationBlock := initialization[0].(map[string]any)
22042213
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
22052214
initializationInterface := initializationBlock[mkInitializationInterface].(string)
2215+
initializationFileFormat := initializationBlock[mkInitializationFileFormat].(string)
22062216

2207-
existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "ide2")
2217+
existingInterface, _ := findExistingCloudInitDrive(vmConfig, vmID, "ide2")
22082218
if initializationInterface == "" {
22092219
initializationInterface = existingInterface
22102220
}
22112221

22122222
tflog.Trace(ctx, fmt.Sprintf("CloudInit IDE interface is '%s'", initializationInterface))
22132223

2214-
cdromCloudInitFileID := fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
2215-
cdromCloudInitMedia := "cdrom"
2216-
ideDevices[initializationInterface] = &vms.CustomStorageDevice{
2217-
FileVolume: cdromCloudInitFileID,
2218-
Media: &cdromCloudInitMedia,
2224+
initializationFileVolume := fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
2225+
2226+
device := &vms.CustomStorageDevice{
2227+
FileVolume: initializationFileVolume,
22192228
}
22202229

2230+
if initializationFileFormat != "" {
2231+
device.Format = &initializationFileFormat
2232+
}
2233+
2234+
ideDevices[initializationInterface] = device
2235+
22212236
if err := deleteIdeDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil {
22222237
return err
22232238
}
@@ -2594,8 +2609,9 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m any) diag.Dia
25942609
}
25952610
}
25962611

2597-
cdromCloudInitFileID := ""
2598-
cdromCloudInitInterface := ""
2612+
initializationFileVolume := ""
2613+
initializationInterface := ""
2614+
initializationFileFormat := ""
25992615

26002616
cpuBlock, err := structure.GetSchemaBlock(
26012617
resource,
@@ -2659,12 +2675,13 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m any) diag.Dia
26592675

26602676
initializationBlock := initialization[0].(map[string]any)
26612677
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
2678+
initializationFileFormat = initializationBlock[mkInitializationFileFormat].(string)
26622679

2663-
cdromCloudInitFileID = fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
2680+
initializationFileVolume = fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
26642681

2665-
cdromCloudInitInterface = initializationBlock[mkInitializationInterface].(string)
2666-
if cdromCloudInitInterface == "" {
2667-
cdromCloudInitInterface = "ide2"
2682+
initializationInterface = initializationBlock[mkInitializationInterface].(string)
2683+
if initializationInterface == "" {
2684+
initializationInterface = "ide2"
26682685
}
26692686
}
26702687

@@ -2794,19 +2811,24 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m any) diag.Dia
27942811
cpuFlagsConverted[fi] = flag.(string)
27952812
}
27962813

2797-
ideDevice2Media := "cdrom"
2814+
cdromMedia := "cdrom"
27982815

2799-
if cdromCloudInitInterface != "" {
2800-
diskDeviceObjects[cdromCloudInitInterface] = &vms.CustomStorageDevice{
2801-
FileVolume: cdromCloudInitFileID,
2802-
Media: &ideDevice2Media,
2816+
if initializationInterface != "" {
2817+
device := &vms.CustomStorageDevice{
2818+
FileVolume: initializationFileVolume,
28032819
}
2820+
2821+
if initializationFileFormat != "" {
2822+
device.Format = &initializationFileFormat
2823+
}
2824+
2825+
diskDeviceObjects[initializationInterface] = device
28042826
}
28052827

28062828
if cdromInterface != "" {
28072829
diskDeviceObjects[cdromInterface] = &vms.CustomStorageDevice{
28082830
FileVolume: cdromFileID,
2809-
Media: &ideDevice2Media,
2831+
Media: &cdromMedia,
28102832
}
28112833
}
28122834

@@ -4384,13 +4406,28 @@ func vmReadCustom(
43844406
// Compare the initialization configuration to the one stored in the state.
43854407
initialization := map[string]any{}
43864408

4387-
initializationInterface := findExistingCloudInitDrive(vmConfig, vmID, "")
4409+
initializationInterface, initializationDevice := findExistingCloudInitDrive(vmConfig, vmID, "")
43884410
if initializationInterface != "" {
4389-
initializationDevice := getStorageDevice(vmConfig, initializationInterface)
4390-
fileVolumeParts := strings.Split(initializationDevice.FileVolume, ":")
4411+
parts := strings.Split(initializationDevice.FileVolume, ",")
4412+
4413+
fileVolume := parts[0]
4414+
fileVolumeParts := strings.Split(fileVolume, ":")
4415+
4416+
datastoreId := fileVolumeParts[0]
4417+
pathInDatastore := fileVolumeParts[1]
43914418

43924419
initialization[mkInitializationInterface] = initializationInterface
4393-
initialization[mkInitializationDatastoreID] = fileVolumeParts[0]
4420+
initialization[mkInitializationDatastoreID] = datastoreId
4421+
4422+
if initializationDevice.Format != nil && *initializationDevice.Format != "" {
4423+
initialization[mkInitializationFileFormat] = *initializationDevice.Format
4424+
} else {
4425+
fileFormat := filepath.Ext(pathInDatastore)
4426+
4427+
if fileFormat != "" {
4428+
initialization[mkInitializationFileFormat] = fileFormat[1:]
4429+
}
4430+
}
43944431
}
43954432

43964433
if vmConfig.CloudInitDNSDomain != nil || vmConfig.CloudInitDNSServer != nil {
@@ -5631,35 +5668,43 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnosti
56315668
initializationBlock := initialization[0].(map[string]any)
56325669
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
56335670
initializationInterface := initializationBlock[mkInitializationInterface].(string)
5634-
cdromMedia := "cdrom"
5671+
initializationFileFormat := initializationBlock[mkInitializationFileFormat].(string)
56355672

5636-
existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "")
5673+
existingInterface, existingDevice := findExistingCloudInitDrive(vmConfig, vmID, "")
56375674
if initializationInterface == "" && existingInterface == "" {
56385675
initializationInterface = "ide2"
56395676
} else if initializationInterface == "" {
56405677
initializationInterface = existingInterface
56415678
}
56425679

5643-
mustMove := existingInterface != "" && initializationInterface != existingInterface
5644-
if mustMove {
5680+
mustChangeInterface := existingInterface != "" && initializationInterface != existingInterface
5681+
if mustChangeInterface {
56455682
tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from %s to %s", existingInterface, initializationInterface))
56465683
}
56475684

56485685
mustChangeDatastore := false
5686+
mustChangeFileFormat := false
56495687

56505688
oldInit, _ := d.GetChange(mkInitialization)
56515689
if len(oldInit.([]any)) > 0 {
56525690
oldInitBlock := oldInit.([]any)[0].(map[string]any)
56535691
prevDatastoreID := oldInitBlock[mkInitializationDatastoreID].(string)
5692+
prevFileFormat := oldInitBlock[mkInitializationFileFormat].(string)
56545693

56555694
mustChangeDatastore = prevDatastoreID != initializationDatastoreID
56565695
if mustChangeDatastore {
56575696
tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from datastore %s to datastore %s",
56585697
prevDatastoreID, initializationDatastoreID))
56595698
}
5699+
5700+
mustChangeFileFormat = prevFileFormat != initializationFileFormat
5701+
if mustChangeFileFormat {
5702+
tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from file format %s to file format %s",
5703+
prevFileFormat, initializationFileFormat))
5704+
}
56605705
}
56615706

5662-
if mustMove || mustChangeDatastore || existingInterface == "" {
5707+
if mustChangeInterface || mustChangeDatastore || mustChangeFileFormat || existingInterface == "" {
56635708
// CloudInit must be moved, either from a device to another or from a datastore
56645709
// to another (or both). This requires the VM to be stopped.
56655710
if !stoppedBeforeUpdate {
@@ -5675,14 +5720,18 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnosti
56755720
stoppedBeforeUpdate = true
56765721
fileVolume = fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
56775722
} else {
5678-
ideDevice := getStorageDevice(vmConfig, existingInterface)
5679-
fileVolume = ideDevice.FileVolume
5723+
fileVolume = existingDevice.FileVolume
56805724
}
56815725

5682-
updateBody.AddCustomStorageDevice(initializationInterface, vms.CustomStorageDevice{
5726+
device := vms.CustomStorageDevice{
56835727
FileVolume: fileVolume,
5684-
Media: &cdromMedia,
5685-
})
5728+
}
5729+
5730+
if initializationFileFormat != "" {
5731+
device.Format = &initializationFileFormat
5732+
}
5733+
5734+
updateBody.AddCustomStorageDevice(initializationInterface, device)
56865735
}
56875736

56885737
cloudInitRebuildRequired = true

proxmoxtf/resource/vm/vm_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ func TestVMSchema(t *testing.T) {
202202
test.AssertOptionalArguments(t, initializationSchema, []string{
203203
mkInitializationDatastoreID,
204204
mkInitializationInterface,
205+
mkInitializationFileFormat,
205206
mkInitializationDNS,
206207
mkInitializationIPConfig,
207208
mkInitializationUserAccount,
@@ -210,6 +211,7 @@ func TestVMSchema(t *testing.T) {
210211
test.AssertValueTypes(t, initializationSchema, map[string]schema.ValueType{
211212
mkInitializationDatastoreID: schema.TypeString,
212213
mkInitializationInterface: schema.TypeString,
214+
mkInitializationFileFormat: schema.TypeString,
213215
mkInitializationDNS: schema.TypeList,
214216
mkInitializationIPConfig: schema.TypeList,
215217
mkInitializationUserAccount: schema.TypeList,

0 commit comments

Comments
 (0)