From dc8bb120098055647a4ea1377697191aab76089f Mon Sep 17 00:00:00 2001 From: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> Date: Tue, 17 May 2022 08:08:00 +0200 Subject: [PATCH] Fixes #366: Added placement policy options to ovirt_vm --- docs/resources/vm.md | 2 + ovirt/resource_ovirt_vm.go | 59 +++++++++++++++ ovirt/resource_ovirt_vm_test.go | 128 +++++++++++++++++++++++++++++--- 3 files changed, 179 insertions(+), 10 deletions(-) diff --git a/docs/resources/vm.md b/docs/resources/vm.md index c492dd93..52bb39ee 100644 --- a/docs/resources/vm.md +++ b/docs/resources/vm.md @@ -37,6 +37,8 @@ resource "ovirt_vm" "test" { - `cpu_sockets` (Number) Number of CPU sockets to allocate to the VM. If set, cpu_cores and cpu_threads must also be specified. - `cpu_threads` (Number) Number of CPU threads to allocate to the VM. If set, cpu_cores and cpu_sockets must also be specified. - `os_type` (String) Operating system type. +- `placement_policy_affinity` (String) Affinity for placement policies. Must be one of: migratable, pinned, user_migratable +- `placement_policy_host_ids` (Set of String) List of hosts to pin the VM to. ### Read-Only diff --git a/ovirt/resource_ovirt_vm.go b/ovirt/resource_ovirt_vm.go index 086545f0..0ef3bf1f 100644 --- a/ovirt/resource_ovirt_vm.go +++ b/ovirt/resource_ovirt_vm.go @@ -76,6 +76,31 @@ var vmSchema = map[string]*schema.Schema{ ForceNew: true, Description: "Operating system type.", }, + "placement_policy_affinity": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"placement_policy_host_ids"}, + Description: "Affinity for placement policies. Must be one of: " + strings.Join(vmAffinityValues(), ", "), + ValidateDiagFunc: validateEnum(vmAffinityValues()), + }, + "placement_policy_host_ids": { + Type: schema.TypeSet, + Optional: true, + RequiredWith: []string{"placement_policy_affinity"}, + Description: "List of hosts to pin the VM to.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, +} + +func vmAffinityValues() []string { + values := ovirtclient.VMAffinityValues() + result := make([]string, len(values)) + for i, a := range values { + result[i] = string(a) + } + return result } func (p *provider) vmResource() *schema.Resource { @@ -130,6 +155,36 @@ func (p *provider) vmCreate( } params.WithOS(osParams) } + placementPolicyBuilder := ovirtclient.NewVMPlacementPolicyParameters() + hasPlacementPolicy := false + var err error + if a, ok := data.GetOk("placement_policy_affinity"); ok && a != "" { + affinity := ovirtclient.VMAffinity(a.(string)) + if err := affinity.Validate(); err != nil { + return errorToDiags("create VM", err) + } + placementPolicyBuilder, err = placementPolicyBuilder.WithAffinity(affinity) + if err != nil { + return errorToDiags("add affinity to placement policy", err) + } + hasPlacementPolicy = true + } + if hIDs, ok := data.GetOk("placement_policy_host_ids"); ok { + hIDList := hIDs.(*schema.Set).List() + hostIDs := make([]ovirtclient.HostID, len(hIDList)) + for i, hostID := range hIDList { + hostIDs[i] = ovirtclient.HostID(hostID.(string)) + } + placementPolicyBuilder, err = placementPolicyBuilder.WithHostIDs(hostIDs) + if err != nil { + return errorToDiags("add host IDs to placement policy", err) + } + hasPlacementPolicy = true + } + if hasPlacementPolicy { + params = params.WithPlacementPolicy(placementPolicyBuilder) + } + vm, err := client.CreateVM( ovirtclient.ClusterID(clusterID), ovirtclient.TemplateID(templateID), @@ -185,6 +240,10 @@ func vmResourceUpdate(vm ovirtclient.VMData, data *schema.ResourceData) diag.Dia if _, ok := data.GetOk("os_type"); ok || vm.OS().Type() != "other" { diags = setResourceField(data, "os_type", vm.OS().Type(), diags) } + if pp, ok := vm.PlacementPolicy(); ok { + diags = setResourceField(data, "placement_policy_host_ids", pp.HostIDs(), diags) + diags = setResourceField(data, "placement_policy_affinity", pp.Affinity(), diags) + } return diags } diff --git a/ovirt/resource_ovirt_vm_test.go b/ovirt/resource_ovirt_vm_test.go index 6b305e22..cddf46d8 100644 --- a/ovirt/resource_ovirt_vm_test.go +++ b/ovirt/resource_ovirt_vm_test.go @@ -1,8 +1,10 @@ package ovirt import ( + "context" "fmt" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -89,9 +91,9 @@ provider "ovirt" { } resource "ovirt_vm" "foo" { - cluster_id = "%s" + cluster_id = "%s" template_id = "%s" - name = "test" + name = "test" } `, clusterID, @@ -186,14 +188,81 @@ resource "ovirt_vm" "foo" { ) } +func TestVMResourcePlacementPolicy(t *testing.T) { + t.Parallel() + + p := newProvider(newTestLogger(t)) + clusterID := p.getTestHelper().GetClusterID() + templateID := p.getTestHelper().GetBlankTemplateID() + + client := p.getTestHelper().GetClient().WithContext(context.Background()) + hosts, err := client.ListHosts() + if err != nil { + t.Fatalf("Failed to list hosts (%v)", err) + } + hostIDs := make([]string, len(hosts)) + for i, host := range hosts { + hostIDs[i] = fmt.Sprintf("\"%s\"", host.ID()) + } + config := fmt.Sprintf( + ` +provider "ovirt" { + mock = true +} + +resource "ovirt_vm" "foo" { + cluster_id = "%s" + template_id = "%s" + name = "test" + placement_policy_affinity = "migratable" + placement_policy_host_ids = [%s] +} +`, + clusterID, + templateID, + strings.Join(hostIDs, ","), + ) + + resource.UnitTest( + t, resource.TestCase{ + ProviderFactories: p.getProviderFactories(), + Steps: []resource.TestStep{ + { + Config: config, + Check: func(state *terraform.State) error { + vmData := state.RootModule().Resources["ovirt_vm.foo"] + vm, err := client.GetVM(ovirtclient.VMID(vmData.Primary.ID)) + if err != nil { + return fmt.Errorf("failed to fetch VM (%w)", err) + } + placementPolicy, ok := vm.PlacementPolicy() + if !ok { + return fmt.Errorf("no placement policy on the VM") + } + if *placementPolicy.Affinity() != ovirtclient.VMAffinityMigratable { + return fmt.Errorf("incorrect VM affinity: %s", *placementPolicy.Affinity()) + } + return nil + }, + }, + { + Config: config, + Destroy: true, + }, + }, + }, + ) +} + type testVM struct { - id ovirtclient.VMID - name string - comment string - clusterID ovirtclient.ClusterID - templateID ovirtclient.TemplateID - status ovirtclient.VMStatus - os ovirtclient.VMOS + id ovirtclient.VMID + name string + comment string + clusterID ovirtclient.ClusterID + templateID ovirtclient.TemplateID + status ovirtclient.VMStatus + os ovirtclient.VMOS + placementPolicy ovirtclient.VMPlacementPolicy } func (t *testVM) InstanceTypeID() *ovirtclient.InstanceTypeID { @@ -233,7 +302,20 @@ func (t *testVM) HostID() *ovirtclient.HostID { } func (t *testVM) PlacementPolicy() (placementPolicy ovirtclient.VMPlacementPolicy, ok bool) { - panic("not implemented for test input") + return t.placementPolicy, t.placementPolicy != nil +} + +type testPlacementPolicy struct { + affinity *ovirtclient.VMAffinity + hostIDs []ovirtclient.HostID +} + +func (t testPlacementPolicy) Affinity() *ovirtclient.VMAffinity { + return t.affinity +} + +func (t testPlacementPolicy) HostIDs() []ovirtclient.HostID { + return t.hostIDs } type testCPU struct { @@ -305,6 +387,7 @@ func (t testOS) Type() string { func TestVMResourceUpdate(t *testing.T) { t.Parallel() + vmAffinity := ovirtclient.VMAffinityMigratable vm := &testVM{ id: "asdf", name: "test VM", @@ -315,6 +398,10 @@ func TestVMResourceUpdate(t *testing.T) { os: &testOS{ t: "linux", }, + placementPolicy: &testPlacementPolicy{ + &vmAffinity, + []ovirtclient.HostID{"asdf"}, + }, } resourceData := schema.TestResourceDataRaw(t, vmSchema, map[string]interface{}{}) diags := vmResourceUpdate(vm, resourceData) @@ -327,6 +414,8 @@ func TestVMResourceUpdate(t *testing.T) { compareResource(t, resourceData, "template_id", string(vm.templateID)) compareResource(t, resourceData, "status", string(vm.status)) compareResource(t, resourceData, "os_type", vm.os.Type()) + compareResource(t, resourceData, "placement_policy_affinity", string(*vm.placementPolicy.Affinity())) + compareResourceStringList(t, resourceData, "placement_policy_host_ids", []string{"asdf"}) } func compareResource(t *testing.T, data *schema.ResourceData, field string, value string) { @@ -334,3 +423,22 @@ func compareResource(t *testing.T, data *schema.ResourceData, field string, valu t.Fatalf("invalid resource %s: %s, expected: %s", field, resourceValue, value) } } + +func compareResourceStringList(t *testing.T, data *schema.ResourceData, field string, expectedValues []string) { + resourceValue := data.Get(field).(*schema.Set) + realValues := resourceValue.List() + if len(realValues) != len(expectedValues) { + t.Fatalf("Incorrect number of values found (expected: %d, found: %d)", len(expectedValues), len(realValues)) + } + for _, value := range realValues { + found := false + for _, expectedValue := range expectedValues { + if expectedValue == value { + found = true + } + } + if !found { + t.Fatalf("Invalid value found: %s", value) + } + } +}