diff --git a/docs/resources/vm_qemu.md b/docs/resources/vm_qemu.md index 89cce8e4..e7d79792 100644 --- a/docs/resources/vm_qemu.md +++ b/docs/resources/vm_qemu.md @@ -137,10 +137,11 @@ The following arguments are supported in the top level resource block. | `searchdomain` | `str` | | Sets default DNS search domain suffix. | | `nameserver` | `str` | | Sets default DNS server for guest. | | `sshkeys` | `str` | | Newline delimited list of SSH public keys to add to authorized keys file for the cloud-init user. | -| `ipconfig0` | `str` | | The first IP address to assign to the guest. Format: `[gw=] [,gw6=] [,ip=] [,ip6=]`. | +| `ipconfig0` | `str` | `''` | The first IP address to assign to the guest. Format: `[gw=] [,gw6=] [,ip=] [,ip6=]`. When `os_type` is `cloud-init` not setting `ip=` is equivalent to `skip_ipv4` == `true` and `ip6=` to `skip_ipv4` == `true` .| | `ipconfig1` to `ipconfig15` | `str` | | The second IP address to assign to the guest. Same format as `ipconfig0`. | | `automatic_reboot` | `bool` | `true` | Automatically reboot the VM when parameter changes require this. If disabled the provider will emit a warning when the VM needs to be rebooted. | - +| `skip_ipv4` | `bool` | `false` | Tells proxmox that acquiring an IPv4 address from the qemu guest agent isn't required, it will still return an ipv4 address if it could obtain one. Useful for reducing retries in environments without ipv4.| +| `skip_ipv6` | `bool` | `false` | Tells proxmox that acquiring an IPv6 address from the qemu guest agent isn't required, it will still return an ipv6 address if it could obtain one. Useful for reducing retries in environments without ipv6.| ### VGA Block @@ -484,7 +485,8 @@ In addition to the arguments above, the following attributes can be referenced f | ---------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ssh_host` | `str` | Read-only attribute. Only applies when `define_connection_info` is true. The hostname or IP to use to connect to the VM for preprovisioning. This can be overridden by defining `ssh_forward_ip`, but if you're using cloud-init and `ipconfig0=dhcp`, the IP reported by qemu-guest-agent is used, otherwise the IP defined in `ipconfig0` is used. | | `ssh_port` | `str` | Read-only attribute. Only applies when `define_connection_info` is true. The port to connect to the VM over SSH for preprovisioning. If using cloud-init and a port is not specified in `ssh_forward_ip`, then 22 is used. If not using cloud-init, a port on the `target_node` will be forwarded to port 22 in the guest, and this attribute will be set to the forwarded port. | -| `default_ipv4_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the vm has. | +| `default_ipv4_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the vm has. The settings `ipconfig0` and `skip_ipv4` have influence on this.| +| `default_ipv6_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the vm has. The settings `ipconfig0` and `skip_ipv6` have influence on this.| ## Import diff --git a/proxmox/heper_qemu.go b/proxmox/heper_qemu.go new file mode 100644 index 00000000..5ba27cbd --- /dev/null +++ b/proxmox/heper_qemu.go @@ -0,0 +1,83 @@ +package proxmox + +import ( + "strings" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" +) + +const ( + ErrorGuestAgentNotRunning string = "500 QEMU guest agent is not running" +) + +func parseCloudInitInterface(ipConfig string, skipIPv4, skipIPv6 bool) (conn connectionInfo) { + conn.SkipIPv4 = skipIPv4 + conn.SkipIPv6 = skipIPv6 + var IPv4Set, IPv6Set bool + for _, e := range strings.Split(ipConfig, ",") { + if len(e) < 4 { + continue + } + if e[:3] == "ip=" { + IPv4Set = true + splitCIDR := strings.Split(e[3:], "/") + if len(splitCIDR) == 2 { + conn.IPs.IPv4 = splitCIDR[0] + } + } + if e[:4] == "ip6=" { + IPv6Set = true + splitCIDR := strings.Split(e[4:], "/") + if len(splitCIDR) == 2 { + conn.IPs.IPv6 = splitCIDR[0] + } + } + } + if !IPv4Set && conn.IPs.IPv4 == "" { + conn.SkipIPv4 = true + } + if !IPv6Set && conn.IPs.IPv6 == "" { + conn.SkipIPv6 = true + } + return +} + +type primaryIPs struct { + IPv4 string + IPv6 string +} + +type connectionInfo struct { + IPs primaryIPs + SkipIPv4 bool + SkipIPv6 bool +} + +func (conn connectionInfo) hasRequiredIP() bool { + if conn.IPs.IPv4 == "" && !conn.SkipIPv4 || conn.IPs.IPv6 == "" && !conn.SkipIPv6 { + return false + } + return true +} + +func (conn connectionInfo) parsePrimaryIPs(interfaces []pxapi.AgentNetworkInterface, mac string) connectionInfo { + lowerCaseMac := strings.ToLower(mac) + for _, iFace := range interfaces { + if iFace.MacAddress.String() == lowerCaseMac { + for _, addr := range iFace.IpAddresses { + if addr.IsGlobalUnicast() { + if addr.To4() != nil { + if conn.IPs.IPv4 == "" { + conn.IPs.IPv4 = addr.String() + } + } else { + if conn.IPs.IPv6 == "" { + conn.IPs.IPv6 = addr.String() + } + } + } + } + } + } + return conn +} diff --git a/proxmox/heper_qemu_test.go b/proxmox/heper_qemu_test.go new file mode 100644 index 00000000..a81aefda --- /dev/null +++ b/proxmox/heper_qemu_test.go @@ -0,0 +1,244 @@ +package proxmox + +import ( + "net" + "testing" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/stretchr/testify/require" +) + +func Test_HasRequiredIP(t *testing.T) { + tests := []struct { + name string + input connectionInfo + output bool + }{ + {name: `IPv4`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1"}}, + output: false}, + {name: `IPv4 SkipIPv4`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1"}, + SkipIPv4: true}, + output: false}, + {name: `SkipIPv4`, + input: connectionInfo{}, + output: false}, + {name: `IPv6`, + input: connectionInfo{IPs: primaryIPs{ + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}, + output: false}, + {name: `IPv6 SkipIPv6`, + input: connectionInfo{IPs: primaryIPs{ + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv6: true}, + output: false}, + {name: `SkipIPv6`, + input: connectionInfo{}, + output: false}, + {name: `IPv4 IPv6`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}, + output: true}, + {name: `IPv4 IPv6 SkipIPv4`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv4: true}, + output: true}, + {name: `IPv4 IPv6 SkipIPv6`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv6: true}, + output: true}, + {name: `IPv4 IPv6 SkipIPv4 SkipIPv6`, + input: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv4: true, + SkipIPv6: true}, + output: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.hasRequiredIP()) + }) + } +} + +func Test_ParseCloudInitInterface(t *testing.T) { + type testInput struct { + ci string + skipIPv4 bool + skipIPv6 bool + } + tests := []struct { + name string + input testInput + output connectionInfo + }{ + {name: `IPv4=DHCP`, + input: testInput{ci: "ip=dhcp"}, + output: connectionInfo{SkipIPv6: true}}, + {name: `IPv4=DHCP SkipIPv4`, + input: testInput{ + ci: "ip=dhcp", + skipIPv4: true}, + output: connectionInfo{ + SkipIPv4: true, + SkipIPv6: true}}, + {name: `IPv4=Static`, + input: testInput{ci: "ip=192.168.1.1/24"}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1"}, + SkipIPv6: true}}, + {name: `IPv4=Static IPv6=Static`, + input: testInput{ci: "ip=192.168.1.1/24,ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}}, + {name: `IPv4=Static SkipIPv4`, + input: testInput{ + ci: "ip=192.168.1.1/24", + skipIPv4: true}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1"}, + SkipIPv4: true, + SkipIPv6: true}}, + {name: `IPv6=DHCP`, + input: testInput{ci: "ip6=dhcp"}, + output: connectionInfo{SkipIPv4: true}}, + {name: `IPv6=DHCP SkipIPv6`, + input: testInput{ + ci: "ip6=dhcp", + skipIPv6: true}, + output: connectionInfo{ + SkipIPv4: true, + SkipIPv6: true}}, + {name: `IPv6=Static`, + input: testInput{ci: "ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"}, + output: connectionInfo{IPs: primaryIPs{ + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv4: true}}, + {name: `IPv6=Static SkipIPv6`, + input: testInput{ + ci: "ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64", + skipIPv6: true}, + output: connectionInfo{IPs: primaryIPs{ + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv4: true, + SkipIPv6: true}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, parseCloudInitInterface(test.input.ci, test.input.skipIPv4, test.input.skipIPv6)) + }) + } +} + +func Test_ParsePrimaryIPs(t *testing.T) { + parseMac := func(mac string) net.HardwareAddr { + realMac, _ := net.ParseMAC(mac) + return realMac + } + parseIP := func(ip string) net.IP { + realIP, _, _ := net.ParseCIDR(ip) + return realIP + } + formatIP := func(ip string) string { + return net.ParseIP(ip).String() + } + type testInput struct { + interfaces []pxapi.AgentNetworkInterface + mac string + conn connectionInfo + } + tests := []struct { + name string + input testInput + output connectionInfo + }{ + {name: `Only Loopback`, + input: testInput{ + mac: "9c:7a:1b:4f:3e:a2", + interfaces: []pxapi.AgentNetworkInterface{ + { + MacAddress: parseMac("9C:7A:1B:4F:3E:A2"), + Name: "eth1", + IpAddresses: []net.IP{ + parseIP("127.0.0.1/8"), + parseIP("::1/128")}}}}}, + {name: `Only IPv4`, + input: testInput{ + mac: "3A:7E:9D:1F:5B:8C", + interfaces: []pxapi.AgentNetworkInterface{ + {MacAddress: parseMac("3A:7E:9D:1F:5B:8C"), + Name: "eth1", + IpAddresses: []net.IP{ + parseIP("127.0.0.1/8"), + parseIP("192.168.1.1/24"), + parseIP("::1/128")}}}}, + output: connectionInfo{IPs: primaryIPs{IPv4: formatIP("192.168.1.1")}}}, + {name: `Only IPv6`, + input: testInput{ + mac: "6F:2C:4A:8E:7D:1B", + interfaces: []pxapi.AgentNetworkInterface{ + {MacAddress: parseMac("6F:2C:4A:8E:7D:1B"), + Name: "eth1", + IpAddresses: []net.IP{ + parseIP("127.0.0.1/8"), + parseIP("::1/128"), + parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}}}}, + output: connectionInfo{IPs: primaryIPs{IPv6: formatIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}}}, + {name: `Full test`, + input: testInput{ + mac: "3A:7E:9D:1F:5B:8C", + interfaces: []pxapi.AgentNetworkInterface{ + {MacAddress: parseMac("6F:2C:4A:8E:7D:1B"), + Name: "lo", + IpAddresses: []net.IP{ + parseIP("127.0.0.1/8"), + parseIP("::1/128")}}, + {MacAddress: parseMac("9C:7A:1B:4F:3E:A2"), + Name: "eth0", + IpAddresses: []net.IP{ + parseIP("192.168.1.1/24"), + parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}}, + {MacAddress: parseMac("3A:7E:9D:1F:5B:8C"), + Name: "wth1", + IpAddresses: []net.IP{ + parseIP("10.10.10.1/16"), + parseIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf/64")}}, + }}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: formatIP("10.10.10.1"), + IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")}, + }, + }, + {name: `IPv4 Already Set`, + input: testInput{ + mac: "3A:7E:9D:1F:5B:8C", + interfaces: []pxapi.AgentNetworkInterface{ + {MacAddress: parseMac("3A:7E:9D:1F:5B:8C"), + IpAddresses: []net.IP{parseIP("192.168.1.1/24")}}}, + conn: connectionInfo{IPs: primaryIPs{IPv4: formatIP("10.10.1.1")}}}, + output: connectionInfo{IPs: primaryIPs{IPv4: formatIP("10.10.1.1")}}}, + {name: `IPv6 Already Set`, + input: testInput{ + mac: "3A:7E:9D:1F:5B:8C", + interfaces: []pxapi.AgentNetworkInterface{ + {MacAddress: parseMac("3A:7E:9D:1F:5B:8C"), + IpAddresses: []net.IP{parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}}}, + conn: connectionInfo{IPs: primaryIPs{IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")}}}, + output: connectionInfo{IPs: primaryIPs{IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")}}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.conn.parsePrimaryIPs(test.input.interfaces, test.input.mac)) + }) + } +} diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 2e0bdd49..7d0cb2b4 100755 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -7,7 +7,6 @@ import ( "fmt" "log" "math/rand" - "net" "net/url" "path" "regexp" @@ -34,6 +33,8 @@ const ( stateRunning string = "running" stateStarted string = "started" stateStopped string = "stopped" + + maxAgentTry uint = 5 ) func resourceVmQemu() *schema.Resource { @@ -764,8 +765,9 @@ func resourceVmQemu() *schema.Resource { Optional: true, }, "ssh_host": { - Type: schema.TypeString, - Computed: true, + Type: schema.TypeString, + Computed: true, + Description: "The ip address used for the ssh connection, this will prefer ipv4 over ipv6 if both are available", }, "ssh_port": { Type: schema.TypeString, @@ -776,6 +778,24 @@ func resourceVmQemu() *schema.Resource { Optional: true, ForceNew: true, }, + "skip_ipv4": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ConflictsWith: []string{"skip_ipv6"}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return true + }, + }, + "skip_ipv6": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ConflictsWith: []string{"skip_ipv4"}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return true + }, + }, "reboot_required": { Type: schema.TypeBool, Computed: true, @@ -786,6 +806,11 @@ func resourceVmQemu() *schema.Resource { Computed: true, Description: "Use to track vm ipv4 address", }, + "default_ipv6_address": { + Type: schema.TypeString, + Computed: true, + Description: "Use to track vm ipv6 address", + }, "define_connection_info": { // by default define SSH for provisioner info Type: schema.TypeBool, Optional: true, @@ -1815,45 +1840,39 @@ func initConnInfo(ctx context.Context, config *pxapi.ConfigQemu, lock *pmApiLockHolder, ) diag.Diagnostics { - logger, _ := CreateSubLogger("initConnInfo") - - var err error - var lasterr error - var interfaces []pxapi.AgentNetworkInterface var diags diag.Diagnostics // allow user to opt-out of setting the connection info for the resource if !d.Get("define_connection_info").(bool) { log.Printf("[INFO][initConnInfo] define_connection_info is %t, no further action", d.Get("define_connection_info").(bool)) logger.Info().Int("vmid", vmr.VmId()).Msgf("define_connection_info is %t, no further action", d.Get("define_connection_info").(bool)) - diags = append(diags, diag.Diagnostic{ + return append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "define_connection_info is %t, no further action.", Detail: "define_connection_info is %t, no further action", - AttributePath: cty.Path{}, - }) - return diags + AttributePath: cty.Path{}}) } - // allow user to opt-out of setting the connection info for the resource - if d.Get("agent") != 1 { - log.Printf("[INFO][initConnInfo] qemu agent is disabled from proxmox config, cant communicate with vm.") - logger.Info().Int("vmid", vmr.VmId()).Msgf("qemu agent is disabled from proxmox config, cant communicate with vm.") - diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Qemu Guest Agent support is disabled from proxmox config.", - Detail: "Qemu Guest Agent support is required to make communications with the VM", - AttributePath: cty.Path{}, - }) - return diags + + var ciAgentEnabled bool + + if config.Agent != nil && config.Agent.Enable != nil && *config.Agent.Enable { + if d.Get("agent") != 1 { // allow user to opt-out of setting the connection info for the resource + log.Printf("[INFO][initConnInfo] qemu agent is disabled from proxmox config, cant communicate with vm.") + logger.Info().Int("vmid", vmr.VmId()).Msgf("qemu agent is disabled from proxmox config, cant communicate with vm.") + return append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Qemu Guest Agent support is disabled from proxmox config.", + Detail: "Qemu Guest Agent support is required to make communications with the VM", + AttributePath: cty.Path{}}) + } + ciAgentEnabled = true } log.Print("[INFO][initConnInfo] trying to get vm ip address for provisioner") logger.Info().Int("vmid", vmr.VmId()).Msgf("trying to get vm ip address for provisioner") sshPort := "22" - sshHost := "" - // assume guest agent not running yet or not enabled - guestAgentRunning := false + var sshHost string // wait until the os has started the guest agent guestAgentTimeout := d.Timeout(schema.TimeoutCreate) @@ -1863,127 +1882,17 @@ func initConnInfo(ctx context.Context, logger.Debug().Int("vmid", vmr.VmId()).Msgf("retrying for at most %v minutes before giving up", guestAgentTimeout) logger.Debug().Int("vmid", vmr.VmId()).Msgf("retries will end at %s", guestAgentWaitEnd) - for time.Now().Before(guestAgentWaitEnd) { - interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) - lasterr = err - if err != nil { - log.Printf("[DEBUG][initConnInfo] check ip result error %s", err.Error()) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("check ip result error %s", err.Error()) - } else if err == nil { - lasterr = nil - log.Print("[INFO][initConnInfo] found working QEMU Agent") - log.Printf("[DEBUG][initConnInfo] interfaces found: %v", interfaces) - logger.Info().Int("vmid", vmr.VmId()).Msgf("found working QEMU Agent") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("interfaces found: %v", interfaces) - - guestAgentRunning = true - break - } else if !strings.Contains(err.Error(), "500 QEMU guest agent is not running") { - // "not running" means either not installed or not started yet. - // any other error should not happen here - return diag.FromErr(err) - } - // wait before next try - time.Sleep(time.Duration(d.Get("additional_wait").(int)) * time.Second) - } - if lasterr != nil { - log.Printf("[INFO][initConnInfo] error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr) - logger.Info().Int("vmid", vmr.VmId()).Msgf("error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr) - return diag.FromErr(fmt.Errorf("error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr)) + IPs, agentDiags := getPrimaryIP(config, vmr, client, d, guestAgentWaitEnd, ciAgentEnabled, d.Get("skip_ipv4").(bool), d.Get("skip_ipv6").(bool)) + if len(agentDiags) > 0 { + return append(diags, agentDiags...) } - vmConfig, err := client.GetVmConfig(vmr) - if err != nil { - return diag.FromErr(err) - } - log.Print("[INFO][initConnInfo] trying to find IP address of first network card") - logger.Info().Int("vmid", vmr.VmId()).Msgf("trying to find IP address of first network card") - // wait until we find a valid ipv4 address - log.Printf("[DEBUG][initConnInfo] checking network card...") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card...") - for guestAgentRunning && time.Now().Before(guestAgentWaitEnd) { - log.Printf("[DEBUG][initConnInfo] checking network card...") - interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) - net0MacAddress := macAddressRegex.FindString(vmConfig["net0"].(string)) - if err != nil { - log.Printf("[DEBUG][initConnInfo] checking network card error %s", err.Error()) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card error %s", err.Error()) - // return err - } else { - log.Printf("[DEBUG][initConnInfo] checking network card loop") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card loop") - for _, iface := range interfaces { - if strings.EqualFold(strings.ToUpper(iface.MACAddress), strings.ToUpper(net0MacAddress)) { - for _, addr := range iface.IPAddresses { - if addr.IsGlobalUnicast() && strings.Count(addr.String(), ":") < 2 { - log.Printf("[DEBUG][initConnInfo] Found IP address: %s", addr.String()) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("Found IP address: %s", addr.String()) - sshHost = addr.String() - } - } - } - } - if sshHost != "" { - log.Printf("[DEBUG][initConnInfo] sshHost not empty: %s", sshHost) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("sshHost not empty: %s", sshHost) - break - } - } - // wait before next try - time.Sleep(time.Duration(d.Get("additional_wait").(int)) * time.Second) + if IPs.IPv4 != "" { + sshHost = IPs.IPv4 + } else if IPs.IPv6 != "" { + sshHost = IPs.IPv6 } - // todo - log a warning if we couldn't get an IP - 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 { - 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) - if err != nil { - log.Printf("[DEBUG][initConnInfo] vmstate error %s", err.Error()) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("vmstate error %s", err.Error()) - 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)) - 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) - if err != nil { - return diag.FromErr(err) - } else { - for _, iface := range interfaces { - if sshHost == ipMatch[1] { - break - } - for _, addr := range iface.IPAddresses { - if addr.Equal(ipconfig0) { - sshHost = ipMatch[1] - break - } - } - } - } - } - } - - log.Print("[DEBUG][initConnInfo] found an ip configuration") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("Found an ip configuration") - // Check if we got a specified port - if strings.Contains(sshHost, ":") { - sshParts := strings.Split(sshHost, ":") - sshHost = sshParts[0] - sshPort = sshParts[1] - } - } if sshHost == "" { log.Print("[DEBUG][initConnInfo] Cannot find any IP address") logger.Debug().Int("vmid", vmr.VmId()).Msgf("Cannot find any IP address") @@ -1994,12 +1903,10 @@ func initConnInfo(ctx context.Context, logger.Debug().Int("vmid", vmr.VmId()).Msgf("this is the vm configuration: %s %s", sshHost, sshPort) // Optional convenience attributes for provisioners - err = d.Set("default_ipv4_address", sshHost) - diags = append(diags, diag.FromErr(err)...) - err = d.Set("ssh_host", sshHost) - diags = append(diags, diag.FromErr(err)...) - err = d.Set("ssh_port", sshPort) - diags = append(diags, diag.FromErr(err)...) + _ = d.Set("default_ipv4_address", sshHost) + _ = d.Set("default_ipv6_address", sshHost) + _ = d.Set("ssh_host", sshHost) + _ = d.Set("ssh_port", sshPort) // This connection INFO is longer shared up to the providers :-( d.SetConnInfo(map[string]string{ @@ -2010,6 +1917,78 @@ func initConnInfo(ctx context.Context, return diags } +func getPrimaryIP(config *pxapi.ConfigQemu, vmr *pxapi.VmRef, client *pxapi.Client, d *schema.ResourceData, endTime time.Time, ciAgentEnabled, skipIPv4 bool, skipIPv6 bool) (primaryIPs, diag.Diagnostics) { + var diags diag.Diagnostics + logger, _ := CreateSubLogger("getPrimaryIP") + // TODO allow the primary interface to be a different one than the first + + conn := connectionInfo{ + SkipIPv4: skipIPv4, + SkipIPv6: skipIPv6, + } + // check if cloud init is enabled + if config.HasCloudInit() { + logger.Debug().Int("vmid", vmr.VmId()).Msgf(" vm has a cloud-init configuration") + CiInterface := d.Get("ipconfig0") + conn = parseCloudInitInterface(CiInterface.(string), conn.SkipIPv4, conn.SkipIPv6) + // early return, we have all information we wanted + if conn.hasRequiredIP() { + if conn.IPs.IPv4 == "" && conn.IPs.IPv6 == "" { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Cloud-init is enabled but no IP config is set", + Detail: "Cloud-init is enabled in your configuration but no static IP address is set, nor is the DHCP option enabled", + AttributePath: cty.Path{}}) + } + return conn.IPs, diags + } + } + + var try uint + // get all information we can from qemu agent until the timer runs out + var err error + if ciAgentEnabled { + var vmConfig map[string]interface{} + vmConfig, err = client.GetVmConfig(vmr) + if err != nil { + return conn.IPs, append(diags, diag.FromErr(err)...) + } + net0MacAddress := macAddressRegex.FindString(vmConfig["net0"].(string)) + for time.Now().Before(endTime) { + var interfaces []pxapi.AgentNetworkInterface + interfaces, err = vmr.GetAgentInformation(client, false) + if err != nil { + if !strings.Contains(err.Error(), ErrorGuestAgentNotRunning) { + return conn.IPs, append(diags, diag.FromErr(err)...) + } + logger.Debug().Int("vmid", vmr.VmId()).Msgf("check ip result error %s", err.Error()) + } else { // vm is running and reachable + if len(interfaces) > 0 { // agent returned some information + logger.Info().Int("vmid", vmr.VmId()).Msgf("found working QEMU Agent") + logger.Debug().Int("vmid", vmr.VmId()).Msgf("interfaces found: %v", interfaces) + conn := conn.parsePrimaryIPs(interfaces, net0MacAddress) + if conn.hasRequiredIP() { + return conn.IPs, diags + } + } + if try > maxAgentTry { + break + } + try += 1 + } + time.Sleep(time.Duration(d.Get("additional_wait").(int)) * time.Second) + } + } + if err != nil && strings.Contains(err.Error(), ErrorGuestAgentNotRunning) { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Qemu Guest Agent is enabled but not working", + Detail: fmt.Sprintf("error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", err), + AttributePath: cty.Path{}}) + } + return conn.IPs, diags +} + func setCloudInitDisk(d *schema.ResourceData, config *pxapi.ConfigQemu) { storage := d.Get("cloudinit_cdrom_storage").(string) if storage != "" { diff --git a/proxmox/util.go b/proxmox/util.go index 1e766806..544d1bc8 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -21,8 +21,6 @@ var rxRsId = regexp.MustCompile(`([^/]+)/([^/]+)/(\d+)`) var rxClusterRsId = regexp.MustCompile(`([^/]+)/([^/]+)`) -var rxIPconfig = regexp.MustCompile(`ip6?=([0-9a-fA-F:\\.]+)`) - var macAddressRegex = regexp.MustCompile(`([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}`) var machineModelsRegex = regexp.MustCompile(`(^pc|^q35|^virt)`)