Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #366: Added placement policy options to ovirt_vm #373

Merged
1 commit merged into from May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/resources/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
59 changes: 59 additions & 0 deletions ovirt/resource_ovirt_vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}

Expand Down
128 changes: 118 additions & 10 deletions ovirt/resource_ovirt_vm_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package ovirt

import (
"context"
"fmt"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -89,9 +91,9 @@ provider "ovirt" {
}

resource "ovirt_vm" "foo" {
cluster_id = "%s"
cluster_id = "%s"
template_id = "%s"
name = "test"
name = "test"
}
`,
clusterID,
Expand Down Expand Up @@ -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 {
This conversation was marked as resolved.
Show resolved Hide resolved
hostIDs[i] = fmt.Sprintf("\"%s\"", host.ID())
}
config := fmt.Sprintf(
`
provider "ovirt" {
mock = true
}

resource "ovirt_vm" "foo" {
cluster_id = "%s"
This conversation was marked as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -327,10 +414,31 @@ 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) {
if resourceValue := data.Get(field); resourceValue != value {
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)
}
}
}