Skip to content

Commit f24add3

Browse files
authored
feat(vm,lxc): add wait_for_ip configuration for agent/network interfaces (#2362)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
1 parent 9353dde commit f24add3

File tree

14 files changed

+434
-80
lines changed

14 files changed

+434
-80
lines changed

docs/resources/virtual_environment_container.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ output "ubuntu_container_public_key" {
249249
- `timeout_delete` - (Optional) Timeout for deleting a container in seconds (defaults to 60).
250250
- `timeout_update` - (Optional) Timeout for updating a container in seconds (defaults to 1800).
251251
- `unprivileged` - (Optional) Whether the container runs as unprivileged on the host (defaults to `false`).
252+
- `wait_for_ip` - (Optional) Configuration for waiting for specific IP address types when the container starts.
253+
- `ipv4` - (Optional) Wait for at least one IPv4 address (non-loopback, non-link-local) (defaults to `false`).
254+
- `ipv6` - (Optional) Wait for at least one IPv6 address (non-loopback, non-link-local) (defaults to `false`).
255+
256+
When `wait_for_ip` is not specified or both `ipv4` and `ipv6` are `false`, the provider waits for any valid global unicast address (IPv4 or IPv6). In dual-stack networks where DHCPv6 responds faster, this may result in only IPv6 addresses being available. Set `ipv4 = true` to ensure IPv4 address availability.
252257
- `vm_id` - (Optional) The container identifier
253258
- `features` - (Optional) The container feature flags. Changing flags (except nesting) is only allowed for `root@pam` authenticated user.
254259
- `nesting` - (Optional) Whether the container is nested (defaults to `false`)

docs/resources/virtual_environment_vm.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ output "ubuntu_vm_public_key" {
137137
- `type` - (Optional) The QEMU agent interface type (defaults to `virtio`).
138138
- `isa` - ISA Serial Port.
139139
- `virtio` - VirtIO (paravirtualized).
140+
- `wait_for_ip` - (Optional) Configuration for waiting for specific IP address types when the VM starts.
141+
- `ipv4` - (Optional) Wait for at least one IPv4 address (non-loopback, non-link-local) (defaults to `false`).
142+
- `ipv6` - (Optional) Wait for at least one IPv6 address (non-loopback, non-link-local) (defaults to `false`).
143+
144+
When `wait_for_ip` is not specified or both `ipv4` and `ipv6` are `false`, the provider waits for any valid global unicast address (IPv4 or IPv6). In dual-stack networks where DHCPv6 responds faster, this may result in only IPv6 addresses being available. Set `ipv4 = true` to ensure IPv4 address availability.
140145
- `amd_sev` - (Optional) Secure Encrypted Virtualization (SEV) features by AMD CPUs.
141146
- `type` - (Optional) Enable standard SEV with `std` or enable experimental SEV-ES with the `es` option or enable experimental SEV-SNP with the `snp` option (defaults to `std`).
142147
- `allow_smt` - (Optional) Sets policy bit to allow Simultaneous Multi Threading (SMT)

example/resource_virtual_environment_container.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ resource "proxmox_virtual_environment_container" "example_template" {
33

44
start_on_boot = "true"
55

6+
wait_for_ip {
7+
ipv4 = true
8+
}
9+
610
disk {
711
datastore_id = var.virtual_environment_storage
812
size = 4
@@ -73,6 +77,10 @@ resource "proxmox_virtual_environment_container" "example" {
7377
vm_id = proxmox_virtual_environment_container.example_template.id
7478
}
7579

80+
wait_for_ip {
81+
ipv4 = true
82+
}
83+
7684
tags = [
7785
"container",
7886
]

example/resource_virtual_environment_trunks.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ resource "proxmox_virtual_environment_vm" "trunks-example" {
4040

4141
agent {
4242
enabled = true
43+
wait_for_ip {
44+
ipv4 = true
45+
}
4346
}
4447

4548
serial_device {}

example/resource_virtual_environment_vm.tf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ locals {
55
resource "proxmox_virtual_environment_vm" "example_template" {
66
agent {
77
enabled = true
8+
wait_for_ip {
9+
ipv4 = true
10+
}
811
}
912

1013
bios = "ovmf"
@@ -147,6 +150,13 @@ resource "proxmox_virtual_environment_vm" "example" {
147150
# memory = 768
148151
# }
149152

153+
agent {
154+
enabled = true
155+
wait_for_ip {
156+
ipv4 = true
157+
}
158+
}
159+
150160
connection {
151161
type = "ssh"
152162
agent = false

fwprovider/test/resource_vm_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,75 @@ func TestAccResourceVMNetwork(t *testing.T) {
845845
}),
846846
),
847847
}}},
848+
{"wait for IPv4 address", []resource.TestStep{{
849+
Config: te.RenderConfig(`
850+
resource "proxmox_virtual_environment_file" "cloud_config" {
851+
content_type = "snippets"
852+
datastore_id = "local"
853+
node_name = "{{.NodeName}}"
854+
source_raw {
855+
data = <<-EOF
856+
#cloud-config
857+
runcmd:
858+
- apt update
859+
- apt install -y qemu-guest-agent
860+
- systemctl enable qemu-guest-agent
861+
- systemctl start qemu-guest-agent
862+
EOF
863+
file_name = "cloud-config.yaml"
864+
}
865+
}
866+
867+
resource "proxmox_virtual_environment_vm" "test_vm_wait_ipv4" {
868+
node_name = "{{.NodeName}}"
869+
started = true
870+
agent {
871+
enabled = true
872+
wait_for_ip {
873+
ipv4 = true
874+
}
875+
}
876+
cpu {
877+
cores = 2
878+
}
879+
memory {
880+
dedicated = 2048
881+
}
882+
disk {
883+
datastore_id = "local-lvm"
884+
file_id = proxmox_virtual_environment_download_file.ubuntu_cloud_image.id
885+
interface = "virtio0"
886+
iothread = true
887+
discard = "on"
888+
size = 20
889+
}
890+
initialization {
891+
ip_config {
892+
ipv4 {
893+
address = "dhcp"
894+
}
895+
}
896+
user_data_file_id = proxmox_virtual_environment_file.cloud_config.id
897+
}
898+
network_device {
899+
bridge = "vmbr0"
900+
}
901+
}
902+
903+
resource "proxmox_virtual_environment_download_file" "ubuntu_cloud_image" {
904+
content_type = "iso"
905+
datastore_id = "local"
906+
node_name = "{{.NodeName}}"
907+
url = "{{.CloudImagesServer}}/minimal/releases/noble/release/ubuntu-24.04-minimal-cloudimg-amd64.img"
908+
overwrite_unmanaged = true
909+
}`),
910+
Check: resource.ComposeTestCheckFunc(
911+
ResourceAttributes("proxmox_virtual_environment_vm.test_vm_wait_ipv4", map[string]string{
912+
"ipv4_addresses.#": "2",
913+
"agent.0.wait_for_ip.0.ipv4": "true",
914+
}),
915+
),
916+
}}},
848917
{"network device disconnected", []resource.TestStep{
849918
{
850919
Config: te.RenderConfig(`

proxmox/nodes/containers/containers.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func (c *Client) ListContainers(ctx context.Context) ([]*ListResponseData, error
162162
func (c *Client) WaitForContainerNetworkInterfaces(
163163
ctx context.Context,
164164
timeout time.Duration,
165+
waitForIPConfig *WaitForIPConfig, // configuration for which IP types to wait for (nil = wait for any global unicast)
165166
) ([]GetNetworkInterfacesData, error) {
166167
errNoIPsYet := errors.New("no ips yet")
167168

@@ -175,17 +176,36 @@ func (c *Client) WaitForContainerNetworkInterfaces(
175176
return nil, err
176177
}
177178

178-
for _, iface := range ifaces {
179-
if iface.Name != "lo" && iface.IPAddresses != nil && len(*iface.IPAddresses) > 0 {
180-
for _, ipAddr := range *iface.IPAddresses {
181-
if ip.IsValidGlobalUnicast(ipAddr.Address) {
182-
return ifaces, nil
183-
}
184-
}
179+
hasIPv4, hasIPv6 := c.checkIPAddresses(ifaces)
180+
181+
if waitForIPConfig == nil {
182+
// backward compatibility: wait for any valid global unicast address
183+
if !hasIPv4 && !hasIPv6 {
184+
return nil, errNoIPsYet
185+
}
186+
187+
return ifaces, nil
188+
}
189+
190+
requiredIPv4 := waitForIPConfig.IPv4
191+
requiredIPv6 := waitForIPConfig.IPv6
192+
193+
// if no specific requirements, wait for any IP (backward compatibility)
194+
if !requiredIPv4 && !requiredIPv6 {
195+
if !hasIPv4 && !hasIPv6 {
196+
return nil, errNoIPsYet
185197
}
198+
199+
return ifaces, nil
186200
}
187201

188-
return nil, errNoIPsYet
202+
// check if all required IP types are available
203+
if (requiredIPv4 && !hasIPv4) || (requiredIPv6 && !hasIPv6) {
204+
return nil, errNoIPsYet
205+
}
206+
207+
// all required IP types are available
208+
return ifaces, nil
189209
},
190210
retry.Context(ctxWithTimeout),
191211
retry.RetryIf(func(err error) bool {
@@ -217,6 +237,34 @@ func (c *Client) WaitForContainerNetworkInterfaces(
217237
return ifaces, nil
218238
}
219239

240+
// checkIPAddresses checks network interfaces for valid IP addresses and returns whether IPv4 and IPv6 are present.
241+
func (c *Client) checkIPAddresses(
242+
ifaces []GetNetworkInterfacesData,
243+
) (bool, bool) {
244+
hasIPv4 := false
245+
hasIPv6 := false
246+
247+
for _, iface := range ifaces {
248+
if iface.Name == "lo" || iface.IPAddresses == nil || len(*iface.IPAddresses) == 0 {
249+
continue
250+
}
251+
252+
for _, ipAddr := range *iface.IPAddresses {
253+
if !ip.IsValidGlobalUnicast(ipAddr.Address) {
254+
continue
255+
}
256+
257+
if ip.IsIPv4(ipAddr.Address) {
258+
hasIPv4 = true
259+
} else if ip.IsIPv6(ipAddr.Address) {
260+
hasIPv6 = true
261+
}
262+
}
263+
}
264+
265+
return hasIPv4, hasIPv6
266+
}
267+
220268
// RebootContainer reboots a container.
221269
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
222270
taskID, err := c.RebootContainerAsync(ctx, d)

proxmox/nodes/containers/containers_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ var (
2727
regexMountPointKey = regexp.MustCompile(`^mp\d+$`)
2828
)
2929

30+
// WaitForIPConfig specifies which IP address types to wait for when waiting for network interfaces.
31+
type WaitForIPConfig struct {
32+
IPv4 bool // Wait for at least one IPv4 address (non-loopback, non-link-local)
33+
IPv6 bool // Wait for at least one IPv6 address (non-loopback, non-link-local)
34+
}
35+
3036
// CloneRequestBody contains the data for an container clone request.
3137
type CloneRequestBody struct {
3238
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`

0 commit comments

Comments
 (0)