diff --git a/.gitignore b/.gitignore index 2da85eb4..a8419f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ examples/*_override.tf bin .vscode *.log -*env \ No newline at end of file +*env +vendor/* + diff --git a/docs/resources/vm_qemu.md b/docs/resources/vm_qemu.md index 6e09056d..79bb89d2 100644 --- a/docs/resources/vm_qemu.md +++ b/docs/resources/vm_qemu.md @@ -39,7 +39,7 @@ parameter to create based on [a Cloud-init configuration file](https://cloudinit.readthedocs.io/en/latest/topics/examples.html) or use the Proxmox variable `ciuser`, `cipassword`, `ipconfig0`, `ipconfig1`, `ipconfig2`, `ipconfig3`, `ipconfig4`, `ipconfig5`, `ipconfig6`, `ipconfig7`, `ipconfig8`, `ipconfig9`, `ipconfig10`, `ipconfig11`, `ipconfig12`, `ipconfig13`, -`ipconfig14`,`ipconfig15`, `searchdomain`, `nameserver` and `sshkeys`. +`ipconfig14`,`ipconfig15`, `searchdomain`, `nameserver` and `sshkeys`. A variable amount of static IPs can be configured using the dynamic [`ipconfig` block](#ipconfig-block) to list multiple IP addresses. For more information, see the [Cloud-init guide](../guides/cloud_init.md). @@ -164,6 +164,12 @@ details. | `queues` | `int` | `1` | Number of packet queues to be used on the device. Requires `virtio` model to have an effect. | | `link_down` | `bool` | `false` | Whether this interface should be disconnected (like pulling the plug). | +### Ipconfig Block + +The `ipconfig` block is used to configure multiple static IP addresses. It may be specified multiple times. +| Argument | Type | Default Value | Description | +| `config` | `str` | | IP address to assign to the guest. Format: [gw=] [,gw6=] [,ip=] [,ip6=]. | + ### Disk Block The `disk` block is used to configure the disk devices. It may be specified multiple times. The order in which the diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 0381d906..21b89228 100755 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -837,6 +837,19 @@ func resourceVmQemu() *schema.Resource { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, + "ipconfig": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "config": { + Type: schema.TypeString, + Required: true, + Description: "ipconfig string in format: [gw=] [,gw6=] [,ip=] [,ip6=]", + }, + }, + }, + }, "ipconfig0": { Type: schema.TypeString, Optional: true, @@ -987,6 +1000,19 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte qemuUsbs, _ := ExpandDevicesList(d.Get("usb").([]interface{})) + // Populate Ipconfig map from iterable variable + qemuIpconfig, _ := ExpandIpconfigList(d.Get("ipconfig").([]interface{})) + // keeping this for backwards compatibility + if len(qemuIpconfig) == 0 { + // Populate Ipconfig map from explicit vars + for i := 0; i < 16; i++ { + iface := fmt.Sprintf("ipconfig%d", i) + if v, ok := d.GetOk(iface); ok { + qemuIpconfig[i] = v.(string) + } + } + } + config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), @@ -1027,15 +1053,9 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte Searchdomain: d.Get("searchdomain").(string), Nameserver: d.Get("nameserver").(string), Sshkeys: d.Get("sshkeys").(string), - Ipconfig: pxapi.IpconfigMap{}, - } - // Populate Ipconfig map - for i := 0; i < 16; i++ { - iface := fmt.Sprintf("ipconfig%d", i) - if v, ok := d.GetOk(iface); ok { - config.Ipconfig[i] = v.(string) - } + Ipconfig: qemuIpconfig, } + if len(qemuVgaList) > 0 { config.QemuVga = qemuVgaList[0].(map[string]interface{}) } @@ -1316,6 +1336,22 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte return diag.FromErr(fmt.Errorf("error while processing Usb configuration: %v", err)) } + // Populate Ipconfig map from iterable variable + qemuIpconfig, err := ExpandIpconfigList(d.Get("ipconfig").([]interface{})) + if err != nil { + return diag.FromErr(fmt.Errorf("error while processing ipconfig configuration: %v", err)) + } + // keeping this for backwards compatibility + if len(qemuIpconfig) == 0 { + // Populate Ipconfig map from explicit vars + for i := 0; i < 16; i++ { + iface := fmt.Sprintf("ipconfig%d", i) + if v, ok := d.GetOk(iface); ok { + qemuIpconfig[i] = v.(string) + } + } + } + d.Partial(true) if d.HasChange("target_node") { // check if it must be migrated manually or it has been migrated by the promox high availability system @@ -1371,24 +1407,7 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte Searchdomain: d.Get("searchdomain").(string), Nameserver: d.Get("nameserver").(string), Sshkeys: d.Get("sshkeys").(string), - Ipconfig: pxapi.IpconfigMap{ - 0: d.Get("ipconfig0").(string), - 1: d.Get("ipconfig1").(string), - 2: d.Get("ipconfig2").(string), - 3: d.Get("ipconfig3").(string), - 4: d.Get("ipconfig4").(string), - 5: d.Get("ipconfig5").(string), - 6: d.Get("ipconfig6").(string), - 7: d.Get("ipconfig7").(string), - 8: d.Get("ipconfig8").(string), - 9: d.Get("ipconfig9").(string), - 10: d.Get("ipconfig10").(string), - 11: d.Get("ipconfig11").(string), - 12: d.Get("ipconfig12").(string), - 13: d.Get("ipconfig13").(string), - 14: d.Get("ipconfig14").(string), - 15: d.Get("ipconfig15").(string), - }, + Ipconfig: qemuIpconfig, } if len(qemuVgaList) > 0 { config.QemuVga = qemuVgaList[0].(map[string]interface{}) @@ -1446,6 +1465,7 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte "searchdomain", "nameserver", "sshkeys", + "ipconfig", "ipconfig0", "ipconfig1", "ipconfig2", @@ -1747,6 +1767,7 @@ func resourceVmQemuRead(ctx context.Context, d *schema.ResourceData, meta interf d.Set("searchdomain", config.Searchdomain) d.Set("nameserver", config.Nameserver) d.Set("sshkeys", config.Sshkeys) + d.Set("ipconfig", config.Ipconfig) d.Set("ipconfig0", config.Ipconfig[0]) d.Set("ipconfig1", config.Ipconfig[1]) d.Set("ipconfig2", config.Ipconfig[2]) @@ -2178,6 +2199,34 @@ func ExpandDevicesList(deviceList []interface{}) (pxapi.QemuDevices, error) { return expandedDevices, nil } +// Consumes a terraform TypeList of a Qemu Device (network, hard drive, etc) and returns the "Expanded" +// version of the equivalent configuration that the API understands (the struct pxapi.IpconfigMap). +func ExpandIpconfigList(ipconfigList []interface{}) (pxapi.IpconfigMap, error) { + expandedDevices := make(pxapi.IpconfigMap) + + if len(ipconfigList) == 0 { + return expandedDevices, nil + } + + ipconfigDevices, err := ExpandDevicesList(ipconfigList) + if err != nil { + return expandedDevices, err + } + + for index, thisDeviceMap := range ipconfigDevices { + // thisDeviceMap := deviceInterface + + // bail out if the device is empty, it is meaningless in this context + if thisDeviceMap == nil { + continue + } + + expandedDevices[index] = thisDeviceMap["config"] + } + + return expandedDevices, nil +} + // Update schema.TypeSet with new values comes from Proxmox API. // TODO: remove these set functions and convert attributes using a set to a list instead. func UpdateDevicesSet( @@ -2332,8 +2381,8 @@ func initConnInfo(ctx context.Context, if config.HasCloudInit() { log.Print("[DEBUG][initConnInfo] vm has a cloud-init configuration") logger.Debug().Int("vmid", vmr.VmId()).Msgf(" vm has a cloud-init configuration") - _, ipconfig0Set := d.GetOk("ipconfig0") - if ipconfig0Set { + _, ipconfigSet := d.GetOk("ipconfig") + if ipconfigSet { vmState, err := client.GetVmState(vmr) log.Printf("[DEBUG][initConnInfo] cloudinitcheck vm state %v", vmState) logger.Debug().Int("vmid", vmr.VmId()).Msgf("cloudinitcheck vm state %v", vmState) @@ -2343,16 +2392,17 @@ func initConnInfo(ctx context.Context, return diag.FromErr(err) } - if d.Get("ipconfig0").(string) != "ip=dhcp" || vmState["agent"] == nil || vmState["agent"].(float64) != 1 { - // parse IP address out of ipconfig0 - ipMatch := rxIPconfig.FindStringSubmatch(d.Get("ipconfig0").(string)) + ipConfig := d.Get("ipconfig").(map[int]string) + if ipConfig[0] != "ip=dhcp" || vmState["agent"] == nil || vmState["agent"].(float64) != 1 { + // parse IP address out of ipconfig + ipMatch := rxIPconfig.FindStringSubmatch(ipConfig[0]) if sshHost == "" { sshHost = ipMatch[1] } ipconfig0 := net.ParseIP(strings.Split(ipMatch[1], ":")[0]) interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) - log.Printf("[DEBUG][initConnInfo] ipconfig0 interfaces: %v", interfaces) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("ipconfig0 interfaces %v", interfaces) + log.Printf("[DEBUG][initConnInfo] ipconfig[0] interfaces: %v", interfaces) + logger.Debug().Int("vmid", vmr.VmId()).Msgf("ipconfig[0] interfaces %v", interfaces) if err != nil { return diag.FromErr(err) } else {