Skip to content

Commit 516842d

Browse files
authored
Replace VM resource's wait_for_ip functionality to validate it whether the ip(s) match a given prefix (#305)
* Remove wait_for_ip top level argument in favor of network block scoped expected_ip_cidr Signed-off-by: Dom Del Nano <[email protected]> * Implement logic for matching against multiple network interface cidr ranges Signed-off-by: Dom Del Nano <[email protected]> * Implement cidr matching for multiple network interfaces and start test for verifying the lack of a match * Implement wait for ip test for the case where a network does not converge to the cidr match Signed-off-by: Dom Del Nano <[email protected]> * Changes to fix V0 state migration test Signed-off-by: Dom Del Nano <[email protected]> * Implement test that verifies migrating from wait_for_ip to expected_cidr_range works as expected Signed-off-by: Dom Del Nano <[email protected]> * Fix issue with failing test and update docs Signed-off-by: Dom Del Nano <[email protected]> --------- Signed-off-by: Dom Del Nano <[email protected]>
1 parent 4ff4181 commit 516842d

File tree

8 files changed

+339
-138
lines changed

8 files changed

+339
-138
lines changed

client/vm.go

+112-50
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"log"
9+
"net"
910
"net/http"
1011
"os"
1112
"strconv"
@@ -135,15 +136,17 @@ type Vm struct {
135136
// These fields are used for passing in disk inputs when
136137
// creating Vms, however, this is not a real field as far
137138
// as the XO api or XAPI is concerned
138-
Disks []Disk `json:"-"`
139-
CloudNetworkConfig string `json:"-"`
140-
VIFsMap []map[string]string `json:"-"`
141-
WaitForIps bool `json:"-"`
142-
Installation Installation `json:"-"`
143-
ManagementAgentDetected bool `json:"managementAgentDetected"`
144-
PVDriversDetected bool `json:"pvDriversDetected"`
145-
DestroyCloudConfigVdiAfterBoot bool `json:"-"`
146-
CloneType string `json:"-"`
139+
Disks []Disk `json:"-"`
140+
CloudNetworkConfig string `json:"-"`
141+
VIFsMap []map[string]string `json:"-"`
142+
// Map where the key is the network interface index and the
143+
// value is a cidr range parsable by net.ParseCIDR
144+
WaitForIps map[string]string `json:"-"`
145+
Installation Installation `json:"-"`
146+
ManagementAgentDetected bool `json:"managementAgentDetected"`
147+
PVDriversDetected bool `json:"pvDriversDetected"`
148+
DestroyCloudConfigVdiAfterBoot bool `json:"-"`
149+
CloneType string `json:"-"`
147150
}
148151

149152
type Installation struct {
@@ -603,58 +606,117 @@ func (c *Client) waitForVmState(id string, stateConf StateChangeConf) error {
603606
return err
604607
}
605608

606-
func (c *Client) waitForModifyVm(id string, desiredPowerState string, waitForIp bool, timeout time.Duration) error {
607-
if !waitForIp {
608-
var pending []string
609-
target := desiredPowerState
610-
switch desiredPowerState {
611-
case RunningPowerState:
612-
pending = []string{HaltedPowerState}
613-
case HaltedPowerState:
614-
pending = []string{RunningPowerState}
615-
default:
616-
return errors.New(fmt.Sprintf("Invalid VM power state requested: %s\n", desiredPowerState))
609+
func waitForPowerStateReached(c *Client, vmId, desiredPowerState string, timeout time.Duration) error {
610+
var pending []string
611+
target := desiredPowerState
612+
switch desiredPowerState {
613+
case RunningPowerState:
614+
pending = []string{HaltedPowerState}
615+
case HaltedPowerState:
616+
pending = []string{RunningPowerState}
617+
default:
618+
return errors.New(fmt.Sprintf("Invalid VM power state requested: %s\n", desiredPowerState))
619+
}
620+
refreshFn := func() (result interface{}, state string, err error) {
621+
vm, err := c.GetVm(Vm{Id: vmId})
622+
623+
if err != nil {
624+
return vm, "", err
617625
}
618-
refreshFn := func() (result interface{}, state string, err error) {
619-
vm, err := c.GetVm(Vm{Id: id})
620626

621-
if err != nil {
622-
return vm, "", err
623-
}
627+
return vm, vm.PowerState, nil
628+
}
629+
stateConf := &StateChangeConf{
630+
Pending: pending,
631+
Refresh: refreshFn,
632+
Target: []string{target},
633+
Timeout: timeout,
634+
}
635+
_, err := stateConf.WaitForState()
636+
return err
637+
}
638+
639+
type ifaceMatchCheck struct {
640+
cidrRange string
641+
ifaceIdx string
642+
ifaceAddrs []string
643+
}
624644

625-
return vm, vm.PowerState, nil
645+
func waitForIPAssignment(c *Client, vmId string, waitForIps map[string]string, timeout time.Duration) error {
646+
var lastResult ifaceMatchCheck
647+
refreshFn := func() (result interface{}, state string, err error) {
648+
vm, err := c.GetVm(Vm{Id: vmId})
649+
650+
if err != nil {
651+
return vm, "", err
626652
}
627-
stateConf := &StateChangeConf{
628-
Pending: pending,
629-
Refresh: refreshFn,
630-
Target: []string{target},
631-
Timeout: timeout,
653+
654+
addrs := vm.Addresses
655+
if len(addrs) == 0 || vm.PowerState != RunningPowerState {
656+
return addrs, "Waiting", nil
632657
}
633-
_, err := stateConf.WaitForState()
634-
return err
635-
} else {
636-
refreshFn := func() (result interface{}, state string, err error) {
637-
vm, err := c.GetVm(Vm{Id: id})
638658

639-
if err != nil {
640-
return vm, "", err
659+
netIfaces := map[string][]string{}
660+
for key, addr := range vm.Addresses {
661+
662+
// key has the following format "{iface_id}/(ipv4|ipv6)/{iface_ip_id}"
663+
ifaceIdx, _, _ := strings.Cut(key, "/")
664+
if _, ok := netIfaces[ifaceIdx]; !ok {
665+
netIfaces[ifaceIdx] = []string{}
641666
}
667+
netIfaces[ifaceIdx] = append(netIfaces[ifaceIdx], addr)
668+
}
642669

643-
l := len(vm.Addresses)
644-
if l == 0 || vm.PowerState != RunningPowerState {
645-
return vm, "Waiting", nil
670+
for ifaceIdx, cidrRange := range waitForIps {
671+
// VM's Addresses member does not contain this network interface yet
672+
if _, ok := netIfaces[ifaceIdx]; !ok {
673+
return addrs, "Waiting", nil
646674
}
647675

648-
return vm, "Ready", nil
649-
}
650-
stateConf := &StateChangeConf{
651-
Pending: []string{"Waiting"},
652-
Refresh: refreshFn,
653-
Target: []string{"Ready"},
654-
Timeout: timeout,
676+
found := false
677+
for _, ipAddr := range netIfaces[ifaceIdx] {
678+
_, ipNet, err := net.ParseCIDR(cidrRange)
679+
680+
if err != nil {
681+
return addrs, "Waiting", err
682+
}
683+
684+
if ipNet.Contains(net.ParseIP(ipAddr)) {
685+
found = true
686+
}
687+
}
688+
689+
if !found {
690+
lastResult = ifaceMatchCheck{
691+
cidrRange: cidrRange,
692+
ifaceIdx: ifaceIdx,
693+
ifaceAddrs: netIfaces[ifaceIdx],
694+
}
695+
696+
return addrs, "Waiting", nil
697+
}
655698
}
656-
_, err := stateConf.WaitForState()
657-
return err
699+
700+
return addrs, "Ready", nil
701+
}
702+
stateConf := &StateChangeConf{
703+
Pending: []string{"Waiting"},
704+
Refresh: refreshFn,
705+
Target: []string{"Ready"},
706+
Timeout: timeout,
707+
}
708+
_, err := stateConf.WaitForState()
709+
if _, ok := err.(*TimeoutError); ok {
710+
return errors.New(fmt.Sprintf("network[%s] never converged to the following cidr: %s, addresses: %s failed to match", lastResult.ifaceIdx, lastResult.cidrRange, lastResult.ifaceAddrs))
711+
}
712+
return err
713+
}
714+
715+
func (c *Client) waitForModifyVm(id string, desiredPowerState string, waitForIps map[string]string, timeout time.Duration) error {
716+
if len(waitForIps) == 0 {
717+
return waitForPowerStateReached(c, id, desiredPowerState, timeout)
718+
} else {
719+
return waitForIPAssignment(c, id, waitForIps, timeout)
658720
}
659721
}
660722

docs/data-sources/vms.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ Read-Only:
8484
- `template` (String)
8585
- `vga` (String)
8686
- `videoram` (Number)
87-
- `wait_for_ip` (Boolean)
8887
- `xenstore` (Map of String)
8988

9089
<a id="nestedobjatt--vms--disk"></a>
@@ -109,6 +108,7 @@ Read-Only:
109108

110109
- `attached` (Boolean)
111110
- `device` (String)
111+
- `expected_ip_cidr` (String)
112112
- `ipv4_addresses` (List of String)
113113
- `ipv6_addresses` (List of String)
114114
- `mac_address` (String)

docs/resources/vm.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,14 @@ resource "xenorchestra_vm" "bar" {
9898
}
9999
}
100100
101-
# vm resource that uses wait_for_ip
101+
# vm resource that waits until its first network interface
102+
# is assigned an IP via DHCP
102103
resource "xenorchestra_vm" "vm" {
103104
...
104-
wait_for_ip = true
105105
# Specify VM with two network interfaces
106106
network {
107107
...
108+
expected_ip_cidr = "10.0.0.0/16"
108109
}
109110
network {
110111
...
@@ -176,14 +177,13 @@ $ xo-cli xo.getAllObjects filter='json:{"id": "cf7b5d7d-3cd5-6b7c-5025-5c935c8cd
176177
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
177178
- `vga` (String) The video adapter the VM should use. Possible values include std and cirrus.
178179
- `videoram` (Number) The videoram option the VM should use. Possible values include 1, 2, 4, 8, 16
179-
- `wait_for_ip` (Boolean) Whether terraform should wait until IP addresses are present on the VM's network interfaces before considering it created. This only works if guest-tools are installed in the VM. Defaults to false.
180180
- `xenstore` (Map of String) The key value pairs to be populated in xenstore.
181181

182182
### Read-Only
183183

184184
- `id` (String) The ID of this resource.
185-
- `ipv4_addresses` (List of String) This is only accessible if guest-tools is installed in the VM and if `wait_for_ip` is set to true. This will contain a list of the ipv4 addresses across all network interfaces in order. See the example terraform code for more details.
186-
- `ipv6_addresses` (List of String) This is only accessible if guest-tools is installed in the VM and if `wait_for_ip` is set to true. This will contain a list of the ipv6 addresses across all network interfaces in order.
185+
- `ipv4_addresses` (List of String) This is only accessible if guest-tools is installed in the VM and if `expected_ip_cidr` is set on any network interfaces. This will contain a list of the ipv4 addresses across all network interfaces in order. See the example terraform code for more details.
186+
- `ipv6_addresses` (List of String) This is only accessible if guest-tools is installed in the VM and if `expected_ip_cidr` is set on any network interfaces. This will contain a list of the ipv6 addresses across all network interfaces in order.
187187

188188
<a id="nestedblock--disk"></a>
189189
### Nested Schema for `disk`
@@ -216,6 +216,7 @@ Required:
216216
Optional:
217217

218218
- `attached` (Boolean) Whether the device should be attached to the VM.
219+
- `expected_ip_cidr` (String) Determines the IP cidr range terraform should watch for on this network interface. Resource creation is not complete until the IP address converges to the specified range. This only works if guest-tools are installed in the VM. Defaults to "", which skips IP address matching.
219220
- `mac_address` (String) The mac address of the network interface. This must be parsable by go's [net.ParseMAC function](https://golang.org/pkg/net/#ParseMAC). All mac addresses are stored in Terraform's state with [HardwareAddr's string representation](https://golang.org/pkg/net/#HardwareAddr.String) i.e. 00:00:5e:00:53:01
220221

221222
Read-Only:

examples/resources/xenorchestra_vm/resource.tf

+3-2
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,14 @@ resource "xenorchestra_vm" "bar" {
6767
}
6868
}
6969

70-
# vm resource that uses wait_for_ip
70+
# vm resource that waits until its first network interface
71+
# is assigned an IP via DHCP
7172
resource "xenorchestra_vm" "vm" {
7273
...
73-
wait_for_ip = true
7474
# Specify VM with two network interfaces
7575
network {
7676
...
77+
expected_ip_cidr = "10.0.0.0/16"
7778
}
7879
network {
7980
...

xoa/data_source_vms.go

-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ func vmToMapList(vms []client.Vm) []map[string]interface{} {
8484
"memory_max": vm.Memory.Static[1],
8585
"affinity_host": vm.AffinityHost,
8686
"template": vm.Template,
87-
"wait_for_ip": vm.WaitForIps,
8887
"high_availability": vm.HA,
8988
"ipv4_addresses": ipv4,
9089
"ipv6_addresses": ipv6,

xoa/internal/state/migrate.go

+79-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66
"log"
77
"net"
88

9-
"github.com/vatesfr/terraform-provider-xenorchestra/client"
109
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1110
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11+
"github.com/vatesfr/terraform-provider-xenorchestra/client"
1212
)
1313

1414
var validVga = []string{
@@ -362,3 +362,81 @@ func suppressAttachedDiffWhenHalted(k, old, new string, d *schema.ResourceData)
362362
log.Printf("[DEBUG] VM '%s' attribute has transitioned from '%s' to '%s' when PowerState '%s'. Suppress diff: %t", k, old, new, powerState, suppress)
363363
return
364364
}
365+
366+
// Used for tests that require the schema of the provider <= v0.28.1
367+
func V1TestAccVmConfigWithWaitForIp(testPrefix, vmName, templateName, netName, poolId, srId string) string {
368+
return fmt.Sprintf(`
369+
resource "xenorchestra_cloud_config" "bar" {
370+
name = "%s-vm-template-%s"
371+
template = "template" # should this be templated?
372+
}
373+
374+
data "xenorchestra_template" "template" {
375+
name_label = "%s"
376+
pool_id = "%s"
377+
}
378+
379+
data "xenorchestra_network" "network" {
380+
name_label = "%s"
381+
pool_id = "%s"
382+
}
383+
resource "xenorchestra_vm" "bar" {
384+
memory_max = 4295000000
385+
cpus = 1
386+
cloud_config = xenorchestra_cloud_config.bar.template
387+
name_label = "%s"
388+
name_description = "description"
389+
template = data.xenorchestra_template.template.id
390+
network {
391+
network_id = data.xenorchestra_network.network.id
392+
}
393+
disk {
394+
sr_id = "%s"
395+
name_label = "disk 1"
396+
size = 10001317888
397+
}
398+
wait_for_ip = true
399+
}
400+
`, testPrefix, vmName, templateName, poolId, netName, poolId, vmName, srId)
401+
}
402+
403+
// terraform configuration that can be used to block changes that should not destroy a VM.
404+
// While this doesn't integrate nicely with the sdk's test helpers (failure is vague), there
405+
// are some cases were options are limited (testing pinned provider versions).
406+
func TestAccV1VmConfigWithDeletionBlocked(testPrefix, vmName, templateName, netName, poolId, srId, waitForIp string) string {
407+
return fmt.Sprintf(`
408+
resource "xenorchestra_cloud_config" "bar" {
409+
name = "%s-vm-template-%s"
410+
template = "template" # should this be templated?
411+
}
412+
413+
data "xenorchestra_template" "template" {
414+
name_label = "%s"
415+
pool_id = "%s"
416+
}
417+
data "xenorchestra_network" "network" {
418+
name_label = "%s"
419+
pool_id = "%s"
420+
}
421+
422+
resource "xenorchestra_vm" "bar" {
423+
memory_max = 4295000000
424+
cpus = 1
425+
cloud_config = xenorchestra_cloud_config.bar.template
426+
name_label = "%s"
427+
name_description = "description"
428+
template = data.xenorchestra_template.template.id
429+
network {
430+
network_id = data.xenorchestra_network.network.id
431+
}
432+
433+
disk {
434+
sr_id = "%s"
435+
name_label = "disk 1"
436+
size = 10001317888
437+
}
438+
wait_for_ip = %s
439+
blocked_operations = ["destroy"]
440+
}
441+
`, testPrefix, vmName, templateName, poolId, netName, poolId, vmName, srId, waitForIp)
442+
}

0 commit comments

Comments
 (0)